Skip to content

Commit 673de60

Browse files
authored
Merge pull request #4076 from sharwell/line-endings
Fix line ending handling
2 parents 4d92a69 + c71c129 commit 673de60

File tree

143 files changed

+1799
-1030
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

143 files changed

+1799
-1030
lines changed

StyleCop.Analyzers/StyleCop.Analyzers.CodeFixes/DocumentationRules/FileHeaderCodeFixProvider.cs

Lines changed: 20 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ namespace StyleCop.Analyzers.DocumentationRules
1818
using Microsoft.CodeAnalysis.CodeActions;
1919
using Microsoft.CodeAnalysis.CodeFixes;
2020
using Microsoft.CodeAnalysis.CSharp;
21-
using Microsoft.CodeAnalysis.Formatting;
21+
using Microsoft.CodeAnalysis.Text;
2222
using StyleCop.Analyzers.Helpers;
2323
using StyleCop.Analyzers.Helpers.ObjectPools;
2424
using StyleCop.Analyzers.Settings.ObjectModel;
@@ -79,13 +79,14 @@ private static async Task<Document> GetTransformedDocumentAsync(Document documen
7979
private static async Task<SyntaxNode> GetTransformedSyntaxRootAsync(Document document, CancellationToken cancellationToken)
8080
{
8181
var root = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false);
82+
var sourceText = await document.GetTextAsync(cancellationToken).ConfigureAwait(false);
8283
var settings = document.Project.AnalyzerOptions.GetStyleCopSettingsInCodeFix(root.SyntaxTree, cancellationToken);
8384

8485
var fileHeader = FileHeaderHelpers.ParseFileHeader(root);
8586
SyntaxNode newSyntaxRoot;
8687
if (fileHeader.IsMissing)
8788
{
88-
newSyntaxRoot = AddHeader(document, root, GetFileName(document), settings);
89+
newSyntaxRoot = AddHeader(document, sourceText, root, GetFileName(document), settings);
8990
}
9091
else
9192
{
@@ -98,18 +99,18 @@ private static async Task<SyntaxNode> GetTransformedSyntaxRootAsync(Document doc
9899
var xmlFileHeader = FileHeaderHelpers.ParseXmlFileHeader(root);
99100
if (isMultiLineComment && !xmlFileHeader.IsMalformed)
100101
{
101-
newSyntaxRoot = ReplaceWellFormedMultiLineCommentHeader(document, root, settings, commentIndex, xmlFileHeader);
102+
newSyntaxRoot = ReplaceWellFormedMultiLineCommentHeader(document, sourceText, root, settings, commentIndex, xmlFileHeader);
102103
}
103104
else
104105
{
105-
newSyntaxRoot = ReplaceHeader(document, root, settings, xmlFileHeader.IsMalformed);
106+
newSyntaxRoot = ReplaceHeader(document, sourceText, root, settings, xmlFileHeader.IsMalformed);
106107
}
107108
}
108109

109110
return newSyntaxRoot;
110111
}
111112

112-
private static SyntaxNode ReplaceWellFormedMultiLineCommentHeader(Document document, SyntaxNode root, StyleCopSettings settings, int commentIndex, XmlFileHeader header)
113+
private static SyntaxNode ReplaceWellFormedMultiLineCommentHeader(Document document, SourceText sourceText, SyntaxNode root, StyleCopSettings settings, int commentIndex, XmlFileHeader header)
113114
{
114115
SyntaxTriviaList trivia = root.GetLeadingTrivia();
115116
var commentTrivia = trivia[commentIndex];
@@ -135,7 +136,10 @@ private static SyntaxNode ReplaceWellFormedMultiLineCommentHeader(Document docum
135136
string interlinePadding = " *";
136137

137138
int minExpectedLength = (commentIndentation + interlinePadding).Length;
138-
string newLineText = document.Project.Solution.Workspace.Options.GetOption(FormattingOptions.NewLine, LanguageNames.CSharp);
139+
var options = document.Project.Solution.Workspace.Options;
140+
var firstToken = root.GetFirstToken(includeZeroWidth: true);
141+
SyntaxTrivia newLineTrivia = FormattingHelper.GetEndOfLineForCodeFix(firstToken, sourceText, options);
142+
string newLineText = newLineTrivia.ToFullString();
139143

140144
// Examine second line to see if we should have stars or not if it's blank
141145
// set the interline padding to be blank also.
@@ -213,7 +217,7 @@ private static SyntaxNode ReplaceWellFormedMultiLineCommentHeader(Document docum
213217
return root.WithLeadingTrivia(trivia.Replace(commentTrivia, newTrivia));
214218
}
215219

216-
private static SyntaxNode ReplaceHeader(Document document, SyntaxNode root, StyleCopSettings settings, bool isMalformedHeader)
220+
private static SyntaxNode ReplaceHeader(Document document, SourceText sourceText, SyntaxNode root, StyleCopSettings settings, bool isMalformedHeader)
217221
{
218222
// If the header is well formed Xml then we parse out the copyright otherwise
219223
// Skip single line comments, whitespace, and end of line trivia until a blank line is encountered.
@@ -312,8 +316,10 @@ private static SyntaxNode ReplaceHeader(Document document, SyntaxNode root, Styl
312316
trivia = trivia.RemoveAt(removalList[i]);
313317
}
314318

315-
string newLineText = document.Project.Solution.Workspace.Options.GetOption(FormattingOptions.NewLine, LanguageNames.CSharp);
316-
var newLineTrivia = SyntaxFactory.EndOfLine(newLineText);
319+
var options = document.Project.Solution.Workspace.Options;
320+
var firstToken = root.GetFirstToken(includeZeroWidth: true);
321+
SyntaxTrivia newLineTrivia = FormattingHelper.GetEndOfLineForCodeFix(firstToken, sourceText, options);
322+
string newLineText = newLineTrivia.ToFullString();
317323

318324
var newHeaderTrivia = CreateNewHeader(leadingSpaces + "//", GetFileName(document), settings, newLineText);
319325
if (!isMalformedHeader && copyrightTriviaIndex.HasValue)
@@ -356,10 +362,12 @@ private static bool FirstLineIsComment(SyntaxTriviaList trivia)
356362
return false;
357363
}
358364

359-
private static SyntaxNode AddHeader(Document document, SyntaxNode root, string name, StyleCopSettings settings)
365+
private static SyntaxNode AddHeader(Document document, SourceText sourceText, SyntaxNode root, string name, StyleCopSettings settings)
360366
{
361-
string newLineText = document.Project.Solution.Workspace.Options.GetOption(FormattingOptions.NewLine, LanguageNames.CSharp);
362-
var newLineTrivia = SyntaxFactory.EndOfLine(newLineText);
367+
var options = document.Project.Solution.Workspace.Options;
368+
var firstToken = root.GetFirstToken(includeZeroWidth: true);
369+
SyntaxTrivia newLineTrivia = FormattingHelper.GetEndOfLineForCodeFix(firstToken, sourceText, options);
370+
string newLineText = newLineTrivia.ToFullString();
363371
var newTrivia = CreateNewHeader("//", name, settings, newLineText).Add(newLineTrivia).Add(newLineTrivia);
364372

365373
// Skip blank lines already at the beginning of the document, since we add our own

StyleCop.Analyzers/StyleCop.Analyzers.CodeFixes/Helpers/FormattingHelper.cs

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,87 @@
66
namespace StyleCop.Analyzers.Helpers
77
{
88
using System.Linq;
9+
using System.Threading;
10+
using System.Threading.Tasks;
911
using Microsoft.CodeAnalysis;
1012
using Microsoft.CodeAnalysis.CodeActions;
1113
using Microsoft.CodeAnalysis.CSharp;
1214
using Microsoft.CodeAnalysis.CSharp.Syntax;
1315
using Microsoft.CodeAnalysis.Formatting;
16+
using Microsoft.CodeAnalysis.Options;
17+
using Microsoft.CodeAnalysis.Text;
1418

1519
internal static class FormattingHelper
1620
{
21+
/// <summary>
22+
/// Retrieves the appropriate end-of-line trivia for use in code fixes at the specified token location.
23+
/// </summary>
24+
/// <remarks><para>This method attempts to preserve the existing line ending style near the specified
25+
/// token. If no suitable end-of-line trivia is found, a default carriage return and line feed (CRLF) is
26+
/// returned.</para></remarks>
27+
/// <param name="token">The syntax token for which to determine the end-of-line trivia.</param>
28+
/// <param name="options">The options to use for formatting. This value is only used if the document does not already contain line endings.</param>
29+
/// <param name="cancellationToken">A cancellation token that can be used to cancel the asynchronous operation.</param>
30+
/// <returns>A <see cref="SyntaxTrivia"/> representing the end-of-line trivia suitable for code fixes at the given token
31+
/// position.</returns>
32+
public static async Task<SyntaxTrivia> GetEndOfLineForCodeFixAsync(SyntaxToken token, OptionSet options, CancellationToken cancellationToken)
33+
{
34+
if (TryGetPrecedingEndOfLineTrivia(token, out var precedingEndOfLine))
35+
{
36+
return precedingEndOfLine;
37+
}
38+
39+
var text = await token.SyntaxTree.GetTextAsync(cancellationToken).ConfigureAwait(false);
40+
return GetEndOfLineForCodeFix(token, text, options);
41+
}
42+
43+
/// <summary>
44+
/// Retrieves the appropriate end-of-line trivia for use in code fixes, based on the position of the specified
45+
/// syntax token and the provided source text.
46+
/// </summary>
47+
/// <remarks><para>This method examines the token's position and surrounding lines to select the most
48+
/// contextually appropriate end-of-line trivia, ensuring consistency with the existing file
49+
/// formatting.</para></remarks>
50+
/// <param name="token">The syntax token for which to determine the end-of-line trivia.</param>
51+
/// <param name="text">The source text containing the token. Used to identify line boundaries and end-of-line characters.</param>
52+
/// <param name="options">The options to use for formatting. This value is only used if the document does not already contain line endings.</param>
53+
/// <returns>A <see cref="SyntaxTrivia"/> representing the end-of-line trivia suitable for code fixes. If no specific
54+
/// trivia is found, returns a default carriage return and line feed trivia.</returns>
55+
public static SyntaxTrivia GetEndOfLineForCodeFix(SyntaxToken token, SourceText text, OptionSet options)
56+
{
57+
if (TryGetPrecedingEndOfLineTrivia(token, out var precedingEndOfLine))
58+
{
59+
return precedingEndOfLine;
60+
}
61+
62+
var lineNumber = token.GetLine();
63+
if (lineNumber >= 0 && lineNumber < text.Lines.Count && GetEndOfLineTriviaForLine(text.Lines[lineNumber]) is { } followingTrivia)
64+
{
65+
return followingTrivia;
66+
}
67+
68+
if (lineNumber > 0 && GetEndOfLineTriviaForLine(text.Lines[lineNumber - 1]) is { } precedingTrivia)
69+
{
70+
return precedingTrivia;
71+
}
72+
73+
return SyntaxFactory.SyntaxTrivia(SyntaxKind.EndOfLineTrivia, options.GetOption(FormattingOptions.NewLine, LanguageNames.CSharp));
74+
75+
static SyntaxTrivia? GetEndOfLineTriviaForLine(TextLine textLine)
76+
{
77+
return (textLine.EndIncludingLineBreak - textLine.End) switch
78+
{
79+
2 => SyntaxFactory.CarriageReturnLineFeed,
80+
1 => textLine.Text[textLine.End] switch
81+
{
82+
'\n' => SyntaxFactory.LineFeed,
83+
char c => SyntaxFactory.EndOfLine(c.ToString()),
84+
},
85+
_ => null,
86+
};
87+
}
88+
}
89+
1790
public static SyntaxTrivia GetNewLineTrivia(Document document)
1891
{
1992
return SyntaxFactory.SyntaxTrivia(SyntaxKind.EndOfLineTrivia, document.Project.Solution.Workspace.Options.GetOption(FormattingOptions.NewLine, LanguageNames.CSharp));
@@ -152,5 +225,39 @@ private static SyntaxTrivia WithoutFormattingImpl(SyntaxTrivia trivia)
152225
{
153226
return trivia.WithoutAnnotations(Formatter.Annotation, SyntaxAnnotation.ElasticAnnotation);
154227
}
228+
229+
/// <summary>
230+
/// Returns the closest end of line trivia preceding the <paramref name="token"/>.
231+
/// This currently only looks immediately before the specified token.
232+
/// </summary>
233+
/// <param name="token">The token to process.</param>
234+
/// <param name="trivia">When this method returns, contains the closest preceding end of line trivia, if found; otherwise, the default value.</param>
235+
/// <returns><see langword="true"/> if an end of line trivia was found; otherwise, <see langword="false"/>.</returns>
236+
private static bool TryGetPrecedingEndOfLineTrivia(this SyntaxToken token, out SyntaxTrivia trivia)
237+
{
238+
var leadingTrivia = token.LeadingTrivia;
239+
for (var i = leadingTrivia.Count - 1; i >= 0; i--)
240+
{
241+
if (leadingTrivia[i].IsKind(SyntaxKind.EndOfLineTrivia))
242+
{
243+
trivia = leadingTrivia[i];
244+
return true;
245+
}
246+
}
247+
248+
var prevToken = token.GetPreviousToken();
249+
var prevTrailingTrivia = prevToken.TrailingTrivia;
250+
for (var i = prevTrailingTrivia.Count - 1; i >= 0; i--)
251+
{
252+
if (prevTrailingTrivia[i].IsKind(SyntaxKind.EndOfLineTrivia))
253+
{
254+
trivia = prevTrailingTrivia[i];
255+
return true;
256+
}
257+
}
258+
259+
trivia = default;
260+
return false;
261+
}
155262
}
156263
}

StyleCop.Analyzers/StyleCop.Analyzers.CodeFixes/LayoutRules/SA1501CodeFixProvider.cs

Lines changed: 1 addition & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,7 @@ private static int DetermineIndentationLevel(IndentationSettings indentationSett
133133
}
134134
else
135135
{
136-
parentIndentationLevel = IndentationHelper.GetIndentationSteps(indentationSettings, GetFirstOnLineParent(parent));
136+
parentIndentationLevel = IndentationHelper.GetIndentationSteps(indentationSettings, parent.GetFirstOnLineAncestorOrSelf());
137137
}
138138

139139
return parentIndentationLevel;
@@ -275,18 +275,6 @@ private static SyntaxNode GetStatementParent(SyntaxNode node)
275275
return statementSyntax;
276276
}
277277

278-
private static SyntaxNode GetFirstOnLineParent(SyntaxNode parent)
279-
{
280-
// if the parent is not the first on a line, find the parent that is.
281-
// This mainly happens for 'else if' statements.
282-
while (!parent.GetFirstToken().IsFirstInLine())
283-
{
284-
parent = parent.Parent;
285-
}
286-
287-
return parent;
288-
}
289-
290278
private class FixAll : DocumentBasedFixAllProvider
291279
{
292280
public static FixAllProvider Instance { get; } =

StyleCop.Analyzers/StyleCop.Analyzers.CodeFixes/LayoutRules/SA1513CodeFixProvider.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,11 +61,13 @@ private static async Task<Document> GetTransformedDocumentAsync(Document documen
6161
private static async Task<SyntaxNode> GetTransformedDocumentAsync(Document document, ImmutableArray<Diagnostic> diagnostics, CancellationToken cancellationToken)
6262
{
6363
var root = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false);
64+
var text = await document.GetTextAsync(cancellationToken).ConfigureAwait(false);
65+
var options = document.Project.Solution.Workspace.Options;
6466
return root.ReplaceTokens(
6567
diagnostics.Select(diagnostic => root.FindToken(diagnostic.Location.SourceSpan.End)),
6668
(originalToken, rewrittenToken) =>
6769
{
68-
var endOfLineTrivia = rewrittenToken.GetPrecedingEndOfLineTrivia();
70+
var endOfLineTrivia = FormattingHelper.GetEndOfLineForCodeFix(rewrittenToken, text, options);
6971
var newTrivia = rewrittenToken.LeadingTrivia.Insert(0, endOfLineTrivia);
7072
return rewrittenToken.WithLeadingTrivia(newTrivia);
7173
});

StyleCop.Analyzers/StyleCop.Analyzers.CodeFixes/NamingRules/RenameToUpperCaseCodeFixProvider.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ Task<Document> RenameNamespace(CancellationToken cancellationToken)
7676
{
7777
IdentifierNameSyntax identifierSyntax = (IdentifierNameSyntax)token.Parent;
7878

79-
var newIdentifierSyntax = identifierSyntax.WithIdentifier(SyntaxFactory.Identifier(newName));
79+
var newIdentifierSyntax = identifierSyntax.WithIdentifier(SyntaxFactory.Identifier(newName)).WithTriviaFrom(identifierSyntax);
8080

8181
var newRoot = root.ReplaceNode(identifierSyntax, newIdentifierSyntax);
8282
return Task.FromResult(context.Document.WithSyntaxRoot(newRoot));

StyleCop.Analyzers/StyleCop.Analyzers.CodeFixes/OrderingRules/SA1212SA1213CodeFixProvider.cs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,8 @@ public override Task RegisterCodeFixesAsync(CodeFixContext context)
5353
private static async Task<Document> GetTransformedDocumentAsync(Document document, Diagnostic diagnostic, CancellationToken cancellationToken)
5454
{
5555
var syntaxRoot = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false);
56+
var sourceText = await document.GetTextAsync(cancellationToken).ConfigureAwait(false);
57+
var options = document.Project.Solution.Workspace.Options;
5658

5759
var accessorToken = syntaxRoot.FindToken(diagnostic.Location.SourceSpan.Start);
5860
var accessorList = (AccessorListSyntax)accessorToken.Parent.Parent;
@@ -67,8 +69,9 @@ private static async Task<Document> GetTransformedDocumentAsync(Document documen
6769

6870
if (HasLeadingBlankLines(secondAccessor))
6971
{
72+
var endOfLine = FormattingHelper.GetEndOfLineForCodeFix(firstAccesor.Keyword, sourceText, options);
7073
trackedFirstAccessor = syntaxRoot.GetCurrentNode(firstAccesor);
71-
var newFirstAccessor = trackedFirstAccessor.WithLeadingTrivia(new[] { SyntaxFactory.CarriageReturnLineFeed }.Concat(firstAccesor.GetFirstToken().WithoutLeadingBlankLines().LeadingTrivia));
74+
var newFirstAccessor = trackedFirstAccessor.WithLeadingTrivia(new[] { endOfLine }.Concat(firstAccesor.GetFirstToken().WithoutLeadingBlankLines().LeadingTrivia));
7275
syntaxRoot = syntaxRoot.ReplaceNode(trackedFirstAccessor, newFirstAccessor);
7376
}
7477

0 commit comments

Comments
 (0)