随着新的RC发布,我很高兴看到现在有一个属性包允许引发的诊断有额外的数据,在我看来,其主要用例是能够在分析仪转移到代码修复程序中,监听该特定诊断。
我现在意识到这个属性包只允许存储字符串值。虽然这可以证明是有用的,但我仍然发现自己必须在我的分析器和我的代码修复程序中运行完全相同的逻辑,因为我没有能力保存这些信息并将其传递给它。我当然在谈论更复杂的类型,例如语法节点和符号。
例如,我创建了一个分析器,在每个文件中强制存在特定的using
指令集。分析器计算缺少哪些指令,并引发诊断通知用户并以文本方式指示缺失的指令。如果我已经有SyntaxNode
我必须实现(我已经在我的分析器中),代码修复提供程序将非常简单,但我现在必须在我的代码中重新运行大部分相同的逻辑修复工具(这就是为什么我最终在我的分析器中使用公共静态辅助方法放置了很多代码)
现在,自从引入属性包以来,这个例子失去了一些相关性,但我仍然认为它是一个有效的用例。我特别担心分析器和代码修复器之间唯一的链接在报告的诊断位置。在我的情况下,我可以有多个DiagnosticDescriptor
个实例,这些实例都可能代表由Diagnostic
及其Id
定义的特定"规则"产生的不同潜在问题(我不知道这是否是罗斯林代码分析领域的一个好习惯,但似乎是一种可接受的操作方式)。
底线是:对于相同的诊断ID,我可能根据具体情况在不同位置(即在完全不同的语法元素上)引发诊断。因此,我失去了确定性"将提供的位置放在确定的和/或相关的语法元素上,然后修复诊断的后续逻辑就会消失。
那么,有没有办法将数据从分析器传递给相关的代码修复提供程序?我还考虑了向下传播一个源自Diagnostic
的自定义类型的实例,但它看起来像是一个代码味道,而且,Diagnostic
充满了抽象成员,我需要重新实现的唯一目的是添加一个属性,SimpleCodeFix
已被密封(argggghhhh)
答案 0 :(得分:3)
由于凯文提到没有真正的方法可以完成我本来想做的事情,因为诊断预计是可序列化的,它让我觉得我可以通过序列化来模仿我想要的东西。我是 发布我提出的解决问题的解决方案。随意批评和/或强调一些潜在的问题。
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()));
}
}
用例(为了简洁而简化)
// 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");