Skip to content

Commit 1566527

Browse files
committed
Merge pull request #1922 from sharwell/fix-1897
Fixed issues with file headers in UsingCodeFixProvider
2 parents b11bba5 + 92a3ade commit 1566527

2 files changed

Lines changed: 124 additions & 8 deletions

File tree

StyleCop.Analyzers/StyleCop.Analyzers.CodeFixes/OrderingRules/UsingCodeFixProvider.cs

Lines changed: 29 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,8 @@ namespace StyleCop.Analyzers.OrderingRules
2727
internal sealed class UsingCodeFixProvider : CodeFixProvider
2828
{
2929
private static readonly List<UsingDirectiveSyntax> EmptyUsingsList = new List<UsingDirectiveSyntax>();
30-
private static readonly SyntaxAnnotation UsingCodeFixAnnotation = new SyntaxAnnotation(nameof(UsingCodeFixProvider));
30+
private static readonly SyntaxAnnotation UsingCodeFixAnnotation = new SyntaxAnnotation(nameof(UsingCodeFixAnnotation));
31+
private static readonly SyntaxAnnotation FileHeaderStrippedAnnotation = new SyntaxAnnotation(nameof(FileHeaderStrippedAnnotation));
3132

3233
/// <inheritdoc/>
3334
public override ImmutableArray<string> FixableDiagnosticIds { get; } =
@@ -175,6 +176,13 @@ private static async Task<Document> GetTransformedDocumentAsync(Document documen
175176

176177
private static SyntaxNode ReAddFileHeader(SyntaxNode syntaxRoot, SyntaxNode newSyntaxRoot)
177178
{
179+
// Only re-add the file header if it was stripped.
180+
var usingDirectives = newSyntaxRoot.GetAnnotatedNodes(FileHeaderStrippedAnnotation);
181+
if (!usingDirectives.Any())
182+
{
183+
return newSyntaxRoot;
184+
}
185+
178186
var oldFirstToken = syntaxRoot.GetFirstToken();
179187
if (!oldFirstToken.HasLeadingTrivia)
180188
{
@@ -584,12 +592,6 @@ internal static List<SyntaxTrivia> GetFileHeader(SyntaxTriviaList newLeadingTriv
584592
return hasHeader ? fileHeader : new List<SyntaxTrivia>();
585593
}
586594

587-
private static List<SyntaxTrivia> StripFileHeader(SyntaxTriviaList leadingTrivia)
588-
{
589-
var fileHeader = GetFileHeader(leadingTrivia);
590-
return leadingTrivia.Skip(fileHeader.Count).ToList();
591-
}
592-
593595
private List<UsingDirectiveSyntax> GenerateUsings(Dictionary<DirectiveSpan, List<UsingDirectiveSyntax>> usingsGroup, DirectiveSpan directiveSpan, string indentation, List<SyntaxTrivia> triviaToMove, bool qualifyNames)
594596
{
595597
List<UsingDirectiveSyntax> result = new List<UsingDirectiveSyntax>();
@@ -614,15 +616,29 @@ private List<UsingDirectiveSyntax> GenerateUsings(List<UsingDirectiveSyntax> usi
614616

615617
for (var i = 0; i < usingsList.Count; i++)
616618
{
619+
bool strippedFileHeader = false;
617620
var currentUsing = usingsList[i];
618621

619622
if (qualifyNames)
620623
{
621624
currentUsing = this.QualifyUsingDirective(currentUsing);
622625
}
623626

627+
// strip the file header, if the using is the first node in the source file.
628+
List<SyntaxTrivia> leadingTrivia;
629+
if ((i == 0) && currentUsing.GetFirstToken().GetPreviousToken().IsMissingOrDefault())
630+
{
631+
var trivia = currentUsing.GetLeadingTrivia();
632+
var fileHeader = GetFileHeader(trivia);
633+
leadingTrivia = trivia.Skip(fileHeader.Count).ToList();
634+
strippedFileHeader = fileHeader.Count > 0;
635+
}
636+
else
637+
{
638+
leadingTrivia = currentUsing.GetLeadingTrivia().ToList();
639+
}
640+
624641
// when there is a directive trivia, add it (and any trivia before it) to the triviaToMove collection.
625-
var leadingTrivia = (i == 0) ? StripFileHeader(currentUsing.GetLeadingTrivia()) : currentUsing.GetLeadingTrivia().ToList();
626642
for (var m = leadingTrivia.Count - 1; m >= 0; m--)
627643
{
628644
if (leadingTrivia[m].IsDirective)
@@ -689,6 +705,11 @@ private List<UsingDirectiveSyntax> GenerateUsings(List<UsingDirectiveSyntax> usi
689705
.WithTrailingTrivia(newTrailingTrivia)
690706
.WithAdditionalAnnotations(UsingCodeFixAnnotation);
691707

708+
if (strippedFileHeader)
709+
{
710+
processedUsing = processedUsing.WithAdditionalAnnotations(FileHeaderStrippedAnnotation);
711+
}
712+
692713
// filter duplicate using declarations, preferring to keep the one with an alias
693714
var existingUsing = result.Find(u => string.Equals(u.Name.ToNormalizedString(), processedUsing.Name.ToNormalizedString(), StringComparison.Ordinal));
694715
if (existingUsing != null)

StyleCop.Analyzers/StyleCop.Analyzers.Test/OrderingRules/SA1210UnitTests.cs

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -317,6 +317,101 @@ public async Task TestPreprocessorDirectivesAsync()
317317
await this.VerifyCSharpFixAsync(testCode, fixedTestCode).ConfigureAwait(false);
318318
}
319319

320+
/// <summary>
321+
/// This is a regression test for DotNetAnalyzers/StyleCopAnalyzers#1897.
322+
/// </summary>
323+
/// <returns>A <see cref="Task"/> representing the asynchronous unit test.</returns>
324+
[Fact]
325+
public async Task TestInvalidOrderedUsingDirectivesInNamespaceDeclarationWithFileHeaderAsync()
326+
{
327+
var testCode = @"// <copyright file=""VoiceCommandService.cs"" company=""Foo Corporation"">
328+
// Copyright (c) FooCorporation. All rights reserved.
329+
// </copyright>
330+
331+
namespace Foo.Garage.XYZ
332+
{
333+
using System;
334+
using Newtonsoft.Json;
335+
using Foo.Garage.XYZ;
336+
}
337+
338+
namespace Newtonsoft.Json
339+
{
340+
}
341+
";
342+
343+
var fixedTestCode = @"// <copyright file=""VoiceCommandService.cs"" company=""Foo Corporation"">
344+
// Copyright (c) FooCorporation. All rights reserved.
345+
// </copyright>
346+
347+
namespace Foo.Garage.XYZ
348+
{
349+
using System;
350+
using Foo.Garage.XYZ;
351+
using Newtonsoft.Json;
352+
}
353+
354+
namespace Newtonsoft.Json
355+
{
356+
}
357+
";
358+
359+
// The same diagnostic is reported multiple times due to a bug in Roslyn 1.0
360+
DiagnosticResult[] expected =
361+
{
362+
this.CSharpDiagnostic().WithLocation(8, 5),
363+
this.CSharpDiagnostic().WithLocation(8, 5),
364+
this.CSharpDiagnostic().WithLocation(8, 5),
365+
};
366+
367+
await this.VerifyCSharpDiagnosticAsync(testCode, expected, CancellationToken.None).ConfigureAwait(false);
368+
await this.VerifyCSharpDiagnosticAsync(fixedTestCode, EmptyDiagnosticResults, CancellationToken.None).ConfigureAwait(false);
369+
await this.VerifyCSharpFixAsync(testCode, fixedTestCode, cancellationToken: CancellationToken.None).ConfigureAwait(false);
370+
}
371+
372+
/// <summary>
373+
/// Verifies that the first using statement will preserve its leading comment.
374+
/// </summary>
375+
/// <returns>A <see cref="Task"/> representing the asynchronous unit test.</returns>
376+
[Fact]
377+
public async Task TestLeadingCommentForFirstUsingInNamespaceIsPreservedAsync()
378+
{
379+
var testCode = @"namespace TestNamespace
380+
{
381+
// With test comment
382+
using System;
383+
using TestNamespace;
384+
using Newtonsoft.Json;
385+
}
386+
387+
namespace Newtonsoft.Json
388+
{
389+
}
390+
";
391+
392+
var fixedTestCode = @"namespace TestNamespace
393+
{
394+
// With test comment
395+
using System;
396+
using Newtonsoft.Json;
397+
using TestNamespace;
398+
}
399+
400+
namespace Newtonsoft.Json
401+
{
402+
}
403+
";
404+
405+
DiagnosticResult[] expected =
406+
{
407+
this.CSharpDiagnostic().WithLocation(5, 5),
408+
};
409+
410+
await this.VerifyCSharpDiagnosticAsync(testCode, expected, CancellationToken.None).ConfigureAwait(false);
411+
await this.VerifyCSharpDiagnosticAsync(fixedTestCode, EmptyDiagnosticResults, CancellationToken.None).ConfigureAwait(false);
412+
await this.VerifyCSharpFixAsync(testCode, fixedTestCode, cancellationToken: CancellationToken.None).ConfigureAwait(false);
413+
}
414+
320415
/// <inheritdoc/>
321416
protected override IEnumerable<string> GetDisabledDiagnostics()
322417
{

0 commit comments

Comments
 (0)