-
Notifications
You must be signed in to change notification settings - Fork 58
Features
- An implementation of one of
ICodeGenerator/IRichCodeGenerator- Any class implementing those interfaces must have a constructor that has single parameter of type
Microsoft.CodeAnalysis.AttributeData. Otherwise an error will be raised on consumer build.
- Any class implementing those interfaces must have a constructor that has single parameter of type
To easier navigate through the convoluted area of multiple levels of builds, runtimes, compilations, code, source and generation, the following expressions are defined:
-CG.R or framework is the CodeGeneration.Roslyn framework in general, including but not limited to the build task, command line tool invoked from it, or the public contract.
-
[Code]Generatoris an implementation that performs the actual generation of source code given a compiled project. -
Consumeris the end-user of the framework and generators -
[Generator]Provideris the third party that actually provides generator implementation -
Trigger [Attribute]is the attribute that triggers framework's engine; this results in engine instantiating and invoking trigger-associated generator. -
original sourceis the source file written manually by consumer -
generated sourceis the source file created by the CG.R
This interface offers basic GenerateAsync method which will be called by CG.R Engine. Context of the generation location is passed via TransformationContext parameter, and two utility parameters are provided: IProgress will print given diagnostics into MSBuild log, and CancellationToken is provided for cancellation control.
Implementations are the actual generators that will then be used in consumer builds.
Example generator:
// file WorkInProgressGenerator.cs in project Awesome.Generators
using System;
using System.Threading;
using System.Threading.Tasks;
using CodeGeneration.Roslyn;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;
namespace Awesome.Generators
{
public class WorkInProgressGenerator : ICodeGenerator
{
public WorkInProgressGenerator(AttributeData attributeData) { }
public async Task<SyntaxList<MemberDeclarationSyntax>> GenerateAsync(
TransformationContext context,
IProgress<Diagnostic> progress,
CancellationToken cancellationToken)
{
SyntaxList<MemberDeclarationSyntax> results = await CreateNewMembersAsync();
return results;
}
private async Task<SyntaxList<MemberDeclarationSyntax>> CreateNewMembersAsync()
{
// TODO
throw new NotImplementedException();
}
}
}When a custom attribute definition is annotated with [CodeGenerationAttribute], it becomes the trigger attribute.
The type of code generator is read from attribute's CodeGenerationAttributeAttribute constructor parameter. This is either:
-
typeof(x)expression which requires code generator to live in the same assembly as the generator, or - a string constant containing fully qualified type name, e.g.
CodeGeneration.Roslyn.Tests.Generators.EmptyPartialGenerator, CodeGeneration.Roslyn.Tests.Generators. If you omit the assembly name (after comma), it'll resolve to the same assembly as the one in which the attribute itself was declared.
An example:
// file WorkInProgressAttribute.cs in project Awesome.Generators.Attributes
using System;
using CodeGeneration.Roslyn;
[CodeGenerationAttribute("Awesome.Generators.WorkInProgressGenerator, Awesome.Generators")]
public class WorkInProgressAttribute : Attribute
{
}The custom attribute can annotate any attribute-allowing syntax node, even a whole assembly (e.g. [assembly: WorkInProgress]), there are no constraints on custom attribute's target.
The generator is instantiated for every applicable attribute occurrence and appropriate AttributeData is passed into it. Immediately after that the method GenerateAsync is called.
The context consists of:
-
CSharpSyntaxNode ProcessingNodewhich is the syntax node that is annotated with a trigger attribute, e.g. a type, method, property, or even whole assembly -
CSharpCompilation Compilationwhich is the compilation model of all original source files. -
SemanticModel SemanticModelfor the wholeCSharpCompilationin use -
string ProjectDirectory- absolute path of the directory where the project file is located -
IEnumerable<UsingDirectiveSyntax> CompilationUnitUsingscontains usings generated by generators that already ran for some node in a document being processed. These usings will be added at the top of the generated source document. -
IEnumerable<ExternAliasDirectiveSyntax> CompilationUnitExternssimilar as above, but forexterndirectives.
GenerateAsync method returns Task<SyntaxList<MemberDeclarationSyntax>>. Member declaration can be any namespace, type or type member (field, property, method etc.) - see Derived in https://docs.microsoft.com/en-us/dotnet/api/microsoft.codeanalysis.csharp.syntax.memberdeclarationsyntax?view=roslyn-dotnet
The list can naturally be empty, but no matter the count, the behavior is the same.
For a ProcessingNode, an ancestry of containing declarations is copied and the generated members are inserted as members. Ancestry is the containing types and namespaces. It's best shown on an example:
-
[Trigger]triggers a generator that for a given ProcessingNode that is aClassDeclarationSyntaxcreates a copy of the class declaration but withGeneratedsuffix - a
ClassDeclarationSyntaxofHasTriggerGeneratedis returned fromGenerateAsyncas a list item - the ancestry of the trigger-annotated class is copied and the generated class declaration is put inside the ancestry
-
usings are copied from the original source - ancestor-wrapped generated members are saved as generated source
// File Content.cs - original source
using System;
namespace Some.Library
{
namespace Nested.Namespace
{
internal static partial class RootContainer<T>
{
public partial struct NestedContainer
{
[Trigger]
private partial class HasTrigger
{
}
}
}
}
}
// File Content.xxxxx.generated.cs - generated source
using System;
namespace Some.Library
{
namespace Nested.Namespace
{
internal static partial class RootContainer<T>
{
public partial struct NestedContainer
{
private partial class HasTriggerGenerated { }
}
}
}
}This interface was introduced late as an enhancement to better control the content of generated source, e.g. add using directives. To keep backwards compatibility, a new interface was needed. If a class implements the new contract, the new is used.
Although it inherits ICodeGenerator, the old method won't be invoked by the up-to-date build tool. However, old versions of the tool would fall-back and call the old contract. Because of that the inheritance makes sense.
The GenerateRichAsync has the same parameters as the old GenerateAsync (see TransformationContext).
The return type however is now a Task<RichGenerationResult>, which gives a lot more power to the generator provider - see below.
This struct has the following members:
-
Externslist ofExternAliasDirectiveSyntax -
Usingslist ofUsingDirectiveSyntax -
AttributeListslist ofAttributeListSyntax -
Memberslist ofMemberDeclarationSyntax
This mirrors the content of the CompilationUnitSyntax and indeed, the results of rich generators are concatenated per category and become the generated source's compilation unit.
As an implementation details, for a given original source file, all generators are run in order of triggers, their results concatenated into the four lists and made into compilation unit. That includes the simpler ICodeGeneration's results, which are wrapped in ancestor declarations and added to Members.