Skip to content

Commit 5fa7e4d

Browse files
paulirwinclaude
andcommitted
Implement LuceneDev4000-4002 NoInlining Analyzers & CodeFix (#1097)
Adds three Roslyn analyzers under the new Performance category covering [MethodImpl(MethodImplOptions.NoInlining)] usage: - LuceneDev4000: Reports when NoInlining is applied to an interface or abstract method. The MethodImpl attribute is not inherited, so the attribute has no effect on the implementation. - LuceneDev4001: Reports when NoInlining is applied to an empty-bodied method. An empty body cannot appear above any frame in a stack trace, so preventing inlining provides no benefit. - LuceneDev4002: Reports when a method referenced by the 2-argument StackTraceHelper.DoesStackTraceContainMethod(className, methodName) overload is missing NoInlining. Without it, the JIT may inline the method out of the stack trace, silently breaking the check. A code fix is provided for 4000 and 4001 (remove the attribute). 4002 has no automated fix because the diagnostic is reported on a method declaration triggered by an invocation in another scope, which Roslyn treats as a non-local diagnostic and refuses to fix automatically. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 429d5b2 commit 5fa7e4d

7 files changed

Lines changed: 1023 additions & 0 deletions

File tree

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
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.Composition;
21+
using System.Threading;
22+
using System.Threading.Tasks;
23+
using Lucene.Net.CodeAnalysis.Dev.Utility;
24+
using Microsoft.CodeAnalysis;
25+
using Microsoft.CodeAnalysis.CodeActions;
26+
using Microsoft.CodeAnalysis.CodeFixes;
27+
using Microsoft.CodeAnalysis.CSharp.Syntax;
28+
29+
namespace Lucene.Net.CodeAnalysis.Dev.CodeFixes.LuceneDev4xxx
30+
{
31+
[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(LuceneDev4000_4001_4002_NoInliningCodeFixProvider)), Shared]
32+
public sealed class LuceneDev4000_4001_4002_NoInliningCodeFixProvider : CodeFixProvider
33+
{
34+
private const string TitleRemoveAttribute = "Remove [MethodImpl(MethodImplOptions.NoInlining)]";
35+
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.
42+
public override ImmutableArray<string> FixableDiagnosticIds =>
43+
ImmutableArray.Create(
44+
Descriptors.LuceneDev4000_NoInliningHasNoEffect.Id,
45+
Descriptors.LuceneDev4001_NoInliningOnEmptyMethod.Id);
46+
47+
public override FixAllProvider GetFixAllProvider() => WellKnownFixAllProviders.BatchFixer;
48+
49+
public override async Task RegisterCodeFixesAsync(CodeFixContext context)
50+
{
51+
var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false);
52+
if (root is null)
53+
return;
54+
55+
var diagnostic = context.Diagnostics[0];
56+
var node = root.FindNode(diagnostic.Location.SourceSpan, getInnermostNodeForTie: true);
57+
58+
var attribute = node as AttributeSyntax
59+
?? node.FirstAncestorOrSelf<AttributeSyntax>();
60+
if (attribute is null)
61+
return;
62+
63+
context.RegisterCodeFix(
64+
CodeAction.Create(
65+
TitleRemoveAttribute,
66+
ct => RemoveAttributeAsync(context.Document, attribute, ct),
67+
equivalenceKey: nameof(TitleRemoveAttribute) + diagnostic.Id),
68+
diagnostic);
69+
}
70+
71+
private static async Task<Document> RemoveAttributeAsync(
72+
Document document,
73+
AttributeSyntax attribute,
74+
CancellationToken cancellationToken)
75+
{
76+
var root = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false);
77+
if (root is null)
78+
return document;
79+
80+
if (attribute.Parent is AttributeListSyntax attrList)
81+
{
82+
SyntaxNode newRoot;
83+
if (attrList.Attributes.Count == 1)
84+
{
85+
newRoot = root.RemoveNode(attrList, SyntaxRemoveOptions.KeepNoTrivia)!;
86+
}
87+
else
88+
{
89+
var newList = attrList.WithAttributes(attrList.Attributes.Remove(attribute));
90+
newRoot = root.ReplaceNode(attrList, newList);
91+
}
92+
return document.WithSyntaxRoot(newRoot);
93+
}
94+
95+
return document;
96+
}
97+
}
98+
}

src/Lucene.Net.CodeAnalysis.Dev/AnalyzerReleases.Unshipped.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@ Rule ID | Category | Severity | Notes
44
--------------|----------|----------|-----------------------------------------------------------------------------------------------------------------------------------------------------------
55
LuceneDev1007 | Design | Warning | Generic Dictionary<TKey, TValue> indexer should not be used to retrieve values because it may throw KeyNotFoundException (value type value)
66
LuceneDev1008 | Design | Warning | Generic Dictionary<TKey, TValue> indexer should not be used to retrieve values because it may throw KeyNotFoundException (reference type value)
7+
LuceneDev4000 | Performance | Warning | [MethodImpl(MethodImplOptions.NoInlining)] has no effect on interface or abstract methods (the attribute is not inherited)
8+
LuceneDev4001 | Performance | Warning | [MethodImpl(MethodImplOptions.NoInlining)] should not be used on empty-bodied methods (no benefit, harms performance)
9+
LuceneDev4002 | Performance | Warning | Methods referenced by the 2-argument StackTraceHelper.DoesStackTraceContainMethod overload should be marked [MethodImpl(MethodImplOptions.NoInlining)] when the method body is non-empty
710
LuceneDev6000 | Usage | Info | IDictionary indexer may be used to retrieve values, but must be checked for null before using the value
811
LuceneDev6001 | Usage | Error | Missing StringComparison argument in String overloads of StartsWith/EndsWith/IndexOf/LastIndexOf; must use Ordinal/OrdinalIgnoreCase
912
LuceneDev6002 | Usage | Error | Invalid StringComparison value in String overloads of StartsWith/EndsWith/IndexOf/LastIndexOf; only Ordinal/OrdinalIgnoreCase allowed

0 commit comments

Comments
 (0)