Search code examples
c#roslynroslyn-code-analysismicrosoft.codeanalysis

Determining if a private field is read using Roslyn


I've been searching all day and have read many posts but I just can't quite come to a conclusion on this. I'm trying to create a Roslyn analyzer to report a diagnostic when a private field is unread. Registering the syntax action and finding out if its private was really easy. But now I'm stuck on trying to find out if the field is read in the class.

Assume we have the following example code:

public class C {
    private int foo; //private field is declared but never read. Should report diagnostic here
    
    public void DoNothing() {
        //irrelevant
    }
}

There are several examples of where I'd want this flagged (initialized or not, injected or not, multiple declarations on single line, etc.), but I think maybe they're not necessary for illustrating the question.

What I have so far is this:

public override void Initialize(AnalysisContext context) {
    context.EnableConcurrentExecution();
    context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);

    context.RegisterSyntaxNodeAction(AnalyzeField, SyntaxKind.FieldDeclaration);
}

private void AnalyzeField(SyntaxNodeAnalysisContext context) {
    if (!(context.Node is FieldDeclarationSyntax fieldDeclarationSyntax)) {
        return;
    }

    foreach (var variableDeclaration in fieldDeclarationSyntax.Declaration.Variables) {
        if (context.SemanticModel.GetDeclaredSymbol(variableDeclaration) is IFieldSymbol variableDeclarationSymbol &&
            IsFieldPrivate(variableDeclarationSymbol) &&
            !IsFieldRead(context, variableDeclarationSymbol)) {
            
            //report diagnostic here
        }
    }
}

private bool IsFieldPrivate(IFieldSymbol fieldSymbol) {
    return fieldSymbol.DeclaredAccessibility == Accessibility.Private || // the field itself is explicitly private
           fieldSymbol.ContainingType?.DeclaredAccessibility == Accessibility.Private; //the field is not private, but is contained within a private class
}

private bool IsFieldRead(SyntaxNodeAnalysisContext context, IFieldSymbol fieldSymbol) {
    //context.Node.Parent will be the class declaration here since we're analyzing a field declaration
    //but let's be safe about that just in case and make sure we traverse up until we find the class declaration
    var classDeclarationSyntax = context.Node.Parent;
    while (!(classDeclarationSyntax is ClassDeclarationSyntax)) {
        classDeclarationSyntax = classDeclarationSyntax.Parent;
    }
    var methodsInClassContainingPrivateField = classDeclarationSyntax.DescendantNodes().OfType<MethodDeclarationSyntax>().ToImmutableArray();
    foreach (var method in methodsInClassContainingPrivateField) {
        var dataFlowAnalysis = context.SemanticModel.AnalyzeDataFlow(method); //does not work because this is not a StatementSyntax or ExpressionSyntax
        if (dataFlowAnalysis.ReadInside.Contains(fieldSymbol) || dataFlowAnalysis.ReadOutside.Contains(fieldSymbol)) {
            return true;
        }
    }

    return false;
}

I just can't quite figure out how to get the IsFieldRead() method to work. This really feels like something that should be easy to do but I just can't quite wrap my head around it. I figured getting the methods and analyzing those for my field to see if it was read would be a decent idea, but that doesn't cover if the field is read by another private field, and I can't get it working anyway. :)


Solution

  • I managed to get this figured out thanks to this other SO answer by someone who actually works on Roslyn at Microsoft. Here is my IsFieldRead() method now. The key apparently lied in the Microsoft.CodeAnalysis.Operations namespace.

    private bool IsFieldRead(SyntaxNodeAnalysisContext context, IFieldSymbol fieldSymbol) {
        var classDeclarationSyntax = context.Node.Parent;
        while (!(classDeclarationSyntax is ClassDeclarationSyntax)) {
            classDeclarationSyntax = classDeclarationSyntax.Parent;
            if (classDeclarationSyntax == null) {
                throw new InvalidOperationException("You have somehow traversed up and out of the syntax tree when determining if a private member field is being read.");
            }
        }
    
        //get all methods in the class
        var methodsInClass = classDeclarationSyntax.DescendantNodes().OfType<MethodDeclarationSyntax>().ToImmutableArray();
        foreach (var method in methodsInClass) {
            //get all member references in those methods
            if (context.SemanticModel.GetOperation(method).Descendants().OfType<IMemberReferenceOperation>().ToImmutableArray().Any(x => x.Member.Equals(fieldSymbol))) {
                return true;
            }
        }
    
        return false;
    }
    

    Note that this only covers usages within methods. There are several other places like other fields, properties, and constructors that would also need to be checked.