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;