Skip to content
This repository was archived by the owner on Jul 12, 2022. It is now read-only.

Commit 5a43307

Browse files
committed
Don't move usings inside #if directives
This changes the move usings outside of a namespace rule to be very wary of #if directives. There is simply no safe way to move a directive of the following nature: ``` csharp namespace NS1 { #if COND using NS2; class C { } #else using NS3; class C { } #endif ``` Lacking a good API to detect a using inside a #if directive we simply abandon this rule if there are any #if directives in the file. closes #71
1 parent c094932 commit 5a43307

File tree

5 files changed

+175
-41
lines changed

5 files changed

+175
-41
lines changed

src/Microsoft.DotNet.CodeFormatting.Tests/Microsoft.DotNet.CodeFormatting.Tests.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,7 @@
128128
<Compile Include="Rules\HasNewLineBeforeFirstUsingFormattingRuleTests.cs" />
129129
<Compile Include="Rules\PrivateFieldNamingRuleTests.cs" />
130130
<Compile Include="Rules\NonAsciiCharactersAreEscapedInLiteralsRuleTests.cs" />
131+
<Compile Include="Rules\UsingLocationRuleTests.cs" />
131132
</ItemGroup>
132133
<ItemGroup>
133134
<Folder Include="Properties\" />
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Linq;
4+
using System.Text;
5+
using System.Threading.Tasks;
6+
using Xunit;
7+
8+
namespace Microsoft.DotNet.CodeFormatting.Tests
9+
{
10+
public sealed class UsingLocationRuleTests : SyntaxRuleTestBase
11+
{
12+
internal override ISyntaxFormattingRule Rule
13+
{
14+
get { return new Rules.UsingLocationRule(); }
15+
}
16+
17+
[Fact]
18+
public void SimpleMove()
19+
{
20+
var source = @"
21+
using NS1;
22+
namespace NS2
23+
{
24+
using NS3;
25+
class C1 { }
26+
}";
27+
28+
var expected = @"
29+
using NS1;
30+
using NS3;
31+
namespace NS2
32+
{
33+
class C1 { }
34+
}";
35+
36+
Verify(source, expected);
37+
}
38+
39+
[Fact]
40+
public void SimpleMoveWithComment()
41+
{
42+
var source = @"
43+
using NS1;
44+
namespace NS2
45+
{
46+
// test
47+
using NS3;
48+
class C1 { }
49+
}";
50+
51+
var expected = @"
52+
using NS1;
53+
// test
54+
using NS3;
55+
namespace NS2
56+
{
57+
class C1 { }
58+
}";
59+
60+
Verify(source, expected);
61+
}
62+
63+
[Fact]
64+
public void SimpleMoveWithBeforeAfterComments()
65+
{
66+
var source = @"
67+
using NS1;
68+
namespace NS2
69+
{
70+
// test1
71+
using NS3;
72+
// test2
73+
class C1 { }
74+
}";
75+
76+
var expected = @"
77+
using NS1;
78+
// test1
79+
using NS3;
80+
namespace NS2
81+
{
82+
// test2
83+
class C1 { }
84+
}";
85+
86+
Verify(source, expected);
87+
}
88+
89+
[Fact]
90+
public void MoveToEmptyList()
91+
{
92+
var source = @"namespace NS2
93+
{
94+
// test
95+
using NS3;
96+
class C1 { }
97+
}";
98+
99+
var expected = @"// test
100+
using NS3;
101+
namespace NS2
102+
{
103+
class C1 { }
104+
}";
105+
106+
Verify(source, expected);
107+
}
108+
109+
/// <summary>
110+
/// In the case a using directive is inside of a #pragma directive there is no
111+
/// way to safely move the using.
112+
/// </summary>
113+
[Fact]
114+
public void Issue71()
115+
{
116+
var source = @"
117+
namespace N
118+
{
119+
#if false
120+
using NS1;
121+
class C { }
122+
#else
123+
using NS2;
124+
using D { }
125+
#endif
126+
}";
127+
128+
Verify(source, source);
129+
}
130+
131+
}
132+
}

src/Microsoft.DotNet.CodeFormatting/Microsoft.DotNet.CodeFormatting.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -134,7 +134,7 @@
134134
<Compile Include="Rules\HasNoIllegalHeadersFormattingRule.cs" />
135135
<Compile Include="Rules\HasNoCustomCopyrightHeaderFormattingRule.cs" />
136136
<Compile Include="Rules\PrivateFieldNamingRule.cs" />
137-
<Compile Include="Rules\HasUsingsOutsideOfNamespaceFormattingRule.cs" />
137+
<Compile Include="Rules\UsingLocationRule.cs" />
138138
<Compile Include="Rules\FormatDocumentFormattingRule.cs" />
139139
<Compile Include="Rules\NonAsciiCharactersAreEscapedInLiteralsRule.cs" />
140140
<Compile Include="Rules\ExplicitThisRule.cs" />

src/Microsoft.DotNet.CodeFormatting/Rules/RuleOrder.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ internal static class SyntaxRuleOrder
1414
{
1515
public const int HasNoCustomCopyrightHeaderFormattingRule = 1;
1616
public const int CopyrightHeaderRule = 2;
17-
public const int HasUsingsOutsideOfNamespaceFormattingRule = 3;
17+
public const int UsingLocationFormattingRule = 3;
1818
public const int HasNewLineBeforeFirstUsingFormattingRule = 4;
1919
public const int HasNewLineBeforeFirstNamespaceFormattingRule = 5;
2020
public const int BraceNewLineRule = 6;

src/Microsoft.DotNet.CodeFormatting/Rules/UsingLocationRule.cs

Lines changed: 40 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,10 @@
1313

1414
namespace Microsoft.DotNet.CodeFormatting.Rules
1515
{
16-
[SyntaxRuleOrder(SyntaxRuleOrder.HasUsingsOutsideOfNamespaceFormattingRule)]
16+
/// <summary>
17+
/// This will ensure that using directives are placed outside of the namespace.
18+
/// </summary>
19+
[SyntaxRuleOrder(SyntaxRuleOrder.UsingLocationFormattingRule)]
1720
internal sealed class UsingLocationRule : CSharpOnlyFormattingRule, ISyntaxFormattingRule
1821
{
1922
public SyntaxNode Process(SyntaxNode syntaxNode, string languageName)
@@ -22,50 +25,48 @@ public SyntaxNode Process(SyntaxNode syntaxNode, string languageName)
2225
if (root == null)
2326
return syntaxNode;
2427

25-
var newRoot = root;
26-
while (true)
28+
// This rule can only be done safely as a syntax transformation when there is a single namespace
29+
// declaration in the file. Once there is more than one it opens up the possibility of introducing
30+
// ambiguities to essentially make using directives global which were previously local.
31+
var namespaceDeclarationList = root.Members.OfType<NamespaceDeclarationSyntax>().ToList();
32+
if (namespaceDeclarationList.Count != 1)
2733
{
28-
var namespaceWithUsings = newRoot.Members.OfType<NamespaceDeclarationSyntax>().FirstOrDefault(n => n.Usings.Any());
29-
if (namespaceWithUsings == null)
30-
break;
31-
32-
// Moving a using with an alias out of a namespace is an operation which requires
33-
// semantic knowledge to get correct.
34-
if (namespaceWithUsings.Usings.Any(x => x.Alias != null))
35-
return syntaxNode;
36-
37-
// Remove nested usings
38-
39-
var emptyUsingList = SyntaxFactory.List<UsingDirectiveSyntax>();
40-
var namespaceWithoutUsings = namespaceWithUsings.WithUsings(emptyUsingList);
41-
newRoot = newRoot.ReplaceNode(namespaceWithUsings, namespaceWithoutUsings);
42-
43-
// Add usings to compilation unit
44-
45-
var usings = namespaceWithUsings.Usings.ToArray();
46-
47-
if (!newRoot.Usings.Any())
48-
{
49-
// Specialize the case where there are no usings yet.
50-
//
51-
// We want to make sure that leading triviva becomes leading trivia
52-
// of the first using.
53-
54-
usings[0] = usings.First().WithLeadingTrivia(newRoot.GetLeadingTrivia());
55-
newRoot = newRoot.WithLeadingTrivia(Enumerable.Empty<SyntaxTrivia>());
34+
return syntaxNode;
35+
}
5636

57-
// We want the last using to be separated from the namespace keyword
58-
// by a blank line.
37+
var namespaceDeclaration = namespaceDeclarationList.Single();
38+
var usingList = namespaceDeclaration.Usings;
39+
if (usingList.Count == 0)
40+
{
41+
return syntaxNode;
42+
}
5943

60-
var trailingTrivia = usings.Last().GetTrailingTrivia();
61-
var linebreak = new[] { SyntaxFactory.CarriageReturnLineFeed };
62-
usings[usings.Length - 1] = usings.Last().WithTrailingTrivia(trailingTrivia.Concat(linebreak));
63-
}
44+
// Moving a using with an alias out of a namespace is an operation which requires
45+
// semantic knowledge to get correct.
46+
if (usingList.Any(x => x.Alias != null))
47+
{
48+
return syntaxNode;
49+
}
6450

65-
newRoot = newRoot.AddUsings(usings);
51+
// We don't have the capability to safely move usings which are embedded inside an #if
52+
// directive.
53+
//
54+
// #if COND
55+
// using NS1;
56+
// #endif
57+
//
58+
// At the time there isn't a great way (that we know of) for detecting this particular
59+
// case. Instead we simply don't do this rewrite if the file contains any #if directives.
60+
if (root.DescendantTrivia().Any(x => x.Kind() == SyntaxKind.IfDirectiveTrivia))
61+
{
62+
return syntaxNode;
6663
}
6764

65+
var newRoot = root;
66+
newRoot = newRoot.ReplaceNode(namespaceDeclaration, namespaceDeclaration.WithUsings(SyntaxFactory.List<UsingDirectiveSyntax>()));
67+
newRoot = newRoot.WithUsings(newRoot.Usings.AddRange(namespaceDeclaration.Usings));
68+
6869
return newRoot;
6970
}
7071
}
71-
}
72+
}

0 commit comments

Comments
 (0)