Skip to content

Commit

Permalink
Fix source generator to support nested arguments class (#174)
Browse files Browse the repository at this point in the history
Fixes #173.
  • Loading branch information
atifaziz authored Apr 3, 2022
1 parent 8a8d09c commit cc1258e
Show file tree
Hide file tree
Showing 8 changed files with 346 additions and 13 deletions.
10 changes: 10 additions & 0 deletions src/DocoptNet/CodeGeneration/CSharpSourceBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,16 @@ void AppendLine()
public CSharpSourceBuilder this[char code] { get { Append(code); return this; } }
public CSharpSourceBuilder this[CSharpSourceBuilder code] { get { AssertSame(code); return this; } }

public CSharpSourceBuilder this[IEnumerable<CSharpSourceBuilder> codes]
{
get
{
foreach (var code in codes)
_ = this[code];
return this;
}
}

public CSharpSourceBuilder Blank() => this;
public CSharpSourceBuilder NewLine { get { AppendLine(); return this; } }

Expand Down
20 changes: 19 additions & 1 deletion src/DocoptNet/CodeGeneration/Extensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,32 @@

namespace DocoptNet.CodeGeneration
{
using System.Collections.Generic;
using System.Diagnostics;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Diagnostics;

static partial class Extensions
{
/// <remarks>
/// Parents are returned in order of nearest to furthest ancestry.
/// </remarks>
public static IEnumerable<TypeDeclarationSyntax> GetParents(this BaseTypeDeclarationSyntax syntax)
{
for (var tds = syntax.Parent as TypeDeclarationSyntax;
tds is not null;
tds = tds.Parent as TypeDeclarationSyntax)
{
yield return tds;
}
}
}

// Inspiration & credit:
// https://github.com/devlooped/ThisAssembly/blob/43eb32fa24c25ddafda1058a53857ea3e305296a/src/GeneratorExtension.cs

static partial class Extensions
partial class Extensions
{
public static void LaunchDebuggerIfFlagged(this GeneratorExecutionContext context,
string generatorName) =>
Expand Down
47 changes: 36 additions & 11 deletions src/DocoptNet/CodeGeneration/SourceGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,9 @@ public void Execute(GeneratorExecutionContext context)
SemanticModel? model = null;
SyntaxTree? modelSyntaxTree = null;

var docoptTypes = new List<(string? Namespace, string Name, DocoptArgumentsAttribute? ArgumentsAttribute,
var docoptTypes = new List<(string? Namespace, string Name,
IEnumerable<TypeDeclarationSyntax> Parents,
DocoptArgumentsAttribute? ArgumentsAttribute,
SourceText Help, GenerationOptions Options)>();

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

foreach (var (ns, name, attribute, help, options) in docoptSources.Concat(docoptTypes))
var hintNameBuilder = new StringBuilder();

foreach (var (ns, name, parents, attribute, help, options) in docoptSources.Concat(docoptTypes))
{
try
{
if (Generate(ns, name, attribute?.HelpConstName, help, options) is { Length: > 0 } source)
context.AddSource((ns is { } someNamespace ? someNamespace + "." + name : name) + ".cs", source);
var parentNames = parents.Select(p => p.Identifier.ToString()).ToArray();
if (Generate(ns, name, parentNames, attribute?.HelpConstName, help, options) is { Length: > 0 } source)
{
hintNameBuilder.Clear();
if (ns is { } someNamespace)
hintNameBuilder.Append(someNamespace).Append('.');
if (parentNames.Length > 0)
{
foreach (var pn in parentNames)
{
// NOTE! Microsoft.CodeAnalysis.CSharp 3.10 does not allow use of "+"
// as is conventional for nested types. It is allowed later versions;
// see: https://github.com/dotnet/roslyn/issues/58476
hintNameBuilder.Append(pn).Append('-');
}
}
hintNameBuilder.Append(name);
context.AddSource(hintNameBuilder.Append(".cs").ToString(), source);
}
}
catch (DocoptLanguageErrorException e)
{
Expand Down Expand Up @@ -208,17 +230,17 @@ enum GenerationOptions
}

public static SourceText Generate(string? ns, string name, SourceText text) =>
Generate(ns, name, null, text, GenerationOptions.None);
Generate(ns, name, Enumerable.Empty<string>(), null, text, GenerationOptions.None);

static SourceText Generate(string? ns, string name, string? helpConstName,
static SourceText Generate(string? ns, string name, IEnumerable<string> parents, string? helpConstName,
SourceText text, GenerationOptions generationOptions) =>
Generate(ns, name, helpConstName, text, null, generationOptions);
Generate(ns, name, parents, helpConstName, text, null, generationOptions);

public static SourceText Generate(string? ns, string name,
SourceText text, Encoding? outputEncoding) =>
Generate(ns, name, null, text, outputEncoding, GenerationOptions.None);
Generate(ns, name, Enumerable.Empty<string>(), null, text, outputEncoding, GenerationOptions.None);

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

Generate(code,
ns is { Length: 0 } ? null : ns,
name, helpConstName ?? DefaultHelpConstName, helpText,
name, parents, helpConstName ?? DefaultHelpConstName, helpText,
options);

return new StringBuilderSourceText(code.StringBuilder, outputEncoding ?? text.Encoding ?? Utf8BomlessEncoding);
Expand All @@ -238,6 +260,7 @@ static SourceText Generate(string? ns, string name, string? helpConstName,
static void Generate(CSharpSourceBuilder code,
string? ns,
string name,
IEnumerable<string> parents,
string helpConstName,
string helpText,
GenerationOptions generationOptions)
Expand Down Expand Up @@ -267,6 +290,7 @@ static void Generate(CSharpSourceBuilder code,

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

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

Expand Down
39 changes: 38 additions & 1 deletion tests/DocoptNet.Tests/CodeGeneration/SourceGeneratorTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -187,8 +187,45 @@ sealed partial class ProgramArguments
{
const string Help = ""Usage: program"";
}
}"))
});
}

[Test]
public void Generate_with_nested_args_class()
{
AssertMatchesSnapshot(new[]
{
("Program.cs", SourceText.From(@"
static partial class Program
{
[DocoptNet.DocoptArguments]
sealed partial class Arguments
{
const string Help = ""Usage: program"";
}
partial class Nested
{
[DocoptNet.DocoptArguments]
sealed partial class Arguments
{
const string Help = ""Usage: program"";
}
}
}
"))
namespace MyConsoleApp
{
static partial class Program
{
[DocoptNet.DocoptArguments]
sealed partial class Arguments
{
const string Help = ""Usage: program"";
}
}
}"))
});
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
#nullable enable annotations

using System.Collections;
using System.Collections.Generic;
using DocoptNet;
using DocoptNet.Internals;
using Leaves = DocoptNet.Internals.ReadOnlyList<DocoptNet.Internals.LeafPattern>;

namespace MyConsoleApp
{
partial class Program
{
partial class Arguments : IEnumerable<KeyValuePair<string, object?>>
{
public const string Usage = "Usage: program";

static readonly IBaselineParser<Arguments> Parser = GeneratedSourceModule.CreateParser(Help, Parse);

public static IBaselineParser<Arguments> CreateParser() => Parser;

static IParser<Arguments>.IResult Parse(IEnumerable<string> args, ParseFlags flags, string? version)
{
var options = new List<Option>
{
};

return GeneratedSourceModule.Parse(Help, Usage, args, options, flags, version, Parse);

static IParser<Arguments>.IResult Parse(Leaves left)
{
var required = new RequiredMatcher(1, left, new Leaves());
Match(ref required);
if (!required.Result || required.Left.Count > 0)
{
return GeneratedSourceModule.CreateInputErrorResult<Arguments>(string.Empty, Usage);
}
var collected = required.Collected;
var result = new Arguments();

return GeneratedSourceModule.CreateArgumentsResult(result);
}

static void Match(ref RequiredMatcher required)
{
// Required(Required())
var a = new RequiredMatcher(1, required.Left, required.Collected);
while (a.Next())
{
// Required()
var b = new RequiredMatcher(0, a.Left, a.Collected);
while (b.Next())
{
if (!b.LastMatched)
{
break;
}
}
a.Fold(b.Result);
if (!a.LastMatched)
{
break;
}
}
required.Fold(a.Result);
}
}

IEnumerator<KeyValuePair<string, object?>> GetEnumerator()
{
yield break;
}

IEnumerator<KeyValuePair<string, object?>> IEnumerable<KeyValuePair<string, object?>>.GetEnumerator() => GetEnumerator();
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
#nullable enable annotations

using System.Collections;
using System.Collections.Generic;
using DocoptNet;
using DocoptNet.Internals;
using Leaves = DocoptNet.Internals.ReadOnlyList<DocoptNet.Internals.LeafPattern>;

partial class Program
{
partial class Arguments : IEnumerable<KeyValuePair<string, object?>>
{
public const string Usage = "Usage: program";

static readonly IBaselineParser<Arguments> Parser = GeneratedSourceModule.CreateParser(Help, Parse);

public static IBaselineParser<Arguments> CreateParser() => Parser;

static IParser<Arguments>.IResult Parse(IEnumerable<string> args, ParseFlags flags, string? version)
{
var options = new List<Option>
{
};

return GeneratedSourceModule.Parse(Help, Usage, args, options, flags, version, Parse);

static IParser<Arguments>.IResult Parse(Leaves left)
{
var required = new RequiredMatcher(1, left, new Leaves());
Match(ref required);
if (!required.Result || required.Left.Count > 0)
{
return GeneratedSourceModule.CreateInputErrorResult<Arguments>(string.Empty, Usage);
}
var collected = required.Collected;
var result = new Arguments();

return GeneratedSourceModule.CreateArgumentsResult(result);
}

static void Match(ref RequiredMatcher required)
{
// Required(Required())
var a = new RequiredMatcher(1, required.Left, required.Collected);
while (a.Next())
{
// Required()
var b = new RequiredMatcher(0, a.Left, a.Collected);
while (b.Next())
{
if (!b.LastMatched)
{
break;
}
}
a.Fold(b.Result);
if (!a.LastMatched)
{
break;
}
}
required.Fold(a.Result);
}
}

IEnumerator<KeyValuePair<string, object?>> GetEnumerator()
{
yield break;
}

IEnumerator<KeyValuePair<string, object?>> IEnumerable<KeyValuePair<string, object?>>.GetEnumerator() => GetEnumerator();
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}
}
Loading

0 comments on commit cc1258e

Please sign in to comment.