diff --git a/Directory.Packages.props b/Directory.Packages.props
index a221a7f..d5f6bd8 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -20,7 +20,6 @@
Package versions made consistent across all packages referenced in this repository
-->
-
@@ -28,11 +27,9 @@
-
-
@@ -41,13 +38,13 @@
-
+
-
+
-
+
@@ -66,8 +63,10 @@
-
-
-
+
+
+
+
+
-
+
\ No newline at end of file
diff --git a/Invoke-Tests.ps1 b/Invoke-Tests.ps1
index 0890e9b..06f1c23 100644
--- a/Invoke-Tests.ps1
+++ b/Invoke-Tests.ps1
@@ -39,7 +39,7 @@ try
try
{
dir (Join-Path $BuildInfo['BuildOutputPath'] 'bin' 'Ubiquity.NET.CommandLine.SrcGen.UT' "$Configuration" 'net10.0' 'Ubiquity.NET.CommandLine.*')
- Invoke-External dotnet test Ubiquity.NET.Utils.slnx '-c' $Configuration '-tl:off' '--logger:trx' '--no-build' '-s' '.\x64.runsettings'
+ Invoke-External dotnet test Ubiquity.NET.Utils.slnx '-c' $Configuration '-tl:off' '--logger:trx' '--no-build' '-s' '.\.runsettings'
}
finally
{
diff --git a/src/x64.runsettings b/src/.runsettings
similarity index 100%
rename from src/x64.runsettings
rename to src/.runsettings
diff --git a/src/Ubiquity.NET.CodeAnalysis.Utils/AttributeDataExtensions.cs b/src/Ubiquity.NET.CodeAnalysis.Utils/AttributeDataExtensions.cs
index ab3681e..010dada 100644
--- a/src/Ubiquity.NET.CodeAnalysis.Utils/AttributeDataExtensions.cs
+++ b/src/Ubiquity.NET.CodeAnalysis.Utils/AttributeDataExtensions.cs
@@ -27,5 +27,13 @@ public static NamespaceQualifiedName GetNamespaceQualifiedName( this AttributeDa
? new( [], string.Empty )
: self.AttributeClass.GetNamespaceQualifiedName();
}
+
+ /// Gets the location from the if available
+ /// to get the location from
+ /// Location of the attribute or null if not available
+ public static Location? GetLocation( this AttributeData self )
+ {
+ return self.ApplicationSyntaxReference?.SyntaxTree.GetLocation( self.ApplicationSyntaxReference.Span );
+ }
}
}
diff --git a/src/Ubiquity.NET.CodeAnalysis.Utils/CompilationExtensions.cs b/src/Ubiquity.NET.CodeAnalysis.Utils/CompilationExtensions.cs
index 70f7fdd..86b2e29 100644
--- a/src/Ubiquity.NET.CodeAnalysis.Utils/CompilationExtensions.cs
+++ b/src/Ubiquity.NET.CodeAnalysis.Utils/CompilationExtensions.cs
@@ -4,7 +4,9 @@
// Mostly from: https://github.com/Sergio0694/PolySharp/blob/main/src/PolySharp.SourceGenerators/Extensions/CompilationExtensions.cs
// Reformatted and adapted to support repo guidelines
+#if SUPPORT_VB
using Microsoft.CodeAnalysis.VisualBasic;
+#endif
namespace Ubiquity.NET.CodeAnalysis.Utils
{
@@ -28,6 +30,12 @@ public static bool HasLanguageVersionAtLeastEqualTo( this Compilation compilatio
: csharpCompilation.LanguageVersion >= languageVersion;
}
+// Support of VB is problematic as [RS1038](https://github.com/dotnet/roslyn/blob/main/docs/roslyn-analyzers/rules/RS1038.md)
+// is aggressive and tests for ALL dependencies. So inclusion of a reference in a dependent assembly will trigger that
+// To fully resolve this in a general means the language specific parts would need to pull out of this assembly and
+// into a distinct one for that language. This is a bit overkill given the need to target any language other than C#
+// is rather limited... Until such is needed, just leave out the VB support
+#if SUPPORT_VB
/// Checks whether a given VB compilation is using at least a given language version.
/// The to consider for analysis.
/// The minimum language version to check.
@@ -39,6 +47,7 @@ public static bool HasLanguageVersionAtLeastEqualTo( this Compilation compilatio
? throw new ArgumentNullException( nameof( compilation ) )
: vbCompilation.LanguageVersion >= languageVersion;
}
+#endif
/// Gets the runtime version by extracting the version from the assembly implementing
/// Compilation to get the version information from
diff --git a/src/Ubiquity.NET.CodeAnalysis.Utils/Ubiquity.NET.CodeAnalysis.Utils.csproj b/src/Ubiquity.NET.CodeAnalysis.Utils/Ubiquity.NET.CodeAnalysis.Utils.csproj
index d025fac..2a6bffe 100644
--- a/src/Ubiquity.NET.CodeAnalysis.Utils/Ubiquity.NET.CodeAnalysis.Utils.csproj
+++ b/src/Ubiquity.NET.CodeAnalysis.Utils/Ubiquity.NET.CodeAnalysis.Utils.csproj
@@ -34,10 +34,16 @@
-
-
+
-
+
+
allruntime; build; native; contentfiles; analyzers; buildtransitive
diff --git a/src/Ubiquity.NET.CommandLine.Pkg/Ubiquity.NET.CommandLine.Pkg.csproj b/src/Ubiquity.NET.CommandLine.Pkg/Ubiquity.NET.CommandLine.Pkg.csproj
index 2bc29f1..faaa7b4 100644
--- a/src/Ubiquity.NET.CommandLine.Pkg/Ubiquity.NET.CommandLine.Pkg.csproj
+++ b/src/Ubiquity.NET.CommandLine.Pkg/Ubiquity.NET.CommandLine.Pkg.csproj
@@ -7,7 +7,7 @@ To support a meta package where the referenced packages may not exist at build t
The Build process for CSPROJ files will require resolving the referenced packages before it "generates" the NuSpec file.
The CSPROJ system for MSBUILD will try to restore referenced packages etc... and ultimately requires the ability to find
the listed dependencies. (They won't exist yet for this build/repo!) so either a mechanism to control build ordering EVEN
-on NuGetRestore is needed, or this approach is used. Given the complexities of trying the former, this approach is used
+on NuGet Restore is needed, or this approach is used. Given the complexities of trying the former, this approach is used
as it is simpler.
-->
diff --git a/src/Ubiquity.NET.CommandLine.SrcGen.UT/CommandAnalyzerTests.cs b/src/Ubiquity.NET.CommandLine.SrcGen.UT/CommandAnalyzerTests.cs
new file mode 100644
index 0000000..244a9e2
--- /dev/null
+++ b/src/Ubiquity.NET.CommandLine.SrcGen.UT/CommandAnalyzerTests.cs
@@ -0,0 +1,121 @@
+// Copyright (c) Ubiquity.NET Contributors. All rights reserved.
+// Licensed under the Apache-2.0 WITH LLVM-exception license. See the LICENSE.md file in the project root for full license information.
+
+namespace Ubiquity.NET.CommandLine.SrcGen.UT
+{
+ [TestClass]
+ public class CommandAnalyzerTests
+ {
+ public TestContext TestContext { get; set; }
+
+ [TestMethod]
+ [DataRow( TestRuntime.Net8_0 )]
+ [DataRow( TestRuntime.Net10_0 )]
+ public async Task Empty_source_analyzes_clean( TestRuntime testRuntime )
+ {
+ var analyzerTest = CreateTestRunner( string.Empty, testRuntime );
+ await analyzerTest.RunAsync( TestContext.CancellationToken );
+ }
+
+ [TestMethod]
+ [DataRow( TestRuntime.Net8_0 )]
+ [DataRow( TestRuntime.Net10_0 )]
+ public async Task Option_attribute_without_command_triggers_diagnostic( TestRuntime testRuntime )
+ {
+ SourceText txt = GetSourceText( nameof(Option_attribute_without_command_triggers_diagnostic), "input.cs" );
+ var analyzerTest = CreateTestRunner( txt, testRuntime );
+
+ // (9,6): warning UNC001: Property attribute OptionAttribute is only allowed on a property in a type attributed with a command attribute. This use will be ignored by the generator.
+ analyzerTest.ExpectedDiagnostics.AddRange(
+ [
+ new DiagnosticResult("UNC001", DiagnosticSeverity.Warning).WithLocation(9, 6),
+ ]
+ );
+
+ await analyzerTest.RunAsync( TestContext.CancellationToken );
+ }
+
+ [TestMethod]
+ [DataRow( TestRuntime.Net8_0 )]
+ [DataRow( TestRuntime.Net10_0 )]
+ public async Task FileValidation_attribute_without_command_triggers_diagnostic( TestRuntime testRuntime )
+ {
+ SourceText txt = GetSourceText( nameof(FileValidation_attribute_without_command_triggers_diagnostic), "input.cs" );
+ var analyzerTest = CreateTestRunner( txt, testRuntime );
+
+ // (10,6): warning UNC001: Property attribute FileValidationAttribute is only allowed on a property in a type attributed with a command attribute. This use will be ignored by the generator.
+ // (10,6): error UNC002: Property attribute FileValidationAttribute is not allowed on a property independent of a qualifying attribute such as OptionAttribute.
+ analyzerTest.ExpectedDiagnostics.AddRange(
+ [
+ new DiagnosticResult("UNC001", DiagnosticSeverity.Warning).WithLocation(10, 6),
+ new DiagnosticResult("UNC002", DiagnosticSeverity.Error).WithLocation(10, 6),
+ ]
+ );
+
+ await analyzerTest.RunAsync( TestContext.CancellationToken );
+ }
+
+ [TestMethod]
+ [DataRow( TestRuntime.Net8_0 )]
+ [DataRow( TestRuntime.Net10_0 )]
+ public async Task FolderValidation_attribute_without_command_triggers_diagnostic( TestRuntime testRuntime )
+ {
+ SourceText txt = GetSourceText( nameof(FolderValidation_attribute_without_command_triggers_diagnostic), "input.cs" );
+ var analyzerTest = CreateTestRunner( txt, testRuntime );
+
+ // (10,6): warning UNC001: Property attribute FolderValidationAttribute is only allowed on a property in a type attributed with a command attribute. This use will be ignored by the generator.
+ // (10,6): error UNC002: Property attribute FolderValidationAttribute is not allowed on a property independent of a qualifying attribute such as OptionAttribute.
+ analyzerTest.ExpectedDiagnostics.AddRange(
+ [
+ new DiagnosticResult("UNC001", DiagnosticSeverity.Warning).WithLocation(10, 6),
+ new DiagnosticResult("UNC002", DiagnosticSeverity.Error).WithLocation(10, 6),
+ ]
+ );
+
+ await analyzerTest.RunAsync( TestContext.CancellationToken );
+ }
+
+ [TestMethod]
+ [DataRow( TestRuntime.Net8_0 )]
+ [DataRow( TestRuntime.Net10_0 )]
+ public async Task GoldenPath_produces_no_diagnostics( TestRuntime testRuntime )
+ {
+ SourceText txt = GetSourceText( nameof(GoldenPath_produces_no_diagnostics), "input.cs" );
+
+ var analyzerTest = CreateTestRunner( txt, testRuntime );
+ await analyzerTest.RunAsync( TestContext.CancellationToken );
+ }
+
+ private AnalyzerTest CreateTestRunner( string source, TestRuntime testRuntime )
+ {
+ return CreateTestRunner( SourceText.From( source ), testRuntime );
+ }
+
+ private AnalyzerTest CreateTestRunner( SourceText source, TestRuntime testRuntime )
+ {
+ // Use a test analyzer with language and reference assemblies that match the runtime for the test run
+ return new SourceAnalyzerTest( testRuntime.DefaultLangVersion )
+ {
+ TestState =
+ {
+ Sources = { source },
+ ReferenceAssemblies = testRuntime.ReferenceAssemblies,
+ AdditionalReferences =
+ {
+ TestContext.GetUbiquityNetCommandLineLib( testRuntime )
+ },
+ OutputKind = OutputKind.DynamicallyLinkedLibrary, // Don't require a Main() method
+ },
+
+ // Allow ALL diagnostics for testing, input source should contain valid C# code
+ // but might otherwise trigger the tested analyzer.
+ CompilerDiagnostics = CompilerDiagnostics.All,
+ };
+ }
+
+ private static SourceText GetSourceText( params string[] nameParts )
+ {
+ return TestHelpers.GetTestText( nameof( CommandAnalyzerTests ), nameParts );
+ }
+ }
+}
diff --git a/src/Ubiquity.NET.CommandLine.SrcGen.UT/GlobalNamespaceImports.cs b/src/Ubiquity.NET.CommandLine.SrcGen.UT/GlobalNamespaceImports.cs
index ac6dd60..9216546 100644
--- a/src/Ubiquity.NET.CommandLine.SrcGen.UT/GlobalNamespaceImports.cs
+++ b/src/Ubiquity.NET.CommandLine.SrcGen.UT/GlobalNamespaceImports.cs
@@ -6,14 +6,15 @@
global using System.Collections.Immutable;
global using System.Diagnostics.CodeAnalysis;
global using System.IO;
+global using System.Linq;
global using System.Runtime.InteropServices;
-global using System.Text;
-
-global using Basic.Reference.Assemblies;
+global using System.Threading.Tasks;
global using Microsoft.CodeAnalysis;
global using Microsoft.CodeAnalysis.CSharp;
+global using Microsoft.CodeAnalysis.Testing;
global using Microsoft.CodeAnalysis.Text;
global using Microsoft.VisualStudio.TestTools.UnitTesting;
global using Ubiquity.NET.SourceGenerator.Test.Utils;
+global using Ubiquity.NET.SourceGenerator.Test.Utils.CSharp;
diff --git a/src/Ubiquity.NET.CommandLine.SrcGen.UT/GlobalSuppression.cs b/src/Ubiquity.NET.CommandLine.SrcGen.UT/GlobalSuppression.cs
index a79c00f..bc164b2 100644
--- a/src/Ubiquity.NET.CommandLine.SrcGen.UT/GlobalSuppression.cs
+++ b/src/Ubiquity.NET.CommandLine.SrcGen.UT/GlobalSuppression.cs
@@ -6,7 +6,5 @@
// Project-level suppressions either have no target or are given
// a specific target and scoped to a namespace, type, member, etc.
-using System.Diagnostics.CodeAnalysis;
-
[assembly: SuppressMessage( "StyleCop.CSharp.DocumentationRules", "SA1600:Elements should be documented", Justification = "Unit Tests" )]
[assembly: SuppressMessage( "StyleCop.CSharp.DocumentationRules", "SA1652:Enable XML documentation output", Justification = "Unit Tests" )]
diff --git a/src/Ubiquity.NET.CommandLine.SrcGen.UT/MSTestVerifier.cs b/src/Ubiquity.NET.CommandLine.SrcGen.UT/MSTestVerifier.cs
new file mode 100644
index 0000000..15462b3
--- /dev/null
+++ b/src/Ubiquity.NET.CommandLine.SrcGen.UT/MSTestVerifier.cs
@@ -0,0 +1,113 @@
+// Copyright (c) Ubiquity.NET Contributors. All rights reserved.
+// Licensed under the Apache-2.0 WITH LLVM-exception license. See the LICENSE.md file in the project root for full license information.
+
+namespace Ubiquity.NET.CommandLine.SrcGen.UT
+{
+ /// Provides an implementation of for Ms Test .
+ ///
+ /// This verifier is dependent on the MsTest framework. Each verification method uses
+ /// in an adapter pattern.
+ ///
+ public class MsTestVerifier
+ : IVerifier
+ {
+ /// Initializes a new instance of the class.
+ public MsTestVerifier( )
+ : this( [] )
+ {
+ }
+
+ ///
+ public virtual void Empty( string collectionName, IEnumerable collection )
+ {
+ Assert.IsEmpty( collection, CreateMessage( $"collection '{collectionName}' is not empty" ) );
+ }
+
+ ///
+ public virtual void NotEmpty( string collectionName, IEnumerable collection )
+ {
+ Assert.IsNotEmpty( collection, CreateMessage( $"collection '{collectionName}' is empty" ) );
+ }
+
+ ///
+ public virtual void LanguageIsSupported( string language )
+ {
+ Assert.IsTrue( language == LanguageNames.CSharp || language == LanguageNames.VisualBasic, CreateMessage( $"Unsupported Language: '{language}'" ) );
+ }
+
+ ///
+ public virtual void Equal( T expected, T actual, string? message = null )
+ {
+ Assert.AreEqual( expected, actual, CreateMessage( message ) );
+ }
+
+ ///
+ public virtual void True( [DoesNotReturnIf( false )] bool assert, string? message = null )
+ {
+ Assert.IsTrue( assert, CreateMessage( message ) );
+ }
+
+ ///
+ public virtual void False( [DoesNotReturnIf( true )] bool assert, string? message = null )
+ {
+ Assert.IsFalse( assert, CreateMessage( message ) );
+ }
+
+ ///
+ [DoesNotReturn]
+ public virtual void Fail( string? message = null )
+ {
+ Assert.Fail( CreateMessage( message ) );
+ }
+
+ ///
+ public virtual void SequenceEqual(
+ IEnumerable expected,
+ IEnumerable actual,
+ IEqualityComparer? equalityComparer = null,
+ string? message = null
+ )
+ {
+ var comparer = new SequenceComparer(equalityComparer);
+ bool areEqual = comparer.Equals(expected, actual);
+ if(!areEqual)
+ {
+ throw new InvalidOperationException( CreateMessage( message ?? $"Sequences are not equal" ) );
+ }
+ }
+
+ ///
+ public virtual IVerifier PushContext( string context )
+ {
+ return new MsTestVerifier( Context.Push( context ) );
+ }
+
+ /// Initializes a new instance of the class with the specified context.
+ /// The verification context, with the innermost verification context label at the top of the stack.
+ /// If is .
+ private MsTestVerifier( ImmutableStack context )
+ {
+ Context = context ?? throw new ArgumentNullException( nameof( context ) );
+ }
+
+ /// Gets the current verification context. The innermost verification context label is the top item on the stack.
+ private ImmutableStack Context { get; }
+
+ ///
+ /// Creates a full message for a verifier failure combining the current verification with
+ /// the for the current verification.
+ ///
+ /// The failure message to report.
+ /// A full failure message containing both the verification context and the failure message for the current test.
+ private string CreateMessage( string? message )
+ {
+ message ??= string.Empty;
+ foreach(string frame in Context)
+ {
+ message = "Context: " + frame + Environment.NewLine + message;
+ }
+
+ return message;
+ }
+ }
+}
diff --git a/src/Ubiquity.NET.CommandLine.SrcGen.UT/RootCommandAttributeTests.cs b/src/Ubiquity.NET.CommandLine.SrcGen.UT/RootCommandAttributeTests.cs
index 3373111..1b8aa65 100644
--- a/src/Ubiquity.NET.CommandLine.SrcGen.UT/RootCommandAttributeTests.cs
+++ b/src/Ubiquity.NET.CommandLine.SrcGen.UT/RootCommandAttributeTests.cs
@@ -9,77 +9,53 @@ public sealed class RootCommandAttributeTests
public TestContext TestContext { get; set; }
[TestMethod]
- public void Basic_golden_path_succeeds( )
+ [DataRow( TestRuntime.Net8_0 )]
+ [DataRow( TestRuntime.Net10_0 )]
+ public async Task Basic_golden_path_succeeds( TestRuntime testRuntime )
{
- var sourceGenerator = new CommandGenerator().AsSourceGenerator();
- GeneratorDriver driver = CSharpGeneratorDriver.Create(
- generators: [sourceGenerator],
- driverOptions: new GeneratorDriverOptions(default, trackIncrementalGeneratorSteps: true)
- );
+ const string inputFileName = "input.cs";
+ const string expectedFileName = "expected.cs";
+ string hintPath = Path.Combine("Ubiquity.NET.CommandLine.SrcGen", "Ubiquity.NET.CommandLine.SrcGen.CommandGenerator", "TestNamespace.TestOptions.g.cs");
- SourceText input = TestHelpers.GetTestText(nameof(RootCommandAttributeTests), "input.cs");
- SourceText expected = TestHelpers.GetTestText(nameof(RootCommandAttributeTests), "expected.cs");
+ SourceText input = GetSourceText( nameof(Basic_golden_path_succeeds), inputFileName );
+ SourceText expected = GetSourceText( nameof(Basic_golden_path_succeeds), expectedFileName );
- CSharpCompilation compilation = CreateCompilation(input, TestProgramCSPath);
- var diagnostics = compilation.GetDiagnostics( TestContext.CancellationToken );
- foreach(var diagnostic in diagnostics)
- {
- TestContext.WriteLine( diagnostic.ToString() );
- }
-
- Assert.HasCount( 0, diagnostics );
-
- var results = driver.RunGeneratorAndAssertResults(compilation, [TrackingNames.CommandClass]);
- Assert.IsEmpty( results.Diagnostics, "Should not have ANY diagnostics reported during generation" );
-
- // validate the generated trees have the correct count and names
- Assert.HasCount( 1, results.GeneratedTrees, "Should create 1 'files' during generation" );
- for(int i = 0; i < results.GeneratedTrees.Length; ++i)
- {
- string expectedName = GeneratedFilePaths[i];
- SyntaxTree tree = results.GeneratedTrees[i];
-
- Assert.AreEqual( expectedName, tree.FilePath, "Generated files should use correct name" );
- Assert.AreEqual( Encoding.UTF8, tree.Encoding, $"Generated files should use UTF8. [{expectedName}]" );
- }
-
- SourceText actual = results.GeneratedTrees[0].GetText( TestContext.CancellationToken );
- string uniDiff = expected.UniDiff(actual);
- if(!string.IsNullOrWhiteSpace( uniDiff ))
- {
- TestContext.WriteLine( uniDiff );
- Assert.Fail( "No Differences Expected" );
- }
+ var runner = CreateTestRunner(input, testRuntime, [TrackingNames.CommandClass], hintPath, expected );
+ await runner.RunAsync( TestContext.CancellationToken );
}
- // simple helper for these tests to create a C# Compilation
- internal static CSharpCompilation CreateCompilation(
+ private SourceGeneratorTest CreateTestRunner(
SourceText source,
- string path,
- CSharpParseOptions? parseOptions = default,
- CSharpCompilationOptions? compileOptions = default,
- List? references = default
+ TestRuntime testRuntime,
+ ImmutableArray trackingNames,
+ string expectedHintPath,
+ SourceText expectedContent
)
{
- parseOptions ??= new CSharpParseOptions(LanguageVersion.CSharp14, DocumentationMode.None);
- compileOptions ??= new CSharpCompilationOptions( OutputKind.DynamicallyLinkedLibrary, nullableContextOptions: NullableContextOptions.Enable );
-
- // Default to .NET 10 if not specified.
- references ??= [ .. Net100.References.All ];
- references.Add( MetadataReference.CreateFromFile( Path.Combine( Environment.CurrentDirectory, "Ubiquity.NET.CommandLine.dll" ) ) );
-
- return CSharpCompilation.Create( "TestAssembly",
- [ CSharpSyntaxTree.ParseText( source, parseOptions, path ) ],
- references,
- compileOptions
- );
+ // Use a test runner with Caching, language and reference assemblies that match the runtime for the test run
+ return new CachedSourceGeneratorTest( trackingNames, testRuntime.DefaultLangVersion )
+ {
+ TestState =
+ {
+ Sources = { source },
+ ReferenceAssemblies = testRuntime.ReferenceAssemblies,
+ AdditionalReferences =
+ {
+ TestContext.GetUbiquityNetCommandLineLib( testRuntime )
+ },
+ OutputKind = OutputKind.DynamicallyLinkedLibrary, // Don't require a Main() method
+ GeneratedSources = { (expectedHintPath, expectedContent) }
+ },
+
+ // Allow ALL diagnostics for testing, input source should contain valid C# code
+ // but might otherwise trigger the tested analyzer.
+ CompilerDiagnostics = CompilerDiagnostics.All,
+ };
}
- private const string TestProgramCSPath = @"input.cs";
-
- private readonly ImmutableArray GeneratedFilePaths
- = [
- @"Ubiquity.NET.CommandLine.SrcGen\Ubiquity.NET.CommandLine.SrcGen.CommandGenerator\TestNamespace.TestOptions.g.cs",
- ];
+ private static SourceText GetSourceText(params string[] nameParts)
+ {
+ return TestHelpers.GetTestText( nameof( RootCommandAttributeTests ), nameParts );
+ }
}
}
diff --git a/src/Ubiquity.NET.CommandLine.SrcGen.UT/TestFiles/CommandAnalyzerTests/FileValidation_attribute_without_command_triggers_diagnostic/input.cs b/src/Ubiquity.NET.CommandLine.SrcGen.UT/TestFiles/CommandAnalyzerTests/FileValidation_attribute_without_command_triggers_diagnostic/input.cs
new file mode 100644
index 0000000..51333e9
--- /dev/null
+++ b/src/Ubiquity.NET.CommandLine.SrcGen.UT/TestFiles/CommandAnalyzerTests/FileValidation_attribute_without_command_triggers_diagnostic/input.cs
@@ -0,0 +1,12 @@
+using System.IO;
+
+using Ubiquity.NET.CommandLine.GeneratorAttributes;
+
+namespace TestNamespace;
+
+internal class testInput1
+{
+ // This attribute alone should trigger UNC002 - Property attribute FileValidation is not allowed on a property independent of a qualifying attribute such as OptionAttribute.
+ [FileValidation( FileValidation.ExistingOnly )]
+ public required FileInfo SomePath { get; init; }
+}
diff --git a/src/Ubiquity.NET.CommandLine.SrcGen.UT/TestFiles/CommandAnalyzerTests/FolderValidation_attribute_without_command_triggers_diagnostic/input.cs b/src/Ubiquity.NET.CommandLine.SrcGen.UT/TestFiles/CommandAnalyzerTests/FolderValidation_attribute_without_command_triggers_diagnostic/input.cs
new file mode 100644
index 0000000..51333e9
--- /dev/null
+++ b/src/Ubiquity.NET.CommandLine.SrcGen.UT/TestFiles/CommandAnalyzerTests/FolderValidation_attribute_without_command_triggers_diagnostic/input.cs
@@ -0,0 +1,12 @@
+using System.IO;
+
+using Ubiquity.NET.CommandLine.GeneratorAttributes;
+
+namespace TestNamespace;
+
+internal class testInput1
+{
+ // This attribute alone should trigger UNC002 - Property attribute FileValidation is not allowed on a property independent of a qualifying attribute such as OptionAttribute.
+ [FileValidation( FileValidation.ExistingOnly )]
+ public required FileInfo SomePath { get; init; }
+}
diff --git a/src/Ubiquity.NET.CommandLine.SrcGen.UT/TestFiles/RootCommandAttributeTests/input.cs b/src/Ubiquity.NET.CommandLine.SrcGen.UT/TestFiles/CommandAnalyzerTests/GoldenPath_produces_no_diagnostics/input.cs
similarity index 100%
rename from src/Ubiquity.NET.CommandLine.SrcGen.UT/TestFiles/RootCommandAttributeTests/input.cs
rename to src/Ubiquity.NET.CommandLine.SrcGen.UT/TestFiles/CommandAnalyzerTests/GoldenPath_produces_no_diagnostics/input.cs
diff --git a/src/Ubiquity.NET.CommandLine.SrcGen.UT/TestFiles/CommandAnalyzerTests/Option_attribute_without_command_triggers_diagnostic/input.cs b/src/Ubiquity.NET.CommandLine.SrcGen.UT/TestFiles/CommandAnalyzerTests/Option_attribute_without_command_triggers_diagnostic/input.cs
new file mode 100644
index 0000000..4138f5d
--- /dev/null
+++ b/src/Ubiquity.NET.CommandLine.SrcGen.UT/TestFiles/CommandAnalyzerTests/Option_attribute_without_command_triggers_diagnostic/input.cs
@@ -0,0 +1,11 @@
+using System.IO;
+
+using Ubiquity.NET.CommandLine.GeneratorAttributes;
+
+namespace TestNamespace;
+
+internal class testInput1
+{
+ [Option( "-o", Description = "Use of this attribute should generate diagnostic UNC001" )]
+ public required DirectoryInfo SomePath { get; init; }
+}
diff --git a/src/Ubiquity.NET.CommandLine.SrcGen.UT/TestFiles/RootCommandAttributeTests/expected.cs b/src/Ubiquity.NET.CommandLine.SrcGen.UT/TestFiles/RootCommandAttributeTests/Basic_golden_path_succeeds/expected.cs
similarity index 100%
rename from src/Ubiquity.NET.CommandLine.SrcGen.UT/TestFiles/RootCommandAttributeTests/expected.cs
rename to src/Ubiquity.NET.CommandLine.SrcGen.UT/TestFiles/RootCommandAttributeTests/Basic_golden_path_succeeds/expected.cs
diff --git a/src/Ubiquity.NET.CommandLine.SrcGen.UT/TestFiles/RootCommandAttributeTests/Basic_golden_path_succeeds/input.cs b/src/Ubiquity.NET.CommandLine.SrcGen.UT/TestFiles/RootCommandAttributeTests/Basic_golden_path_succeeds/input.cs
new file mode 100644
index 0000000..0f3bfe8
--- /dev/null
+++ b/src/Ubiquity.NET.CommandLine.SrcGen.UT/TestFiles/RootCommandAttributeTests/Basic_golden_path_succeeds/input.cs
@@ -0,0 +1,26 @@
+using System.IO;
+using Ubiquity.NET.CommandLine.GeneratorAttributes;
+
+namespace TestNamespace;
+
+[RootCommand( Description = "Root command for tests" )]
+internal partial class TestOptions
+{
+ [Option( "-o", Description = "Test SomePath" )]
+ [FolderValidation( FolderValidation.CreateIfNotExist )]
+ public required DirectoryInfo SomePath { get; init; }
+
+ [Option( "-b", Description = "Test Some existing Path" )]
+ [FolderValidation( FolderValidation.ExistingOnly )]
+ public required DirectoryInfo SomeExistingPath { get; init; }
+
+ [Option( "--thing1", Aliases = [ "-t" ], Required = true, Description = "Test Thing1", HelpName = "Help name for thing1" )]
+ public bool Thing1 { get; init; }
+
+ // This should be ignored by generator
+ public string? NotAnOption { get; set; }
+
+ [Option( "-a", Hidden = true, Required = false, ArityMin = 0, ArityMax = 3, Description = "Test SomeOtherPath" )]
+ [FileValidation( FileValidation.ExistingOnly )]
+ public required FileInfo SomeOtherPath { get; init; }
+}
diff --git a/src/Ubiquity.NET.CommandLine.SrcGen.UT/TestHelpers.cs b/src/Ubiquity.NET.CommandLine.SrcGen.UT/TestHelpers.cs
index e266a2e..9445a86 100644
--- a/src/Ubiquity.NET.CommandLine.SrcGen.UT/TestHelpers.cs
+++ b/src/Ubiquity.NET.CommandLine.SrcGen.UT/TestHelpers.cs
@@ -1,6 +1,10 @@
// Copyright (c) Ubiquity.NET Contributors. All rights reserved.
// Licensed under the Apache-2.0 WITH LLVM-exception license. See the LICENSE.md file in the project root for full license information.
+using System.ComponentModel;
+
+using ReferenceAssemblies = Microsoft.CodeAnalysis.Testing.ReferenceAssemblies;
+
namespace Ubiquity.NET.CommandLine.SrcGen.UT
{
internal static class TestHelpers
@@ -17,12 +21,68 @@ public static Stream GetTestResourceStream( string testName, string name )
?? throw new InvalidOperationException( $"Resource '{resourceName}' not found" );
}
- public static SourceText GetTestText( string testName, string name )
+ public static SourceText GetTestText( string testName, params string[] nameParts )
{
- using var strm = GetTestResourceStream( testName, name );
+ if(nameParts.Length == 0)
+ {
+ throw new ArgumentException( "Must include at least one name part", nameof( nameParts ) );
+ }
+
+ using var strm = GetTestResourceStream( testName, string.Join(".", nameParts) );
return SourceText.From( strm );
}
+ extension( TestContext self )
+ {
+ internal string BuildOutputBinPath
+ {
+ get
+ {
+ string runDir = self.TestRunDirectory ?? throw new InvalidOperationException("No test run directory");
+ return Path.GetFullPath( Path.Combine( runDir, "..", "..", "bin" ) );
+ }
+ }
+
+ public MetadataReference GetUbiquityNetCommandLineLib( TestRuntime testRuntime )
+ {
+ string runtimeName = testRuntime switch
+ {
+ TestRuntime.Net8_0 => "net8.0",
+ TestRuntime.Net10_0 => "net10.0",
+ _ => throw new InvalidEnumArgumentException(nameof(testRuntime), (int)testRuntime, typeof(TestRuntime))
+ };
+
+ string pathName = Path.Combine( self.BuildOutputBinPath, "Ubiquity.NET.CommandLine", ConfigName, runtimeName, "Ubiquity.NET.CommandLine.dll" );
+ return MetadataReference.CreateFromFile( pathName );
+ }
+ }
+
+ #region .NET 10 Reference Assemblies
+
+ /// Gets the .NET 10 reference assemblies
+ ///
+ /// Sadly, Microsoft.CodeAnalysis.Testing.ReferenceAssemblies does not contain any reference for .NET 10
+ /// (even the latest version of that lib)
+ ///
+ ///
+ public static ReferenceAssemblies Net10 => LazyNet10Refs.Value;
+
+ private static readonly Lazy LazyNet10Refs = new(
+ static ()=> new(
+ targetFramework: "net10.0",
+ referenceAssemblyPackage: new PackageIdentity("Microsoft.NETCore.App.Ref", "10.0.0"),
+ referenceAssemblyPath: Path.Combine("ref", "net10.0")
+ )
+ );
+ #endregion
+
+ // There is no way to detect the configuration at runtime to include the correct
+ // reference to the DLL so use the compiler define to do the best available.
+#if DEBUG
+ private const string ConfigName = "Debug";
+#else
+ private const string ConfigName = "Release";
+#endif
private const string TestFilesFolderName = "TestFiles";
}
}
diff --git a/src/Ubiquity.NET.CommandLine.SrcGen.UT/TestRuntime.cs b/src/Ubiquity.NET.CommandLine.SrcGen.UT/TestRuntime.cs
new file mode 100644
index 0000000..2e3edc4
--- /dev/null
+++ b/src/Ubiquity.NET.CommandLine.SrcGen.UT/TestRuntime.cs
@@ -0,0 +1,41 @@
+// Copyright (c) Ubiquity.NET Contributors. All rights reserved.
+// Licensed under the Apache-2.0 WITH LLVM-exception license. See the LICENSE.md file in the project root for full license information.
+
+using System.ComponentModel;
+
+namespace Ubiquity.NET.CommandLine.SrcGen.UT
+{
+ /// Test enumeration for use as a data row to test the component for different runtimes
+ public enum TestRuntime
+ {
+ /// .NET 8.0 runtime
+ Net8_0,
+
+ /// .NET 10.0 runtime
+ Net10_0
+ }
+
+ [SuppressMessage( "StyleCop.CSharp.DocumentationRules", "SA1649:File name should match first type name", Justification = "Extensions of Enum" )]
+ internal static class TestRuntimeExtensions
+ {
+ extension(TestRuntime self)
+ {
+ // Analyzer tests use unresolved references
+ internal Microsoft.CodeAnalysis.Testing.ReferenceAssemblies ReferenceAssemblies
+ => self switch
+ {
+ TestRuntime.Net8_0 => ReferenceAssemblies.Net.Net80,
+ TestRuntime.Net10_0 => TestHelpers.Net10,
+ _ => throw new InvalidEnumArgumentException( nameof( self ), (int)self, typeof( TestRuntime ) ),
+ };
+
+ internal LanguageVersion DefaultLangVersion
+ => self switch
+ {
+ TestRuntime.Net8_0 => LanguageVersion.CSharp12,
+ TestRuntime.Net10_0 => LanguageVersion.CSharp14,
+ _ => throw new InvalidEnumArgumentException( nameof( self ), (int)self, typeof( TestRuntime ) ),
+ };
+ }
+ }
+}
diff --git a/src/Ubiquity.NET.CommandLine.SrcGen.UT/Ubiquity.NET.CommandLine.SrcGen.UT.csproj b/src/Ubiquity.NET.CommandLine.SrcGen.UT/Ubiquity.NET.CommandLine.SrcGen.UT.csproj
index 1cb51da..b9d40eb 100644
--- a/src/Ubiquity.NET.CommandLine.SrcGen.UT/Ubiquity.NET.CommandLine.SrcGen.UT.csproj
+++ b/src/Ubiquity.NET.CommandLine.SrcGen.UT/Ubiquity.NET.CommandLine.SrcGen.UT.csproj
@@ -7,15 +7,15 @@
-
-
-
-
-
-
+
+
+
+
+
+
@@ -29,20 +29,25 @@
-
-
+
+
+
+
+
+
-
-
+
+
+
+
+
+
-
+
diff --git a/src/Ubiquity.NET.CommandLine.SrcGen/AnalyzerReleases.Shipped.md b/src/Ubiquity.NET.CommandLine.SrcGen/AnalyzerReleases.Shipped.md
new file mode 100644
index 0000000..2080dcd
--- /dev/null
+++ b/src/Ubiquity.NET.CommandLine.SrcGen/AnalyzerReleases.Shipped.md
@@ -0,0 +1,3 @@
+; Shipped analyzer releases
+; https://github.com/dotnet/roslyn/blob/main/src/RoslynAnalyzers/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md
+
diff --git a/src/Ubiquity.NET.CommandLine.SrcGen/AnalyzerReleases.Unshipped.md b/src/Ubiquity.NET.CommandLine.SrcGen/AnalyzerReleases.Unshipped.md
new file mode 100644
index 0000000..43c19e2
--- /dev/null
+++ b/src/Ubiquity.NET.CommandLine.SrcGen/AnalyzerReleases.Unshipped.md
@@ -0,0 +1,11 @@
+; Unshipped analyzer release
+; https://github.com/dotnet/roslyn/blob/main/src/RoslynAnalyzers/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md
+
+### New Rules
+
+Rule ID | Category | Severity | Notes
+--------|----------|----------|-------
+UNC000 | Internal | Error | Diagnostics, [Documentation](NotConfigurable)
+UNC001 | Usage | Warning | Diagnostics, [Documentation](Unnecessary)
+UNC002 | Usage | Error | Diagnostics, [Documentation](NotConfigurable)
+UNC003 | Usage | Error | Diagnostics, [Documentation](NotConfigurable)
\ No newline at end of file
diff --git a/src/Ubiquity.NET.CommandLine.SrcGen/CommandGenerator.cs b/src/Ubiquity.NET.CommandLine.SrcGen/CommandGenerator.cs
index fecf04c..5e47c01 100644
--- a/src/Ubiquity.NET.CommandLine.SrcGen/CommandGenerator.cs
+++ b/src/Ubiquity.NET.CommandLine.SrcGen/CommandGenerator.cs
@@ -38,10 +38,11 @@ var optionClasses
{
// Do nothing if the target doesn't support what the generated code needs or something is wrong.
// Errors are detected by a distinct analyzer; code generators just NOP as fast as possible.
+ // see: https://csharp-evolution.com/guides/language-by-platform
var compilation = context.SemanticModel.Compilation;
if( context.Attributes.Length != 1 // Multiple instances not allowed and 0 is just broken.
|| compilation.Language != "C#"
- || !compilation.HasLanguageVersionAtLeastEqualTo(LanguageVersion.CSharp13)
+ || !compilation.HasLanguageVersionAtLeastEqualTo( LanguageVersion.CSharp12 ) // C# 12 => .NET 8.0 => supported until 2026-11-10 (LTS)
|| context.TargetSymbol is not INamedTypeSymbol namedTypeSymbol
|| context.TargetNode is not ClassDeclarationSyntax commandClass
)
@@ -82,7 +83,8 @@ private static void Execute( SourceProductionContext context, RootCommandInfo so
{
var template = new Templates.RootCommandClassTemplate(source);
var generatedSource = template.GenerateText();
- context.AddSource( $"{source.TargetName:R}.g.cs", generatedSource );
+ string hintPath = $"{source.TargetName:R}.g.cs";
+ context.AddSource( hintPath, generatedSource );
}
}
}
diff --git a/src/Ubiquity.NET.CommandLine.SrcGen/CommandLineAnalyzer.cs b/src/Ubiquity.NET.CommandLine.SrcGen/CommandLineAnalyzer.cs
new file mode 100644
index 0000000..e522e8d
--- /dev/null
+++ b/src/Ubiquity.NET.CommandLine.SrcGen/CommandLineAnalyzer.cs
@@ -0,0 +1,222 @@
+// Copyright (c) Ubiquity.NET Contributors. All rights reserved.
+// Licensed under the Apache-2.0 WITH LLVM-exception license. See the LICENSE.md file in the project root for full license information.
+
+using System.Collections.Immutable;
+using System.Diagnostics;
+using System.Linq;
+
+using Microsoft.CodeAnalysis.CSharp;
+using Microsoft.CodeAnalysis.Diagnostics;
+
+using Ubiquity.NET.Extensions;
+
+namespace Ubiquity.NET.CommandLine.SrcGen
+{
+ // Sadly C# (and .NET) doesn't have any real concept of a type alias (typedef in C++)
+ // This simplifies use within this file at least... [Sigh...]
+#pragma warning disable IDE0065 // Misplaced using directive
+#pragma warning disable SA1200 // Using directives should be placed correctly
+#pragma warning disable SA1135 // Using directives should be qualified
+ using PropertyAttributeHandlerAction = Action;
+
+ // Sadly, this can't use the `PropertyAttributeHandlerAction`... symbol and it must use the actual type... [Sigh...]
+ using PropertyAttributeHandlerMap = ImmutableDictionary>;
+
+ using SymbolHandlerAction = Action;
+ using SymbolHandlerMap = ImmutableDictionary>;
+#pragma warning restore SA1135 // Using directives should be qualified
+#pragma warning restore SA1200 // Using directives should be placed correctly
+#pragma warning restore IDE0065 // Misplaced using directive
+
+ /// Analyzer for the command line attribute usage
+ [DiagnosticAnalyzer( LanguageNames.CSharp )]
+ public class CommandLineAnalyzer
+ : DiagnosticAnalyzer
+ {
+ ///
+ public override ImmutableArray SupportedDiagnostics => Diagnostics.CommandLineAnalyzerDiagnostics;
+
+ ///
+ public override void Initialize( AnalysisContext context )
+ {
+ context.ConfigureGeneratedCodeAnalysis( GeneratedCodeAnalysisFlags.None );
+ context.EnableConcurrentExecution();
+
+ // As of right now there is no validation of attributes on a type
+ // [A type for a command is allowed to have no Options/Arguments.]
+ context.RegisterSymbolAction( OnTypeOrProperty, /*SymbolKind.NamedType,*/ SymbolKind.Property );
+ }
+
+ [SuppressMessage( "Design", "CA1031:Do not catch general exception types", Justification = "No loss of information, exception transformed to a diagnostic" )]
+ private void OnTypeOrProperty( SymbolAnalysisContext context )
+ {
+ try
+ {
+ if(!SymbolHandlerMap.TryGetValue( context.Symbol.Kind, out var handler ))
+ {
+ Debug.Assert( false, "Unexpected value for kind!" );
+ return;
+ }
+
+ handler( context );
+ return;
+ }
+ catch(Exception ex)
+ {
+ Location? loc = context.Symbol.Locations.Length < 1 ? default : context.Symbol.Locations[0];
+ ReportDiagnostic( context, Diagnostics.InternalError, loc, ex.Message );
+ }
+ }
+
+ //private static void OnNamedType( SymbolAnalysisContext context )
+ //{
+ // if(context.Symbol is not INamedTypeSymbol symbol)
+ // {
+ // Debug.Assert( false, "Non type symbol for named type..." );
+ // return;
+ // }
+ //
+ // // Currently no validation on a named type symbol
+ //}
+
+ private static void OnProperty( SymbolAnalysisContext context )
+ {
+ if(context.Symbol is not IPropertySymbol propSymbol)
+ {
+ Debug.Assert( false, "Non-property symbol provided to property symbol handler!" );
+ return;
+ }
+
+ // If there is a handler for an attribute, then call it to verify the attribute's information
+ var attribData = context.Symbol.MatchingAttributes( Constants.GeneratingAttributeNames );
+ var attribs = new EquatableAttributeDataCollection( attribData.Select(a=>new EquatableAttributeData(a)) );
+ foreach(var attrib in attribData)
+ {
+ // lookup a handler for this type of attribute, any other attribute is left alone
+ // it might be complete bogus, but that's not validated by this analyzer.
+ if(PropertyHandlerMap.TryGetValue( attrib.GetNamespaceQualifiedName(), out var handler ))
+ {
+ handler( context, propSymbol, attrib.GetLocation(), attribs );
+ }
+ }
+ }
+
+ private static void OnOptionAttribute( SymbolAnalysisContext context, IPropertySymbol symbol, Location? attribLoc, EquatableAttributeDataCollection _ )
+ {
+ // Verify it is applied to a property in a type with a command attribute
+ VerifyCommandAttribute( context, attribLoc, Constants.OptionAttribute.SimpleName );
+
+ // Additional validations...
+ }
+
+ private static void OnFileValidationAttribute( SymbolAnalysisContext context, IPropertySymbol symbol, Location? attribLoc, EquatableAttributeDataCollection attribs )
+ {
+ Location? propertyLoc = symbol.Locations.Length < 1 ? default : symbol.Locations[0];
+
+ // Verify it is applied to a property in a type with a command attribute
+ VerifyCommandAttribute( context, attribLoc, Constants.FileValidationAttribute.SimpleName );
+
+ // Verify an Option property (or maybe an argument attribute once supported) exists
+ VerifyHasConstrainedAttribute( context, attribLoc, attribs );
+
+ // Verify type of the property is System.IO.FileInfo.
+ VerifyPropertyType( context, symbol, Constants.FileInfo, propertyLoc, Constants.FileValidationAttribute.SimpleName );
+
+ // Additional validations...
+ }
+
+ private static void OnFolderValidationAttribute( SymbolAnalysisContext context, IPropertySymbol symbol, Location? attribLoc, EquatableAttributeDataCollection attribs )
+ {
+ Location? propertyLoc = symbol.Locations.Length < 1 ? default : symbol.Locations[0];
+
+ // Verify it is applied to a property in a type with a command attribute
+ VerifyCommandAttribute( context, attribLoc, Constants.FolderValidationAttribute.SimpleName );
+
+ // Verify an Option property (or maybe an argument attribute once supported) exists
+ VerifyHasConstrainedAttribute( context, propertyLoc, attribs );
+
+ // Verify type of the property is System.IO.FileInfo.
+ VerifyPropertyType( context, symbol, Constants.DirectoryInfo, propertyLoc, Constants.FolderValidationAttribute.SimpleName );
+
+ // Additional validations...
+ }
+
+ /// Reports a diagnostic if the containing type for the current symbol has a command attribute
+ /// Context for the symbol to test
+ /// Location of the attribute requiring the attribute on the parent type
+ /// Name of the attribute (For reporting diagnostics)
+ ///
+ /// At present, the only attribute that qualifies as a command attribute is .
+ /// In the future it is possible that this tests for another attribute for a sub-command.
+ ///
+ /// and are for the actual attribute that requires a command
+ /// attribute on the containing type. These are used in forming the diagnostic message text.
+ ///
+ private static void VerifyCommandAttribute( SymbolAnalysisContext context, Location? attribLoc, string attribName )
+ {
+ // At present, there is only RootCommandAttribute, but in future a sub command attribute may exist
+ var parentAttributes = context.Symbol.ContainingType.MatchingAttributes([Constants.RootCommandAttribute]);
+ if(parentAttributes.IsDefaultOrEmpty)
+ {
+ ReportDiagnostic( context, Diagnostics.MissingCommandAttribute, attribLoc, attribName );
+ }
+ }
+
+ /// Verifies (and reports a diagnostic if not) that a set of attributes contains a required constraint
+ /// Context to use for reporting diagnostics
+ /// Location to use for any diagnostics reported
+ /// Set of attributes to check
+ ///
+ /// The is used to report diagnostics that normally references the attribute
+ /// that is missing the constrained attribute. (ex. The `FileValidation` or `FolderValidation` that does NOT
+ /// have an `OptionAttribute` that it validates.
+ ///
+ private static void VerifyHasConstrainedAttribute( SymbolAnalysisContext context, Location? diagnosticLoc, EquatableAttributeDataCollection attribs )
+ {
+ // Verify an Option property (or maybe an argument attribute once supported) exists
+ if(!attribs.TryGetValue( Constants.OptionAttribute, out _ ))
+ {
+ ReportDiagnostic( context, Diagnostics.MissingConstraintAttribute, diagnosticLoc, Constants.FileValidationAttribute.SimpleName );
+ }
+ }
+
+ /// Verifies a property has an expected type
+ /// Context to use for reporting diagnostics
+ /// Property symbol to test
+ /// Name of the expected type
+ /// Location of the attribute that requiring a specific type
+ /// Name of the attribute that requires a specific type
+ ///
+ /// The and are used in any diagnostics
+ /// reported. That is, they reference the attribute that requires a specific type. It is debatable
+ /// if the location of the return type is wrong or the attribute is wrong...
+ ///
+ private static void VerifyPropertyType( SymbolAnalysisContext context, IPropertySymbol symbol, NamespaceQualifiedName expectedType, Location? attribLoc, string attribName )
+ {
+ if(symbol.Type.GetNamespaceQualifiedName() != expectedType)
+ {
+ ReportDiagnostic( context, Diagnostics.IncorrectPropertyType, attribLoc, attribName, expectedType.ToString( "A", null ) );
+ }
+ }
+
+ private static void ReportDiagnostic( SymbolAnalysisContext context, DiagnosticDescriptor descriptor, Location? loc, params object[] args )
+ {
+ context.ReportDiagnostic( Diagnostic.Create( descriptor, loc, args ) );
+ }
+
+ private static readonly SymbolHandlerMap SymbolHandlerMap
+ = new DictionaryBuilder()
+ {
+ // [SymbolKind.NamedType] = OnNamedType,
+ [SymbolKind.Property] = OnProperty,
+ }.ToImmutable();
+
+ private static readonly PropertyAttributeHandlerMap PropertyHandlerMap
+ = new DictionaryBuilder()
+ {
+ [Constants.OptionAttribute] = OnOptionAttribute,
+ [Constants.FileValidationAttribute] = OnFileValidationAttribute,
+ [Constants.FolderValidationAttribute] = OnFolderValidationAttribute,
+ }.ToImmutable();
+ }
+}
diff --git a/src/Ubiquity.NET.CommandLine.SrcGen/Diagnostics.cs b/src/Ubiquity.NET.CommandLine.SrcGen/Diagnostics.cs
new file mode 100644
index 0000000..c08411c
--- /dev/null
+++ b/src/Ubiquity.NET.CommandLine.SrcGen/Diagnostics.cs
@@ -0,0 +1,79 @@
+// Copyright (c) Ubiquity.NET Contributors. All rights reserved.
+// Licensed under the Apache-2.0 WITH LLVM-exception license. See the LICENSE.md file in the project root for full license information.
+
+using System.Collections.Immutable;
+
+using Ubiquity.NET.CommandLine.SrcGen.Properties;
+
+namespace Ubiquity.NET.CommandLine.SrcGen
+{
+ internal class Diagnostics
+ {
+ internal static class IDs
+ {
+ internal const string InternalError = "UNC000";
+ internal const string MissingCommandAttribute = "UNC001";
+ internal const string MissingConstraintAttribute = "UNC002";
+ internal const string IncorrectPropertyType = "UNC003";
+ }
+
+ private static LocalizableResourceString Localized( string resName )
+ {
+ return new LocalizableResourceString( resName, Resources.ResourceManager, typeof( Resources ) );
+ }
+
+ internal static ImmutableArray CommandLineAnalyzerDiagnostics
+ => [
+ InternalError,
+ MissingCommandAttribute,
+ MissingConstraintAttribute,
+ IncorrectPropertyType
+ ];
+
+ internal static readonly DiagnosticDescriptor InternalError = new(
+ id: IDs.InternalError,
+ title: Localized(nameof(Resources.InternalError_Title)),
+ messageFormat: Localized(nameof(Resources.InternalError_MessageFormat)),
+ category: "Internal",
+ defaultSeverity: DiagnosticSeverity.Error,
+ isEnabledByDefault: true,
+ description: Localized(nameof(Resources.InternalError_Description)),
+ WellKnownDiagnosticTags.NotConfigurable
+ );
+
+ internal static readonly DiagnosticDescriptor MissingCommandAttribute = new(
+ id: IDs.MissingCommandAttribute,
+ title: Localized(nameof(Resources.MissingCommandAttribute_Title)),
+ messageFormat: Localized(nameof(Resources.MissingCommandAttribute_MessageFormat)),
+ category: "Usage",
+ defaultSeverity: DiagnosticSeverity.Warning,
+ isEnabledByDefault: true,
+ description: Localized(nameof(Resources.MissingCommandAttribute_Description)),
+ WellKnownDiagnosticTags.Unnecessary,
+ WellKnownDiagnosticTags.Compiler
+ );
+
+ internal static readonly DiagnosticDescriptor MissingConstraintAttribute = new(
+ id: IDs.MissingConstraintAttribute,
+ title: Localized(nameof(Resources.MissingConstraintAttribute_Title)),
+ messageFormat: Localized(nameof(Resources.MissingConstraintAttribute_MessageFormat)),
+ category: "Usage",
+ defaultSeverity: DiagnosticSeverity.Error,
+ isEnabledByDefault: true,
+ description: Localized(nameof(Resources.MissingConstraintAttribute_Description)),
+ WellKnownDiagnosticTags.Compiler
+ );
+
+ internal static readonly DiagnosticDescriptor IncorrectPropertyType = new(
+ id: IDs.IncorrectPropertyType,
+ title: Localized(nameof(Resources.IncorrectPropertyType_Title)),
+ messageFormat: Localized(nameof(Resources.IncorrectPropertyType_MessageFormat)),
+ category: "Usage",
+ defaultSeverity: DiagnosticSeverity.Error,
+ isEnabledByDefault: true,
+ description: Localized(nameof(Resources.IncorrectPropertyType_Description)),
+ WellKnownDiagnosticTags.Compiler,
+ WellKnownDiagnosticTags.NotConfigurable
+ );
+ }
+}
diff --git a/src/Ubiquity.NET.CommandLine.SrcGen/Properties/Resources.Designer.cs b/src/Ubiquity.NET.CommandLine.SrcGen/Properties/Resources.Designer.cs
new file mode 100644
index 0000000..39a5749
--- /dev/null
+++ b/src/Ubiquity.NET.CommandLine.SrcGen/Properties/Resources.Designer.cs
@@ -0,0 +1,171 @@
+//------------------------------------------------------------------------------
+//
+// This code was generated by a tool.
+// Runtime Version:4.0.30319.42000
+//
+// Changes to this file may cause incorrect behavior and will be lost if
+// the code is regenerated.
+//
+//------------------------------------------------------------------------------
+
+namespace Ubiquity.NET.CommandLine.SrcGen.Properties {
+ using System;
+
+
+ ///
+ /// A strongly-typed resource class, for looking up localized strings, etc.
+ ///
+ // This class was auto-generated by the StronglyTypedResourceBuilder
+ // class via a tool like ResGen or Visual Studio.
+ // To add or remove a member, edit your .ResX file then rerun ResGen
+ // with the /str option, or rebuild your VS project.
+ [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "18.0.0.0")]
+ [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
+ [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
+ internal class Resources {
+
+ private static global::System.Resources.ResourceManager resourceMan;
+
+ private static global::System.Globalization.CultureInfo resourceCulture;
+
+ [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
+ internal Resources() {
+ }
+
+ ///
+ /// Returns the cached ResourceManager instance used by this class.
+ ///
+ [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
+ internal static global::System.Resources.ResourceManager ResourceManager {
+ get {
+ if (object.ReferenceEquals(resourceMan, null)) {
+ global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Ubiquity.NET.CommandLine.SrcGen.Properties.Resources", typeof(Resources).Assembly);
+ resourceMan = temp;
+ }
+ return resourceMan;
+ }
+ }
+
+ ///
+ /// Overrides the current thread's CurrentUICulture property for all
+ /// resource lookups using this strongly typed resource class.
+ ///
+ [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
+ internal static global::System.Globalization.CultureInfo Culture {
+ get {
+ return resourceCulture;
+ }
+ set {
+ resourceCulture = value;
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Property has incorrect type for attribute.
+ ///
+ internal static string IncorrectPropertyType_Description {
+ get {
+ return ResourceManager.GetString("IncorrectPropertyType_Description", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Property attribute '{0}' requires a property of type '{1}'..
+ ///
+ internal static string IncorrectPropertyType_MessageFormat {
+ get {
+ return ResourceManager.GetString("IncorrectPropertyType_MessageFormat", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Property has incorrect type for attribute.
+ ///
+ internal static string IncorrectPropertyType_Title {
+ get {
+ return ResourceManager.GetString("IncorrectPropertyType_Title", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to An internal analyzer exception occurred..
+ ///
+ internal static string InternalError_Description {
+ get {
+ return ResourceManager.GetString("InternalError_Description", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Exception message is: '{0}'.
+ ///
+ internal static string InternalError_MessageFormat {
+ get {
+ return ResourceManager.GetString("InternalError_MessageFormat", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Internal Error.
+ ///
+ internal static string InternalError_Title {
+ get {
+ return ResourceManager.GetString("InternalError_Title", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Property attribute only allowed on an attributed command class.
+ ///
+ internal static string MissingCommandAttribute_Description {
+ get {
+ return ResourceManager.GetString("MissingCommandAttribute_Description", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Property attribute '{0}' is only allowed on a property in a type attributed with a command attribute. This use will be ignored by the generator..
+ ///
+ internal static string MissingCommandAttribute_MessageFormat {
+ get {
+ return ResourceManager.GetString("MissingCommandAttribute_MessageFormat", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Missing command attribute on containing type.
+ ///
+ internal static string MissingCommandAttribute_Title {
+ get {
+ return ResourceManager.GetString("MissingCommandAttribute_Title", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Property attribute not allowed standalone.
+ ///
+ internal static string MissingConstraintAttribute_Description {
+ get {
+ return ResourceManager.GetString("MissingConstraintAttribute_Description", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Property attribute '{0}' is not allowed on a property independent of a qualifying attribute such as OptionAttribute..
+ ///
+ internal static string MissingConstraintAttribute_MessageFormat {
+ get {
+ return ResourceManager.GetString("MissingConstraintAttribute_MessageFormat", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Property attribute not allowed standalone.
+ ///
+ internal static string MissingConstraintAttribute_Title {
+ get {
+ return ResourceManager.GetString("MissingConstraintAttribute_Title", resourceCulture);
+ }
+ }
+ }
+}
diff --git a/src/Ubiquity.NET.CommandLine.SrcGen/Properties/Resources.resx b/src/Ubiquity.NET.CommandLine.SrcGen/Properties/Resources.resx
new file mode 100644
index 0000000..4f328de
--- /dev/null
+++ b/src/Ubiquity.NET.CommandLine.SrcGen/Properties/Resources.resx
@@ -0,0 +1,157 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ text/microsoft-resx
+
+
+ 2.0
+
+
+ System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ An internal analyzer exception occurred.
+ An internal error occurred in the analyzer/fixer
+
+
+ Exception message is: '{0}'
+
+
+ Internal Error
+
+
+ Property attribute only allowed on an attributed command class
+
+
+ Property attribute '{0}' is only allowed on a property in a type attributed with a command attribute. This use will be ignored by the generator.
+
+
+ Missing command attribute on containing type
+
+
+ Property attribute not allowed standalone
+
+
+ Property attribute '{0}' is not allowed on a property independent of a qualifying attribute such as OptionAttribute.
+
+
+ Property attribute not allowed standalone
+
+
+ Property has incorrect type for attribute
+
+
+ Property attribute '{0}' requires a property of type '{1}'.
+
+
+ Property has incorrect type for attribute
+
+
\ No newline at end of file
diff --git a/src/Ubiquity.NET.CommandLine.SrcGen/Ubiquity.NET.CommandLine.SrcGen.csproj b/src/Ubiquity.NET.CommandLine.SrcGen/Ubiquity.NET.CommandLine.SrcGen.csproj
index 5cd96a9..6aceaf7 100644
--- a/src/Ubiquity.NET.CommandLine.SrcGen/Ubiquity.NET.CommandLine.SrcGen.csproj
+++ b/src/Ubiquity.NET.CommandLine.SrcGen/Ubiquity.NET.CommandLine.SrcGen.csproj
@@ -9,7 +9,7 @@
-->
netstandard2.0
-
+
12enabletrue
@@ -31,6 +31,7 @@
Apache-2.0 WITH LLVM-exception$(NoWarn);NU5128
+ en-US
@@ -57,8 +58,6 @@
-
-
@@ -75,6 +74,19 @@
+
+
+ True
+ True
+ Resources.resx
+
+
+
+
+ ResXFileCodeGenerator
+ Resources.Designer.cs
+
+
diff --git a/src/Ubiquity.NET.CommandLine.UT/ArgumentExceptionReporterTests.cs b/src/Ubiquity.NET.CommandLine.UT/ArgumentExceptionReporterTests.cs
index cedbcb3..74d5b6c 100644
--- a/src/Ubiquity.NET.CommandLine.UT/ArgumentExceptionReporterTests.cs
+++ b/src/Ubiquity.NET.CommandLine.UT/ArgumentExceptionReporterTests.cs
@@ -6,8 +6,6 @@
using Microsoft.VisualStudio.TestTools.UnitTesting;
-using Ubiquity.NET.CommandLine;
-
namespace Ubiquity.NET.CommandLine.UT
{
[TestClass]
diff --git a/src/Ubiquity.NET.Extensions.UT/LocalizableStringTests.cs b/src/Ubiquity.NET.Extensions.UT/LocalizableStringTests.cs
index 9c59543..e5ee11d 100644
--- a/src/Ubiquity.NET.Extensions.UT/LocalizableStringTests.cs
+++ b/src/Ubiquity.NET.Extensions.UT/LocalizableStringTests.cs
@@ -1,8 +1,6 @@
// Copyright (c) Ubiquity.NET Contributors. All rights reserved.
// Licensed under the Apache-2.0 WITH LLVM-exception license. See the LICENSE.md file in the project root for full license information.
-using Microsoft.VisualStudio.TestTools.UnitTesting;
-
namespace Ubiquity.NET.Extensions.UT
{
[TestClass]
diff --git a/src/Ubiquity.NET.Extensions/Ubiquity.NET.Extensions.csproj b/src/Ubiquity.NET.Extensions/Ubiquity.NET.Extensions.csproj
index 6128a0c..0a38abd 100644
--- a/src/Ubiquity.NET.Extensions/Ubiquity.NET.Extensions.csproj
+++ b/src/Ubiquity.NET.Extensions/Ubiquity.NET.Extensions.csproj
@@ -35,8 +35,8 @@
runtime; build; native; contentfiles; analyzers; buildtransitive
-
-
+
+
diff --git a/src/Ubiquity.NET.InteropHelpers/Ubiquity.NET.InteropHelpers.csproj b/src/Ubiquity.NET.InteropHelpers/Ubiquity.NET.InteropHelpers.csproj
index a7c1b09..874a922 100644
--- a/src/Ubiquity.NET.InteropHelpers/Ubiquity.NET.InteropHelpers.csproj
+++ b/src/Ubiquity.NET.InteropHelpers/Ubiquity.NET.InteropHelpers.csproj
@@ -32,7 +32,7 @@
runtime; build; native; contentfiles; analyzers; buildtransitive
-
+
diff --git a/src/Ubiquity.NET.SourceGenerator.Test.Utils/CSharp/CachedSourceGeneratorTest.cs b/src/Ubiquity.NET.SourceGenerator.Test.Utils/CSharp/CachedSourceGeneratorTest.cs
new file mode 100644
index 0000000..f3f86e1
--- /dev/null
+++ b/src/Ubiquity.NET.SourceGenerator.Test.Utils/CSharp/CachedSourceGeneratorTest.cs
@@ -0,0 +1,161 @@
+// Copyright (c) Ubiquity.NET Contributors. All rights reserved.
+// Licensed under the Apache-2.0 WITH LLVM-exception license. See the LICENSE.md file in the project root for full license information.
+
+using System.Collections.Immutable;
+using System.Linq;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp;
+using Microsoft.CodeAnalysis.Testing;
+using Microsoft.CodeAnalysis.Testing.Model;
+using Microsoft.CodeAnalysis.Text;
+
+namespace Ubiquity.NET.SourceGenerator.Test.Utils.CSharp
+{
+ /// implementation that verifies a generator only uses cached values
+ /// Type of the generator; Must implement
+ /// Type of the verifier; Must implement
+ ///
+ /// The verifier is used to adapt to a variety of test frameworks in a consistent manner. It is usually,
+ /// but may be any type implementing the interface.
+ /// The are used to determine what is verified for cached results. Only the results with matching
+ /// names is validated for cached results. All other diagnostics apply, that is even if is empty the
+ /// diagnostics produced are still validated as well as the generated output. The cached behavior is only tested AFTER all the
+ /// other test states are verified.
+ ///
+ public class CachedSourceGeneratorTest
+ : LanguageVerionsSourceGeneratorTest
+ where TGenerator : IIncrementalGenerator, new()
+ where TVerifier : IVerifier, new()
+ {
+ /// Initializes a new instance of the class.
+ /// Tracking names to validate for cached results (Others are not validated for that)
+ /// Version of the language for the generator
+ /// Nullability behavior for this generator; default is to enable it for language versions that support it.
+ public CachedSourceGeneratorTest(
+ ImmutableArray trackingNames,
+ LanguageVersion languageVersion,
+ NullableContextOptions? nullableContextOptions = null
+ )
+ : base(languageVersion, nullableContextOptions)
+ {
+ TrackingNames = trackingNames;
+ }
+
+ /// Gets the tracking names to use for this test
+ public ImmutableArray TrackingNames { get; }
+
+ ///
+ /// This will run the test AND validate cached output
+ protected override async Task RunImplAsync( CancellationToken cancellationToken )
+ {
+ // This replicates much of what is in the base type in order to expose the driver
+ // or, more particularly the driver results of multiple passes of the same compilation
+ // to ensure that both runs produce the same results AND that the second run uses
+ // only cached results.
+
+ if(!TestState.GeneratedSources.Any())
+ {
+ // Verify the test state has at least one source, which may or may not be generated
+ Verify.NotEmpty( $"{nameof( TestState )}.{nameof( SolutionState.Sources )}", TestState.Sources );
+ }
+
+ SolutionState testState = GetSolutionState();
+ Project project = await CreateProjectAsync( testState, cancellationToken );
+
+ Compilation compilation = await GetAndVerifyCompilation( project, cancellationToken );
+
+ // clone for use in validating cached behavior later
+ var compilationClone = compilation.Clone();
+
+ var sourceGenerator = new TGenerator().AsSourceGenerator();
+ GeneratorDriver driver = CSharpGeneratorDriver.Create(
+ generators: [sourceGenerator],
+ driverOptions: new GeneratorDriverOptions(default, trackIncrementalGeneratorSteps: true)
+ );
+
+ // save the resulting immutable driver for use in second run.
+ driver = driver.RunGenerators( compilation, cancellationToken );
+ GeneratorDriverRunResult runResult1 = driver.GetRunResult();
+ Verify.Empty( "Result diagnostics", runResult1.Diagnostics );
+
+ // validate the generated trees have the correct count and names
+ var expected = testState.GeneratedSources;
+ Verify.Equal( expected.Count, runResult1.GeneratedTrees.Length, $"Should generate {expected.Count} 'files' during generation Actual count is {runResult1.GeneratedTrees.Length}." );
+ VerifyResultsEqual( runResult1, expected, cancellationToken );
+ VerifyCached( driver, compilationClone, runResult1, cancellationToken );
+ }
+
+ private static async Task GetAndVerifyCompilation( Project project, CancellationToken cancellationToken )
+ {
+ Verify.True( project.SupportsCompilation, "Project must support compilation for testing" );
+
+ // Previous Assertion validates compilation won't be null.
+ var compilation = (await project.GetCompilationAsync( cancellationToken ))!;
+
+ var diagnostics = compilation.GetDiagnostics( cancellationToken );
+ var bldr = new StringBuilder();
+ foreach(var diagnostic in diagnostics)
+ {
+ bldr.AppendLine( diagnostic.ToString() );
+ }
+
+ // Report compilation diagnostics as an error
+ Verify.Equal( 0, diagnostics.Length, bldr.ToString() );
+ return compilation;
+ }
+
+ private static void VerifyResultsEqual( GeneratorDriverRunResult runResult1, SourceFileCollection expected, CancellationToken cancellationToken )
+ {
+ for(int i = 0; i < expected.Count; ++i)
+ {
+ (string hintPath, SourceText expectedText) = expected[ i ];
+
+ SyntaxTree tree = runResult1.GeneratedTrees[i];
+
+ Verify.Equal( hintPath, tree.FilePath, "Generated files should use correct name" );
+ Verify.Equal( Encoding.UTF8, tree.Encoding, $"Generated files should use UTF8. [{hintPath}]" );
+
+ SourceText actualText = runResult1.GeneratedTrees[i].GetText( cancellationToken );
+ Verify.AreEqual( expectedText, actualText, "Generated source should match expected content" );
+ }
+ }
+
+ private void VerifyCached( GeneratorDriver driver, Compilation compilationClone, GeneratorDriverRunResult runResult1, CancellationToken cancellationToken )
+ {
+ if(!TrackingNames.IsDefaultOrEmpty)
+ {
+ GeneratorDriverRunResult runResult2 = driver.RunGenerators( compilationClone, cancellationToken )
+ .GetRunResult();
+
+ Verify.AreEqual( runResult1, runResult2, TrackingNames );
+ Verify.Cached( runResult2 );
+ }
+ }
+
+ private Task CreateProjectAsync( SolutionState testState, CancellationToken cancellationToken )
+ {
+ var evaluatedState = new EvaluatedProjectState( testState, ReferenceAssemblies );
+ var additionalProjects = ( from proj in TestState.AdditionalProjects.Values
+ select new EvaluatedProjectState(proj, ReferenceAssemblies)
+ ).ToImmutableArray();
+
+ return CreateProjectAsync( evaluatedState, additionalProjects, cancellationToken );
+ }
+
+ private SolutionState GetSolutionState( )
+ {
+ var analyzers = GetDiagnosticAnalyzers().ToArray();
+ var defaultDiagnostic = GetDefaultDiagnostic(analyzers);
+ var supportedDiagnostics = analyzers.SelectMany(analyzer => analyzer.SupportedDiagnostics)
+ .ToImmutableArray();
+
+ var fixableDiagnostics = ImmutableArray.Empty;
+ return TestState.WithInheritedValuesApplied(null, fixableDiagnostics)
+ .WithProcessedMarkup(MarkupOptions, defaultDiagnostic, supportedDiagnostics, fixableDiagnostics, DefaultFilePath);
+ }
+ }
+}
diff --git a/src/Ubiquity.NET.SourceGenerator.Test.Utils/CSharp/LangVersionExtensions.cs b/src/Ubiquity.NET.SourceGenerator.Test.Utils/CSharp/LangVersionExtensions.cs
new file mode 100644
index 0000000..82d60a9
--- /dev/null
+++ b/src/Ubiquity.NET.SourceGenerator.Test.Utils/CSharp/LangVersionExtensions.cs
@@ -0,0 +1,44 @@
+// Copyright (c) Ubiquity.NET Contributors. All rights reserved.
+// Licensed under the Apache-2.0 WITH LLVM-exception license. See the LICENSE.md file in the project root for full license information.
+
+using System.Diagnostics.CodeAnalysis;
+
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp;
+
+namespace Ubiquity.NET.SourceGenerator.Test.Utils.CSharp
+{
+ /// Utility class to provide extensions to
+ [SuppressMessage( "Design", "CA1034: Nested types should not be visible", Justification = "Bogus; extension see: https://github.com/dotnet/sdk/issues/51681, and https://github.com/dotnet/roslyn-analyzers/issues/7765" )]
+ public static class LangVersionExtensions
+ {
+ extension( LanguageVersion self )
+ {
+ /// Gets the default nullability for a specific language
+ /// Any unknown values for language version gets a result of a default
+ public NullableContextOptions DefaultNullability
+ => self switch
+ {
+ LanguageVersion.CSharp1 or
+ LanguageVersion.CSharp2 or
+ LanguageVersion.CSharp3 or
+ LanguageVersion.CSharp4 or
+ LanguageVersion.CSharp5 or
+ LanguageVersion.CSharp6 or
+ LanguageVersion.CSharp7 or
+ LanguageVersion.CSharp7_1 or
+ LanguageVersion.CSharp7_2 or
+ LanguageVersion.CSharp7_3 => NullableContextOptions.Disable,
+
+ LanguageVersion.CSharp8 or
+ LanguageVersion.CSharp9 or
+ LanguageVersion.CSharp10 or
+ LanguageVersion.CSharp11 or
+ LanguageVersion.CSharp12 or
+ LanguageVersion.CSharp13 or
+ LanguageVersion.CSharp14 => NullableContextOptions.Enable,
+ _ => NullableContextOptions.Disable,
+ };
+ }
+ }
+}
diff --git a/src/Ubiquity.NET.SourceGenerator.Test.Utils/CSharp/LanguageVerionsSourceGeneratorTest.cs b/src/Ubiquity.NET.SourceGenerator.Test.Utils/CSharp/LanguageVerionsSourceGeneratorTest.cs
new file mode 100644
index 0000000..402c88e
--- /dev/null
+++ b/src/Ubiquity.NET.SourceGenerator.Test.Utils/CSharp/LanguageVerionsSourceGeneratorTest.cs
@@ -0,0 +1,53 @@
+// Copyright (c) Ubiquity.NET Contributors. All rights reserved.
+// Licensed under the Apache-2.0 WITH LLVM-exception license. See the LICENSE.md file in the project root for full license information.
+
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp;
+using Microsoft.CodeAnalysis.CSharp.Testing;
+using Microsoft.CodeAnalysis.Testing;
+
+namespace Ubiquity.NET.SourceGenerator.Test.Utils.CSharp
+{
+ /// Source generator tests for C# that allows specification of the language
+ /// Source generator type
+ /// Verifier type
+ ///
+ /// This type is generally used as a base class for language specific test types. It doesn't include logic/overloads
+ /// to test for cached results, which is an important part of testing a source generator. The design of the
+ /// assumes only a single pass which isn't
+ /// sufficient for verification of cached state. For full cached testing use
+ /// which uses this as a base.
+ ///
+ public class LanguageVerionsSourceGeneratorTest
+ : CSharpSourceGeneratorTest
+ where TSourceGenerator : new()
+ where TVerifier : IVerifier, new()
+ {
+ /// Initializes a new instance of the class.
+ /// Version of the language for this test
+ /// Nullability context to use for compilation [default: is based on ]
+ public LanguageVerionsSourceGeneratorTest(LanguageVersion ver, NullableContextOptions? nullableContextOptions = null)
+ {
+ LanguageVersion = ver;
+ NullableContextOptions = nullableContextOptions ?? LanguageVersion.DefaultNullability;
+ }
+
+ /// Gets the version of the language to use for the tests
+ public LanguageVersion LanguageVersion { get; }
+
+ /// Gets the options that determines how nullability is handled
+ public NullableContextOptions NullableContextOptions { get; }
+
+ ///
+ protected override ParseOptions CreateParseOptions( )
+ {
+ return ((CSharpParseOptions)base.CreateParseOptions()).WithLanguageVersion(LanguageVersion);
+ }
+
+ ///
+ protected override CompilationOptions CreateCompilationOptions( )
+ {
+ return new CSharpCompilationOptions( OutputKind.DynamicallyLinkedLibrary, allowUnsafe: true, nullableContextOptions: NullableContextOptions );
+ }
+ }
+}
diff --git a/src/Ubiquity.NET.SourceGenerator.Test.Utils/CSharp/SourceAnalyzerTest.cs b/src/Ubiquity.NET.SourceGenerator.Test.Utils/CSharp/SourceAnalyzerTest.cs
new file mode 100644
index 0000000..e8580ec
--- /dev/null
+++ b/src/Ubiquity.NET.SourceGenerator.Test.Utils/CSharp/SourceAnalyzerTest.cs
@@ -0,0 +1,54 @@
+// Copyright (c) Ubiquity.NET Contributors. All rights reserved.
+// Licensed under the Apache-2.0 WITH LLVM-exception license. See the LICENSE.md file in the project root for full license information.
+
+using System.Collections.Generic;
+
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp;
+using Microsoft.CodeAnalysis.Diagnostics;
+using Microsoft.CodeAnalysis.Testing;
+
+namespace Ubiquity.NET.SourceGenerator.Test.Utils.CSharp
+{
+ /// Source analyzer tests for C# that allows specification of the language
+ /// Analyzer type
+ /// Verifier type
+ public class SourceAnalyzerTest : AnalyzerTest
+ where TAnalyzer : DiagnosticAnalyzer, new()
+ where TVerifier : IVerifier, new()
+ {
+ /// Initializes a new instance of the class.
+ /// Version of the language for the testing
+ /// Nullability option to use for the test compilation
+ public SourceAnalyzerTest(LanguageVersion? languageVersion = null, NullableContextOptions? nullableContextOptions = null)
+ {
+ // .NET standard 2.0 is the lowest still supported, which uses C# 7.3
+ LanguageVersion = languageVersion ?? LanguageVersion.CSharp7_3;
+ NullableContextOptions = nullableContextOptions ?? LanguageVersion.DefaultNullability;
+ }
+
+ ///
+ protected override string DefaultFileExt => "cs";
+
+ ///
+ public override string Language => LanguageNames.CSharp;
+
+ /// Gets the version of the language to use for the tests
+ public LanguageVersion LanguageVersion { get; }
+
+ /// Gets the options that determines how nullability is handled
+ public NullableContextOptions NullableContextOptions { get; }
+
+ ///
+ protected override CompilationOptions CreateCompilationOptions( )
+ => new CSharpCompilationOptions( OutputKind.DynamicallyLinkedLibrary, allowUnsafe: true, nullableContextOptions: NullableContextOptions );
+
+ ///
+ protected override ParseOptions CreateParseOptions( )
+ => new CSharpParseOptions( LanguageVersion, DocumentationMode.Diagnose );
+
+ ///
+ protected override IEnumerable GetDiagnosticAnalyzers( )
+ => [new TAnalyzer()];
+ }
+}
diff --git a/src/Ubiquity.NET.SourceGenerator.Test.Utils/CSharp/SourceGeneratorTest.cs b/src/Ubiquity.NET.SourceGenerator.Test.Utils/CSharp/SourceGeneratorTest.cs
deleted file mode 100644
index 25bc10f..0000000
--- a/src/Ubiquity.NET.SourceGenerator.Test.Utils/CSharp/SourceGeneratorTest.cs
+++ /dev/null
@@ -1,34 +0,0 @@
-// Copyright (c) Ubiquity.NET Contributors. All rights reserved.
-// Licensed under the Apache-2.0 WITH LLVM-exception license. See the LICENSE.md file in the project root for full license information.
-
-using Microsoft.CodeAnalysis;
-using Microsoft.CodeAnalysis.CSharp;
-using Microsoft.CodeAnalysis.Testing;
-
-namespace Ubiquity.NET.SourceGenerator.Test.Utils.CSharp
-{
- /// Source generator tests for C# that allows specification of the language
- /// Source generator type
- /// Verifier type
- public class SourceGeneratorTest
- : Microsoft.CodeAnalysis.CSharp.Testing.CSharpSourceGeneratorTest
- where TSourceGenerator : new()
- where TVerifier : IVerifier, new()
- {
- /// Initializes a new instance of the class.
- /// Version of the language for this test
- public SourceGeneratorTest(LanguageVersion ver)
- {
- LanguageVersion = ver;
- }
-
- ///
- /// Creates parse options
- protected override ParseOptions CreateParseOptions( )
- {
- return ((CSharpParseOptions)base.CreateParseOptions()).WithLanguageVersion(LanguageVersion);
- }
-
- private readonly LanguageVersion LanguageVersion;
- }
-}
diff --git a/src/Ubiquity.NET.SourceGenerator.Test.Utils/EnumerableObjectComparer.cs b/src/Ubiquity.NET.SourceGenerator.Test.Utils/EnumerableObjectComparer.cs
deleted file mode 100644
index 0b692ed..0000000
--- a/src/Ubiquity.NET.SourceGenerator.Test.Utils/EnumerableObjectComparer.cs
+++ /dev/null
@@ -1,38 +0,0 @@
-// Copyright (c) Ubiquity.NET Contributors. All rights reserved.
-// Licensed under the Apache-2.0 WITH LLVM-exception license. See the LICENSE.md file in the project root for full license information.
-
-using System;
-using System.Collections.Generic;
-using System.Collections.Immutable;
-using System.Diagnostics.CodeAnalysis;
-using System.Linq;
-
-namespace Ubiquity.NET.SourceGenerator.Test.Utils
-{
- /// Comparer to test an array (element by element) for equality
- public class EnumerableObjectComparer
- : IEqualityComparer>
- {
- ///
- [SuppressMessage( "StyleCop.CSharp.NamingRules", "SA1305:Field names should not use Hungarian notation", Justification = "xValues and yValues are not Hungarian names" )]
- public bool Equals(IEnumerable