Search code examples
c#roslynroslyn-code-analysissourcegenerators

How to build Roslyn Source Generator pipeline that depends on config options passed from host?


I need to write a source generator that scans the project for one or another set of classes (marked with different attributes), then generates the same code from them (the second set of attributes is a "compatibility" option). I can, of course, just find all type declarations, and later filter them using options, but Microsoft states that ForAttributeWithMetadataName is significantly faster than CreateSyntaxProvider, and offloading all filtering to RegisterSourceOutput would be bad for caching.

So far, the only solution (or, rather, a part of solution) I happened upon is to use nested value providers like that:

var optionsPipeline = context.AnalyzerConfigOptionsProvider.Select((optionsProvider, _) =>
{
   // Transform option value into a list of attribute names
})
.SelectMany((attributeNames,_) => attributeNames);

// At this point, we have an IncrementalValuesProvider<string>

var mainPipeline = optionsPipeline.Select((attributeName, _) =>
{
    var typesDeclarations = context.SyntaxProvider.ForAttributeWithMetadataName(
                            attributeName,
                            predicate: { ... }
                            transform: { ... }
    );
    return typesDeclaration;
});

// At this point, mainPipeline is IncrementalValuesProvider<IncrementalValuesProvider<TypeDeclarationSyntax>>

I need to apply some more transformations to the selected TypeDeclarationSytnax for caching to actually work, and to extract the data I need for actual source generation, but this is irrelevant here.

Right now, I don't understand what's the next step. Ideally, I'd like to transform IncrementalValuesProvider<IncrementalValuesProvider<TypeDeclarationSyntax>> into just IncrementalValuesProvider<TypeDeclarationSyntax> by appending output of each provider, and then call RegisterSourceOutput, but I can't understand if this is even possible to achieve this.

I can call RegisterSourceOutput on the mainPipeline as is, but I'm not even sure it's legal (RegisterSourceOutput's action would receive and ImmutableArray<IncrementalValuesProvider<TypeDeclarationSyntax>>, and I'm not sure it will be able to do ANYTHING with it at that point).

So, does anyone has a clue if I can make this work, or if there is another approach to building option-dependent pipeline without offloading filtering to RegisterSourceOutput? Or is this a fool's errand?


Solution

  • There is no way for this exact solution to work, but my recommendation would be to instead call ForAttributeWithMetadataName for each attribute you need (there's no nice way to do this with a loop since you need to Combine them all later, you just have to write a call for every attribute) and then Combine the "compatibility" attributes with a provider for whether or not the compatibility option is enabled, like so:

    var compatibilityEnabled =
        context.AnalyzerConfigOptionsProvider
            .Select((o, _) =>
                o.GlobalOptions.TryGetValue("yourconfigoption", out var opt) && bool.TryParse(opt, out var optBool) ? optBool : false
            );
    
    var nonCompatibilityAttribute =
        context.SyntaxProvider.ForAttributeWithMetadataName(
            "FirstAttribute",
            predicate: { ... },
            transform: { ... }
        );
        
    var compatibilityAttribute =
        context.SyntaxProvider.ForAttributeWithMetadataName(
            "SecondAttribute",
            predicate: { ... },
            transform: { ... }
        )
        .Combine(compatibilityEnabled)
        .Where(pair => pair.Right)
        .Select((pair, _) => pair.Left);
    
    // finally
    context.RegisterSourceOutput(nonCompatibilityAttribute.Collect().Combine(compatibilityAttribute.Collect()).Combine(...), { ... });