diff --git a/StyleCop.Analyzers/StyleCop.Analyzers.Test/DocumentationRules/SA1629UnitTests.cs b/StyleCop.Analyzers/StyleCop.Analyzers.Test/DocumentationRules/SA1629UnitTests.cs index 56397c96f..f86afd24e 100644 --- a/StyleCop.Analyzers/StyleCop.Analyzers.Test/DocumentationRules/SA1629UnitTests.cs +++ b/StyleCop.Analyzers/StyleCop.Analyzers.Test/DocumentationRules/SA1629UnitTests.cs @@ -546,6 +546,23 @@ public interface ITest await VerifyCSharpFixAsync(testCode, expected, fixedTestCode, default).ConfigureAwait(false); } + [Theory] + [InlineData("a", true)] + [InlineData("see", true)] + [InlineData("seealso", false)] + public async Task TestFullSentenceLinkAsync(string tag, bool insideSummary) + { + var surrounding = insideSummary ? (Start: "", End: "") : (Start: string.Empty, End: string.Empty); + + var testCode = $@" +/// {surrounding.Start}<{tag} href=""someurl"">Periods aren't required to glow white at the end of a full-sentence link.{surrounding.End} +public interface ITest +{{ +}} +"; + await VerifyCSharpDiagnosticAsync(testCode, DiagnosticResult.EmptyDiagnosticResults, default).ConfigureAwait(false); + } + [Theory] [InlineData(",")] [InlineData(";")] diff --git a/StyleCop.Analyzers/StyleCop.Analyzers/DocumentationRules/SA1629DocumentationTextMustEndWithAPeriod.cs b/StyleCop.Analyzers/StyleCop.Analyzers/DocumentationRules/SA1629DocumentationTextMustEndWithAPeriod.cs index 9fd770d77..6f16360cf 100644 --- a/StyleCop.Analyzers/StyleCop.Analyzers/DocumentationRules/SA1629DocumentationTextMustEndWithAPeriod.cs +++ b/StyleCop.Analyzers/StyleCop.Analyzers/DocumentationRules/SA1629DocumentationTextMustEndWithAPeriod.cs @@ -127,10 +127,7 @@ private static void HandleSectionOrBlockXmlElement(SyntaxNodeAnalysisContext con if (!string.IsNullOrEmpty(textWithoutTrailingWhitespace)) { - if (!textWithoutTrailingWhitespace.EndsWith(".", StringComparison.Ordinal) - && !textWithoutTrailingWhitespace.EndsWith(".)", StringComparison.Ordinal) - && (startingWithFinalParagraph || !textWithoutTrailingWhitespace.EndsWith(":", StringComparison.Ordinal)) - && !textWithoutTrailingWhitespace.EndsWith("-or-", StringComparison.Ordinal)) + if (IsMissingRequiredPeriod(textWithoutTrailingWhitespace, startingWithFinalParagraph)) { int spanStart = textToken.SpanStart + textWithoutTrailingWhitespace.Length; ImmutableDictionary properties = null; @@ -164,10 +161,15 @@ void SetReplaceChar() } else if (xmlElement.Content[i].IsInlineElement() && !currentParagraphDone) { - // Treat empty XML elements as a "word not ending with a period" - var location = Location.Create(xmlElement.SyntaxTree, new TextSpan(xmlElement.Content[i].Span.End, 1)); - context.ReportDiagnostic(Diagnostic.Create(Descriptor, location)); - currentParagraphDone = true; + var lastTextElement = XmlCommentHelper.TryGetLastTextElementWithContent(xmlElement.Content[i]); + + if (lastTextElement is null // Treat empty XML elements as a "word not ending with a period" + || IsMissingRequiredPeriod(lastTextElement.TextTokens.Last().Text.TrimEnd(' ', '\r', '\n'), startingWithFinalParagraph)) + { + var location = Location.Create(xmlElement.SyntaxTree, new TextSpan(xmlElement.Content[i].Span.End, 1)); + context.ReportDiagnostic(Diagnostic.Create(Descriptor, location)); + currentParagraphDone = true; + } } else if (xmlElement.Content[i] is XmlElementSyntax childXmlElement) { @@ -200,5 +202,13 @@ void SetReplaceChar() } } } + + private static bool IsMissingRequiredPeriod(string textWithoutTrailingWhitespace, bool startingWithFinalParagraph) + { + return !textWithoutTrailingWhitespace.EndsWith(".", StringComparison.Ordinal) + && !textWithoutTrailingWhitespace.EndsWith(".)", StringComparison.Ordinal) + && (startingWithFinalParagraph || !textWithoutTrailingWhitespace.EndsWith(":", StringComparison.Ordinal)) + && !textWithoutTrailingWhitespace.EndsWith("-or-", StringComparison.Ordinal); + } } } diff --git a/StyleCop.Analyzers/StyleCop.Analyzers/Helpers/XmlCommentHelper.cs b/StyleCop.Analyzers/StyleCop.Analyzers/Helpers/XmlCommentHelper.cs index 114f18a2d..24d2728d1 100644 --- a/StyleCop.Analyzers/StyleCop.Analyzers/Helpers/XmlCommentHelper.cs +++ b/StyleCop.Analyzers/StyleCop.Analyzers/Helpers/XmlCommentHelper.cs @@ -192,6 +192,37 @@ internal static XmlTextSyntax TryGetFirstTextElementWithContent(XmlNodeSyntax no return null; } + /// + /// Returns the last which is not simply empty or whitespace. + /// + /// The XML content to search. + /// The last which is not simply empty or whitespace, or + /// if no such element exists. + internal static XmlTextSyntax TryGetLastTextElementWithContent(XmlNodeSyntax node) + { + if (node is XmlEmptyElementSyntax) + { + return null; + } + else if (node is XmlTextSyntax xmlText) + { + return !IsConsideredEmpty(node) ? xmlText : null; + } + else if (node is XmlElementSyntax xmlElement) + { + for (var i = xmlElement.Content.Count - 1; i >= 0; i--) + { + var nestedContent = TryGetFirstTextElementWithContent(xmlElement.Content[i]); + if (nestedContent != null) + { + return nestedContent; + } + } + } + + return null; + } + /// /// Checks if a contains a and returns /// if it is considered empty.