|
29 | 29 | using Microsoft.CodeAnalysis.CodeFixes; |
30 | 30 | using Microsoft.CodeAnalysis.CSharp; |
31 | 31 | using Microsoft.CodeAnalysis.CSharp.Syntax; |
| 32 | +using Microsoft.CodeAnalysis.Formatting; |
| 33 | +using Microsoft.CodeAnalysis.Options; |
| 34 | +using Microsoft.CodeAnalysis.Text; |
32 | 35 |
|
33 | 36 | namespace Lucene.Net.CodeAnalysis.Dev.CodeFixes.LuceneDev4xxx |
34 | 37 | { |
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> |
42 | 38 | [ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(LuceneDev4002_StackTraceHelperNoInliningCodeFixProvider)), Shared] |
43 | 39 | public sealed class LuceneDev4002_StackTraceHelperNoInliningCodeFixProvider : CodeFixProvider |
44 | 40 | { |
45 | 41 | private const string Title = "Add [MethodImpl(MethodImplOptions.NoInlining)] to the referenced method"; |
| 42 | + private const string CompilerServicesNamespace = "System.Runtime.CompilerServices"; |
46 | 43 |
|
47 | 44 | public override ImmutableArray<string> FixableDiagnosticIds => |
48 | 45 | ImmutableArray.Create(Descriptors.LuceneDev4002_MissingNoInlining.Id); |
@@ -103,7 +100,6 @@ private static async Task<Solution> AddNoInliningToTargetAsync( |
103 | 100 | if (methodImplAttrSymbol is null) |
104 | 101 | return solution; |
105 | 102 |
|
106 | | - // Find the first ordinary method that needs the attribute. |
107 | 103 | MethodDeclarationSyntax? targetDecl = null; |
108 | 104 | foreach (var member in targetType.GetMembers(methodNameValue).OfType<IMethodSymbol>()) |
109 | 105 | { |
@@ -141,76 +137,73 @@ private static async Task<Solution> AddNoInliningToTargetAsync( |
141 | 137 |
|
142 | 138 | var targetRoot = await targetTree.GetRootAsync(cancellationToken).ConfigureAwait(false); |
143 | 139 |
|
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); |
170 | 157 |
|
171 | 158 | var newTargetRoot = targetRoot.ReplaceNode(targetDecl, newMethodDecl); |
172 | 159 |
|
173 | 160 | // 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)) |
175 | 163 | { |
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; |
185 | 169 | } |
186 | 170 |
|
187 | | - return solution.WithDocumentSyntaxRoot(targetDocument.Id, newTargetRoot); |
188 | | - } |
| 171 | + var newTargetDocument = targetDocument.WithSyntaxRoot(newTargetRoot); |
189 | 172 |
|
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; |
200 | 189 | } |
201 | 190 |
|
202 | | - private static SyntaxTrivia ExtractLeadingIndentation(SyntaxNode node) |
| 191 | + private static string DetectNewLine(SourceText text) |
203 | 192 | { |
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) |
207 | 194 | { |
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"; |
212 | 205 | } |
213 | | - return SyntaxFactory.Whitespace(""); |
| 206 | + return "\n"; |
214 | 207 | } |
215 | 208 |
|
216 | 209 | // ---- Argument resolution (mirrors the analyzer) ---- |
|
0 commit comments