Skip to content

Commit 258d447

Browse files
committed
Update SA1629 to analyze the content of paragraphs
1 parent c5d1ef3 commit 258d447

3 files changed

Lines changed: 259 additions & 8 deletions

File tree

StyleCop.Analyzers/StyleCop.Analyzers.Test/DocumentationRules/SA1629UnitTests.cs

Lines changed: 195 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -449,6 +449,201 @@ public interface ITest
449449
await this.VerifyCSharpFixAsync(testCode, fixedTestCode).ConfigureAwait(false);
450450
}
451451

452+
[Fact]
453+
public async Task TestMultipleParagraphBlocksAsync()
454+
{
455+
var testCode = @"
456+
/// <summary>
457+
/// <para>Paragraph 1</para>
458+
/// <para>Paragraph 2</para>
459+
/// <para>Paragraph 3</para>
460+
/// </summary>
461+
public interface ITest
462+
{
463+
}
464+
";
465+
466+
var fixedTestCode = @"
467+
/// <summary>
468+
/// <para>Paragraph 1.</para>
469+
/// <para>Paragraph 2.</para>
470+
/// <para>Paragraph 3.</para>
471+
/// </summary>
472+
public interface ITest
473+
{
474+
}
475+
";
476+
477+
DiagnosticResult[] expected =
478+
{
479+
this.CSharpDiagnostic().WithLocation(3, 22),
480+
this.CSharpDiagnostic().WithLocation(4, 22),
481+
this.CSharpDiagnostic().WithLocation(5, 22),
482+
};
483+
484+
await this.VerifyCSharpDiagnosticAsync(testCode, expected, CancellationToken.None).ConfigureAwait(false);
485+
await this.VerifyCSharpDiagnosticAsync(fixedTestCode, EmptyDiagnosticResults, CancellationToken.None).ConfigureAwait(false);
486+
await this.VerifyCSharpFixAsync(testCode, fixedTestCode).ConfigureAwait(false);
487+
}
488+
489+
[Fact]
490+
public async Task TestMultipleParagraphInlinesAsync()
491+
{
492+
var testCode = @"
493+
/// <summary>
494+
/// Paragraph 1
495+
/// <para/>
496+
/// Paragraph 2
497+
/// <para/>
498+
/// Paragraph 3
499+
/// </summary>
500+
public interface ITest
501+
{
502+
}
503+
";
504+
505+
var fixedTestCode = @"
506+
/// <summary>
507+
/// Paragraph 1.
508+
/// <para/>
509+
/// Paragraph 2.
510+
/// <para/>
511+
/// Paragraph 3.
512+
/// </summary>
513+
public interface ITest
514+
{
515+
}
516+
";
517+
518+
DiagnosticResult[] expected =
519+
{
520+
this.CSharpDiagnostic().WithLocation(3, 16),
521+
this.CSharpDiagnostic().WithLocation(5, 16),
522+
this.CSharpDiagnostic().WithLocation(7, 16),
523+
};
524+
525+
await this.VerifyCSharpDiagnosticAsync(testCode, expected, CancellationToken.None).ConfigureAwait(false);
526+
await this.VerifyCSharpDiagnosticAsync(fixedTestCode, EmptyDiagnosticResults, CancellationToken.None).ConfigureAwait(false);
527+
await this.VerifyCSharpFixAsync(testCode, fixedTestCode).ConfigureAwait(false);
528+
}
529+
530+
[Fact]
531+
public async Task TestMultipleParagraphBlocksAfterFirstAsync()
532+
{
533+
var testCode = @"
534+
/// <summary>
535+
/// Paragraph 1
536+
/// <para>Paragraph 2</para>
537+
/// <para>Paragraph 3</para>
538+
/// </summary>
539+
public interface ITest
540+
{
541+
}
542+
";
543+
544+
var fixedTestCode = @"
545+
/// <summary>
546+
/// Paragraph 1.
547+
/// <para>Paragraph 2.</para>
548+
/// <para>Paragraph 3.</para>
549+
/// </summary>
550+
public interface ITest
551+
{
552+
}
553+
";
554+
555+
DiagnosticResult[] expected =
556+
{
557+
this.CSharpDiagnostic().WithLocation(3, 16),
558+
this.CSharpDiagnostic().WithLocation(4, 22),
559+
this.CSharpDiagnostic().WithLocation(5, 22),
560+
};
561+
562+
await this.VerifyCSharpDiagnosticAsync(testCode, expected, CancellationToken.None).ConfigureAwait(false);
563+
await this.VerifyCSharpDiagnosticAsync(fixedTestCode, EmptyDiagnosticResults, CancellationToken.None).ConfigureAwait(false);
564+
await this.VerifyCSharpFixAsync(testCode, fixedTestCode).ConfigureAwait(false);
565+
}
566+
567+
[Fact]
568+
public async Task TestMultipleParagraphBlocksAfterFirstInNoteAsync()
569+
{
570+
var testCode = @"
571+
/// <summary>
572+
/// Paragraph 0
573+
/// <note>
574+
/// Paragraph 1
575+
/// <para>Paragraph 2</para>
576+
/// <para>Paragraph 3</para>
577+
/// </note>
578+
/// </summary>
579+
public interface ITest
580+
{
581+
}
582+
";
583+
584+
var fixedTestCode = @"
585+
/// <summary>
586+
/// Paragraph 0.
587+
/// <note>
588+
/// Paragraph 1.
589+
/// <para>Paragraph 2.</para>
590+
/// <para>Paragraph 3.</para>
591+
/// </note>
592+
/// </summary>
593+
public interface ITest
594+
{
595+
}
596+
";
597+
598+
DiagnosticResult[] expected =
599+
{
600+
this.CSharpDiagnostic().WithLocation(3, 16),
601+
this.CSharpDiagnostic().WithLocation(5, 16),
602+
this.CSharpDiagnostic().WithLocation(6, 22),
603+
this.CSharpDiagnostic().WithLocation(7, 22),
604+
};
605+
606+
await this.VerifyCSharpDiagnosticAsync(testCode, expected, CancellationToken.None).ConfigureAwait(false);
607+
await this.VerifyCSharpDiagnosticAsync(fixedTestCode, EmptyDiagnosticResults, CancellationToken.None).ConfigureAwait(false);
608+
await this.VerifyCSharpFixAsync(testCode, fixedTestCode).ConfigureAwait(false);
609+
}
610+
611+
[Fact]
612+
public async Task TestCodeBetweenParagraphBlocksAsync()
613+
{
614+
var testCode = @"
615+
/// <summary>
616+
/// Paragraph 1
617+
/// <code>Code block</code>
618+
/// <para>Paragraph 2</para>
619+
/// </summary>
620+
public interface ITest
621+
{
622+
}
623+
";
624+
625+
var fixedTestCode = @"
626+
/// <summary>
627+
/// Paragraph 1.
628+
/// <code>Code block</code>
629+
/// <para>Paragraph 2.</para>
630+
/// </summary>
631+
public interface ITest
632+
{
633+
}
634+
";
635+
636+
DiagnosticResult[] expected =
637+
{
638+
this.CSharpDiagnostic().WithLocation(3, 16),
639+
this.CSharpDiagnostic().WithLocation(5, 22),
640+
};
641+
642+
await this.VerifyCSharpDiagnosticAsync(testCode, expected, CancellationToken.None).ConfigureAwait(false);
643+
await this.VerifyCSharpDiagnosticAsync(fixedTestCode, EmptyDiagnosticResults, CancellationToken.None).ConfigureAwait(false);
644+
await this.VerifyCSharpFixAsync(testCode, fixedTestCode).ConfigureAwait(false);
645+
}
646+
452647
protected override Project ApplyCompilationOptions(Project project)
453648
{
454649
var resolver = new TestXmlReferenceResolver();

StyleCop.Analyzers/StyleCop.Analyzers/DocumentationRules/SA1629DocumentationTextMustEndWithAPeriod.cs

Lines changed: 35 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ protected override void HandleXmlElement(SyntaxNodeAnalysisContext context, bool
7878
{
7979
foreach (var xmlElement in syntaxList.OfType<XmlElementSyntax>())
8080
{
81-
HandleSectionXmlElement(context, xmlElement);
81+
HandleSectionOrBlockXmlElement(context, xmlElement);
8282
}
8383
}
8484

@@ -101,19 +101,19 @@ protected override void HandleCompleteDocumentation(SyntaxNodeAnalysisContext co
101101
}
102102
}
103103

104-
private static void HandleSectionXmlElement(SyntaxNodeAnalysisContext context, XmlElementSyntax xmlElement)
104+
private static void HandleSectionOrBlockXmlElement(SyntaxNodeAnalysisContext context, XmlElementSyntax xmlElement)
105105
{
106106
if (xmlElement.StartTag?.Name?.LocalName.ValueText == XmlCommentHelper.SeeAlsoXmlTag)
107107
{
108108
return;
109109
}
110110

111-
var elementDone = false;
112-
for (var i = xmlElement.Content.Count - 1; !elementDone && (i >= 0); i--)
111+
var currentParagraphDone = false;
112+
for (var i = xmlElement.Content.Count - 1; i >= 0; i--)
113113
{
114114
if (xmlElement.Content[i] is XmlTextSyntax contentNode)
115115
{
116-
for (var j = contentNode.TextTokens.Count - 1; !elementDone && (j >= 0); j--)
116+
for (var j = contentNode.TextTokens.Count - 1; !currentParagraphDone && (j >= 0); j--)
117117
{
118118
var textToken = contentNode.TextTokens[j];
119119
var textWithoutTrailingWhitespace = textToken.Text.TrimEnd(' ', '\r', '\n');
@@ -126,16 +126,43 @@ private static void HandleSectionXmlElement(SyntaxNodeAnalysisContext context, X
126126
context.ReportDiagnostic(Diagnostic.Create(Descriptor, location));
127127
}
128128

129-
elementDone = true;
129+
currentParagraphDone = true;
130130
}
131131
}
132132
}
133-
else if (xmlElement.Content[i].IsInlineElement())
133+
else if (xmlElement.Content[i].IsInlineElement() && !currentParagraphDone)
134134
{
135135
// Treat empty XML elements as a "word not ending with a period"
136136
var location = Location.Create(xmlElement.SyntaxTree, new TextSpan(xmlElement.Content[i].Span.End, 1));
137137
context.ReportDiagnostic(Diagnostic.Create(Descriptor, location));
138-
elementDone = true;
138+
currentParagraphDone = true;
139+
}
140+
else if (xmlElement.Content[i] is XmlElementSyntax childXmlElement)
141+
{
142+
switch (childXmlElement.StartTag?.Name?.LocalName.ValueText)
143+
{
144+
case XmlCommentHelper.NoteXmlTag:
145+
case XmlCommentHelper.ParaXmlTag:
146+
// Recursively handle <note> and <para> elements
147+
HandleSectionOrBlockXmlElement(context, childXmlElement);
148+
break;
149+
150+
default:
151+
break;
152+
}
153+
154+
if (childXmlElement.IsBlockElement())
155+
{
156+
currentParagraphDone = false;
157+
}
158+
}
159+
else if (xmlElement.Content[i] is XmlEmptyElementSyntax emptyElement)
160+
{
161+
// Treat the empty element <para/> as a paragraph separator
162+
if (emptyElement.Name?.LocalName.ValueText == XmlCommentHelper.ParaXmlTag)
163+
{
164+
currentParagraphDone = false;
165+
}
139166
}
140167
}
141168
}

StyleCop.Analyzers/StyleCop.Analyzers/Helpers/XmlCommentHelper.cs

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,10 @@ internal static class XmlCommentHelper
2323
internal const string ValueXmlTag = "value";
2424
internal const string CXmlTag = "c";
2525
internal const string SeeXmlTag = "see";
26+
internal const string CodeXmlTag = "code";
27+
internal const string ListXmlTag = "list";
28+
internal const string NoteXmlTag = "note";
29+
internal const string ParaXmlTag = "para";
2630
internal const string SeeAlsoXmlTag = "seealso";
2731
internal const string ParamXmlTag = "param";
2832
internal const string ParamRefXmlTag = "paramref";
@@ -284,6 +288,16 @@ internal static bool IsInlineElement(this XmlNodeSyntax nodeSyntax)
284288
return false;
285289
}
286290

291+
internal static bool IsBlockElement(this XmlNodeSyntax nodeSyntax)
292+
{
293+
if (nodeSyntax is XmlElementSyntax elementSyntax)
294+
{
295+
return IsBlockElement(elementSyntax.StartTag?.Name?.LocalName.ValueText);
296+
}
297+
298+
return false;
299+
}
300+
287301
private static bool IsInlineElement(string localName)
288302
{
289303
switch (localName)
@@ -298,5 +312,20 @@ private static bool IsInlineElement(string localName)
298312
return false;
299313
}
300314
}
315+
316+
private static bool IsBlockElement(string localName)
317+
{
318+
switch (localName)
319+
{
320+
case CodeXmlTag:
321+
case ListXmlTag:
322+
case NoteXmlTag:
323+
case ParaXmlTag:
324+
return true;
325+
326+
default:
327+
return false;
328+
}
329+
}
301330
}
302331
}

0 commit comments

Comments
 (0)