@@ -28,6 +28,7 @@ namespace StyleCop.Analyzers.OrderingRules
2828 internal sealed class UsingCodeFixProvider : CodeFixProvider
2929 {
3030 private static readonly List < UsingDirectiveSyntax > EmptyUsingsList = new List < UsingDirectiveSyntax > ( ) ;
31+ private static readonly SyntaxAnnotation UsingCodeFixAnnotation = new SyntaxAnnotation ( nameof ( UsingCodeFixProvider ) ) ;
3132
3233 /// <inheritdoc/>
3334 public override ImmutableArray < string > FixableDiagnosticIds { get ; } =
@@ -91,12 +92,16 @@ private static async Task<Document> GetTransformedDocumentAsync(Document documen
9192 {
9293 case UsingDirectivesPlacement . InsideNamespace :
9394 if ( compilationUnit . AttributeLists . Any ( )
94- || compilationUnit . Members . Count != 1
95- || namespaceCount != 1 )
95+ || compilationUnit . Members . Count > 1
96+ || namespaceCount > 1 )
9697 {
9798 // Override the user's setting with a more conservative one
9899 usingDirectivesPlacement = UsingDirectivesPlacement . Preserve ;
99100 }
101+ else if ( namespaceCount == 0 )
102+ {
103+ usingDirectivesPlacement = UsingDirectivesPlacement . OutsideNamespace ;
104+ }
100105 else
101106 {
102107 usingDirectivesPlacement = UsingDirectivesPlacement . InsideNamespace ;
@@ -159,6 +164,8 @@ private static async Task<Document> GetTransformedDocumentAsync(Document documen
159164 newSyntaxRoot = AddUsingsToCompilationRoot ( newSyntaxRoot , usingsHelper , usingsIndentation , replaceMap . Any ( ) ) ;
160165 }
161166
167+ // Final cleanup
168+ newSyntaxRoot = StripMultipleBlankLines ( newSyntaxRoot ) ;
162169 newSyntaxRoot = ReAddFileHeader ( syntaxRoot , newSyntaxRoot ) ;
163170
164171 var newDocument = document . WithSyntaxRoot ( newSyntaxRoot . WithoutFormatting ( ) ) ;
@@ -174,7 +181,7 @@ private static SyntaxNode ReAddFileHeader(SyntaxNode syntaxRoot, SyntaxNode newS
174181 return newSyntaxRoot ;
175182 }
176183
177- var fileHeader = UsingsHelper . GetFileHeader ( oldFirstToken . LeadingTrivia . ToList ( ) ) ;
184+ var fileHeader = UsingsHelper . GetFileHeader ( oldFirstToken . LeadingTrivia ) ;
178185 if ( ! fileHeader . Any ( ) )
179186 {
180187 return newSyntaxRoot ;
@@ -271,9 +278,10 @@ private static SyntaxNode AddUsingsToNamespace(SyntaxNode newSyntaxRoot, UsingsH
271278
272279 var groupedUsings = usingsHelper . GenerateGroupedUsings ( usingsHelper . RootSpan , usingsIndentation , withTrailingBlankLine , qualifyNames : false ) ;
273280 groupedUsings = groupedUsings . AddRange ( rootNamespace . Usings ) ;
274- var newRootNamespace = rootNamespace . WithUsings ( groupedUsings ) ;
275281
282+ var newRootNamespace = rootNamespace . WithUsings ( groupedUsings ) ;
276283 newSyntaxRoot = newSyntaxRoot . ReplaceNode ( rootNamespace , newRootNamespace ) ;
284+
277285 return newSyntaxRoot ;
278286 }
279287
@@ -285,6 +293,52 @@ private static SyntaxNode AddUsingsToCompilationRoot(SyntaxNode newSyntaxRoot, U
285293 var groupedUsings = usingsHelper . GenerateGroupedUsings ( usingsHelper . RootSpan , usingsIndentation , withTrailingBlankLine , qualifyNames : true ) ;
286294 groupedUsings = groupedUsings . AddRange ( newCompilationUnit . Usings ) ;
287295 newSyntaxRoot = newCompilationUnit . WithUsings ( groupedUsings ) ;
296+
297+ return newSyntaxRoot ;
298+ }
299+
300+ private static SyntaxNode StripMultipleBlankLines ( SyntaxNode syntaxRoot )
301+ {
302+ var replaceMap = new Dictionary < SyntaxToken , SyntaxToken > ( ) ;
303+
304+ var usingDirectives = syntaxRoot . GetAnnotatedNodes ( UsingCodeFixAnnotation ) . Cast < UsingDirectiveSyntax > ( ) ;
305+
306+ foreach ( var usingDirective in usingDirectives )
307+ {
308+ var nextToken = usingDirective . SemicolonToken . GetNextToken ( true ) ;
309+
310+ // start at -1 to compensate for the always present end-of-line.
311+ var count = - 1 ;
312+
313+ // count the blanks lines at the end of the using statement.
314+ foreach ( var trivia in usingDirective . SemicolonToken . TrailingTrivia . Reverse ( ) )
315+ {
316+ if ( ! trivia . IsKind ( SyntaxKind . EndOfLineTrivia ) )
317+ {
318+ break ;
319+ }
320+
321+ count ++ ;
322+ }
323+
324+ // count the blank lines at the start of the next token
325+ foreach ( var trivia in nextToken . LeadingTrivia )
326+ {
327+ if ( ! trivia . IsKind ( SyntaxKind . EndOfLineTrivia ) )
328+ {
329+ break ;
330+ }
331+
332+ count ++ ;
333+ }
334+
335+ if ( count > 1 )
336+ {
337+ replaceMap [ nextToken ] = nextToken . WithLeadingTrivia ( nextToken . LeadingTrivia . Skip ( count - 1 ) ) ;
338+ }
339+ }
340+
341+ var newSyntaxRoot = syntaxRoot . ReplaceTokens ( replaceMap . Keys , ( original , rewritten ) => replaceMap [ original ] ) ;
288342 return newSyntaxRoot ;
289343 }
290344
@@ -479,7 +533,7 @@ public SyntaxList<UsingDirectiveSyntax> GenerateGroupedUsings(List<UsingDirectiv
479533 return SyntaxFactory . List ( usingList ) ;
480534 }
481535
482- internal static List < SyntaxTrivia > GetFileHeader ( List < SyntaxTrivia > newLeadingTrivia )
536+ internal static List < SyntaxTrivia > GetFileHeader ( SyntaxTriviaList newLeadingTrivia )
483537 {
484538 var onBlankLine = false ;
485539 var hasHeader = false ;
@@ -528,10 +582,10 @@ internal static List<SyntaxTrivia> GetFileHeader(List<SyntaxTrivia> newLeadingTr
528582 return hasHeader ? fileHeader : new List < SyntaxTrivia > ( ) ;
529583 }
530584
531- private static List < SyntaxTrivia > StripFileHeader ( List < SyntaxTrivia > newLeadingTrivia )
585+ private static List < SyntaxTrivia > StripFileHeader ( SyntaxTriviaList leadingTrivia )
532586 {
533- var fileHeader = GetFileHeader ( newLeadingTrivia ) ;
534- return newLeadingTrivia . Skip ( fileHeader . Count ) . ToList ( ) ;
587+ var fileHeader = GetFileHeader ( leadingTrivia ) ;
588+ return leadingTrivia . Skip ( fileHeader . Count ) . ToList ( ) ;
535589 }
536590
537591 private List < UsingDirectiveSyntax > GenerateUsings ( Dictionary < DirectiveSpan , List < UsingDirectiveSyntax > > usingsGroup , DirectiveSpan directiveSpan , string indentation , List < SyntaxTrivia > triviaToMove , bool qualifyNames )
@@ -636,19 +690,20 @@ private List<UsingDirectiveSyntax> GenerateUsings(List<UsingDirectiveSyntax> usi
636690 } ) ;
637691 }
638692
639- triviaToMove . AddRange ( currentUsing . GetLeadingTrivia ( ) . Where ( tr => tr . IsDirective || tr . IsKind ( SyntaxKind . DisabledTextTrivia ) ) ) ;
640-
641- // preserve leading trivia (excluding directive trivia), indenting each line as appropriate
642- var newLeadingTrivia = currentUsing
643- . GetLeadingTrivia ( )
644- . Where ( tr => ! tr . IsDirective && ! tr . IsKind ( SyntaxKind . DisabledTextTrivia ) )
645- . ToList ( ) ;
646-
647- if ( i == 0 )
693+ // when there is a directive trivia, add it (and any trivia before it) to the triviaToMove collection.
694+ var leadingTrivia = ( i == 0 ) ? StripFileHeader ( currentUsing . GetLeadingTrivia ( ) ) : currentUsing . GetLeadingTrivia ( ) . ToList ( ) ;
695+ for ( var m = leadingTrivia . Count - 1 ; m >= 0 ; m -- )
648696 {
649- newLeadingTrivia = StripFileHeader ( newLeadingTrivia ) ;
697+ if ( leadingTrivia [ m ] . IsDirective )
698+ {
699+ triviaToMove . AddRange ( leadingTrivia . Take ( m + 1 ) ) ;
700+ break ;
701+ }
650702 }
651703
704+ // preserve leading trivia (excluding directive trivia), indenting each line as appropriate
705+ var newLeadingTrivia = leadingTrivia . Except ( triviaToMove ) . ToList ( ) ;
706+
652707 // strip any leading whitespace on each line (and also all blank lines)
653708 var k = 0 ;
654709 var startOfLine = true ;
@@ -698,7 +753,10 @@ private List<UsingDirectiveSyntax> GenerateUsings(List<UsingDirectiveSyntax> usi
698753 newTrailingTrivia = newTrailingTrivia . Add ( SyntaxFactory . CarriageReturnLineFeed ) ;
699754 }
700755
701- var processedUsing = currentUsing . WithLeadingTrivia ( newLeadingTrivia ) . WithTrailingTrivia ( newTrailingTrivia ) ;
756+ var processedUsing = currentUsing
757+ . WithLeadingTrivia ( newLeadingTrivia )
758+ . WithTrailingTrivia ( newTrailingTrivia )
759+ . WithAdditionalAnnotations ( UsingCodeFixAnnotation ) ;
702760
703761 // filter duplicate using declarations, preferring to keep the one with an alias
704762 var existingUsing = result . Find ( u => string . Equals ( u . Name . ToUnaliasedString ( ) , processedUsing . Name . ToUnaliasedString ( ) , StringComparison . Ordinal ) ) ;
0 commit comments