diff --git a/samples/DotNetCampus.CommandLine.Sample/DefaultOptions.cs b/samples/DotNetCampus.CommandLine.Sample/DefaultOptions.cs index da142255..d9281106 100644 --- a/samples/DotNetCampus.CommandLine.Sample/DefaultOptions.cs +++ b/samples/DotNetCampus.CommandLine.Sample/DefaultOptions.cs @@ -7,6 +7,9 @@ namespace DotNetCampus.Cli; internal class DefaultOptions { + [RawArguments] + public required string[] MainArgs { get; init; } + [Option(LocalizableDescription = nameof(LocalizableStrings.SamplePropertyDescription))] public string? DefaultText { get; set; } diff --git a/src/DotNetCampus.CommandLine.Analyzer/AnalyzerReleases.Shipped.md b/src/DotNetCampus.CommandLine.Analyzer/AnalyzerReleases.Shipped.md index 66634c3c..19402e2d 100644 --- a/src/DotNetCampus.CommandLine.Analyzer/AnalyzerReleases.Shipped.md +++ b/src/DotNetCampus.CommandLine.Analyzer/AnalyzerReleases.Shipped.md @@ -9,6 +9,7 @@ Rule ID | Category | Severity | Notes DCL101 | DotNetCampus.AvoidBugs | Warning | DCL201 | DotNetCampus.CodeFixOnly | Hidden | DCL202 | DotNetCampus.RuntimeException | Error | +DCL203 | DotNetCampus.Mechanism | Error | ## Release 3.2 diff --git a/src/DotNetCampus.CommandLine.Analyzer/Analyzers/ConvertOptionProperty/FindOptionPropertyTypeAnalyzer.cs b/src/DotNetCampus.CommandLine.Analyzer/Analyzers/FindOptionPropertyTypeAnalyzer.cs similarity index 72% rename from src/DotNetCampus.CommandLine.Analyzer/Analyzers/ConvertOptionProperty/FindOptionPropertyTypeAnalyzer.cs rename to src/DotNetCampus.CommandLine.Analyzer/Analyzers/FindOptionPropertyTypeAnalyzer.cs index 604c5db7..ec45cd27 100644 --- a/src/DotNetCampus.CommandLine.Analyzer/Analyzers/ConvertOptionProperty/FindOptionPropertyTypeAnalyzer.cs +++ b/src/DotNetCampus.CommandLine.Analyzer/Analyzers/FindOptionPropertyTypeAnalyzer.cs @@ -4,25 +4,32 @@ using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.Diagnostics; -namespace DotNetCampus.CommandLine.Analyzers.ConvertOptionProperty; +namespace DotNetCampus.CommandLine.Analyzers; [DiagnosticAnalyzer(LanguageNames.CSharp)] public class FindOptionPropertyTypeAnalyzer : DiagnosticAnalyzer { - private readonly ImmutableHashSet _nonGenericTypeNames = + private readonly HashSet _nonGenericTypeNames = [ "String", "string", "Boolean", "bool", "Byte", "byte", "Int16", "short", "UInt16", "ushort", "Int32", "int", "UInt32", "uint", "Int64", "long", "UInt64", "ulong", "Single", "float", "Double", "double", "Decimal", "decimal", "IList", "ICollection", "IEnumerable", ]; - private readonly ImmutableHashSet _oneGenericTypeNames = + private readonly HashSet _oneGenericTypeNames = [ "[]", "ImmutableArray", "List", "IList", "IReadOnlyList", "ImmutableHashSet", "Collection", "ICollection", "IReadOnlyCollection", "IEnumerable", ]; - private readonly ImmutableHashSet _twoGenericTypeNames = ["ImmutableDictionary", "Dictionary", "IDictionary", "IReadOnlyDictionary", "KeyValuePair"]; - private readonly ImmutableHashSet _genericKeyArgumentTypeNames = ["String", "string"]; - private readonly ImmutableHashSet _genericArgumentTypeNames = ["String", "string"]; + private readonly HashSet _rawArgumentsGenericTypeNames = + [ + "[]", "IList", "IReadOnlyList", "ICollection", "IReadOnlyCollection", "IEnumerable", + ]; + + private readonly HashSet _twoGenericTypeNames = + ["ImmutableDictionary", "Dictionary", "IDictionary", "IReadOnlyDictionary", "KeyValuePair"]; + + private readonly HashSet _genericKeyArgumentTypeNames = ["String", "string"]; + private readonly HashSet _genericArgumentTypeNames = ["String", "string"]; /// /// Supported diagnostics. @@ -31,6 +38,7 @@ public class FindOptionPropertyTypeAnalyzer : DiagnosticAnalyzer [ Diagnostics.DCL201_SupportedOptionPropertyType, Diagnostics.DCL202_NotSupportedOptionPropertyType, + Diagnostics.DCL203_NotSupportedRawArgumentsPropertyType, ]; /// @@ -56,6 +64,10 @@ private void AnalyzeProperty(SyntaxNodeAnalysisContext context) context.Compilation.GetTypeByMetadataName("DotNetCampus.Cli.Compiler.OptionAttribute"), context.Compilation.GetTypeByMetadataName("DotNetCampus.Cli.Compiler.ValueAttribute"), }; + var rawArgumentsTypes = new[] + { + context.Compilation.GetTypeByMetadataName("DotNetCampus.Cli.Compiler.RawArgumentsAttribute"), + }; foreach (var attributeSyntax in propertyNode.AttributeLists.SelectMany(x => x.Attributes)) { @@ -69,8 +81,11 @@ private void AnalyzeProperty(SyntaxNodeAnalysisContext context) if (attributeName != null) { var attributeType = context.SemanticModel.GetTypeInfo(attributeSyntax).Type; - var isTheAttributeType = optionTypes.Any(x => SymbolEqualityComparer.Default.Equals(x, attributeType)); - if (isTheAttributeType) + var isOptionAttributeType = optionTypes.Any(x => SymbolEqualityComparer.Default.Equals(x, attributeType)); + var isRawArgumentsAttributeType = rawArgumentsTypes.Any(x => SymbolEqualityComparer.Default.Equals(x, attributeType)); + + // [Option], [Value] + if (isOptionAttributeType) { var isValidPropertyUsage = AnalyzeOptionPropertyType(context.SemanticModel, propertyNode); var diagnostic = CreateDiagnosticForTypeSyntax( @@ -81,6 +96,20 @@ private void AnalyzeProperty(SyntaxNodeAnalysisContext context) context.ReportDiagnostic(diagnostic); break; } + + // [RawArguments] + if (isRawArgumentsAttributeType) + { + var isValidPropertyUsage = AnalyzeRawArgumentsPropertyType(context.SemanticModel, propertyNode); + if (!isValidPropertyUsage) + { + var diagnostic = CreateDiagnosticForTypeSyntax( + Diagnostics.DCL203_NotSupportedRawArgumentsPropertyType, + propertyNode); + context.ReportDiagnostic(diagnostic); + break; + } + } } } } @@ -99,14 +128,11 @@ private Diagnostic CreateDiagnosticForTypeSyntax(DiagnosticDescriptor rule, Prop } /// - /// Find LongName argument from the OptionAttribute. + /// Check whether the property type is supported by OptionAttribute or ValueAttribute. /// /// /// - /// - /// typeName: the LongName value. - /// location: the syntax tree location of the LongName argument value. - /// + /// private bool AnalyzeOptionPropertyType(SemanticModel semanticModel, PropertyDeclarationSyntax propertySyntax) { var propertyTypeSyntax = propertySyntax.Type; @@ -149,6 +175,28 @@ private bool AnalyzeOptionPropertyType(SemanticModel semanticModel, PropertyDecl return false; } + /// + /// Check whether the property type is supported by RawArgumentsAttribute. + /// + /// + /// + /// + private bool AnalyzeRawArgumentsPropertyType(SemanticModel semanticModel, PropertyDeclarationSyntax propertySyntax) + { + var propertyTypeSyntax = propertySyntax.Type; + string typeName = GetTypeName(propertyTypeSyntax); + var (genericType0, genericType1) = GetGenericTypeNames(propertyTypeSyntax); + + if (IsRawArgumentsGenericType(typeName) + && genericType0 != null + && IsGenericArgumentType(genericType0)) + { + return true; + } + + return false; + } + private string GetTypeName(TypeSyntax typeSyntax) { if (typeSyntax is NullableTypeSyntax nullableTypeSyntax) @@ -222,6 +270,9 @@ private bool IsNonGenericType(string typeName) private bool IsOneGenericType(string typeName) => _oneGenericTypeNames.Contains(typeName, StringComparer.Ordinal); + private bool IsRawArgumentsGenericType(string typeName) + => _rawArgumentsGenericTypeNames.Contains(typeName, StringComparer.Ordinal); + private bool IsTwoGenericType(string typeName) => _twoGenericTypeNames.Contains(typeName, StringComparer.Ordinal); diff --git a/src/DotNetCampus.CommandLine.Analyzer/Analyzers/ConvertOptionProperty/ConvertOptionPropertyTypeCodeFix.cs b/src/DotNetCampus.CommandLine.Analyzer/CodeFixes/ConvertOptionProperty/ConvertOptionPropertyTypeCodeFix.cs similarity index 97% rename from src/DotNetCampus.CommandLine.Analyzer/Analyzers/ConvertOptionProperty/ConvertOptionPropertyTypeCodeFix.cs rename to src/DotNetCampus.CommandLine.Analyzer/CodeFixes/ConvertOptionProperty/ConvertOptionPropertyTypeCodeFix.cs index 3547eabc..26821f47 100644 --- a/src/DotNetCampus.CommandLine.Analyzer/Analyzers/ConvertOptionProperty/ConvertOptionPropertyTypeCodeFix.cs +++ b/src/DotNetCampus.CommandLine.Analyzer/CodeFixes/ConvertOptionProperty/ConvertOptionPropertyTypeCodeFix.cs @@ -3,7 +3,7 @@ using Microsoft.CodeAnalysis.CodeFixes; using Microsoft.CodeAnalysis.CSharp.Syntax; -namespace DotNetCampus.CommandLine.Analyzers.ConvertOptionProperty; +namespace DotNetCampus.CommandLine.CodeFixes.ConvertOptionProperty; public abstract class ConvertOptionPropertyTypeCodeFix : CodeFixProvider { diff --git a/src/DotNetCampus.CommandLine.Analyzer/Analyzers/ConvertOptionProperty/OptionPropertyTypeToBooleanCodeFix.cs b/src/DotNetCampus.CommandLine.Analyzer/CodeFixes/ConvertOptionProperty/OptionPropertyTypeToBooleanCodeFix.cs similarity index 94% rename from src/DotNetCampus.CommandLine.Analyzer/Analyzers/ConvertOptionProperty/OptionPropertyTypeToBooleanCodeFix.cs rename to src/DotNetCampus.CommandLine.Analyzer/CodeFixes/ConvertOptionProperty/OptionPropertyTypeToBooleanCodeFix.cs index 5dd662a2..da907530 100644 --- a/src/DotNetCampus.CommandLine.Analyzer/Analyzers/ConvertOptionProperty/OptionPropertyTypeToBooleanCodeFix.cs +++ b/src/DotNetCampus.CommandLine.Analyzer/CodeFixes/ConvertOptionProperty/OptionPropertyTypeToBooleanCodeFix.cs @@ -6,7 +6,7 @@ using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; -namespace DotNetCampus.CommandLine.Analyzers.ConvertOptionProperty; +namespace DotNetCampus.CommandLine.CodeFixes.ConvertOptionProperty; [ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(OptionPropertyTypeToBooleanCodeFix)), Shared] public class OptionPropertyTypeToBooleanCodeFix : ConvertOptionPropertyTypeCodeFix diff --git a/src/DotNetCampus.CommandLine.Analyzer/Analyzers/ConvertOptionProperty/OptionPropertyTypeToDictionaryCodeFix.cs b/src/DotNetCampus.CommandLine.Analyzer/CodeFixes/ConvertOptionProperty/OptionPropertyTypeToDictionaryCodeFix.cs similarity index 95% rename from src/DotNetCampus.CommandLine.Analyzer/Analyzers/ConvertOptionProperty/OptionPropertyTypeToDictionaryCodeFix.cs rename to src/DotNetCampus.CommandLine.Analyzer/CodeFixes/ConvertOptionProperty/OptionPropertyTypeToDictionaryCodeFix.cs index c759ddcd..9c065813 100644 --- a/src/DotNetCampus.CommandLine.Analyzer/Analyzers/ConvertOptionProperty/OptionPropertyTypeToDictionaryCodeFix.cs +++ b/src/DotNetCampus.CommandLine.Analyzer/CodeFixes/ConvertOptionProperty/OptionPropertyTypeToDictionaryCodeFix.cs @@ -7,7 +7,7 @@ using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.Simplification; -namespace DotNetCampus.CommandLine.Analyzers.ConvertOptionProperty; +namespace DotNetCampus.CommandLine.CodeFixes.ConvertOptionProperty; [ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(OptionPropertyTypeToDictionaryCodeFix)), Shared] public class OptionPropertyTypeToDictionaryCodeFix : ConvertOptionPropertyTypeCodeFix diff --git a/src/DotNetCampus.CommandLine.Analyzer/Analyzers/ConvertOptionProperty/OptionPropertyTypeToDoubleCodeFix.cs b/src/DotNetCampus.CommandLine.Analyzer/CodeFixes/ConvertOptionProperty/OptionPropertyTypeToDoubleCodeFix.cs similarity index 94% rename from src/DotNetCampus.CommandLine.Analyzer/Analyzers/ConvertOptionProperty/OptionPropertyTypeToDoubleCodeFix.cs rename to src/DotNetCampus.CommandLine.Analyzer/CodeFixes/ConvertOptionProperty/OptionPropertyTypeToDoubleCodeFix.cs index 5abf5a99..ffbe6243 100644 --- a/src/DotNetCampus.CommandLine.Analyzer/Analyzers/ConvertOptionProperty/OptionPropertyTypeToDoubleCodeFix.cs +++ b/src/DotNetCampus.CommandLine.Analyzer/CodeFixes/ConvertOptionProperty/OptionPropertyTypeToDoubleCodeFix.cs @@ -6,7 +6,7 @@ using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; -namespace DotNetCampus.CommandLine.Analyzers.ConvertOptionProperty; +namespace DotNetCampus.CommandLine.CodeFixes.ConvertOptionProperty; [ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(OptionPropertyTypeToDoubleCodeFix)), Shared] public class OptionPropertyTypeToDoubleCodeFix : ConvertOptionPropertyTypeCodeFix diff --git a/src/DotNetCampus.CommandLine.Analyzer/Analyzers/ConvertOptionProperty/OptionPropertyTypeToInt32CodeFix.cs b/src/DotNetCampus.CommandLine.Analyzer/CodeFixes/ConvertOptionProperty/OptionPropertyTypeToInt32CodeFix.cs similarity index 94% rename from src/DotNetCampus.CommandLine.Analyzer/Analyzers/ConvertOptionProperty/OptionPropertyTypeToInt32CodeFix.cs rename to src/DotNetCampus.CommandLine.Analyzer/CodeFixes/ConvertOptionProperty/OptionPropertyTypeToInt32CodeFix.cs index 2105beea..11a73202 100644 --- a/src/DotNetCampus.CommandLine.Analyzer/Analyzers/ConvertOptionProperty/OptionPropertyTypeToInt32CodeFix.cs +++ b/src/DotNetCampus.CommandLine.Analyzer/CodeFixes/ConvertOptionProperty/OptionPropertyTypeToInt32CodeFix.cs @@ -6,7 +6,7 @@ using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; -namespace DotNetCampus.CommandLine.Analyzers.ConvertOptionProperty; +namespace DotNetCampus.CommandLine.CodeFixes.ConvertOptionProperty; [ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(OptionPropertyTypeToInt32CodeFix)), Shared] public class OptionPropertyTypeToInt32CodeFix : ConvertOptionPropertyTypeCodeFix diff --git a/src/DotNetCampus.CommandLine.Analyzer/Analyzers/ConvertOptionProperty/OptionPropertyTypeToListCodeFix.cs b/src/DotNetCampus.CommandLine.Analyzer/CodeFixes/ConvertOptionProperty/OptionPropertyTypeToListCodeFix.cs similarity index 95% rename from src/DotNetCampus.CommandLine.Analyzer/Analyzers/ConvertOptionProperty/OptionPropertyTypeToListCodeFix.cs rename to src/DotNetCampus.CommandLine.Analyzer/CodeFixes/ConvertOptionProperty/OptionPropertyTypeToListCodeFix.cs index e8b937a7..6f9f0041 100644 --- a/src/DotNetCampus.CommandLine.Analyzer/Analyzers/ConvertOptionProperty/OptionPropertyTypeToListCodeFix.cs +++ b/src/DotNetCampus.CommandLine.Analyzer/CodeFixes/ConvertOptionProperty/OptionPropertyTypeToListCodeFix.cs @@ -7,7 +7,7 @@ using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.Simplification; -namespace DotNetCampus.CommandLine.Analyzers.ConvertOptionProperty; +namespace DotNetCampus.CommandLine.CodeFixes.ConvertOptionProperty; [ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(OptionPropertyTypeToListCodeFix)), Shared] public class OptionPropertyTypeToListCodeFix : ConvertOptionPropertyTypeCodeFix diff --git a/src/DotNetCampus.CommandLine.Analyzer/Analyzers/ConvertOptionProperty/OptionPropertyTypeToStringCodeFix.cs b/src/DotNetCampus.CommandLine.Analyzer/CodeFixes/ConvertOptionProperty/OptionPropertyTypeToStringCodeFix.cs similarity index 94% rename from src/DotNetCampus.CommandLine.Analyzer/Analyzers/ConvertOptionProperty/OptionPropertyTypeToStringCodeFix.cs rename to src/DotNetCampus.CommandLine.Analyzer/CodeFixes/ConvertOptionProperty/OptionPropertyTypeToStringCodeFix.cs index d4238508..5394eb76 100644 --- a/src/DotNetCampus.CommandLine.Analyzer/Analyzers/ConvertOptionProperty/OptionPropertyTypeToStringCodeFix.cs +++ b/src/DotNetCampus.CommandLine.Analyzer/CodeFixes/ConvertOptionProperty/OptionPropertyTypeToStringCodeFix.cs @@ -6,7 +6,7 @@ using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; -namespace DotNetCampus.CommandLine.Analyzers.ConvertOptionProperty; +namespace DotNetCampus.CommandLine.CodeFixes.ConvertOptionProperty; [ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(OptionPropertyTypeToStringCodeFix)), Shared] public class OptionPropertyTypeToStringCodeFix : ConvertOptionPropertyTypeCodeFix diff --git a/src/DotNetCampus.CommandLine.Analyzer/Analyzers/OptionLongNameMustBeKebabCaseCodeFixProvider.cs b/src/DotNetCampus.CommandLine.Analyzer/CodeFixes/OptionLongNameMustBeKebabCaseCodeFixProvider.cs similarity index 98% rename from src/DotNetCampus.CommandLine.Analyzer/Analyzers/OptionLongNameMustBeKebabCaseCodeFixProvider.cs rename to src/DotNetCampus.CommandLine.Analyzer/CodeFixes/OptionLongNameMustBeKebabCaseCodeFixProvider.cs index 6e227665..55d98ee9 100644 --- a/src/DotNetCampus.CommandLine.Analyzer/Analyzers/OptionLongNameMustBeKebabCaseCodeFixProvider.cs +++ b/src/DotNetCampus.CommandLine.Analyzer/CodeFixes/OptionLongNameMustBeKebabCaseCodeFixProvider.cs @@ -1,14 +1,14 @@ using System.Collections.Immutable; using System.Composition; +using DotNetCampus.Cli.Utils; +using DotNetCampus.CommandLine.Properties; using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CodeFixes; using Microsoft.CodeAnalysis.CodeActions; +using Microsoft.CodeAnalysis.CodeFixes; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; -using DotNetCampus.CommandLine.Properties; -using DotNetCampus.Cli.Utils; -namespace DotNetCampus.CommandLine.Analyzers; +namespace DotNetCampus.CommandLine.CodeFixes; /// /// [Option("LongName")] diff --git a/src/DotNetCampus.CommandLine.Analyzer/Diagnostics.cs b/src/DotNetCampus.CommandLine.Analyzer/Diagnostics.cs index 6f90c013..f566abad 100644 --- a/src/DotNetCampus.CommandLine.Analyzer/Diagnostics.cs +++ b/src/DotNetCampus.CommandLine.Analyzer/Diagnostics.cs @@ -44,11 +44,22 @@ public static class Diagnostics description: Localize(nameof(DCL202_Description)), helpLinkUri: Url(NotSupportedOptionPropertyType)); + public static readonly DiagnosticDescriptor DCL203_NotSupportedRawArgumentsPropertyType = new DiagnosticDescriptor( + nameof(DCL203), + Localize(nameof(DCL203)), + Localize(nameof(DCL203_Message)), + Categories.Mechanism, + DiagnosticSeverity.Error, + isEnabledByDefault: true, + description: Localize(nameof(DCL203_Description)), + helpLinkUri: Url(NotSupportedRawArgumentsPropertyType)); + #endregion public const string OptionLongNameMustBeKebabCase = "DCL101"; public const string SupportedOptionPropertyType = "DCL201"; public const string NotSupportedOptionPropertyType = "DCL202"; + public const string NotSupportedRawArgumentsPropertyType = "DCL203"; private static class Categories { diff --git a/src/DotNetCampus.CommandLine.Analyzer/Generators/BuilderGenerator.cs b/src/DotNetCampus.CommandLine.Analyzer/Generators/BuilderGenerator.cs index 3bef5cca..991a06bd 100644 --- a/src/DotNetCampus.CommandLine.Analyzer/Generators/BuilderGenerator.cs +++ b/src/DotNetCampus.CommandLine.Analyzer/Generators/BuilderGenerator.cs @@ -64,8 +64,10 @@ private string GenerateCommandObjectCreatorCode(CommandObjectGeneratingModel mod // | 0 | 1 | 1 | 赋值 | // | 1 | 1 | 1 | 赋值 | + var initRawArgumentsProperties = model.RawArgumentsProperties.Where(x => x.IsRequired || x.IsInitOnly).ToImmutableArray(); var initOptionProperties = model.OptionProperties.Where(x => x.IsRequired || x.IsInitOnly).ToImmutableArray(); var initValueProperties = model.ValueProperties.Where(x => x.IsRequired || x.IsInitOnly).ToImmutableArray(); + var setRawArgumentsProperties = model.RawArgumentsProperties.Where(x => !x.IsRequired && !x.IsInitOnly).ToImmutableArray(); var setOptionProperties = model.OptionProperties.Where(x => !x.IsRequired && !x.IsInitOnly).ToImmutableArray(); var setValueProperties = model.ValueProperties.Where(x => !x.IsRequired && !x.IsInitOnly).ToImmutableArray(); return $$""" @@ -82,11 +84,25 @@ public static object CreateInstance(global::DotNetCampus.Cli.CommandLine command var caseSensitive = commandLine.DefaultCaseSensitive; var result = new {{model.CommandObjectType.ToGlobalDisplayString()}} { -{{(initOptionProperties.Length is 0 ? " // There is no option to be initialized." : string.Join("\n", initOptionProperties.Select(GenerateOptionPropertyAssignment)))}} -{{(initValueProperties.Length is 0 ? " // There is no positional argument to be initialized." : string.Join("\n", initValueProperties.Select((x, i) => GenerateValuePropertyAssignment(model, x, i))))}} + // 1. [RawArguments] +{{(initRawArgumentsProperties.Length is 0 ? " // MainArgs = commandLine.CommandLineArguments," : string.Join("\n", initRawArgumentsProperties.Select(GenerateRawArgumentsPropertyAssignment)))}} + + // 2. [Option] +{{(initOptionProperties .Length is 0 ? " // There is no option to be initialized." : string.Join("\n", initOptionProperties.Select(GenerateOptionPropertyAssignment)))}} + + // 3. [Value] +{{(initValueProperties .Length is 0 ? " // There is no positional argument to be initialized." : string.Join("\n", initValueProperties.Select((x, i) => GenerateValuePropertyAssignment(model, x, i))))}} }; -{{(setOptionProperties.Length is 0 ? " // There is no option to be assigned." : string.Join("\n", setOptionProperties.Select(GenerateOptionPropertyAssignment)))}} -{{(setValueProperties.Length is 0 ? " // There is no positional argument to be assigned." : string.Join("\n", setValueProperties.Select((x, i) => GenerateValuePropertyAssignment(model, x, i))))}} + + // 1. [RawArguments] +{{(setRawArgumentsProperties.Length is 0 ? " // result.MainArgs = commandLine.CommandLineArguments;" : string.Join("\n", setRawArgumentsProperties.Select(GenerateRawArgumentsPropertyAssignment)))}} + + // 2. [Option] +{{(setOptionProperties .Length is 0 ? " // There is no option to be assigned." : string.Join("\n", setOptionProperties.Select(GenerateOptionPropertyAssignment)))}} + + // 3. [Value] +{{(setValueProperties .Length is 0 ? " // There is no positional argument to be assigned." : string.Join("\n", setValueProperties.Select((x, i) => GenerateValuePropertyAssignment(model, x, i))))}} + return result; } } @@ -182,6 +198,23 @@ private string GenerateValuePropertyAssignment(CommandObjectGeneratingModel mode } } + private string GenerateRawArgumentsPropertyAssignment(RawArgumentsPropertyGeneratingModel property) + { + var isInitProperty = property.IsRequired || property.IsInitOnly; + if (isInitProperty) + { + return $""" + {property.PropertyName} = ({property.Type.ToDisplayString()})commandLine.CommandLineArguments, +"""; + } + else + { + return $$""" + result.{{property.PropertyName}} = ({{property.Type.ToDisplayString()}})commandLine.CommandLineArguments; +"""; + } + } + /// /// 获取一个方法名,调用该方法可使“命令行属性值”转换为“目标类型”。 /// diff --git a/src/DotNetCampus.CommandLine.Analyzer/Generators/ModelProviding/CommandModelProvider.cs b/src/DotNetCampus.CommandLine.Analyzer/Generators/ModelProviding/CommandModelProvider.cs index 230c7a3c..28cc8b9c 100644 --- a/src/DotNetCampus.CommandLine.Analyzer/Generators/ModelProviding/CommandModelProvider.cs +++ b/src/DotNetCampus.CommandLine.Analyzer/Generators/ModelProviding/CommandModelProvider.cs @@ -40,6 +40,7 @@ public static IncrementalValuesProvider SelectComm // 3. 拥有 [Verb] 特性 // 4. 拥有 [Option] 特性的属性 // 5. 拥有 [Value] 特性的属性 + // 6. 拥有 [RawArguments] 特性的属性 // 1. 实现 ICommandOptions 接口。 var isOptions = typeSymbol.AllInterfaces.Any(i => @@ -52,28 +53,15 @@ public static IncrementalValuesProvider SelectComm .FirstOrDefault(a => a.AttributeClass!.IsAttributeOf()); // 4. 拥有 [Option] 特性的属性。 var optionProperties = typeSymbol - .EnumerateBaseTypesRecursively() // 递归获取所有基类 - .Reverse() // (注意我们先给父类属性赋值,再给子类属性赋值) - .SelectMany(x => x.GetMembers()) // 的所有成员, - .OfType() // 然后取出属性, - .Select(OptionPropertyGeneratingModel.TryParse) // 解析出 OptionPropertyGeneratingModel。 - .OfType() - .GroupBy(x => x.PropertyName) // 按属性名去重。 - .Select(x => x.Last()) // 随后,取子类的属性(去除父类的重名属性)。 - .ToImmutableArray(); + .GetAttributedProperties(OptionPropertyGeneratingModel.TryParse); // 5. 拥有 [Value] 特性的属性。 var valueProperties = typeSymbol - .EnumerateBaseTypesRecursively() // 递归获取所有基类 - .Reverse() // (注意我们先给父类属性赋值,再给子类属性赋值) - .SelectMany(x => x.GetMembers()) // 的所有成员, - .OfType() // 然后取出属性, - .Select(ValuePropertyGeneratingModel.TryParse) // 解析出 ValuePropertyGeneratingModel。 - .OfType() - .GroupBy(x => x.PropertyName) // 按属性名去重。 - .Select(x => x.Last()) // 随后,取子类的属性(去除父类的重名属性)。 - .ToImmutableArray(); - - if (!isOptions && !isHandler && attribute is null && optionProperties.IsEmpty && valueProperties.IsEmpty) + .GetAttributedProperties(ValuePropertyGeneratingModel.TryParse); + // 6. 拥有 [RawArguments] 特性的属性。 + var rawArgumentsProperties = typeSymbol + .GetAttributedProperties(RawArgumentsPropertyGeneratingModel.TryParse); + + if (!isOptions && !isHandler && attribute is null && optionProperties.IsEmpty && valueProperties.IsEmpty && rawArgumentsProperties.IsEmpty) { // 不是命令行选项类型。 return null; @@ -90,6 +78,7 @@ public static IncrementalValuesProvider SelectComm IsHandler = isHandler, OptionProperties = optionProperties, ValueProperties = valueProperties, + RawArgumentsProperties = rawArgumentsProperties, }; }) .Where(m => m is not null) @@ -140,6 +129,8 @@ internal record CommandObjectGeneratingModel public required ImmutableArray ValueProperties { get; init; } + public required ImmutableArray RawArgumentsProperties { get; init; } + public string GetBuilderTypeName() => GetBuilderTypeName(CommandObjectType); public static string GetBuilderTypeName(INamedTypeSymbol commandObjectType) @@ -334,6 +325,37 @@ internal record ValuePropertyGeneratingModel } } +internal record RawArgumentsPropertyGeneratingModel +{ + public required string PropertyName { get; init; } + + public required ITypeSymbol Type { get; init; } + + public required bool IsRequired { get; init; } + + public required bool IsInitOnly { get; init; } + + public required bool IsNullable { get; init; } + + public static RawArgumentsPropertyGeneratingModel? TryParse(IPropertySymbol propertySymbol) + { + var rawArgumentsAttribute = propertySymbol.GetAttributes().FirstOrDefault(a => a.AttributeClass!.IsAttributeOf()); + if (rawArgumentsAttribute is null) + { + return null; + } + + return new RawArgumentsPropertyGeneratingModel + { + PropertyName = propertySymbol.Name, + Type = propertySymbol.Type, + IsRequired = propertySymbol.IsRequired, + IsInitOnly = propertySymbol.SetMethod?.IsInitOnly ?? false, + IsNullable = propertySymbol.Type.NullableAnnotation == NullableAnnotation.Annotated, + }; + } +} + internal record AssemblyCommandsGeneratingModel { public required string Namespace { get; init; } @@ -352,4 +374,21 @@ public static IEnumerable EnumerateBaseTypesRecursively(this ITypeS current = current.BaseType; } } + + public static ImmutableArray GetAttributedProperties(this ITypeSymbol typeSymbol, + Func propertyParser) + where TModel : class + { + return typeSymbol + .EnumerateBaseTypesRecursively() // 递归获取所有基类 + .Reverse() // (注意我们先给父类属性赋值,再给子类属性赋值) + .SelectMany(x => x.GetMembers()) // 的所有成员, + .OfType() // 然后取出属性, + .Select(x => (PropertyName: x.Name, Model: propertyParser(x))) // 解析出 OptionPropertyGeneratingModel。 + .Where(x => x.Model is not null) + .GroupBy(x => x.PropertyName) // 按属性名去重。 + .Select(x => x.Last().Model) // 随后,取子类的属性(去除父类的重名属性)。 + .Cast() + .ToImmutableArray(); + } } diff --git a/src/DotNetCampus.CommandLine.Analyzer/Properties/Localizations.Designer.cs b/src/DotNetCampus.CommandLine.Analyzer/Properties/Localizations.Designer.cs index 5f7c7736..4eabbdcd 100644 --- a/src/DotNetCampus.CommandLine.Analyzer/Properties/Localizations.Designer.cs +++ b/src/DotNetCampus.CommandLine.Analyzer/Properties/Localizations.Designer.cs @@ -177,7 +177,7 @@ public static string DCL201_Message { } /// - /// Looks up a localized string similar to Not supported property type. + /// Looks up a localized string similar to Not supported command-line property type. /// public static string DCL202 { get { @@ -186,7 +186,7 @@ public static string DCL202 { } /// - /// Looks up a localized string similar to This property has the type '{0}' which is not built-in supported. It's recommended to use bool/string/IReadOnlyList<string> or other types that the code fix will suggest you change instead or add a custom converter on your Value or Option attribute.. + /// Looks up a localized string similar to As a command-line option, the property type '{0}' is not supported. It is recommended to use bool/string/IReadOnlyList<string> or another type that the code fix may suggest, or add a custom converter on your Value or Option attribute.. /// public static string DCL202_Description { get { @@ -195,12 +195,39 @@ public static string DCL202_Description { } /// - /// Looks up a localized string similar to This property has the type '{0}' which is not built-in supported.. + /// Looks up a localized string similar to As a command-line option, the property type '{0}' is not supported.. /// public static string DCL202_Message { get { return ResourceManager.GetString("DCL202_Message", resourceCulture); } } + + /// + /// Looks up a localized string similar to Not supported raw command-line argument type. + /// + public static string DCL203 { + get { + return ResourceManager.GetString("DCL203", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to To receive raw command-line arguments (typically string[]), you should also use the same property type string[] or the interface IReadOnlyList<string>.. + /// + public static string DCL203_Description { + get { + return ResourceManager.GetString("DCL203_Description", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to [RawArguments] can only be applied to properties of type string[] or IReadOnlyList<string>.. + /// + public static string DCL203_Message { + get { + return ResourceManager.GetString("DCL203_Message", resourceCulture); + } + } } } diff --git a/src/DotNetCampus.CommandLine.Analyzer/Properties/Localizations.resx b/src/DotNetCampus.CommandLine.Analyzer/Properties/Localizations.resx index 953406d1..59c35002 100644 --- a/src/DotNetCampus.CommandLine.Analyzer/Properties/Localizations.resx +++ b/src/DotNetCampus.CommandLine.Analyzer/Properties/Localizations.resx @@ -141,13 +141,13 @@ Use 'string' type instead - This property has the type '{0}' which is not built-in supported. It's recommended to use bool/string/IReadOnlyList<string> or other types that the code fix will suggest you change instead or add a custom converter on your Value or Option attribute. + As a command-line option, the property type '{0}' is not supported. It is recommended to use bool/string/IReadOnlyList<string> or another type that the code fix may suggest, or add a custom converter on your Value or Option attribute. - This property has the type '{0}' which is not built-in supported. + As a command-line option, the property type '{0}' is not supported. - Not supported property type + Not supported command-line property type The command-line option definition names should be kebab-case, even though you can use any kind of style in the command line environment. @@ -170,4 +170,13 @@ Recommended option property type + + Not supported raw command-line argument type + + + [RawArguments] can only be applied to properties of type string[] or IReadOnlyList<string>. + + + To receive raw command-line arguments (typically string[]), you should also use the same property type string[] or the interface IReadOnlyList<string>. + diff --git a/src/DotNetCampus.CommandLine.Analyzer/Properties/Localizations.zh-hans-cn.resx b/src/DotNetCampus.CommandLine.Analyzer/Properties/Localizations.zh-hans.resx similarity index 86% rename from src/DotNetCampus.CommandLine.Analyzer/Properties/Localizations.zh-hans-cn.resx rename to src/DotNetCampus.CommandLine.Analyzer/Properties/Localizations.zh-hans.resx index 99137b86..62ef8131 100644 --- a/src/DotNetCampus.CommandLine.Analyzer/Properties/Localizations.zh-hans-cn.resx +++ b/src/DotNetCampus.CommandLine.Analyzer/Properties/Localizations.zh-hans.resx @@ -63,4 +63,13 @@ 符合要求的命令行选项类型 + + 不支持此类型的原始命令行参数 + + + [RawArguments] 只能标记在 string[] 或 IReadOnlyList<string> 属性上。 + + + 要接收原始命令行参数(通常是 string[] 类型),你应该也使用相同的属性类型 string[] 或其接口 IReadOnlyList<string>。 + diff --git a/src/DotNetCampus.CommandLine/CommandLineStyle.cs b/src/DotNetCampus.CommandLine/CommandLineStyle.cs index 5d03eac0..58f0c29f 100644 --- a/src/DotNetCampus.CommandLine/CommandLineStyle.cs +++ b/src/DotNetCampus.CommandLine/CommandLineStyle.cs @@ -15,7 +15,7 @@ public enum CommandLineStyle /// 灵活风格是一种包容性最强的命令行参数风格,旨在让不熟悉命令行操作的用户也能轻松使用。它通过智能识别尝试理解用户输入的意图,支持多种参数格式共存。
///
/// 详细规则:
- /// 1. 参数前缀支持多种形式:双破折线(--), 单破折线(-), 斜杠(/)
+ /// 1. 参数前缀支持多种形式:双破折线(--), 单破折线(-), 斜杠(/,仅 Windows)
/// 2. 参数值分隔符兼容多种形式:空格、等号(=)、冒号(:)
/// 3. 参数命名风格兼容kebab-case(--parameter-name)、PascalCase(-ParameterName)和camelCase
/// 4. 默认大小写不敏感,便于初学者使用
@@ -46,7 +46,7 @@ public enum CommandLineStyle /// app -p:value # 短选项冒号分隔 /// app -pvalue # 短选项直接跟值(GNU风格) /// - /// # 斜杠选项(Windows风格) + /// # 斜杠选项(Windows风格,仅在 Windows 系统可用) /// app /parameter value # 斜杠前缀长选项 /// app /p value # 斜杠前缀短选项 /// app /parameter:value # 斜杠前缀冒号分隔(类MSBuild) @@ -151,7 +151,7 @@ public enum CommandLineStyle /// .NET CLI风格,使用冒号分隔参数:
/// 1. 短选项形式为 -参数:值
/// 2. 长选项可以是 --参数:值
- /// 3. 也支持斜杠前缀 /参数:值 + /// 3. 也支持斜杠前缀 /参数:值(仅 Windows 环境下可用) ///
/// /// 这种风格在现代.NET工具链(dotnet CLI、NuGet、MSBuild等)和其他Microsoft工具中广泛使用。
@@ -160,7 +160,7 @@ public enum CommandLineStyle /// 1. 支持使用冒号(:)作为选项和参数值的分隔符
/// 2. 短选项以单破折线(-)开头,后跟选项名,然后是冒号和参数值
/// 3. 长选项以双破折线(--)开头,后跟选项名,然后是冒号和参数值
- /// 4. 也支持使用斜杠(/)作为选项前缀,特别是在Windows环境中
+ /// 4. 也支持使用斜杠(/)作为选项前缀,仅在Windows环境中可用
/// 5. 参数名可以是单个字母、多字符缩写或完整的单词,支持各种命名规范
/// 6. 布尔选项通常不需要值,或使用true/false、on/off等值
/// 7. 多个短选项一般不支持合并(与GNU/POSIX不同)
@@ -185,7 +185,7 @@ public enum CommandLineStyle /// dotnet test --test-category:UnitTest # kebab-case,双破折号 /// dotnet run --projectName:App1 # camelCase,双破折号 /// - /// # 斜杠前缀(Windows风格) + /// # 斜杠选项(Windows风格,仅在 Windows 系统可用) /// msbuild /p:Configuration=Release # MSBuild属性 /// dotnet test /blame # 启用故障分析 /// dotnet nuget push /source:feed # 指定源 diff --git a/src/DotNetCampus.CommandLine/Compiler/RawArgumentsAttribute.cs b/src/DotNetCampus.CommandLine/Compiler/RawArgumentsAttribute.cs new file mode 100644 index 00000000..8b0b51a0 --- /dev/null +++ b/src/DotNetCampus.CommandLine/Compiler/RawArgumentsAttribute.cs @@ -0,0 +1,9 @@ +namespace DotNetCampus.Cli.Compiler; + +/// +/// 标记在一个 string[] 或 IReadOnlyList<string> 类型的属性上,表示此属性将接收保留的原始命令行参数。 +/// +[AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = false)] +public sealed class RawArgumentsAttribute : CommandLineAttribute +{ +} diff --git a/src/DotNetCampus.CommandLine/Utils/Parsers/DotNetStyleParser.cs b/src/DotNetCampus.CommandLine/Utils/Parsers/DotNetStyleParser.cs index 9a947c14..ae0d74d6 100644 --- a/src/DotNetCampus.CommandLine/Utils/Parsers/DotNetStyleParser.cs +++ b/src/DotNetCampus.CommandLine/Utils/Parsers/DotNetStyleParser.cs @@ -105,8 +105,11 @@ internal readonly ref struct DotNetArgument(DotNetParsedType type) public static DotNetArgument Parse(string argument, DotNetParsedType lastType) { var isPostPositionalArgument = lastType is DotNetParsedType.PositionalArgumentSeparator or DotNetParsedType.PostPositionalArgument; + var hasPrefix = OperatingSystem.IsWindows() + ? argument.Length > 0 && (argument[0] is '-' or '/') + : argument.Length > 0 && argument[0] is '-'; - if (!isPostPositionalArgument && argument is ['-', ..] or ['/', ..]) + if (!isPostPositionalArgument && hasPrefix) { if (argument.Length is 1) { diff --git a/src/DotNetCampus.CommandLine/Utils/Parsers/FlexibleStyleParser.cs b/src/DotNetCampus.CommandLine/Utils/Parsers/FlexibleStyleParser.cs index d3e7dafd..31c22c43 100644 --- a/src/DotNetCampus.CommandLine/Utils/Parsers/FlexibleStyleParser.cs +++ b/src/DotNetCampus.CommandLine/Utils/Parsers/FlexibleStyleParser.cs @@ -106,8 +106,11 @@ internal readonly ref struct FlexibleArgument(FlexibleParsedType type) public static FlexibleArgument Parse(string argument, FlexibleParsedType lastType) { var isPostPositionalArgument = lastType is FlexibleParsedType.PositionalArgumentSeparator or FlexibleParsedType.PostPositionalArgument; + var hasPrefix = OperatingSystem.IsWindows() + ? argument.Length > 0 && (argument[0] is '-' or '/') + : argument.Length > 0 && argument[0] is '-'; - if (!isPostPositionalArgument && argument is ['-', ..] or ['/', ..]) + if (!isPostPositionalArgument && hasPrefix) { if (argument.Length is 1) {