diff --git a/src/CodeGeneration.Roslyn.Tests.Generators/AddGeneratedAttributeGenerator.cs b/src/CodeGeneration.Roslyn.Tests.Generators/AddGeneratedAttributeGenerator.cs new file mode 100644 index 000000000..29015e04d --- /dev/null +++ b/src/CodeGeneration.Roslyn.Tests.Generators/AddGeneratedAttributeGenerator.cs @@ -0,0 +1,45 @@ +// Copyright (c) Andrew Arnott. All rights reserved. +// Licensed under the MIT license. See LICENSE.txt file in the project root for full license information. + +namespace CodeGeneration.Roslyn.Tests.Generators +{ + + using System; + using System.Diagnostics; + using Microsoft.CodeAnalysis; + using Microsoft.CodeAnalysis.CSharp; + + [AttributeUsage(AttributeTargets.All, Inherited = false, AllowMultiple = true)] + [CodeGenerationAttribute(typeof(AddGeneratedAttributeGenerator))] + [Conditional("CodeGeneration")] + public class AddGeneratedAttributeAttribute : Attribute + { + public AddGeneratedAttributeAttribute(string attribute) + { + Attribute = attribute; + } + + public string Attribute { get; } + } + + [AttributeUsage(AttributeTargets.All, Inherited = false, AllowMultiple = true)] + public class GeneratedAttribute : Attribute + { } + + public class AddGeneratedAttributeGenerator : RichBaseGenerator + { + + public AddGeneratedAttributeGenerator(AttributeData attributeData) : base(attributeData) + { + Attribute = (string)AttributeData.ConstructorArguments[0].Value; + } + + public string Attribute { get; } + + protected override void Generate(RichGenerationContext context) + { + var attribute = SyntaxFactory.Attribute(SyntaxFactory.ParseName(Attribute)); + context.AddAttribute(attribute); + } + } +} \ No newline at end of file diff --git a/src/CodeGeneration.Roslyn.Tests.Generators/AddGeneratedExternGenerator.cs b/src/CodeGeneration.Roslyn.Tests.Generators/AddGeneratedExternGenerator.cs new file mode 100644 index 000000000..b1209f651 --- /dev/null +++ b/src/CodeGeneration.Roslyn.Tests.Generators/AddGeneratedExternGenerator.cs @@ -0,0 +1,41 @@ +// Copyright (c) Andrew Arnott. All rights reserved. +// Licensed under the MIT license. See LICENSE.txt file in the project root for full license information. + +namespace CodeGeneration.Roslyn.Tests.Generators +{ + + using System; + using System.Diagnostics; + using Microsoft.CodeAnalysis; + using Microsoft.CodeAnalysis.CSharp; + + [AttributeUsage(AttributeTargets.All, Inherited = false, AllowMultiple = true)] + [CodeGenerationAttribute(typeof(AddGeneratedExternGenerator))] + [Conditional("CodeGeneration")] + public class AddGeneratedExternAttribute : Attribute + { + public AddGeneratedExternAttribute(string @extern) + { + Extern = @extern; + } + + public string Extern { get; } + } + + public class AddGeneratedExternGenerator : RichBaseGenerator + { + + public AddGeneratedExternGenerator(AttributeData attributeData) : base(attributeData) + { + Extern = (string)AttributeData.ConstructorArguments[0].Value; + } + + public string Extern { get; } + + protected override void Generate(RichGenerationContext context) + { + var externAlias = SyntaxFactory.ExternAliasDirective(Extern); + context.AddExtern(externAlias); + } + } +} \ No newline at end of file diff --git a/src/CodeGeneration.Roslyn.Tests.Generators/AddGeneratedUsingGenerator.cs b/src/CodeGeneration.Roslyn.Tests.Generators/AddGeneratedUsingGenerator.cs new file mode 100644 index 000000000..5330a887b --- /dev/null +++ b/src/CodeGeneration.Roslyn.Tests.Generators/AddGeneratedUsingGenerator.cs @@ -0,0 +1,41 @@ +// Copyright (c) Andrew Arnott. All rights reserved. +// Licensed under the MIT license. See LICENSE.txt file in the project root for full license information. + +using System.Diagnostics; + +namespace CodeGeneration.Roslyn.Tests.Generators +{ + using System; + using Microsoft.CodeAnalysis; + using Microsoft.CodeAnalysis.CSharp; + + [AttributeUsage(AttributeTargets.All, Inherited = false, AllowMultiple = true)] + [CodeGenerationAttribute(typeof(AddGeneratedUsingGenerator))] + [Conditional("CodeGeneration")] + public class AddGeneratedUsingAttribute : Attribute + { + public AddGeneratedUsingAttribute(string @using) + { + Using = @using; + } + + public string Using { get; } + } + + public class AddGeneratedUsingGenerator : RichBaseGenerator + { + + public AddGeneratedUsingGenerator(AttributeData attributeData) : base(attributeData) + { + Using = (string)AttributeData.ConstructorArguments[0].Value; + } + + public string Using { get; } + + protected override void Generate(RichGenerationContext context) + { + var usingSyntax = SyntaxFactory.UsingDirective(SyntaxFactory.ParseName(Using)); + context.AddUsing(usingSyntax); + } + } +} \ No newline at end of file diff --git a/src/CodeGeneration.Roslyn.Tests.Generators/BaseGenerator.cs b/src/CodeGeneration.Roslyn.Tests.Generators/BaseGenerator.cs new file mode 100644 index 000000000..62b6442ef --- /dev/null +++ b/src/CodeGeneration.Roslyn.Tests.Generators/BaseGenerator.cs @@ -0,0 +1,59 @@ +// Copyright (c) Andrew Arnott. All rights reserved. +// Licensed under the MIT license. See LICENSE.txt file in the project root for full license information. + +namespace CodeGeneration.Roslyn.Tests.Generators +{ + using System; + using System.Collections.Generic; + using System.Threading; + using System.Threading.Tasks; + using Microsoft.CodeAnalysis; + using Microsoft.CodeAnalysis.CSharp; + using Microsoft.CodeAnalysis.CSharp.Syntax; + + public abstract class BaseGenerator : ICodeGenerator + { + public AttributeData AttributeData { get; } + + protected BaseGenerator(AttributeData attributeData) + { + AttributeData = attributeData; + } + + Task> ICodeGenerator.GenerateAsync(TransformationContext context, IProgress progress, CancellationToken cancellationToken) + { + var richGenerationContext = new GenerationContext(context, progress, cancellationToken); + Generate(richGenerationContext); + var result = richGenerationContext.CreateResult(); + return Task.FromResult(result); + } + + protected abstract void Generate(GenerationContext context); + + protected class GenerationContext + { + public GenerationContext(TransformationContext transformationContext, IProgress progress, CancellationToken cancellationToken) + { + TransformationContext = transformationContext; + Progress = progress; + CancellationToken = cancellationToken; + } + + public TransformationContext TransformationContext { get; } + public IProgress Progress { get; } + public CancellationToken CancellationToken { get; } + public List Members { get; } = new List(); + + public GenerationContext AddMember(MemberDeclarationSyntax memberDeclaration) + { + Members.Add(memberDeclaration); + return this; + } + + public SyntaxList CreateResult() + { + return SyntaxFactory.List(Members); + } + } + } +} \ No newline at end of file diff --git a/src/CodeGeneration.Roslyn.Tests.Generators/DuplicateInOtherNamespaceAttribute.cs b/src/CodeGeneration.Roslyn.Tests.Generators/DuplicateInOtherNamespaceAttribute.cs new file mode 100644 index 000000000..5d09c8cd9 --- /dev/null +++ b/src/CodeGeneration.Roslyn.Tests.Generators/DuplicateInOtherNamespaceAttribute.cs @@ -0,0 +1,21 @@ +// Copyright (c) Andrew Arnott. All rights reserved. +// Licensed under the MIT license. See LICENSE.txt file in the project root for full license information. + +namespace CodeGeneration.Roslyn.Tests.Generators +{ + using System; + using System.Diagnostics; + + [AttributeUsage(AttributeTargets.All, Inherited = false, AllowMultiple = true)] + [CodeGenerationAttribute(typeof(DuplicateInOtherNamespaceGenerator))] + [Conditional("CodeGeneration")] + public class DuplicateInOtherNamespaceAttribute : Attribute + { + public DuplicateInOtherNamespaceAttribute(string @namespace) + { + Namespace = @namespace; + } + + public string Namespace { get; } + } +} \ No newline at end of file diff --git a/src/CodeGeneration.Roslyn.Tests.Generators/DuplicateInOtherNamespaceGenerator.cs b/src/CodeGeneration.Roslyn.Tests.Generators/DuplicateInOtherNamespaceGenerator.cs new file mode 100644 index 000000000..878ab6118 --- /dev/null +++ b/src/CodeGeneration.Roslyn.Tests.Generators/DuplicateInOtherNamespaceGenerator.cs @@ -0,0 +1,35 @@ +// Copyright (c) Andrew Arnott. All rights reserved. +// Licensed under the MIT license. See LICENSE.txt file in the project root for full license information. + +namespace CodeGeneration.Roslyn.Tests.Generators +{ + using Microsoft.CodeAnalysis; + using Microsoft.CodeAnalysis.CSharp; + using Microsoft.CodeAnalysis.CSharp.Syntax; + + public class DuplicateInOtherNamespaceGenerator : RichBaseGenerator + { + + public DuplicateInOtherNamespaceGenerator(AttributeData attributeData) : base(attributeData) + { + Namespace = (string)AttributeData.ConstructorArguments[0].Value; + } + + public string Namespace { get; } + + protected override void Generate(RichGenerationContext context) + { + if (!(context.TransformationContext.ProcessingNode is ClassDeclarationSyntax classDeclaration)) + { + return; + } + var partial = SyntaxFactory.ClassDeclaration(classDeclaration.Identifier); + var namespaceSyntax = + SyntaxFactory.NamespaceDeclaration( + SyntaxFactory.ParseName(Namespace)) + .AddMembers(partial); + + context.AddMember(namespaceSyntax); + } + } +} \ No newline at end of file diff --git a/src/CodeGeneration.Roslyn.Tests.Generators/EmptyPartialGenerator.cs b/src/CodeGeneration.Roslyn.Tests.Generators/EmptyPartialGenerator.cs index 10c6bf1c6..ecc1cc01c 100644 --- a/src/CodeGeneration.Roslyn.Tests.Generators/EmptyPartialGenerator.cs +++ b/src/CodeGeneration.Roslyn.Tests.Generators/EmptyPartialGenerator.cs @@ -45,4 +45,4 @@ private static MemberDeclarationSyntax StructPartial(StructDeclarationSyntax dec .WithModifiers(SyntaxTokenList.Create(SyntaxFactory.Token(SyntaxKind.PartialKeyword))); } } -} \ No newline at end of file +} diff --git a/src/CodeGeneration.Roslyn.Tests.Generators/RichBaseGenerator.cs b/src/CodeGeneration.Roslyn.Tests.Generators/RichBaseGenerator.cs new file mode 100644 index 000000000..e608e5c18 --- /dev/null +++ b/src/CodeGeneration.Roslyn.Tests.Generators/RichBaseGenerator.cs @@ -0,0 +1,98 @@ +// Copyright (c) Andrew Arnott. All rights reserved. +// Licensed under the MIT license. See LICENSE.txt file in the project root for full license information. + +namespace CodeGeneration.Roslyn.Tests.Generators +{ + using System; + using System.Collections.Generic; + using System.Threading; + using System.Threading.Tasks; + using Microsoft.CodeAnalysis; + using Microsoft.CodeAnalysis.CSharp; + using Microsoft.CodeAnalysis.CSharp.Syntax; + + public abstract class RichBaseGenerator : IRichCodeGenerator + { + public AttributeData AttributeData { get; } + + protected RichBaseGenerator(AttributeData attributeData) + { + AttributeData = attributeData; + } + + Task> ICodeGenerator.GenerateAsync(TransformationContext context, IProgress progress, CancellationToken cancellationToken) + { + throw new NotImplementedException(); + } + + Task IRichCodeGenerator.GenerateRichAsync(TransformationContext context, IProgress progress, CancellationToken cancellationToken) + { + var richGenerationContext = new RichGenerationContext(context, progress, cancellationToken); + Generate(richGenerationContext); + var result = richGenerationContext.CreateResult(); + return Task.FromResult(result); + } + + protected abstract void Generate(RichGenerationContext context); + + protected class RichGenerationContext + { + public RichGenerationContext(TransformationContext transformationContext, IProgress progress, CancellationToken cancellationToken) + { + TransformationContext = transformationContext; + Progress = progress; + CancellationToken = cancellationToken; + } + + public TransformationContext TransformationContext { get; } + public IProgress Progress { get; } + public CancellationToken CancellationToken { get; } + + public List Usings { get; } = new List(); + public List Externs { get; } = new List(); + public List AttributeLists { get; } = new List(); + public List Members { get; } = new List(); + + public RichGenerationContext AddUsing(UsingDirectiveSyntax usingDirective) + { + Usings.Add(usingDirective); + return this; + } + + public RichGenerationContext AddExtern(ExternAliasDirectiveSyntax externAliasDirective) + { + Externs.Add(externAliasDirective); + return this; + } + + public RichGenerationContext AddAttribute(AttributeSyntax attribute) + { + var list = SyntaxFactory.AttributeList(SyntaxFactory.SingletonSeparatedList(attribute)); + return AddAttributeList(list); + } + + public RichGenerationContext AddAttributeList(AttributeListSyntax attributeList) + { + AttributeLists.Add(attributeList); + return this; + } + + public RichGenerationContext AddMember(MemberDeclarationSyntax memberDeclaration) + { + Members.Add(memberDeclaration); + return this; + } + + public RichGenerationResult CreateResult() + { + return new RichGenerationResult + { + Members = SyntaxFactory.List(Members), + Usings = SyntaxFactory.List(Usings), + AttributeLists = SyntaxFactory.List(AttributeLists), + Externs = SyntaxFactory.List(Externs), + }; + } + } + } +} \ No newline at end of file diff --git a/src/CodeGeneration.Roslyn.Tests/DocumentTransformTests.cs b/src/CodeGeneration.Roslyn.Tests/DocumentTransformTests.cs index 925fce95c..b535fed46 100644 --- a/src/CodeGeneration.Roslyn.Tests/DocumentTransformTests.cs +++ b/src/CodeGeneration.Roslyn.Tests/DocumentTransformTests.cs @@ -324,4 +324,150 @@ partial struct Empty }"; AssertGeneratedAsExpected(source, generated); } + + [Fact] + public void RichGenerator_Wraps_InOtherNamespace() + { + const string source = @" +using System; +using CodeGeneration.Roslyn.Tests.Generators; + +namespace Testing +{ + [DuplicateInOtherNamespace(""Other.Namespace"")] + class Something + { + } +}"; + const string generated = @" +using System; +using CodeGeneration.Roslyn.Tests.Generators; + +namespace Other.Namespace +{ + class Something + { + } +}"; + AssertGeneratedAsExpected(source, generated); + } + + [Fact] + public void RichGenerator_Adds_Using() + { + const string source = @" +using System; +using CodeGeneration.Roslyn.Tests.Generators; + +namespace Testing +{ + [AddGeneratedUsing(""System.Collections.Generic"")] + partial class Something + { + } +}"; + const string generated = @" +using System; +using CodeGeneration.Roslyn.Tests.Generators; +using System.Collections.Generic; + +"; + AssertGeneratedAsExpected(source, generated); + } + + [Fact] + public void RichGenerator_Adds_ExternAlias() + { + const string source = @" +using System; +using CodeGeneration.Roslyn.Tests.Generators; + +namespace Testing +{ + [AddGeneratedExtern(""MyExternAlias"")] + partial class Something + { + } +}"; + const string generated = @" +extern alias MyExternAlias; + +using System; +using CodeGeneration.Roslyn.Tests.Generators; + +"; + AssertGeneratedAsExpected(source, generated); + } + + [Fact] + public void RichGenerator_Adds_Attribute() + { + const string source = @" +using System; +using CodeGeneration.Roslyn.Tests.Generators; + +namespace Testing +{ + [AddGeneratedAttribute(""GeneratedAttribute"")] + partial class Something + { + } +}"; + const string generated = @" +using System; +using CodeGeneration.Roslyn.Tests.Generators; + +[GeneratedAttribute] +"; + AssertGeneratedAsExpected(source, generated); + } + + [Fact] + public void RichGenerator_Appends_MultipleResults() + { + const string source = @" +using System; +using CodeGeneration.Roslyn.Tests.Generators; + +namespace Testing +{ + [DuplicateInOtherNamespace(""Other.Namespace1"")] + [DuplicateInOtherNamespace(""Other.Namespace2"")] + [AddGeneratedUsing(""System.Collections"")] + [AddGeneratedUsing(""System.Collections.Generic"")] + [AddGeneratedExtern(""MyExternAlias1"")] + [AddGeneratedExtern(""MyExternAlias2"")] + [AddGeneratedAttribute(""GeneratedAttribute"")] + [AddGeneratedAttribute(""GeneratedAttribute"")] + partial class Something + { + } +}"; + const string generated = @" +extern alias MyExternAlias1; +extern alias MyExternAlias2; + +using System; +using CodeGeneration.Roslyn.Tests.Generators; +using System.Collections; +using System.Collections.Generic; + +[GeneratedAttribute] +[GeneratedAttribute] +namespace Other.Namespace1 +{ + class Something + { + } +} + +namespace Other.Namespace2 +{ + class Something + { + } +} +"; + AssertGeneratedAsExpected(source, generated); + } } diff --git a/src/CodeGeneration.Roslyn/CodeGeneration.Roslyn.csproj b/src/CodeGeneration.Roslyn/CodeGeneration.Roslyn.csproj index fccbe1afb..4c360c028 100644 --- a/src/CodeGeneration.Roslyn/CodeGeneration.Roslyn.csproj +++ b/src/CodeGeneration.Roslyn/CodeGeneration.Roslyn.csproj @@ -2,6 +2,7 @@ netstandard1.6 + 7.3 True ..\opensource.snk $(PackageTargetFallback);portable-net45+win8+wp8+wpa81; diff --git a/src/CodeGeneration.Roslyn/DocumentTransform.cs b/src/CodeGeneration.Roslyn/DocumentTransform.cs index 95486f351..f86766bd4 100644 --- a/src/CodeGeneration.Roslyn/DocumentTransform.cs +++ b/src/CodeGeneration.Roslyn/DocumentTransform.cs @@ -36,113 +36,78 @@ public static class DocumentTransform /// /// The compilation to which the document belongs. /// The document to scan for generator attributes. + /// The path of the .csproj project file. /// A function that can load an assembly with the given name. /// Reports warnings and errors in code generation. /// A task whose result is the generated document. - public static async Task TransformAsync(CSharpCompilation compilation, SyntaxTree inputDocument, string projectDirectory, Func assemblyLoader, IProgress progress) + public static async Task TransformAsync( + CSharpCompilation compilation, + SyntaxTree inputDocument, + string projectDirectory, + Func assemblyLoader, + IProgress progress) { Requires.NotNull(compilation, nameof(compilation)); Requires.NotNull(inputDocument, nameof(inputDocument)); Requires.NotNull(assemblyLoader, nameof(assemblyLoader)); var inputSemanticModel = compilation.GetSemanticModel(inputDocument); - var inputSyntaxTree = inputSemanticModel.SyntaxTree; - var inputCompilationUnit = inputSyntaxTree.GetCompilationUnitRoot(); + var inputCompilationUnit = inputDocument.GetCompilationUnitRoot(); - var inputFileLevelExternAliases = inputCompilationUnit + var emittedExterns = inputCompilationUnit .Externs .Select(x => x.WithoutTrivia()) .ToImmutableArray(); - var inputFileLevelUsingDirectives = inputCompilationUnit + + var emittedUsings = inputCompilationUnit .Usings .Select(x => x.WithoutTrivia()) .ToImmutableArray(); - var memberNodes = inputSyntaxTree.GetRoot().DescendantNodesAndSelf(n => n is CompilationUnitSyntax || n is NamespaceDeclarationSyntax || n is TypeDeclarationSyntax).OfType(); + var emittedAttributeLists = ImmutableArray.Empty; + var emittedMembers = ImmutableArray.Empty; + + var memberNodes = inputDocument + .GetRoot() + .DescendantNodesAndSelf(n => n is CompilationUnitSyntax || n is NamespaceDeclarationSyntax || n is TypeDeclarationSyntax) + .OfType(); - var emittedMembers = SyntaxFactory.List(); foreach (var memberNode in memberNodes) { var attributeData = GetAttributeData(compilation, inputSemanticModel, memberNode); var generators = FindCodeGenerators(attributeData, assemblyLoader); foreach (var generator in generators) { - var context = new TransformationContext(memberNode, inputSemanticModel, compilation, projectDirectory); - var generatedMembers = await generator.GenerateAsync(context, progress, CancellationToken.None); + var context = new TransformationContext(memberNode, inputSemanticModel, compilation, projectDirectory, + emittedUsings, emittedExterns); - // Figure out ancestry for the generated type, including nesting types and namespaces. - foreach (var ancestor in memberNode.Ancestors()) - { - generatedMembers = WrapInAncestor(generatedMembers, ancestor); - } + var richGenerator = generator as IRichCodeGenerator ?? new EnrichingCodeGeneratorProxy(generator); + + var emitted = await richGenerator.GenerateRichAsync(context, progress, CancellationToken.None); - emittedMembers = emittedMembers.AddRange(generatedMembers); + emittedExterns = emittedExterns.AddRange(emitted.Externs); + emittedUsings = emittedUsings.AddRange(emitted.Usings); + emittedAttributeLists = emittedAttributeLists.AddRange(emitted.AttributeLists); + emittedMembers = emittedMembers.AddRange(emitted.Members); } } - // By default, retain all the using directives that came from the input file. - var resultFileLevelExternAliases = SyntaxFactory.List(inputFileLevelExternAliases); - var resultFileLevelUsingDirectives = SyntaxFactory.List(inputFileLevelUsingDirectives); - - var compilationUnit = SyntaxFactory.CompilationUnit() - .WithExterns(resultFileLevelExternAliases) - .WithUsings(resultFileLevelUsingDirectives) - .WithMembers(emittedMembers) - .WithLeadingTrivia(SyntaxFactory.Comment(GeneratedByAToolPreamble)) - .WithTrailingTrivia(SyntaxFactory.CarriageReturnLineFeed) - .NormalizeWhitespace(); + var compilationUnit = + SyntaxFactory.CompilationUnit( + SyntaxFactory.List(emittedExterns), + SyntaxFactory.List(emittedUsings), + SyntaxFactory.List(emittedAttributeLists), + SyntaxFactory.List(emittedMembers)) + .WithLeadingTrivia(SyntaxFactory.Comment(GeneratedByAToolPreamble)) + .WithTrailingTrivia(SyntaxFactory.CarriageReturnLineFeed) + .NormalizeWhitespace(); return compilationUnit.SyntaxTree; } - private static SyntaxList WrapInAncestor(SyntaxList generatedMembers, SyntaxNode ancestor) - { - switch (ancestor) - { - case NamespaceDeclarationSyntax ancestorNamespace: - generatedMembers = SyntaxFactory.SingletonList( - CopyAsAncestor(ancestorNamespace) - .WithMembers(generatedMembers)); - break; - case ClassDeclarationSyntax nestingClass: - generatedMembers = SyntaxFactory.SingletonList( - CopyAsAncestor(nestingClass) - .WithMembers(generatedMembers)); - break; - case StructDeclarationSyntax nestingStruct: - generatedMembers = SyntaxFactory.SingletonList( - CopyAsAncestor(nestingStruct) - .WithMembers(generatedMembers)); - break; - default: - break; - } - return generatedMembers; - } - - private static NamespaceDeclarationSyntax CopyAsAncestor(NamespaceDeclarationSyntax syntax) - { - return SyntaxFactory.NamespaceDeclaration(syntax.Name.WithoutTrivia()) - .WithExterns(SyntaxFactory.List(syntax.Externs.Select(x => x.WithoutTrivia()))) - .WithUsings(SyntaxFactory.List(syntax.Usings.Select(x => x.WithoutTrivia()))); - } - - private static ClassDeclarationSyntax CopyAsAncestor(ClassDeclarationSyntax syntax) - { - return SyntaxFactory.ClassDeclaration(syntax.Identifier.WithoutTrivia()) - .WithModifiers(SyntaxFactory.TokenList(syntax.Modifiers.Select(x => x.WithoutTrivia()))) - .WithTypeParameterList(syntax.TypeParameterList); - } - - private static StructDeclarationSyntax CopyAsAncestor(StructDeclarationSyntax syntax) - { - return SyntaxFactory.StructDeclaration(syntax.Identifier.WithoutTrivia()) - .WithModifiers(SyntaxFactory.TokenList(syntax.Modifiers.Select(x => x.WithoutTrivia()))) - .WithTypeParameterList(syntax.TypeParameterList); - } - private static ImmutableArray GetAttributeData(Compilation compilation, SemanticModel document, SyntaxNode syntaxNode) { + Requires.NotNull(compilation, nameof(compilation)); Requires.NotNull(document, nameof(document)); Requires.NotNull(syntaxNode, nameof(syntaxNode)); @@ -240,5 +205,76 @@ private static string GetFullTypeName(INamedTypeSymbol symbol) return nameBuilder.ToString(); } + + private class EnrichingCodeGeneratorProxy : IRichCodeGenerator + { + public EnrichingCodeGeneratorProxy(ICodeGenerator codeGenerator) + { + Requires.NotNull(codeGenerator, nameof(codeGenerator)); + CodeGenerator = codeGenerator; + } + + private ICodeGenerator CodeGenerator { get; } + + public Task> GenerateAsync( + TransformationContext context, + IProgress progress, + CancellationToken cancellationToken) + { + return CodeGenerator.GenerateAsync(context, progress, cancellationToken); + } + + public async Task GenerateRichAsync(TransformationContext context, IProgress progress, CancellationToken cancellationToken) + { + var generatedMembers = await CodeGenerator.GenerateAsync(context, progress, CancellationToken.None); + // Figure out ancestry for the generated type, including nesting types and namespaces. + var wrappedMembers = context.ProcessingNode.Ancestors().Aggregate(generatedMembers, WrapInAncestor); + return new RichGenerationResult { Members = wrappedMembers }; + } + + private static SyntaxList WrapInAncestor(SyntaxList generatedMembers, SyntaxNode ancestor) + { + switch (ancestor) + { + case NamespaceDeclarationSyntax ancestorNamespace: + generatedMembers = SyntaxFactory.SingletonList( + CopyAsAncestor(ancestorNamespace) + .WithMembers(generatedMembers)); + break; + case ClassDeclarationSyntax nestingClass: + generatedMembers = SyntaxFactory.SingletonList( + CopyAsAncestor(nestingClass) + .WithMembers(generatedMembers)); + break; + case StructDeclarationSyntax nestingStruct: + generatedMembers = SyntaxFactory.SingletonList( + CopyAsAncestor(nestingStruct) + .WithMembers(generatedMembers)); + break; + } + return generatedMembers; + } + + private static NamespaceDeclarationSyntax CopyAsAncestor(NamespaceDeclarationSyntax syntax) + { + return SyntaxFactory.NamespaceDeclaration(syntax.Name.WithoutTrivia()) + .WithExterns(SyntaxFactory.List(syntax.Externs.Select(x => x.WithoutTrivia()))) + .WithUsings(SyntaxFactory.List(syntax.Usings.Select(x => x.WithoutTrivia()))); + } + + private static ClassDeclarationSyntax CopyAsAncestor(ClassDeclarationSyntax syntax) + { + return SyntaxFactory.ClassDeclaration(syntax.Identifier.WithoutTrivia()) + .WithModifiers(SyntaxFactory.TokenList(syntax.Modifiers.Select(x => x.WithoutTrivia()))) + .WithTypeParameterList(syntax.TypeParameterList); + } + + private static StructDeclarationSyntax CopyAsAncestor(StructDeclarationSyntax syntax) + { + return SyntaxFactory.StructDeclaration(syntax.Identifier.WithoutTrivia()) + .WithModifiers(SyntaxFactory.TokenList(syntax.Modifiers.Select(x => x.WithoutTrivia()))) + .WithTypeParameterList(syntax.TypeParameterList); + } + } } } diff --git a/src/CodeGeneration.Roslyn/IRichCodeGenerator.cs b/src/CodeGeneration.Roslyn/IRichCodeGenerator.cs new file mode 100644 index 000000000..d55da0734 --- /dev/null +++ b/src/CodeGeneration.Roslyn/IRichCodeGenerator.cs @@ -0,0 +1,19 @@ +// Copyright (c) Andrew Arnott. All rights reserved. +// Licensed under the MIT license. See LICENSE.txt file in the project root for full license information. + +namespace CodeGeneration.Roslyn +{ + using System; + using System.Threading; + using System.Threading.Tasks; + using Microsoft.CodeAnalysis; + + /// + /// Describes a code generator that responds to attributes on members to generate code, + /// and returns compilation unit members + /// + public interface IRichCodeGenerator : ICodeGenerator + { + Task GenerateRichAsync(TransformationContext context, IProgress progress, CancellationToken cancellationToken); + } +} diff --git a/src/CodeGeneration.Roslyn/RichGenerationResult.cs b/src/CodeGeneration.Roslyn/RichGenerationResult.cs new file mode 100644 index 000000000..cba9904f1 --- /dev/null +++ b/src/CodeGeneration.Roslyn/RichGenerationResult.cs @@ -0,0 +1,35 @@ +// Copyright (c) Andrew Arnott. All rights reserved. +// Licensed under the MIT license. See LICENSE.txt file in the project root for full license information. + +namespace CodeGeneration.Roslyn +{ + using System.Diagnostics; + using Microsoft.CodeAnalysis; + using Microsoft.CodeAnalysis.CSharp.Syntax; + + /// + /// Contains additions generated by the . + /// + public struct RichGenerationResult + { + /// + /// Gets or sets the to add to generated . + /// + public SyntaxList Members { get; set; } + + /// + /// Gets or sets the to add to generated . + /// + public SyntaxList Usings { get; set; } + + /// + /// Gets or sets the to add to generated . + /// + public SyntaxList Externs { get; set; } + + /// + /// Gets or sets the to add to generated . + /// + public SyntaxList AttributeLists { get; set; } + } +} diff --git a/src/CodeGeneration.Roslyn/TransformationContext.cs b/src/CodeGeneration.Roslyn/TransformationContext.cs index e8c854ae8..b7be45c15 100644 --- a/src/CodeGeneration.Roslyn/TransformationContext.cs +++ b/src/CodeGeneration.Roslyn/TransformationContext.cs @@ -1,5 +1,10 @@ -using Microsoft.CodeAnalysis; +// Copyright (c) Andrew Arnott. All rights reserved. +// Licensed under the MIT license. See LICENSE.txt file in the project root for full license information. + +using System.Collections.Generic; +using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; namespace CodeGeneration.Roslyn { @@ -12,13 +17,19 @@ public class TransformationContext /// The semantic model. /// The overall compilation being generated for. /// The absolute path of the directory where the project file is located + /// The using directives already queued to be generated. + /// The extern aliases already queued to be generated. public TransformationContext(CSharpSyntaxNode processingNode, SemanticModel semanticModel, CSharpCompilation compilation, - string projectDirectory) + string projectDirectory, + IEnumerable compilationUnitUsings, + IEnumerable compilationUnitExterns) { ProcessingNode = processingNode; SemanticModel = semanticModel; Compilation = compilation; ProjectDirectory = projectDirectory; + CompilationUnitUsings = compilationUnitUsings; + CompilationUnitExterns = compilationUnitExterns; } /// Gets the syntax node the generator attribute is found on. @@ -30,9 +41,13 @@ public TransformationContext(CSharpSyntaxNode processingNode, SemanticModel sema /// Gets the overall compilation being generated for. public CSharpCompilation Compilation { get; } - /// - /// Gets the absolute path of the directory where the project file is located - /// + /// Gets the absolute path of the directory where the project file is located. public string ProjectDirectory { get; } + + /// Gets a collection of using directives already queued to be generated. + public IEnumerable CompilationUnitUsings { get; } + + /// Gets a collection of extern aliases already queued to be generated. + public IEnumerable CompilationUnitExterns { get; } } }