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

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

随着新的 RC 版本的发布,我很高兴地看到现在有一​​个 属性 包可以让 raised 诊断获得额外的数据,在我看来,它的一个主要用例是能够将分析器中计算的数据转移到代码修复程序中,以侦听该特定诊断。

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

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

现在,这个例子在引入 属性 包后失去了一些相关性,但我仍然认为它是一个有效的用例。我特别担心报告诊断位置中分析器和代码修复器之间的唯一 link。在我的例子中,我可以有多个 DiagnosticDescriptor 实例,它们都可以代表不同的潜在问题,这些问题源于特定的 "Rule",由 Diagnostic 及其 Id 定义(我有不知道这在 Roslyn 代码分析领域是否是一种好的做法,但这似乎是一种可以接受的操作方式)。

底线是:对于相同的诊断 ID,我可能会根据情况在不同的位置(即完全不同的语法元素)引发诊断。因此,我失去了 "certainty" 将提供的位置放在确定的 and/or 相关语法元素上,并且随后修复诊断的逻辑超出了 window.

那么,有什么方法可以将数据从分析器传递给相关的代码修复提供者吗?我还考虑过向下转换派生自 Diagnostic 的自定义类型的实例,但对我来说这似乎是一种代码味道,而且 Diagnostic 充满了抽象成员,我需要重新 -实现的唯一目的是添加一个属性,并且SimpleCodeFix被密封(argggghhhh)

自从 Kevin 提到没有真正的方法来完成我在本机尝试做的事情,因为诊断预计是可序列化的,这让我想到我可以通过序列化来模拟我想要的东西。我是 发布我想出的解决方案来解决这个问题。随意批评 and/or 强调一些潜在的问题。

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");