diff --git a/src/Lucene.Net.CodeAnalysis.Dev.CodeFixes/LuceneDev4xxx/LuceneDev4000_4001_NoInliningOnNoOpCodeFixProvider.cs b/src/Lucene.Net.CodeAnalysis.Dev.CodeFixes/LuceneDev4xxx/LuceneDev4000_4001_NoInliningOnNoOpCodeFixProvider.cs new file mode 100644 index 0000000..4072500 --- /dev/null +++ b/src/Lucene.Net.CodeAnalysis.Dev.CodeFixes/LuceneDev4xxx/LuceneDev4000_4001_NoInliningOnNoOpCodeFixProvider.cs @@ -0,0 +1,132 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System.Collections.Immutable; +using System.Composition; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Lucene.Net.CodeAnalysis.Dev.Utility; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CodeActions; +using Microsoft.CodeAnalysis.CodeFixes; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; + +namespace Lucene.Net.CodeAnalysis.Dev.CodeFixes.LuceneDev4xxx +{ + [ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(LuceneDev4000_4001_NoInliningOnNoOpCodeFixProvider)), Shared] + public sealed class LuceneDev4000_4001_NoInliningOnNoOpCodeFixProvider : CodeFixProvider + { + private const string TitleRemoveAttribute = "Remove [MethodImpl(MethodImplOptions.NoInlining)]"; + + public override ImmutableArray FixableDiagnosticIds => + ImmutableArray.Create( + Descriptors.LuceneDev4000_NoInliningHasNoEffect.Id, + Descriptors.LuceneDev4001_NoInliningOnEmptyMethod.Id); + + public override FixAllProvider GetFixAllProvider() => WellKnownFixAllProviders.BatchFixer; + + public override async Task RegisterCodeFixesAsync(CodeFixContext context) + { + var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false); + if (root is null) + return; + + var diagnostic = context.Diagnostics[0]; + var node = root.FindNode(diagnostic.Location.SourceSpan, getInnermostNodeForTie: true); + + var attribute = node as AttributeSyntax + ?? node.FirstAncestorOrSelf(); + if (attribute is null) + return; + + context.RegisterCodeFix( + CodeAction.Create( + TitleRemoveAttribute, + ct => RemoveAttributeAsync(context.Document, attribute, ct), + equivalenceKey: nameof(TitleRemoveAttribute) + diagnostic.Id), + diagnostic); + } + + private static async Task RemoveAttributeAsync( + Document document, + AttributeSyntax attribute, + CancellationToken cancellationToken) + { + var root = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false); + if (root is null) + return document; + + if (attribute.Parent is not AttributeListSyntax attrList) + return document; + + SyntaxNode newRoot; + if (attrList.Attributes.Count > 1) + { + // [Foo, MethodImpl(NoInlining), Bar] → [Foo, Bar] + var newList = attrList.WithAttributes(attrList.Attributes.Remove(attribute)); + newRoot = root.ReplaceNode(attrList, newList); + return document.WithSyntaxRoot(newRoot); + } + + // Removing the whole [ ... ] list. + // + // Trivia handling: the attribute list's leading trivia is typically + // (newline)(indent)[comment(newline)(indent)]*. We want to keep any + // comments (and the newline that ends each one) but drop the final + // whitespace block — which is just the indentation for the now-removed + // attribute. The token following the list already carries its own + // newline+indent, so leaving that whitespace in would double-indent the + // next line. We move the trimmed trivia onto the next token. + var leading = attrList.GetLeadingTrivia(); + int trim = leading.Count; + while (trim > 0 && leading[trim - 1].IsKind(SyntaxKind.WhitespaceTrivia)) + { + trim--; + } + var triviaToKeep = SyntaxFactory.TriviaList(leading.Take(trim)); + + // Locate the parent that owns this attribute list. Use the parent's + // AttributeLists collection (e.g. on MethodDeclarationSyntax) so that + // removing the list and re-attaching trivia happens in a single step + // and preserves indentation of the surrounding declaration. + if (attrList.Parent is MemberDeclarationSyntax member) + { + var newAttrLists = member.AttributeLists.Remove(attrList); + MemberDeclarationSyntax newMember = member.WithAttributeLists(newAttrLists); + + // Prepend the trivia we want to keep (e.g. comments) to the new + // first token of the member declaration. + if (triviaToKeep.Count > 0) + { + var firstToken = newMember.GetFirstToken(); + var combined = triviaToKeep.AddRange(firstToken.LeadingTrivia); + newMember = newMember.ReplaceToken(firstToken, firstToken.WithLeadingTrivia(combined)); + } + + newRoot = root.ReplaceNode(member, newMember); + return document.WithSyntaxRoot(newRoot); + } + + // Fallback: just remove the list, dropping its trivia. + newRoot = root.RemoveNode(attrList, SyntaxRemoveOptions.KeepNoTrivia)!; + return document.WithSyntaxRoot(newRoot); + } + } +} diff --git a/src/Lucene.Net.CodeAnalysis.Dev.CodeFixes/LuceneDev4xxx/LuceneDev4002_StackTraceHelperNoInliningCodeFixProvider.cs b/src/Lucene.Net.CodeAnalysis.Dev.CodeFixes/LuceneDev4xxx/LuceneDev4002_StackTraceHelperNoInliningCodeFixProvider.cs new file mode 100644 index 0000000..68bb458 --- /dev/null +++ b/src/Lucene.Net.CodeAnalysis.Dev.CodeFixes/LuceneDev4xxx/LuceneDev4002_StackTraceHelperNoInliningCodeFixProvider.cs @@ -0,0 +1,282 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System.Collections.Immutable; +using System.Composition; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Lucene.Net.CodeAnalysis.Dev.LuceneDev4xxx; +using Lucene.Net.CodeAnalysis.Dev.Utility; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CodeActions; +using Microsoft.CodeAnalysis.CodeFixes; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Formatting; +using Microsoft.CodeAnalysis.Options; +using Microsoft.CodeAnalysis.Text; + +namespace Lucene.Net.CodeAnalysis.Dev.CodeFixes.LuceneDev4xxx +{ + [ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(LuceneDev4002_StackTraceHelperNoInliningCodeFixProvider)), Shared] + public sealed class LuceneDev4002_StackTraceHelperNoInliningCodeFixProvider : CodeFixProvider + { + private const string Title = "Add [MethodImpl(MethodImplOptions.NoInlining)] to the referenced method"; + private const string CompilerServicesNamespace = "System.Runtime.CompilerServices"; + + public override ImmutableArray FixableDiagnosticIds => + ImmutableArray.Create(Descriptors.LuceneDev4002_MissingNoInlining.Id); + + public override FixAllProvider GetFixAllProvider() => WellKnownFixAllProviders.BatchFixer; + + public override async Task RegisterCodeFixesAsync(CodeFixContext context) + { + var diagnostic = context.Diagnostics[0]; + var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false); + if (root is null) + return; + + var node = root.FindNode(diagnostic.Location.SourceSpan, getInnermostNodeForTie: true); + var invocation = node as InvocationExpressionSyntax + ?? node.FirstAncestorOrSelf(); + if (invocation is null) + return; + + context.RegisterCodeFix( + CodeAction.Create( + Title, + ct => AddNoInliningToTargetAsync(context.Document, invocation, ct), + equivalenceKey: nameof(Title)), + diagnostic); + } + + private static async Task AddNoInliningToTargetAsync( + Document document, + InvocationExpressionSyntax invocation, + CancellationToken cancellationToken) + { + var solution = document.Project.Solution; + + var semanticModel = await document.GetSemanticModelAsync(cancellationToken).ConfigureAwait(false); + if (semanticModel is null || invocation.ArgumentList.Arguments.Count != 2) + return solution; + + var classArg = invocation.ArgumentList.Arguments[0].Expression; + var methodArg = invocation.ArgumentList.Arguments[1].Expression; + + var (classNameValue, classTypeFromNameof) = ResolveClassReference(classArg, semanticModel); + if (classNameValue is null) + return solution; + + var methodNameValue = ResolveMethodNameValue(methodArg, semanticModel); + if (methodNameValue is null) + return solution; + + var compilation = semanticModel.Compilation; + var targetType = classTypeFromNameof + ?? FindSourceTypeByName(compilation, classNameValue); + if (targetType is null) + return solution; + + var methodImplAttrSymbol = compilation.GetTypeByMetadataName( + "System.Runtime.CompilerServices.MethodImplAttribute"); + if (methodImplAttrSymbol is null) + return solution; + + MethodDeclarationSyntax? targetDecl = null; + foreach (var member in targetType.GetMembers(methodNameValue).OfType()) + { + if (member.MethodKind != MethodKind.Ordinary) + continue; + + if (NoInliningAttributeHelper.HasNoInliningAttribute(member, methodImplAttrSymbol)) + continue; + + foreach (var declRef in member.DeclaringSyntaxReferences) + { + if (declRef.GetSyntax(cancellationToken) is not MethodDeclarationSyntax methodDecl) + continue; + + if (NoInliningAttributeHelper.HasEmptyBody(methodDecl)) + continue; + if (NoInliningAttributeHelper.IsInterfaceOrAbstractMethod(methodDecl)) + continue; + + targetDecl = methodDecl; + break; + } + + if (targetDecl is not null) + break; + } + + if (targetDecl is null) + return solution; + + var targetTree = targetDecl.SyntaxTree; + var targetDocument = solution.GetDocument(targetTree); + if (targetDocument is null) + return solution; + + var targetRoot = await targetTree.GetRootAsync(cancellationToken).ConfigureAwait(false); + + // Build [MethodImpl(MethodImplOptions.NoInlining)] with no manual trivia, + // and let the Formatter annotation handle indentation and line endings. + var newAttributeList = SyntaxFactory.AttributeList( + SyntaxFactory.SingletonSeparatedList( + SyntaxFactory.Attribute( + SyntaxFactory.IdentifierName("MethodImpl"), + SyntaxFactory.AttributeArgumentList( + SyntaxFactory.SingletonSeparatedList( + SyntaxFactory.AttributeArgument( + SyntaxFactory.MemberAccessExpression( + SyntaxKind.SimpleMemberAccessExpression, + SyntaxFactory.IdentifierName("MethodImplOptions"), + SyntaxFactory.IdentifierName("NoInlining")))))))) + .WithAdditionalAnnotations(Formatter.Annotation); + + var newAttributeLists = targetDecl.AttributeLists.Insert(0, newAttributeList); + var newMethodDecl = targetDecl.WithAttributeLists(newAttributeLists); + + var newTargetRoot = targetRoot.ReplaceNode(targetDecl, newMethodDecl); + + // Add the using if missing. + if (newTargetRoot is CompilationUnitSyntax compilationUnit + && !compilationUnit.Usings.Any(u => u.Name?.ToString() == CompilerServicesNamespace)) + { + var usingDirective = SyntaxFactory.UsingDirective( + SyntaxFactory.ParseName(CompilerServicesNamespace)) + .WithAdditionalAnnotations(Formatter.Annotation); + compilationUnit = compilationUnit.AddUsings(usingDirective); + newTargetRoot = compilationUnit; + } + + var newTargetDocument = targetDocument.WithSyntaxRoot(newTargetRoot); + + // Honor the source file's existing line ending convention so the fix + // doesn't introduce mixed line endings (the workspace's NewLine option + // otherwise defaults to Environment.NewLine, which can disagree with + // a source file that uses the opposite convention). + var sourceText = await targetTree.GetTextAsync(cancellationToken).ConfigureAwait(false); + var newLine = DetectNewLine(sourceText); + var options = newTargetDocument.Project.Solution.Workspace.Options + .WithChangedOption(FormattingOptions.NewLine, LanguageNames.CSharp, newLine); + + var formatted = await Formatter.FormatAsync( + newTargetDocument, + Formatter.Annotation, + options, + cancellationToken).ConfigureAwait(false); + + return formatted.Project.Solution; + } + + private static string DetectNewLine(SourceText text) + { + foreach (var line in text.Lines) + { + var lineBreakLength = line.EndIncludingLineBreak - line.End; + if (lineBreakLength == 0) + continue; + var firstChar = text[line.End]; + if (firstChar == '\r' && lineBreakLength == 2) + return "\r\n"; + if (firstChar == '\n') + return "\n"; + if (firstChar == '\r') + return "\r"; + } + return "\n"; + } + + // ---- Argument resolution (mirrors the analyzer) ---- + + private static (string? Name, INamedTypeSymbol? TypeFromNameof) ResolveClassReference( + ExpressionSyntax expr, + SemanticModel semantic) + { + if (expr is InvocationExpressionSyntax inv + && inv.Expression is IdentifierNameSyntax id + && id.Identifier.ValueText == "nameof" + && inv.ArgumentList.Arguments.Count == 1) + { + var inner = inv.ArgumentList.Arguments[0].Expression; + var typeSymbol = semantic.GetTypeInfo(inner).Type as INamedTypeSymbol + ?? semantic.GetSymbolInfo(inner).Symbol as INamedTypeSymbol; + if (typeSymbol is not null) + return (typeSymbol.Name, typeSymbol); + } + + if (expr is LiteralExpressionSyntax literal && literal.IsKind(SyntaxKind.StringLiteralExpression)) + return (literal.Token.ValueText, null); + + var constant = semantic.GetConstantValue(expr); + if (constant.HasValue && constant.Value is string s) + return (s, null); + + return (null, null); + } + + private static string? ResolveMethodNameValue(ExpressionSyntax expr, SemanticModel semantic) + { + if (expr is InvocationExpressionSyntax inv + && inv.Expression is IdentifierNameSyntax id + && id.Identifier.ValueText == "nameof" + && inv.ArgumentList.Arguments.Count == 1) + { + var inner = inv.ArgumentList.Arguments[0].Expression; + return ExtractRightmostIdentifier(inner); + } + + if (expr is LiteralExpressionSyntax literal && literal.IsKind(SyntaxKind.StringLiteralExpression)) + return literal.Token.ValueText; + + var constant = semantic.GetConstantValue(expr); + if (constant.HasValue && constant.Value is string s) + return s; + + return null; + } + + private static string? ExtractRightmostIdentifier(ExpressionSyntax expr) + { + return expr switch + { + IdentifierNameSyntax id => id.Identifier.ValueText, + MemberAccessExpressionSyntax ma => ma.Name.Identifier.ValueText, + _ => null, + }; + } + + private static INamedTypeSymbol? FindSourceTypeByName(Compilation compilation, string typeName) + { + // Use Roslyn's symbol-name index instead of walking every namespace. + // Restrict to the source assembly so we don't match metadata types. + foreach (var symbol in compilation.GetSymbolsWithName(n => n == typeName, SymbolFilter.Type)) + { + if (symbol is INamedTypeSymbol type + && SymbolEqualityComparer.Default.Equals(type.ContainingAssembly, compilation.Assembly)) + { + return type; + } + } + return null; + } + } +} diff --git a/src/Lucene.Net.CodeAnalysis.Dev.Sample/LuceneDev4xxx/LuceneDev4000_4001_NoInliningOnNoOpSample.cs b/src/Lucene.Net.CodeAnalysis.Dev.Sample/LuceneDev4xxx/LuceneDev4000_4001_NoInliningOnNoOpSample.cs new file mode 100644 index 0000000..88d47dc --- /dev/null +++ b/src/Lucene.Net.CodeAnalysis.Dev.Sample/LuceneDev4xxx/LuceneDev4000_4001_NoInliningOnNoOpSample.cs @@ -0,0 +1,56 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +using System.Runtime.CompilerServices; + +namespace Lucene.Net.CodeAnalysis.Dev.Sample.LuceneDev4xxx; + +public interface ILuceneDev4000Sample +{ + // Triggers LuceneDev4000 (Warning): MethodImpl is not inherited, so NoInlining + // on an interface member has no effect on the implementation. + [MethodImpl(MethodImplOptions.NoInlining)] + void DoWork(); +} + +public abstract class LuceneDev4000Sample +{ + // Triggers LuceneDev4000 (Warning): same reason — abstract methods are not + // bodies that the JIT can inline. + [MethodImpl(MethodImplOptions.NoInlining)] + public abstract void DoWork(); +} + +public class LuceneDev4001Sample +{ + // Triggers LuceneDev4001 (Warning): empty-bodied methods cannot appear above + // any frame in a stack trace, so preventing inlining provides no benefit and + // only harms performance. + [MethodImpl(MethodImplOptions.NoInlining)] + public void EmptyMethod() + { + } + + // No diagnostic: regular method with a non-empty body. + [MethodImpl(MethodImplOptions.NoInlining)] + public int RealWork() + { + return 42; + } +} diff --git a/src/Lucene.Net.CodeAnalysis.Dev.Sample/LuceneDev4xxx/LuceneDev4002_StackTraceHelperNoInliningSample.cs b/src/Lucene.Net.CodeAnalysis.Dev.Sample/LuceneDev4xxx/LuceneDev4002_StackTraceHelperNoInliningSample.cs new file mode 100644 index 0000000..aef4840 --- /dev/null +++ b/src/Lucene.Net.CodeAnalysis.Dev.Sample/LuceneDev4xxx/LuceneDev4002_StackTraceHelperNoInliningSample.cs @@ -0,0 +1,81 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +using System.Runtime.CompilerServices; +using Lucene.Net.Support.ExceptionHandling; + +// Stub mirroring Lucene.Net.Support.ExceptionHandling.StackTraceHelper so the +// sample compiles in isolation. The analyzer matches by full type name. +// Suppress LuceneDev1005 — that rule flags real public types in Lucene.Net.Support; +// this is just a local stand-in for the sample. +#pragma warning disable LuceneDev1005 +namespace Lucene.Net.Support.ExceptionHandling +{ + public static class StackTraceHelper + { + public static bool DoesStackTraceContainMethod(string methodName) => false; + public static bool DoesStackTraceContainMethod(string className, string methodName) => false; + } +} +#pragma warning restore LuceneDev1005 + +namespace Lucene.Net.CodeAnalysis.Dev.Sample.LuceneDev4xxx +{ + public class LuceneDev4002_TargetWithoutNoInlining + { + // Triggers LuceneDev4002 (Warning): this method is referenced by the + // 2-argument StackTraceHelper.DoesStackTraceContainMethod overload below + // but is missing [MethodImpl(MethodImplOptions.NoInlining)]. The JIT may + // inline it out of the stack trace, silently breaking the check. + public void Merge() + { + System.Console.WriteLine(1 + 2); + } + } + + public class LuceneDev4002_TargetWithNoInlining + { + // No diagnostic: the attribute is already applied. + [MethodImpl(MethodImplOptions.NoInlining)] + public void Merge() + { + System.Console.WriteLine(1 + 2); + } + } + + public class LuceneDev4002_Caller + { + public void Check() + { + // The 2-argument overload triggers LuceneDev4002 on the referenced method. + if (StackTraceHelper.DoesStackTraceContainMethod( + nameof(LuceneDev4002_TargetWithoutNoInlining), + nameof(LuceneDev4002_TargetWithoutNoInlining.Merge))) + { + } + + // No diagnostic for this target — already has NoInlining. + if (StackTraceHelper.DoesStackTraceContainMethod( + nameof(LuceneDev4002_TargetWithNoInlining), + nameof(LuceneDev4002_TargetWithNoInlining.Merge))) + { + } + } + } +} diff --git a/src/Lucene.Net.CodeAnalysis.Dev/AnalyzerReleases.Unshipped.md b/src/Lucene.Net.CodeAnalysis.Dev/AnalyzerReleases.Unshipped.md index 5b384e3..80e7811 100644 --- a/src/Lucene.Net.CodeAnalysis.Dev/AnalyzerReleases.Unshipped.md +++ b/src/Lucene.Net.CodeAnalysis.Dev/AnalyzerReleases.Unshipped.md @@ -4,12 +4,6 @@ Rule ID | Category | Severity | Notes --------------|----------|----------|----------------------------------------------------------------------------------------------------------------------------------------------------------- LuceneDev1007 | Design | Warning | Generic Dictionary indexer should not be used to retrieve values because it may throw KeyNotFoundException (value type value) LuceneDev1008 | Design | Warning | Generic Dictionary indexer should not be used to retrieve values because it may throw KeyNotFoundException (reference type value) -LuceneDev6000 | Usage | Info | IDictionary indexer may be used to retrieve values, but must be checked for null before using the value -LuceneDev6001 | Usage | Error | Missing StringComparison argument in String overloads of StartsWith/EndsWith/IndexOf/LastIndexOf; must use Ordinal/OrdinalIgnoreCase -LuceneDev6002 | Usage | Error | Invalid StringComparison value in String overloads of StartsWith/EndsWith/IndexOf/LastIndexOf; only Ordinal/OrdinalIgnoreCase allowed -LuceneDev6003 | Usage | Warning | Redundant StringComparison.Ordinal argument in Span overloads of StartsWith/EndsWith/IndexOf/LastIndexOf; should be removed -LuceneDev6004 | Usage | Error | Invalid StringComparison value in Span overloads of StartsWith/EndsWith/IndexOf/LastIndexOf; only Ordinal or OrdinalIgnoreCase allowed -LuceneDev6005 | Usage | Info | Single-character string arguments should use the char overload of StartsWith/EndsWith/IndexOf/LastIndexOf instead of a string LuceneDev2000 | Globalization | Warning | Numeric Parse/TryParse without IFormatProvider; specify CultureInfo.InvariantCulture (or CurrentCulture) explicitly LuceneDev2001 | Globalization | Warning | Numeric ToString/TryFormat without IFormatProvider; specify CultureInfo.InvariantCulture (or CurrentCulture) explicitly 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 LuceneDev2006 | Globalization | Warning | Numeric value interpolated into string formats using current culture; use FormattableString.Invariant or wrap with .ToString(CultureInfo.InvariantCulture) explicitly LuceneDev2007 | Globalization | Warning | Numeric format/parse passes a non-invariant IFormatProvider; suppress when intentional LuceneDev2008 | Globalization | Disabled | Numeric format/parse passes CultureInfo.InvariantCulture (review-sweep aid; default Info severity, disabled by default) +LuceneDev4000 | Performance | Warning | [MethodImpl(MethodImplOptions.NoInlining)] has no effect on interface or abstract methods (the attribute is not inherited) +LuceneDev4001 | Performance | Warning | [MethodImpl(MethodImplOptions.NoInlining)] should not be used on empty-bodied methods (no benefit, harms performance) +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 +LuceneDev6000 | Usage | Info | IDictionary indexer may be used to retrieve values, but must be checked for null before using the value +LuceneDev6001 | Usage | Error | Missing StringComparison argument in String overloads of StartsWith/EndsWith/IndexOf/LastIndexOf; must use Ordinal/OrdinalIgnoreCase +LuceneDev6002 | Usage | Error | Invalid StringComparison value in String overloads of StartsWith/EndsWith/IndexOf/LastIndexOf; only Ordinal/OrdinalIgnoreCase allowed +LuceneDev6003 | Usage | Warning | Redundant StringComparison.Ordinal argument in Span overloads of StartsWith/EndsWith/IndexOf/LastIndexOf; should be removed +LuceneDev6004 | Usage | Error | Invalid StringComparison value in Span overloads of StartsWith/EndsWith/IndexOf/LastIndexOf; only Ordinal or OrdinalIgnoreCase allowed +LuceneDev6005 | Usage | Info | Single-character string arguments should use the char overload of StartsWith/EndsWith/IndexOf/LastIndexOf instead of a string diff --git a/src/Lucene.Net.CodeAnalysis.Dev/LuceneDev4xxx/LuceneDev4000_4001_NoInliningOnNoOpAnalyzer.cs b/src/Lucene.Net.CodeAnalysis.Dev/LuceneDev4xxx/LuceneDev4000_4001_NoInliningOnNoOpAnalyzer.cs new file mode 100644 index 0000000..ea50d8c --- /dev/null +++ b/src/Lucene.Net.CodeAnalysis.Dev/LuceneDev4xxx/LuceneDev4000_4001_NoInliningOnNoOpAnalyzer.cs @@ -0,0 +1,92 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System.Collections.Immutable; +using System.Linq; +using Lucene.Net.CodeAnalysis.Dev.Utility; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Diagnostics; + +namespace Lucene.Net.CodeAnalysis.Dev.LuceneDev4xxx +{ + /// + /// Reports cases where [MethodImpl(MethodImplOptions.NoInlining)] is applied but + /// has no useful effect: + /// - LuceneDev4000: on an interface or abstract method (the attribute is not + /// inherited, so it has no effect on the implementation). + /// - LuceneDev4001: on an empty-bodied method (it cannot appear above any + /// stack frame, so preventing inlining gives no benefit). + /// + [DiagnosticAnalyzer(LanguageNames.CSharp)] + public sealed class LuceneDev4000_4001_NoInliningOnNoOpAnalyzer : DiagnosticAnalyzer + { + public override ImmutableArray SupportedDiagnostics + => ImmutableArray.Create( + Descriptors.LuceneDev4000_NoInliningHasNoEffect, + Descriptors.LuceneDev4001_NoInliningOnEmptyMethod); + + public override void Initialize(AnalysisContext context) + { + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze); + context.EnableConcurrentExecution(); + + context.RegisterCompilationStartAction(compilationCtx => + { + var methodImplAttrSymbol = compilationCtx.Compilation.GetTypeByMetadataName( + "System.Runtime.CompilerServices.MethodImplAttribute"); + if (methodImplAttrSymbol is null) + return; + + compilationCtx.RegisterSyntaxNodeAction( + ctx => Analyze(ctx, methodImplAttrSymbol), + SyntaxKind.MethodDeclaration); + }); + } + + private static void Analyze(SyntaxNodeAnalysisContext ctx, INamedTypeSymbol methodImplAttrSymbol) + { + var methodDecl = (MethodDeclarationSyntax)ctx.Node; + + var attribute = NoInliningAttributeHelper.FindNoInliningAttribute( + methodDecl, ctx.SemanticModel, methodImplAttrSymbol); + if (attribute is null) + return; + + // 4000: interface or abstract method + if (NoInliningAttributeHelper.IsInterfaceOrAbstractMethod(methodDecl)) + { + ctx.ReportDiagnostic(Diagnostic.Create( + Descriptors.LuceneDev4000_NoInliningHasNoEffect, + attribute.GetLocation(), + methodDecl.Identifier.ValueText)); + return; + } + + // 4001: empty-bodied method + if (NoInliningAttributeHelper.HasEmptyBody(methodDecl)) + { + ctx.ReportDiagnostic(Diagnostic.Create( + Descriptors.LuceneDev4001_NoInliningOnEmptyMethod, + attribute.GetLocation(), + methodDecl.Identifier.ValueText)); + } + } + } +} diff --git a/src/Lucene.Net.CodeAnalysis.Dev/LuceneDev4xxx/LuceneDev4002_StackTraceHelperNoInliningAnalyzer.cs b/src/Lucene.Net.CodeAnalysis.Dev/LuceneDev4xxx/LuceneDev4002_StackTraceHelperNoInliningAnalyzer.cs new file mode 100644 index 0000000..243cb29 --- /dev/null +++ b/src/Lucene.Net.CodeAnalysis.Dev/LuceneDev4xxx/LuceneDev4002_StackTraceHelperNoInliningAnalyzer.cs @@ -0,0 +1,205 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System.Collections.Immutable; +using System.Linq; +using Lucene.Net.CodeAnalysis.Dev.Utility; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Diagnostics; + +namespace Lucene.Net.CodeAnalysis.Dev.LuceneDev4xxx +{ + /// + /// LuceneDev4002: Reports methods referenced by the 2-argument + /// StackTraceHelper.DoesStackTraceContainMethod(className, methodName) overload + /// that lack [MethodImpl(MethodImplOptions.NoInlining)]. Without it the JIT may + /// inline the method out of the stack trace, silently breaking the check. + /// The diagnostic is reported on the invocation so the IDE surfaces it as a + /// local diagnostic and a code fix can apply the attribute to the target method. + /// + [DiagnosticAnalyzer(LanguageNames.CSharp)] + public sealed class LuceneDev4002_StackTraceHelperNoInliningAnalyzer : DiagnosticAnalyzer + { + private const string StackTraceHelperFullName = "Lucene.Net.Support.ExceptionHandling.StackTraceHelper"; + private const string DoesStackTraceContainMethodName = "DoesStackTraceContainMethod"; + + public override ImmutableArray SupportedDiagnostics + => ImmutableArray.Create(Descriptors.LuceneDev4002_MissingNoInlining); + + public override void Initialize(AnalysisContext context) + { + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze); + context.EnableConcurrentExecution(); + + context.RegisterCompilationStartAction(compilationCtx => + { + var methodImplAttrSymbol = compilationCtx.Compilation.GetTypeByMetadataName( + "System.Runtime.CompilerServices.MethodImplAttribute"); + if (methodImplAttrSymbol is null) + return; + + compilationCtx.RegisterSyntaxNodeAction( + ctx => AnalyzeInvocation(ctx, methodImplAttrSymbol), + SyntaxKind.InvocationExpression); + }); + } + + private static void AnalyzeInvocation(SyntaxNodeAnalysisContext ctx, INamedTypeSymbol methodImplAttrSymbol) + { + var invocation = (InvocationExpressionSyntax)ctx.Node; + + if (invocation.Expression is not MemberAccessExpressionSyntax memberAccess) + return; + if (memberAccess.Name.Identifier.ValueText != DoesStackTraceContainMethodName) + return; + + // Only the 2-argument overload (className, methodName) is in scope. + if (invocation.ArgumentList.Arguments.Count != 2) + return; + + var symbol = ctx.SemanticModel.GetSymbolInfo(invocation).Symbol as IMethodSymbol; + if (symbol is null) + return; + if (symbol.ContainingType?.ToDisplayString() != StackTraceHelperFullName) + return; + + var classArg = invocation.ArgumentList.Arguments[0].Expression; + var methodArg = invocation.ArgumentList.Arguments[1].Expression; + + var (classNameValue, classTypeFromNameof) = ResolveClassReference(classArg, ctx.SemanticModel); + if (classNameValue is null) + return; + + var methodNameValue = ResolveMethodNameValue(methodArg, ctx.SemanticModel); + if (methodNameValue is null) + return; + + var targetType = classTypeFromNameof + ?? FindSourceTypeByName(ctx.SemanticModel.Compilation, classNameValue); + if (targetType is null) + return; + + // Report once per call site if any matching method declaration in source + // is missing NoInlining. Locating the diagnostic on the invocation makes + // it a "local" diagnostic relative to the syntax tree the analyzer is + // visiting, which means the IDE surfaces it (compilation-wide non-local + // diagnostics are filtered out of live IDE analysis) and a code fix can + // be wired up in the future. + foreach (var member in targetType.GetMembers(methodNameValue).OfType()) + { + if (member.MethodKind != MethodKind.Ordinary) + continue; + + if (NoInliningAttributeHelper.HasNoInliningAttribute(member, methodImplAttrSymbol)) + continue; + + foreach (var declRef in member.DeclaringSyntaxReferences) + { + if (declRef.GetSyntax(ctx.CancellationToken) is not MethodDeclarationSyntax methodDecl) + continue; + + if (NoInliningAttributeHelper.HasEmptyBody(methodDecl)) + continue; + + if (NoInliningAttributeHelper.IsInterfaceOrAbstractMethod(methodDecl)) + continue; + + ctx.ReportDiagnostic(Diagnostic.Create( + Descriptors.LuceneDev4002_MissingNoInlining, + invocation.GetLocation(), + $"{targetType.Name}.{methodDecl.Identifier.ValueText}")); + return; + } + } + } + + private static (string? Name, INamedTypeSymbol? TypeFromNameof) ResolveClassReference( + ExpressionSyntax expr, + SemanticModel semantic) + { + if (expr is InvocationExpressionSyntax inv + && inv.Expression is IdentifierNameSyntax id + && id.Identifier.ValueText == "nameof" + && inv.ArgumentList.Arguments.Count == 1) + { + var inner = inv.ArgumentList.Arguments[0].Expression; + var typeSymbol = semantic.GetTypeInfo(inner).Type as INamedTypeSymbol + ?? semantic.GetSymbolInfo(inner).Symbol as INamedTypeSymbol; + if (typeSymbol is not null) + return (typeSymbol.Name, typeSymbol); + } + + if (expr is LiteralExpressionSyntax literal && literal.IsKind(SyntaxKind.StringLiteralExpression)) + return (literal.Token.ValueText, null); + + var constant = semantic.GetConstantValue(expr); + if (constant.HasValue && constant.Value is string s) + return (s, null); + + return (null, null); + } + + private static string? ResolveMethodNameValue(ExpressionSyntax expr, SemanticModel semantic) + { + if (expr is InvocationExpressionSyntax inv + && inv.Expression is IdentifierNameSyntax id + && id.Identifier.ValueText == "nameof" + && inv.ArgumentList.Arguments.Count == 1) + { + var inner = inv.ArgumentList.Arguments[0].Expression; + return ExtractRightmostIdentifier(inner); + } + + if (expr is LiteralExpressionSyntax literal && literal.IsKind(SyntaxKind.StringLiteralExpression)) + return literal.Token.ValueText; + + var constant = semantic.GetConstantValue(expr); + if (constant.HasValue && constant.Value is string s) + return s; + + return null; + } + + private static string? ExtractRightmostIdentifier(ExpressionSyntax expr) + { + return expr switch + { + IdentifierNameSyntax id => id.Identifier.ValueText, + MemberAccessExpressionSyntax ma => ma.Name.Identifier.ValueText, + _ => null, + }; + } + + private static INamedTypeSymbol? FindSourceTypeByName(Compilation compilation, string typeName) + { + // Use Roslyn's symbol-name index instead of walking every namespace. + // Restrict to the source assembly so we don't match metadata types. + foreach (var symbol in compilation.GetSymbolsWithName(n => n == typeName, SymbolFilter.Type)) + { + if (symbol is INamedTypeSymbol type + && SymbolEqualityComparer.Default.Equals(type.ContainingAssembly, compilation.Assembly)) + { + return type; + } + } + return null; + } + } +} diff --git a/src/Lucene.Net.CodeAnalysis.Dev/LuceneDev4xxx/NoInliningAttributeHelper.cs b/src/Lucene.Net.CodeAnalysis.Dev/LuceneDev4xxx/NoInliningAttributeHelper.cs new file mode 100644 index 0000000..c0e0df5 --- /dev/null +++ b/src/Lucene.Net.CodeAnalysis.Dev/LuceneDev4xxx/NoInliningAttributeHelper.cs @@ -0,0 +1,111 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System.Linq; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; + +namespace Lucene.Net.CodeAnalysis.Dev.LuceneDev4xxx +{ + public static class NoInliningAttributeHelper + { + private const int NoInlining = 0x0008; + + public static AttributeSyntax? FindNoInliningAttribute( + MethodDeclarationSyntax methodDecl, + SemanticModel semantic, + INamedTypeSymbol methodImplAttrSymbol) + { + foreach (var attrList in methodDecl.AttributeLists) + { + foreach (var attr in attrList.Attributes) + { + var attrType = semantic.GetTypeInfo(attr).Type as INamedTypeSymbol + ?? semantic.GetSymbolInfo(attr).Symbol?.ContainingType; + if (!SymbolEqualityComparer.Default.Equals(attrType, methodImplAttrSymbol)) + continue; + + if (AttributeSpecifiesNoInlining(attr, semantic)) + return attr; + } + } + return null; + } + + // Symbol-based variant: works across syntax trees and avoids + // Compilation.GetSemanticModel (which would trip RS1030 in an analyzer). + public static bool HasNoInliningAttribute( + IMethodSymbol method, + INamedTypeSymbol methodImplAttrSymbol) + { + foreach (var attr in method.GetAttributes()) + { + if (!SymbolEqualityComparer.Default.Equals(attr.AttributeClass, methodImplAttrSymbol)) + continue; + + if (attr.ConstructorArguments.Length == 0) + continue; + + var first = attr.ConstructorArguments[0]; + if (first.Value is int intValue && (intValue & NoInlining) == NoInlining) + return true; + } + return false; + } + + private static bool AttributeSpecifiesNoInlining(AttributeSyntax attr, SemanticModel semantic) + { + if (attr.ArgumentList is null || attr.ArgumentList.Arguments.Count == 0) + return false; + + // Only the first positional argument controls MethodImplOptions; the second + // optional argument is MethodCodeType. Skip named arguments. + var firstPositional = attr.ArgumentList.Arguments + .FirstOrDefault(a => a.NameEquals is null && a.NameColon is null); + if (firstPositional is null) + return false; + + var constant = semantic.GetConstantValue(firstPositional.Expression); + if (constant.HasValue && constant.Value is int intValue) + { + return (intValue & NoInlining) == NoInlining; + } + + return false; + } + + public static bool IsInterfaceOrAbstractMethod(MethodDeclarationSyntax methodDecl) + { + if (methodDecl.Parent is InterfaceDeclarationSyntax) + return true; + if (methodDecl.Modifiers.Any(SyntaxKind.AbstractKeyword)) + return true; + return false; + } + + public static bool HasEmptyBody(MethodDeclarationSyntax methodDecl) + { + if (methodDecl.Body is null && methodDecl.ExpressionBody is null) + return false; + if (methodDecl.ExpressionBody is not null) + return false; + return methodDecl.Body!.Statements.Count == 0; + } + } +} diff --git a/src/Lucene.Net.CodeAnalysis.Dev/Resources.resx b/src/Lucene.Net.CodeAnalysis.Dev/Resources.resx index 90216e6..2bf6cb7 100644 --- a/src/Lucene.Net.CodeAnalysis.Dev/Resources.resx +++ b/src/Lucene.Net.CodeAnalysis.Dev/Resources.resx @@ -406,4 +406,37 @@ under the License. Call to '{0}' passes CultureInfo.InvariantCulture; verify this matches Lucene's behavior at this site. + + + [MethodImpl(MethodImplOptions.NoInlining)] has no effect on interface or abstract methods + + + The MethodImpl attribute is not inherited, so applying NoInlining to an interface or abstract method declaration has no effect on the eventual implementation. Apply the attribute to the implementing method instead. + + + [MethodImpl(MethodImplOptions.NoInlining)] on '{0}' has no effect because the method is abstract or an interface member; remove the attribute or apply it to the implementation. + + + + + [MethodImpl(MethodImplOptions.NoInlining)] should not be used on empty-bodied methods + + + An empty-bodied method calls nothing, so it cannot appear above any frame in a stack trace. Preventing inlining provides no benefit and only harms performance. + + + [MethodImpl(MethodImplOptions.NoInlining)] on '{0}' has no benefit because the method body is empty; remove the attribute. + + + + + Method referenced by StackTraceHelper.DoesStackTraceContainMethod should be marked NoInlining + + + If a method is referenced by StackTraceHelper.DoesStackTraceContainMethod and the JIT inlines it, the method will not appear in the stack trace and the check will silently fail. Apply [MethodImpl(MethodImplOptions.NoInlining)] to the method. + + + '{0}' is referenced by this StackTraceHelper.DoesStackTraceContainMethod call and should be marked with [MethodImpl(MethodImplOptions.NoInlining)] to prevent the JIT from inlining it out of the stack trace. + + diff --git a/src/Lucene.Net.CodeAnalysis.Dev/Utility/Descriptors.LuceneDev4xxx.cs b/src/Lucene.Net.CodeAnalysis.Dev/Utility/Descriptors.LuceneDev4xxx.cs new file mode 100644 index 0000000..e29a93d --- /dev/null +++ b/src/Lucene.Net.CodeAnalysis.Dev/Utility/Descriptors.LuceneDev4xxx.cs @@ -0,0 +1,57 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using Microsoft.CodeAnalysis; +using static Microsoft.CodeAnalysis.DiagnosticSeverity; +using static Lucene.Net.CodeAnalysis.Dev.Utility.Category; + +namespace Lucene.Net.CodeAnalysis.Dev.Utility +{ + public static partial class Descriptors + { + // IMPORTANT: Do not make these into properties! + // The AnalyzerReleases release management analyzers do not recognize them + // and will report RS2002 warnings if it cannot read the DiagnosticDescriptor + // instance through a field. + + // 4000: [MethodImpl(MethodImplOptions.NoInlining)] on an interface or abstract method + public static readonly DiagnosticDescriptor LuceneDev4000_NoInliningHasNoEffect = + Diagnostic( + "LuceneDev4000", + Performance, + Warning + ); + + // 4001: [MethodImpl(MethodImplOptions.NoInlining)] on an empty-bodied method + public static readonly DiagnosticDescriptor LuceneDev4001_NoInliningOnEmptyMethod = + Diagnostic( + "LuceneDev4001", + Performance, + Warning + ); + + // 4002: Method referenced by StackTraceHelper.DoesStackTraceContainMethod (2-arg) + // is missing [MethodImpl(MethodImplOptions.NoInlining)] + public static readonly DiagnosticDescriptor LuceneDev4002_MissingNoInlining = + Diagnostic( + "LuceneDev4002", + Performance, + Warning + ); + } +} diff --git a/tests/Lucene.Net.CodeAnalysis.Dev.CodeFixes.Tests/LuceneDev4xxx/TestLuceneDev4000_4001_NoInliningOnNoOpCodeFixProvider.cs b/tests/Lucene.Net.CodeAnalysis.Dev.CodeFixes.Tests/LuceneDev4xxx/TestLuceneDev4000_4001_NoInliningOnNoOpCodeFixProvider.cs new file mode 100644 index 0000000..a44196a --- /dev/null +++ b/tests/Lucene.Net.CodeAnalysis.Dev.CodeFixes.Tests/LuceneDev4xxx/TestLuceneDev4000_4001_NoInliningOnNoOpCodeFixProvider.cs @@ -0,0 +1,289 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using Lucene.Net.CodeAnalysis.Dev.CodeFixes.LuceneDev4xxx; +using Lucene.Net.CodeAnalysis.Dev.LuceneDev4xxx; +using Lucene.Net.CodeAnalysis.Dev.TestUtilities; +using Lucene.Net.CodeAnalysis.Dev.Utility; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Testing; +using NUnit.Framework; +using System.Threading.Tasks; + +namespace Lucene.Net.CodeAnalysis.Dev.CodeFixes.Tests.LuceneDev4xxx +{ + [TestFixture] + public class TestLuceneDev4000_4001_NoInliningOnNoOpCodeFixProvider + { + // ----------------------------------------------------------------- + // 4000: remove attribute on interface / abstract method + // ----------------------------------------------------------------- + + [Test] + public async Task Fix_LuceneDev4000_RemovesAttribute_From_InterfaceMethod() + { + var testCode = @" +using System.Runtime.CompilerServices; + +public interface ISample +{ + [MethodImpl(MethodImplOptions.NoInlining)] + void DoWork(); +}"; + + var fixedCode = @" +using System.Runtime.CompilerServices; + +public interface ISample +{ + void DoWork(); +}"; + + var expected = new DiagnosticResult(Descriptors.LuceneDev4000_NoInliningHasNoEffect) + .WithSeverity(DiagnosticSeverity.Warning) + .WithSpan(6, 6, 6, 46) + .WithArguments("DoWork"); + + var test = new InjectableCodeFixTest( + () => new LuceneDev4000_4001_NoInliningOnNoOpAnalyzer(), + () => new LuceneDev4000_4001_NoInliningOnNoOpCodeFixProvider()) + { + TestCode = testCode, + FixedCode = fixedCode, + ExpectedDiagnostics = { expected } + }; + + await test.RunAsync(); + } + + // ----------------------------------------------------------------- + // 4001: remove attribute on empty-bodied method + // ----------------------------------------------------------------- + + [Test] + public async Task Fix_LuceneDev4001_RemovesAttribute_From_EmptyBodiedMethod() + { + var testCode = @" +using System.Runtime.CompilerServices; + +public class Sample +{ + [MethodImpl(MethodImplOptions.NoInlining)] + public void DoWork() + { + } +}"; + + var fixedCode = @" +using System.Runtime.CompilerServices; + +public class Sample +{ + public void DoWork() + { + } +}"; + + var expected = new DiagnosticResult(Descriptors.LuceneDev4001_NoInliningOnEmptyMethod) + .WithSeverity(DiagnosticSeverity.Warning) + .WithSpan(6, 6, 6, 46) + .WithArguments("DoWork"); + + var test = new InjectableCodeFixTest( + () => new LuceneDev4000_4001_NoInliningOnNoOpAnalyzer(), + () => new LuceneDev4000_4001_NoInliningOnNoOpCodeFixProvider()) + { + TestCode = testCode, + FixedCode = fixedCode, + ExpectedDiagnostics = { expected } + }; + + await test.RunAsync(); + } + + // ----------------------------------------------------------------- + // Regression: removing the attribute must preserve comments and + // blank lines that precede it. + // ----------------------------------------------------------------- + + [Test] + public async Task Fix_PreservesLeadingCommentBeforeAttribute() + { + var testCode = @" +using System.Runtime.CompilerServices; + +public class Sample +{ + // Important comment about the method. + [MethodImpl(MethodImplOptions.NoInlining)] + public void DoWork() + { + } +}"; + + var fixedCode = @" +using System.Runtime.CompilerServices; + +public class Sample +{ + // Important comment about the method. + public void DoWork() + { + } +}"; + + var expected = new DiagnosticResult(Descriptors.LuceneDev4001_NoInliningOnEmptyMethod) + .WithSeverity(DiagnosticSeverity.Warning) + .WithSpan(7, 6, 7, 46) + .WithArguments("DoWork"); + + var test = new InjectableCodeFixTest( + () => new LuceneDev4000_4001_NoInliningOnNoOpAnalyzer(), + () => new LuceneDev4000_4001_NoInliningOnNoOpCodeFixProvider()) + { + TestCode = testCode, + FixedCode = fixedCode, + ExpectedDiagnostics = { expected } + }; + + await test.RunAsync(); + } + + // ----------------------------------------------------------------- + // Multiple attributes: only the [MethodImpl(NoInlining)] attribute + // should be removed; siblings must remain intact. + // ----------------------------------------------------------------- + + [Test] + public async Task Fix_RemovesOnlyTargetAttribute_WithinSingleAttributeList() + { + // [A, MethodImpl(NoInlining), B] → [A, B] + var testCode = @" +using System; +using System.Runtime.CompilerServices; + +[AttributeUsage(AttributeTargets.Method, AllowMultiple = true)] +public class FooAttribute : Attribute { } + +[AttributeUsage(AttributeTargets.Method, AllowMultiple = true)] +public class BarAttribute : Attribute { } + +public class Sample +{ + [Foo, MethodImpl(MethodImplOptions.NoInlining), Bar] + public void DoWork() + { + } +}"; + + var fixedCode = @" +using System; +using System.Runtime.CompilerServices; + +[AttributeUsage(AttributeTargets.Method, AllowMultiple = true)] +public class FooAttribute : Attribute { } + +[AttributeUsage(AttributeTargets.Method, AllowMultiple = true)] +public class BarAttribute : Attribute { } + +public class Sample +{ + [Foo, Bar] + public void DoWork() + { + } +}"; + + var expected = new DiagnosticResult(Descriptors.LuceneDev4001_NoInliningOnEmptyMethod) + .WithSeverity(DiagnosticSeverity.Warning) + .WithSpan(13, 11, 13, 51) + .WithArguments("DoWork"); + + var test = new InjectableCodeFixTest( + () => new LuceneDev4000_4001_NoInliningOnNoOpAnalyzer(), + () => new LuceneDev4000_4001_NoInliningOnNoOpCodeFixProvider()) + { + TestCode = testCode, + FixedCode = fixedCode, + ExpectedDiagnostics = { expected } + }; + + await test.RunAsync(); + } + + [Test] + public async Task Fix_RemovesOnlyTargetAttributeList_AmongMultipleLists() + { + // [A] [MethodImpl(NoInlining)] [B] → [A] [B] + var testCode = @" +using System; +using System.Runtime.CompilerServices; + +[AttributeUsage(AttributeTargets.Method, AllowMultiple = true)] +public class FooAttribute : Attribute { } + +[AttributeUsage(AttributeTargets.Method, AllowMultiple = true)] +public class BarAttribute : Attribute { } + +public class Sample +{ + [Foo] + [MethodImpl(MethodImplOptions.NoInlining)] + [Bar] + public void DoWork() + { + } +}"; + + var fixedCode = @" +using System; +using System.Runtime.CompilerServices; + +[AttributeUsage(AttributeTargets.Method, AllowMultiple = true)] +public class FooAttribute : Attribute { } + +[AttributeUsage(AttributeTargets.Method, AllowMultiple = true)] +public class BarAttribute : Attribute { } + +public class Sample +{ + [Foo] + [Bar] + public void DoWork() + { + } +}"; + + var expected = new DiagnosticResult(Descriptors.LuceneDev4001_NoInliningOnEmptyMethod) + .WithSeverity(DiagnosticSeverity.Warning) + .WithSpan(14, 6, 14, 46) + .WithArguments("DoWork"); + + var test = new InjectableCodeFixTest( + () => new LuceneDev4000_4001_NoInliningOnNoOpAnalyzer(), + () => new LuceneDev4000_4001_NoInliningOnNoOpCodeFixProvider()) + { + TestCode = testCode, + FixedCode = fixedCode, + ExpectedDiagnostics = { expected } + }; + + await test.RunAsync(); + } + } +} diff --git a/tests/Lucene.Net.CodeAnalysis.Dev.CodeFixes.Tests/LuceneDev4xxx/TestLuceneDev4002_StackTraceHelperNoInliningCodeFixProvider.cs b/tests/Lucene.Net.CodeAnalysis.Dev.CodeFixes.Tests/LuceneDev4xxx/TestLuceneDev4002_StackTraceHelperNoInliningCodeFixProvider.cs new file mode 100644 index 0000000..56b8d1a --- /dev/null +++ b/tests/Lucene.Net.CodeAnalysis.Dev.CodeFixes.Tests/LuceneDev4xxx/TestLuceneDev4002_StackTraceHelperNoInliningCodeFixProvider.cs @@ -0,0 +1,301 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using Lucene.Net.CodeAnalysis.Dev.CodeFixes.LuceneDev4xxx; +using Lucene.Net.CodeAnalysis.Dev.LuceneDev4xxx; +using Lucene.Net.CodeAnalysis.Dev.TestUtilities; +using Lucene.Net.CodeAnalysis.Dev.Utility; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Testing; +using NUnit.Framework; +using System.Threading.Tasks; + +namespace Lucene.Net.CodeAnalysis.Dev.CodeFixes.Tests.LuceneDev4xxx +{ + [TestFixture] + public class TestLuceneDev4002_StackTraceHelperNoInliningCodeFixProvider + { + private const string StackTraceHelperStub = @" +namespace Lucene.Net.Support.ExceptionHandling +{ + public static class StackTraceHelper + { + public static bool DoesStackTraceContainMethod(string methodName) => false; + public static bool DoesStackTraceContainMethod(string className, string methodName) => false; + } +} +"; + + [Test] + public async Task Fix_AddsAttribute_WhenUsingAlreadyPresent() + { + var testCode = @" +using System.Runtime.CompilerServices; + +public class Target +{ + public void Merge() + { + var x = 1; + } +} + +public class Caller +{ + public void Check() + { + if (Lucene.Net.Support.ExceptionHandling.StackTraceHelper.DoesStackTraceContainMethod(nameof(Target), nameof(Target.Merge))) + { + } + } +}" + StackTraceHelperStub; + + var fixedCode = @" +using System.Runtime.CompilerServices; + +public class Target +{ + [MethodImpl(MethodImplOptions.NoInlining)] + public void Merge() + { + var x = 1; + } +} + +public class Caller +{ + public void Check() + { + if (Lucene.Net.Support.ExceptionHandling.StackTraceHelper.DoesStackTraceContainMethod(nameof(Target), nameof(Target.Merge))) + { + } + } +}" + StackTraceHelperStub; + + var expected = new DiagnosticResult(Descriptors.LuceneDev4002_MissingNoInlining) + .WithSeverity(DiagnosticSeverity.Warning) + .WithSpan(16, 13, 16, 132) + .WithArguments("Target.Merge"); + + var test = new InjectableCodeFixTest( + () => new LuceneDev4002_StackTraceHelperNoInliningAnalyzer(), + () => new LuceneDev4002_StackTraceHelperNoInliningCodeFixProvider()) + { + TestCode = testCode, + FixedCode = fixedCode, + ExpectedDiagnostics = { expected } + }; + + await test.RunAsync(); + } + + [Test] + public async Task Fix_AddsAttributeAndUsing_WhenUsingMissing() + { + // No `using System.Runtime.CompilerServices;` initially — fix must add it. + var testCode = @" +public class Target +{ + public void Merge() + { + var x = 1; + } +} + +public class Caller +{ + public void Check() + { + if (Lucene.Net.Support.ExceptionHandling.StackTraceHelper.DoesStackTraceContainMethod(nameof(Target), nameof(Target.Merge))) + { + } + } +}" + StackTraceHelperStub; + + var fixedCode = @"using System.Runtime.CompilerServices; + +public class Target +{ + [MethodImpl(MethodImplOptions.NoInlining)] + public void Merge() + { + var x = 1; + } +} + +public class Caller +{ + public void Check() + { + if (Lucene.Net.Support.ExceptionHandling.StackTraceHelper.DoesStackTraceContainMethod(nameof(Target), nameof(Target.Merge))) + { + } + } +}" + StackTraceHelperStub; + + var expected = new DiagnosticResult(Descriptors.LuceneDev4002_MissingNoInlining) + .WithSeverity(DiagnosticSeverity.Warning) + .WithSpan(14, 13, 14, 132) + .WithArguments("Target.Merge"); + + var test = new InjectableCodeFixTest( + () => new LuceneDev4002_StackTraceHelperNoInliningAnalyzer(), + () => new LuceneDev4002_StackTraceHelperNoInliningCodeFixProvider()) + { + TestCode = testCode, + FixedCode = fixedCode, + ExpectedDiagnostics = { expected } + }; + + await test.RunAsync(); + } + + [Test] + public async Task Fix_AddsAttributeAndUsing_InTargetDocument_WhenTargetIsInDifferentFile() + { + // Target lives in /0/Test0.cs, Caller in /0/Test1.cs. The code fix must + // edit the target's document (adding the attribute and the using) rather + // than the caller's. + var targetSource = @" +public class Target +{ + public void Merge() + { + var x = 1; + } +} +"; + + var callerSource = @" +public class Caller +{ + public void Check() + { + if (Lucene.Net.Support.ExceptionHandling.StackTraceHelper.DoesStackTraceContainMethod(nameof(Target), nameof(Target.Merge))) + { + } + } +}" + StackTraceHelperStub; + + var fixedTargetSource = @"using System.Runtime.CompilerServices; + +public class Target +{ + [MethodImpl(MethodImplOptions.NoInlining)] + public void Merge() + { + var x = 1; + } +} +"; + + var expected = new DiagnosticResult(Descriptors.LuceneDev4002_MissingNoInlining) + .WithSeverity(DiagnosticSeverity.Warning) + .WithLocation("/0/Test1.cs", line: 6, column: 13) + .WithArguments("Target.Merge"); + + var test = new InjectableCodeFixTest( + () => new LuceneDev4002_StackTraceHelperNoInliningAnalyzer(), + () => new LuceneDev4002_StackTraceHelperNoInliningCodeFixProvider()) + { + ExpectedDiagnostics = { expected } + }; + test.TestState.Sources.Add(targetSource); + test.TestState.Sources.Add(callerSource); + test.FixedState.Sources.Add(fixedTargetSource); + test.FixedState.Sources.Add(callerSource); + + await test.RunAsync(); + } + + [Test] + public async Task Fix_PreservesExistingAttributeOnTarget() + { + // Target method has another attribute; the new MethodImpl list should + // be inserted ahead of it without disturbing it. + var testCode = @" +using System; +using System.Runtime.CompilerServices; + +[AttributeUsage(AttributeTargets.Method)] +public class FooAttribute : Attribute { } + +public class Target +{ + [Foo] + public void Merge() + { + var x = 1; + } +} + +public class Caller +{ + public void Check() + { + if (Lucene.Net.Support.ExceptionHandling.StackTraceHelper.DoesStackTraceContainMethod(nameof(Target), nameof(Target.Merge))) + { + } + } +}" + StackTraceHelperStub; + + var fixedCode = @" +using System; +using System.Runtime.CompilerServices; + +[AttributeUsage(AttributeTargets.Method)] +public class FooAttribute : Attribute { } + +public class Target +{ + [MethodImpl(MethodImplOptions.NoInlining)] + [Foo] + public void Merge() + { + var x = 1; + } +} + +public class Caller +{ + public void Check() + { + if (Lucene.Net.Support.ExceptionHandling.StackTraceHelper.DoesStackTraceContainMethod(nameof(Target), nameof(Target.Merge))) + { + } + } +}" + StackTraceHelperStub; + + var expected = new DiagnosticResult(Descriptors.LuceneDev4002_MissingNoInlining) + .WithSeverity(DiagnosticSeverity.Warning) + .WithSpan(21, 13, 21, 132) + .WithArguments("Target.Merge"); + + var test = new InjectableCodeFixTest( + () => new LuceneDev4002_StackTraceHelperNoInliningAnalyzer(), + () => new LuceneDev4002_StackTraceHelperNoInliningCodeFixProvider()) + { + TestCode = testCode, + FixedCode = fixedCode, + ExpectedDiagnostics = { expected } + }; + + await test.RunAsync(); + } + } +} diff --git a/tests/Lucene.Net.CodeAnalysis.Dev.Tests/LuceneDev4xxx/TestLuceneDev4000_4001_NoInliningOnNoOpAnalyzer.cs b/tests/Lucene.Net.CodeAnalysis.Dev.Tests/LuceneDev4xxx/TestLuceneDev4000_4001_NoInliningOnNoOpAnalyzer.cs new file mode 100644 index 0000000..ee08917 --- /dev/null +++ b/tests/Lucene.Net.CodeAnalysis.Dev.Tests/LuceneDev4xxx/TestLuceneDev4000_4001_NoInliningOnNoOpAnalyzer.cs @@ -0,0 +1,188 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using Lucene.Net.CodeAnalysis.Dev.LuceneDev4xxx; +using Lucene.Net.CodeAnalysis.Dev.TestUtilities; +using Lucene.Net.CodeAnalysis.Dev.Utility; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Testing; +using NUnit.Framework; +using System.Threading.Tasks; + +namespace Lucene.Net.CodeAnalysis.Dev.Tests.LuceneDev4xxx +{ + [TestFixture] + public class TestLuceneDev4000_4001_NoInliningOnNoOpAnalyzer + { + // --------------------------------------------------------------------- + // LuceneDev4000: NoInlining on interface / abstract methods + // --------------------------------------------------------------------- + + [Test] + public async Task LuceneDev4000_Reports_When_NoInlining_On_InterfaceMethod() + { + var testCode = @" +using System.Runtime.CompilerServices; + +public interface ISample +{ + [MethodImpl(MethodImplOptions.NoInlining)] + void DoWork(); +}"; + + var expected = new DiagnosticResult(Descriptors.LuceneDev4000_NoInliningHasNoEffect) + .WithSeverity(DiagnosticSeverity.Warning) + .WithSpan(6, 6, 6, 46) + .WithArguments("DoWork"); + + var test = new InjectableCSharpAnalyzerTest(() => new LuceneDev4000_4001_NoInliningOnNoOpAnalyzer()) + { + TestCode = testCode, + ExpectedDiagnostics = { expected } + }; + + await test.RunAsync(); + } + + [Test] + public async Task LuceneDev4000_Reports_When_NoInlining_On_AbstractMethod() + { + var testCode = @" +using System.Runtime.CompilerServices; + +public abstract class Sample +{ + [MethodImpl(MethodImplOptions.NoInlining)] + public abstract void DoWork(); +}"; + + var expected = new DiagnosticResult(Descriptors.LuceneDev4000_NoInliningHasNoEffect) + .WithSeverity(DiagnosticSeverity.Warning) + .WithSpan(6, 6, 6, 46) + .WithArguments("DoWork"); + + var test = new InjectableCSharpAnalyzerTest(() => new LuceneDev4000_4001_NoInliningOnNoOpAnalyzer()) + { + TestCode = testCode, + ExpectedDiagnostics = { expected } + }; + + await test.RunAsync(); + } + + [Test] + public async Task LuceneDev4000_NoDiagnostic_When_NoInlining_On_RegularMethod() + { + var testCode = @" +using System.Runtime.CompilerServices; + +public class Sample +{ + [MethodImpl(MethodImplOptions.NoInlining)] + public void DoWork() + { + var x = 1 + 2; + } +}"; + + var test = new InjectableCSharpAnalyzerTest(() => new LuceneDev4000_4001_NoInliningOnNoOpAnalyzer()) + { + TestCode = testCode + }; + + await test.RunAsync(); + } + + // --------------------------------------------------------------------- + // LuceneDev4001: NoInlining on empty-bodied methods + // --------------------------------------------------------------------- + + [Test] + public async Task LuceneDev4001_Reports_When_NoInlining_On_EmptyBodiedMethod() + { + var testCode = @" +using System.Runtime.CompilerServices; + +public class Sample +{ + [MethodImpl(MethodImplOptions.NoInlining)] + public void DoWork() + { + } +}"; + + var expected = new DiagnosticResult(Descriptors.LuceneDev4001_NoInliningOnEmptyMethod) + .WithSeverity(DiagnosticSeverity.Warning) + .WithSpan(6, 6, 6, 46) + .WithArguments("DoWork"); + + var test = new InjectableCSharpAnalyzerTest(() => new LuceneDev4000_4001_NoInliningOnNoOpAnalyzer()) + { + TestCode = testCode, + ExpectedDiagnostics = { expected } + }; + + await test.RunAsync(); + } + + [Test] + public async Task LuceneDev4001_NoDiagnostic_When_NoInlining_On_ExpressionBodiedThrow() + { + // An expression-bodied method that throws is not "empty" — has a real expression body. + var testCode = @" +using System; +using System.Runtime.CompilerServices; + +public class Sample +{ + [MethodImpl(MethodImplOptions.NoInlining)] + public void DoWork() => throw new InvalidOperationException(); +}"; + + var test = new InjectableCSharpAnalyzerTest(() => new LuceneDev4000_4001_NoInliningOnNoOpAnalyzer()) + { + TestCode = testCode + }; + + await test.RunAsync(); + } + + [Test] + public async Task LuceneDev4001_NoDiagnostic_When_NoInlining_On_NonEmptyMethod() + { + var testCode = @" +using System.Runtime.CompilerServices; + +public class Sample +{ + [MethodImpl(MethodImplOptions.NoInlining)] + public int DoWork() + { + return 42; + } +}"; + + var test = new InjectableCSharpAnalyzerTest(() => new LuceneDev4000_4001_NoInliningOnNoOpAnalyzer()) + { + TestCode = testCode + }; + + await test.RunAsync(); + } + } +} diff --git a/tests/Lucene.Net.CodeAnalysis.Dev.Tests/LuceneDev4xxx/TestLuceneDev4002_StackTraceHelperNoInliningAnalyzer.cs b/tests/Lucene.Net.CodeAnalysis.Dev.Tests/LuceneDev4xxx/TestLuceneDev4002_StackTraceHelperNoInliningAnalyzer.cs new file mode 100644 index 0000000..2ae3414 --- /dev/null +++ b/tests/Lucene.Net.CodeAnalysis.Dev.Tests/LuceneDev4xxx/TestLuceneDev4002_StackTraceHelperNoInliningAnalyzer.cs @@ -0,0 +1,231 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using Lucene.Net.CodeAnalysis.Dev.LuceneDev4xxx; +using Lucene.Net.CodeAnalysis.Dev.TestUtilities; +using Lucene.Net.CodeAnalysis.Dev.Utility; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Testing; +using NUnit.Framework; +using System.Threading.Tasks; + +namespace Lucene.Net.CodeAnalysis.Dev.Tests.LuceneDev4xxx +{ + [TestFixture] + public class TestLuceneDev4002_StackTraceHelperNoInliningAnalyzer + { + // Stub of StackTraceHelper appended to test sources, matching the + // Lucene.Net.Support.ExceptionHandling.StackTraceHelper API surface. + private const string StackTraceHelperStub = @" +namespace Lucene.Net.Support.ExceptionHandling +{ + public static class StackTraceHelper + { + public static bool DoesStackTraceContainMethod(string methodName) => false; + public static bool DoesStackTraceContainMethod(string className, string methodName) => false; + } +} +"; + + [Test] + public async Task LuceneDev4002_Reports_When_TargetMethod_Missing_NoInlining() + { + var testCode = @" +public class Target +{ + public void Merge() + { + var x = 1; + } +} + +public class Caller +{ + public void Check() + { + if (Lucene.Net.Support.ExceptionHandling.StackTraceHelper.DoesStackTraceContainMethod(nameof(Target), nameof(Target.Merge))) + { + } + } +}" + StackTraceHelperStub; + + // Diagnostic is now reported on the DoesStackTraceContainMethod invocation + // (call site) so the IDE can surface it and a future code fix can be hooked + // up. Argument is the qualified target method name. + var expected = new DiagnosticResult(Descriptors.LuceneDev4002_MissingNoInlining) + .WithSeverity(DiagnosticSeverity.Warning) + .WithSpan(14, 13, 14, 132) + .WithArguments("Target.Merge"); + + var test = new InjectableCSharpAnalyzerTest(() => new LuceneDev4002_StackTraceHelperNoInliningAnalyzer()) + { + TestCode = testCode, + ExpectedDiagnostics = { expected } + }; + + await test.RunAsync(); + } + + [Test] + public async Task LuceneDev4002_NoDiagnostic_When_TargetMethod_Already_Has_NoInlining() + { + var testCode = @" +using System.Runtime.CompilerServices; + +public class Target +{ + [MethodImpl(MethodImplOptions.NoInlining)] + public void Merge() + { + var x = 1; + } +} + +public class Caller +{ + public void Check() + { + if (Lucene.Net.Support.ExceptionHandling.StackTraceHelper.DoesStackTraceContainMethod(nameof(Target), nameof(Target.Merge))) + { + } + } +}" + StackTraceHelperStub; + + var test = new InjectableCSharpAnalyzerTest(() => new LuceneDev4002_StackTraceHelperNoInliningAnalyzer()) + { + TestCode = testCode + }; + + await test.RunAsync(); + } + + [Test] + public async Task LuceneDev4002_NoDiagnostic_When_TargetMethod_Has_EmptyBody() + { + // Empty-bodied methods can never appear in a stack trace under the relevant + // call (they call nothing); NoInlining gives no benefit. Don't flag. + var testCode = @" +public class Target +{ + public void Merge() + { + } +} + +public class Caller +{ + public void Check() + { + if (Lucene.Net.Support.ExceptionHandling.StackTraceHelper.DoesStackTraceContainMethod(nameof(Target), nameof(Target.Merge))) + { + } + } +}" + StackTraceHelperStub; + + var test = new InjectableCSharpAnalyzerTest(() => new LuceneDev4002_StackTraceHelperNoInliningAnalyzer()) + { + TestCode = testCode + }; + + await test.RunAsync(); + } + + [Test] + public async Task LuceneDev4002_Reports_When_TargetMethod_Is_In_Different_Document() + { + // The Target type and the Caller live in separate source files. The analyzer + // must build a SemanticModel against the Target's syntax tree before + // inspecting attributes — using the caller's SemanticModel against a node + // from a different tree throws ArgumentException. Target carries an + // unrelated attribute so the attribute-walk path actually executes. + var targetSource = @" +using System; + +[AttributeUsage(AttributeTargets.Method)] +public class FooAttribute : Attribute { } + +public class Target +{ + [Foo] + public void Merge() + { + var x = 1; + } +} +"; + + var callerSource = @" +public class Caller +{ + public void Check() + { + if (Lucene.Net.Support.ExceptionHandling.StackTraceHelper.DoesStackTraceContainMethod(nameof(Target), nameof(Target.Merge))) + { + } + } +}" + StackTraceHelperStub; + + var expected = new DiagnosticResult(Descriptors.LuceneDev4002_MissingNoInlining) + .WithSeverity(DiagnosticSeverity.Warning) + .WithLocation("/0/Test1.cs", line: 6, column: 13) + .WithArguments("Target.Merge"); + + var test = new InjectableCSharpAnalyzerTest(() => new LuceneDev4002_StackTraceHelperNoInliningAnalyzer()) + { + ExpectedDiagnostics = { expected } + }; + test.TestState.Sources.Add(targetSource); + test.TestState.Sources.Add(callerSource); + + await test.RunAsync(); + } + + [Test] + public async Task LuceneDev4002_NoDiagnostic_For_SingleArgOverload() + { + // The single-arg overload doesn't validate the owning class, so we don't + // require methods referenced through it to have NoInlining. Per the issue, + // only the 2-arg form is in scope. + var testCode = @" +public class Target +{ + public void Merge() + { + var x = 1; + } +} + +public class Caller +{ + public void Check() + { + if (Lucene.Net.Support.ExceptionHandling.StackTraceHelper.DoesStackTraceContainMethod(nameof(Target.Merge))) + { + } + } +}" + StackTraceHelperStub; + + var test = new InjectableCSharpAnalyzerTest(() => new LuceneDev4002_StackTraceHelperNoInliningAnalyzer()) + { + TestCode = testCode + }; + + await test.RunAsync(); + } + } +}