Skip to content

Commit 67c0e57

Browse files
committed
Improves SA1642/SA1643 codefix when working with default Visual Studio generated documentation headers
1 parent a8b427d commit 67c0e57

3 files changed

Lines changed: 175 additions & 0 deletions

File tree

StyleCop.Analyzers/StyleCop.Analyzers.CodeFixes/DocumentationRules/SA1642SA1643CodeFixProvider.cs

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,9 @@ private static Task<Document> GetTransformedDocumentAsync(Document document, Syn
140140

141141
var list = BuildStandardText(typeDeclaration.Identifier, typeParameterList, newLineText, standardText[0], standardText[1] + trailingString);
142142
newContent = newContent.InsertRange(0, list);
143+
144+
newContent = RemoveTrailingEmptyLines(newContent);
145+
143146
var newNode = node.WithContent(newContent).AdjustDocumentationCommentNewLineTrivia();
144147

145148
var newRoot = root.ReplaceNode(node, newNode);
@@ -284,5 +287,39 @@ private static TypeArgumentListSyntax ParameterToArgumentListSyntax(TypeParamete
284287

285288
return SyntaxFactory.TypeArgumentList(list);
286289
}
290+
291+
private static SyntaxList<XmlNodeSyntax> RemoveTrailingEmptyLines(SyntaxList<XmlNodeSyntax> content)
292+
{
293+
var xmlText = content[content.Count - 1] as XmlTextSyntax;
294+
if (xmlText == null)
295+
{
296+
return content;
297+
}
298+
299+
// skip the last token, as it contains the documentation comment for the closing tag, which needs to remain.
300+
var firstEmptyToken = -1;
301+
for (var j = xmlText.TextTokens.Count - 2; j >= 0; j--)
302+
{
303+
var textToken = xmlText.TextTokens[j];
304+
305+
if (textToken.IsXmlWhitespace())
306+
{
307+
firstEmptyToken = j;
308+
}
309+
else if (textToken.IsKind(SyntaxKind.XmlTextLiteralToken) && !string.IsNullOrWhiteSpace(textToken.Text))
310+
{
311+
break;
312+
}
313+
}
314+
315+
if (firstEmptyToken > -1)
316+
{
317+
var newContent = SyntaxFactory.List(content.Take(firstEmptyToken));
318+
newContent.Add(newContent.Last());
319+
return newContent;
320+
}
321+
322+
return content;
323+
}
287324
}
288325
}

StyleCop.Analyzers/StyleCop.Analyzers.Test/DocumentationRules/SA1642UnitTests.cs

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -861,6 +861,76 @@ public TestClass() { }
861861
Assert.Empty(offeredFixes);
862862
}
863863

864+
/// <summary>
865+
/// Verify that the codefix will work properly with Visual Studio generated documentation headers.
866+
/// </summary>
867+
/// <returns>A <see cref="Task"/> representing the asynchronous unit test.</returns>
868+
[Fact]
869+
public async Task TestWithDefaultVisualStudioGenerationDocumentationAsync()
870+
{
871+
var testCode = @"
872+
public class TestClass
873+
{
874+
/// <summary>
875+
///
876+
/// </summary>
877+
public TestClass() { }
878+
}
879+
";
880+
881+
var fixedCode = @"
882+
public class TestClass
883+
{
884+
/// <summary>
885+
/// Initializes a new instance of the <see cref=""TestClass""/> class.
886+
/// </summary>
887+
public TestClass() { }
888+
}
889+
";
890+
891+
var expected = this.CSharpDiagnostic().WithLocation(4, 9);
892+
893+
await this.VerifyCSharpDiagnosticAsync(testCode, expected, CancellationToken.None).ConfigureAwait(false);
894+
await this.VerifyCSharpDiagnosticAsync(fixedCode, EmptyDiagnosticResults, CancellationToken.None).ConfigureAwait(false);
895+
await this.VerifyCSharpFixAsync(testCode, fixedCode, numberOfFixAllIterations: 2, cancellationToken: CancellationToken.None).ConfigureAwait(false);
896+
}
897+
898+
/// <summary>
899+
/// Verify that the codefix will work properly when there are multiple empty lines.
900+
/// </summary>
901+
/// <returns>A <see cref="Task"/> representing the asynchronous unit test.</returns>
902+
[Fact]
903+
public async Task TestWithMultipleEmptyLinesAsync()
904+
{
905+
var testCode = @"
906+
public class TestClass
907+
{
908+
/// <summary>
909+
///
910+
///
911+
///
912+
/// </summary>
913+
public TestClass() { }
914+
}
915+
";
916+
917+
var fixedCode = @"
918+
public class TestClass
919+
{
920+
/// <summary>
921+
/// Initializes a new instance of the <see cref=""TestClass""/> class.
922+
/// </summary>
923+
public TestClass() { }
924+
}
925+
";
926+
927+
var expected = this.CSharpDiagnostic().WithLocation(4, 9);
928+
929+
await this.VerifyCSharpDiagnosticAsync(testCode, expected, CancellationToken.None).ConfigureAwait(false);
930+
await this.VerifyCSharpDiagnosticAsync(fixedCode, EmptyDiagnosticResults, CancellationToken.None).ConfigureAwait(false);
931+
await this.VerifyCSharpFixAsync(testCode, fixedCode, numberOfFixAllIterations: 2, cancellationToken: CancellationToken.None).ConfigureAwait(false);
932+
}
933+
864934
protected override Project ApplyCompilationOptions(Project project)
865935
{
866936
var resolver = new TestXmlReferenceResolver();

StyleCop.Analyzers/StyleCop.Analyzers.Test/DocumentationRules/SA1643UnitTests.cs

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,74 @@ public class TestClass
152152
Assert.Empty(offeredFixes);
153153
}
154154

155+
/// <summary>
156+
/// Verify that the codefix will work properly with Visual Studio generated documentation headers.
157+
/// </summary>
158+
/// <returns>A <see cref="Task"/> representing the asynchronous unit test.</returns>
159+
[Fact]
160+
public async Task TestWithDefaultVisualStudioGenerationDocumentationAsync()
161+
{
162+
var testCode = @"
163+
public class TestClass
164+
{
165+
/// <summary>
166+
///
167+
/// </summary>
168+
~TestClass() { }
169+
}
170+
";
171+
172+
var fixedCode = @"
173+
public class TestClass
174+
{
175+
/// <summary>
176+
/// Finalizes an instance of the <see cref=""TestClass""/> class.
177+
/// </summary>
178+
~TestClass() { }
179+
}
180+
";
181+
182+
var expected = this.CSharpDiagnostic().WithLocation(4, 9);
183+
184+
await this.VerifyCSharpDiagnosticAsync(testCode, expected, CancellationToken.None).ConfigureAwait(false);
185+
await this.VerifyCSharpDiagnosticAsync(fixedCode, EmptyDiagnosticResults, CancellationToken.None).ConfigureAwait(false);
186+
await this.VerifyCSharpFixAsync(testCode, fixedCode, numberOfFixAllIterations: 2, cancellationToken: CancellationToken.None).ConfigureAwait(false);
187+
}
188+
189+
/// <summary>
190+
/// Verify that the codefix will work properly when there are multiple empty lines.
191+
/// </summary>
192+
/// <returns>A <see cref="Task"/> representing the asynchronous unit test.</returns>
193+
[Fact]
194+
public async Task TestWithMultipleEmptyLinesAsync()
195+
{
196+
var testCode = @"
197+
public class TestClass
198+
{
199+
/// <summary>
200+
///
201+
/// </summary>
202+
~TestClass() { }
203+
}
204+
";
205+
206+
var fixedCode = @"
207+
public class TestClass
208+
{
209+
/// <summary>
210+
/// Finalizes an instance of the <see cref=""TestClass""/> class.
211+
/// </summary>
212+
~TestClass() { }
213+
}
214+
";
215+
216+
var expected = this.CSharpDiagnostic().WithLocation(4, 9);
217+
218+
await this.VerifyCSharpDiagnosticAsync(testCode, expected, CancellationToken.None).ConfigureAwait(false);
219+
await this.VerifyCSharpDiagnosticAsync(fixedCode, EmptyDiagnosticResults, CancellationToken.None).ConfigureAwait(false);
220+
await this.VerifyCSharpFixAsync(testCode, fixedCode, numberOfFixAllIterations: 2, cancellationToken: CancellationToken.None).ConfigureAwait(false);
221+
}
222+
155223
protected override Project ApplyCompilationOptions(Project project)
156224
{
157225
var resolver = new TestXmlReferenceResolver();

0 commit comments

Comments
 (0)