Skip to content

Instantly share code, notes, and snippets.

@TessenR
Last active August 26, 2020 05:19
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save TessenR/a59c07d91b0128e711ef2dce25e5ac61 to your computer and use it in GitHub Desktop.
Save TessenR/a59c07d91b0128e711ef2dce25e5ac61 to your computer and use it in GitHub Desktop.
Chained source generators where subsequent generators aware of the previous generators' changes
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);
}
}
}
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));
}
}
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
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