From cc1258e4ab85eb87ef17b2e0b19c054c2de1c6a4 Mon Sep 17 00:00:00 2001 From: Atif Aziz Date: Sun, 3 Apr 2022 22:15:05 +0200 Subject: [PATCH] Fix source generator to support nested arguments class (#174) Fixes #173. --- .../CodeGeneration/CSharpSourceBuilder.cs | 10 +++ src/DocoptNet/CodeGeneration/Extensions.cs | 20 ++++- .../CodeGeneration/SourceGenerator.cs | 47 ++++++++--- .../CodeGeneration/SourceGeneratorTests.cs | 39 +++++++++- .../MyConsoleApp.Program-Arguments.cs | 77 +++++++++++++++++++ .../Program-Arguments.cs | 74 ++++++++++++++++++ .../Program-Nested-Arguments.cs | 77 +++++++++++++++++++ tests/Integration/NavalFateTests.cs | 15 ++++ 8 files changed, 346 insertions(+), 13 deletions(-) create mode 100644 tests/DocoptNet.Tests/CodeGeneration/SourceGeneratorTests/Generate_with_nested_args_class/MyConsoleApp.Program-Arguments.cs create mode 100644 tests/DocoptNet.Tests/CodeGeneration/SourceGeneratorTests/Generate_with_nested_args_class/Program-Arguments.cs create mode 100644 tests/DocoptNet.Tests/CodeGeneration/SourceGeneratorTests/Generate_with_nested_args_class/Program-Nested-Arguments.cs diff --git a/src/DocoptNet/CodeGeneration/CSharpSourceBuilder.cs b/src/DocoptNet/CodeGeneration/CSharpSourceBuilder.cs index fa63db9a..51988218 100644 --- a/src/DocoptNet/CodeGeneration/CSharpSourceBuilder.cs +++ b/src/DocoptNet/CodeGeneration/CSharpSourceBuilder.cs @@ -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 codes] + { + get + { + foreach (var code in codes) + _ = this[code]; + return this; + } + } + public CSharpSourceBuilder Blank() => this; public CSharpSourceBuilder NewLine { get { AppendLine(); return this; } } diff --git a/src/DocoptNet/CodeGeneration/Extensions.cs b/src/DocoptNet/CodeGeneration/Extensions.cs index 6e03e552..7ed0faf0 100644 --- a/src/DocoptNet/CodeGeneration/Extensions.cs +++ b/src/DocoptNet/CodeGeneration/Extensions.cs @@ -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 + { + /// + /// Parents are returned in order of nearest to furthest ancestry. + /// + public static IEnumerable 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) => diff --git a/src/DocoptNet/CodeGeneration/SourceGenerator.cs b/src/DocoptNet/CodeGeneration/SourceGenerator.cs index b0d13e44..5b874c97 100644 --- a/src/DocoptNet/CodeGeneration/SourceGenerator.cs +++ b/src/DocoptNet/CodeGeneration/SourceGenerator.cs @@ -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 Parents, + DocoptArgumentsAttribute? ArgumentsAttribute, SourceText Help, GenerationOptions Options)>(); foreach (var (cds, attributeData) in syntaxReceiver.ClassAttributes) @@ -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)); } @@ -156,18 +158,38 @@ public void Execute(GeneratorExecutionContext context) && !string.IsNullOrWhiteSpace(name) ? name : Path.GetFileName(at.Path).Partition(".").Item1 + "Arguments", + Enumerable.Empty(), (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) { @@ -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(), null, text, GenerationOptions.None); - static SourceText Generate(string? ns, string name, string? helpConstName, + static SourceText Generate(string? ns, string name, IEnumerable 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(), null, text, outputEncoding, GenerationOptions.None); - static SourceText Generate(string? ns, string name, string? helpConstName, + static SourceText Generate(string? ns, string name, IEnumerable parents, string? helpConstName, SourceText text, Encoding? outputEncoding, GenerationOptions options) { if (text.Length == 0) @@ -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); @@ -238,6 +260,7 @@ static SourceText Generate(string? ns, string name, string? helpConstName, static void Generate(CSharpSourceBuilder code, string? ns, string name, + IEnumerable parents, string helpConstName, string helpText, GenerationOptions generationOptions) @@ -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>"].NewLine.SkipNextNewLine.Block[code [(generationOptions & GenerationOptions.SkipHelpConst) == GenerationOptions.SkipHelpConst @@ -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(); diff --git a/tests/DocoptNet.Tests/CodeGeneration/SourceGeneratorTests.cs b/tests/DocoptNet.Tests/CodeGeneration/SourceGeneratorTests.cs index d9ea58ce..5dee3355 100644 --- a/tests/DocoptNet.Tests/CodeGeneration/SourceGeneratorTests.cs +++ b/tests/DocoptNet.Tests/CodeGeneration/SourceGeneratorTests.cs @@ -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""; + } + } + }")) }); } diff --git a/tests/DocoptNet.Tests/CodeGeneration/SourceGeneratorTests/Generate_with_nested_args_class/MyConsoleApp.Program-Arguments.cs b/tests/DocoptNet.Tests/CodeGeneration/SourceGeneratorTests/Generate_with_nested_args_class/MyConsoleApp.Program-Arguments.cs new file mode 100644 index 00000000..6834300a --- /dev/null +++ b/tests/DocoptNet.Tests/CodeGeneration/SourceGeneratorTests/Generate_with_nested_args_class/MyConsoleApp.Program-Arguments.cs @@ -0,0 +1,77 @@ +#nullable enable annotations + +using System.Collections; +using System.Collections.Generic; +using DocoptNet; +using DocoptNet.Internals; +using Leaves = DocoptNet.Internals.ReadOnlyList; + +namespace MyConsoleApp +{ + partial class Program + { + partial class Arguments : IEnumerable> + { + public const string Usage = "Usage: program"; + + static readonly IBaselineParser Parser = GeneratedSourceModule.CreateParser(Help, Parse); + + public static IBaselineParser CreateParser() => Parser; + + static IParser.IResult Parse(IEnumerable args, ParseFlags flags, string? version) + { + var options = new List