Skip to content

Commit 277421e

Browse files
committed
Update SA1611 to handle included documentation (fixes #1904)
1 parent c18b86b commit 277421e

4 files changed

Lines changed: 318 additions & 47 deletions

File tree

StyleCop.Analyzers/StyleCop.Analyzers.Test/DocumentationRules/SA1611UnitTests.cs

Lines changed: 116 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;
@@ -228,6 +230,120 @@ public static explicit operator TestClass(int value)
228230
await this.VerifyCSharpDiagnosticAsync(testCode, expected, CancellationToken.None).ConfigureAwait(false);
229231
}
230232

233+
/// <summary>
234+
/// Verifies that included documentation with valid documentation does not produce diagnostics.
235+
/// </summary>
236+
/// <returns>A <see cref="Task"/> representing the asynchronous unit test.</returns>
237+
[Fact]
238+
public async Task VerifyIncludedDocumentationAsync()
239+
{
240+
var testCode = @"
241+
/// <summary>
242+
/// Foo
243+
/// </summary>
244+
public class ClassName
245+
{
246+
/// <include file='WithElementDocumentation.xml' path='/TestClass/TestMethod/*' />
247+
public void TestMethod(string param1, string param2, string param3)
248+
{
249+
}
250+
}";
251+
await this.VerifyCSharpDiagnosticAsync(testCode, EmptyDiagnosticResults, CancellationToken.None).ConfigureAwait(false);
252+
}
253+
254+
/// <summary>
255+
/// Verifies that included documentation with missing elements produces the expected diagnostics.
256+
/// </summary>
257+
/// <returns>A <see cref="Task"/> representing the asynchronous unit test.</returns>
258+
[Fact]
259+
public async Task VerifyIncludedDocumentationMissingElementsAsync()
260+
{
261+
var testCode = @"
262+
/// <summary>
263+
/// Foo
264+
/// </summary>
265+
public class ClassName
266+
{
267+
/// <include file='MissingElementDocumentation.xml' path='/TestClass/TestMethod/*' />
268+
public void TestMethod(string param1, string param2, string param3)
269+
{
270+
}
271+
}";
272+
DiagnosticResult[] expected =
273+
{
274+
this.CSharpDiagnostic().WithLocation(8, 35).WithArguments("param1"),
275+
this.CSharpDiagnostic().WithLocation(8, 50).WithArguments("param2"),
276+
this.CSharpDiagnostic().WithLocation(8, 65).WithArguments("param3"),
277+
};
278+
279+
await this.VerifyCSharpDiagnosticAsync(testCode, expected, CancellationToken.None).ConfigureAwait(false);
280+
}
281+
282+
/// <summary>
283+
/// Verifies that included documentation with an <c>&lt;inheritdoc&gt;</c> tag is ignored.
284+
/// </summary>
285+
/// <returns>A <see cref="Task"/> representing the asynchronous unit test.</returns>
286+
[Fact]
287+
public async Task VerifyIncludedInheritedDocumentationAsync()
288+
{
289+
var testCode = @"
290+
/// <summary>
291+
/// Foo
292+
/// </summary>
293+
public class ClassName
294+
{
295+
/// <include file='InheritedDocumentation.xml' path='/TestClass/TestMethod/*' />
296+
public void TestMethod(string param1, string param2, string param3)
297+
{
298+
}
299+
}";
300+
await this.VerifyCSharpDiagnosticAsync(testCode, EmptyDiagnosticResults, CancellationToken.None).ConfigureAwait(false);
301+
}
302+
303+
/// <inheritdoc/>
304+
protected override Project CreateProject(string[] sources, string language = "C#", string[] filenames = null)
305+
{
306+
var resolver = new TestXmlReferenceResolver();
307+
308+
string contentWithoutElementDocumentation = @"<?xml version=""1.0"" encoding=""utf-8"" ?>
309+
<TestClass>
310+
<TestMethod>
311+
<summary>
312+
Foo
313+
</summary>
314+
</TestMethod>
315+
</TestClass>
316+
";
317+
resolver.XmlReferences.Add("MissingElementDocumentation.xml", contentWithoutElementDocumentation);
318+
319+
string contentWithElementDocumentation = @"<?xml version=""1.0"" encoding=""utf-8"" ?>
320+
<TestClass>
321+
<TestMethod>
322+
<summary>
323+
Foo
324+
</summary>
325+
<param name=""param1"">Param 1</param>
326+
<param name=""param2"">Param 2</param>
327+
<param name=""param3"">Param 3</param>
328+
</TestMethod>
329+
</TestClass>
330+
";
331+
resolver.XmlReferences.Add("WithElementDocumentation.xml", contentWithElementDocumentation);
332+
333+
string contentWithInheritedDocumentation = @"<?xml version=""1.0"" encoding=""utf-8"" ?>
334+
<TestClass>
335+
<TestMethod>
336+
<inheritdoc />
337+
</TestMethod>
338+
</TestClass>
339+
";
340+
resolver.XmlReferences.Add("InheritedDocumentation.xml", contentWithInheritedDocumentation);
341+
342+
Project project = base.CreateProject(sources, language, filenames);
343+
project = project.WithCompilationOptions(project.CompilationOptions.WithXmlReferenceResolver(resolver));
344+
return project;
345+
}
346+
231347
/// <inheritdoc/>
232348
protected override IEnumerable<DiagnosticAnalyzer> GetCSharpDiagnosticAnalyzers()
233349
{
Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
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.DocumentationRules
5+
{
6+
using System;
7+
using System.Collections.Generic;
8+
using System.Linq;
9+
using System.Xml.Linq;
10+
using Microsoft.CodeAnalysis;
11+
using Microsoft.CodeAnalysis.CSharp;
12+
using Microsoft.CodeAnalysis.CSharp.Syntax;
13+
using Microsoft.CodeAnalysis.Diagnostics;
14+
using StyleCop.Analyzers.Helpers;
15+
16+
/// <summary>
17+
/// This is the base class for analyzers which examine the <c>&lt;param&gt;</c> text of a documentation comment on an element declaration.
18+
/// </summary>
19+
internal abstract class ElementDocumentationParameterBase : DiagnosticAnalyzer
20+
{
21+
private readonly Action<CompilationStartAnalysisContext> compilationStartAction;
22+
private readonly Action<SyntaxNodeAnalysisContext> methodDeclarationAction;
23+
private readonly Action<SyntaxNodeAnalysisContext> constructorDeclarationAction;
24+
private readonly Action<SyntaxNodeAnalysisContext> delegateDeclarationAction;
25+
private readonly Action<SyntaxNodeAnalysisContext> indexerDeclarationAction;
26+
private readonly Action<SyntaxNodeAnalysisContext> operatorDeclarationAction;
27+
private readonly Action<SyntaxNodeAnalysisContext> conversionOperatorDeclarationAction;
28+
29+
protected ElementDocumentationParameterBase()
30+
{
31+
this.compilationStartAction = this.HandleCompilationStart;
32+
this.methodDeclarationAction = this.HandleMethodDeclaration;
33+
this.constructorDeclarationAction = this.HandleConstructorDeclaration;
34+
this.delegateDeclarationAction = this.HandleDelegateDeclaration;
35+
this.indexerDeclarationAction = this.HandleIndexerDeclaration;
36+
this.operatorDeclarationAction = this.HandleOperatorDeclaration;
37+
this.conversionOperatorDeclarationAction = this.HandleConversionOperatorDeclaration;
38+
}
39+
40+
/// <inheritdoc/>
41+
public override void Initialize(AnalysisContext context)
42+
{
43+
context.RegisterCompilationStartAction(this.compilationStartAction);
44+
}
45+
46+
/// <summary>
47+
/// Analyzes the top-level <c>&lt;param&gt;</c> elements of a documentation comment.
48+
/// </summary>
49+
/// <param name="context">The current analysis context.</param>
50+
/// <param name="syntaxList">The <see cref="XmlElementSyntax"/> or <see cref="XmlEmptyElementSyntax"/> of the node
51+
/// to examine.</param>
52+
/// <param name="completeDocumentation">The complete documentation for the declared symbol, with any
53+
/// <c>&lt;include&gt;</c> elements expanded. If the XML documentation comment included a <c>&lt;param&gt;</c>
54+
/// element, this value will be <see langword="null"/>, even if the XML documentation comment also included an
55+
/// <c>&lt;include&gt;</c> element.</param>
56+
/// <param name="diagnosticLocations">The location(s) where diagnostics, if any, should be reported.</param>
57+
protected abstract void HandleXmlElement(SyntaxNodeAnalysisContext context, IEnumerable<XmlNodeSyntax> syntaxList, XElement completeDocumentation, params Location[] diagnosticLocations);
58+
59+
private void HandleCompilationStart(CompilationStartAnalysisContext context)
60+
{
61+
context.RegisterSyntaxNodeActionHonorExclusions(this.methodDeclarationAction, SyntaxKind.MethodDeclaration);
62+
context.RegisterSyntaxNodeActionHonorExclusions(this.constructorDeclarationAction, SyntaxKind.ConstructorDeclaration);
63+
context.RegisterSyntaxNodeActionHonorExclusions(this.delegateDeclarationAction, SyntaxKind.DelegateDeclaration);
64+
context.RegisterSyntaxNodeActionHonorExclusions(this.indexerDeclarationAction, SyntaxKind.IndexerDeclaration);
65+
context.RegisterSyntaxNodeActionHonorExclusions(this.operatorDeclarationAction, SyntaxKind.OperatorDeclaration);
66+
context.RegisterSyntaxNodeActionHonorExclusions(this.conversionOperatorDeclarationAction, SyntaxKind.ConversionOperatorDeclaration);
67+
}
68+
69+
private void HandleMethodDeclaration(SyntaxNodeAnalysisContext context)
70+
{
71+
var node = (MethodDeclarationSyntax)context.Node;
72+
if (node.Identifier.IsMissing)
73+
{
74+
return;
75+
}
76+
77+
this.HandleDeclaration(context, node, node.Identifier.GetLocation());
78+
}
79+
80+
private void HandleConstructorDeclaration(SyntaxNodeAnalysisContext context)
81+
{
82+
var node = (ConstructorDeclarationSyntax)context.Node;
83+
if (node.Identifier.IsMissing)
84+
{
85+
return;
86+
}
87+
88+
this.HandleDeclaration(context, node, node.Identifier.GetLocation());
89+
}
90+
91+
private void HandleDelegateDeclaration(SyntaxNodeAnalysisContext context)
92+
{
93+
var node = (DelegateDeclarationSyntax)context.Node;
94+
if (node.Identifier.IsMissing)
95+
{
96+
return;
97+
}
98+
99+
this.HandleDeclaration(context, node, node.Identifier.GetLocation());
100+
}
101+
102+
private void HandleIndexerDeclaration(SyntaxNodeAnalysisContext context)
103+
{
104+
var node = (IndexerDeclarationSyntax)context.Node;
105+
if (node.ThisKeyword.IsMissing)
106+
{
107+
return;
108+
}
109+
110+
this.HandleDeclaration(context, node, node.ThisKeyword.GetLocation());
111+
}
112+
113+
private void HandleOperatorDeclaration(SyntaxNodeAnalysisContext context)
114+
{
115+
var node = (OperatorDeclarationSyntax)context.Node;
116+
if (node.OperatorToken.IsMissing)
117+
{
118+
return;
119+
}
120+
121+
this.HandleDeclaration(context, node, node.OperatorToken.GetLocation());
122+
}
123+
124+
private void HandleConversionOperatorDeclaration(SyntaxNodeAnalysisContext context)
125+
{
126+
var node = (ConversionOperatorDeclarationSyntax)context.Node;
127+
128+
this.HandleDeclaration(context, node, node.GetLocation());
129+
}
130+
131+
private void HandleDeclaration(SyntaxNodeAnalysisContext context, SyntaxNode node, params Location[] locations)
132+
{
133+
var documentation = node.GetDocumentationCommentTriviaSyntax();
134+
if (documentation == null)
135+
{
136+
// missing documentation is reported by SA1600, SA1601, and SA1602
137+
return;
138+
}
139+
140+
if (documentation.Content.GetFirstXmlElement(XmlCommentHelper.InheritdocXmlTag) != null)
141+
{
142+
// Ignore nodes with an <inheritdoc/> tag.
143+
return;
144+
}
145+
146+
XElement completeDocumentation = null;
147+
var paramXmlElements = documentation.Content.GetXmlElements(XmlCommentHelper.ParamXmlTag);
148+
if (!paramXmlElements.Any())
149+
{
150+
var includedDocumentation = documentation.Content.GetFirstXmlElement(XmlCommentHelper.IncludeXmlTag);
151+
if (includedDocumentation != null)
152+
{
153+
var declaration = context.SemanticModel.GetDeclaredSymbol(node, context.CancellationToken);
154+
var rawDocumentation = declaration?.GetDocumentationCommentXml(expandIncludes: true, cancellationToken: context.CancellationToken);
155+
completeDocumentation = XElement.Parse(rawDocumentation, LoadOptions.None);
156+
if (completeDocumentation.Nodes().OfType<XElement>().Any(element => element.Name == XmlCommentHelper.InheritdocXmlTag))
157+
{
158+
// Ignore nodes with an <inheritdoc/> tag in the included XML.
159+
return;
160+
}
161+
}
162+
}
163+
164+
this.HandleXmlElement(context, paramXmlElements, completeDocumentation, locations);
165+
}
166+
}
167+
}

0 commit comments

Comments
 (0)