Skip to content

Commit 6b7717a

Browse files
committed
Implement full sort handling for global using directives
1 parent 50e541d commit 6b7717a

File tree

5 files changed

+140
-40
lines changed

5 files changed

+140
-40
lines changed

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

Lines changed: 60 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -92,16 +92,39 @@ public List<UsingDirectiveSyntax> GetContainedUsings(TreeTextSpan directiveSpan)
9292
return result;
9393
}
9494

95-
public SyntaxList<UsingDirectiveSyntax> GenerateGroupedUsings(TreeTextSpan directiveSpan, string indentation, bool withLeadingBlankLine, bool withTrailingBlankLine, bool qualifyNames)
95+
public SyntaxList<UsingDirectiveSyntax> GenerateGroupedUsings(TreeTextSpan directiveSpan, string indentation, bool withLeadingBlankLine, bool withTrailingBlankLine, bool qualifyNames, bool includeGlobal, bool includeLocal)
9696
{
9797
var usingList = new List<UsingDirectiveSyntax>();
9898
List<SyntaxTrivia> triviaToMove = new List<SyntaxTrivia>();
99+
int lastGlobalDirective = -1;
99100

100-
usingList.AddRange(this.GenerateUsings(this.systemUsings, directiveSpan, indentation, triviaToMove, qualifyNames));
101-
usingList.AddRange(this.GenerateUsings(this.namespaceUsings, directiveSpan, indentation, triviaToMove, qualifyNames));
102-
usingList.AddRange(this.GenerateUsings(this.systemStaticImports, directiveSpan, indentation, triviaToMove, qualifyNames));
103-
usingList.AddRange(this.GenerateUsings(this.staticImports, directiveSpan, indentation, triviaToMove, qualifyNames));
104-
usingList.AddRange(this.GenerateUsings(this.aliases, directiveSpan, indentation, triviaToMove, qualifyNames));
101+
if (includeGlobal)
102+
{
103+
usingList.AddRange(this.GenerateUsings(this.systemUsings, directiveSpan, indentation, triviaToMove, qualifyNames, isGlobal: true));
104+
usingList.AddRange(this.GenerateUsings(this.namespaceUsings, directiveSpan, indentation, triviaToMove, qualifyNames, isGlobal: true));
105+
usingList.AddRange(this.GenerateUsings(this.systemStaticImports, directiveSpan, indentation, triviaToMove, qualifyNames, isGlobal: true));
106+
usingList.AddRange(this.GenerateUsings(this.staticImports, directiveSpan, indentation, triviaToMove, qualifyNames, isGlobal: true));
107+
usingList.AddRange(this.GenerateUsings(this.aliases, directiveSpan, indentation, triviaToMove, qualifyNames, isGlobal: true));
108+
lastGlobalDirective = usingList.Count - 1;
109+
}
110+
111+
if (includeLocal)
112+
{
113+
usingList.AddRange(this.GenerateUsings(this.systemUsings, directiveSpan, indentation, triviaToMove, qualifyNames, isGlobal: false));
114+
usingList.AddRange(this.GenerateUsings(this.namespaceUsings, directiveSpan, indentation, triviaToMove, qualifyNames, isGlobal: false));
115+
usingList.AddRange(this.GenerateUsings(this.systemStaticImports, directiveSpan, indentation, triviaToMove, qualifyNames, isGlobal: false));
116+
usingList.AddRange(this.GenerateUsings(this.staticImports, directiveSpan, indentation, triviaToMove, qualifyNames, isGlobal: false));
117+
usingList.AddRange(this.GenerateUsings(this.aliases, directiveSpan, indentation, triviaToMove, qualifyNames, isGlobal: false));
118+
}
119+
120+
if (!this.insertBlankLinesBetweenGroups && lastGlobalDirective >= 0 && lastGlobalDirective < usingList.Count - 1)
121+
{
122+
// Need to ensure there is a blank line after the global usings so they are separated from the local
123+
// usings
124+
var last = usingList[lastGlobalDirective];
125+
126+
usingList[lastGlobalDirective] = last.WithTrailingTrivia(last.GetTrailingTrivia().Add(SyntaxFactory.CarriageReturnLineFeed));
127+
}
105128

106129
if (triviaToMove.Count > 0)
107130
{
@@ -129,11 +152,27 @@ public SyntaxList<UsingDirectiveSyntax> GenerateGroupedUsings(List<UsingDirectiv
129152
var usingList = new List<UsingDirectiveSyntax>();
130153
List<SyntaxTrivia> triviaToMove = new List<SyntaxTrivia>();
131154

132-
usingList.AddRange(this.GenerateUsings(this.systemUsings, usingsList, indentation, triviaToMove, qualifyNames));
133-
usingList.AddRange(this.GenerateUsings(this.namespaceUsings, usingsList, indentation, triviaToMove, qualifyNames));
134-
usingList.AddRange(this.GenerateUsings(this.systemStaticImports, usingsList, indentation, triviaToMove, qualifyNames));
135-
usingList.AddRange(this.GenerateUsings(this.staticImports, usingsList, indentation, triviaToMove, qualifyNames));
136-
usingList.AddRange(this.GenerateUsings(this.aliases, usingsList, indentation, triviaToMove, qualifyNames));
155+
usingList.AddRange(this.GenerateUsings(this.systemUsings, usingsList, indentation, triviaToMove, qualifyNames, isGlobal: true));
156+
usingList.AddRange(this.GenerateUsings(this.namespaceUsings, usingsList, indentation, triviaToMove, qualifyNames, isGlobal: true));
157+
usingList.AddRange(this.GenerateUsings(this.systemStaticImports, usingsList, indentation, triviaToMove, qualifyNames, isGlobal: true));
158+
usingList.AddRange(this.GenerateUsings(this.staticImports, usingsList, indentation, triviaToMove, qualifyNames, isGlobal: true));
159+
usingList.AddRange(this.GenerateUsings(this.aliases, usingsList, indentation, triviaToMove, qualifyNames, isGlobal: true));
160+
int lastGlobalDirective = usingList.Count - 1;
161+
162+
usingList.AddRange(this.GenerateUsings(this.systemUsings, usingsList, indentation, triviaToMove, qualifyNames, isGlobal: false));
163+
usingList.AddRange(this.GenerateUsings(this.namespaceUsings, usingsList, indentation, triviaToMove, qualifyNames, isGlobal: false));
164+
usingList.AddRange(this.GenerateUsings(this.systemStaticImports, usingsList, indentation, triviaToMove, qualifyNames, isGlobal: false));
165+
usingList.AddRange(this.GenerateUsings(this.staticImports, usingsList, indentation, triviaToMove, qualifyNames, isGlobal: false));
166+
usingList.AddRange(this.GenerateUsings(this.aliases, usingsList, indentation, triviaToMove, qualifyNames, isGlobal: false));
167+
168+
if (!this.insertBlankLinesBetweenGroups && lastGlobalDirective >= 0 && lastGlobalDirective < usingList.Count - 1)
169+
{
170+
// Need to ensure there is a blank line after the global usings so they are separated from the local
171+
// usings
172+
var last = usingList[lastGlobalDirective];
173+
174+
usingList[lastGlobalDirective] = last.WithTrailingTrivia(last.GetTrailingTrivia().Add(SyntaxFactory.CarriageReturnLineFeed));
175+
}
137176

138177
if (triviaToMove.Count > 0)
139178
{
@@ -156,7 +195,7 @@ public SyntaxList<UsingDirectiveSyntax> GenerateGroupedUsings(List<UsingDirectiv
156195
return SyntaxFactory.List(usingList);
157196
}
158197

159-
private List<UsingDirectiveSyntax> GenerateUsings(Dictionary<TreeTextSpan, List<UsingDirectiveSyntax>> usingsGroup, TreeTextSpan directiveSpan, string indentation, List<SyntaxTrivia> triviaToMove, bool qualifyNames)
198+
private List<UsingDirectiveSyntax> GenerateUsings(Dictionary<TreeTextSpan, List<UsingDirectiveSyntax>> usingsGroup, TreeTextSpan directiveSpan, string indentation, List<SyntaxTrivia> triviaToMove, bool qualifyNames, bool isGlobal)
160199
{
161200
List<UsingDirectiveSyntax> result = new List<UsingDirectiveSyntax>();
162201
List<UsingDirectiveSyntax> usingsList;
@@ -166,10 +205,10 @@ private List<UsingDirectiveSyntax> GenerateUsings(Dictionary<TreeTextSpan, List<
166205
return result;
167206
}
168207

169-
return this.GenerateUsings(usingsList, indentation, triviaToMove, qualifyNames);
208+
return this.GenerateUsings(usingsList, indentation, triviaToMove, qualifyNames, isGlobal);
170209
}
171210

172-
private List<UsingDirectiveSyntax> GenerateUsings(List<UsingDirectiveSyntax> usingsList, string indentation, List<SyntaxTrivia> triviaToMove, bool qualifyNames)
211+
private List<UsingDirectiveSyntax> GenerateUsings(List<UsingDirectiveSyntax> usingsList, string indentation, List<SyntaxTrivia> triviaToMove, bool qualifyNames, bool isGlobal)
173212
{
174213
List<UsingDirectiveSyntax> result = new List<UsingDirectiveSyntax>();
175214

@@ -181,6 +220,10 @@ private List<UsingDirectiveSyntax> GenerateUsings(List<UsingDirectiveSyntax> usi
181220
for (var i = 0; i < usingsList.Count; i++)
182221
{
183222
var currentUsing = usingsList[i];
223+
if (currentUsing.GlobalKeyword().IsKind(SyntaxKind.GlobalKeyword) != isGlobal)
224+
{
225+
continue;
226+
}
184227

185228
// strip the file header, if the using is the first node in the source file.
186229
List<SyntaxTrivia> leadingTrivia;
@@ -335,7 +378,7 @@ private List<UsingDirectiveSyntax> GenerateUsings(List<UsingDirectiveSyntax> usi
335378

336379
result.Sort(this.CompareUsings);
337380

338-
if (this.insertBlankLinesBetweenGroups)
381+
if (this.insertBlankLinesBetweenGroups && result.Count > 0)
339382
{
340383
var last = result[result.Count - 1];
341384

@@ -533,11 +576,11 @@ private void AddUsingDirective(Dictionary<TreeTextSpan, List<UsingDirectiveSynta
533576
usingList.Add(usingDirective);
534577
}
535578

536-
private List<UsingDirectiveSyntax> GenerateUsings(Dictionary<TreeTextSpan, List<UsingDirectiveSyntax>> usingsGroup, List<UsingDirectiveSyntax> usingsList, string indentation, List<SyntaxTrivia> triviaToMove, bool qualifyNames)
579+
private List<UsingDirectiveSyntax> GenerateUsings(Dictionary<TreeTextSpan, List<UsingDirectiveSyntax>> usingsGroup, List<UsingDirectiveSyntax> usingsList, string indentation, List<SyntaxTrivia> triviaToMove, bool qualifyNames, bool isGlobal)
537580
{
538581
var filteredUsingsList = this.FilterRelevantUsings(usingsGroup, usingsList);
539582

540-
return this.GenerateUsings(filteredUsingsList, indentation, triviaToMove, qualifyNames);
583+
return this.GenerateUsings(filteredUsingsList, indentation, triviaToMove, qualifyNames, isGlobal);
541584
}
542585

543586
private List<UsingDirectiveSyntax> FilterRelevantUsings(Dictionary<TreeTextSpan, List<UsingDirectiveSyntax>> usingsGroup, List<UsingDirectiveSyntax> usingsList)

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

Lines changed: 12 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -70,9 +70,8 @@ public override async Task RegisterCodeFixesAsync(CodeFixContext context)
7070
}
7171

7272
// Force preserving the placement of using directives when we are fixing a diagnostic not directly
73-
// related to placement of using directives inside/outside namespaces, and also when there are global
74-
// usings present.
75-
bool forcePreservePlacement = !isSA1200 || compilationUnit.Usings.Any(static syntax => syntax.GlobalKeyword().IsKind(SyntaxKind.GlobalKeyword));
73+
// related to placement of using directives inside/outside namespaces.
74+
bool forcePreservePlacement = !isSA1200;
7675

7776
context.RegisterCodeFix(
7877
CodeAction.Create(
@@ -122,9 +121,11 @@ private static async Task<Document> GetTransformedDocumentAsync(Document documen
122121
{
123122
newSyntaxRoot = AddUsingsToNamespace(newSyntaxRoot, usingsHelper, usingsIndentation, replaceMap.Any());
124123
}
125-
else if (usingDirectivesPlacement == UsingDirectivesPlacement.OutsideNamespace)
124+
125+
if (usingDirectivesPlacement != UsingDirectivesPlacement.Preserve)
126126
{
127-
newSyntaxRoot = AddUsingsToCompilationRoot(newSyntaxRoot, usingsHelper, usingsIndentation, replaceMap.Any());
127+
bool onlyGlobal = usingDirectivesPlacement == UsingDirectivesPlacement.InsideNamespace;
128+
newSyntaxRoot = AddUsingsToCompilationRoot(newSyntaxRoot, usingsHelper, replaceMap.Any(), onlyGlobal);
128129
}
129130

130131
// Final cleanup
@@ -262,7 +263,7 @@ private static void BuildReplaceMapForConditionalDirectives(UsingsSorter usingsH
262263

263264
var indentation = IndentationHelper.GenerateIndentationString(indentationSettings, indentationSteps);
264265

265-
var modifiedUsings = usingsHelper.GenerateGroupedUsings(childSpan, indentation, false, false, qualifyNames: false);
266+
var modifiedUsings = usingsHelper.GenerateGroupedUsings(childSpan, indentation, false, false, qualifyNames: false, includeGlobal: true, includeLocal: true);
266267

267268
for (var i = 0; i < originalUsings.Count; i++)
268269
{
@@ -285,7 +286,7 @@ private static SyntaxNode AddUsingsToNamespace(SyntaxNode newSyntaxRoot, UsingsS
285286
var withLeadingBlankLine = rootNamespace.SyntaxNode.IsKind(SyntaxKindEx.FileScopedNamespaceDeclaration);
286287
var withTrailingBlankLine = hasConditionalDirectives || rootNamespace.Members.Any() || rootNamespace.Externs.Any();
287288

288-
var groupedUsings = usingsHelper.GenerateGroupedUsings(TreeTextSpan.Empty, usingsIndentation, withLeadingBlankLine, withTrailingBlankLine, qualifyNames: false);
289+
var groupedUsings = usingsHelper.GenerateGroupedUsings(TreeTextSpan.Empty, usingsIndentation, withLeadingBlankLine, withTrailingBlankLine, qualifyNames: false, includeGlobal: false, includeLocal: true);
289290
groupedUsings = groupedUsings.AddRange(rootNamespace.Usings);
290291

291292
var newRootNamespace = rootNamespace.WithUsings(groupedUsings);
@@ -294,12 +295,12 @@ private static SyntaxNode AddUsingsToNamespace(SyntaxNode newSyntaxRoot, UsingsS
294295
return newSyntaxRoot;
295296
}
296297

297-
private static SyntaxNode AddUsingsToCompilationRoot(SyntaxNode newSyntaxRoot, UsingsSorter usingsHelper, string usingsIndentation, bool hasConditionalDirectives)
298+
private static SyntaxNode AddUsingsToCompilationRoot(SyntaxNode newSyntaxRoot, UsingsSorter usingsHelper, bool hasConditionalDirectives, bool onlyGlobal)
298299
{
299300
var newCompilationUnit = (CompilationUnitSyntax)newSyntaxRoot;
300301
var withTrailingBlankLine = hasConditionalDirectives || newCompilationUnit.AttributeLists.Any() || newCompilationUnit.Members.Any() || newCompilationUnit.Externs.Any();
301302

302-
var groupedUsings = usingsHelper.GenerateGroupedUsings(TreeTextSpan.Empty, usingsIndentation, withLeadingBlankLine: false, withTrailingBlankLine, qualifyNames: true);
303+
var groupedUsings = usingsHelper.GenerateGroupedUsings(TreeTextSpan.Empty, indentation: string.Empty, withLeadingBlankLine: false, withTrailingBlankLine, qualifyNames: true, includeGlobal: true, includeLocal: !onlyGlobal);
303304
groupedUsings = groupedUsings.AddRange(newCompilationUnit.Usings);
304305
newSyntaxRoot = newCompilationUnit.WithUsings(groupedUsings);
305306

@@ -521,10 +522,8 @@ protected override async Task<SyntaxNode> FixAllInDocumentAsync(FixAllContext fi
521522
var syntaxRoot = await document.GetSyntaxRootAsync(fixAllContext.CancellationToken).ConfigureAwait(false);
522523

523524
// Force preserving the placement of using directives when we are fixing any diagnostic not directly
524-
// related to placement of using directives inside/outside namespaces, and also when there are global
525-
// usings present.
526-
var forcePreserve = diagnostics.All(d => d.Id != SA1200UsingDirectivesMustBePlacedCorrectly.DiagnosticId)
527-
|| ((CompilationUnitSyntax)syntaxRoot).Usings.Any(static syntax => syntax.GlobalKeyword().IsKind(SyntaxKind.GlobalKeyword));
525+
// related to placement of using directives inside/outside namespaces.
526+
var forcePreserve = diagnostics.All(d => d.Id != SA1200UsingDirectivesMustBePlacedCorrectly.DiagnosticId);
528527

529528
Document newDocument = await GetTransformedDocumentAsync(document, syntaxRoot, forcePreserve, fixAllContext.CancellationToken).ConfigureAwait(false);
530529
return await newDocument.GetSyntaxRootAsync(fixAllContext.CancellationToken).ConfigureAwait(false);

StyleCop.Analyzers/StyleCop.Analyzers.Test.CSharp10/OrderingRules/SA1200CSharp10UnitTests.cs

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -69,22 +69,20 @@ namespace TestNamespace
6969
public async Task TestGlobalUsingStatementInFileWithOtherUsingDirectivesAsync()
7070
{
7171
var testCode = @"global using System;
72+
7273
[|using System.Linq;|]
7374
7475
namespace TestNamespace
7576
{
7677
}";
78+
var fixedTestCode = @"global using System;
7779
78-
await new CSharpTest
79-
{
80-
TestCode = testCode,
81-
82-
// UsingCodeFixProvider currently leaves all using directives in the same location (either inside or
83-
// outside the namespace) when the file contains any global using directives.
84-
FixedCode = testCode,
85-
NumberOfIncrementalIterations = 1,
86-
NumberOfFixAllIterations = 1,
87-
}.RunAsync(CancellationToken.None).ConfigureAwait(false);
80+
namespace TestNamespace
81+
{
82+
using System.Linq;
83+
}";
84+
85+
await VerifyCSharpFixAsync(testCode, DiagnosticResult.EmptyDiagnosticResults, fixedTestCode, CancellationToken.None).ConfigureAwait(false);
8886
}
8987
}
9088
}

StyleCop.Analyzers/StyleCop.Analyzers.Test.CSharp10/OrderingRules/SA1200OutsideNamespaceCSharp10UnitTests.cs

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,5 +46,49 @@ public async Task TestOnlyGlobalUsingStatementInFileAsync(string leadingTrivia)
4646

4747
await VerifyCSharpDiagnosticAsync(testCode, DiagnosticResult.EmptyDiagnosticResults, CancellationToken.None).ConfigureAwait(false);
4848
}
49+
50+
[Fact]
51+
[WorkItem(3875, "https://github.com/DotNetAnalyzers/StyleCopAnalyzers/issues/3875")]
52+
public async Task TestGlobalUsingStatementInFileWithOtherUsingDirectivesAsync()
53+
{
54+
var testCode = @"global using System;
55+
56+
namespace TestNamespace
57+
{
58+
[|using System.Linq;|]
59+
}";
60+
var fixedTestCode = @"global using System;
61+
62+
using System.Linq;
63+
64+
namespace TestNamespace
65+
{
66+
}";
67+
68+
await VerifyCSharpFixAsync(testCode, DiagnosticResult.EmptyDiagnosticResults, fixedTestCode, CancellationToken.None).ConfigureAwait(false);
69+
}
70+
71+
[Fact]
72+
[WorkItem(3875, "https://github.com/DotNetAnalyzers/StyleCopAnalyzers/issues/3875")]
73+
public async Task TestGlobalUsingStatementInFileWithOtherUsingDirectives2Async()
74+
{
75+
// This test specifically covers the case where the local using directive would sort before the global using
76+
// directive if the global using directive wasn't treated as always being first.
77+
var testCode = @"global using System.Linq;
78+
79+
namespace TestNamespace
80+
{
81+
[|using System;|]
82+
}";
83+
var fixedTestCode = @"global using System.Linq;
84+
85+
using System;
86+
87+
namespace TestNamespace
88+
{
89+
}";
90+
91+
await VerifyCSharpFixAsync(testCode, DiagnosticResult.EmptyDiagnosticResults, fixedTestCode, CancellationToken.None).ConfigureAwait(false);
92+
}
4993
}
5094
}

StyleCop.Analyzers/StyleCop.Analyzers.Test.CSharp10/OrderingRules/SA1200PreserveCSharp10UnitTests.cs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,5 +51,21 @@ public async Task TestOnlyGlobalUsingStatementInFileAsync(string leadingTrivia)
5151

5252
await VerifyCSharpDiagnosticAsync(testCode, DiagnosticResult.EmptyDiagnosticResults, CancellationToken.None).ConfigureAwait(false);
5353
}
54+
55+
[Fact]
56+
[WorkItem(3875, "https://github.com/DotNetAnalyzers/StyleCopAnalyzers/issues/3875")]
57+
public async Task TestGlobalUsingStatementInFileWithOtherUsingDirectivesAsync()
58+
{
59+
var testCode = @"global using System;
60+
61+
using System.Collections.Generic;
62+
63+
namespace TestNamespace
64+
{
65+
using System.Linq;
66+
}";
67+
68+
await VerifyCSharpDiagnosticAsync(testCode, DiagnosticResult.EmptyDiagnosticResults, CancellationToken.None).ConfigureAwait(false);
69+
}
5470
}
5571
}

0 commit comments

Comments
 (0)