0

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?

1 Answer 1

1

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(...), { ... });
Sign up to request clarification or add additional context in comments.

1 Comment

Thank you, that was my second-best idea, too. It's not a beautiful code (especially when you have to combine more than 2 results this way), but the speed of ForAttributeWithMetadataName really does matter (especially since you also get target symbol "for free" with this approach).

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.