Skip to content

Commit f80d806

Browse files
committed
Fix SA1102 hard-coding of CRLF
1 parent 4d92a69 commit f80d806

5 files changed

Lines changed: 123 additions & 39 deletions

File tree

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/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/ReadabilityRules/SA1102CodeFixProvider.cs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ namespace StyleCop.Analyzers.ReadabilityRules
1313
using Microsoft.CodeAnalysis;
1414
using Microsoft.CodeAnalysis.CodeActions;
1515
using Microsoft.CodeAnalysis.CodeFixes;
16-
using Microsoft.CodeAnalysis.CSharp;
1716
using StyleCop.Analyzers.Helpers;
1817

1918
/// <summary>
@@ -52,16 +51,19 @@ public override Task RegisterCodeFixesAsync(CodeFixContext context)
5251
private static async Task<Document> GetTransformedDocumentAsync(Document document, Diagnostic diagnostic, CancellationToken cancellationToken)
5352
{
5453
var syntaxRoot = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false);
54+
var text = await document.GetTextAsync(cancellationToken).ConfigureAwait(false);
5555
var token = syntaxRoot.FindToken(diagnostic.Location.SourceSpan.Start);
5656

5757
var settings = SettingsHelper.GetStyleCopSettingsInCodeFix(document.Project.AnalyzerOptions, syntaxRoot.SyntaxTree, cancellationToken);
5858
var indentationTrivia = QueryIndentationHelpers.GetQueryIndentationTrivia(settings.Indentation, token);
5959

6060
var precedingToken = token.GetPreviousToken();
61+
var options = document.Project.Solution.Workspace.Options;
62+
var endOfLineTrivia = FormattingHelper.GetEndOfLineForCodeFix(token, text, options);
6163

6264
var replaceMap = new Dictionary<SyntaxToken, SyntaxToken>()
6365
{
64-
[precedingToken] = precedingToken.WithTrailingTrivia(SyntaxFactory.CarriageReturnLineFeed),
66+
[precedingToken] = precedingToken.WithTrailingTrivia(endOfLineTrivia),
6567
[token] = token.WithLeadingTrivia(indentationTrivia),
6668
};
6769

StyleCop.Analyzers/StyleCop.Analyzers.Test/ReadabilityRules/SA1102UnitTests.cs

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,15 +9,18 @@ namespace StyleCop.Analyzers.Test.ReadabilityRules
99
using System.Threading.Tasks;
1010
using Microsoft.CodeAnalysis.Testing;
1111
using StyleCop.Analyzers.ReadabilityRules;
12+
using StyleCop.Analyzers.Test.Helpers;
1213
using Xunit;
1314
using static StyleCop.Analyzers.Test.Verifiers.StyleCopCodeFixVerifier<
1415
StyleCop.Analyzers.ReadabilityRules.SA110xQueryClauses,
1516
StyleCop.Analyzers.ReadabilityRules.SA1102CodeFixProvider>;
1617

1718
public class SA1102UnitTests
1819
{
19-
[Fact]
20-
public async Task TestSelectOnSeparateLineWithAdditionalEmptyLineAsync()
20+
[Theory]
21+
[InlineData("\n")]
22+
[InlineData("\r\n")]
23+
public async Task TestSelectOnSeparateLineWithAdditionalEmptyLineAsync(string lineEnding)
2124
{
2225
var testCode = @"
2326
using System.Linq;
@@ -31,9 +34,9 @@ public void Bar()
3134
from m in source
3235
where m > 0
3336
34-
select m;
37+
{|#0:select|} m;
3538
}
36-
}";
39+
}".ReplaceLineEndings(lineEnding);
3740

3841
var fixedTestCode = @"
3942
using System.Linq;
@@ -48,9 +51,9 @@ from m in source
4851
where m > 0
4952
select m;
5053
}
51-
}";
54+
}".ReplaceLineEndings(lineEnding);
5255

53-
DiagnosticResult expected = Diagnostic(SA110xQueryClauses.SA1102Descriptor).WithLocation(13, 13);
56+
DiagnosticResult expected = Diagnostic(SA110xQueryClauses.SA1102Descriptor).WithLocation(0);
5457

5558
await VerifyCSharpFixAsync(testCode, expected, fixedTestCode, CancellationToken.None).ConfigureAwait(false);
5659
}

StyleCop.Analyzers/StyleCop.Analyzers/Helpers/TokenHelper.cs

Lines changed: 0 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -175,35 +175,5 @@ internal static bool IsFollowedByWhitespace(this SyntaxToken token)
175175
triviaList = token.GetNextToken().LeadingTrivia;
176176
return triviaList.Count > 0 && triviaList.First().IsKind(SyntaxKind.WhitespaceTrivia);
177177
}
178-
179-
/// <summary>
180-
/// Returns the closest end of line trivia preceding the <paramref name="token"/>.
181-
/// This currently only looks immediately before the specified token.
182-
/// </summary>
183-
/// <param name="token">The token to process.</param>
184-
/// <returns>The closest preceding end of line trivia, or <see cref="SyntaxFactory.CarriageReturnLineFeed"/> if none is found.</returns>
185-
internal static SyntaxTrivia GetPrecedingEndOfLineTrivia(this SyntaxToken token)
186-
{
187-
var leadingTrivia = token.LeadingTrivia;
188-
for (var i = leadingTrivia.Count - 1; i >= 0; i--)
189-
{
190-
if (leadingTrivia[i].IsKind(SyntaxKind.EndOfLineTrivia))
191-
{
192-
return leadingTrivia[i];
193-
}
194-
}
195-
196-
var prevToken = token.GetPreviousToken();
197-
var prevTrailingTrivia = prevToken.TrailingTrivia;
198-
for (var i = prevTrailingTrivia.Count - 1; i >= 0; i--)
199-
{
200-
if (prevTrailingTrivia[i].IsKind(SyntaxKind.EndOfLineTrivia))
201-
{
202-
return prevTrailingTrivia[i];
203-
}
204-
}
205-
206-
return SyntaxFactory.CarriageReturnLineFeed;
207-
}
208178
}
209179
}

0 commit comments

Comments
 (0)