From 7a556c33f84a0d8da70dcda967ee0093ecd25c71 Mon Sep 17 00:00:00 2001 From: "Dear.Va" <1328886154@qq.com> Date: Thu, 7 Dec 2023 15:36:16 +0800 Subject: [PATCH] Add support for multiple parameters. --- readme.md | 2 +- readme_cn.md | 3 +- .../.idea/.name | 2 +- src/Antelcat.Parameterization.Demo/Program.cs | 69 ++- .../Attributes/ArgumentAttribute.cs | 2 +- .../Extensions/EnumerableExtension.cs | 134 ++--- .../Extensions/NullableExtension.cs | 72 +-- .../Extensions/StringExtension.cs | 8 +- .../Extensions/SyntaxExtension.cs | 131 ++--- .../Generators/ClassAttributeBaseGenerator.cs | 34 +- .../Generators/CommonGenerator.cs | 333 ++++++----- .../Generators/ConvertersGenerator.cs | 94 ++-- .../Generators/ParameterizationGenerator.cs | 527 ++++++++++-------- .../Utils/SourceStringBuilder.cs | 30 +- .../Antelcat.Parameterization.csproj | 2 +- 15 files changed, 772 insertions(+), 671 deletions(-) diff --git a/readme.md b/readme.md index 0dd0f16..86750d1 100644 --- a/readme.md +++ b/readme.md @@ -133,7 +133,7 @@ We welcome contributions to this project! Whether it's reporting bugs, suggestin ## TODO - [x] Nuget package. +- [x] Multiple parameters, use `T[]`. - [ ] Automatically generate help documents. - - [ ] Check for excess parameters. - [ ] Parameter combination, e.g. `-it` will both open `-i` and `-t`. \ No newline at end of file diff --git a/readme_cn.md b/readme_cn.md index 7bc2867..71963cf 100644 --- a/readme_cn.md +++ b/readme_cn.md @@ -133,7 +133,8 @@ ## 待办事项 -- [ ] Nuget 包。 +- [x] Nuget 包。 +- [x] 解析多个参数,使用`T[]`。 - [ ] 自动生成帮助文档。 - [ ] 检查多余参数。 - [ ] 参数组合,例如 `-it` 将同时开启 `-i` 和 `-t`。 \ No newline at end of file diff --git a/src/.idea/.idea.AntelCat.Parameterization/.idea/.name b/src/.idea/.idea.AntelCat.Parameterization/.idea/.name index 154c354..825478d 100644 --- a/src/.idea/.idea.AntelCat.Parameterization/.idea/.name +++ b/src/.idea/.idea.AntelCat.Parameterization/.idea/.name @@ -1 +1 @@ -AntelCat.Parameterization \ No newline at end of file +Antelcat.Parameterization \ No newline at end of file diff --git a/src/Antelcat.Parameterization.Demo/Program.cs b/src/Antelcat.Parameterization.Demo/Program.cs index f2e4fc7..95e1a05 100644 --- a/src/Antelcat.Parameterization.Demo/Program.cs +++ b/src/Antelcat.Parameterization.Demo/Program.cs @@ -43,10 +43,10 @@ public static void Main(string[] args) [Command(ShortName = "ps", Description = "Display a list of container(s) resources usage statistics")] private static void Stats( - [Argument(FullName = "all", ShortName = "a", Description = "Show all containers (default shows just running)", DefaultValue = "true")] + [Argument(FullName = "all", ShortName = 'a', Description = "Show all containers (default shows just running)", DefaultValue = "true")] bool showAll = false) { - Console.WriteLine("CONTAINER_ID IMAGE NAME STATUS"); + Console.WriteLine("CONTAINER_ID IMAGE NAME STATUS PORTS"); foreach (var container in Containers.Where(container => showAll || container.IsRunning)) { Console.WriteLine($"{container.Id} {container.Image} {container.Name} {(container.IsRunning ? "running" : "stopped")}"); @@ -74,11 +74,14 @@ private static void Pull(Image image) } [Command] - private static void Run(Image image, string? name = null) + private static void Run( + Image image, + string? name = null, + [Argument(ShortName = 'p', Converter = typeof(PortMappingConverter))] PortMapping[]? portMappings = null) { Pull(image); name ??= image.Name; - var container = new Container(image, Guid.NewGuid().ToString("N")[..8], name, true); + var container = new Container(image, Guid.NewGuid().ToString("N")[..8], name, true, portMappings); Containers.Add(container); Console.WriteLine(container); } @@ -105,22 +108,7 @@ private static void Exit() } } -public class ImageConverter : StringConverter -{ - public override object ConvertFrom(ITypeDescriptorContext? context, CultureInfo? culture, object? value) - { - if (value is not string str) throw new ArgumentException("Invalid image string format"); - var args = str.Split(':'); - return args switch - { - { Length: 1 } => new Image(str, null), - { Length: 2 } => new Image(args[0], Version.Parse(args[1])), - _ => throw new ArgumentException("Invalid image string format") - }; - } -} - -public class Container(Image image, string id, string name, bool isRunning) +public class Container(Image image, string id, string name, bool isRunning, PortMapping[]? portMappings) { public override int GetHashCode() { @@ -134,13 +122,29 @@ public virtual bool Equals(Container? other) public override string ToString() { - return $"{Image} {Name} {(IsRunning ? "running" : "stopped")}"; + return $"{Image} {Name} {(IsRunning ? "running" : "stopped")} {(PortMappings == null ? string.Empty : string.Join(", ", PortMappings))}"; } public Image Image { get; } = image; public string Id { get; } = id; public string Name { get; init; } = name; public bool IsRunning { get; set; } = isRunning; + public PortMapping[]? PortMappings { get; } = portMappings; +} + +public class ImageConverter : StringConverter +{ + public override object ConvertFrom(ITypeDescriptorContext? context, CultureInfo? culture, object? value) + { + if (value is not string str) throw new ArgumentException("Invalid image string format"); + var args = str.Split(':'); + return args switch + { + { Length: 1 } => new Image(str, null), + { Length: 2 } => new Image(args[0], Version.Parse(args[1])), + _ => throw new ArgumentException("Invalid image string format") + }; + } } [TypeConverter(typeof(ImageConverter))] @@ -150,4 +154,27 @@ public override string ToString() { return $"{Name}:{Version?.ToString() ?? "latest"}"; } +} + +public class PortMappingConverter : StringConverter +{ + public override object ConvertFrom(ITypeDescriptorContext? context, CultureInfo? culture, object? value) + { + if (value is not string str) throw new ArgumentException("Invalid port mapping string format"); + var args = str.Split(':'); + return args switch + { + { Length: 1 } => new PortMapping(int.Parse(args[0]), int.Parse(args[0])), + { Length: 2 } => new PortMapping(int.Parse(args[0]), int.Parse(args[1])), + _ => throw new ArgumentException("Invalid port mapping string format") + }; + } +} + +public record PortMapping(int HostPort, int ContainerPort) +{ + public override string ToString() + { + return $"{HostPort}->{ContainerPort}/tcp"; + } } \ No newline at end of file diff --git a/src/Antelcat.Parameterization.Shared/Attributes/ArgumentAttribute.cs b/src/Antelcat.Parameterization.Shared/Attributes/ArgumentAttribute.cs index b750937..b0f81a9 100644 --- a/src/Antelcat.Parameterization.Shared/Attributes/ArgumentAttribute.cs +++ b/src/Antelcat.Parameterization.Shared/Attributes/ArgumentAttribute.cs @@ -13,7 +13,7 @@ public class ArgumentAttribute : Attribute /// /// ShortName switch, e.g. -n /// - public string? ShortName { get; set; } + public char ShortName { get; set; } public string? Description { get; set; } diff --git a/src/Antelcat.Parameterization.SourceGenerators/Extensions/EnumerableExtension.cs b/src/Antelcat.Parameterization.SourceGenerators/Extensions/EnumerableExtension.cs index 6a96161..d6451d8 100644 --- a/src/Antelcat.Parameterization.SourceGenerators/Extensions/EnumerableExtension.cs +++ b/src/Antelcat.Parameterization.SourceGenerators/Extensions/EnumerableExtension.cs @@ -10,79 +10,79 @@ namespace Antelcat.Parameterization.SourceGenerators.Extensions; public static class EnumerableExtension { - /// - /// Python: enumerator - /// - /// - /// - /// - /// - /// - public static IEnumerable<(int index, T value)> WithIndex( - this IEnumerable enumerable, - int startIndex = 0, - int step = 1) - { + /// + /// Python: enumerator + /// + /// + /// + /// + /// + /// + public static IEnumerable<(int index, T value)> WithIndex( + this IEnumerable enumerable, + int startIndex = 0, + int step = 1) + { - foreach (var item in enumerable) - { - yield return (startIndex, item); - startIndex += step; - } - } + foreach (var item in enumerable) + { + yield return (startIndex, item); + startIndex += step; + } + } - public static IEnumerable> Zip( - this IEnumerable source1, - IEnumerable source2) - { - return source1.Zip(source2, static (a, b) => (a, b)); - } + public static IEnumerable> Zip( + this IEnumerable source1, + IEnumerable source2) + { + return source1.Zip(source2, static (a, b) => (a, b)); + } - public static ObservableCollection ToObservableCollection(this IEnumerable enumerable) - { - return new(enumerable); - } + public static ObservableCollection ToObservableCollection(this IEnumerable enumerable) + { + return new(enumerable); + } - public static IEnumerable Reversed(this IList list) - { - for (var i = list.Count - 1; i >= 0; i--) - { - yield return list[i]; - } - } + public static IEnumerable Reversed(this IList list) + { + for (var i = list.Count - 1; i >= 0; i--) + { + yield return list[i]; + } + } - public static int FindIndexOf(this IList list, Predicate predicate) - { - for (var i = 0; i < list.Count; i++) - { - if (predicate(list[i])) - { - return i; - } - } + public static int FindIndexOf(this IList list, Predicate predicate) + { + for (var i = 0; i < list.Count; i++) + { + if (predicate(list[i])) + { + return i; + } + } - return -1; - } + return -1; + } - /// - /// 完全枚举一个 ,并丢弃所有元素 - /// - /// - [MethodImpl(MethodImplOptions.NoOptimization)] - public static void Discard(this IEnumerable enumerable) - { - foreach (var _ in enumerable) { } - } + /// + /// 完全枚举一个 ,并丢弃所有元素 + /// + /// + [MethodImpl(MethodImplOptions.NoOptimization)] + public static void Discard(this IEnumerable enumerable) + { + foreach (var _ in enumerable) { } + } - /// - /// 完全枚举一个 ,并丢弃所有元素 - /// - /// - /// - [MethodImpl(MethodImplOptions.NoOptimization)] - [DebuggerNonUserCode] - public static void Discard(this IEnumerable enumerable) - { - foreach (var _ in enumerable) { } - } + /// + /// 完全枚举一个 ,并丢弃所有元素 + /// + /// + /// + [MethodImpl(MethodImplOptions.NoOptimization)] + [DebuggerNonUserCode] + public static void Discard(this IEnumerable enumerable) + { + foreach (var _ in enumerable) { } + } } \ No newline at end of file diff --git a/src/Antelcat.Parameterization.SourceGenerators/Extensions/NullableExtension.cs b/src/Antelcat.Parameterization.SourceGenerators/Extensions/NullableExtension.cs index c0f79c3..4faec5a 100644 --- a/src/Antelcat.Parameterization.SourceGenerators/Extensions/NullableExtension.cs +++ b/src/Antelcat.Parameterization.SourceGenerators/Extensions/NullableExtension.cs @@ -4,43 +4,43 @@ namespace Antelcat.Parameterization.SourceGenerators.Extensions; public static class NullableExtension { - /// - /// 将一个可能为空的转成不可空,如果为null将抛出 - /// - /// - /// - /// - /// - public static T NotNull(this T? t) where T : notnull => t ?? throw new NullReferenceException(); + /// + /// 将一个可能为空的转成不可空,如果为null将抛出 + /// + /// + /// + /// + /// + public static T NotNull(this T? t) where T : notnull => t ?? throw new NullReferenceException(); - /// - /// 将一个可能为空的转成不可空,如果为null将抛出 - /// - /// - /// - /// - /// - /// - public static T NotNull(this T? t) - where T : notnull where TException : Exception, new() => t ?? throw new TException(); + /// + /// 将一个可能为空的转成不可空,如果为null将抛出 + /// + /// + /// + /// + /// + /// + public static T NotNull(this T? t) + where T : notnull where TException : Exception, new() => t ?? throw new TException(); - /// - /// 将一个可能为空的转成不可空,如果为null将抛出 - /// - /// - /// - /// - /// - public static T NotNull(this object? t) where T : notnull => (T?)t ?? throw new NullReferenceException(); + /// + /// 将一个可能为空的转成不可空,如果为null将抛出 + /// + /// + /// + /// + /// + public static T NotNull(this object? t) where T : notnull => (T?)t ?? throw new NullReferenceException(); - /// - /// 将一个可能为空的转成不可空,如果为null将抛出 - /// - /// - /// - /// - /// - /// - public static T NotNull(this T? t, string errorMessage) => - t ?? throw new NullReferenceException(errorMessage); + /// + /// 将一个可能为空的转成不可空,如果为null将抛出 + /// + /// + /// + /// + /// + /// + public static T NotNull(this T? t, string errorMessage) => + t ?? throw new NullReferenceException(errorMessage); } \ No newline at end of file diff --git a/src/Antelcat.Parameterization.SourceGenerators/Extensions/StringExtension.cs b/src/Antelcat.Parameterization.SourceGenerators/Extensions/StringExtension.cs index 43d8751..1813c28 100644 --- a/src/Antelcat.Parameterization.SourceGenerators/Extensions/StringExtension.cs +++ b/src/Antelcat.Parameterization.SourceGenerators/Extensions/StringExtension.cs @@ -2,8 +2,8 @@ public static class StringExtension { - public static string Escape(this string? s) - { - return s == null ? "null" : $"\"{s.Replace("\"", "\\\"")}\""; - } + public static string Escape(this string? s) + { + return s == null ? "null" : $"\"{s.Replace("\"", "\\\"")}\""; + } } \ No newline at end of file diff --git a/src/Antelcat.Parameterization.SourceGenerators/Extensions/SyntaxExtension.cs b/src/Antelcat.Parameterization.SourceGenerators/Extensions/SyntaxExtension.cs index 4febe3a..c7ee5ce 100644 --- a/src/Antelcat.Parameterization.SourceGenerators/Extensions/SyntaxExtension.cs +++ b/src/Antelcat.Parameterization.SourceGenerators/Extensions/SyntaxExtension.cs @@ -10,72 +10,77 @@ namespace Antelcat.Parameterization.SourceGenerators.Extensions; public static class SyntaxExtension { - public static IEnumerable GetAllAttributes(this SyntaxNode syntax) - { - var attributeLists = syntax switch - { - MemberDeclarationSyntax member => member.AttributeLists, - BaseParameterSyntax parameter => parameter.AttributeLists, - LambdaExpressionSyntax lambda => lambda.AttributeLists, - StatementSyntax statementSyntax => statementSyntax.AttributeLists, - CompilationUnitSyntax compilationUnitSyntax => compilationUnitSyntax.AttributeLists, - _ => throw new NotSupportedException($"{syntax.GetType().Name} is not supported for GetAllAttributes.") - }; - return attributeLists.SelectMany(attributeList => attributeList.Attributes); - } + public static IEnumerable GetAllAttributes(this SyntaxNode syntax) + { + var attributeLists = syntax switch + { + MemberDeclarationSyntax member => member.AttributeLists, + BaseParameterSyntax parameter => parameter.AttributeLists, + LambdaExpressionSyntax lambda => lambda.AttributeLists, + StatementSyntax statementSyntax => statementSyntax.AttributeLists, + CompilationUnitSyntax compilationUnitSyntax => compilationUnitSyntax.AttributeLists, + _ => throw new NotSupportedException($"{syntax.GetType().Name} is not supported for GetAllAttributes.") + }; + return attributeLists.SelectMany(attributeList => attributeList.Attributes); + } - public static IEnumerable GetSpecifiedAttributes( - this SyntaxNode syntax, - SemanticModel semanticModel, - string attributeFullName, - CancellationToken cancellationToken = default) - { - foreach (var attributeSyntax in syntax.GetAllAttributes()) - { - if (cancellationToken.IsCancellationRequested) yield break; - if (semanticModel.GetSymbolInfo(attributeSyntax, cancellationToken).Symbol is not IMethodSymbol attributeSymbol) continue; - var attributeName = attributeSymbol.ContainingType.ToDisplayString(); - if (attributeName == attributeFullName) yield return attributeSyntax; - } - } + public static IEnumerable GetSpecifiedAttributes( + this SyntaxNode syntax, + SemanticModel semanticModel, + string attributeFullName, + CancellationToken cancellationToken = default) + { + foreach (var attributeSyntax in syntax.GetAllAttributes()) + { + if (cancellationToken.IsCancellationRequested) yield break; + if (semanticModel.GetSymbolInfo(attributeSyntax, cancellationToken).Symbol is not IMethodSymbol attributeSymbol) continue; + var attributeName = attributeSymbol.ContainingType.ToDisplayString(); + if (attributeName == attributeFullName) yield return attributeSyntax; + } + } - /// - /// 获取一个Type下面的所有指定的Attribute - /// - /// - /// - /// - /// - /// - public static IEnumerable GetSpecifiedAttributes( - this SyntaxNode syntax, - SemanticModel semanticModel, - CancellationToken cancellationToken = default) - where T : Attribute - { - var attributeType = typeof(T); - var attributeFullName = attributeType.FullName; - if (attributeFullName == null) yield break; - foreach (var attribute in syntax.GetSpecifiedAttributes(semanticModel, attributeFullName, cancellationToken)) - { - yield return new AttributeProxy(attribute); - } - } + /// + /// 获取一个Type下面的所有指定的Attribute + /// + /// + /// + /// + /// + /// + public static IEnumerable GetSpecifiedAttributes( + this SyntaxNode syntax, + SemanticModel semanticModel, + CancellationToken cancellationToken = default) + where T : Attribute + { + var attributeType = typeof(T); + var attributeFullName = attributeType.FullName; + if (attributeFullName == null) yield break; + foreach (var attribute in syntax.GetSpecifiedAttributes(semanticModel, attributeFullName, cancellationToken)) + { + yield return new AttributeProxy(attribute); + } + } - public static bool IsDerivedFrom(this ITypeSymbol? typeSymbol) - { - var targetTypeFullName = typeof(T).FullName; - while (typeSymbol != null) - { - if (typeSymbol.ToDisplayString() == targetTypeFullName) return true; - typeSymbol = typeSymbol.BaseType; - } + public static bool IsDerivedFrom(this ITypeSymbol? typeSymbol) + { + var targetTypeFullName = typeof(T).FullName; + while (typeSymbol != null) + { + if (typeSymbol.ToDisplayString() == targetTypeFullName) return true; + typeSymbol = typeSymbol.BaseType; + } - return false; - } + return false; + } - public static string? ToDisplayName(this SyntaxNode? syntax, SemanticModel semanticModel, SymbolDisplayFormat? symbolDisplayFormat = null) - { - return syntax == null ? null : semanticModel.GetSymbolInfo(syntax).Symbol?.ToDisplayString(symbolDisplayFormat); - } + public static string? ToDisplayName(this SyntaxNode syntax, SemanticModel semanticModel, SymbolDisplayFormat? symbolDisplayFormat = null) + { + return semanticModel.GetSymbolInfo(syntax).Symbol?.ToDisplayString(symbolDisplayFormat); + } + + public static bool IsArray(this SyntaxNode syntax, SemanticModel semanticModel) + { + return semanticModel.GetSymbolInfo(syntax).Symbol is IArrayTypeSymbol; + } } \ No newline at end of file diff --git a/src/Antelcat.Parameterization.SourceGenerators/Generators/ClassAttributeBaseGenerator.cs b/src/Antelcat.Parameterization.SourceGenerators/Generators/ClassAttributeBaseGenerator.cs index 057d0fd..c5464fd 100644 --- a/src/Antelcat.Parameterization.SourceGenerators/Generators/ClassAttributeBaseGenerator.cs +++ b/src/Antelcat.Parameterization.SourceGenerators/Generators/ClassAttributeBaseGenerator.cs @@ -8,24 +8,24 @@ namespace Antelcat.Parameterization.SourceGenerators.Generators; public abstract class ClassAttributeBaseGenerator : IIncrementalGenerator { - protected abstract string AttributeName { get; } + protected abstract string AttributeName { get; } - public void Initialize(IncrementalGeneratorInitializationContext context) - { - var provider = context.SyntaxProvider.ForAttributeWithMetadataName( - AttributeName, - static (syntax, _) => syntax is ClassDeclarationSyntax, - (syntaxContext, token) => - { - var node = (ClassDeclarationSyntax)syntaxContext.TargetNode; - return (syntaxContext, node.GetSpecifiedAttributes(syntaxContext.SemanticModel, AttributeName, token).FirstOrDefault()); - } - ).Where(x => x.Item2 != null); + public void Initialize(IncrementalGeneratorInitializationContext context) + { + var provider = context.SyntaxProvider.ForAttributeWithMetadataName( + AttributeName, + static (syntax, _) => syntax is ClassDeclarationSyntax, + (syntaxContext, token) => + { + var node = (ClassDeclarationSyntax)syntaxContext.TargetNode; + return (syntaxContext, node.GetSpecifiedAttributes(syntaxContext.SemanticModel, AttributeName, token).FirstOrDefault()); + } + ).Where(x => x.Item2 != null); - context.RegisterSourceOutput(provider.Collect(), GenerateCode); - } + context.RegisterSourceOutput(provider.Collect(), GenerateCode); + } - protected abstract void GenerateCode( - SourceProductionContext context, - ImmutableArray<(GeneratorAttributeSyntaxContext, AttributeSyntax)> targets); + protected abstract void GenerateCode( + SourceProductionContext context, + ImmutableArray<(GeneratorAttributeSyntaxContext, AttributeSyntax)> targets); } \ No newline at end of file diff --git a/src/Antelcat.Parameterization.SourceGenerators/Generators/CommonGenerator.cs b/src/Antelcat.Parameterization.SourceGenerators/Generators/CommonGenerator.cs index bf0167b..a65b158 100644 --- a/src/Antelcat.Parameterization.SourceGenerators/Generators/CommonGenerator.cs +++ b/src/Antelcat.Parameterization.SourceGenerators/Generators/CommonGenerator.cs @@ -4,155 +4,188 @@ namespace Antelcat.Parameterization.SourceGenerators.Generators; public static class CommonGenerator { - public static void Execute(in SourceProductionContext context) - { - context.AddSource($"{Global.Namespace}.Common.g.cs", - $$"""" - // - #nullable enable - using System; - using System.Collections.Generic; - using System.Text.RegularExpressions; + public static void Execute(in SourceProductionContext context) + { + context.AddSource($"{Global.Namespace}.Common.g.cs", + $$"""" + // + #nullable enable + using System; + using System.Collections.Generic; + using System.Text.RegularExpressions; - namespace Antelcat.Parameterization - { - public class CommandNotFoundException(string commandName) : Exception($"Command \"{commandName}\" not found.") { } - - public struct ParsedArgument - { - public string Name { get; } - - public object? Value { - get => value; - set - { - this.value = value; - hasValue = true; - } - } - - public bool HasValue => hasValue; - - private object? value; - private bool hasValue; - - public ParsedArgument(string name) - { - Name = name; - } - } - - public static class Common - { - public static Regex CommandRegex { get; } = new Regex(@"[^\s""]+|""([^""]|(\\""))*"""); - public static Regex ArgumentRegex { get; } = new Regex(@"[^,""]+|""([^""]|(\\""))*"""); - public static Regex QuotationRegex { get; } = new Regex(@"^""|""$"); - - internal static int FindIndexOf(this IReadOnlyList list, Predicate predicate) { - for (var i = 0; i < list.Count; i++) { - if (predicate(list[i])) { - return i; - } - } - - return -1; - } - - /// - /// - /// - /// - public static ParsedArgument[] ParseArguments( - IReadOnlyList arguments, - IReadOnlyList<(string fullName, string? shortName)> parameterNames, - IReadOnlyList defaultValues, - IReadOnlyList<{{Global.TypeConverter}}> argumentConverters) - { - var results = new ParsedArgument[parameterNames.Count]; - for (var i = 0; i < results.Length; i++) - { - results[i] = new ParsedArgument(parameterNames[i].fullName); - } - - var isNamedArgumentUsed = false; - for (var i = 1; i < arguments.Count; i++) - { - var argumentIndex = -1; - var argument = arguments[i]; - - if (argument.StartsWith("--")) - { - argument = argument[2..]; - argumentIndex = parameterNames.FindIndexOf(x => x.fullName == argument); - if (argumentIndex == -1) - { - throw new ArgumentException($"Argument \"--{argument}\" not found."); - } - isNamedArgumentUsed = true; - } - else if (argument.StartsWith('-')) - { - argument = argument[1..]; - argumentIndex = parameterNames.FindIndexOf(x => x.shortName == argument); - if (argumentIndex == -1) - { - throw new ArgumentException($"Argument \"-{argument}\" not found."); - } - isNamedArgumentUsed = true; - } - else if (isNamedArgumentUsed) - { - throw new ArgumentException("Named results must come after all anonymous results."); - } - - if ((argumentIndex != -1 && results[argumentIndex].HasValue) || - (argumentIndex == -1 && results[i - 1].HasValue)) - { - throw new ArgumentException($"Argument \"{argument}\" is duplicated."); - } - - if (argumentIndex != -1) - { - // 当前是命名参数,那么下一个才是参数的值 - if (i == arguments.Count - 1 || arguments[i + 1].StartsWith('-')) - { - // 如果没有下一个参数,或者下一个参数是命名参数 - var defaultValue = defaultValues[argumentIndex]; - if (defaultValue == null) - { - throw new ArgumentException($"The value of argument \"{argument}\" is not specified."); - } - - results[argumentIndex].Value = argumentConverters[argumentIndex].ConvertFromString(defaultValue); - continue; - } - - argument = arguments[++i]; - } - else - { - argumentIndex = i - 1; - } - - results[argumentIndex].Value = argumentConverters[argumentIndex].ConvertFromString(argument); - } - - return results; - } - - public static T ConvertArgument(ParsedArgument parsed) - { - if (!parsed.HasValue) throw new ArgumentException($"Argument \"{parsed.Name}\" is not specified."); - return parsed.Value is T result ? result : throw new ArgumentException($"Argument \"{parsed.Name}\" is not of type {typeof(T).FullName}."); - } - - public static T ConvertArgument(ParsedArgument parsed, T defaultValue) - { - if (!parsed.HasValue) return defaultValue; - return parsed.Value is T result ? result : throw new ArgumentException($"Argument \"{parsed.Name}\" is not of type {typeof(T).FullName}."); - } - } - } - """"); - } + namespace Antelcat.Parameterization + { + public class CommandNotFoundException(string commandName) : Exception($"Command \"{commandName}\" not found.") { } + + public class ParsedArgument + { + public string Name { get; } + + public object? Value => value; + public bool HasValue => hasValue; + + protected object? value; + protected bool hasValue; + + public ParsedArgument(string name) + { + Name = name; + } + + public virtual void SetValue(object? value) + { + if (hasValue) + { + throw new ArgumentException($"Multiple value of argument \"{Name}\" is not supported."); + } + + this.value = value; + hasValue = true; + } + } + + public class ParsedArrayArgument : ParsedArgument + { + public ParsedArrayArgument(string name) : base(name) + { + value = new List(1); + } + + public object?[] ToArray() => ((List)value!).ToArray(); + + public override void SetValue(object? value) + { + ((List)this.value!).Add(value); + hasValue = true; + } + } + + public static class Common + { + public static Regex CommandRegex { get; } = new Regex(@"[^\s""]+|""([^""]|(\\""))*"""); + public static Regex ArgumentRegex { get; } = new Regex(@"[^,""]+|""([^""]|(\\""))*"""); + public static Regex QuotationRegex { get; } = new Regex(@"^""|""$"); + + internal static int FindIndexOf(this IReadOnlyList list, Predicate predicate) { + for (var i = 0; i < list.Count; i++) { + if (predicate(list[i])) { + return i; + } + } + + return -1; + } + + public static void ParseArguments( + IReadOnlyList parsedArguments, + IReadOnlyList arguments, + IReadOnlyList<(string fullName, string? shortName)> parameterNames, + IReadOnlyList defaultValues, + IReadOnlyList<{{Global.TypeConverter}}> argumentConverters) + { + var isNamedArgumentUsed = false; + for (var i = 1; i < arguments.Count; i++) + { + var argumentIndex = -1; + var argument = arguments[i]; + + if (argument.StartsWith("---")) + { + throw new ArgumentException($"Bad switch syntax: {argument}"); + } + if (argument.StartsWith("--")) + { + argument = argument[2..]; + argumentIndex = parameterNames.FindIndexOf(x => x.fullName == argument); + if (argumentIndex == -1) + { + throw new ArgumentException($"Argument \"--{argument}\" not found."); + } + isNamedArgumentUsed = true; + } + else if (argument.StartsWith('-')) + { + argument = argument[1..]; + argumentIndex = parameterNames.FindIndexOf(x => x.shortName == argument); + if (argumentIndex == -1) + { + throw new ArgumentException($"Argument \"-{argument}\" not found."); + } + isNamedArgumentUsed = true; + } + else if (isNamedArgumentUsed) + { + throw new ArgumentException("Named results must come after all anonymous results."); + } + + if (argumentIndex != -1) + { + // 当前是命名参数,那么下一个才是参数的值 + if (i == arguments.Count - 1 || arguments[i + 1].StartsWith('-')) + { + // 如果没有下一个参数,或者下一个参数是命名参数 + var defaultValue = defaultValues[argumentIndex]; + if (defaultValue == null) + { + throw new ArgumentException($"The value of argument \"{argument}\" is not specified."); + } + + parsedArguments[argumentIndex].SetValue(argumentConverters[argumentIndex].ConvertFromString(defaultValue)); + continue; + } + + for (++i; i < arguments.Count; i++) + { + argument = arguments[i]; + if (argument.StartsWith('-')) + { + i--; + break; + } + + parsedArguments[argumentIndex].SetValue(argumentConverters[argumentIndex].ConvertFromString(argument)); + } + } + else + { + argumentIndex = i - 1; + parsedArguments[argumentIndex].SetValue(argumentConverters[argumentIndex].ConvertFromString(argument)); + } + } + } + + public static T ConvertArgument(ParsedArgument parsed) + { + if (!parsed.HasValue) throw new ArgumentException($"Argument \"{parsed.Name}\" is not specified."); + object? value; + if (parsed is ParsedArrayArgument parsedArray) + { + value = parsedArray.ToArray(); + } + else + { + value = parsed.Value; + } + return value is T result ? result : throw new ArgumentException($"Argument \"{parsed.Name}\" is not of type {typeof(T).FullName}."); + } + + public static T ConvertArgument(ParsedArgument parsed, T defaultValue) + { + if (!parsed.HasValue) return defaultValue; + object? value; + if (parsed is ParsedArrayArgument parsedArray) + { + value = parsedArray.ToArray(); + } + else + { + value = parsed.Value; + } + return value is T result ? result : throw new ArgumentException($"Argument \"{parsed.Name}\" is not of type {typeof(T).FullName}."); + } + } + } + """"); + } } \ No newline at end of file diff --git a/src/Antelcat.Parameterization.SourceGenerators/Generators/ConvertersGenerator.cs b/src/Antelcat.Parameterization.SourceGenerators/Generators/ConvertersGenerator.cs index 3e4ba5b..689ceef 100644 --- a/src/Antelcat.Parameterization.SourceGenerators/Generators/ConvertersGenerator.cs +++ b/src/Antelcat.Parameterization.SourceGenerators/Generators/ConvertersGenerator.cs @@ -6,51 +6,51 @@ namespace Antelcat.Parameterization.SourceGenerators.Generators; public class ConvertersGenerator { - public IReadOnlyDictionary ConvertersMap => convertersMap; - - private readonly Dictionary convertersMap = new(SymbolEqualityComparer.Default); - private readonly SourceStringBuilder converterBuilder = new(initialIndentCount: 2); - - private string GetConverterName(ITypeSymbol typeSymbol) - { - var converterName = typeSymbol.Name; - if (convertersMap.ContainsValue(converterName)) - { - var i = 1; - while (convertersMap.ContainsValue(converterName + i)) - { - i++; - } - converterName += i; - } - - convertersMap.Add(typeSymbol, converterName); - return converterName; - } - - public void AddConverter(ITypeSymbol type, string initializer) - { - converterBuilder.AppendLine( - $"public static {Global.TypeConverter} {GetConverterName(type)}Converter {{ get; }} = {initializer};"); - } - - public void Execute(in SourceProductionContext context) - { - if (convertersMap.Count == 0) return; - - context.AddSource($"{Global.Namespace}.Converters.g.cs", - $$""" - // - #nullable enable - using System; - - namespace Antelcat.Parameterization - { - public static class Converters - { - {{converterBuilder}} - } - } - """); - } + public IReadOnlyDictionary ConvertersMap => convertersMap; + + private readonly Dictionary convertersMap = new(SymbolEqualityComparer.Default); + private readonly SourceStringBuilder converterBuilder = new(initialIndentCount: 2); + + private string GetConverterName(ITypeSymbol typeSymbol) + { + var converterName = typeSymbol.Name; + if (convertersMap.ContainsValue(converterName)) + { + var i = 1; + while (convertersMap.ContainsValue(converterName + i)) + { + i++; + } + converterName += i; + } + + convertersMap.Add(typeSymbol, converterName); + return converterName; + } + + public void AddConverter(ITypeSymbol type, string initializer) + { + converterBuilder.AppendLine( + $"public static {Global.TypeConverter} {GetConverterName(type)}Converter {{ get; }} = {initializer};"); + } + + public void Execute(in SourceProductionContext context) + { + if (convertersMap.Count == 0) return; + + context.AddSource($"{Global.Namespace}.Converters.g.cs", + $$""" + // + #nullable enable + using System; + + namespace Antelcat.Parameterization + { + public static class Converters + { + {{converterBuilder}} + } + } + """); + } } \ No newline at end of file diff --git a/src/Antelcat.Parameterization.SourceGenerators/Generators/ParameterizationGenerator.cs b/src/Antelcat.Parameterization.SourceGenerators/Generators/ParameterizationGenerator.cs index 8b30345..e447835 100644 --- a/src/Antelcat.Parameterization.SourceGenerators/Generators/ParameterizationGenerator.cs +++ b/src/Antelcat.Parameterization.SourceGenerators/Generators/ParameterizationGenerator.cs @@ -14,239 +14,296 @@ namespace Antelcat.Parameterization.SourceGenerators.Generators; [Generator(LanguageNames.CSharp)] public class ParameterizationGenerator : ClassAttributeBaseGenerator { - protected override string AttributeName => $"{Global.Namespace}.{nameof(ParameterizationAttribute)}"; - - protected override void GenerateCode(SourceProductionContext context, ImmutableArray<(GeneratorAttributeSyntaxContext, AttributeSyntax)> targets) - { - if (targets.Length == 0) return; - - CommonGenerator.Execute(context); - - var convertersGenerator = new ConvertersGenerator(); - - foreach (var (syntaxContext, attributeSyntax) in targets) - { - var attributeProxy = new AttributeProxy(attributeSyntax); - var caseSensitive = attributeProxy[nameof(ParameterizationAttribute.CaseSensitive)] as bool? ?? false; - - var classDeclarationSyntax = (ClassDeclarationSyntax)syntaxContext.TargetNode; - - var classAccessModifier = - classDeclarationSyntax.Modifiers - .FirstOrDefault(modifier => - modifier.IsKind(SyntaxKind.PublicKeyword) || modifier.IsKind(SyntaxKind.InternalKeyword)) - .Text ?? - "internal"; // 默认为 internal - var isClassStatic = classDeclarationSyntax.Modifiers.Any(modifier => modifier.IsKind(SyntaxKind.StaticKeyword)); - - IEnumerable<(MethodDeclarationSyntax, AttributeProxy)> GetCandidateMethods() - { - foreach (var method in classDeclarationSyntax.Members.OfType()) - { - // 如果类是static,那么方法也必须是static - if (method.Modifiers.All(modifier => !modifier.IsKind(SyntaxKind.StaticKeyword)) && isClassStatic) continue; - if (method.GetSpecifiedAttributes(syntaxContext.SemanticModel, context.CancellationToken) - .FirstOrDefault() is not { } commandAttribute) continue; - yield return (method, commandAttribute); - } - } - - var parameterTypes = new HashSet(SymbolEqualityComparer.Default); - var converterTypes = new HashSet(SymbolEqualityComparer.Default); - foreach (var (method, _) in GetCandidateMethods()) - { - foreach (var parameter in method.ParameterList.Parameters) - { - if (parameter.GetSpecifiedAttributes( - syntaxContext.SemanticModel, - context.CancellationToken) - .FirstOrDefault()?[nameof(ArgumentAttribute.Converter)] is TypeSyntax converterType && - syntaxContext.SemanticModel.GetSymbolInfo(converterType).Symbol is ITypeSymbol typeSymbol) - { - if (!typeSymbol.IsDerivedFrom()) - { - var diagnostic = Diagnostic.Create( - new DiagnosticDescriptor( - "PC0001", - "Error", - $"Converter \"{converterType}\" must be derived from \"{nameof(StringConverter)}\".", - nameof(ParameterizationAttribute), - DiagnosticSeverity.Error, - isEnabledByDefault: true - ), - parameter.GetLocation() - ); - - context.ReportDiagnostic(diagnostic); - return; - } - - if (typeSymbol.GetMembers().All(symbol => - symbol is not IMethodSymbol - { - MethodKind: MethodKind.Constructor, - Parameters.IsEmpty: true, - DeclaredAccessibility: Accessibility.Public - })) - { - var diagnostic = Diagnostic.Create( - new DiagnosticDescriptor( - "PC0002", - "Error", - $"Converter \"{converterType}\" must have a public parameterless constructor.", - nameof(ParameterizationAttribute), - DiagnosticSeverity.Error, - isEnabledByDefault: true - ), - parameter.GetLocation() - ); - - context.ReportDiagnostic(diagnostic); - return; - } - - converterTypes.Add(syntaxContext.SemanticModel.GetSymbolInfo(converterType).Symbol.NotNull()); - } - else - { - parameterTypes.Add(syntaxContext.SemanticModel.GetSymbolInfo(parameter.Type.NotNull()).Symbol.NotNull()); - } - } - } - - foreach (var parameterType in parameterTypes) - { - convertersGenerator.AddConverter( - parameterType, $"{Global.TypeDescriptor}.GetConverter(typeof({parameterType}))"); - } - foreach (var converterType in converterTypes) - { - convertersGenerator.AddConverter( - converterType, $"new {converterType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)}()"); - } - - var caseBuilder = new SourceStringBuilder(initialIndentCount: 3); - foreach (var (method, commandAttribute) in GetCandidateMethods()) - { - var fullName = commandAttribute[nameof(CommandAttribute.FullName)] as string ?? method.Identifier.ValueText; - caseBuilder.AppendLine($"case \"{(caseSensitive ? fullName : fullName.ToLower())}\":"); - if (commandAttribute[nameof(CommandAttribute.ShortName)] is string shortName) - { - caseBuilder.AppendLine($"case \"{(caseSensitive ? shortName : shortName.ToLower())}\":"); - } - - caseBuilder.AppendLine('{').Indent(); - - if (method.ParameterList.Parameters.Count > 0) - { - var methodParameters = method.ParameterList.Parameters.ToImmutableList(); - var parameterAttributes = methodParameters - .Select(parameter => parameter.GetSpecifiedAttributes( - syntaxContext.SemanticModel, - context.CancellationToken).FirstOrDefault()) - .ToImmutableList(); - - caseBuilder - .AppendLine($"var names = new {Global.ValueTuple}<{Global.String}, {Global.String}?>[] {{ {string.Join(", ", - parameterAttributes.Zip(methodParameters) - .Select(x => - ( - x.Item1?[nameof(ArgumentAttribute.FullName)] as string ?? x.Item2.Identifier.ValueText, - x.Item1?[nameof(ArgumentAttribute.ShortName)] as string - )) - .Select(x => $"new {Global.ValueTuple}<{Global.String}, {Global.String}?>({x.Item1.Escape()}, {x.Item2.Escape()})"))} }};") - .AppendLine($"var defaultValues = new {Global.String}?[] {{ {string.Join(", ", - parameterAttributes - .Select(x => x?[nameof(ArgumentAttribute.DefaultValue)] as string) - .Select(x => x == null ? "null" : $"\"{x}\""))} }};") - .AppendLine($"var argumentConverters = new {Global.TypeConverter}[] {{ {string.Join(", ", - parameterAttributes.Zip(methodParameters) - .Select(x => x.Item1?[nameof(ArgumentAttribute.Converter)] is TypeSyntax converterType - ? convertersGenerator.ConvertersMap[syntaxContext.SemanticModel.GetSymbolInfo(converterType).Symbol.NotNull()] - : convertersGenerator.ConvertersMap[syntaxContext.SemanticModel.GetSymbolInfo(x.Item2.Type.NotNull()).Symbol.NotNull()]) - .Select(name => $"{Global.GlobalNamespace}.Converters.{name}Converter"))} }};") - .AppendLine( - $"var args = {Global.GlobalNamespace}.Common.ParseArguments(arguments, names, defaultValues, argumentConverters);") - .AppendLine($"{method.Identifier.ValueText}({string.Join(", ", - methodParameters - .Select(p => - ( - typeName: p.Type.ToDisplayName(syntaxContext.SemanticModel).NotNull(), - defaultValue: p.Default?.Value.ToString() - )) - .WithIndex() - .Select(x => - $"{Global.GlobalNamespace}.Common.ConvertArgument<{x.value.typeName}>(args[{x.index}]{( - x.value.defaultValue == null ? string.Empty : $", {x.value.defaultValue}")})"))});"); - } - else - { - caseBuilder.AppendLine($"{method.Identifier.ValueText}();"); - } - - caseBuilder.AppendLine("break;").OutDent().AppendLine('}'); - } - - var sourceBuilder = new SourceStringBuilder().AppendLine("// ").AppendLine("#nullable enable"); - - var namespaceSyntax = classDeclarationSyntax.Parent as BaseNamespaceDeclarationSyntax; - if (namespaceSyntax != null) - { - sourceBuilder.AppendLine($"namespace {namespaceSyntax.Name}").AppendLine('{').Indent(); - } - var className = classDeclarationSyntax.Identifier.ValueText; - - sourceBuilder.Append( - $$""" - {{classAccessModifier}} {{(isClassStatic ? "static" : string.Empty)}} partial class {{className}} - { - private static void ExecuteInput({{Global.String}}? input) - { - if ({{Global.String}}.IsNullOrEmpty(input)) - { - return; - } - - var arguments = new {{Global.GenericList}}<{{Global.String}}>(); - foreach ({{Global.Match}} match in {{Global.GlobalNamespace}}.Common.CommandRegex.Matches(input)) - { - var part = match.Value; - part = {{Global.GlobalNamespace}}.Common.QuotationRegex.Replace(part, "").Replace("\\\"", "\""){{(caseSensitive ? "" : ".ToLower()")}}; - arguments.Add(part); - } - - ExecuteArguments(arguments); - } - - private static void ExecuteArguments({{Global.GenericIReadonlyList}}<{{Global.String}}> arguments) - { - if (arguments.Count == 0) - { - return; - } - - switch (arguments[0]) - { - {{caseBuilder}} - default: - { - throw new {{Global.ArgumentException}}($"Command \"{arguments[0]}\" not found."); - } - } - } - } - """); - - if (namespaceSyntax == null) - { - context.AddSource($"{className}.g.cs", sourceBuilder.ToString()); - } - else - { - context.AddSource($"{namespaceSyntax.Name}.{className}.g.cs", sourceBuilder.OutDent().Append('}').ToString()); - } - } - - convertersGenerator.Execute(context); - } + protected override string AttributeName => $"{Global.Namespace}.{nameof(ParameterizationAttribute)}"; + + protected override void GenerateCode(SourceProductionContext context, ImmutableArray<(GeneratorAttributeSyntaxContext, AttributeSyntax)> targets) + { + if (targets.Length == 0) return; + + CommonGenerator.Execute(context); + + var convertersGenerator = new ConvertersGenerator(); + + foreach (var (syntaxContext, attributeSyntax) in targets) + { + var attributeProxy = new AttributeProxy(attributeSyntax); + var caseSensitive = attributeProxy[nameof(ParameterizationAttribute.CaseSensitive)] as bool? ?? false; + + var classDeclarationSyntax = (ClassDeclarationSyntax)syntaxContext.TargetNode; + + var classAccessModifier = + classDeclarationSyntax.Modifiers + .FirstOrDefault(modifier => + modifier.IsKind(SyntaxKind.PublicKeyword) || modifier.IsKind(SyntaxKind.InternalKeyword)) + .Text ?? + "internal"; // 默认为 internal + var isClassStatic = classDeclarationSyntax.Modifiers.Any(modifier => modifier.IsKind(SyntaxKind.StaticKeyword)); + + IEnumerable<(MethodDeclarationSyntax, AttributeProxy)> GetCandidateMethods() + { + foreach (var method in classDeclarationSyntax.Members.OfType()) + { + // 如果类是static,那么方法也必须是static + if (method.Modifiers.All(modifier => !modifier.IsKind(SyntaxKind.StaticKeyword)) && isClassStatic) continue; + if (method.GetSpecifiedAttributes(syntaxContext.SemanticModel, context.CancellationToken) + .FirstOrDefault() is not { } commandAttribute) continue; + yield return (method, commandAttribute); + } + } + + var parameterTypes = new HashSet(SymbolEqualityComparer.Default); + var converterTypes = new HashSet(SymbolEqualityComparer.Default); + foreach (var (method, _) in GetCandidateMethods()) + { + foreach (var parameter in method.ParameterList.Parameters) + { + if (parameter.GetSpecifiedAttributes( + syntaxContext.SemanticModel, + context.CancellationToken) + .FirstOrDefault()?[nameof(ArgumentAttribute.Converter)] is TypeSyntax converterType && + syntaxContext.SemanticModel.GetSymbolInfo(converterType).Symbol is ITypeSymbol typeSymbol) + { + if (!typeSymbol.IsDerivedFrom()) + { + var diagnostic = Diagnostic.Create( + new DiagnosticDescriptor( + "AP0001", + "Error", + $"Converter \"{converterType}\" must be derived from \"{nameof(StringConverter)}\".", + nameof(ParameterizationAttribute), + DiagnosticSeverity.Error, + isEnabledByDefault: true + ), + parameter.GetLocation() + ); + context.ReportDiagnostic(diagnostic); + return; + } + + if (typeSymbol.GetMembers().All(symbol => + symbol is not IMethodSymbol + { + MethodKind: MethodKind.Constructor, + Parameters.IsEmpty: true, + DeclaredAccessibility: Accessibility.Public + })) + { + var diagnostic = Diagnostic.Create( + new DiagnosticDescriptor( + "AP0002", + "Error", + $"Converter \"{converterType}\" must have a public parameterless constructor.", + nameof(ParameterizationAttribute), + DiagnosticSeverity.Error, + isEnabledByDefault: true + ), + parameter.GetLocation() + ); + context.ReportDiagnostic(diagnostic); + return; + } + + converterTypes.Add(syntaxContext.SemanticModel.GetSymbolInfo(converterType).Symbol.NotNull()); + } + else + { + parameterTypes.Add(syntaxContext.SemanticModel.GetSymbolInfo(parameter.Type.NotNull()).Symbol.NotNull()); + } + } + } + + foreach (var parameterType in parameterTypes) + { + convertersGenerator.AddConverter( + parameterType, + $"{Global.TypeDescriptor}.GetConverter(typeof({parameterType}))"); + } + foreach (var converterType in converterTypes) + { + convertersGenerator.AddConverter( + converterType, + $"new {converterType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)}()"); + } + + var caseBuilder = new SourceStringBuilder(initialIndentCount: 3); + foreach (var (method, commandAttribute) in GetCandidateMethods()) + { + { + var fullName = commandAttribute[nameof(CommandAttribute.FullName)] as string ?? method.Identifier.ValueText; + caseBuilder.AppendLine($"case \"{(caseSensitive ? fullName : fullName.ToLower())}\":"); + if (commandAttribute[nameof(CommandAttribute.ShortName)] is string shortName) + { + caseBuilder.AppendLine($"case \"{(caseSensitive ? shortName : shortName.ToLower())}\":"); + } + } + + caseBuilder.AppendLine('{').Indent(); + + if (method.ParameterList.Parameters.Count > 0) + { + var methodParameters = method.ParameterList.Parameters.ToImmutableList(); + var parameterAttributes = methodParameters + .Select(parameter => parameter.GetSpecifiedAttributes( + syntaxContext.SemanticModel, + context.CancellationToken).FirstOrDefault()) + .ToImmutableList(); + + var parameterNames = new (string fullName, string? shortName)[methodParameters.Count]; + for (var i = 0; i < parameterNames.Length; i++) + { + var fullName = parameterAttributes[i]?[nameof(ArgumentAttribute.FullName)] as string ?? + methodParameters[i].Identifier.ValueText; + var shortName = parameterAttributes[i]?[nameof(ArgumentAttribute.ShortName)] as char? ?? '\0'; + if (shortName != '\0' && shortName is not (>= 'a' and <= 'z' or >= 'A' and <= 'Z' or >= '0' and <= '9')) + { + var diagnostic = Diagnostic.Create( + new DiagnosticDescriptor( + "AP0003", + "Error", + $"ShortName \"{shortName}\" must be a single letter or number.", + nameof(ParameterizationAttribute), + DiagnosticSeverity.Error, + isEnabledByDefault: true + ), + methodParameters[i].GetLocation() + ); + context.ReportDiagnostic(diagnostic); + return; + } + + parameterNames[i] = (fullName, shortName == '\0' ? null : shortName.ToString()); + } + + var parameterNamesDictionary = new Dictionary(); + for (var i = 0; i < parameterNames.Length; i++) + { + var (fullName, shortName) = parameterNames[i]; + if (parameterNamesDictionary.TryGetValue(fullName, out var conflict) || + shortName != null && parameterNamesDictionary.TryGetValue(shortName, out conflict)) + { + var diagnostic = Diagnostic.Create( + new DiagnosticDescriptor( + "AP0004", + "Error", + $"Parameter \"{methodParameters[i].Identifier.ValueText}\" conflicts with parameter \"{conflict.Identifier.ValueText}\".", + nameof(ParameterizationAttribute), + DiagnosticSeverity.Error, + isEnabledByDefault: true + ), + conflict.GetLocation() + ); + context.ReportDiagnostic(diagnostic); + return; + } + + parameterNamesDictionary.Add(fullName, methodParameters[i]); + if (shortName != null) + { + parameterNamesDictionary.Add(shortName, methodParameters[i]); + } + } + + caseBuilder + .AppendLine($"var parsedArguments = new {Global.GlobalNamespace}.ParsedArgument[] {{ {string.Join(", ", + methodParameters + .Select(p => p.Type.NotNull().IsArray(syntaxContext.SemanticModel) ? + $"new {Global.GlobalNamespace}.ParsedArrayArgument(\"{p.Identifier.ValueText}\")" : + $"new {Global.GlobalNamespace}.ParsedArgument(\"{p.Identifier.ValueText}\")"))} }};") + .AppendLine($"var argumentNames = new {Global.ValueTuple}<{Global.String}, {Global.String}?>[] {{ {string.Join(", ", + parameterNames + .Select(x => $"new {Global.ValueTuple}<{Global.String}, {Global.String}?>({x.Item1.Escape()}, {x.Item2.Escape()})"))} }};") + .AppendLine($"var defaultValues = new {Global.String}?[] {{ {string.Join(", ", + parameterAttributes + .Select(x => x?[nameof(ArgumentAttribute.DefaultValue)] as string) + .Select(x => x == null ? "null" : $"\"{x}\""))} }};") + .AppendLine($"var argumentConverters = new {Global.TypeConverter}[] {{ {string.Join(", ", + parameterAttributes.Zip(methodParameters) + .Select(x => x.Item1?[nameof(ArgumentAttribute.Converter)] is TypeSyntax converterType + ? convertersGenerator.ConvertersMap[syntaxContext.SemanticModel.GetSymbolInfo(converterType).Symbol.NotNull()] + : convertersGenerator.ConvertersMap[syntaxContext.SemanticModel.GetSymbolInfo(x.Item2.Type.NotNull()).Symbol.NotNull()]) + .Select(name => $"{Global.GlobalNamespace}.Converters.{name}Converter"))} }};") + .AppendLine( + $"{Global.GlobalNamespace}.Common.ParseArguments(parsedArguments, arguments, argumentNames, defaultValues, argumentConverters);") + .AppendLine($"{method.Identifier.ValueText}({string.Join(", ", + methodParameters + .Select(p => + ( + typeName: p.Type.NotNull().ToDisplayName(syntaxContext.SemanticModel), + defaultValue: p.Default?.Value.ToString() + )) + .WithIndex() + .Select(x => + $"{Global.GlobalNamespace}.Common.ConvertArgument<{x.value.typeName}>(parsedArguments[{x.index}]{( + x.value.defaultValue == null ? string.Empty : $", {x.value.defaultValue}")})"))});"); + } + else + { + caseBuilder.AppendLine($"{method.Identifier.ValueText}();"); + } + + caseBuilder.AppendLine("break;").OutDent().AppendLine('}'); + } + + var sourceBuilder = new SourceStringBuilder().AppendLine("// ").AppendLine("#nullable enable"); + + var namespaceSyntax = classDeclarationSyntax.Parent as BaseNamespaceDeclarationSyntax; + if (namespaceSyntax != null) + { + sourceBuilder.AppendLine($"namespace {namespaceSyntax.Name}").AppendLine('{').Indent(); + } + var className = classDeclarationSyntax.Identifier.ValueText; + + sourceBuilder.Append( + $$""" + {{classAccessModifier}} {{(isClassStatic ? "static" : string.Empty)}} partial class {{className}} + { + private static void ExecuteInput({{Global.String}}? input) + { + if ({{Global.String}}.IsNullOrEmpty(input)) + { + return; + } + + var arguments = new {{Global.GenericList}}<{{Global.String}}>(); + foreach ({{Global.Match}} match in {{Global.GlobalNamespace}}.Common.CommandRegex.Matches(input)) + { + var part = match.Value; + part = {{Global.GlobalNamespace}}.Common.QuotationRegex.Replace(part, "").Replace("\\\"", "\""){{(caseSensitive ? "" : ".ToLower()")}}; + arguments.Add(part); + } + + ExecuteArguments(arguments); + } + + private static void ExecuteArguments({{Global.GenericIReadonlyList}}<{{Global.String}}> arguments) + { + if (arguments.Count == 0) + { + return; + } + + switch (arguments[0]) + { + {{caseBuilder}} + default: + { + throw new {{Global.ArgumentException}}($"Command \"{arguments[0]}\" not found."); + } + } + } + } + """); + + if (namespaceSyntax == null) + { + context.AddSource($"{className}.g.cs", sourceBuilder.ToString()); + } + else + { + context.AddSource($"{namespaceSyntax.Name}.{className}.g.cs", sourceBuilder.OutDent().Append('}').ToString()); + } + } + + convertersGenerator.Execute(context); + } } \ No newline at end of file diff --git a/src/Antelcat.Parameterization.SourceGenerators/Utils/SourceStringBuilder.cs b/src/Antelcat.Parameterization.SourceGenerators/Utils/SourceStringBuilder.cs index 82cfb57..e8d4b6c 100644 --- a/src/Antelcat.Parameterization.SourceGenerators/Utils/SourceStringBuilder.cs +++ b/src/Antelcat.Parameterization.SourceGenerators/Utils/SourceStringBuilder.cs @@ -11,31 +11,9 @@ public class SourceStringBuilder private int currentIndentCount; private bool isLineStart = true; - public SourceStringBuilder(string indentString = "\t", int indentCount = 1, int initialIndentCount = 0, int capacity = 16) + public SourceStringBuilder(string indentString = " ", int initialIndentCount = 0, int capacity = 16) { - switch (indentCount) - { - case <= 0: - { - throw new ArgumentOutOfRangeException(nameof(indentCount)); - } - case 1: - { - this.indentString = indentString; - break; - } - default: - { - var builder = new StringBuilder(indentString.Length * indentCount); - for (var i = 0; i < indentCount; i++) - { - builder.Append(indentString); - } - - this.indentString = builder.ToString(); - break; - } - } + this.indentString = indentString; if (initialIndentCount < 0) throw new ArgumentOutOfRangeException(nameof(initialIndentCount)); currentIndentCount = initialIndentCount; @@ -73,7 +51,7 @@ public SourceStringBuilder Append(string value) AppendIndent(); stringBuilder.AppendLine(line); isLineStart = true; - } + } } return this; @@ -92,7 +70,7 @@ public SourceStringBuilder Append(char value) stringBuilder.Append(value); isLineStart = value == '\n'; - + return this; } diff --git a/src/Antelcat.Parameterization/Antelcat.Parameterization.csproj b/src/Antelcat.Parameterization/Antelcat.Parameterization.csproj index 8c3316e..216aae4 100644 --- a/src/Antelcat.Parameterization/Antelcat.Parameterization.csproj +++ b/src/Antelcat.Parameterization/Antelcat.Parameterization.csproj @@ -4,7 +4,7 @@ netstandard2.0 preview enable - 1.0.1 + 1.1.0 Antelcat 1.0.1 1.0.1