Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Is there a way to pass data (other than through the property bag) between an analyzer and a code fix provider in Roslyn?

with the new RC release, I was excited to see that there now was a property bag to allow raised diagnostics to have additional data, a major use case of which was, in my opinion, to be able to have data computed in the analyzer carried over into a code fixer listening for that particular diagnostic.

I now realize that this property bag only allows to store string values. While this can prove useful, I still find myself having to run the exact same logic in my analyzer and my code fixer since I do not have the ability to just keep this information and pass it on. I am of course talking about more complex types, such as syntax nodes and symbols.

For example, I created an analyzer that enforced the presence of particular set of using directives in every file. The analyzer computes which directives are missing and raises a diagnostic that notifies the user and textually indicates the missing directives. The code fix provider would be pretty straight forward if I already had the SyntaxNodes I have to implement (which I already have in my analyzer), but I now have to re-run much of the same logic in my code fixer (which is why I ended up putting a lot of code present in my analyzer in public static helper methods)

Now, this example lost some of its relevance since the introduction of the property bag, but I still think it is a valid use case. I am especially concerned that the only link between the analyzer and the code fixer in the location of the reported diagnostic. In my case, I can have multiple DiagnosticDescriptor instances that could all represent different potential problems stemming from a particular "Rule", as defined by a Diagnostic and its Id (I have no idea if that is a good practice in the realm of Roslyn code analysis, but is seemed like an acceptable way to operate).

Bottom line is: for the same diagnostic Id, I could potentially have the diagnostic raised at different locations (i.e. on completely different syntax elements) depending on the case. I therefore lose the "certainty" of having the provided location be on a definitive and/or relevant syntax element, and the subsequent logic to fix the diagnostic goes out the window.

So, is there any way to pass data from the analyzer to a related code fix provider ? I also thought about downcasting an instance of a custom type that derives from Diagnostic, but it seemed like a code smell to me and, furthermore, Diagnostic is full of abstract members that I would need to re-implement for the sole purpose of adding one property, and SimpleCodeFix is sealed (argggghhhh)

like image 216
Phil Gref Avatar asked May 06 '15 18:05

Phil Gref


1 Answers

Since Kevin mentioned that there was no real way to accomplish what I was trying to do natively because the diagnostics are expected to be serializable, it got me thinking that I could somewhat emulate what I wanted through serialization. I am posting the solution I came up with to solve the issue. Feel free to criticize and/or underline some potential issues.

SyntaxElementContainer

public class SyntaxElementContainer<TKey> : Dictionary<string, string>
{
    private const string Separator = "...";
    private static readonly string DeserializationPattern = GetFormattedRange(@"(\d+)", @"(\d+)");

    private static string GetFormattedRange(string start, string end)
    {
        return $"{start}{Separator}{end}";
    }

    public SyntaxElementContainer()
    {
    }

    public SyntaxElementContainer(ImmutableDictionary<string, string> propertyBag)
        : base(propertyBag)
    {
    }

    public void Add(TKey nodeKey, SyntaxNode node)
    {
        Add(nodeKey.ToString(), SerializeSpan(node?.Span));
    }

    public void Add(TKey tokenKey, SyntaxToken token)
    {
        Add(tokenKey.ToString(), SerializeSpan(token.Span));
    }

    public void Add(TKey triviaKey, SyntaxTrivia trivia)
    {
        Add(triviaKey.ToString(), SerializeSpan(trivia.Span));
    }


    public TextSpan GetTextSpanFromKey(string syntaxElementKey)
    {
        var spanAsText = this[syntaxElementKey];
        return DeSerializeSpan(spanAsText);
    }

    public int GetTextSpanStartFromKey(string syntaxElementKey)
    {
        var span = GetTextSpanFromKey(syntaxElementKey);
        return span.Start;
    }

    private string SerializeSpan(TextSpan? span)
    {
        var actualSpan = span == null || span.Value.IsEmpty ? default(TextSpan) : span.Value; 
        return GetFormattedRange(actualSpan.Start.ToString(), actualSpan.End.ToString());
    }

    private TextSpan DeSerializeSpan(string spanAsText)
    {
        var match = Regex.Match(spanAsText, DeserializationPattern);
        if (match.Success)
        {
            var spanStartAsText = match.Groups[1].Captures[0].Value;
            var spanEndAsText = match.Groups[2].Captures[0].Value;

            return TextSpan.FromBounds(int.Parse(spanStartAsText), int.Parse(spanEndAsText));
        }

        return new TextSpan();
    }   
}

PropertyBagSyntaxInterpreter

public class PropertyBagSyntaxInterpreter<TKey>
{
    private readonly SyntaxNode _root;

    public SyntaxElementContainer<TKey> Container { get; }

    protected PropertyBagSyntaxInterpreter(ImmutableDictionary<string, string> propertyBag, SyntaxNode root)
    {
        _root = root;
        Container = new SyntaxElementContainer<TKey>(propertyBag);
    }

    public PropertyBagSyntaxInterpreter(Diagnostic diagnostic, SyntaxNode root)
        : this(diagnostic.Properties, root)
    {
    }

    public SyntaxNode GetNode(TKey nodeKey)
    {
        return _root.FindNode(Container.GetTextSpanFromKey(nodeKey.ToString()));
    }

    public TSyntaxType GetNodeAs<TSyntaxType>(TKey nodeKey) where TSyntaxType : SyntaxNode
    {
        return _root.FindNode(Container.GetTextSpanFromKey(nodeKey.ToString())) as TSyntaxType;
    }


    public SyntaxToken GetToken(TKey tokenKey)
    {

        return _root.FindToken(Container.GetTextSpanStartFromKey(tokenKey.ToString()));
    }

    public SyntaxTrivia GetTrivia(TKey triviaKey)
    {
        return _root.FindTrivia(Container.GetTextSpanStartFromKey(triviaKey.ToString()));
    }
}

Use case (simplified for shortness' sake)

// In the analyzer
MethodDeclarationSyntax someMethodSyntax = ...
var container = new SyntaxElementContainer<string>
{
    {"TargetMethodKey", someMethodSyntax}
};

// In the code fixer
var bagInterpreter = new PropertyBagSyntaxInterpreter<string>(diagnostic, root);
var myMethod = bagInterpreter.GetNodeAs<MethodDeclarationSyntax>("TargetMethodKey");
like image 98
Phil Gref Avatar answered Oct 05 '22 23:10

Phil Gref