| 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); } } }