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

Commit ced83ad

Browse files
committed
Fix double new lines with extern directives
Fix the double new lines which got inserted in the presence of an extern directive in the code base. Note: As a part of this change I had to completely revisit the new line above rule because it didn't take into account a number of subtle scenarios. The rules around #pragma were simply making the implementation very difficult to get correct and hence I decided to change them to make it more managable. closes #83
1 parent 3b9388d commit ced83ad

File tree

3 files changed

+120
-50
lines changed

3 files changed

+120
-50
lines changed

src/Microsoft.DotNet.CodeFormatting.Tests/Rules/NewLineAboveRuleTests.cs

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -58,8 +58,8 @@ public void UsingWithOnlyDirectiveAbove()
5858
var text = @"#pragma warning disable 1591
5959
using System;";
6060

61-
var expected = @"
62-
#pragma warning disable 1591
61+
var expected = @"#pragma warning disable 1591
62+
6363
using System;";
6464

6565
Verify(text, expected);
@@ -158,8 +158,8 @@ public void TestNewLineBeforeFirstUsing03()
158158
";
159159
var expected = @"
160160
// copyright comment
161-
162161
#pragma warning disable 1591
162+
163163
using System;
164164
";
165165
Verify(text, expected);
@@ -228,6 +228,7 @@ namespace N { }
228228
using System;
229229
230230
#pragma warning disable 1591
231+
231232
namespace N { }
232233
";
233234
Verify(text, expected);
@@ -245,5 +246,23 @@ namespace N { }
245246
";
246247
Verify(text, text);
247248
}
249+
250+
[Fact]
251+
public void Issue83()
252+
{
253+
var text = @"
254+
extern alias PDB;
255+
using System;
256+
namespace N { }";
257+
258+
var expected = @"
259+
extern alias PDB;
260+
261+
using System;
262+
263+
namespace N { }";
264+
265+
Verify(text, expected);
266+
}
248267
}
249268
}

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

Lines changed: 83 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
using System.Collections;
66
using System.Collections.Generic;
77
using System.ComponentModel.Composition;
8+
using System.Diagnostics;
89
using System.Linq;
910
using System.Threading;
1011
using System.Threading.Tasks;
@@ -80,8 +81,8 @@ private SyntaxNode ProcessCore<TNode>(SyntaxNode syntaxRoot, TNode node) where T
8081
return syntaxRoot;
8182
}
8283

83-
SyntaxTriviaList newTriviaList;
84-
if (!TryGetNewLeadingTrivia(node, out newTriviaList))
84+
var newTriviaList = GetNewLeadingTrivia(node);
85+
if (newTriviaList == node.GetLeadingTrivia())
8586
{
8687
return syntaxRoot;
8788
}
@@ -93,69 +94,104 @@ private SyntaxNode ProcessCore<TNode>(SyntaxNode syntaxRoot, TNode node) where T
9394
/// Get the new leading trivia list that will add the double blank line that we are looking
9495
/// for.
9596
/// </summary>
96-
private bool TryGetNewLeadingTrivia(SyntaxNode node, out SyntaxTriviaList newTriviaList)
97+
private SyntaxTriviaList GetNewLeadingTrivia(SyntaxNode node)
9798
{
98-
var newLineTrivia = SyntaxUtil.GetBestNewLineTrivia(node);
9999
var list = node.GetLeadingTrivia();
100-
var index = list.Count - 1;
100+
var searchIndex = 0;
101+
var newLineTrivia = SyntaxUtil.GetBestNewLineTrivia(node);
102+
var prev = node.FindPreviousNodeInParent();
101103

102-
MoveBackwardsPastWhitespace(list, ref index);
103-
if (index < 0 || !list[index].IsAnyEndOfLine())
104+
if (prev == null)
104105
{
105-
// There is no newline before the using at all. Add a double newline to
106-
// get the blank we are looking for
107-
newTriviaList = list.InsertRange(index + 1, new[] { newLineTrivia, newLineTrivia });
108-
return true;
106+
if (node.Span.Start == 0)
107+
{
108+
// First item in the file. Do nothing for this case.
109+
return list;
110+
}
111+
}
112+
else
113+
{
114+
// If there is no new line in the trailing trivia of the previous node then need to add
115+
// one to put this node on the next line.
116+
if (prev.GetTrailingTrivia().Count == 0 || !prev.GetTrailingTrivia().Last().IsAnyEndOfLine())
117+
{
118+
list = list.Insert(0, newLineTrivia);
119+
searchIndex = 1;
120+
}
109121
}
110122

111-
var wasDirective = list[index].IsDirective;
112-
index--;
113-
114-
// Move past any directives that are above the token. The newline needs to
115-
// be above them.
116-
while (index >= 0 && list[index].IsDirective)
123+
// Ensure there are blank above #pragma directives here. This is an attempt to maintain compatibility
124+
// with the original design of this rule which had special spacing rules for #pragma. No reason
125+
// was given for the special casing, only tests.
126+
if (searchIndex < list.Count && list[0].IsKind(SyntaxKind.PragmaWarningDirectiveTrivia) && list[0].FullSpan.Start != 0)
117127
{
118-
index--;
128+
list = list.Insert(searchIndex, newLineTrivia);
129+
searchIndex++;
119130
}
120131

121-
if (wasDirective)
132+
EnsureHasBlankLineAtEnd(ref list, searchIndex, newLineTrivia);
133+
134+
return list;
135+
}
136+
137+
/// <summary>
138+
/// Ensure the trivia list has a blank line at the end. Both the second to last
139+
/// and final line may contain spaces.
140+
///
141+
/// Note: This function assumes the trivia token before <param name="startIndex" />
142+
/// is an end of line trivia.
143+
/// </summary>
144+
private static void EnsureHasBlankLineAtEnd(ref SyntaxTriviaList list, int startIndex, SyntaxTrivia newLineTrivia)
145+
{
146+
const int StateNone = 0;
147+
const int StateEol = 1;
148+
const int StateBlankLine = 2;
149+
150+
var state = StateEol;
151+
var index = startIndex;
152+
var eolIndex = startIndex - 1;
153+
154+
while (index < list.Count)
122155
{
123-
// There was a directive above the using and index now points directly before
124-
// that. This token must be a new line.
125-
if (index < 0 || !list[index].IsKind(SyntaxKind.EndOfLineTrivia))
156+
var current = list[index];
157+
if (current.IsKind(SyntaxKind.WhitespaceTrivia))
126158
{
127-
newTriviaList = list.Insert(index + 1, newLineTrivia);
128-
return true;
159+
index++;
160+
continue;
129161
}
130162

131-
index--;
132-
}
133-
134-
// In the logical line above the using. Need to see <blank><eol> in order for the
135-
// using to be correct
136-
var insertIndex = index + 1;
137-
MoveBackwardsPastWhitespace(list, ref index);
138-
if (index < 0 || !list[index].IsAnyEndOfLine())
139-
{
140-
// If this is the first item in the file then there is no need for a double
141-
// blank line.
142-
if (index >= 0 || node.FullSpan.Start != 0)
163+
var isStateAnyEol = (state == StateEol || state == StateBlankLine);
164+
if (isStateAnyEol && current.IsKind(SyntaxKind.EndOfLineTrivia))
143165
{
144-
newTriviaList = list.Insert(insertIndex, newLineTrivia);
145-
return true;
166+
state = StateBlankLine;
167+
}
168+
else if (current.IsAnyEndOfLine())
169+
{
170+
eolIndex = index;
171+
state = StateEol;
172+
}
173+
else
174+
{
175+
state = StateNone;
146176
}
147-
}
148177

149-
// The using is well formed so there is no work to be done.
150-
newTriviaList = SyntaxTriviaList.Empty;
151-
return false;
152-
}
178+
index++;
179+
}
153180

154-
private static void MoveBackwardsPastWhitespace(SyntaxTriviaList list, ref int index)
155-
{
156-
while (index >= 0 && list[index].IsKind(SyntaxKind.WhitespaceTrivia))
181+
switch (state)
157182
{
158-
index--;
183+
case StateNone:
184+
list = list.InsertRange(list.Count, new[] { newLineTrivia, newLineTrivia });
185+
break;
186+
case StateEol:
187+
list = list.Insert(eolIndex + 1, newLineTrivia);
188+
break;
189+
case StateBlankLine:
190+
// Nothing to do.
191+
break;
192+
default:
193+
Debug.Assert(false);
194+
break;
159195
}
160196
}
161197
}

src/Microsoft.DotNet.CodeFormatting/SyntaxUtil.cs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,5 +91,20 @@ internal static bool IsAnyEndOfLine(this SyntaxTrivia trivia)
9191
{
9292
return trivia.IsKind(SyntaxKind.EndOfLineTrivia) || trivia.IsDirective;
9393
}
94+
95+
/// <summary>
96+
/// Find the node directly before this in the parent. Returns null in the case it
97+
/// cannot be found.
98+
/// </summary>
99+
internal static SyntaxNode FindPreviousNodeInParent(this SyntaxNode node)
100+
{
101+
var parent = node.Parent;
102+
if (parent == null)
103+
{
104+
return null;
105+
}
106+
107+
return parent.ChildNodes().Where(x => x.FullSpan.End == node.FullSpan.Start).FirstOrDefault();
108+
}
94109
}
95110
}

0 commit comments

Comments
 (0)