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 SyntaxNode
s 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)
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");
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With