Skip to content

Commit cc1258e

Browse files
authored
Fix source generator to support nested arguments class (#174)
Fixes #173.
1 parent 8a8d09c commit cc1258e

File tree

8 files changed

+346
-13
lines changed

8 files changed

+346
-13
lines changed

src/DocoptNet/CodeGeneration/CSharpSourceBuilder.cs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,16 @@ void AppendLine()
8282
public CSharpSourceBuilder this[char code] { get { Append(code); return this; } }
8383
public CSharpSourceBuilder this[CSharpSourceBuilder code] { get { AssertSame(code); return this; } }
8484

85+
public CSharpSourceBuilder this[IEnumerable<CSharpSourceBuilder> codes]
86+
{
87+
get
88+
{
89+
foreach (var code in codes)
90+
_ = this[code];
91+
return this;
92+
}
93+
}
94+
8595
public CSharpSourceBuilder Blank() => this;
8696
public CSharpSourceBuilder NewLine { get { AppendLine(); return this; } }
8797

src/DocoptNet/CodeGeneration/Extensions.cs

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,14 +18,32 @@
1818

1919
namespace DocoptNet.CodeGeneration
2020
{
21+
using System.Collections.Generic;
2122
using System.Diagnostics;
2223
using Microsoft.CodeAnalysis;
24+
using Microsoft.CodeAnalysis.CSharp.Syntax;
2325
using Microsoft.CodeAnalysis.Diagnostics;
2426

27+
static partial class Extensions
28+
{
29+
/// <remarks>
30+
/// Parents are returned in order of nearest to furthest ancestry.
31+
/// </remarks>
32+
public static IEnumerable<TypeDeclarationSyntax> GetParents(this BaseTypeDeclarationSyntax syntax)
33+
{
34+
for (var tds = syntax.Parent as TypeDeclarationSyntax;
35+
tds is not null;
36+
tds = tds.Parent as TypeDeclarationSyntax)
37+
{
38+
yield return tds;
39+
}
40+
}
41+
}
42+
2543
// Inspiration & credit:
2644
// https://github.com/devlooped/ThisAssembly/blob/43eb32fa24c25ddafda1058a53857ea3e305296a/src/GeneratorExtension.cs
2745

28-
static partial class Extensions
46+
partial class Extensions
2947
{
3048
public static void LaunchDebuggerIfFlagged(this GeneratorExecutionContext context,
3149
string generatorName) =>

src/DocoptNet/CodeGeneration/SourceGenerator.cs

Lines changed: 36 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,9 @@ public void Execute(GeneratorExecutionContext context)
111111
SemanticModel? model = null;
112112
SyntaxTree? modelSyntaxTree = null;
113113

114-
var docoptTypes = new List<(string? Namespace, string Name, DocoptArgumentsAttribute? ArgumentsAttribute,
114+
var docoptTypes = new List<(string? Namespace, string Name,
115+
IEnumerable<TypeDeclarationSyntax> Parents,
116+
DocoptArgumentsAttribute? ArgumentsAttribute,
115117
SourceText Help, GenerationOptions Options)>();
116118

117119
foreach (var (cds, attributeData) in syntaxReceiver.ClassAttributes)
@@ -137,7 +139,7 @@ public void Execute(GeneratorExecutionContext context)
137139
&& name == attribute.HelpConstName ? Some(help) : default)
138140
.FirstOrDefault();
139141
if (help is { } someHelp)
140-
docoptTypes.Add((namespaceName, className, attribute, SourceText.From(someHelp), GenerationOptions.SkipHelpConst));
142+
docoptTypes.Add((namespaceName, className, cds.GetParents().Where(tds => tds is ClassDeclarationSyntax).Reverse(), attribute, SourceText.From(someHelp), GenerationOptions.SkipHelpConst));
141143
else
142144
context.ReportDiagnostic(Diagnostic.Create(MissingHelpConstError, symbol.Locations.First(), symbol, attribute.HelpConstName));
143145
}
@@ -156,18 +158,38 @@ public void Execute(GeneratorExecutionContext context)
156158
&& !string.IsNullOrWhiteSpace(name)
157159
? name
158160
: Path.GetFileName(at.Path).Partition(".").Item1 + "Arguments",
161+
Enumerable.Empty<TypeDeclarationSyntax>(),
159162
(DocoptArgumentsAttribute?)null,
160163
text,
161164
GenerationOptions.None))
162165
: default)
163166
.ToImmutableArray();
164167

165-
foreach (var (ns, name, attribute, help, options) in docoptSources.Concat(docoptTypes))
168+
var hintNameBuilder = new StringBuilder();
169+
170+
foreach (var (ns, name, parents, attribute, help, options) in docoptSources.Concat(docoptTypes))
166171
{
167172
try
168173
{
169-
if (Generate(ns, name, attribute?.HelpConstName, help, options) is { Length: > 0 } source)
170-
context.AddSource((ns is { } someNamespace ? someNamespace + "." + name : name) + ".cs", source);
174+
var parentNames = parents.Select(p => p.Identifier.ToString()).ToArray();
175+
if (Generate(ns, name, parentNames, attribute?.HelpConstName, help, options) is { Length: > 0 } source)
176+
{
177+
hintNameBuilder.Clear();
178+
if (ns is { } someNamespace)
179+
hintNameBuilder.Append(someNamespace).Append('.');
180+
if (parentNames.Length > 0)
181+
{
182+
foreach (var pn in parentNames)
183+
{
184+
// NOTE! Microsoft.CodeAnalysis.CSharp 3.10 does not allow use of "+"
185+
// as is conventional for nested types. It is allowed later versions;
186+
// see: https://github.com/dotnet/roslyn/issues/58476
187+
hintNameBuilder.Append(pn).Append('-');
188+
}
189+
}
190+
hintNameBuilder.Append(name);
191+
context.AddSource(hintNameBuilder.Append(".cs").ToString(), source);
192+
}
171193
}
172194
catch (DocoptLanguageErrorException e)
173195
{
@@ -208,17 +230,17 @@ enum GenerationOptions
208230
}
209231

210232
public static SourceText Generate(string? ns, string name, SourceText text) =>
211-
Generate(ns, name, null, text, GenerationOptions.None);
233+
Generate(ns, name, Enumerable.Empty<string>(), null, text, GenerationOptions.None);
212234

213-
static SourceText Generate(string? ns, string name, string? helpConstName,
235+
static SourceText Generate(string? ns, string name, IEnumerable<string> parents, string? helpConstName,
214236
SourceText text, GenerationOptions generationOptions) =>
215-
Generate(ns, name, helpConstName, text, null, generationOptions);
237+
Generate(ns, name, parents, helpConstName, text, null, generationOptions);
216238

217239
public static SourceText Generate(string? ns, string name,
218240
SourceText text, Encoding? outputEncoding) =>
219-
Generate(ns, name, null, text, outputEncoding, GenerationOptions.None);
241+
Generate(ns, name, Enumerable.Empty<string>(), null, text, outputEncoding, GenerationOptions.None);
220242

221-
static SourceText Generate(string? ns, string name, string? helpConstName,
243+
static SourceText Generate(string? ns, string name, IEnumerable<string> parents, string? helpConstName,
222244
SourceText text, Encoding? outputEncoding, GenerationOptions options)
223245
{
224246
if (text.Length == 0)
@@ -229,7 +251,7 @@ static SourceText Generate(string? ns, string name, string? helpConstName,
229251

230252
Generate(code,
231253
ns is { Length: 0 } ? null : ns,
232-
name, helpConstName ?? DefaultHelpConstName, helpText,
254+
name, parents, helpConstName ?? DefaultHelpConstName, helpText,
233255
options);
234256

235257
return new StringBuilderSourceText(code.StringBuilder, outputEncoding ?? text.Encoding ?? Utf8BomlessEncoding);
@@ -238,6 +260,7 @@ static SourceText Generate(string? ns, string name, string? helpConstName,
238260
static void Generate(CSharpSourceBuilder code,
239261
string? ns,
240262
string name,
263+
IEnumerable<string> parents,
241264
string helpConstName,
242265
string helpText,
243266
GenerationOptions generationOptions)
@@ -267,6 +290,7 @@ static void Generate(CSharpSourceBuilder code,
267290

268291
.NewLine
269292
[ns is not null ? code.Namespace(ns) : code.Blank()]
293+
[from p in parents select code.Partial.Class[p].NewLine.BlockStart]
270294

271295
.Partial.Class[name][" : IEnumerable<KeyValuePair<string, object?>>"].NewLine.SkipNextNewLine.Block[code
272296
[(generationOptions & GenerationOptions.SkipHelpConst) == GenerationOptions.SkipHelpConst
@@ -353,6 +377,7 @@ static void Generate(CSharpSourceBuilder code,
353377
}]
354378
.NewLine)
355379
] // class
380+
[from p in parents select code.BlockEnd]
356381
[ns is not null ? code.BlockEnd : code.Blank()]
357382
.Blank();
358383

tests/DocoptNet.Tests/CodeGeneration/SourceGeneratorTests.cs

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -187,8 +187,45 @@ sealed partial class ProgramArguments
187187
{
188188
const string Help = ""Usage: program"";
189189
}
190+
}"))
191+
});
192+
}
193+
194+
[Test]
195+
public void Generate_with_nested_args_class()
196+
{
197+
AssertMatchesSnapshot(new[]
198+
{
199+
("Program.cs", SourceText.From(@"
200+
static partial class Program
201+
{
202+
[DocoptNet.DocoptArguments]
203+
sealed partial class Arguments
204+
{
205+
const string Help = ""Usage: program"";
206+
}
207+
208+
partial class Nested
209+
{
210+
[DocoptNet.DocoptArguments]
211+
sealed partial class Arguments
212+
{
213+
const string Help = ""Usage: program"";
214+
}
215+
}
190216
}
191-
"))
217+
218+
namespace MyConsoleApp
219+
{
220+
static partial class Program
221+
{
222+
[DocoptNet.DocoptArguments]
223+
sealed partial class Arguments
224+
{
225+
const string Help = ""Usage: program"";
226+
}
227+
}
228+
}"))
192229
});
193230
}
194231

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
#nullable enable annotations
2+
3+
using System.Collections;
4+
using System.Collections.Generic;
5+
using DocoptNet;
6+
using DocoptNet.Internals;
7+
using Leaves = DocoptNet.Internals.ReadOnlyList<DocoptNet.Internals.LeafPattern>;
8+
9+
namespace MyConsoleApp
10+
{
11+
partial class Program
12+
{
13+
partial class Arguments : IEnumerable<KeyValuePair<string, object?>>
14+
{
15+
public const string Usage = "Usage: program";
16+
17+
static readonly IBaselineParser<Arguments> Parser = GeneratedSourceModule.CreateParser(Help, Parse);
18+
19+
public static IBaselineParser<Arguments> CreateParser() => Parser;
20+
21+
static IParser<Arguments>.IResult Parse(IEnumerable<string> args, ParseFlags flags, string? version)
22+
{
23+
var options = new List<Option>
24+
{
25+
};
26+
27+
return GeneratedSourceModule.Parse(Help, Usage, args, options, flags, version, Parse);
28+
29+
static IParser<Arguments>.IResult Parse(Leaves left)
30+
{
31+
var required = new RequiredMatcher(1, left, new Leaves());
32+
Match(ref required);
33+
if (!required.Result || required.Left.Count > 0)
34+
{
35+
return GeneratedSourceModule.CreateInputErrorResult<Arguments>(string.Empty, Usage);
36+
}
37+
var collected = required.Collected;
38+
var result = new Arguments();
39+
40+
return GeneratedSourceModule.CreateArgumentsResult(result);
41+
}
42+
43+
static void Match(ref RequiredMatcher required)
44+
{
45+
// Required(Required())
46+
var a = new RequiredMatcher(1, required.Left, required.Collected);
47+
while (a.Next())
48+
{
49+
// Required()
50+
var b = new RequiredMatcher(0, a.Left, a.Collected);
51+
while (b.Next())
52+
{
53+
if (!b.LastMatched)
54+
{
55+
break;
56+
}
57+
}
58+
a.Fold(b.Result);
59+
if (!a.LastMatched)
60+
{
61+
break;
62+
}
63+
}
64+
required.Fold(a.Result);
65+
}
66+
}
67+
68+
IEnumerator<KeyValuePair<string, object?>> GetEnumerator()
69+
{
70+
yield break;
71+
}
72+
73+
IEnumerator<KeyValuePair<string, object?>> IEnumerable<KeyValuePair<string, object?>>.GetEnumerator() => GetEnumerator();
74+
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
75+
}
76+
}
77+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
#nullable enable annotations
2+
3+
using System.Collections;
4+
using System.Collections.Generic;
5+
using DocoptNet;
6+
using DocoptNet.Internals;
7+
using Leaves = DocoptNet.Internals.ReadOnlyList<DocoptNet.Internals.LeafPattern>;
8+
9+
partial class Program
10+
{
11+
partial class Arguments : IEnumerable<KeyValuePair<string, object?>>
12+
{
13+
public const string Usage = "Usage: program";
14+
15+
static readonly IBaselineParser<Arguments> Parser = GeneratedSourceModule.CreateParser(Help, Parse);
16+
17+
public static IBaselineParser<Arguments> CreateParser() => Parser;
18+
19+
static IParser<Arguments>.IResult Parse(IEnumerable<string> args, ParseFlags flags, string? version)
20+
{
21+
var options = new List<Option>
22+
{
23+
};
24+
25+
return GeneratedSourceModule.Parse(Help, Usage, args, options, flags, version, Parse);
26+
27+
static IParser<Arguments>.IResult Parse(Leaves left)
28+
{
29+
var required = new RequiredMatcher(1, left, new Leaves());
30+
Match(ref required);
31+
if (!required.Result || required.Left.Count > 0)
32+
{
33+
return GeneratedSourceModule.CreateInputErrorResult<Arguments>(string.Empty, Usage);
34+
}
35+
var collected = required.Collected;
36+
var result = new Arguments();
37+
38+
return GeneratedSourceModule.CreateArgumentsResult(result);
39+
}
40+
41+
static void Match(ref RequiredMatcher required)
42+
{
43+
// Required(Required())
44+
var a = new RequiredMatcher(1, required.Left, required.Collected);
45+
while (a.Next())
46+
{
47+
// Required()
48+
var b = new RequiredMatcher(0, a.Left, a.Collected);
49+
while (b.Next())
50+
{
51+
if (!b.LastMatched)
52+
{
53+
break;
54+
}
55+
}
56+
a.Fold(b.Result);
57+
if (!a.LastMatched)
58+
{
59+
break;
60+
}
61+
}
62+
required.Fold(a.Result);
63+
}
64+
}
65+
66+
IEnumerator<KeyValuePair<string, object?>> GetEnumerator()
67+
{
68+
yield break;
69+
}
70+
71+
IEnumerator<KeyValuePair<string, object?>> IEnumerable<KeyValuePair<string, object?>>.GetEnumerator() => GetEnumerator();
72+
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
73+
}
74+
}

0 commit comments

Comments
 (0)