Skip to content

Commit

Permalink
Warn when options type has custom help text generator, but no `--help…
Browse files Browse the repository at this point in the history
…` special command
  • Loading branch information
DoctorKrolic committed Apr 29, 2024
1 parent d4187e3 commit dfcc31f
Show file tree
Hide file tree
Showing 4 changed files with 238 additions and 6 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,4 @@ ARGP0044 | ArgumentParsing | Warning |
ARGP0045 | ArgumentParsing | Error |
ARGP0046 | ArgumentParsing | Error |
ARGP0047 | ArgumentParsing | Error |
ARGP0048 | ArgumentParsing | Warning |
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@ public sealed class ParserSignatureAnalyzer : DiagnosticAnalyzer
DiagnosticDescriptors.InvalidOptionsType,
DiagnosticDescriptors.OptionsTypeMustBeAnnotatedWithAttribute,
DiagnosticDescriptors.ParserArgumentIsASet,
DiagnosticDescriptors.InvalidSpecialCommandHandlerTypeSpecifier);
DiagnosticDescriptors.InvalidSpecialCommandHandlerTypeSpecifier,
DiagnosticDescriptors.OptionsTypeHasHelpTextGeneratorButNoHelpCommandHandlerInParser);

public override void Initialize(AnalysisContext context)
{
Expand All @@ -36,6 +37,8 @@ public override void Initialize(AnalysisContext context)
ParseResultOfTType = comp.ParseResultOfTType(),
OptionsTypeAttributeType = comp.OptionsTypeAttributeType(),
ISpecialCommandHandlerType = comp.ISpecialCommandHandlerType(),
SpecialCommandAliasesAttributeType = comp.SpecialCommandAliasesAttributeType(),
HelpTextGeneratorAttributeType = comp.HelpTextGeneratorAttributeType(),
};

context.RegisterSymbolAction(context => AnalyzeParserSignature(context, knownTypes), SymbolKind.Method);
Expand All @@ -52,11 +55,12 @@ private static void AnalyzeParserSignature(SymbolAnalysisContext context, KnownT
return;
}

var hasHelpCommand = true;
var iSpecialCommandHandlerType = knownTypes.ISpecialCommandHandlerType;

if (iSpecialCommandHandlerType is not null &&
generatedArgParserAttrData.NamedArguments
.FirstOrDefault(static n => n.Key == "SpecialCommandHandlers").Value is { IsNull: false, Values: { IsDefaultOrEmpty: false } specialCommandHandlers })
.FirstOrDefault(static n => n.Key == "SpecialCommandHandlers").Value is { IsNull: false, Values: var specialCommandHandlers })
{
var attributeSyntax = (AttributeSyntax?)generatedArgParserAttrData.ApplicationSyntaxReference?.GetSyntax(context.CancellationToken);
var specialCommandHandlersCollectionSyntax = attributeSyntax?.ArgumentList?.Arguments.First(static a => a.NameEquals?.Name.Identifier.ValueText == "SpecialCommandHandlers").Expression;
Expand All @@ -66,6 +70,8 @@ private static void AnalyzeParserSignature(SymbolAnalysisContext context, KnownT
? arrayCreation.Initializer?.Expressions
: null)).ToArray();

hasHelpCommand = false;

for (var i = 0; i < specialCommandHandlers.Length; i++)
{
var commandHandler = specialCommandHandlers[i];
Expand All @@ -85,6 +91,11 @@ private static void AnalyzeParserSignature(SymbolAnalysisContext context, KnownT
associatedSyntaxNode?.GetLocation() ?? attributeSyntax?.GetLocation() ?? method.Locations.First(),
commandHandler.IsNull ? (associatedSyntaxNode?.ToString() ?? "null") : commandHandler.Value));
}
else if (namedHandlerType.GetAttributes().FirstOrDefault(a => SymbolEqualityComparer.Default.Equals(a.AttributeClass, knownTypes.SpecialCommandAliasesAttributeType)) is { } aliasesAttr &&
aliasesAttr.ConstructorArguments is [{ Kind: TypedConstantKind.Array, Values: var aliases }])
{
hasHelpCommand |= aliases.Any(static a => (string?)a.Value == "--help");
}
}
}

Expand Down Expand Up @@ -174,11 +185,24 @@ private static void AnalyzeParserSignature(SymbolAnalysisContext context, KnownT
DiagnosticDescriptors.InvalidOptionsType, genericArgumentErrorSyntax.GetLocation()));
}
}
else if (!namedOptionsType.GetAttributes().Any(a => a.AttributeClass?.Equals(knownTypes.OptionsTypeAttributeType, SymbolEqualityComparer.Default) == true))
else
{
context.ReportDiagnostic(
Diagnostic.Create(
DiagnosticDescriptors.OptionsTypeMustBeAnnotatedWithAttribute, genericArgumentErrorSyntax.GetLocation()));
var attributes = namedOptionsType.GetAttributes();

if (!attributes.Any(a => SymbolEqualityComparer.Default.Equals(a.AttributeClass, knownTypes.OptionsTypeAttributeType)))
{
context.ReportDiagnostic(
Diagnostic.Create(
DiagnosticDescriptors.OptionsTypeMustBeAnnotatedWithAttribute, genericArgumentErrorSyntax.GetLocation()));
}
else if (!hasHelpCommand && attributes.Any(a => SymbolEqualityComparer.Default.Equals(a.AttributeClass, knownTypes.HelpTextGeneratorAttributeType)))
{
context.ReportDiagnostic(
Diagnostic.Create(
DiagnosticDescriptors.OptionsTypeHasHelpTextGeneratorButNoHelpCommandHandlerInParser,
method.Locations.First(),
namedOptionsType));
}
}
}

Expand All @@ -193,5 +217,9 @@ private readonly struct KnownTypes
public required INamedTypeSymbol? OptionsTypeAttributeType { get; init; }

public required INamedTypeSymbol? ISpecialCommandHandlerType { get; init; }

public required INamedTypeSymbol? SpecialCommandAliasesAttributeType { get; init; }

public required INamedTypeSymbol? HelpTextGeneratorAttributeType { get; init; }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -397,4 +397,12 @@ public static class DiagnosticDescriptors
category: ArgumentParsingCategoryName,
defaultSeverity: DiagnosticSeverity.Error,
isEnabledByDefault: true);

public static readonly DiagnosticDescriptor OptionsTypeHasHelpTextGeneratorButNoHelpCommandHandlerInParser = new(
id: "ARGP0048",
title: "Associated options type has a custom help text generator, but no '--help' special command handler found for the parser",
messageFormat: "Associated options type '{0}' has a custom help text generator, but no '--help' special command handler found for the parser",
category: ArgumentParsingCategoryName,
defaultSeverity: DiagnosticSeverity.Warning,
isEnabledByDefault: true);
}
195 changes: 195 additions & 0 deletions tests/ArgumentParsing.Tests.Unit/ParserSignatureAnalyzerTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -714,4 +714,199 @@ class InfoSpecialCommandHandler : ISpecialCommandHandler

await VerifyAnalyzerAsync(source);
}

[Fact]
public async Task SpecialCommandHandlers_HelpTextGeneratorButNoHelpCommand1()
{
var source = """
partial class C
{
[GeneratedArgumentParser(SpecialCommandHandlers = [])]
public static partial ParseResult<MyOptions> {|ARGP0048:{|CS8795:ParseArguments|}|}(string[] args);
}
[OptionsType, HelpTextGenerator(typeof(MyOptions), "GenerateHelpText")]
class MyOptions
{
public static string GenerateHelpText(ParseErrorCollection errors = null) => "";
}
""";

await VerifyAnalyzerAsync(source);
}

[Fact]
public async Task SpecialCommandHandlers_HelpTextGeneratorButNoHelpCommand2()
{
var source = """
partial class C
{
[GeneratedArgumentParser(SpecialCommandHandlers = [typeof(MySpecialCommandHandler)])]
public static partial ParseResult<MyOptions> {|ARGP0048:{|CS8795:ParseArguments|}|}(string[] args);
}
[OptionsType, HelpTextGenerator(typeof(MyOptions), "GenerateHelpText")]
class MyOptions
{
public static string GenerateHelpText(ParseErrorCollection errors = null) => "";
}
class MySpecialCommandHandler : ISpecialCommandHandler
{
public int HandleCommand() => 0;
}
""";

await VerifyAnalyzerAsync(source);
}

[Fact]
public async Task SpecialCommandHandlers_HelpTextGeneratorButNoHelpCommand3()
{
var source = """
partial class C
{
[GeneratedArgumentParser(SpecialCommandHandlers = [typeof(MySpecialCommandHandler)])]
public static partial ParseResult<MyOptions> {|ARGP0048:{|CS8795:ParseArguments|}|}(string[] args);
}
[OptionsType, HelpTextGenerator(typeof(MyOptions), "GenerateHelpText")]
class MyOptions
{
public static string GenerateHelpText(ParseErrorCollection errors = null) => "";
}
[SpecialCommandAliases]
class MySpecialCommandHandler : ISpecialCommandHandler
{
public int HandleCommand() => 0;
}
""";

await VerifyAnalyzerAsync(source);
}

[Fact]
public async Task SpecialCommandHandlers_HelpTextGeneratorButNoHelpCommand4()
{
var source = """
partial class C
{
[GeneratedArgumentParser(SpecialCommandHandlers = [typeof(MySpecialCommandHandler)])]
public static partial ParseResult<MyOptions> {|ARGP0048:{|CS8795:ParseArguments|}|}(string[] args);
}
[OptionsType, HelpTextGenerator(typeof(MyOptions), "GenerateHelpText")]
class MyOptions
{
public static string GenerateHelpText(ParseErrorCollection errors = null) => "";
}
[SpecialCommandAliases("--not-help")]
class MySpecialCommandHandler : ISpecialCommandHandler
{
public int HandleCommand() => 0;
}
""";

await VerifyAnalyzerAsync(source);
}

[Fact]
public async Task SpecialCommandHandlers_HelpTextGeneratorButNoHelpCommand5()
{
var source = """
partial class C
{
[GeneratedArgumentParser(SpecialCommandHandlers = [typeof(MySpecialCommandHandler)])]
public static partial ParseResult<MyOptions> {|ARGP0048:{|CS8795:ParseArguments|}|}(string[] args);
}
[OptionsType, HelpTextGenerator(typeof(MyOptions), "GenerateHelpText")]
class MyOptions
{
public static string GenerateHelpText(ParseErrorCollection errors = null) => "";
}
[SpecialCommandAliases("--not-help-1", "--not-help-2")]
class MySpecialCommandHandler : ISpecialCommandHandler
{
public int HandleCommand() => 0;
}
""";

await VerifyAnalyzerAsync(source);
}

[Fact]
public async Task SpecialCommandHandlers_HelpTextGeneratorAndHelpCommand1()
{
var source = """
partial class C
{
[GeneratedArgumentParser]
public static partial ParseResult<MyOptions> {|CS8795:ParseArguments|}(string[] args);
}
[OptionsType, HelpTextGenerator(typeof(MyOptions), "GenerateHelpText")]
class MyOptions
{
public static string GenerateHelpText(ParseErrorCollection errors = null) => "";
}
""";

await VerifyAnalyzerAsync(source);
}

[Fact]
public async Task SpecialCommandHandlers_HelpTextGeneratorAndHelpCommand2()
{
var source = """
partial class C
{
[GeneratedArgumentParser(SpecialCommandHandlers = [typeof(MySpecialCommandHandler)])]
public static partial ParseResult<MyOptions> {|CS8795:ParseArguments|}(string[] args);
}
[OptionsType, HelpTextGenerator(typeof(MyOptions), "GenerateHelpText")]
class MyOptions
{
public static string GenerateHelpText(ParseErrorCollection errors = null) => "";
}
[SpecialCommandAliases("--help")]
class MySpecialCommandHandler : ISpecialCommandHandler
{
public int HandleCommand() => 0;
}
""";

await VerifyAnalyzerAsync(source);
}

[Fact]
public async Task SpecialCommandHandlers_HelpTextGeneratorAndHelpCommand3()
{
var source = """
partial class C
{
[GeneratedArgumentParser(SpecialCommandHandlers = [typeof(MySpecialCommandHandler)])]
public static partial ParseResult<MyOptions> {|CS8795:ParseArguments|}(string[] args);
}
[OptionsType, HelpTextGenerator(typeof(MyOptions), "GenerateHelpText")]
class MyOptions
{
public static string GenerateHelpText(ParseErrorCollection errors = null) => "";
}
[SpecialCommandAliases("-?", "--help")]
class MySpecialCommandHandler : ISpecialCommandHandler
{
public int HandleCommand() => 0;
}
""";

await VerifyAnalyzerAsync(source);
}
}

0 comments on commit dfcc31f

Please sign in to comment.