Skip to content

Commit 30ded09

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 66f539c commit 30ded09

7 files changed

Lines changed: 1029 additions & 6 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: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,6 @@ 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-
LuceneDev6000 | Usage | Info | IDictionary indexer may be used to retrieve values, but must be checked for null before using the value
8-
LuceneDev6001 | Usage | Error | Missing StringComparison argument in String overloads of StartsWith/EndsWith/IndexOf/LastIndexOf; must use Ordinal/OrdinalIgnoreCase
9-
LuceneDev6002 | Usage | Error | Invalid StringComparison value in String overloads of StartsWith/EndsWith/IndexOf/LastIndexOf; only Ordinal/OrdinalIgnoreCase allowed
10-
LuceneDev6003 | Usage | Warning | Redundant StringComparison.Ordinal argument in Span overloads of StartsWith/EndsWith/IndexOf/LastIndexOf; should be removed
11-
LuceneDev6004 | Usage | Error | Invalid StringComparison value in Span overloads of StartsWith/EndsWith/IndexOf/LastIndexOf; only Ordinal or OrdinalIgnoreCase allowed
12-
LuceneDev6005 | Usage | Info | Single-character string arguments should use the char overload of StartsWith/EndsWith/IndexOf/LastIndexOf instead of a string
137
LuceneDev2000 | Globalization | Warning | Numeric Parse/TryParse without IFormatProvider; specify CultureInfo.InvariantCulture (or CurrentCulture) explicitly
148
LuceneDev2001 | Globalization | Warning | Numeric ToString/TryFormat without IFormatProvider; specify CultureInfo.InvariantCulture (or CurrentCulture) explicitly
159
LuceneDev2002 | Globalization | Warning | System.Convert numeric to/from string without IFormatProvider; specify CultureInfo.InvariantCulture (or CurrentCulture) explicitly
@@ -19,3 +13,12 @@ LuceneDev2005 | Globalization | Warning | Numeric value concatenated with string
1913
LuceneDev2006 | Globalization | Warning | Numeric value interpolated into string formats using current culture; use FormattableString.Invariant or wrap with .ToString(CultureInfo.InvariantCulture) explicitly
2014
LuceneDev2007 | Globalization | Warning | Numeric format/parse passes a non-invariant IFormatProvider; suppress when intentional
2115
LuceneDev2008 | Globalization | Disabled | Numeric format/parse passes CultureInfo.InvariantCulture (review-sweep aid; default Info severity, disabled by default)
16+
LuceneDev4000 | Performance | Warning | [MethodImpl(MethodImplOptions.NoInlining)] has no effect on interface or abstract methods (the attribute is not inherited)
17+
LuceneDev4001 | Performance | Warning | [MethodImpl(MethodImplOptions.NoInlining)] should not be used on empty-bodied methods (no benefit, harms performance)
18+
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
19+
LuceneDev6000 | Usage | Info | IDictionary indexer may be used to retrieve values, but must be checked for null before using the value
20+
LuceneDev6001 | Usage | Error | Missing StringComparison argument in String overloads of StartsWith/EndsWith/IndexOf/LastIndexOf; must use Ordinal/OrdinalIgnoreCase
21+
LuceneDev6002 | Usage | Error | Invalid StringComparison value in String overloads of StartsWith/EndsWith/IndexOf/LastIndexOf; only Ordinal/OrdinalIgnoreCase allowed
22+
LuceneDev6003 | Usage | Warning | Redundant StringComparison.Ordinal argument in Span overloads of StartsWith/EndsWith/IndexOf/LastIndexOf; should be removed
23+
LuceneDev6004 | Usage | Error | Invalid StringComparison value in Span overloads of StartsWith/EndsWith/IndexOf/LastIndexOf; only Ordinal or OrdinalIgnoreCase allowed
24+
LuceneDev6005 | Usage | Info | Single-character string arguments should use the char overload of StartsWith/EndsWith/IndexOf/LastIndexOf instead of a string

0 commit comments

Comments
 (0)