Skip to content

Commit

Permalink
Rework special commands
Browse files Browse the repository at this point in the history
  • Loading branch information
DoctorKrolic authored Jul 25, 2024
1 parent 82246de commit 6ca3655
Show file tree
Hide file tree
Showing 16 changed files with 584 additions and 484 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,4 @@ ARGP0045 | ArgumentParsing | Error |
ARGP0046 | ArgumentParsing | Error |
ARGP0047 | ArgumentParsing | Error |
ARGP0048 | ArgumentParsing | Warning |
ARGP0049 | ArgumentParsing | Error |
43 changes: 22 additions & 21 deletions src/ArgumentParsing.Generators/ArgumentParserGenerator.CodeGen.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ private static void EmitArgumentParser(SourceProductionContext context, (Argumen

var cancellationToken = context.CancellationToken;

var (hierarchy, method, optionsInfo, specialCommandHandlers) = parserInfo;
var (hierarchy, method, optionsInfo, builtInCommandHandlers, additionalCommandHandlers) = parserInfo;
var (qualifiedName, hasAtLeastInternalAccessibility, optionInfos, parameterInfos, remainingParametersInfo, helpTextGeneratorInfo) = optionsInfo;

var writer = new CodeWriter();
Expand All @@ -29,10 +29,12 @@ private static void EmitArgumentParser(SourceProductionContext context, (Argumen
var generatorType = typeof(ArgumentParserGenerator);
var generatedCodeAttribute = $"[global::System.CodeDom.Compiler.GeneratedCodeAttribute(\"{generatorType.FullName}\", \"{generatorType.Assembly.GetName().Version}\")]";

if (hasAtLeastInternalAccessibility && (!specialCommandHandlers.HasValue) || helpTextGeneratorInfo is not null)
{
var hasSpecialCommandHandlers = !specialCommandHandlers.HasValue || !specialCommandHandlers.Value.IsEmpty;
var hasAnySpecialCommandHandlers = builtInCommandHandlers.HasFlag(BuiltInCommandHandlers.Help) ||
builtInCommandHandlers.HasFlag(BuiltInCommandHandlers.Version) ||
!additionalCommandHandlers.IsEmpty;

if (hasAtLeastInternalAccessibility && builtInCommandHandlers.HasFlag(BuiltInCommandHandlers.Help) || helpTextGeneratorInfo is not null)
{
writer.WriteLine("namespace ArgumentParsing.Generated");
writer.OpenBlock();
writer.WriteLine("internal static partial class ParseResultExtensions");
Expand All @@ -42,7 +44,7 @@ private static void EmitArgumentParser(SourceProductionContext context, (Argumen
writer.WriteLine("/// <list type=\"bullet\">");
writer.WriteLine("/// <item>If <paramref name=\"result\"/> is in <see cref=\"global::ArgumentParsing.Results.ParseResultState.ParsedOptions\"/> state invokes provided <paramref name=\"action\"/> with parsed options object</item>");
writer.WriteLine("/// <item>If <paramref name=\"result\"/> is in <see cref=\"global::ArgumentParsing.Results.ParseResultState.ParsedWithErrors\"/> state writes help screen text with parse errors to <see cref=\"global::System.Console.Error\"/> and exits application with code 1</item>");
if (hasSpecialCommandHandlers)
if (hasAnySpecialCommandHandlers)
{
writer.WriteLine("/// <item>If <paramref name=\"result\"/> is in <see cref=\"global::ArgumentParsing.Results.ParseResultState.ParsedSpecialCommand\"/> state executes parsed handler and exits application with code, returned from the handler</item>");
}
Expand Down Expand Up @@ -72,7 +74,7 @@ private static void EmitArgumentParser(SourceProductionContext context, (Argumen
writer.WriteLine("global::System.Console.Error.WriteLine(errorScreenText);");
writer.WriteLine("global::System.Environment.Exit(1);");
writer.WriteLine("break;");
if (hasSpecialCommandHandlers)
if (hasAnySpecialCommandHandlers)
{
writer.WriteLine("case global::ArgumentParsing.Results.ParseResultState.ParsedSpecialCommand:", identDelta: -1);
writer.WriteLine("int exitCode = result.SpecialCommandHandler.HandleCommand();");
Expand Down Expand Up @@ -134,7 +136,6 @@ private static void EmitArgumentParser(SourceProductionContext context, (Argumen

var hasAnyOptions = optionInfos.Length > 0;
var hasAnyParameters = parameterInfos.Length > 0;
var hasAnySpecialCommandHandlers = !specialCommandHandlers.HasValue || !specialCommandHandlers.Value.IsEmpty;

writer.WriteLine();
writer.WriteLine($"int state = {(hasAnySpecialCommandHandlers ? "-3" : "0")};");
Expand Down Expand Up @@ -172,30 +173,30 @@ private static void EmitArgumentParser(SourceProductionContext context, (Argumen
writer.OpenBlock();
writer.WriteLine("switch (arg)");
writer.OpenBlock();
if (specialCommandHandlers.HasValue)
{
foreach (var commandHandler in specialCommandHandlers.Value)
{
foreach (var alias in commandHandler.Aliases)
{
writer.WriteLine($"case \"{alias}\":");
}
writer.Ident++;
writer.WriteLine($"return new {method.ReturnType}(new {commandHandler.Type}());");
writer.Ident--;
}
}
else
if (builtInCommandHandlers.HasFlag(BuiltInCommandHandlers.Help))
{
writer.WriteLine("case \"--help\":");
writer.Ident++;
writer.WriteLine($"return new {method.ReturnType}(new global::ArgumentParsing.Generated.HelpCommandHandler_{qualifiedName.Replace('.', '_')}());");
writer.Ident--;
}
if (builtInCommandHandlers.HasFlag(BuiltInCommandHandlers.Version))
{
writer.WriteLine("case \"--version\":");
writer.Ident++;
writer.WriteLine($"return new {method.ReturnType}(new global::ArgumentParsing.Generated.VersionCommandHandler());");
writer.Ident--;
}
foreach (var commandHandler in additionalCommandHandlers)
{
foreach (var alias in commandHandler.Aliases)
{
writer.WriteLine($"case \"{alias}\":");
}
writer.Ident++;
writer.WriteLine($"return new {method.ReturnType}(new {commandHandler.Type}());");
writer.Ident--;
}
writer.CloseBlock();
writer.WriteLine();
writer.WriteLine("state = 0;");
Expand Down
37 changes: 31 additions & 6 deletions src/ArgumentParsing.Generators/ArgumentParserGenerator.Extract.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,15 +23,34 @@ public partial class ArgumentParserGenerator
var genArgParserAttrType = comp.GetTypeByMetadataName(GeneratedArgumentParserAttributeName)!;

var genArgParserAttrData = context.Attributes.First(a => SymbolEqualityComparer.Default.Equals(a.AttributeClass, genArgParserAttrType));
ImmutableArray<SpecialCommandHandlerInfo>.Builder? specialCommandHandlerInfosBuilder = null;
var builtInCommandHandlers = BuiltInCommandHandlers.Help | BuiltInCommandHandlers.Version;
var additionalCommandHandlerInfosBuilder = ImmutableArray.CreateBuilder<SpecialCommandHandlerInfo>();

if (genArgParserAttrData.NamedArguments.FirstOrDefault(static n => n.Key == "SpecialCommandHandlers").Value is { IsNull: false, Values: { IsDefault: false } specialCommandHandlers })
var namedArgs = genArgParserAttrData.NamedArguments;

if (namedArgs.FirstOrDefault(static n => n.Key == "BuiltInCommandHandlers").Value is { Value: byte builtInHandlersByte })
{
builtInCommandHandlers = (BuiltInCommandHandlers)builtInHandlersByte;
}

var registeredCommands = new HashSet<string>();

if (builtInCommandHandlers.HasFlag(BuiltInCommandHandlers.Help))
{
registeredCommands.Add("--help");
}

if (builtInCommandHandlers.HasFlag(BuiltInCommandHandlers.Version))
{
registeredCommands.Add("--version");
}

if (namedArgs.FirstOrDefault(static n => n.Key == "AdditionalCommandHandlers").Value is { IsNull: false, Values: { IsDefault: false } additionalCommandHandlers })
{
specialCommandHandlerInfosBuilder = ImmutableArray.CreateBuilder<SpecialCommandHandlerInfo>();
var iSpecialCommandHandlerType = comp.ISpecialCommandHandlerType();
var specialCommandAliasesAttributeType = comp.SpecialCommandAliasesAttributeType();

foreach (var commandHandler in specialCommandHandlers)
foreach (var commandHandler in additionalCommandHandlers)
{
if (commandHandler.Value is not INamedTypeSymbol commandHandlerType ||
!commandHandlerType.AllInterfaces.Any(i => i.Equals(iSpecialCommandHandlerType, SymbolEqualityComparer.Default)) ||
Expand Down Expand Up @@ -63,10 +82,15 @@ public partial class ArgumentParserGenerator
return null;
}

if (!registeredCommands.Add(aliasVal))
{
return null;
}

aliasesBuilder.Add(aliasVal);
}

specialCommandHandlerInfosBuilder.Add(new(
additionalCommandHandlerInfosBuilder.Add(new(
commandHandlerType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat),
aliasesBuilder.ToImmutable()));
}
Expand Down Expand Up @@ -133,7 +157,8 @@ public partial class ArgumentParserGenerator
HierarchyInfo.From(argumentParserMethodSymbol.ContainingType),
methodInfo,
optionsInfo,
specialCommandHandlerInfosBuilder?.ToImmutable());
builtInCommandHandlers,
additionalCommandHandlerInfosBuilder.ToImmutable());

return argumentParserInfo;
}
Expand Down
9 changes: 5 additions & 4 deletions src/ArgumentParsing.Generators/ArgumentParserGenerator.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using ArgumentParsing.Generators.Models;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;

Expand Down Expand Up @@ -30,19 +31,19 @@ public void Initialize(IncrementalGeneratorInitializationContext context)
.Select((info, _) => info.AssemblyVersionInfo);

var optionsHelpInfos = argumentParserInfos
.Where(info => !info.SpecialCommandHandlersInfos.HasValue)
.Where(info => info.BuiltInCommandHandlers.HasFlag(BuiltInCommandHandlers.Help))
.Select((info, _) => info.OptionsInfo)
.WithComparer(HelpOnlyOptionsInfoComparer.Instance)
.Combine(assemblyVersionInfo);

context.RegisterSourceOutput(optionsHelpInfos, EmitHelpCommandHandler);

// Candidate for `Any` API: https://github.com/dotnet/roslyn/issues/59690
var hasAnyParsersWithDefaultHandlers = argumentParserInfos
.Where(a => !a.SpecialCommandHandlersInfos.HasValue)
var hasAnyParsersWithBuiltInVersionHandlerHandlers = argumentParserInfos
.Where(info => info.BuiltInCommandHandlers.HasFlag(BuiltInCommandHandlers.Version))
.Collect()
.Select((parsers, _) => !parsers.IsEmpty);

context.RegisterSourceOutput(assemblyVersionInfo.Combine(hasAnyParsersWithDefaultHandlers), EmitVersionCommandHandler);
context.RegisterSourceOutput(assemblyVersionInfo.Combine(hasAnyParsersWithBuiltInVersionHandlerHandlers), EmitVersionCommandHandler);
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System.Collections.Immutable;
using ArgumentParsing.Generators.Extensions;
using ArgumentParsing.Generators.Models;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Diagnostics;
Expand All @@ -20,7 +21,8 @@ public sealed class ParserSignatureAnalyzer : DiagnosticAnalyzer
DiagnosticDescriptors.OptionsTypeMustBeAnnotatedWithAttribute,
DiagnosticDescriptors.ParserArgumentIsASet,
DiagnosticDescriptors.InvalidSpecialCommandHandlerTypeSpecifier,
DiagnosticDescriptors.OptionsTypeHasHelpTextGeneratorButNoHelpCommandHandlerInParser);
DiagnosticDescriptors.OptionsTypeHasHelpTextGeneratorButNoHelpCommandHandlerInParser,
DiagnosticDescriptors.DuplicateSpecialCommand);

public override void Initialize(AnalysisContext context)
{
Expand Down Expand Up @@ -55,26 +57,44 @@ private static void AnalyzeParserSignature(SymbolAnalysisContext context, KnownT
return;
}

var hasHelpCommand = true;
var hasHelpCommand = false;
var registeredCommands = new HashSet<string>();
var namedArgs = generatedArgParserAttrData.NamedArguments;

var builtInHandlers = BuiltInCommandHandlers.Help | BuiltInCommandHandlers.Version;

if (namedArgs.FirstOrDefault(static n => n.Key == "BuiltInCommandHandlers").Value is { Value: byte builtInHandlersByte })
{
builtInHandlers = (BuiltInCommandHandlers)builtInHandlersByte;
}

if (builtInHandlers.HasFlag(BuiltInCommandHandlers.Help))
{
hasHelpCommand = true;
registeredCommands.Add("--help");
}

if (builtInHandlers.HasFlag(BuiltInCommandHandlers.Version))
{
registeredCommands.Add("--version");
}

var iSpecialCommandHandlerType = knownTypes.ISpecialCommandHandlerType;

if (iSpecialCommandHandlerType is not null &&
generatedArgParserAttrData.NamedArguments
.FirstOrDefault(static n => n.Key == "SpecialCommandHandlers").Value is { IsNull: false, Values: var specialCommandHandlers })
namedArgs.FirstOrDefault(static n => n.Key == "AdditionalCommandHandlers").Value is { IsNull: false, Values: var additionalCommandHandlers })
{
var attributeSyntax = (AttributeSyntax?)generatedArgParserAttrData.ApplicationSyntaxReference?.GetSyntax(context.CancellationToken);
var specialCommandHandlersCollectionSyntax = attributeSyntax?.ArgumentList?.Arguments.First(static a => a.NameEquals?.Name.Identifier.ValueText == "SpecialCommandHandlers").Expression;
var namedParametersList = (specialCommandHandlersCollectionSyntax is CollectionExpressionSyntax collectionExpression
var additionalCommandHandlersCollectionSyntax = attributeSyntax?.ArgumentList?.Arguments.First(static a => a.NameEquals?.Name.Identifier.ValueText == "AdditionalCommandHandlers").Expression;
var namedParametersList = (additionalCommandHandlersCollectionSyntax is CollectionExpressionSyntax collectionExpression
? collectionExpression.Elements.Select(static ce => ((ExpressionElementSyntax)ce).Expression)
: (specialCommandHandlersCollectionSyntax is ArrayCreationExpressionSyntax arrayCreation
: (additionalCommandHandlersCollectionSyntax is ArrayCreationExpressionSyntax arrayCreation
? arrayCreation.Initializer?.Expressions
: null)).ToArray();

hasHelpCommand = false;

for (var i = 0; i < specialCommandHandlers.Length; i++)
for (var i = 0; i < additionalCommandHandlers.Length; i++)
{
var commandHandler = specialCommandHandlers[i];
var commandHandler = additionalCommandHandlers[i];

if (commandHandler is not { Value: INamedTypeSymbol namedHandlerType } ||
(namedHandlerType.TypeKind != TypeKind.Error && !namedHandlerType.AllInterfaces.Contains(iSpecialCommandHandlerType)))
Expand All @@ -94,7 +114,29 @@ private static void AnalyzeParserSignature(SymbolAnalysisContext context, KnownT
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");
foreach (var alias in aliases)
{
var aliasValue = (string?)alias.Value;

if (aliasValue is null)
{
continue;
}

if (aliasValue == "--help")
{
hasHelpCommand = true;
}

if (!registeredCommands.Add(aliasValue))
{
context.ReportDiagnostic(
Diagnostic.Create(
DiagnosticDescriptors.DuplicateSpecialCommand,
method.Locations.First(),
aliasValue));
}
}
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -405,4 +405,12 @@ public static class DiagnosticDescriptors
category: ArgumentParsingCategoryName,
defaultSeverity: DiagnosticSeverity.Warning,
isEnabledByDefault: true);

public static readonly DiagnosticDescriptor DuplicateSpecialCommand = new(
id: "ARGP0049",
title: "Duplicate special command",
messageFormat: "Special command '{0}' is already defined for this parser",
category: ArgumentParsingCategoryName,
defaultSeverity: DiagnosticSeverity.Error,
isEnabledByDefault: true);
}
3 changes: 2 additions & 1 deletion src/ArgumentParsing.Generators/Models/ArgumentParserInfo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,5 @@ internal sealed record ArgumentParserInfo(
HierarchyInfo ContainingTypeHierarchy,
ArgumentParserMethodInfo MethodInfo,
OptionsInfo OptionsInfo,
ImmutableEquatableArray<SpecialCommandHandlerInfo>? SpecialCommandHandlersInfos);
BuiltInCommandHandlers BuiltInCommandHandlers,
ImmutableEquatableArray<SpecialCommandHandlerInfo> AdditionalCommandHandlersInfos);
10 changes: 10 additions & 0 deletions src/ArgumentParsing.Generators/Models/BuiltInCommandHandlers.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
namespace ArgumentParsing.Generators.Models;

// Keep in sync with the same type in ArgumentParsing project
[Flags]
internal enum BuiltInCommandHandlers : byte
{
None = 0,
Help = 1,
Version = 2,
}
23 changes: 23 additions & 0 deletions src/ArgumentParsing/BuiltInCommandHandlers.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
namespace ArgumentParsing;

/// <summary>
/// Built-in special command handlers' flags
/// </summary>
[Flags]
public enum BuiltInCommandHandlers : byte
{
/// <summary>
/// Represents no built-in command handlers
/// </summary>
None = 0,

/// <summary>
/// Represents built-in <c>--help</c> command handler
/// </summary>
Help = 1,

/// <summary>
/// Represents built-in <c>--version</c> command handler
/// </summary>
Version = 2,
}
11 changes: 7 additions & 4 deletions src/ArgumentParsing/GeneratedArgumentParserAttribute.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,12 @@ namespace ArgumentParsing;
public sealed class GeneratedArgumentParserAttribute : Attribute
{
/// <summary>
/// List of special command handlers' types of this parser.
/// If <see langword="null"/> is supplied, parser generates and uses
/// default implementations of <c>--help</c> and <c>--version</c> commands
/// Built-in special command handlers of this parser
/// </summary>
public Type[]? SpecialCommandHandlers { get; set; }
public BuiltInCommandHandlers BuiltInCommandHandlers { get; set; } = BuiltInCommandHandlers.Help | BuiltInCommandHandlers.Version;

/// <summary>
/// Additional special command handlers' types of this parser
/// </summary>
public Type[] AdditionalCommandHandlers { get; set; } = [];
}
Loading

0 comments on commit 6ca3655

Please sign in to comment.