diff --git a/src/Microsoft.DotNet.CodeFormatting.Tests/Microsoft.DotNet.CodeFormatting.Tests.csproj b/src/Microsoft.DotNet.CodeFormatting.Tests/Microsoft.DotNet.CodeFormatting.Tests.csproj
index 2f26297e..0dec6077 100644
--- a/src/Microsoft.DotNet.CodeFormatting.Tests/Microsoft.DotNet.CodeFormatting.Tests.csproj
+++ b/src/Microsoft.DotNet.CodeFormatting.Tests/Microsoft.DotNet.CodeFormatting.Tests.csproj
@@ -112,6 +112,7 @@
+
diff --git a/src/Microsoft.DotNet.CodeFormatting.Tests/Rules/CombinationTest.cs b/src/Microsoft.DotNet.CodeFormatting.Tests/Rules/CombinationTest.cs
index 9aa4f00b..a9648210 100644
--- a/src/Microsoft.DotNet.CodeFormatting.Tests/Rules/CombinationTest.cs
+++ b/src/Microsoft.DotNet.CodeFormatting.Tests/Rules/CombinationTest.cs
@@ -69,7 +69,8 @@ private void M()
{
N(_field);
}
-}";
+}
+";
Verify(text, expected, runFormatter: false);
}
@@ -96,7 +97,8 @@ private void M()
{
_field = 42;
}
-}";
+}
+";
Verify(text, expected, runFormatter: false);
}
@@ -119,7 +121,8 @@ internal class C
#if DOG
void M() { }
#endif
-}";
+}
+";
Verify(text, expected, runFormatter: false);
}
@@ -145,7 +148,8 @@ internal void M()
{
}
#endif
-}";
+}
+";
s_formattingEngine.PreprocessorConfigurations = ImmutableArray.CreateRange(new[] { new[] { "DOG" } });
Verify(text, expected, runFormatter: false);
@@ -179,7 +183,8 @@ private void G()
void M() {
}
#endif
-}";
+}
+";
Verify(text, expected, runFormatter: false);
}
@@ -219,7 +224,8 @@ void G()
void M() {
}
#endif
-}";
+}
+";
s_formattingEngine.PreprocessorConfigurations = ImmutableArray.CreateRange(new[] { new[] { "TEST" } });
Verify(text, expected, runFormatter: false);
@@ -262,7 +268,8 @@ private void M()
{
}
}
-}";
+}
+";
Verify(source, expected, runFormatter: false);
}
diff --git a/src/Microsoft.DotNet.CodeFormatting.Tests/Rules/NewLineAtEndOfFileRuleTests.cs b/src/Microsoft.DotNet.CodeFormatting.Tests/Rules/NewLineAtEndOfFileRuleTests.cs
new file mode 100644
index 00000000..47d3a41f
--- /dev/null
+++ b/src/Microsoft.DotNet.CodeFormatting.Tests/Rules/NewLineAtEndOfFileRuleTests.cs
@@ -0,0 +1,86 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using Xunit;
+
+namespace Microsoft.DotNet.CodeFormatting.Tests
+{
+ public class NewLineAtEndOfFileRuleTests : SyntaxRuleTestBase
+ {
+ internal override ISyntaxFormattingRule Rule
+ {
+ get { return new Rules.NewLineAtEndOfFileRule(); }
+ }
+
+ [Fact]
+ public void SimpleClassWithoutNewLineAtEndOfFile()
+ {
+ var text = @"using System;
+public class TestClass
+{
+}";
+
+ var expected = @"using System;
+public class TestClass
+{
+}
+";
+
+ Verify(text, expected);
+ }
+
+ [Fact]
+ public void SimpleClassWithNewLineAtEndOfFile()
+ {
+ var text = @"using System;
+public class TestClass
+{
+}
+";
+
+ Verify(text, text);
+ }
+
+ [Fact]
+ public void CommentAtEndOfFile()
+ {
+ var text = @"using System;
+public class TestClass
+{
+}
+// Hello World";
+
+ var expected = @"using System;
+public class TestClass
+{
+}
+// Hello World
+";
+
+ Verify(text, expected);
+ }
+
+ [Fact]
+ public void IfDefAtEndOfFile()
+ {
+ var text = @"using System;
+public class TestClass
+{
+}
+#if TEST
+#endif";
+
+ var expected = @"using System;
+public class TestClass
+{
+}
+#if TEST
+#endif
+";
+
+ Verify(text, expected);
+ }
+ }
+}
diff --git a/src/Microsoft.DotNet.CodeFormatting/Microsoft.DotNet.CodeFormatting.csproj b/src/Microsoft.DotNet.CodeFormatting/Microsoft.DotNet.CodeFormatting.csproj
index e549e17d..c88f3f17 100644
--- a/src/Microsoft.DotNet.CodeFormatting/Microsoft.DotNet.CodeFormatting.csproj
+++ b/src/Microsoft.DotNet.CodeFormatting/Microsoft.DotNet.CodeFormatting.csproj
@@ -88,6 +88,7 @@
+
diff --git a/src/Microsoft.DotNet.CodeFormatting/Rules/NewLineAtEndOfFileRule.cs b/src/Microsoft.DotNet.CodeFormatting/Rules/NewLineAtEndOfFileRule.cs
new file mode 100644
index 00000000..43cdb33d
--- /dev/null
+++ b/src/Microsoft.DotNet.CodeFormatting/Rules/NewLineAtEndOfFileRule.cs
@@ -0,0 +1,70 @@
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace Microsoft.DotNet.CodeFormatting.Rules
+{
+ [SyntaxRule(SyntaxRuleOrder.NewLineAtEndOfFileRule)]
+ internal sealed class NewLineAtEndOfFileRule : CSharpOnlyFormattingRule, ISyntaxFormattingRule
+ {
+ public SyntaxNode Process(SyntaxNode syntaxRoot, string languageName)
+ {
+ bool needsNewLine;
+ var endOfFileToken = syntaxRoot.GetLastToken(true, true, true, true);
+ if (!endOfFileToken.IsKind(SyntaxKind.EndOfFileToken))
+ {
+ throw new InvalidOperationException("Expected last token to be EndOfFileToken, was actually: " + endOfFileToken.Kind());
+ }
+
+ if (endOfFileToken.HasLeadingTrivia)
+ {
+ return AddNewLineToEndOfFileTokenLeadingTriviaIfNecessary(syntaxRoot, endOfFileToken);
+ }
+
+ var lastToken = syntaxRoot.GetLastToken();
+ if (!lastToken.HasTrailingTrivia)
+ {
+ needsNewLine = true;
+ }
+ else
+ {
+ var lastTrivia = lastToken.TrailingTrivia.Last();
+ if (lastTrivia.IsKind(SyntaxKind.EndOfLineTrivia))
+ {
+ needsNewLine = false;
+ }
+ else
+ {
+ needsNewLine = true;
+ }
+ }
+
+ if (needsNewLine)
+ {
+ var newLine = SyntaxUtil.GetBestNewLineTriviaRecursive(lastToken.Parent);
+ var newLastToken = lastToken.WithTrailingTrivia(lastToken.TrailingTrivia.Concat(new[] { newLine }));
+ return syntaxRoot.ReplaceToken(lastToken, newLastToken);
+ }
+ return syntaxRoot;
+ }
+
+ SyntaxNode AddNewLineToEndOfFileTokenLeadingTriviaIfNecessary(SyntaxNode syntaxRoot, SyntaxToken endofFileToken)
+ {
+ if (endofFileToken.LeadingTrivia.Last().IsKind(SyntaxKind.EndOfLineTrivia))
+ {
+ return syntaxRoot;
+ }
+
+ var newLine = SyntaxUtil.GetBestNewLineTriviaRecursive(endofFileToken.Parent);
+ var newLastToken = endofFileToken.WithTrailingTrivia(endofFileToken.TrailingTrivia.Concat(new[] { newLine }));
+ return syntaxRoot.ReplaceToken(endofFileToken, newLastToken);
+
+
+ return syntaxRoot;
+ }
+ }
+}
diff --git a/src/Microsoft.DotNet.CodeFormatting/Rules/RuleOrder.cs b/src/Microsoft.DotNet.CodeFormatting/Rules/RuleOrder.cs
index 7ce282ae..2bccad21 100644
--- a/src/Microsoft.DotNet.CodeFormatting/Rules/RuleOrder.cs
+++ b/src/Microsoft.DotNet.CodeFormatting/Rules/RuleOrder.cs
@@ -16,6 +16,7 @@ internal static class SyntaxRuleOrder
public const int CopyrightHeaderRule = 2;
public const int UsingLocationFormattingRule = 3;
public const int NewLineAboveFormattingRule = 4;
+ public const int NewLineAtEndOfFileRule = 5;
public const int BraceNewLineRule = 6;
public const int NonAsciiChractersAreEscapedInLiterals = 7;
}
diff --git a/src/Microsoft.DotNet.CodeFormatting/SyntaxUtil.cs b/src/Microsoft.DotNet.CodeFormatting/SyntaxUtil.cs
index b1a61e56..eed94474 100644
--- a/src/Microsoft.DotNet.CodeFormatting/SyntaxUtil.cs
+++ b/src/Microsoft.DotNet.CodeFormatting/SyntaxUtil.cs
@@ -30,6 +30,23 @@ internal static SyntaxTrivia GetBestNewLineTrivia(SyntaxNode node, SyntaxTrivia?
return defaultNewLineTrivia ?? SyntaxFactory.CarriageReturnLineFeed;
}
+ internal static SyntaxTrivia GetBestNewLineTriviaRecursive(SyntaxNode node, SyntaxTrivia? defaultNewLineTrivia = null)
+ {
+ while(node != null)
+ {
+ SyntaxTrivia trivia;
+ if (TryGetExistingNewLine(node.GetLeadingTrivia(), out trivia) ||
+ TryGetExistingNewLine(node.GetTrailingTrivia(), out trivia))
+ {
+ return trivia;
+ }
+
+ node = node.Parent;
+ }
+
+ return defaultNewLineTrivia ?? SyntaxFactory.CarriageReturnLineFeed;
+ }
+
internal static SyntaxTrivia GetBestNewLineTrivia(SyntaxToken token, SyntaxTrivia? defaultNewLineTrivia = null)
{
SyntaxTrivia trivia;