有没有办法在Roslyn的分析器和代码修复提供程序之间传递数据(除了通过属性包)?

Phi*_*ref 6 c# code-analysis roslyn

随着新的RC发布,我很高兴看到现在有一​​个属性包允许提升的诊断功能有额外的数据,在我看来,其主要用例是能够在分析仪中运行数据计算进入代码修复程序,监听该特定诊断.

我现在意识到这个属性包只允许存储字符串值.虽然这可以证明是有用的,但我仍然发现自己必须在我的分析器和我的代码修复器中运行完全相同的逻辑,因为我没有能力只保留这些信息并将其传递给它.我当然在谈论更复杂的类型,例如语法节点和符号.

例如,我创建了一个分析器,强制using在每个文件中存在特定的指令集.分析器计算缺少哪些指令,并引发诊断通知用户并以文本方式指示缺失的指令.如果我已经拥有了SyntaxNode我必须实现的(我已经在我的分析器中),代码修复提供程序将非常简单,但我现在必须在我的代码修复程序中重新运行大部分相同的逻辑(这就是为什么我最终在我的分析器中使用公共静态辅助方法提供了大量代码)

现在,这个例子自从引入属性包以来失去了一些相关性,但我仍然认为它是一个有效的用例.我特别担心分析器和代码修复器之间唯一的链接在报告的诊断位置.在我的情况下,我可以有多个DiagnosticDescriptor实例,这些实例都可以代表源自特定"规则"的不同潜在问题,由a Diagnostic和它定义Id(我不知道这是否是Roslyn代码分析领域的一个好习惯,但是似乎是一种可接受的操作方式).

底线是:对于相同的诊断ID,我可能根据具体情况在不同位置(即在完全不同的语法元素上)引发诊断.因此,我失去了将所提供的位置放在确定的和/或相关的语法元素上的"确定性",并且修复诊断的后续逻辑消失了.

那么,有没有办法将数据从分析器传递给相关的代码修复提供程序?我还考虑过转发一个源自的自定义类型的实例Diagnostic,但它看起来像是一个代码味道,而且,Diagnostic还有很多抽象成员,我需要重新实现它的唯一目的是添加一个属性,并SimpleCodeFix密封(argggghhhh)

Phi*_*ref 4

由于凯文提到没有真正的方法来完成我在本地尝试做的事情,因为诊断预计是可序列化的,这让我想到我可以通过序列化在某种程度上模拟我想要的东西。我发布了我为解决该问题而提出的解决方案。请随意批评和/或强调一些潜在的问题。

语法元素容器

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();
    }   
}
Run Code Online (Sandbox Code Playgroud)

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()));
    }
}
Run Code Online (Sandbox Code Playgroud)

用例(为了简短起见,进行了简化)

// 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");
Run Code Online (Sandbox Code Playgroud)