File: InternalReferencedInPublicDocAnalyzer.cs
Web Access
Project: src\src\Analyzers\Microsoft.Analyzers.Local\Microsoft.Analyzers.Local.csproj (Microsoft.Analyzers.Local)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System;
using System.Collections.Immutable;
using System.Linq;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.Extensions.LocalAnalyzers.Utilities;
 
namespace Microsoft.Extensions.LocalAnalyzers;
 
/// <summary>
/// C# analyzer that warns about referencing internal symbols in public xml documentation.
/// </summary>
[DiagnosticAnalyzer(LanguageNames.CSharp)]
public class InternalReferencedInPublicDocAnalyzer : DiagnosticAnalyzer
{
    private static readonly ImmutableArray<DiagnosticDescriptor> _supportedDiagnostics = ImmutableArray.Create(DiagDescriptors.InternalReferencedInPublicDoc);
 
    private static MemberDeclarationSyntax? FindDocumentedSymbol(XmlCrefAttributeSyntax crefNode)
    {
        // Find the documentation comment the cref node is part of
        var documentationComment = crefNode.Ancestors(ascendOutOfTrivia: false).OfType<DocumentationCommentTriviaSyntax>().FirstOrDefault();
        if (documentationComment == null)
        {
            return null;
        }
 
        // Find documented symbol simply as first parent that is a declaration
        // If the comment is not placed above any declaration, this takes the enclosing declaration
        var symbolNode = crefNode.Ancestors().OfType<MemberDeclarationSyntax>().FirstOrDefault();
        if (symbolNode == null)
        {
            return null;
        }
 
        // To filter out the cases when enclosing declaration is taken,
        // make sure that the comment of found symbol is the same as the comment of cref being analyzed
        var symbolComment = symbolNode.GetLeadingTrivia()
            .Select(trivia => trivia.GetStructure())
            .OfType<DocumentationCommentTriviaSyntax>()
            .FirstOrDefault();
        if (symbolComment != documentationComment)
        {
            return null;
        }
 
        return symbolNode;
    }
 
    private static bool IsNodeExternallyVisible(MemberDeclarationSyntax memberNode)
    {
        // In a way, the code replicates SymbolExtensions.IsExternallyVisible on syntax tree level
        // It traverses up to namespace declaration and checks if all levels are externally visible
        MemberDeclarationSyntax? node = memberNode;
        while (node != null && !IsNamespace(node))
        {
            bool isPublic = false;
            bool isProtected = false;
            bool isPrivate = false;
            bool hasModifiers = false;
            foreach (var modifier in node.Modifiers)
            {
                switch (modifier.Text)
                {
                    case "public":
                        isPublic = true;
                        break;
                    case "protected":
                        isProtected = true;
                        break;
                    case "private":
                        isPrivate = true;
                        break;
                }
 
                hasModifiers = true;
            }
 
            if (!hasModifiers // no modifiers => internal, not visible
                || isPrivate // private and private protected are both not visible
                || (!isPublic && !isProtected) // public and protected are only other externally visible options
               )
            {
                return false;
            }
 
            node = node.Parent as MemberDeclarationSyntax;
        }
 
        return true;
 
        static bool IsNamespace(MemberDeclarationSyntax n) =>
            n is BaseNamespaceDeclarationSyntax;
    }
 
    /// <inheritdoc/>
    public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => _supportedDiagnostics;
 
    /// <inheritdoc/>
    public override void Initialize(AnalysisContext context)
    {
        _ = context ?? throw new ArgumentNullException(nameof(context));
 
        context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
        context.EnableConcurrentExecution();
 
        context.RegisterSyntaxNodeAction(ValidateCref, SyntaxKind.XmlCrefAttribute);
    }
 
    private void ValidateCref(SyntaxNodeAnalysisContext context)
    {
        var crefNode = (XmlCrefAttributeSyntax)context.Node;
        if (crefNode.IsMissing)
        {
            return;
        }
 
        var symbolNode = FindDocumentedSymbol(crefNode);
        if (symbolNode == null)
        {
            return;
        }
 
        // Only externally visible symbols should be considered
        // Sometimes (for fields and events) the symbol is unknown
        // In such a case, use nodes instead of symbols
        var symbol = context.SemanticModel.GetDeclaredSymbol(symbolNode);
        var isExternallyVisible = symbol?.IsExternallyVisible() ?? IsNodeExternallyVisible(symbolNode);
        if (!isExternallyVisible)
        {
            return;
        }
 
        var referencedName = crefNode.Cref.ToString();
        if (string.IsNullOrWhiteSpace(referencedName))
        {
            return;
        }
 
        // Find what the cref attribute references; only successful binding is considered now, candidates aren't analyzed
        var referencedSymbol = context.SemanticModel.GetSymbolInfo(crefNode.Cref).Symbol;
        if (referencedSymbol == null)
        {
            return;
        }
 
        // Report referencing a not externally visible symbol
        if (!referencedSymbol.IsExternallyVisible())
        {
            var diagnostic = Diagnostic.Create(DiagDescriptors.InternalReferencedInPublicDoc, crefNode.Cref.GetLocation(), referencedName);
            context.ReportDiagnostic(diagnostic);
        }
    }
}