diff --git a/StyleCop.Analyzers/StyleCop.Analyzers.Test/DocumentationRules/SA1623UnitTests.cs b/StyleCop.Analyzers/StyleCop.Analyzers.Test/DocumentationRules/SA1623UnitTests.cs index 0e6503f58..a5f6cd901 100644 --- a/StyleCop.Analyzers/StyleCop.Analyzers.Test/DocumentationRules/SA1623UnitTests.cs +++ b/StyleCop.Analyzers/StyleCop.Analyzers.Test/DocumentationRules/SA1623UnitTests.cs @@ -345,5 +345,42 @@ public class TestClass await VerifyCSharpDiagnosticAsync(testCode, DiagnosticResult.EmptyDiagnosticResults, CancellationToken.None).ConfigureAwait(false); } + + [Theory] + [InlineData("")] + [InlineData(" XYZ")] + [WorkItem(3465, "https://github.com/DotNetAnalyzers/StyleCopAnalyzers/issues/3465")] + public async Task VerifyInheritdocInSummaryTagIsAllowedAsync(string summary) + { + var testCode = $@" +public class TestClass +{{ + /// + /// {summary} + /// + public int TestProperty {{ get; set; }} +}} +"; + + await VerifyCSharpDiagnosticAsync(testCode, DiagnosticResult.EmptyDiagnosticResults, CancellationToken.None).ConfigureAwait(false); + } + + [Fact] + [WorkItem(3465, "https://github.com/DotNetAnalyzers/StyleCopAnalyzers/issues/3465")] + public async Task VerifyInheritdocAfterTextStillReportsWarningAsync() + { + var testCode = @" +public class TestClass +{ + /// + /// XYZ + /// + public int {|#0:TestProperty|} { get; set; } +} +"; + + var expected = Diagnostic(PropertySummaryDocumentationAnalyzer.SA1623Descriptor).WithLocation(0).WithArguments("Gets or sets"); + await VerifyCSharpDiagnosticAsync(testCode, expected, CancellationToken.None).ConfigureAwait(false); + } } } diff --git a/StyleCop.Analyzers/StyleCop.Analyzers.Test/DocumentationRules/SA1624UnitTests.cs b/StyleCop.Analyzers/StyleCop.Analyzers.Test/DocumentationRules/SA1624UnitTests.cs index fb9ce294e..8cf5f174f 100644 --- a/StyleCop.Analyzers/StyleCop.Analyzers.Test/DocumentationRules/SA1624UnitTests.cs +++ b/StyleCop.Analyzers/StyleCop.Analyzers.Test/DocumentationRules/SA1624UnitTests.cs @@ -156,5 +156,42 @@ public class TestClass await VerifyCSharpDiagnosticAsync(testCode, DiagnosticResult.EmptyDiagnosticResults, CancellationToken.None).ConfigureAwait(false); } + + [Theory] + [InlineData("")] + [InlineData(" XYZ")] + [WorkItem(3465, "https://github.com/DotNetAnalyzers/StyleCopAnalyzers/issues/3465")] + public async Task VerifyInheritdocInSummaryTagIsAllowedAsync(string summary) + { + var testCode = $@" +public class TestClass +{{ + /// + /// {summary} + /// + public int TestProperty {{ get; private set; }} +}} +"; + + await VerifyCSharpDiagnosticAsync(testCode, DiagnosticResult.EmptyDiagnosticResults, CancellationToken.None).ConfigureAwait(false); + } + + [Fact] + [WorkItem(3465, "https://github.com/DotNetAnalyzers/StyleCopAnalyzers/issues/3465")] + public async Task VerifyInheritdocAfterTextStillReportsWarningAsync() + { + var testCode = @" +public class TestClass +{ + /// + /// XYZ + /// + public int {|#0:TestProperty|} { get; private set; } +} +"; + + var expected = Diagnostic(PropertySummaryDocumentationAnalyzer.SA1623Descriptor).WithLocation(0).WithArguments("Gets"); + await VerifyCSharpDiagnosticAsync(testCode, expected, CancellationToken.None).ConfigureAwait(false); + } } } diff --git a/StyleCop.Analyzers/StyleCop.Analyzers/DocumentationRules/PropertySummaryDocumentationAnalyzer.cs b/StyleCop.Analyzers/StyleCop.Analyzers/DocumentationRules/PropertySummaryDocumentationAnalyzer.cs index 0580c4aad..9170d7367 100644 --- a/StyleCop.Analyzers/StyleCop.Analyzers/DocumentationRules/PropertySummaryDocumentationAnalyzer.cs +++ b/StyleCop.Analyzers/StyleCop.Analyzers/DocumentationRules/PropertySummaryDocumentationAnalyzer.cs @@ -61,6 +61,18 @@ internal class PropertySummaryDocumentationAnalyzer : PropertyDocumentationBase /// protected override void HandleXmlElement(SyntaxNodeAnalysisContext context, StyleCopSettings settings, bool needsComment, XmlNodeSyntax syntax, XElement completeDocumentation, Location diagnosticLocation) { + if (!(syntax is XmlElementSyntax summaryElement)) + { + // This is reported by SA1604 or SA1606. + return; + } + + if (SummaryStartsWithInheritdoc(summaryElement)) + { + // Ignore nodes starting with an tag. + return; + } + var propertyDeclaration = (PropertyDeclarationSyntax)context.Node; var propertyType = context.SemanticModel.GetTypeInfo(propertyDeclaration.Type.StripRefFromType()); var culture = settings.DocumentationRules.DocumentationCultureInfo; @@ -70,7 +82,7 @@ protected override void HandleXmlElement(SyntaxNodeAnalysisContext context, Styl { AnalyzeSummaryElement( context, - syntax, + summaryElement, diagnosticLocation, propertyDeclaration, resourceManager.GetString(nameof(DocumentationResources.StartingTextGetsWhether), culture), @@ -82,7 +94,7 @@ protected override void HandleXmlElement(SyntaxNodeAnalysisContext context, Styl { AnalyzeSummaryElement( context, - syntax, + summaryElement, diagnosticLocation, propertyDeclaration, resourceManager.GetString(nameof(DocumentationResources.StartingTextGets), culture), @@ -92,7 +104,7 @@ protected override void HandleXmlElement(SyntaxNodeAnalysisContext context, Styl } } - private static void AnalyzeSummaryElement(SyntaxNodeAnalysisContext context, XmlNodeSyntax syntax, Location diagnosticLocation, PropertyDeclarationSyntax propertyDeclaration, string startingTextGets, string startingTextSets, string startingTextGetsOrSets, string startingTextReturns) + private static void AnalyzeSummaryElement(SyntaxNodeAnalysisContext context, XmlElementSyntax summaryElement, Location diagnosticLocation, PropertyDeclarationSyntax propertyDeclaration, string startingTextGets, string startingTextSets, string startingTextGetsOrSets, string startingTextReturns) { var diagnosticProperties = ImmutableDictionary.CreateBuilder(); ArrowExpressionClauseSyntax expressionBody = propertyDeclaration.ExpressionBody; @@ -116,12 +128,6 @@ private static void AnalyzeSummaryElement(SyntaxNodeAnalysisContext context, Xml } } - if (!(syntax is XmlElementSyntax summaryElement)) - { - // This is reported by SA1604 or SA1606. - return; - } - // Add a no code fix tag when the summary element is empty. // This will only impact SA1623, because SA1624 cannot trigger with an empty summary. if (summaryElement.Content.Count == 0) @@ -284,6 +290,73 @@ private static void AnalyzeSummaryElement(SyntaxNodeAnalysisContext context, Xml } } + private static bool SummaryStartsWithInheritdoc(XmlElementSyntax summaryElement) + { + foreach (var child in summaryElement.Content) + { + var firstContent = GetFirstMeaningfulChild(child); + if (firstContent is null) + { + continue; + } + + return string.Equals(firstContent.GetName()?.ToString(), XmlCommentHelper.InheritdocXmlTag, StringComparison.Ordinal); + } + + return false; + } + + private static XmlNodeSyntax GetFirstMeaningfulChild(XmlNodeSyntax node) + { + switch (node) + { + case XmlTextSyntax textSyntax: + foreach (var token in textSyntax.TextTokens) + { + if (!string.IsNullOrWhiteSpace(token.ValueText)) + { + return textSyntax; + } + } + + return null; + + case XmlEmptyElementSyntax emptyElement: + return emptyElement; + + case XmlElementSyntax elementSyntax: + if (string.Equals(elementSyntax.StartTag?.Name?.ToString(), XmlCommentHelper.InheritdocXmlTag, StringComparison.Ordinal)) + { + return elementSyntax; + } + + foreach (var child in elementSyntax.Content) + { + var nested = GetFirstMeaningfulChild(child); + if (nested != null) + { + return nested; + } + } + + return null; + + case XmlCDataSectionSyntax cdataSyntax: + foreach (var token in cdataSyntax.TextTokens) + { + if (!string.IsNullOrWhiteSpace(token.ValueText)) + { + return cdataSyntax; + } + } + + return null; + + default: + return null; + } + } + private static void ReportSA1623(SyntaxNodeAnalysisContext context, Location diagnosticLocation, ImmutableDictionary.Builder diagnosticProperties, string text, string expectedStartingText, string unexpectedStartingText1, string unexpectedStartingText2 = null, string unexpectedStartingText3 = null) { diagnosticProperties.Add(ExpectedTextKey, expectedStartingText);