Why doesn't my Roslyn analyzer report warnings?

Viewed 160

I'm writing a Roslyn analyzer to check that a target type for JSON deserialization follows a few rules. In essence, if type Foo occurs in the expression JsonConvert.DeserializeObject<Foo>, then it should be a simple DTO (default constructor, public properties with JSON-friendly types).

I have registered a syntax node action to trigger on invocation expressions. Finding the appropriate calls works just fine. I then proceed to get the type symbol (for Foo in this case) from the generic method call using the semantic model. Finally I run my checks on type Foo using the type symbol. So far so good. The problem is when I want to report warnings. If I use the location of the invocation expression in my call to SyntaxNodeAnalysisContext.ReportDiagnostic, it works: my warnings appear, and I get a squiggly under the invocation. But that's not what I want. I want the squigglies to appear in the declaration of Foo, for instance under a problematic property or a non-default constructor. However, that doesn't seem to work. The warning simply doesn't appear in the warning list, and there is no squiggly. It seems like the reporting is ignored or silently fails for some reason.

I have tried multiple approaches to getting the appropriate location to use:

  • Use the Locations of the symbol directly: symbol.Locations[0]
  • Using FindNode on the source tree to find the SyntaxNode for the symbol: symbolLocation.SourceTree?.GetRoot()?.FindNode(symbolLocation.SourceSpan)?.GetLocation()
  • Using DeclaringSyntaxReferences: symbol.DeclaringSyntaxReferences[0].GetSyntax().GetLocation()

All of those approaches yield a location (it is not null). But my warnings fail to appear.

The code looks like this:

private void DoAnalyzeInvocationExpression(SyntaxNodeAnalysisContext context)
{
    var invocation = (InvocationExpressionSyntax)context.Node;

    if (invocation.Expression is MemberAccessExpressionSyntax memberAccess)
    {
        if (memberAccess.Expression is IdentifierNameSyntax typeIdentifierSyntax)
        {
            var textIdentifierText = typeIdentifierSyntax.Identifier.Text;
            if (textIdentifierText == "JsonConvert")
            {
                if (memberAccess.Name is GenericNameSyntax genericNameSyntax)
                {
                    if (genericNameSyntax.Identifier is SyntaxToken syntaxToken)
                    {
                        if (syntaxToken.Text == "DeserializeObject")
                        {
                            var typeArgs = genericNameSyntax.TypeArgumentList.Arguments;
                            if (typeArgs.Count == 1)
                            {
                                var typeArg = typeArgs[0];
                                var symbolInfo = context.SemanticModel.GetSymbolInfo(typeArg);
                                var typeSymbol = (INamedTypeSymbol)symbolInfo.Symbol;
                                var incompatibilities = VerifyDeserializationType(typeSymbol);
                                var diagnostics = incompatibilities.Select(it => ToSyntaxNodeAnalysisDiagnostic(it, memberAccess));
                                if (diagnostics.Any())
                                {
                                    // This works, and gives an overall warning that the deserialization has problems.
                                    context.ReportDiagnostic(Diagnostic.Create(DeserializeRule, invocation.GetLocation(), typeSymbol.Name));
                                    try
                                    {
                                        foreach (var d in diagnostics)
                                        {
                                            // These don't work, presumably because there is something wrong with the location.
                                            context.ReportDiagnostic(d);
                                        }
                                    }
                                    catch (Exception ex)
                                    {
                                        // This doesn't happen.
                                        context.ReportDiagnostic(Diagnostic.Create(DebugRule, memberAccess.GetLocation(), $"EXCEPTION: {ex.Message}"));
                                    }
                                }
                            }
                        }
                    }
                }
            }
        }
    }
}

private static Diagnostic ToSyntaxNodeAnalysisDiagnostic(Incompatibility incompatibility, SyntaxNode sourceNode)
{
    var symbolLocation = incompatibility.Symbol.Locations[0];

    var syntaxRef = incompatibility.Symbol.DeclaringSyntaxReferences[0];
    var syntax = syntaxRef.GetSyntax();

    //var node = symbolLocation.SourceTree?.GetRoot()?.FindNode(symbolLocation.SourceSpan);

    var sourceLocation = sourceNode.GetLocation();

    if (syntax == null)
    {
        return Diagnostic.Create(DebugRule, sourceLocation, "node == null");
    }

    var location = syntax.GetLocation();
    if (location == null)
    {
        return Diagnostic.Create(DebugRule, sourceLocation, "location == null");
    }

    if (incompatibility is PropIncompatibility propIncompatibility)
    {
        return Diagnostic.Create(PropRule, location, propIncompatibility.Symbol.Name, propIncompatibility.Explanation);
    }

    if (incompatibility is CtorIncompatibility)
    {
        return Diagnostic.Create(CtorRule, location, incompatibility.Symbol.ContainingSymbol.Name);
    }

    return Diagnostic.Create(DebugRule, location, "Unhandled incompatibility...");
}

Any suggestions would be welcome.

0 Answers
Related