From 6528ecf9b7e43427c8a4e8ea8a0c1540831bcb87 Mon Sep 17 00:00:00 2001 From: Steven Maillet Date: Wed, 21 Jan 2026 09:15:56 -0800 Subject: [PATCH 1/2] Added arity checks to analyzer * Added support to NamespaceQualifiedTypeName to support arrays * Added support to `TypeSymbolExtensions.IsCollection` * Added testing of the demo source (as a link) to ensure it will operate correctly --- docfx/CommandLine/Diagnostics/UNC005.md | 15 +++ docfx/CommandLine/index.md | 1 + docfx/IgnoredWords.dic | 1 + src/DemoCommandLineSrcGen/TestOptions.cs | 14 +- .../DiagnosticInfo.cs | 8 +- .../NamespaceQualifiedNameFormatter.cs | 6 + .../NamespaceQualifiedTypeName.cs | 26 +++- .../TypeSymbolExtensions.cs | 18 +++ .../CommandAnalyzerTests.cs | 22 ++- .../RootCommandAttributeTests.cs | 22 ++- .../input.cs | 2 +- .../input.cs | 20 +++ .../DemoSource_succeeds/expected.cs | 90 +++++++++++++ .../Ubiquity.NET.CommandLine.SrcGen.UT.csproj | 8 ++ .../AnalyzerReleases.Unshipped.md | 11 +- .../CommandGenerator.cs | 8 +- .../CommandLineAnalyzer.cs | 29 +++- .../CommonAttributeData.cs | 24 ++++ .../Constants.cs | 2 + .../Diagnostics.cs | 127 +++++++++++++----- .../OptionInfo.cs | 16 +-- .../Properties/Resources.Designer.cs | 29 +++- .../Properties/Resources.resx | 9 ++ .../Templates/RootCommandClassTemplate.cs | 3 + .../CSharp/CachedSourceGeneratorTest.cs | 1 + 25 files changed, 433 insertions(+), 79 deletions(-) create mode 100644 docfx/CommandLine/Diagnostics/UNC005.md create mode 100644 src/Ubiquity.NET.CommandLine.SrcGen.UT/TestFiles/CommandAnalyzerTests/OptionArity_Not_matching_type_produces_diagnostic/input.cs create mode 100644 src/Ubiquity.NET.CommandLine.SrcGen.UT/TestFiles/RootCommandAttributeTests/DemoSource_succeeds/expected.cs create mode 100644 src/Ubiquity.NET.CommandLine.SrcGen/CommonAttributeData.cs diff --git a/docfx/CommandLine/Diagnostics/UNC005.md b/docfx/CommandLine/Diagnostics/UNC005.md new file mode 100644 index 0000000..caf78bc --- /dev/null +++ b/docfx/CommandLine/Diagnostics/UNC005.md @@ -0,0 +1,15 @@ +# UNC005 : Arity specified for property type is invalid. +This diagnostic indicates that the arity specified in an attribute does not match the type +of value for that property^1^. The default arity is usually enough but it is sometimes valid +to limit the "or more" default to a max value. In particular with collections there may be +a limit to the maximum number of values allowed so the arity specifies that. Not that the +arity applies to the ***values*** of a property. That is: +`--foo true` is ONLY allowed if the minimum arity is > 1, otherwise only the option itself +is allowed (for example: `--foo`). Setting the arity to a maximum that is > 1 requires a +collection type to bind the parsed values to. Setting a minimum > 0 makes it required to +specify a value for the command. That is, with a minimum arity of 1 `--foo` is an error. + + +--- +^1^ see the [System.CommandLine docs](https://learn.microsoft.com/en-us/dotnet/standard/commandline/syntax#argument-arity) +for details. diff --git a/docfx/CommandLine/index.md b/docfx/CommandLine/index.md index 3395438..ef338a9 100644 --- a/docfx/CommandLine/index.md +++ b/docfx/CommandLine/index.md @@ -14,3 +14,4 @@ Rule ID | Title | [UNC002](https://ubiquitydotnet.github.io/Ubiquity.NET.Utils/CommandLine/diagnostics/UNC002.html) | Property attribute not allowed standalone. | [UNC003](https://ubiquitydotnet.github.io/Ubiquity.NET.Utils/CommandLine/diagnostics/UNC003.html) | Property has incorrect type for attribute. | [UNC004](https://ubiquitydotnet.github.io/Ubiquity.NET.Utils/CommandLine/diagnostics/UNC004.html) | Property type is nullable but marked as required. | +[UNC005](https://ubiquitydotnet.github.io/Ubiquity.NET.Utils/CommandLine/diagnostics/UNC004.html) | Arity specified for property type is invalid. | diff --git a/docfx/IgnoredWords.dic b/docfx/IgnoredWords.dic index 35bce0a..b28cf3e 100644 --- a/docfx/IgnoredWords.dic +++ b/docfx/IgnoredWords.dic @@ -1 +1,2 @@ +arity nullable diff --git a/src/DemoCommandLineSrcGen/TestOptions.cs b/src/DemoCommandLineSrcGen/TestOptions.cs index 13537e7..833e8dc 100644 --- a/src/DemoCommandLineSrcGen/TestOptions.cs +++ b/src/DemoCommandLineSrcGen/TestOptions.cs @@ -3,8 +3,8 @@ #pragma warning disable IDE0130 // Namespace does not match folder structure -using System.CommandLine; using System.IO; +using System.Linq; using Ubiquity.NET.CommandLine; using Ubiquity.NET.CommandLine.GeneratorAttributes; @@ -18,14 +18,14 @@ namespace TestNamespace [RootCommand( Description = "Root command for tests" )] internal partial class TestOptions { - [Option( "-o", Description = "Test SomePath" )] + [Option( "-o", Description = "Test SomePath", Required = true )] [FolderValidation( FolderValidation.CreateIfNotExist )] public required DirectoryInfo SomePath { get; init; } [Option( "-v", Description = "Verbosity Level" )] public MsgLevel Verbosity { get; init; } = MsgLevel.Information; - [Option( "-b", Description = "Test Some existing Path" )] + [Option( "-b", Description = "Test Some existing Path", Required = true )] [FolderValidation( FolderValidation.ExistingOnly )] public required DirectoryInfo SomeExistingPath { get; init; } @@ -35,9 +35,12 @@ internal partial class TestOptions // This should be ignored by generator public string? NotAnOption { get; set; } - [Option( "-a", Hidden = true, Required = false, ArityMin = 0, ArityMax = 3, Description = "Test SomeOtherPath" )] + [Option( "-i", ArityMin = 0, Description = "include path" )] + public required DirectoryInfo[] IncludePath { get; init; } + + [Option( "-a", Hidden = true, Required = false, Description = "Test SomeOtherPath" )] [FileValidation( FileValidation.ExistingOnly )] - public required FileInfo SomeOtherPath { get; init; } + public required FileInfo? SomeOtherPath { get; init; } public override string ToString( ) { @@ -48,6 +51,7 @@ public override string ToString( ) Thing1 = {Thing1} NotAnOption = {NotAnOption ?? ""} SomeOtherPath = '{SomeOtherPath?.FullName ?? ""}' + IncludePath = '{string.Join(";", IncludePath.Select(di=>di.FullName))}' """; } } diff --git a/src/Ubiquity.NET.CodeAnalysis.Utils/DiagnosticInfo.cs b/src/Ubiquity.NET.CodeAnalysis.Utils/DiagnosticInfo.cs index f7b4d3b..e9df540 100644 --- a/src/Ubiquity.NET.CodeAnalysis.Utils/DiagnosticInfo.cs +++ b/src/Ubiquity.NET.CodeAnalysis.Utils/DiagnosticInfo.cs @@ -19,8 +19,8 @@ public sealed class DiagnosticInfo /// Descriptor for the diagnostic /// Location in the source file that triggered this diagnostic /// Args for the message - public DiagnosticInfo(DiagnosticDescriptor descriptor, Location? location, params string[] msgArgs) - : this(descriptor, location, (IEnumerable)msgArgs) + public DiagnosticInfo(DiagnosticDescriptor descriptor, Location? location, params object[] msgArgs) + : this(descriptor, location, (IEnumerable)msgArgs) { } @@ -28,7 +28,7 @@ public DiagnosticInfo(DiagnosticDescriptor descriptor, Location? location, param /// Descriptor for the diagnostic /// Location in the source file that triggered this diagnostic /// Args for the message - public DiagnosticInfo(DiagnosticDescriptor descriptor, Location? location, IEnumerable msgArgs) + public DiagnosticInfo(DiagnosticDescriptor descriptor, Location? location, IEnumerable msgArgs) #else /// Initializes a new instance of the class. /// Descriptor for the diagnostic @@ -43,7 +43,7 @@ public DiagnosticInfo(DiagnosticDescriptor descriptor, Location? location, param } /// Gets the parameters for this diagnostic - public ImmutableArray Params { get; } + public ImmutableArray Params { get; } /// Gets the descriptor for this diagnostic public DiagnosticDescriptor Descriptor { get; } diff --git a/src/Ubiquity.NET.CodeAnalysis.Utils/NamespaceQualifiedNameFormatter.cs b/src/Ubiquity.NET.CodeAnalysis.Utils/NamespaceQualifiedNameFormatter.cs index 8033910..3941662 100644 --- a/src/Ubiquity.NET.CodeAnalysis.Utils/NamespaceQualifiedNameFormatter.cs +++ b/src/Ubiquity.NET.CodeAnalysis.Utils/NamespaceQualifiedNameFormatter.cs @@ -111,6 +111,12 @@ public string Format( string format, NamespaceQualifiedName arg, IFormatProvider /// is not supported public string Format( string format, NamespaceQualifiedTypeName arg, IFormatProvider? formatProvider ) { + if(arg.IsArray) + { + string formattedElement = Format(format, arg.ElementType, formatProvider); + return $"{formattedElement}[]"; + } + string formattedString = Format(format, (NamespaceQualifiedName)arg, formatProvider); return arg.NullableAnnotation == NullableAnnotation.Annotated ? $"{formattedString}?" diff --git a/src/Ubiquity.NET.CodeAnalysis.Utils/NamespaceQualifiedTypeName.cs b/src/Ubiquity.NET.CodeAnalysis.Utils/NamespaceQualifiedTypeName.cs index 53d0add..60c98de 100644 --- a/src/Ubiquity.NET.CodeAnalysis.Utils/NamespaceQualifiedTypeName.cs +++ b/src/Ubiquity.NET.CodeAnalysis.Utils/NamespaceQualifiedTypeName.cs @@ -49,13 +49,25 @@ public NamespaceQualifiedTypeName( public NamespaceQualifiedTypeName( ITypeSymbol sym ) : this( GetNullableNamespaceNames( sym ), GetNullableSimpleName( sym ), sym.NullableAnnotation ) { + if(sym is IArrayTypeSymbol arrayType) + { + IsArray = true; + ElementType = arrayType.ElementType.GetNamespaceQualifiedName(); + } } /// Gets a value indicating whether this type has nullability annotation (and a generator should use a language specific nullability form) public bool IsNullable => NullableAnnotation == NullableAnnotation.Annotated; /// Gets the nullability annotation state for this type - public NullableAnnotation NullableAnnotation { get; init; } + public NullableAnnotation NullableAnnotation { get; } + + /// Gets a value indicating whether this name is an array + [MemberNotNullWhen( true, nameof( ElementType ) )] + public bool IsArray { get; } + + /// Gets the array element type (Only valid if is true) + public NamespaceQualifiedTypeName? ElementType { get; } /// Formats this instance according to the args /// Format string for this instance (see remarks) @@ -132,15 +144,27 @@ public override int GetHashCode( ) #endregion // IFF sym is a Nullable (Nullable value type) this will get the simple name of T + [SuppressMessage( "Style", "IDE0046:Convert to conditional expression", Justification = "Nested conditionals != simpler" )] private static string GetNullableSimpleName( ITypeSymbol sym ) { + if(sym is IArrayTypeSymbol arrayTypeSymbol) + { + return "Array"; + } + return sym.IsNullableValueType() && sym is INamedTypeSymbol ns ? ns.TypeArguments[ 0 ].Name : sym.Name; } + [SuppressMessage( "Style", "IDE0046:Convert to conditional expression", Justification = "Nested conditionals != simpler" )] private static IEnumerable GetNullableNamespaceNames( ITypeSymbol sym ) { + if(sym is IArrayTypeSymbol arrayTypeSymbol) + { + return ["System"]; + } + return sym.IsNullableValueType() && sym is INamedTypeSymbol ns ? ns.TypeArguments[ 0 ].GetNamespaceNames() : sym.GetNamespaceNames(); diff --git a/src/Ubiquity.NET.CodeAnalysis.Utils/TypeSymbolExtensions.cs b/src/Ubiquity.NET.CodeAnalysis.Utils/TypeSymbolExtensions.cs index b6c5991..2c91a29 100644 --- a/src/Ubiquity.NET.CodeAnalysis.Utils/TypeSymbolExtensions.cs +++ b/src/Ubiquity.NET.CodeAnalysis.Utils/TypeSymbolExtensions.cs @@ -52,6 +52,24 @@ public static bool IsNullableValueType( this ITypeSymbol self ) && self.IsValueType; } + /// Determines if this type symbol is a collection + /// Type symbol to test + /// true if the type is a collection and false if not + public static bool IsCollection( this ITypeSymbol self ) + { + var collectionItf = new NamespaceQualifiedTypeName(["System", "Collections"], "ICollection"); + for(int i = 0; i < self.AllInterfaces.Length; ++i) + { + var itf = self.AllInterfaces[i]; + if(itf.GetNamespaceQualifiedName() == collectionItf) + { + return true; + } + } + + return false; + } + // private iterator to defer the perf hit for reverse walk until the names // are iterated. The call to GetEnumerator() will take the hit to reverse walk // the names. diff --git a/src/Ubiquity.NET.CommandLine.SrcGen.UT/CommandAnalyzerTests.cs b/src/Ubiquity.NET.CommandLine.SrcGen.UT/CommandAnalyzerTests.cs index b8d0740..c57c756 100644 --- a/src/Ubiquity.NET.CommandLine.SrcGen.UT/CommandAnalyzerTests.cs +++ b/src/Ubiquity.NET.CommandLine.SrcGen.UT/CommandAnalyzerTests.cs @@ -138,9 +138,25 @@ public async Task Required_nullable_types_produce_diagnostic( TestRuntime testRu await analyzerTest.RunAsync( TestContext.CancellationToken ); } - // TODO: Test that a nullable value is not marked as required. (That's a conflicting claim, if it's required it can't be null) - // A nullable type MAY have a default value handler to provide a null default. Additional test - anything with a default - // value provider shouldn't be "required" it's also nonsensical. + [TestMethod] + [DataRow( TestRuntime.Net8_0 )] + [DataRow( TestRuntime.Net10_0 )] + public async Task OptionArity_Not_matching_type_produces_diagnostic( TestRuntime testRuntime ) + { + SourceText txt = GetSourceText( nameof(OptionArity_Not_matching_type_produces_diagnostic), "input.cs" ); + var analyzerTest = CreateTestRunner( txt, testRuntime ); + + // (9,6): error UNC005: Property '{0}' has type of '{1}' does not support arity of ({2}, {3}). + analyzerTest.ExpectedDiagnostics.AddRange( + [ + new DiagnosticResult("UNC005", DiagnosticSeverity.Error) + .WithArguments("Thing1", "bool", 3, 5) + .WithSpan(10, 6, 10, 138), + ] + ); + + await analyzerTest.RunAsync( TestContext.CancellationToken ); + } private AnalyzerTest CreateTestRunner( string source, TestRuntime testRuntime ) { diff --git a/src/Ubiquity.NET.CommandLine.SrcGen.UT/RootCommandAttributeTests.cs b/src/Ubiquity.NET.CommandLine.SrcGen.UT/RootCommandAttributeTests.cs index 4e361ce..3d592ab 100644 --- a/src/Ubiquity.NET.CommandLine.SrcGen.UT/RootCommandAttributeTests.cs +++ b/src/Ubiquity.NET.CommandLine.SrcGen.UT/RootCommandAttributeTests.cs @@ -20,7 +20,7 @@ public async Task Basic_golden_path_succeeds( TestRuntime testRuntime ) SourceText input = GetSourceText( nameof(Basic_golden_path_succeeds), inputFileName ); SourceText expected = GetSourceText( nameof(Basic_golden_path_succeeds), expectedFileName ); - var runner = CreateTestRunner(input, testRuntime, [TrackingNames.CommandClass], hintPath, expected ); + var runner = CreateTestRunner( input, testRuntime, [TrackingNames.CommandClass], hintPath, expected ); await runner.RunAsync( TestContext.CancellationToken ); } @@ -36,7 +36,23 @@ public async Task Generator_handles_nullable_types( TestRuntime testRuntime ) SourceText input = GetSourceText( nameof(Generator_handles_nullable_types), inputFileName ); SourceText expected = GetSourceText( nameof(Generator_handles_nullable_types), expectedFileName ); - var runner = CreateTestRunner(input, testRuntime, [TrackingNames.CommandClass], hintPath, expected ); + var runner = CreateTestRunner( input, testRuntime, [TrackingNames.CommandClass], hintPath, expected ); + await runner.RunAsync( TestContext.CancellationToken ); + } + + [TestMethod] + [DataRow( TestRuntime.Net8_0 )] + [DataRow( TestRuntime.Net10_0 )] + public async Task DemoSource_succeeds( TestRuntime testRuntime ) + { + const string inputFileName = "TestOptions.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 = GetSourceText( nameof(DemoSource_succeeds), inputFileName ); + SourceText expected = GetSourceText( nameof(DemoSource_succeeds), expectedFileName ); + + var runner = CreateTestRunner( input, testRuntime, [TrackingNames.CommandClass], hintPath, expected ); await runner.RunAsync( TestContext.CancellationToken ); } @@ -69,7 +85,7 @@ SourceText expectedContent }; } - private static SourceText GetSourceText(params string[] nameParts) + private static SourceText GetSourceText( params string[] nameParts ) { return TestHelpers.GetTestText( nameof( RootCommandAttributeTests ), nameParts ); } diff --git a/src/Ubiquity.NET.CommandLine.SrcGen.UT/TestFiles/CommandAnalyzerTests/GoldenPath_produces_no_diagnostics/input.cs b/src/Ubiquity.NET.CommandLine.SrcGen.UT/TestFiles/CommandAnalyzerTests/GoldenPath_produces_no_diagnostics/input.cs index 0f3bfe8..a33dd6b 100644 --- a/src/Ubiquity.NET.CommandLine.SrcGen.UT/TestFiles/CommandAnalyzerTests/GoldenPath_produces_no_diagnostics/input.cs +++ b/src/Ubiquity.NET.CommandLine.SrcGen.UT/TestFiles/CommandAnalyzerTests/GoldenPath_produces_no_diagnostics/input.cs @@ -20,7 +20,7 @@ internal partial class TestOptions // This should be ignored by generator public string? NotAnOption { get; set; } - [Option( "-a", Hidden = true, Required = false, ArityMin = 0, ArityMax = 3, Description = "Test SomeOtherPath" )] + [Option( "-a", Hidden = true, Required = false, ArityMin = 0, ArityMax = 1, Description = "Test SomeOtherPath" )] [FileValidation( FileValidation.ExistingOnly )] public required FileInfo SomeOtherPath { get; init; } } diff --git a/src/Ubiquity.NET.CommandLine.SrcGen.UT/TestFiles/CommandAnalyzerTests/OptionArity_Not_matching_type_produces_diagnostic/input.cs b/src/Ubiquity.NET.CommandLine.SrcGen.UT/TestFiles/CommandAnalyzerTests/OptionArity_Not_matching_type_produces_diagnostic/input.cs new file mode 100644 index 0000000..2f9966d --- /dev/null +++ b/src/Ubiquity.NET.CommandLine.SrcGen.UT/TestFiles/CommandAnalyzerTests/OptionArity_Not_matching_type_produces_diagnostic/input.cs @@ -0,0 +1,20 @@ +using System.IO; +using Ubiquity.NET.CommandLine.GeneratorAttributes; + +namespace TestNamespace; + +[RootCommand( Description = "Root command for tests" )] +internal partial class TestOptions +{ + // Warning : UNC005 : Property 'Thing1' has type of 'bool' which does not support an arity of (3, 5). + [Option( "--thing1", Aliases = [ "-t" ], ArityMin = 3, ArityMax = 5, Description = "Test Thing1", HelpName = "Help name for thing1" )] + public bool Thing1 { get; init; } + + // No diagnostic on this + [Option( "--thing2", Aliases = [ "-t" ], Description = "Test Thing2", HelpName = "Help name for thing2" )] + public bool Thing2 { get; init; } + + // No diagnostic on this + [Option("-i", ArityMin = 0, Description = "include path")] + public required DirectoryInfo[] IncludePath { get; init; } +} diff --git a/src/Ubiquity.NET.CommandLine.SrcGen.UT/TestFiles/RootCommandAttributeTests/DemoSource_succeeds/expected.cs b/src/Ubiquity.NET.CommandLine.SrcGen.UT/TestFiles/RootCommandAttributeTests/DemoSource_succeeds/expected.cs new file mode 100644 index 0000000..1b5e765 --- /dev/null +++ b/src/Ubiquity.NET.CommandLine.SrcGen.UT/TestFiles/RootCommandAttributeTests/DemoSource_succeeds/expected.cs @@ -0,0 +1,90 @@ +// ------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// testhost [18.0.1] +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +// ------------------------------------------------------------------------------ +using static global::Ubiquity.NET.CommandLine.SymbolValidationExtensions; + +namespace TestNamespace +{ + internal partial class TestOptions + : global::Ubiquity.NET.CommandLine.IRootCommandBuilderWithSettings + , global::Ubiquity.NET.CommandLine.ICommandBinder + { + public static global::Ubiquity.NET.CommandLine.CommandLineSettings Settings => new(); + + public static TestOptions Bind( global::System.CommandLine.ParseResult parseResult ) + { + return new() + { + SomePath = parseResult.GetRequiredValue( Descriptors.SomePath ), + Verbosity = parseResult.GetRequiredValue( Descriptors.Verbosity ), + SomeExistingPath = parseResult.GetRequiredValue( Descriptors.SomeExistingPath ), + Thing1 = parseResult.GetValue( Descriptors.Thing1 ), + IncludePath = parseResult.GetRequiredValue( Descriptors.IncludePath ), + SomeOtherPath = parseResult.GetValue( Descriptors.SomeOtherPath ), + }; + } + + public static global::Ubiquity.NET.CommandLine.AppControlledDefaultsRootCommand Build( ) + { + return new global::Ubiquity.NET.CommandLine.AppControlledDefaultsRootCommand( Settings, "Root command for tests" ) + { + Descriptors.SomePath, + Descriptors.Verbosity, + Descriptors.SomeExistingPath, + Descriptors.Thing1, + Descriptors.IncludePath, + Descriptors.SomeOtherPath, + }; + } + } + + file static class Descriptors + { + internal static readonly global::System.CommandLine.Option SomePath + = new global::System.CommandLine.Option("-o") + { + Description = "Test SomePath", + Required = true, + }.EnsureFolder(); + + internal static readonly global::System.CommandLine.Option Verbosity + = new global::System.CommandLine.Option("-v") + { + Description = "Verbosity Level", + }; + + internal static readonly global::System.CommandLine.Option SomeExistingPath + = new global::System.CommandLine.Option("-b") + { + Description = "Test Some existing Path", + Required = true, + }.AcceptExistingFolderOnly(); + + internal static readonly global::System.CommandLine.Option Thing1 + = new global::System.CommandLine.Option("--thing1", "-t") + { + HelpName = "Help name for thing1", + Description = "Test Thing1", + }; + + internal static readonly global::System.CommandLine.Option IncludePath + = new global::System.CommandLine.Option("-i") + { + Description = "include path", + }; + + internal static readonly global::System.CommandLine.Option SomeOtherPath + = new global::System.CommandLine.Option("-a") + { + Description = "Test SomeOtherPath", + Required = false, + Hidden = true, + }; + } +} 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 edd2aed..108e3f9 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 @@ -34,6 +34,8 @@ + + @@ -41,12 +43,18 @@ + + + + + + diff --git a/src/Ubiquity.NET.CommandLine.SrcGen/AnalyzerReleases.Unshipped.md b/src/Ubiquity.NET.CommandLine.SrcGen/AnalyzerReleases.Unshipped.md index 42acf4f..61e8472 100644 --- a/src/Ubiquity.NET.CommandLine.SrcGen/AnalyzerReleases.Unshipped.md +++ b/src/Ubiquity.NET.CommandLine.SrcGen/AnalyzerReleases.Unshipped.md @@ -5,8 +5,9 @@ Rule ID | Category | Severity | Notes --------|----------|----------|------- -UNC000 | Internal | Error | Diagnostics, [Documentation](NotConfigurable) -UNC001 | Usage | Error | Diagnostics -UNC002 | Usage | Error | Diagnostics, [Documentation](NotConfigurable) -UNC003 | Usage | Error | Diagnostics, [Documentation](NotConfigurable) -UNC004 | Usage | Warning | Diagnostics, [Documentation](Compiler) +UNC000 | Internal | Error | Diagnostics, [Documentation](https://ubiquitydotnet.github.io/Ubiquity.NET.Utils/CommandLine/diagnostics/UNC000.html) +UNC001 | Usage | Error | Diagnostics, [Documentation](https://ubiquitydotnet.github.io/Ubiquity.NET.Utils/CommandLine/diagnostics/UNC001.html) +UNC002 | Usage | Error | Diagnostics, [Documentation](https://ubiquitydotnet.github.io/Ubiquity.NET.Utils/CommandLine/diagnostics/UNC002.html) +UNC003 | Usage | Error | Diagnostics, [Documentation](https://ubiquitydotnet.github.io/Ubiquity.NET.Utils/CommandLine/diagnostics/UNC003.html) +UNC004 | Usage | Warning | Diagnostics, [Documentation](https://ubiquitydotnet.github.io/Ubiquity.NET.Utils/CommandLine/diagnostics/UNC004.html) +UNC005 | Usage | Error | Diagnostics, [Documentation](https://ubiquitydotnet.github.io/Ubiquity.NET.Utils/CommandLine/diagnostics/UNC005.html) diff --git a/src/Ubiquity.NET.CommandLine.SrcGen/CommandGenerator.cs b/src/Ubiquity.NET.CommandLine.SrcGen/CommandGenerator.cs index edb5752..fe0ef74 100644 --- a/src/Ubiquity.NET.CommandLine.SrcGen/CommandGenerator.cs +++ b/src/Ubiquity.NET.CommandLine.SrcGen/CommandGenerator.cs @@ -41,10 +41,10 @@ var optionClasses // 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.CSharp12 ) // C# 12 => .NET 8.0 => supported until 2026-11-10 (LTS) - || context.TargetSymbol is not INamedTypeSymbol namedTypeSymbol - || context.TargetNode is not ClassDeclarationSyntax commandClass + || compilation.Language != "C#" + || !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 ) { return null; diff --git a/src/Ubiquity.NET.CommandLine.SrcGen/CommandLineAnalyzer.cs b/src/Ubiquity.NET.CommandLine.SrcGen/CommandLineAnalyzer.cs index 16d196a..7f7f72c 100644 --- a/src/Ubiquity.NET.CommandLine.SrcGen/CommandLineAnalyzer.cs +++ b/src/Ubiquity.NET.CommandLine.SrcGen/CommandLineAnalyzer.cs @@ -64,7 +64,7 @@ private void OnTypeOrProperty( SymbolAnalysisContext context ) catch(Exception ex) { Location? loc = context.Symbol.Locations.Length < 1 ? default : context.Symbol.Locations[0]; - ReportDiagnostic( context, Diagnostics.InternalError, loc, ex.Message ); + context.ReportDiagnostic( Diagnostics.InternalError( loc, ex ) ); } } @@ -117,6 +117,8 @@ EquatableAttributeDataCollection attributes EquatableAttributeData attribute = attributes[Constants.OptionAttribute]; VerifyNotNullableRequired( context, symbol, attribute, attribLoc ); + VerifyArity( context, symbol, attribLoc, attribute ); + // Additional validations... } @@ -175,7 +177,7 @@ private static void VerifyCommandAttribute( SymbolAnalysisContext context, Locat var parentAttributes = context.Symbol.ContainingType.MatchingAttributes([Constants.RootCommandAttribute]); if(parentAttributes.IsDefaultOrEmpty) { - ReportDiagnostic( context, Diagnostics.MissingCommandAttribute, attribLoc, attribName ); + context.ReportDiagnostic( Diagnostics.MissingCommandAttribute( attribLoc, attribName ) ); } } @@ -199,7 +201,7 @@ string typeConstraintName // Verify an Option property if(!attribs.TryGetValue( Constants.OptionAttribute, out _ )) { - ReportDiagnostic( context, Diagnostics.MissingConstraintAttribute, attribLoc, typeConstraintName ); + context.ReportDiagnostic( Diagnostics.MissingConstraintAttribute( attribLoc, typeConstraintName ) ); } // TODO: validate an Argument attribute or Option attribute @@ -226,7 +228,7 @@ string attribName { if(symbol.Type.GetNamespaceQualifiedName() != expectedType) { - ReportDiagnostic( context, Diagnostics.IncorrectPropertyType, attribLoc, attribName, expectedType.ToString( "A", null ) ); + context.ReportDiagnostic( Diagnostics.IncorrectPropertyType( attribLoc, attribName, expectedType.ToString( "A", null ) ) ); } } @@ -242,14 +244,27 @@ private static void VerifyNotNullableRequired( NamespaceQualifiedTypeName propType = property.Type.GetNamespaceQualifiedName(); if(isRequired && propType.IsNullable) { - ReportDiagnostic( context, Diagnostics.RequiredNullableType, attribLoc, $"{propType:A}", property.Name ); + context.ReportDiagnostic( Diagnostics.RequiredNullableType( attribLoc, propType, property.Name ) ); } } } - private static void ReportDiagnostic( SymbolAnalysisContext context, DiagnosticDescriptor descriptor, Location? loc, params object[] args ) + private static void VerifyArity( SymbolAnalysisContext context, IPropertySymbol propSym, Location? attribLoc, EquatableAttributeData attribute ) { - context.ReportDiagnostic( Diagnostic.Create( descriptor, loc, args ) ); + // if arity is provided for non enumerable type, that's an error + // Except: 0|1 (normal default for an optional bool) + // Except: 1 (normal default for !bool && !collection) + var optionalArity = attribute.GetArity(); + if(optionalArity.HasValue) + { + (int minArity, int maxArity) = optionalArity.Value; + + // collectionRequired = arityMin > 1 || arityMax > 1; + if((minArity > 1 || maxArity > 1) && !propSym.Type.IsCollection()) + { + context.ReportDiagnostic( Diagnostics.PropertyTypeArityMismatch( attribLoc, propSym, minArity, maxArity )); + } + } } private static readonly SymbolHandlerMap SymbolHandlerMap diff --git a/src/Ubiquity.NET.CommandLine.SrcGen/CommonAttributeData.cs b/src/Ubiquity.NET.CommandLine.SrcGen/CommonAttributeData.cs new file mode 100644 index 0000000..8d987b1 --- /dev/null +++ b/src/Ubiquity.NET.CommandLine.SrcGen/CommonAttributeData.cs @@ -0,0 +1,24 @@ +// 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 +{ + internal static class CommonAttributeData + { + public static Optional IsRequired( this EquatableAttributeData self ) + { + return self.GetNamedArgValue( Constants.OptionAttributeNamedArgs.Required ); + } + + public static Optional<(int Min, int Max)> GetArity( this EquatableAttributeData self ) + { + // ignore argument if both aren't available + Optional min = self.GetNamedArgValue( Constants.CommonAttributeNamedArgs.ArityMin ); + Optional max = self.GetNamedArgValue( Constants.CommonAttributeNamedArgs.ArityMax ); + + return min.HasValue && max.HasValue + ? new( (min.Value, max.Value) ) + : default; + } + } +} diff --git a/src/Ubiquity.NET.CommandLine.SrcGen/Constants.cs b/src/Ubiquity.NET.CommandLine.SrcGen/Constants.cs index 3ae1e84..aa4eb12 100644 --- a/src/Ubiquity.NET.CommandLine.SrcGen/Constants.cs +++ b/src/Ubiquity.NET.CommandLine.SrcGen/Constants.cs @@ -81,6 +81,8 @@ internal static readonly ImmutableArray GeneratingAt internal static class CommonAttributeNamedArgs { internal const string Required = nameof(Required); + internal const string ArityMin = nameof(ArityMin); + internal const string ArityMax = nameof(ArityMax); } internal static class RootCommandAttributeNamedArgs diff --git a/src/Ubiquity.NET.CommandLine.SrcGen/Diagnostics.cs b/src/Ubiquity.NET.CommandLine.SrcGen/Diagnostics.cs index f14792b..b2dbdec 100644 --- a/src/Ubiquity.NET.CommandLine.SrcGen/Diagnostics.cs +++ b/src/Ubiquity.NET.CommandLine.SrcGen/Diagnostics.cs @@ -11,97 +11,162 @@ 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"; - internal const string RequiredNullableType = "UNC004"; - } - - private static LocalizableResourceString Localized( string resName ) - { - return new LocalizableResourceString( resName, Resources.ResourceManager, typeof( Resources ) ); + internal const string InternalErrorDescriptor = "UNC000"; + internal const string MissingCommandAttributeDescriptor = "UNC001"; + internal const string MissingConstraintAttributeDescriptor = "UNC002"; + internal const string IncorrectPropertyTypeDescriptor = "UNC003"; + internal const string RequiredNullableTypeDescriptor = "UNC004"; + internal const string PropertyTypeArityMismatchDescriptor = "UNC005"; } internal static ImmutableArray CommandLineAnalyzerDiagnostics => [ - InternalError, - MissingCommandAttribute, - MissingConstraintAttribute, - IncorrectPropertyType, - RequiredNullableType, + InternalErrorDescriptor, + MissingCommandAttributeDescriptor, + MissingConstraintAttributeDescriptor, + IncorrectPropertyTypeDescriptor, + RequiredNullableTypeDescriptor, + PropertyTypeArityMismatchDescriptor, ]; + internal static DiagnosticInfo InternalErrorInfo(Location? loc, Exception ex) + { + return new(InternalErrorDescriptor, loc, ex ); + } + + internal static Diagnostic InternalError( Location? loc, Exception ex ) + { + return Diagnostic.Create( InternalErrorDescriptor, loc, ex.Message ); + } + + internal static Diagnostic MissingCommandAttribute( Location? loc, string attributeName ) + { + return Diagnostic.Create( MissingCommandAttributeDescriptor, loc, attributeName ); + } + + internal static Diagnostic MissingConstraintAttribute( Location? loc, string attributeName ) + { + return Diagnostic.Create( MissingConstraintAttributeDescriptor, loc, attributeName ); + } + + internal static Diagnostic IncorrectPropertyType( + Location? loc, + string attributeName, + string expectedTypeName + ) + { + return Diagnostic.Create( IncorrectPropertyTypeDescriptor, loc, attributeName, expectedTypeName ); + } + + internal static Diagnostic RequiredNullableType( + Location? loc, + NamespaceQualifiedTypeName propertyType, + string propertyName + ) + { + string propertyTypeName = propertyType.ToString( "A", null ); + return Diagnostic.Create( RequiredNullableTypeDescriptor, loc, propertyTypeName, propertyName ); + } + + internal static Diagnostic PropertyTypeArityMismatch( + Location? loc, + IPropertySymbol propertySymbol, + int minArity, + int maxArity + ) + { + string propertyTypeName = propertySymbol.Type.GetNamespaceQualifiedName().ToString("A", null); + return Diagnostic.Create( PropertyTypeArityMismatchDescriptor, loc, propertySymbol.Name, propertyTypeName, minArity, maxArity ); + } + // Exception message is: '{0}' - internal static readonly DiagnosticDescriptor InternalError = new( - id: IDs.InternalError, + private static readonly DiagnosticDescriptor InternalErrorDescriptor = new( + id: IDs.InternalErrorDescriptor, 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)), - helpLinkUri: FormatHelpUri(IDs.InternalError), + helpLinkUri: FormatHelpUri(IDs.InternalErrorDescriptor), WellKnownDiagnosticTags.NotConfigurable ); // 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 readonly DiagnosticDescriptor MissingCommandAttribute = new( - id: IDs.MissingCommandAttribute, + private static readonly DiagnosticDescriptor MissingCommandAttributeDescriptor = new( + id: IDs.MissingCommandAttributeDescriptor, title: Localized(nameof(Resources.MissingCommandAttribute_Title)), messageFormat: Localized(nameof(Resources.MissingCommandAttribute_MessageFormat)), category: "Usage", defaultSeverity: DiagnosticSeverity.Error, isEnabledByDefault: true, description: Localized(nameof(Resources.MissingCommandAttribute_Description)), - helpLinkUri: FormatHelpUri(IDs.MissingCommandAttribute), + helpLinkUri: FormatHelpUri(IDs.MissingCommandAttributeDescriptor), WellKnownDiagnosticTags.Unnecessary, WellKnownDiagnosticTags.Compiler ); // Property attribute '{0}' is not allowed on a property independent of a qualifying attribute such as OptionAttribute. - internal static readonly DiagnosticDescriptor MissingConstraintAttribute = new( - id: IDs.MissingConstraintAttribute, + private static readonly DiagnosticDescriptor MissingConstraintAttributeDescriptor = new( + id: IDs.MissingConstraintAttributeDescriptor, 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)), - helpLinkUri: FormatHelpUri(IDs.MissingConstraintAttribute), + helpLinkUri: FormatHelpUri(IDs.MissingConstraintAttributeDescriptor), WellKnownDiagnosticTags.Compiler ); // Property attribute '{0}' requires a property of type '{1}'. - internal static readonly DiagnosticDescriptor IncorrectPropertyType = new( - id: IDs.IncorrectPropertyType, + private static readonly DiagnosticDescriptor IncorrectPropertyTypeDescriptor = new( + id: IDs.IncorrectPropertyTypeDescriptor, 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)), - helpLinkUri: FormatHelpUri(IDs.IncorrectPropertyType), + helpLinkUri: FormatHelpUri(IDs.IncorrectPropertyTypeDescriptor), WellKnownDiagnosticTags.Compiler, WellKnownDiagnosticTags.NotConfigurable ); // Type '{0}' for property '{1}' is nullable but marked as required; These annotations conflict resulting in behavior that is explicitly UNDEFINED. - internal static readonly DiagnosticDescriptor RequiredNullableType = new( - id: IDs.RequiredNullableType, + private static readonly DiagnosticDescriptor RequiredNullableTypeDescriptor = new( + id: IDs.RequiredNullableTypeDescriptor, title: Localized(nameof(Resources.RequiredNullableType_Title)), messageFormat: Localized(nameof(Resources.RequiredNullableType_MessageFormat)), category: "Usage", defaultSeverity: DiagnosticSeverity.Warning, isEnabledByDefault: true, description: Localized(nameof(Resources.RequiredNullableType_Description)), - helpLinkUri: FormatHelpUri(IDs.RequiredNullableType), + helpLinkUri: FormatHelpUri(IDs.RequiredNullableTypeDescriptor), + WellKnownDiagnosticTags.Compiler + ); + + // Property '{0}' has type of '{1}' which does not support an arity of ({2}, {3}). + private static readonly DiagnosticDescriptor PropertyTypeArityMismatchDescriptor = new( + id: IDs.PropertyTypeArityMismatchDescriptor, + title: Localized(nameof(Resources.PropertyTypeArityMismatch_Title)), + messageFormat: Localized(nameof(Resources.PropertyTypeArityMismatch_MessageFormat)), + category: "Usage", + defaultSeverity: DiagnosticSeverity.Error, + isEnabledByDefault: true, + description: Localized(nameof(Resources.PropertyTypeArityMismatch_Description)), + helpLinkUri: FormatHelpUri(IDs.PropertyTypeArityMismatchDescriptor), WellKnownDiagnosticTags.Compiler ); - private static string FormatHelpUri(string id) + private static string FormatHelpUri( string id ) { return $"https://ubiquitydotnet.github.io/Ubiquity.NET.Utils/CommandLine/diagnostics/{id}.html"; } + + private static LocalizableResourceString Localized( string resName ) + { + return new LocalizableResourceString( resName, Resources.ResourceManager, typeof( Resources ) ); + } } } diff --git a/src/Ubiquity.NET.CommandLine.SrcGen/OptionInfo.cs b/src/Ubiquity.NET.CommandLine.SrcGen/OptionInfo.cs index dcabdb8..b728682 100644 --- a/src/Ubiquity.NET.CommandLine.SrcGen/OptionInfo.cs +++ b/src/Ubiquity.NET.CommandLine.SrcGen/OptionInfo.cs @@ -38,23 +38,11 @@ public OptionInfo( EquatableAttributeData attributeInfo ) public Optional Description => AttributeInfo.GetNamedArgValue( Constants.OptionAttributeNamedArgs.Description ); - public Optional Required => AttributeInfo.GetNamedArgValue( Constants.OptionAttributeNamedArgs.Required ); + public Optional Required => AttributeInfo.IsRequired(); public Optional Hidden => AttributeInfo.GetNamedArgValue( Constants.OptionAttributeNamedArgs.Hidden ); - public Optional<(int Min, int Max)> Arity - { - get - { - // ignore argument if both aren't available - Optional min = AttributeInfo.GetNamedArgValue( Constants.OptionAttributeNamedArgs.ArityMin); - Optional max = AttributeInfo.GetNamedArgValue( Constants.OptionAttributeNamedArgs.ArityMax); - - return min.HasValue && max.HasValue - ? new((min.Value, max.Value)) - : default; - } - } + public Optional<(int Min, int Max)> Arity => AttributeInfo.GetArity(); private readonly EquatableAttributeData AttributeInfo; diff --git a/src/Ubiquity.NET.CommandLine.SrcGen/Properties/Resources.Designer.cs b/src/Ubiquity.NET.CommandLine.SrcGen/Properties/Resources.Designer.cs index 67b3508..d13b28d 100644 --- a/src/Ubiquity.NET.CommandLine.SrcGen/Properties/Resources.Designer.cs +++ b/src/Ubiquity.NET.CommandLine.SrcGen/Properties/Resources.Designer.cs @@ -168,6 +168,33 @@ internal static string MissingConstraintAttribute_Title { } } + /// + /// Looks up a localized string similar to Property type does not support the arity specified. + /// + internal static string PropertyTypeArityMismatch_Description { + get { + return ResourceManager.GetString("PropertyTypeArityMismatch_Description", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Property '{0}' has type of '{1}' which does not support an arity of ({2}, {3}).. + /// + internal static string PropertyTypeArityMismatch_MessageFormat { + get { + return ResourceManager.GetString("PropertyTypeArityMismatch_MessageFormat", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Arity specified for property type is invalid.. + /// + internal static string PropertyTypeArityMismatch_Title { + get { + return ResourceManager.GetString("PropertyTypeArityMismatch_Title", resourceCulture); + } + } + /// /// Looks up a localized string similar to Property type is nullable but marked as required; These annotations conflict resulting in behavior that is explicitly UNDEFINED.. /// @@ -178,7 +205,7 @@ internal static string RequiredNullableType_Description { } /// - /// Looks up a localized string similar to attrib.NamedArguments. + /// Looks up a localized string similar to Type '{0}' for property '{1}' is nullable but marked as required; These annotations conflict resulting in behavior that is explicitly UNDEFINED.. /// internal static string RequiredNullableType_MessageFormat { get { diff --git a/src/Ubiquity.NET.CommandLine.SrcGen/Properties/Resources.resx b/src/Ubiquity.NET.CommandLine.SrcGen/Properties/Resources.resx index 7a8f367..f3af761 100644 --- a/src/Ubiquity.NET.CommandLine.SrcGen/Properties/Resources.resx +++ b/src/Ubiquity.NET.CommandLine.SrcGen/Properties/Resources.resx @@ -163,4 +163,13 @@ Property type is nullable but marked as required. + + Property type does not support the arity specified + + + Property '{0}' has type of '{1}' which does not support an arity of ({2}, {3}). + + + Arity specified for property type is invalid. + diff --git a/src/Ubiquity.NET.CommandLine.SrcGen/Templates/RootCommandClassTemplate.cs b/src/Ubiquity.NET.CommandLine.SrcGen/Templates/RootCommandClassTemplate.cs index f54ad93..4e3fa53 100644 --- a/src/Ubiquity.NET.CommandLine.SrcGen/Templates/RootCommandClassTemplate.cs +++ b/src/Ubiquity.NET.CommandLine.SrcGen/Templates/RootCommandClassTemplate.cs @@ -41,6 +41,9 @@ public SourceText GenerateText( ) writer.WriteAutoGeneratedComment(ToolInfo.Name, ToolInfo.Version); writer.WriteEmptyLine(); + writer.WriteLine("#nullable enable"); + writer.WriteEmptyLine(); + writer.WriteLine($"using static {Constants.SymbolValidationExtensions:G};"); writer.WriteEmptyLine(); diff --git a/src/Ubiquity.NET.SourceGenerator.Test.Utils/CSharp/CachedSourceGeneratorTest.cs b/src/Ubiquity.NET.SourceGenerator.Test.Utils/CSharp/CachedSourceGeneratorTest.cs index f3f86e1..8c12a48 100644 --- a/src/Ubiquity.NET.SourceGenerator.Test.Utils/CSharp/CachedSourceGeneratorTest.cs +++ b/src/Ubiquity.NET.SourceGenerator.Test.Utils/CSharp/CachedSourceGeneratorTest.cs @@ -80,6 +80,7 @@ protected override async Task RunImplAsync( CancellationToken cancellationToken // 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 From ce0549f9d5748ea59fda0479175f24bfdcebb9cb Mon Sep 17 00:00:00 2001 From: Steven Maillet Date: Wed, 21 Jan 2026 09:24:20 -0800 Subject: [PATCH 2/2] Added nullability enable to all expected files * Allows nullability annotations for any nullable types --- .../Basic_golden_path_succeeds/expected.cs | 2 ++ .../RootCommandAttributeTests/DemoSource_succeeds/expected.cs | 2 ++ .../Generator_handles_nullable_types/expected.cs | 2 ++ 3 files changed, 6 insertions(+) diff --git a/src/Ubiquity.NET.CommandLine.SrcGen.UT/TestFiles/RootCommandAttributeTests/Basic_golden_path_succeeds/expected.cs b/src/Ubiquity.NET.CommandLine.SrcGen.UT/TestFiles/RootCommandAttributeTests/Basic_golden_path_succeeds/expected.cs index d0b442a..6f9b1fe 100644 --- a/src/Ubiquity.NET.CommandLine.SrcGen.UT/TestFiles/RootCommandAttributeTests/Basic_golden_path_succeeds/expected.cs +++ b/src/Ubiquity.NET.CommandLine.SrcGen.UT/TestFiles/RootCommandAttributeTests/Basic_golden_path_succeeds/expected.cs @@ -7,6 +7,8 @@ // the code is regenerated. // // ------------------------------------------------------------------------------ +#nullable enable + using static global::Ubiquity.NET.CommandLine.SymbolValidationExtensions; namespace TestNamespace diff --git a/src/Ubiquity.NET.CommandLine.SrcGen.UT/TestFiles/RootCommandAttributeTests/DemoSource_succeeds/expected.cs b/src/Ubiquity.NET.CommandLine.SrcGen.UT/TestFiles/RootCommandAttributeTests/DemoSource_succeeds/expected.cs index 1b5e765..e25d996 100644 --- a/src/Ubiquity.NET.CommandLine.SrcGen.UT/TestFiles/RootCommandAttributeTests/DemoSource_succeeds/expected.cs +++ b/src/Ubiquity.NET.CommandLine.SrcGen.UT/TestFiles/RootCommandAttributeTests/DemoSource_succeeds/expected.cs @@ -7,6 +7,8 @@ // the code is regenerated. // // ------------------------------------------------------------------------------ +#nullable enable + using static global::Ubiquity.NET.CommandLine.SymbolValidationExtensions; namespace TestNamespace diff --git a/src/Ubiquity.NET.CommandLine.SrcGen.UT/TestFiles/RootCommandAttributeTests/Generator_handles_nullable_types/expected.cs b/src/Ubiquity.NET.CommandLine.SrcGen.UT/TestFiles/RootCommandAttributeTests/Generator_handles_nullable_types/expected.cs index 3359011..9532ada 100644 --- a/src/Ubiquity.NET.CommandLine.SrcGen.UT/TestFiles/RootCommandAttributeTests/Generator_handles_nullable_types/expected.cs +++ b/src/Ubiquity.NET.CommandLine.SrcGen.UT/TestFiles/RootCommandAttributeTests/Generator_handles_nullable_types/expected.cs @@ -7,6 +7,8 @@ // the code is regenerated. // // ------------------------------------------------------------------------------ +#nullable enable + using static global::Ubiquity.NET.CommandLine.SymbolValidationExtensions; namespace TestNamespace