Skip to content

Commit 9a90a23

Browse files
authored
Allow re-ordering options past subcommands (#25)
This pull request introduces several structural and functional improvements, focusing on reorganizing the project layout, adding a Roslyn analyzer for command-line attribute validation, and making enhancements to the deserialization logic for better handling of command-line arguments and parent options.
1 parent 03e927f commit 9a90a23

22 files changed

+707
-176
lines changed

Directory.Packages.props

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@
55
</PropertyGroup>
66
<ItemGroup>
77
<PackageVersion Include="BenchmarkDotNet" Version="0.15.8" />
8+
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp" Version="4.8.0" />
9+
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="4.8.0" />
10+
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp.Analyzer.Testing.XUnit" Version="1.1.2" />
811
<PackageVersion Include="Serde" Version="0.9.1" />
912
<PackageVersion Include="Spectre.Console" Version="0.54.0" />
1013
<PackageVersion Include="StaticCs" Version="0.3.1" />

bench/bench.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
</PropertyGroup>
1010

1111
<ItemGroup>
12-
<ProjectReference Include="..\src\Serde.CmdLine.csproj" />
12+
<ProjectReference Include="..\src\Serde.CmdLine\Serde.CmdLine.csproj" />
1313
</ItemGroup>
1414

1515
<ItemGroup>

cmdline.sln

Lines changed: 59 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,19 @@ Microsoft Visual Studio Solution File, Format Version 12.00
33
# Visual Studio Version 17
44
VisualStudioVersion = 17.0.31903.59
55
MinimumVisualStudioVersion = 10.0.40219.1
6+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "bench", "bench\bench.csproj", "{B16C946A-F5BA-47B7-A751-79EAEEE0296D}"
7+
EndProject
68
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{827E0CD3-B72D-47B6-A68D-7590B98EB39B}"
79
EndProject
8-
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Serde.CmdLine", "src\Serde.CmdLine.csproj", "{36E277D7-EA89-4A06-8AC2-A931C90347B5}"
10+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Serde.CmdLine", "src\Serde.CmdLine\Serde.CmdLine.csproj", "{DB38D678-1389-4C18-9BE2-D745C708FCCF}"
11+
EndProject
12+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Serde.CmdLine.Analyzers", "src\Serde.CmdLine.Analyzers\Serde.CmdLine.Analyzers.csproj", "{D8BA4FB3-519B-4D42-9803-CD775BEF29FD}"
913
EndProject
1014
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{0C88DD14-F956-CE84-757C-A364CCF449FC}"
1115
EndProject
12-
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Serde.CmdLine.Test", "test\Serde.CmdLine.Test.csproj", "{40480398-EDF1-4893-8491-3C47792F1608}"
16+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Serde.CmdLine.Test", "test\Serde.CmdLine.Test\Serde.CmdLine.Test.csproj", "{EC640983-252D-404F-9D36-52DDC421DBEF}"
1317
EndProject
14-
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "bench", "bench\bench.csproj", "{B16C946A-F5BA-47B7-A751-79EAEEE0296D}"
18+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Serde.CmdLine.Analyzers.Test", "test\Serde.CmdLine.Analyzers.Test\Serde.CmdLine.Analyzers.Test.csproj", "{828B11A8-D042-4D5F-B9BE-208AFF11C437}"
1519
EndProject
1620
Global
1721
GlobalSection(SolutionConfigurationPlatforms) = preSolution
@@ -23,30 +27,6 @@ Global
2327
Release|x86 = Release|x86
2428
EndGlobalSection
2529
GlobalSection(ProjectConfigurationPlatforms) = postSolution
26-
{36E277D7-EA89-4A06-8AC2-A931C90347B5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
27-
{36E277D7-EA89-4A06-8AC2-A931C90347B5}.Debug|Any CPU.Build.0 = Debug|Any CPU
28-
{36E277D7-EA89-4A06-8AC2-A931C90347B5}.Debug|x64.ActiveCfg = Debug|Any CPU
29-
{36E277D7-EA89-4A06-8AC2-A931C90347B5}.Debug|x64.Build.0 = Debug|Any CPU
30-
{36E277D7-EA89-4A06-8AC2-A931C90347B5}.Debug|x86.ActiveCfg = Debug|Any CPU
31-
{36E277D7-EA89-4A06-8AC2-A931C90347B5}.Debug|x86.Build.0 = Debug|Any CPU
32-
{36E277D7-EA89-4A06-8AC2-A931C90347B5}.Release|Any CPU.ActiveCfg = Release|Any CPU
33-
{36E277D7-EA89-4A06-8AC2-A931C90347B5}.Release|Any CPU.Build.0 = Release|Any CPU
34-
{36E277D7-EA89-4A06-8AC2-A931C90347B5}.Release|x64.ActiveCfg = Release|Any CPU
35-
{36E277D7-EA89-4A06-8AC2-A931C90347B5}.Release|x64.Build.0 = Release|Any CPU
36-
{36E277D7-EA89-4A06-8AC2-A931C90347B5}.Release|x86.ActiveCfg = Release|Any CPU
37-
{36E277D7-EA89-4A06-8AC2-A931C90347B5}.Release|x86.Build.0 = Release|Any CPU
38-
{40480398-EDF1-4893-8491-3C47792F1608}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
39-
{40480398-EDF1-4893-8491-3C47792F1608}.Debug|Any CPU.Build.0 = Debug|Any CPU
40-
{40480398-EDF1-4893-8491-3C47792F1608}.Debug|x64.ActiveCfg = Debug|Any CPU
41-
{40480398-EDF1-4893-8491-3C47792F1608}.Debug|x64.Build.0 = Debug|Any CPU
42-
{40480398-EDF1-4893-8491-3C47792F1608}.Debug|x86.ActiveCfg = Debug|Any CPU
43-
{40480398-EDF1-4893-8491-3C47792F1608}.Debug|x86.Build.0 = Debug|Any CPU
44-
{40480398-EDF1-4893-8491-3C47792F1608}.Release|Any CPU.ActiveCfg = Release|Any CPU
45-
{40480398-EDF1-4893-8491-3C47792F1608}.Release|Any CPU.Build.0 = Release|Any CPU
46-
{40480398-EDF1-4893-8491-3C47792F1608}.Release|x64.ActiveCfg = Release|Any CPU
47-
{40480398-EDF1-4893-8491-3C47792F1608}.Release|x64.Build.0 = Release|Any CPU
48-
{40480398-EDF1-4893-8491-3C47792F1608}.Release|x86.ActiveCfg = Release|Any CPU
49-
{40480398-EDF1-4893-8491-3C47792F1608}.Release|x86.Build.0 = Release|Any CPU
5030
{B16C946A-F5BA-47B7-A751-79EAEEE0296D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
5131
{B16C946A-F5BA-47B7-A751-79EAEEE0296D}.Debug|Any CPU.Build.0 = Debug|Any CPU
5232
{B16C946A-F5BA-47B7-A751-79EAEEE0296D}.Debug|x64.ActiveCfg = Debug|Any CPU
@@ -59,12 +39,62 @@ Global
5939
{B16C946A-F5BA-47B7-A751-79EAEEE0296D}.Release|x64.Build.0 = Release|Any CPU
6040
{B16C946A-F5BA-47B7-A751-79EAEEE0296D}.Release|x86.ActiveCfg = Release|Any CPU
6141
{B16C946A-F5BA-47B7-A751-79EAEEE0296D}.Release|x86.Build.0 = Release|Any CPU
42+
{DB38D678-1389-4C18-9BE2-D745C708FCCF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
43+
{DB38D678-1389-4C18-9BE2-D745C708FCCF}.Debug|Any CPU.Build.0 = Debug|Any CPU
44+
{DB38D678-1389-4C18-9BE2-D745C708FCCF}.Debug|x64.ActiveCfg = Debug|Any CPU
45+
{DB38D678-1389-4C18-9BE2-D745C708FCCF}.Debug|x64.Build.0 = Debug|Any CPU
46+
{DB38D678-1389-4C18-9BE2-D745C708FCCF}.Debug|x86.ActiveCfg = Debug|Any CPU
47+
{DB38D678-1389-4C18-9BE2-D745C708FCCF}.Debug|x86.Build.0 = Debug|Any CPU
48+
{DB38D678-1389-4C18-9BE2-D745C708FCCF}.Release|Any CPU.ActiveCfg = Release|Any CPU
49+
{DB38D678-1389-4C18-9BE2-D745C708FCCF}.Release|Any CPU.Build.0 = Release|Any CPU
50+
{DB38D678-1389-4C18-9BE2-D745C708FCCF}.Release|x64.ActiveCfg = Release|Any CPU
51+
{DB38D678-1389-4C18-9BE2-D745C708FCCF}.Release|x64.Build.0 = Release|Any CPU
52+
{DB38D678-1389-4C18-9BE2-D745C708FCCF}.Release|x86.ActiveCfg = Release|Any CPU
53+
{DB38D678-1389-4C18-9BE2-D745C708FCCF}.Release|x86.Build.0 = Release|Any CPU
54+
{D8BA4FB3-519B-4D42-9803-CD775BEF29FD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
55+
{D8BA4FB3-519B-4D42-9803-CD775BEF29FD}.Debug|Any CPU.Build.0 = Debug|Any CPU
56+
{D8BA4FB3-519B-4D42-9803-CD775BEF29FD}.Debug|x64.ActiveCfg = Debug|Any CPU
57+
{D8BA4FB3-519B-4D42-9803-CD775BEF29FD}.Debug|x64.Build.0 = Debug|Any CPU
58+
{D8BA4FB3-519B-4D42-9803-CD775BEF29FD}.Debug|x86.ActiveCfg = Debug|Any CPU
59+
{D8BA4FB3-519B-4D42-9803-CD775BEF29FD}.Debug|x86.Build.0 = Debug|Any CPU
60+
{D8BA4FB3-519B-4D42-9803-CD775BEF29FD}.Release|Any CPU.ActiveCfg = Release|Any CPU
61+
{D8BA4FB3-519B-4D42-9803-CD775BEF29FD}.Release|Any CPU.Build.0 = Release|Any CPU
62+
{D8BA4FB3-519B-4D42-9803-CD775BEF29FD}.Release|x64.ActiveCfg = Release|Any CPU
63+
{D8BA4FB3-519B-4D42-9803-CD775BEF29FD}.Release|x64.Build.0 = Release|Any CPU
64+
{D8BA4FB3-519B-4D42-9803-CD775BEF29FD}.Release|x86.ActiveCfg = Release|Any CPU
65+
{D8BA4FB3-519B-4D42-9803-CD775BEF29FD}.Release|x86.Build.0 = Release|Any CPU
66+
{EC640983-252D-404F-9D36-52DDC421DBEF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
67+
{EC640983-252D-404F-9D36-52DDC421DBEF}.Debug|Any CPU.Build.0 = Debug|Any CPU
68+
{EC640983-252D-404F-9D36-52DDC421DBEF}.Debug|x64.ActiveCfg = Debug|Any CPU
69+
{EC640983-252D-404F-9D36-52DDC421DBEF}.Debug|x64.Build.0 = Debug|Any CPU
70+
{EC640983-252D-404F-9D36-52DDC421DBEF}.Debug|x86.ActiveCfg = Debug|Any CPU
71+
{EC640983-252D-404F-9D36-52DDC421DBEF}.Debug|x86.Build.0 = Debug|Any CPU
72+
{EC640983-252D-404F-9D36-52DDC421DBEF}.Release|Any CPU.ActiveCfg = Release|Any CPU
73+
{EC640983-252D-404F-9D36-52DDC421DBEF}.Release|Any CPU.Build.0 = Release|Any CPU
74+
{EC640983-252D-404F-9D36-52DDC421DBEF}.Release|x64.ActiveCfg = Release|Any CPU
75+
{EC640983-252D-404F-9D36-52DDC421DBEF}.Release|x64.Build.0 = Release|Any CPU
76+
{EC640983-252D-404F-9D36-52DDC421DBEF}.Release|x86.ActiveCfg = Release|Any CPU
77+
{EC640983-252D-404F-9D36-52DDC421DBEF}.Release|x86.Build.0 = Release|Any CPU
78+
{828B11A8-D042-4D5F-B9BE-208AFF11C437}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
79+
{828B11A8-D042-4D5F-B9BE-208AFF11C437}.Debug|Any CPU.Build.0 = Debug|Any CPU
80+
{828B11A8-D042-4D5F-B9BE-208AFF11C437}.Debug|x64.ActiveCfg = Debug|Any CPU
81+
{828B11A8-D042-4D5F-B9BE-208AFF11C437}.Debug|x64.Build.0 = Debug|Any CPU
82+
{828B11A8-D042-4D5F-B9BE-208AFF11C437}.Debug|x86.ActiveCfg = Debug|Any CPU
83+
{828B11A8-D042-4D5F-B9BE-208AFF11C437}.Debug|x86.Build.0 = Debug|Any CPU
84+
{828B11A8-D042-4D5F-B9BE-208AFF11C437}.Release|Any CPU.ActiveCfg = Release|Any CPU
85+
{828B11A8-D042-4D5F-B9BE-208AFF11C437}.Release|Any CPU.Build.0 = Release|Any CPU
86+
{828B11A8-D042-4D5F-B9BE-208AFF11C437}.Release|x64.ActiveCfg = Release|Any CPU
87+
{828B11A8-D042-4D5F-B9BE-208AFF11C437}.Release|x64.Build.0 = Release|Any CPU
88+
{828B11A8-D042-4D5F-B9BE-208AFF11C437}.Release|x86.ActiveCfg = Release|Any CPU
89+
{828B11A8-D042-4D5F-B9BE-208AFF11C437}.Release|x86.Build.0 = Release|Any CPU
6290
EndGlobalSection
6391
GlobalSection(SolutionProperties) = preSolution
6492
HideSolutionNode = FALSE
6593
EndGlobalSection
6694
GlobalSection(NestedProjects) = preSolution
67-
{36E277D7-EA89-4A06-8AC2-A931C90347B5} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
68-
{40480398-EDF1-4893-8491-3C47792F1608} = {0C88DD14-F956-CE84-757C-A364CCF449FC}
95+
{DB38D678-1389-4C18-9BE2-D745C708FCCF} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
96+
{D8BA4FB3-519B-4D42-9803-CD775BEF29FD} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
97+
{EC640983-252D-404F-9D36-52DDC421DBEF} = {0C88DD14-F956-CE84-757C-A364CCF449FC}
98+
{828B11A8-D042-4D5F-B9BE-208AFF11C437} = {0C88DD14-F956-CE84-757C-A364CCF449FC}
6999
EndGlobalSection
70100
EndGlobal
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
; Shipped analyzer releases
2+
; https://github.com/dotnet/roslyn-analyzers/blob/main/src/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md
3+
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
; Unshipped analyzer releases
2+
; https://github.com/dotnet/roslyn-analyzers/blob/main/src/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md
3+
4+
### New Rules
5+
6+
Rule ID | Category | Severity | Notes
7+
--------|----------|----------|-------
8+
SERDECMD001 | Design | Error | CommandParameterAndGroupAnalyzer, [Documentation](https://github.com/serdedotnet/cmdline)
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
using System.Collections.Immutable;
2+
using System.Linq;
3+
using Microsoft.CodeAnalysis;
4+
using Microsoft.CodeAnalysis.Diagnostics;
5+
6+
namespace Serde.CmdLine.Analyzers;
7+
8+
[DiagnosticAnalyzer(LanguageNames.CSharp)]
9+
public sealed class CommandParameterAndGroupAnalyzer : DiagnosticAnalyzer
10+
{
11+
public const string DiagnosticId = "SERDECMD001";
12+
13+
private static readonly LocalizableString Title =
14+
"CommandParameter and CommandGroup cannot be used together";
15+
16+
private static readonly LocalizableString MessageFormat =
17+
"Type '{0}' has both [CommandParameter] and [CommandGroup] attributes. These cannot be combined on the same type.";
18+
19+
private static readonly LocalizableString Description =
20+
"A command type cannot have both positional parameters and subcommands. Move parameters to the subcommand types instead.";
21+
22+
private const string Category = "Design";
23+
24+
private static readonly DiagnosticDescriptor Rule = new DiagnosticDescriptor(
25+
DiagnosticId,
26+
Title,
27+
MessageFormat,
28+
Category,
29+
DiagnosticSeverity.Error,
30+
isEnabledByDefault: true,
31+
description: Description);
32+
33+
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics =>
34+
ImmutableArray.Create(Rule);
35+
36+
public override void Initialize(AnalysisContext context)
37+
{
38+
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
39+
context.EnableConcurrentExecution();
40+
41+
context.RegisterSymbolAction(AnalyzeNamedType, SymbolKind.NamedType);
42+
}
43+
44+
private static void AnalyzeNamedType(SymbolAnalysisContext context)
45+
{
46+
var namedType = (INamedTypeSymbol)context.Symbol;
47+
48+
bool hasCommandParameter = false;
49+
bool hasCommandGroup = false;
50+
51+
foreach (var member in namedType.GetMembers())
52+
{
53+
if (!(member is IPropertySymbol || member is IFieldSymbol))
54+
continue;
55+
56+
foreach (var attribute in member.GetAttributes())
57+
{
58+
var attrName = attribute.AttributeClass?.Name;
59+
60+
if (attrName == "CommandParameterAttribute" || attrName == "CommandParameter")
61+
{
62+
hasCommandParameter = true;
63+
}
64+
else if (attrName == "CommandGroupAttribute" || attrName == "CommandGroup")
65+
{
66+
hasCommandGroup = true;
67+
}
68+
}
69+
}
70+
71+
if (hasCommandParameter && hasCommandGroup)
72+
{
73+
// Report on the type itself
74+
var location = namedType.Locations.Length > 0 ? namedType.Locations[0] : Location.None;
75+
var diagnostic = Diagnostic.Create(Rule, location, namedType.Name);
76+
77+
context.ReportDiagnostic(diagnostic);
78+
}
79+
}
80+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<TargetFramework>netstandard2.0</TargetFramework>
5+
<RootNamespace>Serde.CmdLine.Analyzers</RootNamespace>
6+
<Nullable>enable</Nullable>
7+
<LangVersion>latest</LangVersion>
8+
<EnforceExtendedAnalyzerRules>true</EnforceExtendedAnalyzerRules>
9+
<IsRoslynComponent>true</IsRoslynComponent>
10+
</PropertyGroup>
11+
12+
<ItemGroup>
13+
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" PrivateAssets="all" />
14+
</ItemGroup>
15+
16+
<ItemGroup>
17+
<AdditionalFiles Include="AnalyzerReleases.Shipped.md" />
18+
<AdditionalFiles Include="AnalyzerReleases.Unshipped.md" />
19+
</ItemGroup>
20+
21+
</Project>
File renamed without changes.
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ public sealed record Help(IReadOnlyList<ISerdeInfo> HelpInfos) : ParsedArgsOrHel
2323
public static ParsedArgsOrHelpInfos<T> ParseRawWithHelp<T>(string[] args)
2424
where T : IDeserializeProvider<T>
2525
{
26-
var deserializer = new Deserializer(args, handleHelp: true);
26+
using var deserializer = new Deserializer(args, handleHelp: true);
2727
try
2828
{
2929
var cmd = T.Instance.Deserialize(deserializer);
@@ -54,9 +54,9 @@ public static ParsedArgsOrHelpInfos<T> ParseRawWithHelp<T>(string[] args)
5454
/// </summary>
5555
public static T ParseRaw<T>(string[] args) where T : IDeserializeProvider<T>
5656
{
57-
var deserializer = new Deserializer(args, handleHelp: false);
5857
try
5958
{
59+
using var deserializer = new Deserializer(args, handleHelp: false);
6060
return T.Instance.Deserialize(deserializer);
6161
}
6262
catch (DeserializeException e)

0 commit comments

Comments
 (0)