Skip to content

Commit d43c913

Browse files
committed
Implement SA1625
1 parent d6c83c4 commit d43c913

4 files changed

Lines changed: 292 additions & 2 deletions

File tree

Lines changed: 225 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,225 @@
1+
// Copyright (c) Tunnel Vision Laboratories, LLC. All Rights Reserved.
2+
// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information.
3+
4+
namespace StyleCop.Analyzers.Test.DocumentationRules
5+
{
6+
using System.Collections.Generic;
7+
using System.Threading;
8+
using System.Threading.Tasks;
9+
using Microsoft.CodeAnalysis.CodeFixes;
10+
using Microsoft.CodeAnalysis.Diagnostics;
11+
using StyleCop.Analyzers.DocumentationRules;
12+
using TestHelper;
13+
using Xunit;
14+
15+
/// <summary>
16+
/// This class contains the unit tests for SA1625.
17+
/// </summary>
18+
public class SA1625UnitTests : DiagnosticVerifier
19+
{
20+
public static IEnumerable<object[]> Members
21+
{
22+
get
23+
{
24+
yield return new[] { "public void Test() { }" };
25+
yield return new[] { "public string Test { get; set; }" };
26+
yield return new[] { "public string Test;" };
27+
yield return new[] { "public class Test { }" };
28+
yield return new[] { "public struct Test { }" };
29+
yield return new[] { "public enum Test { }" };
30+
yield return new[] { "public delegate void Test();" };
31+
}
32+
}
33+
34+
[Theory]
35+
[MemberData(nameof(Members))]
36+
public async Task VerifyThatCorrectDocumentationDoesNotReportADiagnosticAsync(string member)
37+
{
38+
var testCode = $@"
39+
public class TestClass
40+
{{
41+
/// <summary>
42+
/// Some documentation.
43+
/// </summary>
44+
/// <remark>Some remark.</remark>
45+
{member}
46+
}}
47+
";
48+
await this.VerifyCSharpDiagnosticAsync(testCode, EmptyDiagnosticResults, CancellationToken.None).ConfigureAwait(false);
49+
}
50+
51+
[Theory]
52+
[MemberData(nameof(Members))]
53+
public async Task VerifyThatDublicatedDocumentationDoesReportADiagnosticAsync(string member)
54+
{
55+
var testCode = $@"
56+
public class TestClass
57+
{{
58+
/// <summary>Some documentation.</summary>
59+
/// <remark>Some documentation.</remark>
60+
{member}
61+
}}
62+
";
63+
var expected = this.CSharpDiagnostic().WithLocation(5, 9);
64+
65+
await this.VerifyCSharpDiagnosticAsync(testCode, expected, CancellationToken.None).ConfigureAwait(false);
66+
}
67+
68+
[Theory]
69+
[MemberData(nameof(Members))]
70+
public async Task VerifyThatAnalyzerIgnoresLeadingAndTrailingWhitespaceAsync(string member)
71+
{
72+
var testCode = $@"
73+
public class TestClass
74+
{{
75+
/// <summary>
76+
/// Some documentation.
77+
///
78+
///
79+
/// </summary>
80+
/// <remark> Some documentation. </remark>
81+
{member}
82+
}}
83+
";
84+
var expected = this.CSharpDiagnostic().WithLocation(9, 9);
85+
await this.VerifyCSharpDiagnosticAsync(testCode, expected, CancellationToken.None).ConfigureAwait(false);
86+
}
87+
88+
[Theory]
89+
[MemberData(nameof(Members))]
90+
public async Task VerifyThatAnalysisIgnoresUnusedParametersAsync(string member)
91+
{
92+
var testCode = $@"
93+
public class TestClass
94+
{{
95+
/// <summary>The parameter is not used.</summary>
96+
/// <remark>Documentation</remark>
97+
/// <remark>The parameter is not used.</remark>
98+
/// <remark>Documentation</remark>
99+
{member}
100+
}}
101+
";
102+
var expected = this.CSharpDiagnostic().WithLocation(7, 9);
103+
await this.VerifyCSharpDiagnosticAsync(testCode, expected, CancellationToken.None).ConfigureAwait(false);
104+
}
105+
106+
[Theory]
107+
[MemberData(nameof(Members))]
108+
public async Task VerifyThatAnalysisIgnoresEmptyElementsAsync(string member)
109+
{
110+
var testCode = $@"
111+
public class TestClass
112+
{{
113+
/// <summary></summary>
114+
/// <remark>Documentation</remark>
115+
/// <remark></remark>
116+
/// <remark>Documentation</remark>
117+
{member}
118+
}}
119+
";
120+
var expected = this.CSharpDiagnostic().WithLocation(7, 9);
121+
await this.VerifyCSharpDiagnosticAsync(testCode, expected, CancellationToken.None).ConfigureAwait(false);
122+
}
123+
124+
[Theory]
125+
[MemberData(nameof(Members))]
126+
public async Task VerifyThatCorrectDocumentationDoesNotReportADiagnosticMultiLineAsync(string member)
127+
{
128+
var testCode = $@"
129+
public class TestClass
130+
{{
131+
/** <summary>
132+
* Some documentation.
133+
* </summary>
134+
* <remark>Some remark.</remark>
135+
**/
136+
{member}
137+
}}
138+
";
139+
await this.VerifyCSharpDiagnosticAsync(testCode, EmptyDiagnosticResults, CancellationToken.None).ConfigureAwait(false);
140+
}
141+
142+
[Theory]
143+
[MemberData(nameof(Members))]
144+
public async Task VerifyThatDublicatedDocumentationDoesReportADiagnosticMultiLineAsync(string member)
145+
{
146+
var testCode = $@"
147+
public class TestClass
148+
{{
149+
/** <summary>Some documentation.</summary>
150+
* <remark>Some documentation.</remark>
151+
**/
152+
{member}
153+
}}
154+
";
155+
var expected = this.CSharpDiagnostic().WithLocation(5, 7);
156+
157+
await this.VerifyCSharpDiagnosticAsync(testCode, expected, CancellationToken.None).ConfigureAwait(false);
158+
}
159+
160+
[Theory]
161+
[MemberData(nameof(Members))]
162+
public async Task VerifyThatAnalyzerIgnoresLeadingAndTrailingWhitespaceMultiLineAsync(string member)
163+
{
164+
var testCode = $@"
165+
public class TestClass
166+
{{
167+
/** <summary>
168+
* Some documentation.
169+
*
170+
*
171+
* </summary>
172+
* <remark> Some documentation. </remark>
173+
**/
174+
{member}
175+
}}
176+
";
177+
var expected = this.CSharpDiagnostic().WithLocation(9, 7);
178+
await this.VerifyCSharpDiagnosticAsync(testCode, expected, CancellationToken.None).ConfigureAwait(false);
179+
}
180+
181+
[Theory]
182+
[MemberData(nameof(Members))]
183+
public async Task VerifyThatAnalysisIgnoresUnusedParametersMultiLineAsync(string member)
184+
{
185+
var testCode = $@"
186+
public class TestClass
187+
{{
188+
/** <summary>The parameter is not used.</summary>
189+
* <remark>Documentation</remark>
190+
* <remark>The parameter is not used.</remark>
191+
* <remark>Documentation</remark>
192+
**/
193+
{member}
194+
}}
195+
";
196+
var expected = this.CSharpDiagnostic().WithLocation(7, 7);
197+
await this.VerifyCSharpDiagnosticAsync(testCode, expected, CancellationToken.None).ConfigureAwait(false);
198+
}
199+
200+
[Theory]
201+
[MemberData(nameof(Members))]
202+
public async Task VerifyThatAnalysisIgnoresEmptyElementsMultiLineAsync(string member)
203+
{
204+
var testCode = $@"
205+
public class TestClass
206+
{{
207+
/** <summary></summary>
208+
* <remark>Documentation</remark>
209+
* <remark></remark>
210+
* <remark>Documentation</remark>
211+
**/
212+
{member}
213+
}}
214+
";
215+
var expected = this.CSharpDiagnostic().WithLocation(7, 7);
216+
await this.VerifyCSharpDiagnosticAsync(testCode, expected, CancellationToken.None).ConfigureAwait(false);
217+
}
218+
219+
/// <inheritdoc/>
220+
protected override IEnumerable<DiagnosticAnalyzer> GetCSharpDiagnosticAnalyzers()
221+
{
222+
yield return new SA1625ElementDocumentationMustNotBeCopiedAndPasted();
223+
}
224+
}
225+
}

StyleCop.Analyzers/StyleCop.Analyzers.Test/StyleCop.Analyzers.Test.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,7 @@
141141
<Compile Include="DocumentationRules\SA1621UnitTests.cs" />
142142
<Compile Include="DocumentationRules\SA1622UnitTests.cs" />
143143
<Compile Include="DocumentationRules\SA1623UnitTests.cs" />
144+
<Compile Include="DocumentationRules\SA1625UnitTests.cs" />
144145
<Compile Include="DocumentationRules\SA1624UnitTests.cs" />
145146
<Compile Include="DocumentationRules\SA1626UnitTests.cs" />
146147
<Compile Include="DocumentationRules\SA1627UnitTests.cs" />

StyleCop.Analyzers/StyleCop.Analyzers/DocumentationRules/SA1625ElementDocumentationMustNotBeCopiedAndPasted.cs

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,12 @@
33

44
namespace StyleCop.Analyzers.DocumentationRules
55
{
6+
using System.Collections.Generic;
67
using System.Collections.Immutable;
8+
using Helpers;
79
using Microsoft.CodeAnalysis;
10+
using Microsoft.CodeAnalysis.CSharp;
11+
using Microsoft.CodeAnalysis.CSharp.Syntax;
812
using Microsoft.CodeAnalysis.Diagnostics;
913

1014
/// <summary>
@@ -60,8 +64,9 @@ internal class SA1625ElementDocumentationMustNotBeCopiedAndPasted : DiagnosticAn
6064
/// analyzer.
6165
/// </summary>
6266
public const string DiagnosticId = "SA1625";
67+
private const string ParameterNotUsed = "The parameter is not used.";
6368
private const string Title = "Element documentation must not be copied and pasted";
64-
private const string MessageFormat = "TODO: Message format";
69+
private const string MessageFormat = "Element documentation must not be copied and pasted";
6570
private const string Description = "The Xml documentation for a C# element contains two or more identical entries, indicating that the documentation has been copied and pasted. This can sometimes indicate invalid or poorly written documentation.";
6671
private const string HelpLink = "https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1625.md";
6772

@@ -75,7 +80,40 @@ internal class SA1625ElementDocumentationMustNotBeCopiedAndPasted : DiagnosticAn
7580
/// <inheritdoc/>
7681
public override void Initialize(AnalysisContext context)
7782
{
78-
// TODO: Implement analysis
83+
context.RegisterCompilationStartAction(HandleCompilationStart);
84+
}
85+
86+
private static void HandleCompilationStart(CompilationStartAnalysisContext context)
87+
{
88+
context.RegisterSyntaxNodeActionHonorExclusions(HandleDocumentationTrivia, SyntaxKind.SingleLineDocumentationCommentTrivia);
89+
context.RegisterSyntaxNodeActionHonorExclusions(HandleDocumentationTrivia, SyntaxKind.MultiLineDocumentationCommentTrivia);
90+
}
91+
92+
private static void HandleDocumentationTrivia(SyntaxNodeAnalysisContext context)
93+
{
94+
DocumentationCommentTriviaSyntax syntax = context.Node as DocumentationCommentTriviaSyntax;
95+
96+
HashSet<string> documentationTexts = new HashSet<string>();
97+
98+
foreach (var content in syntax.Content)
99+
{
100+
string text = XmlCommentHelper.GetText(content, true).Trim();
101+
102+
if (string.IsNullOrWhiteSpace(text) || string.Equals(text, ParameterNotUsed, System.StringComparison.Ordinal))
103+
{
104+
continue;
105+
}
106+
107+
if (documentationTexts.Contains(text))
108+
{
109+
// Add violation
110+
context.ReportDiagnostic(Diagnostic.Create(Descriptor, content.GetLocation()));
111+
}
112+
else
113+
{
114+
documentationTexts.Add(text);
115+
}
116+
}
79117
}
80118
}
81119
}

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

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,32 @@ internal static bool HasDocumentation(SyntaxNode node)
163163
return commentTrivia != null && !IsMissingOrEmpty(commentTrivia.ParentTrivia);
164164
}
165165

166+
internal static string GetText(XmlNodeSyntax nodeSyntax, bool normalizeWhitespace = false)
167+
{
168+
var xmlTextSyntax = nodeSyntax as XmlTextSyntax;
169+
170+
if (xmlTextSyntax != null)
171+
{
172+
return GetText(xmlTextSyntax, normalizeWhitespace);
173+
}
174+
175+
var xmlElementSyntax = nodeSyntax as XmlElementSyntax;
176+
177+
if (xmlElementSyntax != null)
178+
{
179+
var stringBuilder = StringBuilderPool.Allocate();
180+
181+
foreach (var node in xmlElementSyntax.Content)
182+
{
183+
stringBuilder.Append(GetText(node, normalizeWhitespace));
184+
}
185+
186+
return StringBuilderPool.ReturnAndFree(stringBuilder);
187+
}
188+
189+
return null;
190+
}
191+
166192
internal static string GetText(XmlTextSyntax textElement)
167193
{
168194
return GetText(textElement, false);

0 commit comments

Comments
 (0)