Skip to content

Commit 3fd68b3

Browse files
paulirwinclaude
andcommitted
Split LuceneDev4002 into its own analyzer class
LuceneDev4002 (StackTraceHelper-driven NoInlining requirement) is a distinct rule from 4000/4001 (NoInlining-as-no-op detection): it has a different trigger (invocation vs. method declaration) and no code fix. Extract it into its own analyzer + test class. Shared logic for recognising the [MethodImpl(MethodImplOptions.NoInlining)] attribute, empty bodies, and interface/abstract methods moves into NoInliningAttributeHelper. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 30ded09 commit 3fd68b3

8 files changed

Lines changed: 597 additions & 545 deletions

File tree

src/Lucene.Net.CodeAnalysis.Dev.CodeFixes/LuceneDev4xxx/LuceneDev4000_4001_4002_NoInliningCodeFixProvider.cs renamed to src/Lucene.Net.CodeAnalysis.Dev.CodeFixes/LuceneDev4xxx/LuceneDev4000_4001_NoInliningOnNoOpCodeFixProvider.cs

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -28,17 +28,11 @@
2828

2929
namespace Lucene.Net.CodeAnalysis.Dev.CodeFixes.LuceneDev4xxx
3030
{
31-
[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(LuceneDev4000_4001_4002_NoInliningCodeFixProvider)), Shared]
32-
public sealed class LuceneDev4000_4001_4002_NoInliningCodeFixProvider : CodeFixProvider
31+
[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(LuceneDev4000_4001_NoInliningOnNoOpCodeFixProvider)), Shared]
32+
public sealed class LuceneDev4000_4001_NoInliningOnNoOpCodeFixProvider : CodeFixProvider
3333
{
3434
private const string TitleRemoveAttribute = "Remove [MethodImpl(MethodImplOptions.NoInlining)]";
3535

36-
// Note: LuceneDev4002 has no code fix here. Its diagnostic is reported on the
37-
// referenced method declaration but is triggered from a separate
38-
// StackTraceHelper.DoesStackTraceContainMethod invocation — Roslyn treats this
39-
// as a "non-local" diagnostic, which the code fix pipeline does not permit
40-
// fixing automatically. The IDE still surfaces the warning on the declaration
41-
// and the user adds the attribute manually.
4236
public override ImmutableArray<string> FixableDiagnosticIds =>
4337
ImmutableArray.Create(
4438
Descriptors.LuceneDev4000_NoInliningHasNoEffect.Id,

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

Lines changed: 0 additions & 370 deletions
This file was deleted.
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
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.Immutable;
20+
using System.Linq;
21+
using Lucene.Net.CodeAnalysis.Dev.Utility;
22+
using Microsoft.CodeAnalysis;
23+
using Microsoft.CodeAnalysis.CSharp;
24+
using Microsoft.CodeAnalysis.CSharp.Syntax;
25+
using Microsoft.CodeAnalysis.Diagnostics;
26+
27+
namespace Lucene.Net.CodeAnalysis.Dev.LuceneDev4xxx
28+
{
29+
/// <summary>
30+
/// Reports cases where [MethodImpl(MethodImplOptions.NoInlining)] is applied but
31+
/// has no useful effect:
32+
/// - LuceneDev4000: on an interface or abstract method (the attribute is not
33+
/// inherited, so it has no effect on the implementation).
34+
/// - LuceneDev4001: on an empty-bodied method (it cannot appear above any
35+
/// stack frame, so preventing inlining gives no benefit).
36+
/// </summary>
37+
[DiagnosticAnalyzer(LanguageNames.CSharp)]
38+
public sealed class LuceneDev4000_4001_NoInliningOnNoOpAnalyzer : DiagnosticAnalyzer
39+
{
40+
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics
41+
=> ImmutableArray.Create(
42+
Descriptors.LuceneDev4000_NoInliningHasNoEffect,
43+
Descriptors.LuceneDev4001_NoInliningOnEmptyMethod);
44+
45+
public override void Initialize(AnalysisContext context)
46+
{
47+
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze);
48+
context.EnableConcurrentExecution();
49+
50+
context.RegisterCompilationStartAction(compilationCtx =>
51+
{
52+
var methodImplAttrSymbol = compilationCtx.Compilation.GetTypeByMetadataName(
53+
"System.Runtime.CompilerServices.MethodImplAttribute");
54+
if (methodImplAttrSymbol is null)
55+
return;
56+
57+
compilationCtx.RegisterSyntaxNodeAction(
58+
ctx => Analyze(ctx, methodImplAttrSymbol),
59+
SyntaxKind.MethodDeclaration);
60+
});
61+
}
62+
63+
private static void Analyze(SyntaxNodeAnalysisContext ctx, INamedTypeSymbol methodImplAttrSymbol)
64+
{
65+
var methodDecl = (MethodDeclarationSyntax)ctx.Node;
66+
67+
var attribute = NoInliningAttributeHelper.FindNoInliningAttribute(
68+
methodDecl, ctx.SemanticModel, methodImplAttrSymbol);
69+
if (attribute is null)
70+
return;
71+
72+
// 4000: interface or abstract method
73+
if (NoInliningAttributeHelper.IsInterfaceOrAbstractMethod(methodDecl))
74+
{
75+
ctx.ReportDiagnostic(Diagnostic.Create(
76+
Descriptors.LuceneDev4000_NoInliningHasNoEffect,
77+
attribute.GetLocation(),
78+
methodDecl.Identifier.ValueText));
79+
return;
80+
}
81+
82+
// 4001: empty-bodied method
83+
if (NoInliningAttributeHelper.HasEmptyBody(methodDecl))
84+
{
85+
ctx.ReportDiagnostic(Diagnostic.Create(
86+
Descriptors.LuceneDev4001_NoInliningOnEmptyMethod,
87+
attribute.GetLocation(),
88+
methodDecl.Identifier.ValueText));
89+
}
90+
}
91+
}
92+
}
Lines changed: 224 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,224 @@
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.Linq;
22+
using Lucene.Net.CodeAnalysis.Dev.Utility;
23+
using Microsoft.CodeAnalysis;
24+
using Microsoft.CodeAnalysis.CSharp;
25+
using Microsoft.CodeAnalysis.CSharp.Syntax;
26+
using Microsoft.CodeAnalysis.Diagnostics;
27+
28+
namespace Lucene.Net.CodeAnalysis.Dev.LuceneDev4xxx
29+
{
30+
/// <summary>
31+
/// LuceneDev4002: Reports methods referenced by the 2-argument
32+
/// StackTraceHelper.DoesStackTraceContainMethod(className, methodName) overload
33+
/// that lack [MethodImpl(MethodImplOptions.NoInlining)]. Without it the JIT may
34+
/// inline the method out of the stack trace, silently breaking the check.
35+
///
36+
/// This analyzer has no code fix: the diagnostic is reported on the referenced
37+
/// method declaration but is triggered by a separate invocation, which Roslyn
38+
/// treats as a non-local diagnostic and does not allow code fixes for.
39+
/// </summary>
40+
[DiagnosticAnalyzer(LanguageNames.CSharp)]
41+
public sealed class LuceneDev4002_StackTraceHelperNoInliningAnalyzer : DiagnosticAnalyzer
42+
{
43+
private const string StackTraceHelperFullName = "Lucene.Net.Support.ExceptionHandling.StackTraceHelper";
44+
private const string DoesStackTraceContainMethodName = "DoesStackTraceContainMethod";
45+
46+
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics
47+
=> ImmutableArray.Create(Descriptors.LuceneDev4002_MissingNoInlining);
48+
49+
public override void Initialize(AnalysisContext context)
50+
{
51+
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze);
52+
context.EnableConcurrentExecution();
53+
54+
context.RegisterCompilationStartAction(compilationCtx =>
55+
{
56+
var methodImplAttrSymbol = compilationCtx.Compilation.GetTypeByMetadataName(
57+
"System.Runtime.CompilerServices.MethodImplAttribute");
58+
if (methodImplAttrSymbol is null)
59+
return;
60+
61+
compilationCtx.RegisterSyntaxNodeAction(
62+
ctx => AnalyzeInvocation(ctx, methodImplAttrSymbol),
63+
SyntaxKind.InvocationExpression);
64+
});
65+
}
66+
67+
private static void AnalyzeInvocation(SyntaxNodeAnalysisContext ctx, INamedTypeSymbol methodImplAttrSymbol)
68+
{
69+
var invocation = (InvocationExpressionSyntax)ctx.Node;
70+
71+
if (invocation.Expression is not MemberAccessExpressionSyntax memberAccess)
72+
return;
73+
if (memberAccess.Name.Identifier.ValueText != DoesStackTraceContainMethodName)
74+
return;
75+
76+
// Only the 2-argument overload (className, methodName) is in scope.
77+
if (invocation.ArgumentList.Arguments.Count != 2)
78+
return;
79+
80+
var symbol = ctx.SemanticModel.GetSymbolInfo(invocation).Symbol as IMethodSymbol;
81+
if (symbol is null)
82+
return;
83+
if (symbol.ContainingType?.ToDisplayString() != StackTraceHelperFullName)
84+
return;
85+
86+
var classArg = invocation.ArgumentList.Arguments[0].Expression;
87+
var methodArg = invocation.ArgumentList.Arguments[1].Expression;
88+
89+
var (classNameValue, classTypeFromNameof) = ResolveClassReference(classArg, ctx.SemanticModel);
90+
if (classNameValue is null)
91+
return;
92+
93+
var methodNameValue = ResolveMethodNameValue(methodArg, ctx.SemanticModel);
94+
if (methodNameValue is null)
95+
return;
96+
97+
var targetType = classTypeFromNameof
98+
?? FindSourceTypeByName(ctx.SemanticModel.Compilation, classNameValue);
99+
if (targetType is null)
100+
return;
101+
102+
foreach (var member in targetType.GetMembers(methodNameValue).OfType<IMethodSymbol>())
103+
{
104+
if (member.MethodKind != MethodKind.Ordinary)
105+
continue;
106+
107+
foreach (var declRef in member.DeclaringSyntaxReferences)
108+
{
109+
if (declRef.GetSyntax(ctx.CancellationToken) is not MethodDeclarationSyntax methodDecl)
110+
continue;
111+
112+
if (NoInliningAttributeHelper.FindNoInliningAttribute(methodDecl, ctx.SemanticModel, methodImplAttrSymbol) is not null)
113+
continue;
114+
115+
if (NoInliningAttributeHelper.HasEmptyBody(methodDecl))
116+
continue;
117+
118+
if (NoInliningAttributeHelper.IsInterfaceOrAbstractMethod(methodDecl))
119+
continue;
120+
121+
ctx.ReportDiagnostic(Diagnostic.Create(
122+
Descriptors.LuceneDev4002_MissingNoInlining,
123+
methodDecl.GetLocation(),
124+
methodDecl.Identifier.ValueText));
125+
}
126+
}
127+
}
128+
129+
private static (string? Name, INamedTypeSymbol? TypeFromNameof) ResolveClassReference(
130+
ExpressionSyntax expr,
131+
SemanticModel semantic)
132+
{
133+
if (expr is InvocationExpressionSyntax inv
134+
&& inv.Expression is IdentifierNameSyntax id
135+
&& id.Identifier.ValueText == "nameof"
136+
&& inv.ArgumentList.Arguments.Count == 1)
137+
{
138+
var inner = inv.ArgumentList.Arguments[0].Expression;
139+
var typeSymbol = semantic.GetTypeInfo(inner).Type as INamedTypeSymbol
140+
?? semantic.GetSymbolInfo(inner).Symbol as INamedTypeSymbol;
141+
if (typeSymbol is not null)
142+
return (typeSymbol.Name, typeSymbol);
143+
}
144+
145+
if (expr is LiteralExpressionSyntax literal && literal.IsKind(SyntaxKind.StringLiteralExpression))
146+
return (literal.Token.ValueText, null);
147+
148+
var constant = semantic.GetConstantValue(expr);
149+
if (constant.HasValue && constant.Value is string s)
150+
return (s, null);
151+
152+
return (null, null);
153+
}
154+
155+
private static string? ResolveMethodNameValue(ExpressionSyntax expr, SemanticModel semantic)
156+
{
157+
if (expr is InvocationExpressionSyntax inv
158+
&& inv.Expression is IdentifierNameSyntax id
159+
&& id.Identifier.ValueText == "nameof"
160+
&& inv.ArgumentList.Arguments.Count == 1)
161+
{
162+
var inner = inv.ArgumentList.Arguments[0].Expression;
163+
return ExtractRightmostIdentifier(inner);
164+
}
165+
166+
if (expr is LiteralExpressionSyntax literal && literal.IsKind(SyntaxKind.StringLiteralExpression))
167+
return literal.Token.ValueText;
168+
169+
var constant = semantic.GetConstantValue(expr);
170+
if (constant.HasValue && constant.Value is string s)
171+
return s;
172+
173+
return null;
174+
}
175+
176+
private static string? ExtractRightmostIdentifier(ExpressionSyntax expr)
177+
{
178+
return expr switch
179+
{
180+
IdentifierNameSyntax id => id.Identifier.ValueText,
181+
MemberAccessExpressionSyntax ma => ma.Name.Identifier.ValueText,
182+
_ => null,
183+
};
184+
}
185+
186+
private static INamedTypeSymbol? FindSourceTypeByName(Compilation compilation, string typeName)
187+
{
188+
foreach (var type in EnumerateAllTypes(compilation.Assembly.GlobalNamespace))
189+
{
190+
if (type.Name == typeName)
191+
return type;
192+
}
193+
return null;
194+
}
195+
196+
private static IEnumerable<INamedTypeSymbol> EnumerateAllTypes(INamespaceSymbol ns)
197+
{
198+
foreach (var member in ns.GetMembers())
199+
{
200+
if (member is INamedTypeSymbol type)
201+
{
202+
yield return type;
203+
foreach (var nested in EnumerateNestedTypes(type))
204+
yield return nested;
205+
}
206+
else if (member is INamespaceSymbol child)
207+
{
208+
foreach (var t in EnumerateAllTypes(child))
209+
yield return t;
210+
}
211+
}
212+
}
213+
214+
private static IEnumerable<INamedTypeSymbol> EnumerateNestedTypes(INamedTypeSymbol type)
215+
{
216+
foreach (var nested in type.GetTypeMembers())
217+
{
218+
yield return nested;
219+
foreach (var deeper in EnumerateNestedTypes(nested))
220+
yield return deeper;
221+
}
222+
}
223+
}
224+
}

0 commit comments

Comments
 (0)