diff --git a/src/ArgumentParsing.CodeFixes/DeclareSpecialCommandAliasCodeFixProvider.cs b/src/ArgumentParsing.CodeFixes/DeclareSpecialCommandAliasCodeFixProvider.cs new file mode 100644 index 0000000..093d24e --- /dev/null +++ b/src/ArgumentParsing.CodeFixes/DeclareSpecialCommandAliasCodeFixProvider.cs @@ -0,0 +1,181 @@ +using System.Collections.Immutable; +using System.Composition; +using System.Diagnostics; +using ArgumentParsing.Generators.Diagnostics; +using ArgumentParsing.Generators.Diagnostics.Analyzers; +using ArgumentParsing.Generators.Extensions; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CodeActions; +using Microsoft.CodeAnalysis.CodeFixes; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Editing; + +namespace ArgumentParsing.CodeFixes; + +[ExportCodeFixProvider(LanguageNames.CSharp), Shared] +public sealed class DeclareSpecialCommandAliasCodeFixProvider : CodeFixProvider +{ + public override ImmutableArray FixableDiagnosticIds { get; } = ImmutableArray.Create(DiagnosticDescriptors.SpecialCommandHandlerMustHaveAliases.Id); + + public override FixAllProvider? GetFixAllProvider() + => WellKnownFixAllProviders.BatchFixer; + + public override async Task RegisterCodeFixesAsync(CodeFixContext context) + { + var document = context.Document; + var cancellationToken = context.CancellationToken; + + var root = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false); + if (root?.FindNode(context.Span) is not TypeDeclarationSyntax commandHandlerDeclaration) + { + return; + } + + var diagnostic = context.Diagnostics[0]; + var diagType = diagnostic.Properties["Type"]; + if (diagType == SpecialCommandHandlerAnalyzer.SpecialCommandHandlerMustHaveAliasesDiagnosticTypes.NoAttribute) + { + context.RegisterCodeFix( + CodeAction.Create( + "Add [SpecialCommandAliases] attribute", + ct => AddAliasesAttribute(document, root, commandHandlerDeclaration, ct), + $"{nameof(DeclareSpecialCommandAliasCodeFixProvider)}_{nameof(AddAliasesAttribute)}"), + diagnostic); + } + else + { + var semanticModel = await document.GetSemanticModelAsync(cancellationToken).ConfigureAwait(false); + if (semanticModel?.GetDeclaredSymbol(commandHandlerDeclaration) is not { } commandHandler || + semanticModel.Compilation.SpecialCommandAliasesAttributeType() is not { } specialCommandAliasesAttributeType) + { + return; + } + + var aliasesAttributeReference = commandHandler + .GetAttributes() + .First(a => a.AttributeClass?.Equals(specialCommandAliasesAttributeType, SymbolEqualityComparer.Default) == true) + .ApplicationSyntaxReference; + + if (aliasesAttributeReference is null) + { + return; + } + + var attributeSyntax = (AttributeSyntax)(await aliasesAttributeReference.GetSyntaxAsync(cancellationToken).ConfigureAwait(false)); + var attributeDocument = document.Project.Solution.GetDocument(attributeSyntax.SyntaxTree)!; + + switch (diagType) + { + case SpecialCommandHandlerAnalyzer.SpecialCommandHandlerMustHaveAliasesDiagnosticTypes.NullValues: + context.RegisterCodeFix( + CodeAction.Create( + "Replace 'null' with valid alias", + ct => ChangeToValidAliasFromNullValue(attributeDocument, commandHandler.Name, attributeSyntax, ct), + $"{nameof(DeclareSpecialCommandAliasCodeFixProvider)}_{nameof(ChangeToValidAliasFromNullValue)}"), + diagnostic); + break; + case SpecialCommandHandlerAnalyzer.SpecialCommandHandlerMustHaveAliasesDiagnosticTypes.EmptyValues: + context.RegisterCodeFix( + CodeAction.Create( + "Add command alias", + ct => AddAliasToAttributeArguments(attributeDocument, commandHandler.Name, attributeSyntax, ct), + $"{nameof(DeclareSpecialCommandAliasCodeFixProvider)}_{nameof(AddAliasToAttributeArguments)}"), + diagnostic); + break; + } + } + } + + private static async Task AddAliasesAttribute(Document document, SyntaxNode root, TypeDeclarationSyntax commandHandlerDeclaration, CancellationToken cancellationToken) + { + var semanticModel = await document.GetSemanticModelAsync(cancellationToken).ConfigureAwait(false); + if (semanticModel?.Compilation.SpecialCommandAliasesAttributeType() is not { } specialCommandAliasesAttributeType) + { + Debug.Fail("Shouldn't really happen"); + return document; + } + + var generator = SyntaxGenerator.GetGenerator(document); + + var attributeArgument = + generator.AttributeArgument( + generator.LiteralExpression( + GetBestCommandAliasPrediction(commandHandlerDeclaration.Identifier.ValueText))); + var attribute = generator.Attribute(generator.TypeExpression(specialCommandAliasesAttributeType), [attributeArgument]); + + var fixedDeclaration = generator.AddAttributes(commandHandlerDeclaration, attribute); + var fixedRoot = root.ReplaceNode(commandHandlerDeclaration, fixedDeclaration); + return document.WithSyntaxRoot(fixedRoot); + } + + private static async Task ChangeToValidAliasFromNullValue(Document document, string commandHandlerName, AttributeSyntax attribute, CancellationToken cancellationToken) + { + var generator = SyntaxGenerator.GetGenerator(document); + var attributeArgument = + generator.AttributeArgument( + generator.LiteralExpression( + GetBestCommandAliasPrediction(commandHandlerName))); + + var argumentList = attribute.ArgumentList!; + var arguments = argumentList.Arguments; + var originalArgument = arguments.First(); + + var fixedAttribute = attribute + .WithArgumentList( + argumentList.WithArguments( + arguments.Replace(originalArgument, (AttributeArgumentSyntax)attributeArgument))); + + var root = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false); + var fixedRoot = root!.ReplaceNode(attribute, fixedAttribute); + return document.WithSyntaxRoot(fixedRoot).Project.Solution; + } + + private static async Task AddAliasToAttributeArguments(Document document, string commandHandlerName, AttributeSyntax attribute, CancellationToken cancellationToken) + { + var generator = SyntaxGenerator.GetGenerator(document); + var attributeArgument = + generator.AttributeArgument( + generator.LiteralExpression( + GetBestCommandAliasPrediction(commandHandlerName))); + + var argumentList = attribute.ArgumentList; + AttributeSyntax fixedAttribute; + if (argumentList is null or { Arguments.Count: 0 }) + { + fixedAttribute = ((AttributeListSyntax)generator.AddAttributeArguments(attribute, [attributeArgument])).Attributes.First(); + } + else + { + var arguments = argumentList.Arguments; + var originalArgument = arguments.First(); + + fixedAttribute = attribute + .WithArgumentList( + argumentList.WithArguments( + arguments.Replace(originalArgument, (AttributeArgumentSyntax)attributeArgument))); + } + + var root = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false); + var fixedRoot = root!.ReplaceNode(attribute, fixedAttribute); + return document.WithSyntaxRoot(fixedRoot).Project.Solution; + } + + private static string GetBestCommandAliasPrediction(string typeName) + { + const string specialCommandHandlerSuffix = "SpecialCommandHandler"; + const string commandHandlerSuffix = "CommandHandler"; + + if (typeName.EndsWith(specialCommandHandlerSuffix)) + { + typeName = typeName.Substring(0, typeName.Length - specialCommandHandlerSuffix.Length); + } + + if (typeName.EndsWith(commandHandlerSuffix)) + { + typeName = typeName.Substring(0, typeName.Length - commandHandlerSuffix.Length); + } + + return $"--{typeName.ToKebabCase()}"; + } +} diff --git a/src/ArgumentParsing.CodeFixes/WrapReturnTypeIntoParseResultCodeFixProvider.cs b/src/ArgumentParsing.CodeFixes/WrapReturnTypeIntoParseResultCodeFixProvider.cs index b08195e..2ec43ec 100644 --- a/src/ArgumentParsing.CodeFixes/WrapReturnTypeIntoParseResultCodeFixProvider.cs +++ b/src/ArgumentParsing.CodeFixes/WrapReturnTypeIntoParseResultCodeFixProvider.cs @@ -41,14 +41,7 @@ public override async Task RegisterCodeFixesAsync(CodeFixContext context) private static async Task WrapReturnTypeIntoParseResult(Document document, SyntaxNode root, TypeSyntax returnTypeSyntax, CancellationToken cancellationToken) { var semanticModel = await document.GetSemanticModelAsync(cancellationToken).ConfigureAwait(false); - if (semanticModel is null) - { - Debug.Fail("Shouldn't really happen"); - return document; - } - - var parseResultOfTType = semanticModel.Compilation.ParseResultOfTType(); - if (parseResultOfTType is null) + if (semanticModel?.Compilation.ParseResultOfTType() is not { } parseResultOfTType) { Debug.Fail("Shouldn't really happen"); return document; diff --git a/src/ArgumentParsing.Generators/Extensions/StringExtensions.cs b/src/ArgumentParsing.Generators/Extensions/StringExtensions.cs index 2e0a6f4..a0e1120 100644 --- a/src/ArgumentParsing.Generators/Extensions/StringExtensions.cs +++ b/src/ArgumentParsing.Generators/Extensions/StringExtensions.cs @@ -1,6 +1,6 @@ namespace ArgumentParsing.Generators.Extensions; -internal static class StringExtensions +public static class StringExtensions { public static string ToKebabCase(this string s) { diff --git a/tests/ArgumentParsing.Tests.Unit/SpecialCommandHandlerAnalyzerTests.cs b/tests/ArgumentParsing.Tests.Unit/SpecialCommandHandlerAnalyzerTests.cs index 8b9e9f5..4805f6c 100644 --- a/tests/ArgumentParsing.Tests.Unit/SpecialCommandHandlerAnalyzerTests.cs +++ b/tests/ArgumentParsing.Tests.Unit/SpecialCommandHandlerAnalyzerTests.cs @@ -96,72 +96,433 @@ partial class InfoCommandHandler await VerifyAnalyzerWithCodeFixAsync(source, fixedSource); } - [Fact] - public async Task NoAliases_NoAttribute() + [Theory] + [InlineData("Info")] + [InlineData("InfoCommandHandler")] + [InlineData("InfoSpecialCommandHandler")] + public async Task NoAliases_NoAttribute(string handlerName) { - var source = """ - class {|ARGP0039:InfoCommandHandler|} : ISpecialCommandHandler + var source = $$""" + class {|ARGP0039:{{handlerName}}|} : ISpecialCommandHandler { public int HandleCommand() => 0; } """; - await VerifyAnalyzerAsync(source); + var fixedSource = $$""" + [SpecialCommandAliases("--info")] + class {{handlerName}} : ISpecialCommandHandler + { + public int HandleCommand() => 0; + } + """; + + await VerifyAnalyzerWithCodeFixAsync(source, fixedSource); } - [Fact] - public async Task NoAliases_NullValuesInAttribute() + [Theory] + [InlineData("Info")] + [InlineData("InfoCommandHandler")] + [InlineData("InfoSpecialCommandHandler")] + public async Task NoAliases_NullValuesInAttribute(string handlerName) { - var source = """ + var source = $$""" [SpecialCommandAliases(null)] - class {|ARGP0039:InfoCommandHandler|} : ISpecialCommandHandler + class {|ARGP0039:{{handlerName}}|} : ISpecialCommandHandler { public int HandleCommand() => 0; } """; - await VerifyAnalyzerAsync(source); + var fixedSource = $$""" + [SpecialCommandAliases("--info")] + class {{handlerName}} : ISpecialCommandHandler + { + public int HandleCommand() => 0; + } + """; + + await VerifyAnalyzerWithCodeFixAsync(source, fixedSource); } - [Fact] - public async Task NoAliases_NoValuesInAttribute1() + [Theory] + [InlineData("Info")] + [InlineData("InfoCommandHandler")] + [InlineData("InfoSpecialCommandHandler")] + public async Task NoAliases_NullValuesInAttribute_AttributeOnAnotherPartialDeclaration(string handlerName) { - var source = """ + var source = $$""" + partial class {|ARGP0039:{{handlerName}}|} : ISpecialCommandHandler + { + public int HandleCommand() => 0; + } + + [SpecialCommandAliases(null)] + partial class {{handlerName}} + { + } + """; + + var fixedSource = $$""" + partial class {|ARGP0039:{{handlerName}}|} : ISpecialCommandHandler + { + public int HandleCommand() => 0; + } + + [SpecialCommandAliases("--info")] + partial class {{handlerName}} + { + } + """; + + await VerifyAnalyzerWithCodeFixAsync(source, fixedSource); + } + + [Theory] + [InlineData("Info")] + [InlineData("InfoCommandHandler")] + [InlineData("InfoSpecialCommandHandler")] + public async Task NoAliases_NoValuesInAttribute1(string handlerName) + { + var source = $$""" [SpecialCommandAliases] - class {|ARGP0039:InfoCommandHandler|} : ISpecialCommandHandler + class {|ARGP0039:{{handlerName}}|} : ISpecialCommandHandler { public int HandleCommand() => 0; } """; - await VerifyAnalyzerAsync(source); + var fixedSource = $$""" + [SpecialCommandAliases("--info")] + class {|ARGP0039:{{handlerName}}|} : ISpecialCommandHandler + { + public int HandleCommand() => 0; + } + """; + + await VerifyAnalyzerWithCodeFixAsync(source, fixedSource); } - [Fact] - public async Task NoAliases_NoValuesInAttribute2() + [Theory] + [InlineData("Info")] + [InlineData("InfoCommandHandler")] + [InlineData("InfoSpecialCommandHandler")] + public async Task NoAliases_NoValuesInAttribute1_MultipleAttributesInTheList(string handlerName) { - var source = """ + var source = $$""" + [Obsolete, SpecialCommandAliases] + class {|ARGP0039:{{handlerName}}|} : ISpecialCommandHandler + { + public int HandleCommand() => 0; + } + """; + + var fixedSource = $$""" + [Obsolete, SpecialCommandAliases("--info")] + class {|ARGP0039:{{handlerName}}|} : ISpecialCommandHandler + { + public int HandleCommand() => 0; + } + """; + + await VerifyAnalyzerWithCodeFixAsync(source, fixedSource); + } + + [Theory] + [InlineData("Info")] + [InlineData("InfoCommandHandler")] + [InlineData("InfoSpecialCommandHandler")] + public async Task NoAliases_NoValuesInAttribute1_AttributeOnAnotherPartialDeclaration(string handlerName) + { + var source = $$""" + partial class {|ARGP0039:{{handlerName}}|} : ISpecialCommandHandler + { + public int HandleCommand() => 0; + } + + [SpecialCommandAliases] + partial class {{handlerName}} + { + } + """; + + var fixedSource = $$""" + partial class {|ARGP0039:{{handlerName}}|} : ISpecialCommandHandler + { + public int HandleCommand() => 0; + } + + [SpecialCommandAliases("--info")] + partial class {{handlerName}} + { + } + """; + + await VerifyAnalyzerWithCodeFixAsync(source, fixedSource); + } + + [Theory] + [InlineData("Info")] + [InlineData("InfoCommandHandler")] + [InlineData("InfoSpecialCommandHandler")] + public async Task NoAliases_NoValuesInAttribute1_AttributeOnAnotherPartialDeclaration_MultipleAttributesInTheList(string handlerName) + { + var source = $$""" + partial class {|ARGP0039:{{handlerName}}|} : ISpecialCommandHandler + { + public int HandleCommand() => 0; + } + + [Obsolete, SpecialCommandAliases] + partial class {{handlerName}} + { + } + """; + + var fixedSource = $$""" + partial class {|ARGP0039:{{handlerName}}|} : ISpecialCommandHandler + { + public int HandleCommand() => 0; + } + + [Obsolete, SpecialCommandAliases("--info")] + partial class {{handlerName}} + { + } + """; + + await VerifyAnalyzerWithCodeFixAsync(source, fixedSource); + } + + [Theory] + [InlineData("Info")] + [InlineData("InfoCommandHandler")] + [InlineData("InfoSpecialCommandHandler")] + public async Task NoAliases_NoValuesInAttribute2(string handlerName) + { + var source = $$""" [SpecialCommandAliases()] - class {|ARGP0039:InfoCommandHandler|} : ISpecialCommandHandler + class {|ARGP0039:{{handlerName}}|} : ISpecialCommandHandler { public int HandleCommand() => 0; } """; - await VerifyAnalyzerAsync(source); + var fixedSource = $$""" + [SpecialCommandAliases("--info")] + class {|ARGP0039:{{handlerName}}|} : ISpecialCommandHandler + { + public int HandleCommand() => 0; + } + """; + + await VerifyAnalyzerWithCodeFixAsync(source, fixedSource); } - [Fact] - public async Task NoAliases_NoValuesInAttribute3() + [Theory] + [InlineData("Info")] + [InlineData("InfoCommandHandler")] + [InlineData("InfoSpecialCommandHandler")] + public async Task NoAliases_NoValuesInAttribute2_MultipleAttributesInTheList(string handlerName) { - var source = """ + var source = $$""" + [Obsolete, SpecialCommandAliases()] + class {|ARGP0039:{{handlerName}}|} : ISpecialCommandHandler + { + public int HandleCommand() => 0; + } + """; + + var fixedSource = $$""" + [Obsolete, SpecialCommandAliases("--info")] + class {|ARGP0039:{{handlerName}}|} : ISpecialCommandHandler + { + public int HandleCommand() => 0; + } + """; + + await VerifyAnalyzerWithCodeFixAsync(source, fixedSource); + } + + [Theory] + [InlineData("Info")] + [InlineData("InfoCommandHandler")] + [InlineData("InfoSpecialCommandHandler")] + public async Task NoAliases_NoValuesInAttribute2_AttributeOnAnotherPartialDeclaration(string handlerName) + { + var source = $$""" + partial class {|ARGP0039:{{handlerName}}|} : ISpecialCommandHandler + { + public int HandleCommand() => 0; + } + + [SpecialCommandAliases()] + partial class {{handlerName}} + { + } + """; + + var fixedSource = $$""" + partial class {|ARGP0039:{{handlerName}}|} : ISpecialCommandHandler + { + public int HandleCommand() => 0; + } + + [SpecialCommandAliases("--info")] + partial class {{handlerName}} + { + } + """; + + await VerifyAnalyzerWithCodeFixAsync(source, fixedSource); + } + + [Theory] + [InlineData("Info")] + [InlineData("InfoCommandHandler")] + [InlineData("InfoSpecialCommandHandler")] + public async Task NoAliases_NoValuesInAttribute2_AttributeOnAnotherPartialDeclaration_MultipleAttributesInTheList(string handlerName) + { + var source = $$""" + partial class {|ARGP0039:{{handlerName}}|} : ISpecialCommandHandler + { + public int HandleCommand() => 0; + } + + [Obsolete, SpecialCommandAliases()] + partial class {{handlerName}} + { + } + """; + + var fixedSource = $$""" + partial class {|ARGP0039:{{handlerName}}|} : ISpecialCommandHandler + { + public int HandleCommand() => 0; + } + + [Obsolete, SpecialCommandAliases("--info")] + partial class {{handlerName}} + { + } + """; + + await VerifyAnalyzerWithCodeFixAsync(source, fixedSource); + } + + [Theory] + [InlineData("Info")] + [InlineData("InfoCommandHandler")] + [InlineData("InfoSpecialCommandHandler")] + public async Task NoAliases_NoValuesInAttribute3(string handlerName) + { + var source = $$""" [SpecialCommandAliases([])] - class {|ARGP0039:InfoCommandHandler|} : ISpecialCommandHandler + class {|ARGP0039:{{handlerName}}|} : ISpecialCommandHandler { public int HandleCommand() => 0; } """; - await VerifyAnalyzerAsync(source); + var fixedSource = $$""" + [SpecialCommandAliases("--info")] + class {|ARGP0039:{{handlerName}}|} : ISpecialCommandHandler + { + public int HandleCommand() => 0; + } + """; + + await VerifyAnalyzerWithCodeFixAsync(source, fixedSource); + } + + [Theory] + [InlineData("Info")] + [InlineData("InfoCommandHandler")] + [InlineData("InfoSpecialCommandHandler")] + public async Task NoAliases_NoValuesInAttribute3_MultipleAttributesInTheList(string handlerName) + { + var source = $$""" + [Obsolete, SpecialCommandAliases([])] + class {|ARGP0039:{{handlerName}}|} : ISpecialCommandHandler + { + public int HandleCommand() => 0; + } + """; + + var fixedSource = $$""" + [Obsolete, SpecialCommandAliases("--info")] + class {|ARGP0039:{{handlerName}}|} : ISpecialCommandHandler + { + public int HandleCommand() => 0; + } + """; + + await VerifyAnalyzerWithCodeFixAsync(source, fixedSource); + } + + [Theory] + [InlineData("Info")] + [InlineData("InfoCommandHandler")] + [InlineData("InfoSpecialCommandHandler")] + public async Task NoAliases_NoValuesInAttribute3_AttributeOnAnotherPartialDeclaration(string handlerName) + { + var source = $$""" + partial class {|ARGP0039:{{handlerName}}|} : ISpecialCommandHandler + { + public int HandleCommand() => 0; + } + + [SpecialCommandAliases([])] + partial class {{handlerName}} + { + } + """; + + var fixedSource = $$""" + partial class {|ARGP0039:{{handlerName}}|} : ISpecialCommandHandler + { + public int HandleCommand() => 0; + } + + [SpecialCommandAliases("--info")] + partial class {{handlerName}} + { + } + """; + + await VerifyAnalyzerWithCodeFixAsync(source, fixedSource); + } + + [Theory] + [InlineData("Info")] + [InlineData("InfoCommandHandler")] + [InlineData("InfoSpecialCommandHandler")] + public async Task NoAliases_NoValuesInAttribute3_AttributeOnAnotherPartialDeclaration_MultipleAttributesInTheList(string handlerName) + { + var source = $$""" + partial class {|ARGP0039:{{handlerName}}|} : ISpecialCommandHandler + { + public int HandleCommand() => 0; + } + + [Obsolete, SpecialCommandAliases([])] + partial class {{handlerName}} + { + } + """; + + var fixedSource = $$""" + partial class {|ARGP0039:{{handlerName}}|} : ISpecialCommandHandler + { + public int HandleCommand() => 0; + } + + [Obsolete, SpecialCommandAliases("--info")] + partial class {{handlerName}} + { + } + """; + + await VerifyAnalyzerWithCodeFixAsync(source, fixedSource); } }