diff --git a/StyleCop.Analyzers/StyleCop.Analyzers/DocumentationRules/SA1629DocumentationTextMustEndWithAPeriod.cs b/StyleCop.Analyzers/StyleCop.Analyzers/DocumentationRules/SA1629DocumentationTextMustEndWithAPeriod.cs index 7ba6658e0..cd5ee62b3 100644 --- a/StyleCop.Analyzers/StyleCop.Analyzers/DocumentationRules/SA1629DocumentationTextMustEndWithAPeriod.cs +++ b/StyleCop.Analyzers/StyleCop.Analyzers/DocumentationRules/SA1629DocumentationTextMustEndWithAPeriod.cs @@ -6,6 +6,7 @@ namespace StyleCop.Analyzers.DocumentationRules using System; using System.Collections.Generic; using System.Collections.Immutable; +using System.IO; using System.Linq; using System.Xml.Linq; using Microsoft.CodeAnalysis; @@ -125,10 +126,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; @@ -162,10 +160,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) { @@ -198,5 +201,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 9d2aefb9f..fd9c815ee 100644 --- a/StyleCop.Analyzers/StyleCop.Analyzers/Helpers/XmlCommentHelper.cs +++ b/StyleCop.Analyzers/StyleCop.Analyzers/Helpers/XmlCommentHelper.cs @@ -191,6 +191,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.