diff --git a/src/Analyzers/Microsoft.Analyzers.Local/DiagDescriptors.cs b/src/Analyzers/Microsoft.Analyzers.Local/DiagDescriptors.cs
index b99bf81abaf..46af17c1e0b 100644
--- a/src/Analyzers/Microsoft.Analyzers.Local/DiagDescriptors.cs
+++ b/src/Analyzers/Microsoft.Analyzers.Local/DiagDescriptors.cs
@@ -104,6 +104,14 @@ internal static class DiagDescriptors
description: Resources.PublishedSymbolsCantChangeDescription,
defaultSeverity: DiagnosticSeverity.Warning);
+ public static DiagnosticDescriptor InternalReferencedInPublicDoc { get; } = Make(
+ id: "LA0008",
+ messageFormat: Resources.InternalReferencedInPublicDocMessage,
+ title: Resources.InternalReferencedInPublicDocTitle,
+ category: Correctness,
+ description: Resources.InternalReferencedInPublicDocDescription,
+ defaultSeverity: DiagnosticSeverity.Warning);
+
private static DiagnosticDescriptor Make(string id, string title, string description, string messageFormat, string category, DiagnosticSeverity defaultSeverity)
=> new(id, title, messageFormat, category, defaultSeverity, true, description);
}
diff --git a/src/Analyzers/Microsoft.Analyzers.Local/InternalReferencedInPublicDocAnalyzer.cs b/src/Analyzers/Microsoft.Analyzers.Local/InternalReferencedInPublicDocAnalyzer.cs
new file mode 100644
index 00000000000..e83ae1f28f8
--- /dev/null
+++ b/src/Analyzers/Microsoft.Analyzers.Local/InternalReferencedInPublicDocAnalyzer.cs
@@ -0,0 +1,158 @@
+// 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;
+
+///
+/// C# analyzer that warns about referencing internal symbols in public xml documentation.
+///
+[DiagnosticAnalyzer(LanguageNames.CSharp)]
+public class InternalReferencedInPublicDocAnalyzer : DiagnosticAnalyzer
+{
+ private static readonly ImmutableArray _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().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().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()
+ .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;
+ }
+
+ ///
+ public override ImmutableArray SupportedDiagnostics => _supportedDiagnostics;
+
+ ///
+ 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);
+ }
+ }
+}
diff --git a/src/Analyzers/Microsoft.Analyzers.Local/Resources.Designer.cs b/src/Analyzers/Microsoft.Analyzers.Local/Resources.Designer.cs
index d19c3e90704..b9868d9fc50 100644
--- a/src/Analyzers/Microsoft.Analyzers.Local/Resources.Designer.cs
+++ b/src/Analyzers/Microsoft.Analyzers.Local/Resources.Designer.cs
@@ -114,6 +114,33 @@ internal static string ExperimentalSymbolsCantBeMarkedObsoleteTitle {
}
}
+ ///
+ /// Looks up a localized string similar to Externally visible XML documentation references an internal member, this will perplex the readers..
+ ///
+ internal static string InternalReferencedInPublicDocDescription {
+ get {
+ return ResourceManager.GetString("InternalReferencedInPublicDocDescription", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Remove the reference or make '{0}' externally visible; also consider making the referencing member private or internal.
+ ///
+ internal static string InternalReferencedInPublicDocMessage {
+ get {
+ return ResourceManager.GetString("InternalReferencedInPublicDocMessage", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Externally visible documentation references internals.
+ ///
+ internal static string InternalReferencedInPublicDocTitle {
+ get {
+ return ResourceManager.GetString("InternalReferencedInPublicDocTitle", resourceCulture);
+ }
+ }
+
///
/// Looks up a localized string similar to Symbols being added to the public API of an assembly must be marked as experimental until they have been appoved.
///
diff --git a/src/Analyzers/Microsoft.Analyzers.Local/Resources.resx b/src/Analyzers/Microsoft.Analyzers.Local/Resources.resx
index 704117a6d59..d438082456b 100644
--- a/src/Analyzers/Microsoft.Analyzers.Local/Resources.resx
+++ b/src/Analyzers/Microsoft.Analyzers.Local/Resources.resx
@@ -204,4 +204,13 @@
Symbols that have been removed from the public API of an assembly must be marked as obsolete
-
\ No newline at end of file
+
+ Externally visible XML documentation references an internal member, this will perplex the readers.
+
+
+ Remove the reference or make '{0}' externally visible; also consider making the referencing member private or internal
+
+
+ Externally visible documentation references internals
+
+
diff --git a/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Latency/RequestCheckpointConstants.cs b/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Latency/RequestCheckpointConstants.cs
index 02fed943a5e..e03d87bf4c9 100644
--- a/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Latency/RequestCheckpointConstants.cs
+++ b/src/Libraries/Microsoft.AspNetCore.Diagnostics.Middleware/Latency/RequestCheckpointConstants.cs
@@ -19,7 +19,7 @@ public static class RequestCheckpointConstants
public const string ElapsedTillFinished = "eltltf";
///
- /// The time elapsed before hitting the middleware.
+ /// The time elapsed before hitting the pipeline exit middleware.
///
public const string ElapsedTillPipelineExitMiddleware = "eltexm";
diff --git a/src/Libraries/Microsoft.Extensions.Options.Contextual/NullConfigureContextualOptions.cs b/src/Libraries/Microsoft.Extensions.Options.Contextual/NullConfigureContextualOptions.cs
index 19c35c1611a..caaa969ee55 100644
--- a/src/Libraries/Microsoft.Extensions.Options.Contextual/NullConfigureContextualOptions.cs
+++ b/src/Libraries/Microsoft.Extensions.Options.Contextual/NullConfigureContextualOptions.cs
@@ -9,7 +9,7 @@ namespace Microsoft.Extensions.Options.Contextual;
public static class NullConfigureContextualOptions
{
///
- /// Gets a singleton instance of .
+ /// Gets a singleton instance of an empty configuration context.
///
/// The options type to configure.
/// A do-nothing instance of .
diff --git a/test/Analyzers/Microsoft.Analyzers.Local.Tests/InternalReferencedInPublicDocAnalyzerTests.cs b/test/Analyzers/Microsoft.Analyzers.Local.Tests/InternalReferencedInPublicDocAnalyzerTests.cs
new file mode 100644
index 00000000000..4f6962bbc0b
--- /dev/null
+++ b/test/Analyzers/Microsoft.Analyzers.Local.Tests/InternalReferencedInPublicDocAnalyzerTests.cs
@@ -0,0 +1,491 @@
+// 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.Generic;
+using System.Diagnostics;
+using System.Linq;
+using System.Reflection;
+using System.Threading.Tasks;
+using Microsoft.CodeAnalysis;
+using Microsoft.Extensions.LocalAnalyzers.Resource.Test;
+using Xunit;
+
+namespace Microsoft.Extensions.LocalAnalyzers.Test;
+
+public class InternalReferencedInPublicDocAnalyzerTests
+{
+ private static readonly string[] _members =
+ {
+ "void Method() {}", "int Property {get; set;}", "event System.EventHandler Event;", "int _field;", "string this[int i] { get => string.Empty; set {} }",
+ };
+ private static readonly string[] _membersReferenced = { "void Referenced() {}", "int Referenced {get; set;}", "event System.EventHandler Referenced;", "int Referenced;", };
+ private static readonly string[] _typesReferenced = { "class Referenced {}", "struct Referenced {}", "interface Referenced {}", "delegate void Referenced(int value);", "enum Referenced {}", };
+ private static readonly string[] _compositeTypeNames = { "class", "struct", "interface" };
+
+ public static IEnumerable References => new Assembly[]
+ {
+ // add references here
+ };
+
+ public static IEnumerable