Skip to content

Commit 654e5b4

Browse files
committed
Merge pull request #1848 from pdelvo/SA1625
Implement SA1625
2 parents c032b21 + ccc7685 commit 654e5b4

4 files changed

Lines changed: 314 additions & 5 deletions

File tree

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

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

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

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

Lines changed: 49 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,14 @@
33

44
namespace StyleCop.Analyzers.DocumentationRules
55
{
6+
using System;
7+
using System.Collections.Generic;
68
using System.Collections.Immutable;
9+
using Helpers;
10+
using Helpers.ObjectPools;
711
using Microsoft.CodeAnalysis;
12+
using Microsoft.CodeAnalysis.CSharp;
13+
using Microsoft.CodeAnalysis.CSharp.Syntax;
814
using Microsoft.CodeAnalysis.Diagnostics;
915

1016
/// <summary>
@@ -60,13 +66,18 @@ internal class SA1625ElementDocumentationMustNotBeCopiedAndPasted : DiagnosticAn
6066
/// analyzer.
6167
/// </summary>
6268
public const string DiagnosticId = "SA1625";
69+
private const string ParameterNotUsed = "The parameter is not used.";
6370
private const string Title = "Element documentation must not be copied and pasted";
64-
private const string MessageFormat = "TODO: Message format";
71+
private const string MessageFormat = "Element documentation must not be copied and pasted";
6572
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.";
6673
private const string HelpLink = "https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1625.md";
6774

6875
private static readonly DiagnosticDescriptor Descriptor =
69-
new DiagnosticDescriptor(DiagnosticId, Title, MessageFormat, AnalyzerCategory.DocumentationRules, DiagnosticSeverity.Warning, AnalyzerConstants.DisabledNoTests, Description, HelpLink);
76+
new DiagnosticDescriptor(DiagnosticId, Title, MessageFormat, AnalyzerCategory.DocumentationRules, DiagnosticSeverity.Warning, AnalyzerConstants.EnabledByDefault, Description, HelpLink);
77+
78+
private static readonly Action<CompilationStartAnalysisContext> CompilationStartAction = HandleCompilationStart;
79+
private static readonly Action<SyntaxNodeAnalysisContext> DocumentationTriviaAction = HandleDocumentationTrivia;
80+
private static readonly ImmutableArray<SyntaxKind> DocumentationSyntaxKinds = ImmutableArray.Create(SyntaxKind.SingleLineDocumentationCommentTrivia, SyntaxKind.MultiLineDocumentationCommentTrivia);
7081

7182
/// <inheritdoc/>
7283
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics { get; } =
@@ -75,7 +86,42 @@ internal class SA1625ElementDocumentationMustNotBeCopiedAndPasted : DiagnosticAn
7586
/// <inheritdoc/>
7687
public override void Initialize(AnalysisContext context)
7788
{
78-
// TODO: Implement analysis
89+
context.RegisterCompilationStartAction(CompilationStartAction);
90+
}
91+
92+
private static void HandleCompilationStart(CompilationStartAnalysisContext context)
93+
{
94+
context.RegisterSyntaxNodeActionHonorExclusions(DocumentationTriviaAction, DocumentationSyntaxKinds);
95+
}
96+
97+
private static void HandleDocumentationTrivia(SyntaxNodeAnalysisContext context)
98+
{
99+
DocumentationCommentTriviaSyntax syntax = context.Node as DocumentationCommentTriviaSyntax;
100+
101+
var objectPool = SharedPools.Default<HashSet<string>>();
102+
HashSet<string> documentationTexts = objectPool.Allocate();
103+
104+
foreach (var content in syntax.Content)
105+
{
106+
string text = XmlCommentHelper.GetText(content, true)?.Trim();
107+
108+
if (string.IsNullOrWhiteSpace(text) || string.Equals(text, ParameterNotUsed, StringComparison.Ordinal))
109+
{
110+
continue;
111+
}
112+
113+
if (documentationTexts.Contains(text))
114+
{
115+
// Add violation
116+
context.ReportDiagnostic(Diagnostic.Create(Descriptor, content.GetLocation()));
117+
}
118+
else
119+
{
120+
documentationTexts.Add(text);
121+
}
122+
}
123+
124+
objectPool.ClearAndFree(documentationTexts);
79125
}
80126
}
81127
}

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

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

44
namespace StyleCop.Analyzers.Helpers
55
{
6-
using System;
7-
using System.Collections.Generic;
86
using System.Linq;
97
using System.Text;
108
using System.Text.RegularExpressions;
@@ -163,6 +161,32 @@ internal static bool HasDocumentation(SyntaxNode node)
163161
return commentTrivia != null && !IsMissingOrEmpty(commentTrivia.ParentTrivia);
164162
}
165163

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

0 commit comments

Comments
 (0)