Skip to content

Commit f92d020

Browse files
paulirwinclaude
andcommitted
Add code fix for LuceneDev4002
Adds [MethodImpl(MethodImplOptions.NoInlining)] to the target method referenced by a DoesStackTraceContainMethod call. The fix resolves the target from the call's nameof() (or string-literal) arguments, locates its declaration in source, and edits that document — even when it lives in a different file from the call. Adds 'using System.Runtime.CompilerServices;' to the target's compilation unit if missing, and prepends the new attribute list ahead of any existing attributes on the method. Promote NoInliningAttributeHelper from internal to public so the code fix project can reuse it. Adds tests covering: target with using already present, target without the using, and target with an existing attribute. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent aba5ad8 commit f92d020

3 files changed

Lines changed: 544 additions & 1 deletion

File tree

Lines changed: 300 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,300 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing, software
13+
* distributed under the License is distributed on an "AS IS" BASIS,
14+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
* See the License for the specific language governing permissions and
16+
* limitations under the License.
17+
*/
18+
19+
using System.Collections.Generic;
20+
using System.Collections.Immutable;
21+
using System.Composition;
22+
using System.Linq;
23+
using System.Threading;
24+
using System.Threading.Tasks;
25+
using Lucene.Net.CodeAnalysis.Dev.LuceneDev4xxx;
26+
using Lucene.Net.CodeAnalysis.Dev.Utility;
27+
using Microsoft.CodeAnalysis;
28+
using Microsoft.CodeAnalysis.CodeActions;
29+
using Microsoft.CodeAnalysis.CodeFixes;
30+
using Microsoft.CodeAnalysis.CSharp;
31+
using Microsoft.CodeAnalysis.CSharp.Syntax;
32+
33+
namespace Lucene.Net.CodeAnalysis.Dev.CodeFixes.LuceneDev4xxx
34+
{
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+
[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(LuceneDev4002_StackTraceHelperNoInliningCodeFixProvider)), Shared]
43+
public sealed class LuceneDev4002_StackTraceHelperNoInliningCodeFixProvider : CodeFixProvider
44+
{
45+
private const string Title = "Add [MethodImpl(MethodImplOptions.NoInlining)] to the referenced method";
46+
47+
public override ImmutableArray<string> FixableDiagnosticIds =>
48+
ImmutableArray.Create(Descriptors.LuceneDev4002_MissingNoInlining.Id);
49+
50+
public override FixAllProvider GetFixAllProvider() => WellKnownFixAllProviders.BatchFixer;
51+
52+
public override async Task RegisterCodeFixesAsync(CodeFixContext context)
53+
{
54+
var diagnostic = context.Diagnostics[0];
55+
var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false);
56+
if (root is null)
57+
return;
58+
59+
var node = root.FindNode(diagnostic.Location.SourceSpan, getInnermostNodeForTie: true);
60+
var invocation = node as InvocationExpressionSyntax
61+
?? node.FirstAncestorOrSelf<InvocationExpressionSyntax>();
62+
if (invocation is null)
63+
return;
64+
65+
context.RegisterCodeFix(
66+
CodeAction.Create(
67+
Title,
68+
ct => AddNoInliningToTargetAsync(context.Document, invocation, ct),
69+
equivalenceKey: nameof(Title)),
70+
diagnostic);
71+
}
72+
73+
private static async Task<Solution> AddNoInliningToTargetAsync(
74+
Document document,
75+
InvocationExpressionSyntax invocation,
76+
CancellationToken cancellationToken)
77+
{
78+
var solution = document.Project.Solution;
79+
80+
var semanticModel = await document.GetSemanticModelAsync(cancellationToken).ConfigureAwait(false);
81+
if (semanticModel is null || invocation.ArgumentList.Arguments.Count != 2)
82+
return solution;
83+
84+
var classArg = invocation.ArgumentList.Arguments[0].Expression;
85+
var methodArg = invocation.ArgumentList.Arguments[1].Expression;
86+
87+
var (classNameValue, classTypeFromNameof) = ResolveClassReference(classArg, semanticModel);
88+
if (classNameValue is null)
89+
return solution;
90+
91+
var methodNameValue = ResolveMethodNameValue(methodArg, semanticModel);
92+
if (methodNameValue is null)
93+
return solution;
94+
95+
var compilation = semanticModel.Compilation;
96+
var targetType = classTypeFromNameof
97+
?? FindSourceTypeByName(compilation, classNameValue);
98+
if (targetType is null)
99+
return solution;
100+
101+
var methodImplAttrSymbol = compilation.GetTypeByMetadataName(
102+
"System.Runtime.CompilerServices.MethodImplAttribute");
103+
if (methodImplAttrSymbol is null)
104+
return solution;
105+
106+
// Find the first ordinary method that needs the attribute.
107+
MethodDeclarationSyntax? targetDecl = null;
108+
foreach (var member in targetType.GetMembers(methodNameValue).OfType<IMethodSymbol>())
109+
{
110+
if (member.MethodKind != MethodKind.Ordinary)
111+
continue;
112+
113+
foreach (var declRef in member.DeclaringSyntaxReferences)
114+
{
115+
if (declRef.GetSyntax(cancellationToken) is not MethodDeclarationSyntax methodDecl)
116+
continue;
117+
118+
var declSemantic = compilation.GetSemanticModel(methodDecl.SyntaxTree);
119+
if (NoInliningAttributeHelper.FindNoInliningAttribute(methodDecl, declSemantic, methodImplAttrSymbol) is not null)
120+
continue;
121+
if (NoInliningAttributeHelper.HasEmptyBody(methodDecl))
122+
continue;
123+
if (NoInliningAttributeHelper.IsInterfaceOrAbstractMethod(methodDecl))
124+
continue;
125+
126+
targetDecl = methodDecl;
127+
break;
128+
}
129+
130+
if (targetDecl is not null)
131+
break;
132+
}
133+
134+
if (targetDecl is null)
135+
return solution;
136+
137+
var targetTree = targetDecl.SyntaxTree;
138+
var targetDocument = solution.GetDocument(targetTree);
139+
if (targetDocument is null)
140+
return solution;
141+
142+
var targetRoot = await targetTree.GetRootAsync(cancellationToken).ConfigureAwait(false);
143+
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 newAttributeList = SyntaxFactory.AttributeList(SyntaxFactory.SingletonSeparatedList(attribute))
160+
.WithLeadingTrivia(targetDecl.GetLeadingTrivia())
161+
.WithTrailingTrivia(SyntaxFactory.EndOfLine("\n"), leadingIndent);
162+
163+
var newAttributeLists = SyntaxFactory.List<AttributeListSyntax>(
164+
new[] { newAttributeList }.Concat(targetDecl.AttributeLists));
165+
166+
var newMethodDecl = targetDecl
167+
.WithLeadingTrivia(SyntaxFactory.TriviaList())
168+
.WithAttributeLists(newAttributeLists);
169+
170+
var newTargetRoot = targetRoot.ReplaceNode(targetDecl, newMethodDecl);
171+
172+
// Add the using if missing.
173+
if (newTargetRoot is CompilationUnitSyntax compilationUnit)
174+
{
175+
const string requiredNs = "System.Runtime.CompilerServices";
176+
bool hasUsing = compilationUnit.Usings.Any(u => u.Name?.ToString() == requiredNs);
177+
if (!hasUsing)
178+
{
179+
var usingDirective = SyntaxFactory.UsingDirective(SyntaxFactory.ParseName(requiredNs))
180+
.WithTrailingTrivia(SyntaxFactory.EndOfLine("\n"));
181+
compilationUnit = compilationUnit.AddUsings(usingDirective);
182+
newTargetRoot = compilationUnit;
183+
}
184+
}
185+
186+
return solution.WithDocumentSyntaxRoot(targetDocument.Id, newTargetRoot);
187+
}
188+
189+
private static SyntaxTrivia ExtractLeadingIndentation(SyntaxNode node)
190+
{
191+
// Indentation = trailing whitespace of the leading trivia (after the
192+
// last newline). Used to align the new attribute list with the method.
193+
foreach (var t in node.GetLeadingTrivia().Reverse())
194+
{
195+
if (t.IsKind(SyntaxKind.WhitespaceTrivia))
196+
return t;
197+
if (t.IsKind(SyntaxKind.EndOfLineTrivia))
198+
break;
199+
}
200+
return SyntaxFactory.Whitespace("");
201+
}
202+
203+
// ---- Argument resolution (mirrors the analyzer) ----
204+
205+
private static (string? Name, INamedTypeSymbol? TypeFromNameof) ResolveClassReference(
206+
ExpressionSyntax expr,
207+
SemanticModel semantic)
208+
{
209+
if (expr is InvocationExpressionSyntax inv
210+
&& inv.Expression is IdentifierNameSyntax id
211+
&& id.Identifier.ValueText == "nameof"
212+
&& inv.ArgumentList.Arguments.Count == 1)
213+
{
214+
var inner = inv.ArgumentList.Arguments[0].Expression;
215+
var typeSymbol = semantic.GetTypeInfo(inner).Type as INamedTypeSymbol
216+
?? semantic.GetSymbolInfo(inner).Symbol as INamedTypeSymbol;
217+
if (typeSymbol is not null)
218+
return (typeSymbol.Name, typeSymbol);
219+
}
220+
221+
if (expr is LiteralExpressionSyntax literal && literal.IsKind(SyntaxKind.StringLiteralExpression))
222+
return (literal.Token.ValueText, null);
223+
224+
var constant = semantic.GetConstantValue(expr);
225+
if (constant.HasValue && constant.Value is string s)
226+
return (s, null);
227+
228+
return (null, null);
229+
}
230+
231+
private static string? ResolveMethodNameValue(ExpressionSyntax expr, SemanticModel semantic)
232+
{
233+
if (expr is InvocationExpressionSyntax inv
234+
&& inv.Expression is IdentifierNameSyntax id
235+
&& id.Identifier.ValueText == "nameof"
236+
&& inv.ArgumentList.Arguments.Count == 1)
237+
{
238+
var inner = inv.ArgumentList.Arguments[0].Expression;
239+
return ExtractRightmostIdentifier(inner);
240+
}
241+
242+
if (expr is LiteralExpressionSyntax literal && literal.IsKind(SyntaxKind.StringLiteralExpression))
243+
return literal.Token.ValueText;
244+
245+
var constant = semantic.GetConstantValue(expr);
246+
if (constant.HasValue && constant.Value is string s)
247+
return s;
248+
249+
return null;
250+
}
251+
252+
private static string? ExtractRightmostIdentifier(ExpressionSyntax expr)
253+
{
254+
return expr switch
255+
{
256+
IdentifierNameSyntax id => id.Identifier.ValueText,
257+
MemberAccessExpressionSyntax ma => ma.Name.Identifier.ValueText,
258+
_ => null,
259+
};
260+
}
261+
262+
private static INamedTypeSymbol? FindSourceTypeByName(Compilation compilation, string typeName)
263+
{
264+
foreach (var type in EnumerateAllTypes(compilation.Assembly.GlobalNamespace))
265+
{
266+
if (type.Name == typeName)
267+
return type;
268+
}
269+
return null;
270+
}
271+
272+
private static IEnumerable<INamedTypeSymbol> EnumerateAllTypes(INamespaceSymbol ns)
273+
{
274+
foreach (var member in ns.GetMembers())
275+
{
276+
if (member is INamedTypeSymbol type)
277+
{
278+
yield return type;
279+
foreach (var nested in EnumerateNestedTypes(type))
280+
yield return nested;
281+
}
282+
else if (member is INamespaceSymbol child)
283+
{
284+
foreach (var t in EnumerateAllTypes(child))
285+
yield return t;
286+
}
287+
}
288+
}
289+
290+
private static IEnumerable<INamedTypeSymbol> EnumerateNestedTypes(INamedTypeSymbol type)
291+
{
292+
foreach (var nested in type.GetTypeMembers())
293+
{
294+
yield return nested;
295+
foreach (var deeper in EnumerateNestedTypes(nested))
296+
yield return deeper;
297+
}
298+
}
299+
}
300+
}

src/Lucene.Net.CodeAnalysis.Dev/LuceneDev4xxx/NoInliningAttributeHelper.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323

2424
namespace Lucene.Net.CodeAnalysis.Dev.LuceneDev4xxx
2525
{
26-
internal static class NoInliningAttributeHelper
26+
public static class NoInliningAttributeHelper
2727
{
2828
public static AttributeSyntax? FindNoInliningAttribute(
2929
MethodDeclarationSyntax methodDecl,

0 commit comments

Comments
 (0)