Skip to content

Commit 81cce2f

Browse files
paulirwinclaude
andcommitted
Simplify 4002 code fix and rely on Formatter for layout
Replaces hand-rolled trivia construction with a single Formatter pass. The new attribute list and using directive are built without trivia and formatted via Formatter.FormatAsync, with the workspace NewLine option explicitly set from the source file's existing line endings so the output doesn't mix CRLF and LF on platforms where Environment.NewLine disagrees with the file (e.g. CRLF source on Linux CI). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent a70b521 commit 81cce2f

1 file changed

Lines changed: 58 additions & 65 deletions

File tree

src/Lucene.Net.CodeAnalysis.Dev.CodeFixes/LuceneDev4xxx/LuceneDev4002_StackTraceHelperNoInliningCodeFixProvider.cs

Lines changed: 58 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -29,20 +29,17 @@
2929
using Microsoft.CodeAnalysis.CodeFixes;
3030
using Microsoft.CodeAnalysis.CSharp;
3131
using Microsoft.CodeAnalysis.CSharp.Syntax;
32+
using Microsoft.CodeAnalysis.Formatting;
33+
using Microsoft.CodeAnalysis.Options;
34+
using Microsoft.CodeAnalysis.Text;
3235

3336
namespace Lucene.Net.CodeAnalysis.Dev.CodeFixes.LuceneDev4xxx
3437
{
35-
/// <summary>
36-
/// Code fix for LuceneDev4002: adds [MethodImpl(MethodImplOptions.NoInlining)]
37-
/// to the target method declaration referenced by the
38-
/// StackTraceHelper.DoesStackTraceContainMethod call. Adds
39-
/// `using System.Runtime.CompilerServices;` to the target's compilation unit
40-
/// if missing.
41-
/// </summary>
4238
[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(LuceneDev4002_StackTraceHelperNoInliningCodeFixProvider)), Shared]
4339
public sealed class LuceneDev4002_StackTraceHelperNoInliningCodeFixProvider : CodeFixProvider
4440
{
4541
private const string Title = "Add [MethodImpl(MethodImplOptions.NoInlining)] to the referenced method";
42+
private const string CompilerServicesNamespace = "System.Runtime.CompilerServices";
4643

4744
public override ImmutableArray<string> FixableDiagnosticIds =>
4845
ImmutableArray.Create(Descriptors.LuceneDev4002_MissingNoInlining.Id);
@@ -103,7 +100,6 @@ private static async Task<Solution> AddNoInliningToTargetAsync(
103100
if (methodImplAttrSymbol is null)
104101
return solution;
105102

106-
// Find the first ordinary method that needs the attribute.
107103
MethodDeclarationSyntax? targetDecl = null;
108104
foreach (var member in targetType.GetMembers(methodNameValue).OfType<IMethodSymbol>())
109105
{
@@ -141,76 +137,73 @@ private static async Task<Solution> AddNoInliningToTargetAsync(
141137

142138
var targetRoot = await targetTree.GetRootAsync(cancellationToken).ConfigureAwait(false);
143139

144-
// Build [MethodImpl(MethodImplOptions.NoInlining)] as its own attribute
145-
// list. Place it ahead of any existing lists, copying the method's
146-
// leading trivia onto our new list and re-attaching one indent's worth
147-
// of trivia between the list and the original method position.
148-
var attribute = SyntaxFactory.Attribute(
149-
SyntaxFactory.IdentifierName("MethodImpl"),
150-
SyntaxFactory.AttributeArgumentList(
151-
SyntaxFactory.SingletonSeparatedList(
152-
SyntaxFactory.AttributeArgument(
153-
SyntaxFactory.MemberAccessExpression(
154-
SyntaxKind.SimpleMemberAccessExpression,
155-
SyntaxFactory.IdentifierName("MethodImplOptions"),
156-
SyntaxFactory.IdentifierName("NoInlining"))))));
157-
158-
var leadingIndent = ExtractLeadingIndentation(targetDecl);
159-
var endOfLine = DetectEndOfLine(targetRoot);
160-
var newAttributeList = SyntaxFactory.AttributeList(SyntaxFactory.SingletonSeparatedList(attribute))
161-
.WithLeadingTrivia(targetDecl.GetLeadingTrivia())
162-
.WithTrailingTrivia(endOfLine, leadingIndent);
163-
164-
var newAttributeLists = SyntaxFactory.List<AttributeListSyntax>(
165-
new[] { newAttributeList }.Concat(targetDecl.AttributeLists));
166-
167-
var newMethodDecl = targetDecl
168-
.WithLeadingTrivia(SyntaxFactory.TriviaList())
169-
.WithAttributeLists(newAttributeLists);
140+
// Build [MethodImpl(MethodImplOptions.NoInlining)] with no manual trivia,
141+
// and let the Formatter annotation handle indentation and line endings.
142+
var newAttributeList = SyntaxFactory.AttributeList(
143+
SyntaxFactory.SingletonSeparatedList(
144+
SyntaxFactory.Attribute(
145+
SyntaxFactory.IdentifierName("MethodImpl"),
146+
SyntaxFactory.AttributeArgumentList(
147+
SyntaxFactory.SingletonSeparatedList(
148+
SyntaxFactory.AttributeArgument(
149+
SyntaxFactory.MemberAccessExpression(
150+
SyntaxKind.SimpleMemberAccessExpression,
151+
SyntaxFactory.IdentifierName("MethodImplOptions"),
152+
SyntaxFactory.IdentifierName("NoInlining"))))))))
153+
.WithAdditionalAnnotations(Formatter.Annotation);
154+
155+
var newAttributeLists = targetDecl.AttributeLists.Insert(0, newAttributeList);
156+
var newMethodDecl = targetDecl.WithAttributeLists(newAttributeLists);
170157

171158
var newTargetRoot = targetRoot.ReplaceNode(targetDecl, newMethodDecl);
172159

173160
// Add the using if missing.
174-
if (newTargetRoot is CompilationUnitSyntax compilationUnit)
161+
if (newTargetRoot is CompilationUnitSyntax compilationUnit
162+
&& !compilationUnit.Usings.Any(u => u.Name?.ToString() == CompilerServicesNamespace))
175163
{
176-
const string requiredNs = "System.Runtime.CompilerServices";
177-
bool hasUsing = compilationUnit.Usings.Any(u => u.Name?.ToString() == requiredNs);
178-
if (!hasUsing)
179-
{
180-
var usingDirective = SyntaxFactory.UsingDirective(SyntaxFactory.ParseName(requiredNs))
181-
.WithTrailingTrivia(endOfLine);
182-
compilationUnit = compilationUnit.AddUsings(usingDirective);
183-
newTargetRoot = compilationUnit;
184-
}
164+
var usingDirective = SyntaxFactory.UsingDirective(
165+
SyntaxFactory.ParseName(CompilerServicesNamespace))
166+
.WithAdditionalAnnotations(Formatter.Annotation);
167+
compilationUnit = compilationUnit.AddUsings(usingDirective);
168+
newTargetRoot = compilationUnit;
185169
}
186170

187-
return solution.WithDocumentSyntaxRoot(targetDocument.Id, newTargetRoot);
188-
}
171+
var newTargetDocument = targetDocument.WithSyntaxRoot(newTargetRoot);
189172

190-
private static SyntaxTrivia DetectEndOfLine(SyntaxNode root)
191-
{
192-
// Match the source's existing line-ending convention so the fixed
193-
// output doesn't mix CRLF and LF.
194-
foreach (var trivia in root.DescendantTrivia())
195-
{
196-
if (trivia.IsKind(SyntaxKind.EndOfLineTrivia))
197-
return trivia;
198-
}
199-
return SyntaxFactory.EndOfLine("\n");
173+
// Honor the source file's existing line ending convention so the fix
174+
// doesn't introduce mixed line endings (the workspace's NewLine option
175+
// otherwise defaults to Environment.NewLine, which can disagree with
176+
// a source file that uses the opposite convention).
177+
var sourceText = await targetTree.GetTextAsync(cancellationToken).ConfigureAwait(false);
178+
var newLine = DetectNewLine(sourceText);
179+
var options = newTargetDocument.Project.Solution.Workspace.Options
180+
.WithChangedOption(FormattingOptions.NewLine, LanguageNames.CSharp, newLine);
181+
182+
var formatted = await Formatter.FormatAsync(
183+
newTargetDocument,
184+
Formatter.Annotation,
185+
options,
186+
cancellationToken).ConfigureAwait(false);
187+
188+
return formatted.Project.Solution;
200189
}
201190

202-
private static SyntaxTrivia ExtractLeadingIndentation(SyntaxNode node)
191+
private static string DetectNewLine(SourceText text)
203192
{
204-
// Indentation = trailing whitespace of the leading trivia (after the
205-
// last newline). Used to align the new attribute list with the method.
206-
foreach (var t in node.GetLeadingTrivia().Reverse())
193+
foreach (var line in text.Lines)
207194
{
208-
if (t.IsKind(SyntaxKind.WhitespaceTrivia))
209-
return t;
210-
if (t.IsKind(SyntaxKind.EndOfLineTrivia))
211-
break;
195+
var lineBreakLength = line.EndIncludingLineBreak - line.End;
196+
if (lineBreakLength == 0)
197+
continue;
198+
var firstChar = text[line.End];
199+
if (firstChar == '\r' && lineBreakLength == 2)
200+
return "\r\n";
201+
if (firstChar == '\n')
202+
return "\n";
203+
if (firstChar == '\r')
204+
return "\r";
212205
}
213-
return SyntaxFactory.Whitespace("");
206+
return "\n";
214207
}
215208

216209
// ---- Argument resolution (mirrors the analyzer) ----

0 commit comments

Comments
 (0)