From 126fc655aa96e06f97e6649af8e863419b4ded1a Mon Sep 17 00:00:00 2001 From: dotnetKyle Date: Sat, 25 Oct 2025 13:12:33 -0400 Subject: [PATCH] Add analyzer warnings for command name issues --- .../Services/IntermediateCaGenerator.cs | 2 +- .../CommandSourceGenerator.cs | 21 ++++++++- .../DiagnosticErrors.cs | 45 +++++++++++++++++++ .../GeneratorBindingsProvider.cs | 27 +++++++++++ 4 files changed, 92 insertions(+), 3 deletions(-) create mode 100644 src/System.CommandLine.Minimal.SourceGenerator/DiagnosticErrors.cs diff --git a/src/DemoApp/Services/IntermediateCaGenerator.cs b/src/DemoApp/Services/IntermediateCaGenerator.cs index a2b4ec6..f1f0f31 100644 --- a/src/DemoApp/Services/IntermediateCaGenerator.cs +++ b/src/DemoApp/Services/IntermediateCaGenerator.cs @@ -6,7 +6,7 @@ namespace DemoApp.Services; public class IntermediateCaGenerator { - ISerialNumberProvider _serialNumberProvider; + readonly ISerialNumberProvider _serialNumberProvider; public IntermediateCaGenerator(ISerialNumberProvider serialNumberProvider) { _serialNumberProvider = serialNumberProvider; diff --git a/src/System.CommandLine.Minimal.SourceGenerator/CommandSourceGenerator.cs b/src/System.CommandLine.Minimal.SourceGenerator/CommandSourceGenerator.cs index c28514e..0df4630 100644 --- a/src/System.CommandLine.Minimal.SourceGenerator/CommandSourceGenerator.cs +++ b/src/System.CommandLine.Minimal.SourceGenerator/CommandSourceGenerator.cs @@ -1,6 +1,8 @@ using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp.Syntax; +using System.Collections.Generic; using System.Collections.Immutable; +using System.CommandLine.Minimal.SourceGenerator; using System.Threading; namespace System.CommandLine.Minimal.SourceGeneration; @@ -22,17 +24,32 @@ public void Initialize(IncrementalGeneratorInitializationContext context) ).Collect(); context.RegisterSourceOutput(bindersProvider, (spc, binders) => { - + HashSet commandNames = new(); + // first generate and create all of the CommandOptions classes foreach (GeneratingCommandBinder? binder in binders) { + if(binder.CommandName is null || string.IsNullOrWhiteSpace(binder.CommandName)) + { + spc.ReportCommandNameEmptyError(binder.CommandNameLocation); + continue; + } + if(commandNames.Contains(binder.CommandName)) + { + spc.ReportCommandNameConflict(binder.CommandNameLocation, binder.CommandName); + continue; + } + + // command name is validated so add to the hashset + commandNames.Add(binder.CommandName); + string? code = CommandOptionsWriter.GenerateOptions(binder); if(code is not null) { spc.AddSource($"{binder.ClassName}_{binder.MethodName}_Command.g.cs", code); } } - + // Emit the aggregated Register method string? registryCode = MapAllCommandsExtensionWriter.GenerateMapAllCommandsExt(binders); if(registryCode is not null) diff --git a/src/System.CommandLine.Minimal.SourceGenerator/DiagnosticErrors.cs b/src/System.CommandLine.Minimal.SourceGenerator/DiagnosticErrors.cs new file mode 100644 index 0000000..feb1d27 --- /dev/null +++ b/src/System.CommandLine.Minimal.SourceGenerator/DiagnosticErrors.cs @@ -0,0 +1,45 @@ +using Microsoft.CodeAnalysis; + +namespace System.CommandLine.Minimal.SourceGenerator +{ + internal static class DiagnosticErrors + { + const string CommandNameCategory = "Command Name"; + static readonly DiagnosticDescriptor CommandNameConflict = + new DiagnosticDescriptor( + "MIN0001", + "Command name conflict", + "Another Handler attribute already has this command name \"{0}\"", + CommandNameCategory, + DiagnosticSeverity.Error, + true + ); + static readonly DiagnosticDescriptor CommandNameEmpty = + new DiagnosticDescriptor( + "MIN0002", + "Command name empty", + "A command name is null or empty", + CommandNameCategory, + DiagnosticSeverity.Error, + true + ); + + /// + /// MIN0002 empty command name. + /// + internal static void ReportCommandNameEmptyError( + this SourceProductionContext ctx, + Location? location) + => ctx.ReportDiagnostic(Diagnostic.Create(CommandNameEmpty, location)); + + /// + /// MIN0001 duplicate command name. + /// + internal static void ReportCommandNameConflict( + this SourceProductionContext ctx, + Location? location, + string commandName) + => ctx.ReportDiagnostic(Diagnostic.Create(CommandNameConflict, location, commandName)); + + } +} diff --git a/src/System.CommandLine.Minimal.SourceGenerator/GeneratorBindingsProvider.cs b/src/System.CommandLine.Minimal.SourceGenerator/GeneratorBindingsProvider.cs index 37a6902..6dc9fd1 100644 --- a/src/System.CommandLine.Minimal.SourceGenerator/GeneratorBindingsProvider.cs +++ b/src/System.CommandLine.Minimal.SourceGenerator/GeneratorBindingsProvider.cs @@ -25,6 +25,31 @@ public static GeneratingCommandBinder Transform(GeneratorAttributeSyntaxContext && a.AttributeClass.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat) == "global::System.CommandLine.Minimal.HandlerAttribute"); string? commandName = handlerAttribute?.ConstructorArguments.FirstOrDefault().Value as string; + Location? argumentLocation = null; + if (ctx.TargetNode is MethodDeclarationSyntax methodDecl) + { + AttributeSyntax? handlerAttrSyntax = methodDecl.AttributeLists + .SelectMany(attrs => attrs.Attributes) + .FirstOrDefault(attr => ctx.SemanticModel + .GetTypeInfo(attr) + .Type? + .ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat) == "global::System.CommandLine.Minimal.HandlerAttribute" + ); + argumentLocation = handlerAttrSyntax?.ArgumentList?.Arguments.FirstOrDefault()?.GetLocation(); + } + //// Find the AttributeSyntax node for the HandlerAttribute + //AttributeSyntax? handlerAttributeSyntax = ctx.TargetNode switch + //{ + // MethodDeclarationSyntax methodDecl => methodDecl.AttributeLists + // .SelectMany(list => list.Attributes) + // .FirstOrDefault(attr => + // ctx.SemanticModel.GetTypeInfo(attr).Type?.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat) + // == "global::System.CommandLine.Minimal.HandlerAttribute"), + // _ => null + //}; + //Location? argumentLocation = handlerAttributeSyntax?.ArgumentList?.Arguments.FirstOrDefault()?.GetLocation(); + + string classNamespace = methodSymbol.ContainingNamespace.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); string className = methodSymbol.ContainingType.Name; string methodName = methodSymbol.Name; @@ -73,6 +98,7 @@ public static GeneratingCommandBinder Transform(GeneratorAttributeSyntaxContext MethodName: methodName, MethodReturnType: methodReturnType, MethodIsStatic: methodIsStatic, + CommandNameLocation: argumentLocation, Bindings: bindings.ToImmutableArray()); } @@ -221,6 +247,7 @@ internal record GeneratingCommandBinder( string MethodName, string MethodReturnType, bool MethodIsStatic, + Location? CommandNameLocation, ImmutableArray? Bindings ) {