Search code examples
c#.netroslynsourcegeneratorsincremental-generator

Incremental generator produces duplicate filename


I'm trying to build an incremental source generator which generates c# constructors for Dependency-injection. However, it seems my generator produces 2 sources with the same name Classes.g.cs.

The project is hosted here. In order to try it:

git clone https://github.com/MintPlayer/MintPlayer.Dotnet.Tools

Launch Visual Studio, and set the SourceGenerators project (NOT SourceGenerators.Debug) as startup project, and hit F5. If you have the .NET Compiler Platform SDK installed through the Visual Studio Installer, this will run the generators on the files in the Debug project.

You'll see that the breakpoint statement I added in code is being hit twice, so the generator produces this file one time too often.

Why is this generator being run twice?

Background

The XxxSourceGenerator classes are the actual generators. They will be run by Visual Studio while you're typing your code.

[Generator(LanguageNames.CSharp)]
public class ClassNamesSourceGenerator : IIncrementalGenerator {
    public void Initialize(IncrementalGeneratorInitializationContext context) {
        var classDeclarationsProvider = context.SyntaxProvider
            .CreateSyntaxProvider(
                static (node, ct) => node is ClassDeclarationSyntax { } classDeclaration,
                static (context, ct) => {
                    if (context.Node is ClassDeclarationSyntax classDeclaration &&
                        context.SemanticModel.GetDeclaredSymbol(classDeclaration, ct) is INamedTypeSymbol symbol) {
                        return new Models.ClassDeclaration { Name = symbol.Name };
                    } else {
                        return default;
                    }
                }
            )
            .WithComparer(ValueComparers.ClassDeclarationValueComparer.Instance)
            .Collect();

        var fieldDeclarationsProvider = context.SyntaxProvider
            .CreateSyntaxProvider(
                static (node, ct) => node is FieldDeclarationSyntax { AttributeLists.Count: > 0 } fieldDeclaration
                    && fieldDeclaration.Modifiers.Any(Microsoft.CodeAnalysis.CSharp.SyntaxKind.ReadOnlyKeyword),
                static (context2, ct) => {
                    ...
                    return new Models.FieldDeclaration {
                        Namespace = namespaceDeclaration.Name.ToString(),
                        FullyQualifiedClassName = classSymbol.ToDisplayString(),
                        ClassName = classSymbol.Name,
                        Name = symbol.Name,
                        FullyQualifiedTypeName = symbol.Type.ToDisplayString(),
                        Type = symbol.Type.Name,
                    };
                }
            )
            .Collect();

They first transofrm the data from the providers, and in turn call on the multiple producers which individually transofrm the symbols into c# code.

var classNamesSourceProvider = classDeclarationsProvider
    .Combine(config)
    .Select(static (p, ct) => new Producers.ClassNamesProducer(declarations: p.Left, rootNamespace: p.Right.RootNamespace!));

var classNameListSourceProvider = classDeclarationsProvider
    .Combine(config)
    .Select(static (p, ct) => new Producers.ClassNameListProducer(declarations: p.Left, rootNamespace: p.Right.RootNamespace!));

var fieldDeclarationSourceProvider = fieldDeclarationsProvider
    .Combine(config)
    .Select(static (p, ct) => new Producers.FieldNameListProducer(declarations: p.Left, rootNamespace: p.Right.RootNamespace!));

Then you need to combine the output of all these producers and pass your sourceprovider to .NET/VS

// Combine all Source Providers
var sourceProvider = classNamesSourceProvider
    .Combine(classNameListSourceProvider)
    .SelectMany(static (p, _) => new Producer[] { p.Left, p.Right })
    .Combine(fieldDeclarationSourceProvider)
    .SelectMany(static (p, _) => new Producer[] { p.Left, p.Right });

// Generate Code
context.RegisterSourceOutput(sourceProvider, static (c, g) => g?.Produce(c));

Edit

Remarkably, if I use this snippet instead:

var sourceProvider = fieldDeclarationSourceProvider
    .Combine(classNamesSourceProvider)
    .SelectMany(static (p, _) => new Producer[] { p.Left, p.Right })
    .Combine(classNameListSourceProvider)
    .SelectMany(static (p, _) => new Producer[] { p.Left, p.Right });

the fieldDeclarations file is only generated once, but classNameList file is being generated twice, so apparently the bottom provider is being triggered twice

// Generates ClassNameList.g.cs twice
var sourceProvider = classNamesSourceProvider
    .Combine(fieldDeclarationSourceProvider)
    .SelectMany(static (p, _) => new Producer[] { p.Left, p.Right })
    .Combine(classNameListSourceProvider)
    .SelectMany(static (p, _) => new Producer[] { p.Left, p.Right });
// Generates FieldNameList.g.cs twice
var sourceProvider = classNamesSourceProvider
    .Combine(classNameListSourceProvider)
    .SelectMany(static (p, _) => new Producer[] { p.Left, p.Right })
    .Combine(fieldDeclarationSourceProvider)
    .SelectMany(static (p, _) => new Producer[] { p.Left, p.Right });
// Generates ClassNameList.g.cs twice
var sourceProvider = fieldDeclarationSourceProvider
    .Combine(classNamesSourceProvider)
    .SelectMany(static (p, _) => new Producer[] { p.Left, p.Right })
    .Combine(classNameListSourceProvider)
    .SelectMany(static (p, _) => new Producer[] { p.Left, p.Right });
// Generates ClassNames.g.cs twice
var sourceProvider = fieldDeclarationSourceProvider
    .Combine(classNameListSourceProvider)
    .SelectMany(static (p, _) => new Producer[] { p.Left, p.Right })
    .Combine(classNamesSourceProvider)
    .SelectMany(static (p, _) => new Producer[] { p.Left, p.Right });
// Generates ClassNames.g.cs twice
var sourceProvider = classNameListSourceProvider
    .Combine(fieldDeclarationSourceProvider)
    .SelectMany(static (p, _) => new Producer[] { p.Left, p.Right })
    .Combine(classNamesSourceProvider)
    .SelectMany(static (p, _) => new Producer[] { p.Left, p.Right });
// Generates FieldNameList.g.cs twice
var sourceProvider = classNameListSourceProvider
    .Combine(classNamesSourceProvider)
    .SelectMany(static (p, _) => new Producer[] { p.Left, p.Right })
    .Combine(fieldDeclarationSourceProvider)
    .SelectMany(static (p, _) => new Producer[] { p.Left, p.Right });

Solution

  • Turns out I had to use the following instead

    // Combine all Source Providers
    var sourceProvider = classNamesSourceProvider
        .Combine(fieldDeclarationSourceProvider)
        .SelectMany(static (p, _) => new Producer[] { p.Left, p.Right })
        .Collect()
        .Combine(classNameListSourceProvider)
        .SelectMany(static (p, _) => p.Left.Concat(new Producer[] { p.Right }));