Last active
August 26, 2020 05:19
-
-
Save TessenR/a59c07d91b0128e711ef2dce25e5ac61 to your computer and use it in GitHub Desktop.
Chained source generators where subsequent generators aware of the previous generators' changes
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System; | |
using System.Collections; | |
using System.Collections.Generic; | |
using System.Collections.Immutable; | |
using System.Diagnostics; | |
using System.Globalization; | |
using System.IO; | |
using System.Reflection; | |
using System.Text; | |
using Microsoft.CodeAnalysis; | |
using Microsoft.CodeAnalysis.CSharp; | |
using Microsoft.CodeAnalysis.CSharp.Syntax; | |
using Microsoft.CodeAnalysis.Text; | |
// other generators must not have the [Generator] attribute as they won't be run directly by the compiler | |
[Generator] | |
public class ChainedGenerator : ISourceGenerator | |
{ | |
// collect your generators in correct order - static as there, or registered via attributes | |
// in this example SecondGenerator will see the output of FirstGenerator | |
private List<ISourceGenerator> myGenerators = new List<ISourceGenerator> {new FirstGenerator(), new SecondGenerator()}; | |
// generators can rely on syntax visitors, we'll have to pass the events to them | |
public void Initialize(InitializationContext context) | |
{ | |
var syntaxReceivers = new Dictionary<ISourceGenerator, ISyntaxReceiver>(); | |
foreach (var generator in myGenerators) | |
{ | |
// create unique context to register syntax receivers for each generator | |
var childContext = (InitializationContext) Activator.CreateInstance(typeof(InitializationContext), bindingAttr: BindingFlags.Public | BindingFlags.Static | BindingFlags.NonPublic |BindingFlags.Instance | BindingFlags.Default | BindingFlags.NonPublic, binder: default(Binder), args: new object[] { context.CancellationToken }, culture: default(CultureInfo)); | |
generator.Initialize(childContext); | |
// get the syntax receiver back and remember it to pass events to it later | |
var infoBuilder = childContext.GetType().GetProperty("InfoBuilder", BindingFlags.NonPublic | BindingFlags.Instance).GetValue(childContext); | |
var factory = infoBuilder.GetType().GetProperty("SyntaxReceiverCreator", BindingFlags.NonPublic | BindingFlags.Instance).GetValue(infoBuilder); | |
var childReceiver = factory.GetType().GetMethod("Invoke").Invoke(factory, new object[0]); | |
syntaxReceivers.Add(generator, (ISyntaxReceiver) childReceiver); | |
} | |
// register our own aggregating receiver that would pass visited nodes to all generators | |
context.RegisterForSyntaxNotifications(() => new PropagatingReceiver(syntaxReceivers)); | |
} | |
public void Execute(SourceGeneratorContext context) | |
{ | |
CSharpParseOptions options = (CSharpParseOptions) ((CSharpCompilation)context.Compilation).SyntaxTrees[0].Options; | |
var receiver = (PropagatingReceiver) context.SyntaxReceiver!; | |
// compilation unit that will be updated as we run generators | |
var compilation = context.Compilation; | |
foreach (var generator in myGenerators) | |
{ | |
// create context for the next generator, reuse updated compilation unit with output from previous generators | |
var childContext = (SourceGeneratorContext) Activator.CreateInstance( | |
typeof(SourceGeneratorContext), | |
BindingFlags.Public | BindingFlags.Static | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Default | BindingFlags.NonPublic, | |
default, | |
new object[] | |
{ | |
compilation, | |
ImmutableArray<AdditionalText>.Empty, // you'll need your own mechanism to provide theses | |
context.GetType().GetProperty("AnalyzerConfigOptions", BindingFlags.Public | BindingFlags.Static | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Default | BindingFlags.NonPublic).GetValue(context), | |
receiver.myReceivers[generator], | |
context.GetType().GetField("_diagnostics", BindingFlags.NonPublic | BindingFlags.Instance).GetValue(context), | |
context.CancellationToken | |
}, | |
default); | |
// run the generator | |
generator.Execute(childContext); | |
// now extract the sources it added to the compilation | |
var addedSources = childContext.GetType().GetProperty("AdditionalSources", BindingFlags.NonPublic | BindingFlags.Instance).GetValue(childContext); | |
var methodInfo = addedSources.GetType().GetMethod("ToImmutableAndFree", BindingFlags.NonPublic | BindingFlags.Instance); | |
var generatedSources = (IEnumerable) methodInfo.Invoke(addedSources, BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.InvokeMethod, default, new object[0], default); | |
foreach (var generatedSourceText in generatedSources) | |
{ | |
var hintName = (string) generatedSourceText.GetType().GetProperty("HintName", BindingFlags.Public | BindingFlags.Instance).GetValue(generatedSourceText); | |
var sourceText = (SourceText) generatedSourceText.GetType().GetProperty("Text", BindingFlags.Public | BindingFlags.Instance).GetValue(generatedSourceText); | |
// add the source to the real compilation | |
context.AddSource(hintName, sourceText); | |
// run syntax visitor on the added files to notify subsequent generators | |
var syntaxTree = CSharpSyntaxTree.ParseText(sourceText, options); | |
Visit(syntaxTree.GetRoot()); | |
// update the compilation that will be passed to subsequent generators with new sources | |
compilation = compilation.AddSyntaxTrees(syntaxTree); | |
} | |
void Visit(SyntaxNode node) | |
{ | |
receiver.OnVisitSyntaxNode(node); | |
foreach (var child in node.ChildNodes()) | |
{ | |
Visit(child); | |
} | |
} | |
} | |
} | |
} | |
class PropagatingReceiver : ISyntaxReceiver | |
{ | |
public readonly IReadOnlyDictionary<ISourceGenerator, ISyntaxReceiver> myReceivers; | |
public PropagatingReceiver(IReadOnlyDictionary<ISourceGenerator, ISyntaxReceiver> receivers) => myReceivers = receivers; | |
public void OnVisitSyntaxNode(SyntaxNode syntaxNode) | |
{ | |
foreach (var receiver in myReceivers.Values) | |
{ | |
receiver.OnVisitSyntaxNode(syntaxNode); | |
} | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
public class SyntaxReceiver<T> : ISyntaxReceiver where T: SyntaxNode | |
{ | |
public List<T> VisitedNodes = new List<T>(); | |
public void OnVisitSyntaxNode(SyntaxNode syntaxNode) | |
{ | |
if (syntaxNode is T node) | |
VisitedNodes.Add(node); | |
} | |
} | |
public class FirstGenerator : ISourceGenerator | |
{ | |
public void Initialize(InitializationContext context) => context.RegisterForSyntaxNotifications(() => new SyntaxReceiver<ClassDeclarationSyntax>()); | |
public void Execute(SourceGeneratorContext context) | |
{ | |
var source = new StringBuilder(); | |
source.Append(@" | |
static class FirstGeneratorOutput | |
{ | |
public static void Method() | |
{ | |
"); | |
foreach (var tree in context.Compilation.SyntaxTrees) | |
{ | |
var fileName = Path.GetFileName(tree.FilePath); | |
if (string.IsNullOrWhitespace(fileName)) | |
fileName = "Unnamed"; | |
source.AppendLine(" System.Console.WriteLine(\"Available source: " + fileName + "\");"); | |
} | |
var receiver = (SyntaxReceiver<ClassDeclarationSyntax>) context.SyntaxReceiver!; | |
foreach (var classDeclarationSyntax in receiver.VisitedNodes) | |
{ | |
source.AppendLine(" System.Console.WriteLine(\"Syntax processed for class: " + classDeclarationSyntax.Identifier.Text + "\");"); | |
} | |
source.Append(@" | |
} | |
}"); | |
context.AddSource("firstOutput", SourceText.From(source.ToString(), Encoding.UTF8)); | |
} | |
} | |
public class SecondGenerator : ISourceGenerator | |
{ | |
public void Initialize(InitializationContext context) => context.RegisterForSyntaxNotifications(() => new SyntaxReceiver<ClassDeclarationSyntax>()); | |
public void Execute(SourceGeneratorContext context) | |
{ | |
var source = new StringBuilder(); | |
source.Append(@" | |
static class SecondGeneratorOutput | |
{ | |
public static void Method() | |
{ | |
"); | |
foreach (var tree in context.Compilation.SyntaxTrees) | |
{ | |
var fileName = Path.GetFileName(tree.FilePath); | |
if (string.IsNullOrWhitespace(fileName)) | |
fileName = "Unnamed"; | |
source.AppendLine(" System.Console.WriteLine(\"Available source: " + fileName + "\");"); | |
} | |
var receiver = (SyntaxReceiver<ClassDeclarationSyntax>) context.SyntaxReceiver!; | |
foreach (var classDeclarationSyntax in receiver.VisitedNodes) | |
{ | |
source.AppendLine(" System.Console.WriteLine(\"Syntax processed for: " + classDeclarationSyntax.Identifier.Text + "\");"); | |
} | |
source.Append(@" | |
} | |
}"); | |
context.AddSource("secondOutput", SourceText.From(source.ToString(), Encoding.UTF8)); | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Available source: Program.cs | |
Available source: .NETCoreApp,Version=v5.0.AssemblyAttributes.cs | |
Available source: GeneratorConsumer.AssemblyInfo.cs | |
Syntax processed for class: C | |
Available source: Program.cs | |
Available source: .NETCoreApp,Version=v5.0.AssemblyAttributes.cs | |
Available source: GeneratorConsumer.AssemblyInfo.cs | |
Available source: Unnamed | |
Syntax processed for: C | |
Syntax processed for: FirstGeneratorOutput |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System; | |
public class C | |
{ | |
static void Main(string[] args) | |
{ | |
FirstGeneratorOutput.Method(); | |
Console.WriteLine(); | |
Console.WriteLine(); | |
SecondGeneratorOutput.Method(); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment