Skip to content

Commit ebcb9d1

Browse files
committed
Update SA1614 to handle included documentation (fixes #1907)
1 parent c93106e commit ebcb9d1

2 files changed

Lines changed: 214 additions & 26 deletions

File tree

StyleCop.Analyzers/StyleCop.Analyzers.Test/DocumentationRules/SA1614UnitTests.cs

Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ namespace StyleCop.Analyzers.Test.DocumentationRules
77
using System.Threading;
88
using System.Threading.Tasks;
99
using Analyzers.DocumentationRules;
10+
using Helpers;
11+
using Microsoft.CodeAnalysis;
1012
using Microsoft.CodeAnalysis.Diagnostics;
1113
using TestHelper;
1214
using Xunit;
@@ -138,6 +140,169 @@ public class ClassName
138140
await this.VerifyCSharpDiagnosticAsync(testCode.Replace("$$", declaration), expected, CancellationToken.None).ConfigureAwait(false);
139141
}
140142

143+
[Fact]
144+
public async Task VerifyMemberIncludedDocumentationNoDocumentationAsync()
145+
{
146+
var testCode = @"
147+
/// <summary>
148+
/// Foo
149+
/// </summary>
150+
public class ClassName
151+
{
152+
/// <include file='NoDocumentation.xml' path='/ClassName/Method/*' />
153+
public ClassName Method(string foo, string bar) { return null; }
154+
}";
155+
await this.VerifyCSharpDiagnosticAsync(testCode, EmptyDiagnosticResults, CancellationToken.None).ConfigureAwait(false);
156+
}
157+
158+
[Fact]
159+
public async Task VerifyMemberIncludedDocumentationWithoutParamsAsync()
160+
{
161+
var testCode = @"
162+
/// <summary>
163+
/// Foo
164+
/// </summary>
165+
public class ClassName
166+
{
167+
/// <include file='NoParamDocumentation.xml' path='/ClassName/Method/*' />
168+
public ClassName Method(string foo, string bar) { return null; }
169+
}";
170+
await this.VerifyCSharpDiagnosticAsync(testCode, EmptyDiagnosticResults, CancellationToken.None).ConfigureAwait(false);
171+
}
172+
173+
[Fact]
174+
public async Task VerifyMemberIncludedDocumentationWithValidParamsAsync()
175+
{
176+
var testCode = @"
177+
/// <summary>
178+
/// Foo
179+
/// </summary>
180+
public class ClassName
181+
{
182+
/// <include file='ParamDocumentation.xml' path='/ClassName/Method/*' />
183+
public ClassName Method(string foo, string bar) { return null; }
184+
}";
185+
await this.VerifyCSharpDiagnosticAsync(testCode, EmptyDiagnosticResults, CancellationToken.None).ConfigureAwait(false);
186+
}
187+
188+
[Fact]
189+
public async Task VerifyMemberIncludedDocumentationWithEmptyParamsAsync()
190+
{
191+
var testCode = @"
192+
/// <summary>
193+
/// Foo
194+
/// </summary>
195+
public class ClassName
196+
{
197+
/// <include file='EmptyParamDocumentation.xml' path='/ClassName/Method/*' />
198+
public ClassName Method(string foo, string bar) { return null; }
199+
}";
200+
201+
var expected = new[]
202+
{
203+
this.CSharpDiagnostic().WithLocation(8, 22),
204+
this.CSharpDiagnostic().WithLocation(8, 22)
205+
};
206+
207+
await this.VerifyCSharpDiagnosticAsync(testCode, expected, CancellationToken.None).ConfigureAwait(false);
208+
}
209+
210+
[Fact]
211+
public async Task VerifyMemberIncludedDocumentationWithEmptyParams2Async()
212+
{
213+
var testCode = @"
214+
/// <summary>
215+
/// Foo
216+
/// </summary>
217+
public class ClassName
218+
{
219+
/// <include file='EmptyParamDocumentation2.xml' path='/ClassName/Method/*' />
220+
public ClassName Method(string foo, string bar) { return null; }
221+
}";
222+
223+
var expected = new[]
224+
{
225+
this.CSharpDiagnostic().WithLocation(8, 22),
226+
this.CSharpDiagnostic().WithLocation(8, 22)
227+
};
228+
229+
await this.VerifyCSharpDiagnosticAsync(testCode, expected, CancellationToken.None).ConfigureAwait(false);
230+
}
231+
232+
/// <inheritdoc/>
233+
protected override Project ApplyCompilationOptions(Project project)
234+
{
235+
var resolver = new TestXmlReferenceResolver();
236+
237+
string contentWithoutDocumentation = @"<?xml version=""1.0"" encoding=""utf-8"" ?>
238+
<ClassName>
239+
<Method>
240+
</Method>
241+
</ClassName>
242+
";
243+
resolver.XmlReferences.Add("NoDocumentation.xml", contentWithoutDocumentation);
244+
245+
string contentWithoutParamDocumentation = @"<?xml version=""1.0"" encoding=""utf-8"" ?>
246+
<ClassName>
247+
<Method>
248+
<summary>
249+
Foo
250+
</summary>
251+
</Method>
252+
</ClassName>
253+
";
254+
resolver.XmlReferences.Add("NoParamDocumentation.xml", contentWithoutParamDocumentation);
255+
256+
string contentWithParamDocumentation = @"<?xml version=""1.0"" encoding=""utf-8"" ?>
257+
<ClassName>
258+
<Method>
259+
<summary>
260+
Foo
261+
</summary>
262+
<param name=""foo"">Test</param>
263+
<param name=""bar"">Test</param>
264+
</Method>
265+
</ClassName>
266+
";
267+
resolver.XmlReferences.Add("ParamDocumentation.xml", contentWithParamDocumentation);
268+
269+
string contentWithEmptyParamDocumentation = @"<?xml version=""1.0"" encoding=""utf-8"" ?>
270+
<ClassName>
271+
<Method>
272+
<summary>
273+
Foo
274+
</summary>
275+
<param name=""foo""></param>
276+
<param name=""bar"">
277+
278+
</param>
279+
</Method>
280+
</ClassName>
281+
";
282+
resolver.XmlReferences.Add("EmptyParamDocumentation.xml", contentWithEmptyParamDocumentation);
283+
284+
string contentWithEmptyParamDocumentation2 = @"<?xml version=""1.0"" encoding=""utf-8"" ?>
285+
<ClassName>
286+
<Method>
287+
<summary>
288+
Foo
289+
</summary>
290+
<param name=""foo""/>
291+
<param name=""bar"">
292+
<para>
293+
294+
</para>
295+
</param>
296+
</Method>
297+
</ClassName>
298+
";
299+
resolver.XmlReferences.Add("EmptyParamDocumentation2.xml", contentWithEmptyParamDocumentation2);
300+
301+
project = base.ApplyCompilationOptions(project);
302+
project = project.WithCompilationOptions(project.CompilationOptions.WithXmlReferenceResolver(resolver));
303+
return project;
304+
}
305+
141306
protected override IEnumerable<DiagnosticAnalyzer> GetCSharpDiagnosticAnalyzers()
142307
{
143308
yield return new SA1614ElementParameterDocumentationMustHaveText();

StyleCop.Analyzers/StyleCop.Analyzers/DocumentationRules/SA1614ElementParameterDocumentationMustHaveText.cs

Lines changed: 49 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,11 @@
44
namespace StyleCop.Analyzers.DocumentationRules
55
{
66
using System;
7+
using System.Collections.Generic;
78
using System.Collections.Immutable;
9+
using System.Linq;
10+
using System.Xml.Linq;
811
using Microsoft.CodeAnalysis;
9-
using Microsoft.CodeAnalysis.CSharp;
1012
using Microsoft.CodeAnalysis.CSharp.Syntax;
1113
using Microsoft.CodeAnalysis.Diagnostics;
1214
using StyleCop.Analyzers.Helpers;
@@ -26,7 +28,7 @@ namespace StyleCop.Analyzers.DocumentationRules
2628
/// </remarks>
2729
[DiagnosticAnalyzer(LanguageNames.CSharp)]
2830
[NoCodeFix("Cannot generate documentation")]
29-
internal class SA1614ElementParameterDocumentationMustHaveText : DiagnosticAnalyzer
31+
internal class SA1614ElementParameterDocumentationMustHaveText : ElementDocumentationParameterBase
3032
{
3133
/// <summary>
3234
/// The ID for diagnostics produced by the <see cref="SA1614ElementParameterDocumentationMustHaveText"/>
@@ -41,47 +43,68 @@ internal class SA1614ElementParameterDocumentationMustHaveText : DiagnosticAnaly
4143
private static readonly DiagnosticDescriptor Descriptor =
4244
new DiagnosticDescriptor(DiagnosticId, Title, MessageFormat, AnalyzerCategory.DocumentationRules, DiagnosticSeverity.Warning, AnalyzerConstants.EnabledByDefault, Description, HelpLink);
4345

44-
private static readonly Action<CompilationStartAnalysisContext> CompilationStartAction = HandleCompilationStart;
45-
private static readonly Action<SyntaxNodeAnalysisContext> XmlElementAction = HandleXmlElement;
46-
private static readonly Action<SyntaxNodeAnalysisContext> XmlEmptyElementAction = HandleXmlEmptyElement;
46+
public SA1614ElementParameterDocumentationMustHaveText()
47+
: base(inheritDocSuppressesWarnings: true)
48+
{
49+
}
4750

4851
/// <inheritdoc/>
4952
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics { get; } =
5053
ImmutableArray.Create(Descriptor);
5154

5255
/// <inheritdoc/>
53-
public override void Initialize(AnalysisContext context)
54-
{
55-
context.RegisterCompilationStartAction(CompilationStartAction);
56-
}
57-
58-
private static void HandleCompilationStart(CompilationStartAnalysisContext context)
56+
protected override void HandleXmlElement(SyntaxNodeAnalysisContext context, IEnumerable<XmlNodeSyntax> syntaxList, XElement completeDocumentation, params Location[] diagnosticLocations)
5957
{
60-
context.RegisterSyntaxNodeActionHonorExclusions(XmlElementAction, SyntaxKind.XmlElement);
61-
context.RegisterSyntaxNodeActionHonorExclusions(XmlEmptyElementAction, SyntaxKind.XmlEmptyElement);
62-
}
58+
bool includeElementPresent = completeDocumentation != null;
59+
if (includeElementPresent)
60+
{
61+
var xmlParameterNames = completeDocumentation.Nodes()
62+
.OfType<XElement>()
63+
.Where(e => e.Name == XmlCommentHelper.ParamXmlTag)
64+
.Select(x =>
65+
{
66+
return new Tuple<bool, Location>(XmlCommentHelper.IsConsideredEmpty(x), null);
67+
})
68+
.ToImmutableArray();
6369

64-
private static void HandleXmlElement(SyntaxNodeAnalysisContext context)
65-
{
66-
XmlElementSyntax emptyElement = context.Node as XmlElementSyntax;
70+
VerifyParameters(context, xmlParameterNames, diagnosticLocations.First());
71+
}
72+
else if (syntaxList != null)
73+
{
74+
var xmlParameterNames = syntaxList
75+
.Where(x => string.Equals(GetName(x)?.ToString(), XmlCommentHelper.ParamXmlTag))
76+
.Select(x =>
77+
{
78+
bool isEmpty = x is XmlEmptyElementSyntax || XmlCommentHelper.IsConsideredEmpty(x);
79+
var location = x.GetLocation();
6780

68-
var name = emptyElement?.StartTag?.Name;
81+
return new Tuple<bool, Location>(isEmpty, location);
82+
})
83+
.ToImmutableArray();
6984

70-
if (string.Equals(name.ToString(), XmlCommentHelper.ParamXmlTag) && XmlCommentHelper.IsConsideredEmpty(emptyElement))
71-
{
72-
context.ReportDiagnostic(Diagnostic.Create(Descriptor, emptyElement.GetLocation()));
85+
VerifyParameters(context, xmlParameterNames, diagnosticLocations.First());
7386
}
7487
}
7588

76-
private static void HandleXmlEmptyElement(SyntaxNodeAnalysisContext context)
89+
private static void VerifyParameters(SyntaxNodeAnalysisContext context, ImmutableArray<Tuple<bool, Location>> documentationParameters, Location identifierLocation)
7790
{
78-
XmlEmptyElementSyntax emptyElement = context.Node as XmlEmptyElementSyntax;
91+
var index = 0;
7992

80-
if (string.Equals(emptyElement?.Name.ToString(), XmlCommentHelper.ParamXmlTag))
93+
foreach (var documentedParameter in documentationParameters)
8194
{
82-
// <param .../> is empty.
83-
context.ReportDiagnostic(Diagnostic.Create(Descriptor, emptyElement.GetLocation()));
95+
if (documentedParameter.Item1)
96+
{
97+
context.ReportDiagnostic(Diagnostic.Create(Descriptor, documentedParameter.Item2 ?? identifierLocation));
98+
}
99+
100+
index++;
84101
}
85102
}
103+
104+
private static XmlNameSyntax GetName(XmlNodeSyntax element)
105+
{
106+
return (element as XmlElementSyntax)?.StartTag?.Name
107+
?? (element as XmlEmptyElementSyntax)?.Name;
108+
}
86109
}
87110
}

0 commit comments

Comments
 (0)