Skip to content

Commit 04fa43a

Browse files
authored
Merge pull request #3938 from bjornhellander/feature/sa1200-only-global-usings
Update SA1200UsingDirectivesMustBePlacedCorrectly to not trigger in files with only global using directives
2 parents 27446fa + 44710e1 commit 04fa43a

File tree

34 files changed

+1018
-72
lines changed

34 files changed

+1018
-72
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: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -69,10 +69,14 @@ public override async Task RegisterCodeFixesAsync(CodeFixContext context)
6969
continue;
7070
}
7171

72+
// 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.
74+
bool forcePreservePlacement = !isSA1200;
75+
7276
context.RegisterCodeFix(
7377
CodeAction.Create(
7478
OrderingResources.UsingCodeFix,
75-
cancellationToken => GetTransformedDocumentAsync(context.Document, syntaxRoot, !isSA1200, cancellationToken),
79+
cancellationToken => GetTransformedDocumentAsync(context.Document, syntaxRoot, forcePreservePlacement, cancellationToken),
7680
nameof(UsingCodeFixProvider)),
7781
diagnostic);
7882
}
@@ -117,9 +121,11 @@ private static async Task<Document> GetTransformedDocumentAsync(Document documen
117121
{
118122
newSyntaxRoot = AddUsingsToNamespace(newSyntaxRoot, usingsHelper, usingsIndentation, replaceMap.Any());
119123
}
120-
else if (usingDirectivesPlacement == UsingDirectivesPlacement.OutsideNamespace)
124+
125+
if (usingDirectivesPlacement != UsingDirectivesPlacement.Preserve)
121126
{
122-
newSyntaxRoot = AddUsingsToCompilationRoot(newSyntaxRoot, usingsHelper, usingsIndentation, replaceMap.Any());
127+
bool onlyGlobal = usingDirectivesPlacement == UsingDirectivesPlacement.InsideNamespace;
128+
newSyntaxRoot = AddUsingsToCompilationRoot(newSyntaxRoot, usingsHelper, replaceMap.Any(), onlyGlobal);
123129
}
124130

125131
// Final cleanup
@@ -257,7 +263,7 @@ private static void BuildReplaceMapForConditionalDirectives(UsingsSorter usingsH
257263

258264
var indentation = IndentationHelper.GenerateIndentationString(indentationSettings, indentationSteps);
259265

260-
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);
261267

262268
for (var i = 0; i < originalUsings.Count; i++)
263269
{
@@ -280,7 +286,7 @@ private static SyntaxNode AddUsingsToNamespace(SyntaxNode newSyntaxRoot, UsingsS
280286
var withLeadingBlankLine = rootNamespace.SyntaxNode.IsKind(SyntaxKindEx.FileScopedNamespaceDeclaration);
281287
var withTrailingBlankLine = hasConditionalDirectives || rootNamespace.Members.Any() || rootNamespace.Externs.Any();
282288

283-
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);
284290
groupedUsings = groupedUsings.AddRange(rootNamespace.Usings);
285291

286292
var newRootNamespace = rootNamespace.WithUsings(groupedUsings);
@@ -289,12 +295,12 @@ private static SyntaxNode AddUsingsToNamespace(SyntaxNode newSyntaxRoot, UsingsS
289295
return newSyntaxRoot;
290296
}
291297

292-
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)
293299
{
294300
var newCompilationUnit = (CompilationUnitSyntax)newSyntaxRoot;
295301
var withTrailingBlankLine = hasConditionalDirectives || newCompilationUnit.AttributeLists.Any() || newCompilationUnit.Members.Any() || newCompilationUnit.Externs.Any();
296302

297-
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);
298304
groupedUsings = groupedUsings.AddRange(newCompilationUnit.Usings);
299305
newSyntaxRoot = newCompilationUnit.WithUsings(groupedUsings);
300306

@@ -513,9 +519,12 @@ protected override async Task<SyntaxNode> FixAllInDocumentAsync(FixAllContext fi
513519
return null;
514520
}
515521

522+
var syntaxRoot = await document.GetSyntaxRootAsync(fixAllContext.CancellationToken).ConfigureAwait(false);
523+
524+
// Force preserving the placement of using directives when we are fixing any diagnostic not directly
525+
// related to placement of using directives inside/outside namespaces.
516526
var forcePreserve = diagnostics.All(d => d.Id != SA1200UsingDirectivesMustBePlacedCorrectly.DiagnosticId);
517527

518-
var syntaxRoot = await document.GetSyntaxRootAsync(fixAllContext.CancellationToken).ConfigureAwait(false);
519528
Document newDocument = await GetTransformedDocumentAsync(document, syntaxRoot, forcePreserve, fixAllContext.CancellationToken).ConfigureAwait(false);
520529
return await newDocument.GetSyntaxRootAsync(fixAllContext.CancellationToken).ConfigureAwait(false);
521530
}

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

Lines changed: 46 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
// Copyright (c) Tunnel Vision Laboratories, LLC. All Rights Reserved.
22
// Licensed under the MIT License. See LICENSE in the project root for license information.
33

4-
#nullable disable
5-
64
namespace StyleCop.Analyzers.Test.CSharp10.OrderingRules
75
{
86
using System.Threading;
@@ -40,5 +38,51 @@ namespace TestNamespace;
4038

4139
await VerifyCSharpFixAsync(testCode, expectedResults, fixedTestCode, CancellationToken.None).ConfigureAwait(false);
4240
}
41+
42+
[Theory]
43+
[InlineData("")]
44+
[InlineData("\n")]
45+
[InlineData("// A comment.\n")]
46+
[WorkItem(3875, "https://github.com/DotNetAnalyzers/StyleCopAnalyzers/issues/3875")]
47+
public async Task TestOnlyGlobalUsingStatementInFileAsync(string leadingTrivia)
48+
{
49+
var testCode = $@"{leadingTrivia}global using System;";
50+
51+
await VerifyCSharpDiagnosticAsync(testCode, DiagnosticResult.EmptyDiagnosticResults, CancellationToken.None).ConfigureAwait(false);
52+
}
53+
54+
[Fact]
55+
[WorkItem(3875, "https://github.com/DotNetAnalyzers/StyleCopAnalyzers/issues/3875")]
56+
public async Task TestGlobalUsingStatementInFileWithNamespaceAsync()
57+
{
58+
var testCode = @"global using System;
59+
60+
namespace TestNamespace
61+
{
62+
}";
63+
64+
await VerifyCSharpDiagnosticAsync(testCode, DiagnosticResult.EmptyDiagnosticResults, CancellationToken.None).ConfigureAwait(false);
65+
}
66+
67+
[Fact]
68+
[WorkItem(3875, "https://github.com/DotNetAnalyzers/StyleCopAnalyzers/issues/3875")]
69+
public async Task TestGlobalUsingStatementInFileWithOtherUsingDirectivesAsync()
70+
{
71+
var testCode = @"global using System;
72+
73+
[|using System.Linq;|]
74+
75+
namespace TestNamespace
76+
{
77+
}";
78+
var fixedTestCode = @"global using System;
79+
80+
namespace TestNamespace
81+
{
82+
using System.Linq;
83+
}";
84+
85+
await VerifyCSharpFixAsync(testCode, DiagnosticResult.EmptyDiagnosticResults, fixedTestCode, CancellationToken.None).ConfigureAwait(false);
86+
}
4387
}
4488
}

0 commit comments

Comments
 (0)