Skip to content

Commit b279a88

Browse files
committed
Merge pull request #1967 from vweijsters/fix-1941
Improved file header handling for UsingCodeFixProvider
2 parents 74abaa9 + 15d0bab commit b279a88

2 files changed

Lines changed: 132 additions & 101 deletions

File tree

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

Lines changed: 93 additions & 101 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,6 @@ internal sealed class UsingCodeFixProvider : CodeFixProvider
2828
{
2929
private static readonly List<UsingDirectiveSyntax> EmptyUsingsList = new List<UsingDirectiveSyntax>();
3030
private static readonly SyntaxAnnotation UsingCodeFixAnnotation = new SyntaxAnnotation(nameof(UsingCodeFixAnnotation));
31-
private static readonly SyntaxAnnotation FileHeaderStrippedAnnotation = new SyntaxAnnotation(nameof(FileHeaderStrippedAnnotation));
3231

3332
/// <inheritdoc/>
3433
public override ImmutableArray<string> FixableDiagnosticIds { get; } =
@@ -73,13 +72,14 @@ public override async Task RegisterCodeFixesAsync(CodeFixContext context)
7372

7473
private static async Task<Document> GetTransformedDocumentAsync(Document document, SyntaxNode syntaxRoot, CancellationToken cancellationToken)
7574
{
75+
var fileHeader = GetFileHeader(syntaxRoot);
7676
var compilationUnit = (CompilationUnitSyntax)syntaxRoot;
7777

7878
var settings = SettingsHelper.GetStyleCopSettings(document.Project.AnalyzerOptions, cancellationToken);
7979
var semanticModel = await document.GetSemanticModelAsync(cancellationToken).ConfigureAwait(false);
8080
var indentationOptions = IndentationOptions.FromDocument(document);
8181

82-
var usingsHelper = new UsingsHelper(settings, semanticModel, document, compilationUnit);
82+
var usingsHelper = new UsingsHelper(settings, semanticModel, compilationUnit, fileHeader);
8383
var namespaceCount = CountNamespaces(compilationUnit.Members);
8484

8585
// Only move using declarations inside the namespace when
@@ -88,8 +88,7 @@ private static async Task<Document> GetTransformedDocumentAsync(Document documen
8888
// - OrderingSettings.UsingDirectivesPlacement is set to InsideNamespace
8989
UsingDirectivesPlacement usingDirectivesPlacement;
9090

91-
OrderingSettings orderingSettings = SettingsHelper.GetStyleCopSettings(document.Project.AnalyzerOptions, cancellationToken).OrderingRules;
92-
switch (orderingSettings.UsingDirectivesPlacement)
91+
switch (settings.OrderingRules.UsingDirectivesPlacement)
9392
{
9493
case UsingDirectivesPlacement.InsideNamespace:
9594
if (compilationUnit.AttributeLists.Any()
@@ -153,7 +152,7 @@ private static async Task<Document> GetTransformedDocumentAsync(Document documen
153152

154153
BuildReplaceMapForConditionalDirectives(usingsHelper, replaceMap, indentationOptions, usingsHelper.RootSpan);
155154

156-
var usingSyntaxRewriter = new UsingSyntaxRewriter(stripList, replaceMap);
155+
var usingSyntaxRewriter = new UsingSyntaxRewriter(stripList, replaceMap, fileHeader);
157156
var newSyntaxRoot = usingSyntaxRewriter.Visit(syntaxRoot);
158157

159158
if (usingDirectivesPlacement == UsingDirectivesPlacement.InsideNamespace)
@@ -167,39 +166,13 @@ private static async Task<Document> GetTransformedDocumentAsync(Document documen
167166

168167
// Final cleanup
169168
newSyntaxRoot = StripMultipleBlankLines(newSyntaxRoot);
170-
newSyntaxRoot = ReAddFileHeader(syntaxRoot, newSyntaxRoot);
169+
newSyntaxRoot = ReAddFileHeader(newSyntaxRoot, fileHeader);
171170

172171
var newDocument = document.WithSyntaxRoot(newSyntaxRoot.WithoutFormatting());
173172

174173
return newDocument;
175174
}
176175

177-
private static SyntaxNode ReAddFileHeader(SyntaxNode syntaxRoot, SyntaxNode newSyntaxRoot)
178-
{
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-
186-
var oldFirstToken = syntaxRoot.GetFirstToken();
187-
if (!oldFirstToken.HasLeadingTrivia)
188-
{
189-
return newSyntaxRoot;
190-
}
191-
192-
var fileHeader = UsingsHelper.GetFileHeader(oldFirstToken.LeadingTrivia);
193-
if (!fileHeader.Any())
194-
{
195-
return newSyntaxRoot;
196-
}
197-
198-
var newFirstToken = newSyntaxRoot.GetFirstToken();
199-
var newLeadingTrivia = newFirstToken.LeadingTrivia.InsertRange(0, fileHeader);
200-
return newSyntaxRoot.ReplaceToken(newFirstToken, newFirstToken.WithLeadingTrivia(newLeadingTrivia));
201-
}
202-
203176
private static int CountNamespaces(SyntaxList<MemberDeclarationSyntax> members)
204177
{
205178
var result = 0;
@@ -350,6 +323,73 @@ private static SyntaxNode StripMultipleBlankLines(SyntaxNode syntaxRoot)
350323
return newSyntaxRoot;
351324
}
352325

326+
private static ImmutableArray<SyntaxTrivia> GetFileHeader(SyntaxNode syntaxRoot)
327+
{
328+
var onBlankLine = false;
329+
var hasHeader = false;
330+
var fileHeaderBuilder = ImmutableArray.CreateBuilder<SyntaxTrivia>();
331+
332+
var firstToken = syntaxRoot.GetFirstToken(includeZeroWidth: true);
333+
var firstTokenLeadingTrivia = firstToken.LeadingTrivia;
334+
335+
int i;
336+
for (i = 0; i < firstTokenLeadingTrivia.Count; i++)
337+
{
338+
bool done = false;
339+
switch (firstTokenLeadingTrivia[i].Kind())
340+
{
341+
case SyntaxKind.SingleLineCommentTrivia:
342+
case SyntaxKind.MultiLineCommentTrivia:
343+
fileHeaderBuilder.Add(firstTokenLeadingTrivia[i]);
344+
onBlankLine = false;
345+
hasHeader = true;
346+
break;
347+
348+
case SyntaxKind.WhitespaceTrivia:
349+
fileHeaderBuilder.Add(firstTokenLeadingTrivia[i]);
350+
break;
351+
352+
case SyntaxKind.EndOfLineTrivia:
353+
fileHeaderBuilder.Add(firstTokenLeadingTrivia[i]);
354+
355+
if (onBlankLine)
356+
{
357+
done = true;
358+
}
359+
else
360+
{
361+
onBlankLine = true;
362+
}
363+
364+
break;
365+
366+
default:
367+
done = true;
368+
break;
369+
}
370+
371+
if (done)
372+
{
373+
break;
374+
}
375+
}
376+
377+
return hasHeader ? fileHeaderBuilder.ToImmutableArray() : ImmutableArray.Create<SyntaxTrivia>();
378+
}
379+
380+
private static SyntaxNode ReAddFileHeader(SyntaxNode syntaxRoot, ImmutableArray<SyntaxTrivia> fileHeader)
381+
{
382+
if (fileHeader.IsEmpty)
383+
{
384+
// Only re-add the file header if it was stripped.
385+
return syntaxRoot;
386+
}
387+
388+
var firstToken = syntaxRoot.GetFirstToken(includeZeroWidth: true);
389+
var newLeadingTrivia = firstToken.LeadingTrivia.InsertRange(0, fileHeader);
390+
return syntaxRoot.ReplaceToken(firstToken, firstToken.WithLeadingTrivia(newLeadingTrivia));
391+
}
392+
353393
private class DirectiveSpan
354394
{
355395
private List<DirectiveSpan> children = new List<DirectiveSpan>();
@@ -447,13 +487,16 @@ private class UsingsHelper
447487
{
448488
private readonly StyleCopSettings settings;
449489
private readonly SemanticModel semanticModel;
490+
private readonly ImmutableArray<SyntaxTrivia> fileHeader;
450491
private readonly DirectiveSpan conditionalDirectiveTree;
451492
private readonly bool separateSystemDirectives;
452493

453-
public UsingsHelper(StyleCopSettings settings, SemanticModel semanticModel, Document document, CompilationUnitSyntax compilationUnit)
494+
public UsingsHelper(StyleCopSettings settings, SemanticModel semanticModel, CompilationUnitSyntax compilationUnit, ImmutableArray<SyntaxTrivia> fileHeader)
454495
{
455496
this.settings = settings;
456497
this.semanticModel = semanticModel;
498+
this.fileHeader = fileHeader;
499+
457500
this.conditionalDirectiveTree = DirectiveSpan.BuildConditionalDirectiveTree(compilationUnit);
458501
this.separateSystemDirectives = settings.OrderingRules.SystemUsingDirectivesFirst;
459502

@@ -543,55 +586,6 @@ public SyntaxList<UsingDirectiveSyntax> GenerateGroupedUsings(List<UsingDirectiv
543586
return SyntaxFactory.List(usingList);
544587
}
545588

546-
internal static List<SyntaxTrivia> GetFileHeader(SyntaxTriviaList newLeadingTrivia)
547-
{
548-
var onBlankLine = false;
549-
var hasHeader = false;
550-
var fileHeader = new List<SyntaxTrivia>();
551-
for (var i = 0; i < newLeadingTrivia.Count; i++)
552-
{
553-
bool done = false;
554-
switch (newLeadingTrivia[i].Kind())
555-
{
556-
case SyntaxKind.SingleLineCommentTrivia:
557-
case SyntaxKind.MultiLineCommentTrivia:
558-
fileHeader.Add(newLeadingTrivia[i]);
559-
onBlankLine = false;
560-
hasHeader = true;
561-
break;
562-
563-
case SyntaxKind.WhitespaceTrivia:
564-
fileHeader.Add(newLeadingTrivia[i]);
565-
break;
566-
567-
case SyntaxKind.EndOfLineTrivia:
568-
fileHeader.Add(newLeadingTrivia[i]);
569-
570-
if (onBlankLine)
571-
{
572-
done = true;
573-
}
574-
else
575-
{
576-
onBlankLine = true;
577-
}
578-
579-
break;
580-
581-
default:
582-
done = true;
583-
break;
584-
}
585-
586-
if (done)
587-
{
588-
break;
589-
}
590-
}
591-
592-
return hasHeader ? fileHeader : new List<SyntaxTrivia>();
593-
}
594-
595589
private List<UsingDirectiveSyntax> GenerateUsings(Dictionary<DirectiveSpan, List<UsingDirectiveSyntax>> usingsGroup, DirectiveSpan directiveSpan, string indentation, List<SyntaxTrivia> triviaToMove, bool qualifyNames)
596590
{
597591
List<UsingDirectiveSyntax> result = new List<UsingDirectiveSyntax>();
@@ -616,22 +610,13 @@ private List<UsingDirectiveSyntax> GenerateUsings(List<UsingDirectiveSyntax> usi
616610

617611
for (var i = 0; i < usingsList.Count; i++)
618612
{
619-
bool strippedFileHeader = false;
620613
var currentUsing = usingsList[i];
621614

622-
if (qualifyNames)
623-
{
624-
currentUsing = this.QualifyUsingDirective(currentUsing);
625-
}
626-
627615
// strip the file header, if the using is the first node in the source file.
628616
List<SyntaxTrivia> leadingTrivia;
629617
if ((i == 0) && currentUsing.GetFirstToken().GetPreviousToken().IsMissingOrDefault())
630618
{
631-
var trivia = currentUsing.GetLeadingTrivia();
632-
var fileHeader = GetFileHeader(trivia);
633-
leadingTrivia = trivia.Skip(fileHeader.Count).ToList();
634-
strippedFileHeader = fileHeader.Count > 0;
619+
leadingTrivia = currentUsing.GetLeadingTrivia().Except(this.fileHeader).ToList();
635620
}
636621
else
637622
{
@@ -700,16 +685,11 @@ private List<UsingDirectiveSyntax> GenerateUsings(List<UsingDirectiveSyntax> usi
700685
newTrailingTrivia = newTrailingTrivia.Add(SyntaxFactory.CarriageReturnLineFeed);
701686
}
702687

703-
var processedUsing = currentUsing
688+
var processedUsing = (qualifyNames ? this.QualifyUsingDirective(currentUsing) : currentUsing)
704689
.WithLeadingTrivia(newLeadingTrivia)
705690
.WithTrailingTrivia(newTrailingTrivia)
706691
.WithAdditionalAnnotations(UsingCodeFixAnnotation);
707692

708-
if (strippedFileHeader)
709-
{
710-
processedUsing = processedUsing.WithAdditionalAnnotations(FileHeaderStrippedAnnotation);
711-
}
712-
713693
result.Add(processedUsing);
714694
}
715695

@@ -899,14 +879,16 @@ private List<UsingDirectiveSyntax> FilterRelevantUsings(Dictionary<DirectiveSpan
899879

900880
private class UsingSyntaxRewriter : CSharpSyntaxRewriter
901881
{
902-
private List<UsingDirectiveSyntax> stripList;
903-
private Dictionary<UsingDirectiveSyntax, UsingDirectiveSyntax> replaceMap;
882+
private readonly List<UsingDirectiveSyntax> stripList;
883+
private readonly Dictionary<UsingDirectiveSyntax, UsingDirectiveSyntax> replaceMap;
884+
private readonly ImmutableArray<SyntaxTrivia> fileHeader;
904885
private LinkedList<SyntaxToken> tokensToStrip = new LinkedList<SyntaxToken>();
905886

906-
public UsingSyntaxRewriter(List<UsingDirectiveSyntax> stripList, Dictionary<UsingDirectiveSyntax, UsingDirectiveSyntax> replaceMap)
887+
public UsingSyntaxRewriter(List<UsingDirectiveSyntax> stripList, Dictionary<UsingDirectiveSyntax, UsingDirectiveSyntax> replaceMap, ImmutableArray<SyntaxTrivia> fileHeader)
907888
{
908889
this.stripList = stripList;
909890
this.replaceMap = replaceMap;
891+
this.fileHeader = fileHeader;
910892
}
911893

912894
public override SyntaxNode VisitUsingDirective(UsingDirectiveSyntax node)
@@ -951,6 +933,16 @@ public override SyntaxToken VisitToken(SyntaxToken token)
951933

952934
return base.VisitToken(token);
953935
}
936+
937+
public override SyntaxTrivia VisitTrivia(SyntaxTrivia trivia)
938+
{
939+
if (this.fileHeader.Contains(trivia))
940+
{
941+
return default(SyntaxTrivia);
942+
}
943+
944+
return base.VisitTrivia(trivia);
945+
}
954946
}
955947

956948
private class FixAll : DocumentBasedFixAllProvider

StyleCop.Analyzers/StyleCop.Analyzers.Test/OrderingRules/SA1200OutsideNamespaceUnitTests.cs

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,9 @@ public class SA1200OutsideNamespaceUnitTests : CodeFixVerifier
2424
""settings"": {
2525
""orderingRules"": {
2626
""usingDirectivesPlacement"": ""outsideNamespace""
27+
},
28+
""documentationRules"": {
29+
""xmlHeader"": false
2730
}
2831
}
2932
}
@@ -227,6 +230,42 @@ namespace TestNamespace
227230
await this.VerifyCSharpDiagnosticAsync(testCode, EmptyDiagnosticResults, CancellationToken.None).ConfigureAwait(false);
228231
}
229232

233+
/// <summary>
234+
/// Verifies that the file header of a file is properly preserved when moving using statements out of a namespace.
235+
/// This is a regression test for #1941.
236+
/// </summary>
237+
/// <returns>A <see cref="Task"/> representing the asynchronous unit test.</returns>
238+
[Fact]
239+
public async Task TestFileHeaderIsProperlyPreservedWhenMovingUsingStatementsAsync()
240+
{
241+
var testCode = @"// Copyright (c) Tunnel Vision Laboratories, LLC. All Rights Reserved.
242+
// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information.
243+
244+
namespace TestNamespace
245+
{
246+
using System;
247+
}
248+
";
249+
var fixedTestCode = @"// Copyright (c) Tunnel Vision Laboratories, LLC. All Rights Reserved.
250+
// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information.
251+
252+
using System;
253+
254+
namespace TestNamespace
255+
{
256+
}
257+
";
258+
259+
DiagnosticResult[] expectedResults =
260+
{
261+
this.CSharpDiagnostic(SA1200UsingDirectivesMustBePlacedCorrectly.DescriptorOutside).WithLocation(6, 5),
262+
};
263+
264+
await this.VerifyCSharpDiagnosticAsync(testCode, expectedResults, CancellationToken.None).ConfigureAwait(false);
265+
await this.VerifyCSharpDiagnosticAsync(fixedTestCode, EmptyDiagnosticResults, CancellationToken.None).ConfigureAwait(false);
266+
await this.VerifyCSharpFixAsync(testCode, fixedTestCode).ConfigureAwait(false);
267+
}
268+
230269
/// <inheritdoc/>
231270
protected override string GetSettings()
232271
{

0 commit comments

Comments
 (0)