diff --git a/README.md b/README.md index 75a8470df..74e813b82 100755 --- a/README.md +++ b/README.md @@ -194,6 +194,7 @@ If you are already using other analyzers, you can check [which rules are duplica |[MA0176](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0176.md)|Performance|Optimize guid creation|ℹ️|✔️|✔️| |[MA0177](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0177.md)|Style|Use single-line XML comment syntax when possible|ℹ️|❌|✔️| |[MA0178](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0178.md)|Design|Use TimeSpan.Zero instead of TimeSpan.FromXXX(0)|ℹ️|✔️|✔️| +|[MA0179](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0179.md)|Performance|Use Attribute.IsDefined instead of GetCustomAttribute(s)|ℹ️|✔️|✔️| diff --git a/docs/README.md b/docs/README.md index ea087d645..2128a625f 100755 --- a/docs/README.md +++ b/docs/README.md @@ -178,6 +178,7 @@ |[MA0176](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0176.md)|Performance|Optimize guid creation|ℹ️|✔️|✔️| |[MA0177](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0177.md)|Style|Use single-line XML comment syntax when possible|ℹ️|❌|✔️| |[MA0178](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0178.md)|Design|Use TimeSpan.Zero instead of TimeSpan.FromXXX(0)|ℹ️|✔️|✔️| +|[MA0179](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0179.md)|Performance|Use Attribute.IsDefined instead of GetCustomAttribute(s)|ℹ️|✔️|✔️| |Id|Suppressed rule|Justification| |--|---------------|-------------| @@ -720,6 +721,9 @@ dotnet_diagnostic.MA0177.severity = none # MA0178: Use TimeSpan.Zero instead of TimeSpan.FromXXX(0) dotnet_diagnostic.MA0178.severity = suggestion + +# MA0179: Use Attribute.IsDefined instead of GetCustomAttribute(s) +dotnet_diagnostic.MA0179.severity = suggestion ``` # .editorconfig - all rules disabled @@ -1255,4 +1259,7 @@ dotnet_diagnostic.MA0177.severity = none # MA0178: Use TimeSpan.Zero instead of TimeSpan.FromXXX(0) dotnet_diagnostic.MA0178.severity = none + +# MA0179: Use Attribute.IsDefined instead of GetCustomAttribute(s) +dotnet_diagnostic.MA0179.severity = none ``` diff --git a/docs/Rules/MA0179.md b/docs/Rules/MA0179.md new file mode 100644 index 000000000..3f66faf49 --- /dev/null +++ b/docs/Rules/MA0179.md @@ -0,0 +1,42 @@ +# MA0179 - Use Attribute.IsDefined instead of GetCustomAttribute(s) + +Sources: [UseAttributeIsDefinedAnalyzer.cs](https://github.com/meziantou/Meziantou.Analyzer/blob/main/src/Meziantou.Analyzer/Rules/UseAttributeIsDefinedAnalyzer.cs), [UseAttributeIsDefinedFixer.cs](https://github.com/meziantou/Meziantou.Analyzer/blob/main/src/Meziantou.Analyzer.CodeFixers/Rules/UseAttributeIsDefinedFixer.cs) + + +`Attribute.IsDefined` is more efficient than `GetCustomAttribute()` when you only need to check if an attribute exists on a member, type, assembly, or module. The method avoids allocating the attribute instance and is optimized for simple existence checks. + +````csharp +using System; +using System.Reflection; + +// non-compliant +if (type.GetCustomAttribute() != null) { } +if (type.GetCustomAttribute() == null) { } +if (type.GetCustomAttribute() is null) { } +if (type.GetCustomAttribute() is not null) { } +if (member.GetCustomAttributes().Any()) { } +if (member.GetCustomAttributes().Count() > 0) { } +if (member.GetCustomAttributes().Length > 0) { } +if (member.GetCustomAttributes().Length == 0) { } + +// compliant +if (Attribute.IsDefined(type, typeof(ObsoleteAttribute))) { } +if (!Attribute.IsDefined(type, typeof(ObsoleteAttribute))) { } +if (!Attribute.IsDefined(type, typeof(ObsoleteAttribute))) { } +if (Attribute.IsDefined(type, typeof(ObsoleteAttribute))) { } +if (Attribute.IsDefined(member, typeof(ObsoleteAttribute))) { } +if (Attribute.IsDefined(member, typeof(ObsoleteAttribute))) { } +if (Attribute.IsDefined(member, typeof(ObsoleteAttribute))) { } +if (!Attribute.IsDefined(member, typeof(ObsoleteAttribute))) { } + +// compliant - with predicate (not detected) +if (member.GetCustomAttributes().Any(a => a.Message != null)) { } +if (member.GetCustomAttributes().Count(a => a.Message != null) > 0) { } + +// compliant - accessing attribute properties +var attr = type.GetCustomAttribute(); +if (attr != null) +{ + _ = attr.Message; +} +```` diff --git a/src/Meziantou.Analyzer.CodeFixers/Rules/UseAttributeIsDefinedFixer.cs b/src/Meziantou.Analyzer.CodeFixers/Rules/UseAttributeIsDefinedFixer.cs new file mode 100644 index 000000000..b2b71fb09 --- /dev/null +++ b/src/Meziantou.Analyzer.CodeFixers/Rules/UseAttributeIsDefinedFixer.cs @@ -0,0 +1,319 @@ +using System.Collections.Immutable; +using System.Composition; +using Meziantou.Analyzer.Internals; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CodeActions; +using Microsoft.CodeAnalysis.CodeFixes; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Editing; +using Microsoft.CodeAnalysis.Operations; + +namespace Meziantou.Analyzer.Rules; + +[ExportCodeFixProvider(LanguageNames.CSharp), Shared] +public sealed class UseAttributeIsDefinedFixer : CodeFixProvider +{ + private const string EnumerableAnyMethodDocId = "M:System.Linq.Enumerable.Any``1(System.Collections.Generic.IEnumerable{``0})"; + private const string EnumerableCountMethodDocId = "M:System.Linq.Enumerable.Count``1(System.Collections.Generic.IEnumerable{``0})"; + + public override ImmutableArray FixableDiagnosticIds => ImmutableArray.Create(RuleIdentifiers.UseAttributeIsDefined); + + public override FixAllProvider GetFixAllProvider() + { + return WellKnownFixAllProviders.BatchFixer; + } + + public override async Task RegisterCodeFixesAsync(CodeFixContext context) + { + var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false); + var nodeToFix = root?.FindNode(context.Span, getInnermostNodeForTie: true); + if (nodeToFix is null) + return; + + var title = "Use Attribute.IsDefined"; + var codeAction = CodeAction.Create( + title, + ct => ReplaceWithAttributeIsDefined(context.Document, nodeToFix, ct), + equivalenceKey: title); + + context.RegisterCodeFix(codeAction, context.Diagnostics); + } + + private static async Task ReplaceWithAttributeIsDefined(Document document, SyntaxNode nodeToFix, CancellationToken cancellationToken) + { + var editor = await DocumentEditor.CreateAsync(document, cancellationToken).ConfigureAwait(false); + var semanticModel = editor.SemanticModel; + var generator = editor.Generator; + + var operation = semanticModel.GetOperation(nodeToFix, cancellationToken); + if (operation is null) + return document; + + SyntaxNode? replacement = null; + + if (operation is IBinaryOperation binaryOperation) + { + var negate = binaryOperation.OperatorKind == BinaryOperatorKind.Equals; + var invocation = GetGetCustomAttributeInvocation(binaryOperation.LeftOperand) ?? GetGetCustomAttributeInvocation(binaryOperation.RightOperand); + if (invocation is not null) + { + replacement = CreateAttributeIsDefinedInvocation(generator, semanticModel, invocation, negate); + } + else + { + // Check for GetCustomAttributes().Length comparisons + var lengthInvocation = GetGetCustomAttributesLengthInvocation(binaryOperation.LeftOperand, out var isLeftSide); + if (lengthInvocation is null) + { + lengthInvocation = GetGetCustomAttributesLengthInvocation(binaryOperation.RightOperand, out isLeftSide); + isLeftSide = !isLeftSide; // If found on right, flip the comparison perspective + } + + if (lengthInvocation is not null) + { + var negateLength = ShouldNegateLengthComparison(binaryOperation, isLeftSide); + replacement = CreateAttributeIsDefinedInvocation(generator, semanticModel, lengthInvocation, negateLength); + } + else + { + // Check for GetCustomAttributes().Count() comparisons + var countInvocation = GetGetCustomAttributesCountInvocation(semanticModel, binaryOperation.LeftOperand, out var foundOnLeft); + var countIsOnLeft = foundOnLeft; + if (countInvocation is null) + { + countInvocation = GetGetCustomAttributesCountInvocation(semanticModel, binaryOperation.RightOperand, out var foundOnRight); + if (foundOnRight) + { + countIsOnLeft = false; // Count is on right side + } + } + + if (countInvocation is not null) + { + var negateCount = ShouldNegateLengthComparison(binaryOperation, countIsOnLeft); + replacement = CreateAttributeIsDefinedInvocation(generator, semanticModel, countInvocation, negateCount); + } + } + } + } + else if (operation is IIsPatternOperation isPatternOperation) + { + var negate = isPatternOperation.Pattern is IConstantPatternOperation; + var invocation = GetGetCustomAttributeInvocation(isPatternOperation.Value); + if (invocation is not null) + { + replacement = CreateAttributeIsDefinedInvocation(generator, semanticModel, invocation, negate); + } + } + else if (operation is IInvocationOperation invocationOperation) + { + // Check if this is the Any(IEnumerable) method + var enumerableAnyMethod = DocumentationCommentId.GetFirstSymbolForDeclarationId(EnumerableAnyMethodDocId, semanticModel.Compilation) as IMethodSymbol; + if (enumerableAnyMethod is not null && + SymbolEqualityComparer.Default.Equals(invocationOperation.TargetMethod.OriginalDefinition, enumerableAnyMethod) && + invocationOperation.Arguments.Length == 1 && + invocationOperation.Arguments[0].Value is IInvocationOperation getCustomAttributesInvocation) + { + replacement = CreateAttributeIsDefinedInvocation(generator, semanticModel, getCustomAttributesInvocation, negate: false); + } + } + + if (replacement is not null) + { + editor.ReplaceNode(nodeToFix, replacement.WithTriviaFrom(nodeToFix)); + return editor.GetChangedDocument(); + } + + return document; + } + + private static IInvocationOperation? GetGetCustomAttributeInvocation(IOperation operation) + { + if (operation.UnwrapConversionOperations() is IInvocationOperation invocation && + (invocation.TargetMethod.Name == "GetCustomAttribute" || invocation.TargetMethod.Name == "GetCustomAttributes")) + { + return invocation; + } + + return null; + } + + private static IInvocationOperation? GetGetCustomAttributesLengthInvocation(IOperation operation, out bool isFound) + { + isFound = false; + if (operation is IPropertyReferenceOperation propertyReference && + propertyReference.Property.Name == "Length" && + propertyReference.Instance is IInvocationOperation invocation && + invocation.TargetMethod.Name == "GetCustomAttributes") + { + isFound = true; + return invocation; + } + + return null; + } + + private static IInvocationOperation? GetGetCustomAttributesCountInvocation(SemanticModel semanticModel, IOperation operation, out bool isFound) + { + isFound = false; + if (operation is not IInvocationOperation countInvocation) + return null; + + // Check if this is the specific Count(IEnumerable) method + var enumerableCountMethod = DocumentationCommentId.GetFirstSymbolForDeclarationId(EnumerableCountMethodDocId, semanticModel.Compilation) as IMethodSymbol; + if (enumerableCountMethod is null || + !SymbolEqualityComparer.Default.Equals(countInvocation.TargetMethod.OriginalDefinition, enumerableCountMethod)) + return null; + + if (countInvocation.Arguments.Length != 1) + return null; + + if (countInvocation.Arguments[0].Value is not IInvocationOperation invocation) + return null; + + if (invocation.TargetMethod.Name != "GetCustomAttributes") + return null; + + isFound = true; + return invocation; + } + + private static bool ShouldNegateLengthComparison(IBinaryOperation operation, bool lengthIsOnLeft) + { + var otherOperand = lengthIsOnLeft ? operation.RightOperand : operation.LeftOperand; + if (otherOperand.ConstantValue is not { HasValue: true, Value: int value }) + return false; + + // Determine if we should negate based on operator and compared value + // Patterns that mean "has attributes" -> IsDefined (no negation): + // length > 0, length >= 1, length != 0, 0 < length, 1 <= length, 0 != length + // Patterns that mean "no attributes" -> !IsDefined (negate): + // length == 0, length <= 0, length < 1, 0 == length, 0 >= length, 1 > length + + if (lengthIsOnLeft) + { + return operation.OperatorKind switch + { + BinaryOperatorKind.Equals when value == 0 => true, // length == 0 -> !IsDefined + BinaryOperatorKind.NotEquals when value == 0 => false, // length != 0 -> IsDefined + BinaryOperatorKind.GreaterThan when value == 0 => false, // length > 0 -> IsDefined + BinaryOperatorKind.GreaterThanOrEqual when value == 1 => false, // length >= 1 -> IsDefined + BinaryOperatorKind.LessThan when value == 1 => true, // length < 1 -> !IsDefined + BinaryOperatorKind.LessThanOrEqual when value == 0 => true, // length <= 0 -> !IsDefined + _ => false, + }; + } + else + { + // When length is on the right: reverse the operator logic + return operation.OperatorKind switch + { + BinaryOperatorKind.Equals when value == 0 => true, // 0 == length -> !IsDefined + BinaryOperatorKind.NotEquals when value == 0 => false, // 0 != length -> IsDefined + BinaryOperatorKind.LessThan when value == 0 => false, // 0 < length (length > 0) -> IsDefined + BinaryOperatorKind.LessThanOrEqual when value == 1 => false, // 1 <= length (length >= 1) -> IsDefined + BinaryOperatorKind.GreaterThan when value == 1 => true, // 1 > length (length < 1) -> !IsDefined + BinaryOperatorKind.GreaterThanOrEqual when value == 0 => true, // 0 >= length (length <= 0) -> !IsDefined + _ => false, + }; + } + } + + private static SyntaxNode CreateAttributeIsDefinedInvocation(SyntaxGenerator generator, SemanticModel semanticModel, IInvocationOperation invocation, bool negate) + { + var attributeTypeSymbol = semanticModel.Compilation.GetBestTypeByMetadataName("System.Attribute"); + var systemTypeSymbol = semanticModel.Compilation.GetBestTypeByMetadataName("System.Type"); + var attributeTypeSyntax = generator.TypeExpression(attributeTypeSymbol!); + + var instance = invocation.Instance; + var instanceSyntax = instance?.Syntax; + + // For extension methods, the instance is in the first argument + if (instanceSyntax is null && invocation.TargetMethod.IsExtensionMethod && invocation.Arguments.Length > 0) + { + instanceSyntax = invocation.Arguments[0].Syntax; + } + else if (instanceSyntax is null && invocation.TargetMethod.IsStatic && SymbolEqualityComparer.Default.Equals(invocation.TargetMethod.ContainingType, attributeTypeSymbol)) + { + if (invocation.Arguments.Length > 0) + { + instanceSyntax = invocation.Arguments[0].Syntax; + } + } + + var arguments = new List(); + if (instanceSyntax is not null) + { + arguments.Add(instanceSyntax); + } + + // Find Type argument + SyntaxNode? typeSyntax = null; + if (invocation.TargetMethod.IsGenericMethod) + { + var typeArg = invocation.TargetMethod.TypeArguments[0]; + typeSyntax = generator.TypeOfExpression(generator.TypeExpression(typeArg)); + } + else + { + foreach (var arg in invocation.Arguments) + { + // Skip instance argument for extension methods + if (invocation.TargetMethod.IsExtensionMethod && arg == invocation.Arguments[0]) + continue; + + // Skip instance argument for static Attribute methods + if (invocation.TargetMethod.IsStatic && SymbolEqualityComparer.Default.Equals(invocation.TargetMethod.ContainingType, attributeTypeSymbol) && arg == invocation.Arguments[0]) + continue; + + if (SymbolEqualityComparer.Default.Equals(arg.Parameter?.Type, systemTypeSymbol)) + { + typeSyntax = arg.Syntax; + break; + } + } + } + + if (typeSyntax is null) + { + typeSyntax = generator.TypeOfExpression(attributeTypeSyntax); + } + arguments.Add(typeSyntax); + + // Find inherit argument + SyntaxNode? inheritSyntax = null; + foreach (var arg in invocation.Arguments) + { + // Skip instance argument for extension methods + if (invocation.TargetMethod.IsExtensionMethod && arg == invocation.Arguments[0]) + continue; + + // Skip instance argument for static Attribute methods + if (invocation.TargetMethod.IsStatic && SymbolEqualityComparer.Default.Equals(invocation.TargetMethod.ContainingType, attributeTypeSymbol) && arg == invocation.Arguments[0]) + continue; + + if (arg.Parameter?.Type.SpecialType == SpecialType.System_Boolean && arg.Parameter.Name == "inherit") + { + inheritSyntax = arg.Syntax; + break; + } + } + + if (inheritSyntax is not null) + { + arguments.Add(inheritSyntax); + } + + var isDefinedInvocation = generator.InvocationExpression( + generator.MemberAccessExpression(attributeTypeSyntax, "IsDefined"), + arguments); + + if (negate) + { + return generator.LogicalNotExpression(isDefinedInvocation); + } + + return isDefinedInvocation; + } +} diff --git a/src/Meziantou.Analyzer.Pack/configuration/default.editorconfig b/src/Meziantou.Analyzer.Pack/configuration/default.editorconfig index a4247fabe..18284e80c 100644 --- a/src/Meziantou.Analyzer.Pack/configuration/default.editorconfig +++ b/src/Meziantou.Analyzer.Pack/configuration/default.editorconfig @@ -532,3 +532,6 @@ dotnet_diagnostic.MA0177.severity = none # MA0178: Use TimeSpan.Zero instead of TimeSpan.FromXXX(0) dotnet_diagnostic.MA0178.severity = suggestion + +# MA0179: Use Attribute.IsDefined instead of GetCustomAttribute(s) +dotnet_diagnostic.MA0179.severity = suggestion diff --git a/src/Meziantou.Analyzer.Pack/configuration/none.editorconfig b/src/Meziantou.Analyzer.Pack/configuration/none.editorconfig index fd989423f..179fd62c2 100644 --- a/src/Meziantou.Analyzer.Pack/configuration/none.editorconfig +++ b/src/Meziantou.Analyzer.Pack/configuration/none.editorconfig @@ -532,3 +532,6 @@ dotnet_diagnostic.MA0177.severity = none # MA0178: Use TimeSpan.Zero instead of TimeSpan.FromXXX(0) dotnet_diagnostic.MA0178.severity = none + +# MA0179: Use Attribute.IsDefined instead of GetCustomAttribute(s) +dotnet_diagnostic.MA0179.severity = none diff --git a/src/Meziantou.Analyzer/RuleIdentifiers.cs b/src/Meziantou.Analyzer/RuleIdentifiers.cs index 3bf49361a..3ce1f4f8c 100755 --- a/src/Meziantou.Analyzer/RuleIdentifiers.cs +++ b/src/Meziantou.Analyzer/RuleIdentifiers.cs @@ -179,6 +179,7 @@ internal static class RuleIdentifiers public const string OptimizeGuidCreation = "MA0176"; public const string UseSingleLineXmlCommentSyntaxWhenPossible = "MA0177"; public const string UseTimeSpanZero = "MA0178"; + public const string UseAttributeIsDefined = "MA0179"; public static string GetHelpUri(string identifier) { diff --git a/src/Meziantou.Analyzer/Rules/UseAttributeIsDefinedAnalyzer.cs b/src/Meziantou.Analyzer/Rules/UseAttributeIsDefinedAnalyzer.cs new file mode 100644 index 000000000..6ab0edbbd --- /dev/null +++ b/src/Meziantou.Analyzer/Rules/UseAttributeIsDefinedAnalyzer.cs @@ -0,0 +1,302 @@ +using System.Collections.Immutable; +using Meziantou.Analyzer.Internals; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis.Operations; + +namespace Meziantou.Analyzer.Rules; + +[DiagnosticAnalyzer(LanguageNames.CSharp)] +public sealed class UseAttributeIsDefinedAnalyzer : DiagnosticAnalyzer +{ + private const string EnumerableAnyMethodDocId = "M:System.Linq.Enumerable.Any``1(System.Collections.Generic.IEnumerable{``0})"; + private const string EnumerableCountMethodDocId = "M:System.Linq.Enumerable.Count``1(System.Collections.Generic.IEnumerable{``0})"; + + private static readonly DiagnosticDescriptor Rule = new( + RuleIdentifiers.UseAttributeIsDefined, + title: "Use Attribute.IsDefined instead of GetCustomAttribute(s)", + messageFormat: "Use 'Attribute.IsDefined' instead of '{0}'", + RuleCategories.Performance, + DiagnosticSeverity.Info, + isEnabledByDefault: true, + description: "Detects inefficient attribute existence checks that can be replaced with Attribute.IsDefined for better performance.", + helpLinkUri: RuleIdentifiers.GetHelpUri(RuleIdentifiers.UseAttributeIsDefined)); + + public override ImmutableArray SupportedDiagnostics => ImmutableArray.Create(Rule); + + public override void Initialize(AnalysisContext context) + { + context.EnableConcurrentExecution(); + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); + + context.RegisterCompilationStartAction(compilationContext => + { + var analyzerContext = new AnalyzerContext(compilationContext.Compilation); + if (analyzerContext.IsValid) + { + compilationContext.RegisterOperationAction(analyzerContext.AnalyzeBinary, OperationKind.Binary); + compilationContext.RegisterOperationAction(analyzerContext.AnalyzeIsPattern, OperationKind.IsPattern); + compilationContext.RegisterOperationAction(analyzerContext.AnalyzeInvocation, OperationKind.Invocation); + } + }); + } + + private sealed class AnalyzerContext(Compilation compilation) + { + private readonly INamedTypeSymbol? _attributeSymbol = compilation.GetBestTypeByMetadataName("System.Attribute"); + private readonly INamedTypeSymbol? _assemblySymbol = compilation.GetBestTypeByMetadataName("System.Reflection.Assembly"); + private readonly INamedTypeSymbol? _moduleSymbol = compilation.GetBestTypeByMetadataName("System.Reflection.Module"); + private readonly INamedTypeSymbol? _memberInfoSymbol = compilation.GetBestTypeByMetadataName("System.Reflection.MemberInfo"); + private readonly INamedTypeSymbol? _parameterInfoSymbol = compilation.GetBestTypeByMetadataName("System.Reflection.ParameterInfo"); + private readonly INamedTypeSymbol? _typeSymbol = compilation.GetBestTypeByMetadataName("System.Type"); + private readonly INamedTypeSymbol? _customAttributeExtensionsSymbol = compilation.GetBestTypeByMetadataName("System.Reflection.CustomAttributeExtensions"); + private readonly IMethodSymbol? _enumerableAnyMethod = DocumentationCommentId.GetFirstSymbolForDeclarationId(EnumerableAnyMethodDocId, compilation) as IMethodSymbol; + private readonly IMethodSymbol? _enumerableCountMethod = DocumentationCommentId.GetFirstSymbolForDeclarationId(EnumerableCountMethodDocId, compilation) as IMethodSymbol; + + public bool IsValid => _attributeSymbol is not null; + + public void AnalyzeBinary(OperationAnalysisContext context) + { + var operation = (IBinaryOperation)context.Operation; + if (operation.OperatorKind is not (BinaryOperatorKind.Equals or BinaryOperatorKind.NotEquals or BinaryOperatorKind.GreaterThan or BinaryOperatorKind.LessThan or BinaryOperatorKind.GreaterThanOrEqual or BinaryOperatorKind.LessThanOrEqual)) + return; + + if (IsGetCustomAttributeComparison(operation.LeftOperand, operation.RightOperand, out var invocation)) + { + context.ReportDiagnostic(Rule, operation, invocation!.TargetMethod.Name); + return; + } + + if (IsGetCustomAttributesLengthComparison(operation, operation.LeftOperand, operation.RightOperand, out _)) + { + context.ReportDiagnostic(Rule, operation, "GetCustomAttributes().Length"); + } + else if (IsGetCustomAttributesCountComparison(operation, operation.LeftOperand, operation.RightOperand, out _)) + { + context.ReportDiagnostic(Rule, operation, "GetCustomAttributes().Count()"); + } + } + + public void AnalyzeIsPattern(OperationAnalysisContext context) + { + var operation = (IIsPatternOperation)context.Operation; + + if (operation.Pattern is not (IConstantPatternOperation or INegatedPatternOperation)) + return; + + if (!IsGetCustomAttributeInvocation(operation.Value, out var invocation)) + return; + + context.ReportDiagnostic(Rule, operation, invocation!.TargetMethod.Name); + } + + public void AnalyzeInvocation(OperationAnalysisContext context) + { + var operation = (IInvocationOperation)context.Operation; + + // Check if this is the specific Any(IEnumerable) method + if (operation.TargetMethod.OriginalDefinition.IsEqualTo(_enumerableAnyMethod)) + { + if (operation.Arguments.Length != 1) + return; + + var instance = operation.Arguments[0].Value; + if (!IsGetCustomAttributesInvocation(instance, out _)) + return; + + context.ReportDiagnostic(Rule, operation, "GetCustomAttributes().Any()"); + } + } + + private bool IsGetCustomAttributeComparison(IOperation left, IOperation right, out IInvocationOperation? invocation) + { + if (right.IsNull()) + return IsGetCustomAttributeInvocation(left, out invocation); + + if (left.IsNull()) + return IsGetCustomAttributeInvocation(right, out invocation); + + invocation = null; + return false; + } + + private bool IsGetCustomAttributesLengthComparison(IBinaryOperation binaryOp, IOperation left, IOperation right, out IInvocationOperation? invocation) + { + invocation = null; + + if (left is not IPropertyReferenceOperation propertyReference) + return false; + + if (propertyReference.Property.Name is not "Length") + return false; + + if (propertyReference.Instance is null) + return false; + + if (!IsGetCustomAttributesInvocation(propertyReference.Instance, out invocation)) + return false; + + // Only allow clear-cut patterns that unambiguously check for existence + if (right.ConstantValue is not { HasValue: true, Value: int value }) + return false; + + // Validate that the operator + value combination makes sense + return IsValidLengthComparisonPattern(binaryOp.OperatorKind, value, lengthIsOnLeft: true); + } + + private static bool IsValidLengthComparisonPattern(BinaryOperatorKind operatorKind, int value, bool lengthIsOnLeft) + { + if (lengthIsOnLeft) + { + return (operatorKind, value) switch + { + (BinaryOperatorKind.Equals, 0) => true, // length == 0 + (BinaryOperatorKind.NotEquals, 0) => true, // length != 0 + (BinaryOperatorKind.GreaterThan, 0) => true, // length > 0 + (BinaryOperatorKind.GreaterThanOrEqual, 1) => true, // length >= 1 + (BinaryOperatorKind.LessThan, 1) => true, // length < 1 + (BinaryOperatorKind.LessThanOrEqual, 0) => true, // length <= 0 + _ => false, + }; + } + else + { + return (operatorKind, value) switch + { + (BinaryOperatorKind.Equals, 0) => true, // 0 == length + (BinaryOperatorKind.NotEquals, 0) => true, // 0 != length + (BinaryOperatorKind.LessThan, 0) => true, // 0 < length (length > 0) + (BinaryOperatorKind.LessThanOrEqual, 1) => true, // 1 <= length (length >= 1) + (BinaryOperatorKind.GreaterThan, 1) => true, // 1 > length (length < 1) + (BinaryOperatorKind.GreaterThanOrEqual, 0) => true, // 0 >= length (length <= 0) + _ => false, + }; + } + } + + private bool IsGetCustomAttributesCountComparison(IBinaryOperation binaryOp, IOperation left, IOperation right, out IInvocationOperation? invocation) + { + invocation = null; + + if (left is not IInvocationOperation countInvocation) + return false; + + // Check if this is the specific Count(IEnumerable) method + if (_enumerableCountMethod is null || + !SymbolEqualityComparer.Default.Equals(countInvocation.TargetMethod.OriginalDefinition, _enumerableCountMethod)) + return false; + + // Only detect Count() without predicate (1 argument = the collection itself) + if (countInvocation.Arguments.Length != 1) + return false; + + var instance = countInvocation.Arguments[0].Value; + if (!IsGetCustomAttributesInvocation(instance, out invocation)) + return false; + + // Only allow clear-cut patterns that unambiguously check for existence + if (right.ConstantValue is not { HasValue: true, Value: int value }) + return false; + + // Use the same validation as Length (Count and Length have the same semantics) + return IsValidLengthComparisonPattern(binaryOp.OperatorKind, value, lengthIsOnLeft: true); + } + + private bool IsGetCustomAttributeInvocation(IOperation operation, out IInvocationOperation? invocation) + { + invocation = operation.UnwrapConversionOperations() as IInvocationOperation; + if (invocation is null) + return false; + + if (invocation.TargetMethod.Name != "GetCustomAttribute") + return false; + + // For extension methods, the instance is in the first argument + var instance = invocation.Instance; + if (instance is null && invocation.TargetMethod.IsExtensionMethod && invocation.Arguments.Length > 0) + { + instance = invocation.Arguments[0].Value; + } + else if (instance is null && invocation.TargetMethod.IsStatic && SymbolEqualityComparer.Default.Equals(invocation.TargetMethod.ContainingType, _attributeSymbol)) + { + if (invocation.Arguments.Length > 0) + { + instance = invocation.Arguments[0].Value; + } + } + + if (instance is null) + return false; + + return IsValidInstanceType(instance.Type); + } + + private bool IsGetCustomAttributesInvocation(IOperation operation, out IInvocationOperation? invocation) + { + invocation = operation as IInvocationOperation; + if (invocation is null) + return false; + + if (invocation.TargetMethod.Name != "GetCustomAttributes") + return false; + + if (!IsMethodFromReflectionTypes(invocation.TargetMethod)) + return false; + + // For extension methods, the instance is in the first argument + var instance = invocation.Instance; + if (instance is null && invocation.TargetMethod.IsExtensionMethod && invocation.Arguments.Length > 0) + { + instance = invocation.Arguments[0].Value; + } + else if (instance is null && invocation.TargetMethod.IsStatic && SymbolEqualityComparer.Default.Equals(invocation.TargetMethod.ContainingType, _attributeSymbol)) + { + if (invocation.Arguments.Length > 0) + { + instance = invocation.Arguments[0].Value; + } + } + + if (instance is null) + return false; + + return IsValidInstanceType(instance.Type); + } + + private bool IsMethodFromReflectionTypes(IMethodSymbol method) + { + if (SymbolEqualityComparer.Default.Equals(method.ContainingType, _customAttributeExtensionsSymbol)) + return true; + + if (SymbolEqualityComparer.Default.Equals(method.ContainingType, _attributeSymbol)) + return true; + + // Check for extension methods on reflection types + if (method.Name is "GetCustomAttribute" or "GetCustomAttributes") + { + if (method.Parameters.Length > 0) + { + var firstParamType = method.Parameters[0].Type; + if (IsReflectionType(firstParamType) || IsParameterInfo(firstParamType)) + return true; + } + } + + return false; + } + + private bool IsParameterInfo(ITypeSymbol type) => type.IsEqualTo(_parameterInfoSymbol); + private bool IsReflectionType(ITypeSymbol type) => type.IsEqualToAny(_assemblySymbol, _moduleSymbol, _memberInfoSymbol, _typeSymbol); + + private bool IsValidInstanceType(ITypeSymbol? type) + { + if (type is null) + return false; + + return type.IsOrInheritFrom(_assemblySymbol) || + type.IsOrInheritFrom(_moduleSymbol) || + type.IsOrInheritFrom(_memberInfoSymbol) || + type.IsOrInheritFrom(_typeSymbol); + } + } +} diff --git a/tests/Meziantou.Analyzer.Test/Rules/UseAttributeIsDefinedAnalyzerTests.cs b/tests/Meziantou.Analyzer.Test/Rules/UseAttributeIsDefinedAnalyzerTests.cs new file mode 100644 index 000000000..49eee20db --- /dev/null +++ b/tests/Meziantou.Analyzer.Test/Rules/UseAttributeIsDefinedAnalyzerTests.cs @@ -0,0 +1,658 @@ +using Meziantou.Analyzer.Rules; +using TestHelper; + +namespace Meziantou.Analyzer.Test.Rules; + +public sealed class UseAttributeIsDefinedAnalyzerTests +{ + private static ProjectBuilder CreateProjectBuilder() + { + return new ProjectBuilder() + .WithAnalyzer() + .WithCodeFixProvider(); + } + + [Fact] + public async Task GetCustomAttribute_NotEqualNull_MemberInfo() + { + await CreateProjectBuilder() + .WithSourceCode(""" +using System; +using System.Reflection; + +class TestClass +{ + void Test(MemberInfo member) + { + _ = [|member.GetCustomAttribute() != null|]; + } +} +""") + .ShouldFixCodeWith(""" +using System; +using System.Reflection; + +class TestClass +{ + void Test(MemberInfo member) + { + _ = Attribute.IsDefined(member, typeof(ObsoleteAttribute)); + } +} +""") + .ValidateAsync(); + } + + [Fact] + public async Task GetCustomAttribute_EqualNull_MemberInfo() + { + await CreateProjectBuilder() + .WithSourceCode(""" +using System; +using System.Reflection; + +class TestClass +{ + void Test(MemberInfo member) + { + _ = [|member.GetCustomAttribute() == null|]; + } +} +""") + .ShouldFixCodeWith(""" +using System; +using System.Reflection; + +class TestClass +{ + void Test(MemberInfo member) + { + _ = !Attribute.IsDefined(member, typeof(ObsoleteAttribute)); + } +} +""") + .ValidateAsync(); + } + + [Fact] + public async Task GetCustomAttribute_IsNull_MemberInfo() + { + await CreateProjectBuilder() + .WithSourceCode(""" +using System; +using System.Reflection; + +class TestClass +{ + void Test(MemberInfo member) + { + _ = [|member.GetCustomAttribute() is null|]; + } +} +""") + .ShouldFixCodeWith(""" +using System; +using System.Reflection; + +class TestClass +{ + void Test(MemberInfo member) + { + _ = !Attribute.IsDefined(member, typeof(ObsoleteAttribute)); + } +} +""") + .ValidateAsync(); + } + + [Fact] + public async Task GetCustomAttribute_IsNotNull_MemberInfo() + { + await CreateProjectBuilder() + .WithSourceCode(""" +using System; +using System.Reflection; + +class TestClass +{ + void Test(MemberInfo member) + { + _ = [|member.GetCustomAttribute() is not null|]; + } +} +""") + .ShouldFixCodeWith(""" +using System; +using System.Reflection; + +class TestClass +{ + void Test(MemberInfo member) + { + _ = Attribute.IsDefined(member, typeof(ObsoleteAttribute)); + } +} +""") + .ValidateAsync(); + } + + [Fact] + public async Task GetCustomAttributes_Any_MemberInfo() + { + await CreateProjectBuilder() + .WithSourceCode(""" +using System; +using System.Linq; +using System.Reflection; + +class TestClass +{ + void Test(MemberInfo member) + { + _ = [|member.GetCustomAttributes().Any()|]; + } +} +""") + .ShouldFixCodeWith(""" +using System; +using System.Linq; +using System.Reflection; + +class TestClass +{ + void Test(MemberInfo member) + { + _ = Attribute.IsDefined(member, typeof(ObsoleteAttribute)); + } +} +""") + .ValidateAsync(); + } + + [Fact] + public async Task GetCustomAttribute_NotEqualNull_Type() + { + await CreateProjectBuilder() + .WithSourceCode(""" +using System; +using System.Reflection; + +class TestClass +{ + void Test(Type type) + { + _ = [|type.GetCustomAttribute() != null|]; + } +} +""") + .ShouldFixCodeWith(""" +using System; +using System.Reflection; + +class TestClass +{ + void Test(Type type) + { + _ = Attribute.IsDefined(type, typeof(ObsoleteAttribute)); + } +} +""") + .ValidateAsync(); + } + + [Fact] + public async Task GetCustomAttribute_NotEqualNull_Assembly() + { + await CreateProjectBuilder() + .WithSourceCode(""" +using System; +using System.Reflection; + +class TestClass +{ + void Test(Assembly assembly) + { + _ = [|assembly.GetCustomAttribute() != null|]; + } +} +""") + .ShouldFixCodeWith(""" +using System; +using System.Reflection; + +class TestClass +{ + void Test(Assembly assembly) + { + _ = Attribute.IsDefined(assembly, typeof(ObsoleteAttribute)); + } +} +""") + .ValidateAsync(); + } + + [Fact] + public async Task GetCustomAttribute_NotEqualNull_Module() + { + await CreateProjectBuilder() + .WithSourceCode(""" +using System; +using System.Reflection; + +class TestClass +{ + void Test(Module module) + { + _ = [|module.GetCustomAttribute() != null|]; + } +} +""") + .ShouldFixCodeWith(""" +using System; +using System.Reflection; + +class TestClass +{ + void Test(Module module) + { + _ = Attribute.IsDefined(module, typeof(ObsoleteAttribute)); + } +} +""") + .ValidateAsync(); + } + + [Fact] + public async Task GetCustomAttribute_WithInherit_NotEqualNull_MemberInfo() + { + await CreateProjectBuilder() + .WithSourceCode(""" +using System; +using System.Reflection; + +class TestClass +{ + void Test(MemberInfo member) + { + _ = [|member.GetCustomAttribute(inherit: true) != null|]; + } +} +""") + .ShouldFixCodeWith(""" +using System; +using System.Reflection; + +class TestClass +{ + void Test(MemberInfo member) + { + _ = Attribute.IsDefined(member, typeof(ObsoleteAttribute), inherit: true); + } +} +""") + .ValidateAsync(); + } + + [Fact] + public async Task GetCustomAttributes_WithInherit_Any_MemberInfo() + { + await CreateProjectBuilder() + .WithSourceCode(""" +using System; +using System.Linq; +using System.Reflection; + +class TestClass +{ + void Test(MemberInfo member) + { + _ = [|member.GetCustomAttributes(inherit: true).Any()|]; + } +} +""") + .ShouldFixCodeWith(""" +using System; +using System.Linq; +using System.Reflection; + +class TestClass +{ + void Test(MemberInfo member) + { + _ = Attribute.IsDefined(member, typeof(ObsoleteAttribute), inherit: true); + } +} +""") + .ValidateAsync(); + } + + [Fact] + public async Task GetCustomAttribute_UsedDirectly_ShouldNotReport() + { + await CreateProjectBuilder() + .WithSourceCode(""" +using System; +using System.Reflection; + +class TestClass +{ + void Test(MemberInfo member) + { + var attr = member.GetCustomAttribute(); + _ = attr.Message; + } +} +""") + .ValidateAsync(); + } + + [Fact] + public async Task GetCustomAttributes_WithPredicate_ShouldNotReport() + { + await CreateProjectBuilder() + .WithSourceCode(""" +using System; +using System.Linq; +using System.Reflection; + +class TestClass +{ + void Test(MemberInfo member) + { + _ = member.GetCustomAttributes().Any(a => a.Message != null); + } +} +""") + .ValidateAsync(); + } + + [Fact] + public async Task GetCustomAttributes_Any_WithTruePredicate_ShouldNotReport() + { + await CreateProjectBuilder() + .WithSourceCode(""" +using System; +using System.Linq; +using System.Reflection; + +class TestClass +{ + void Test(MemberInfo member) + { + _ = member.GetCustomAttributes().Any(attr => true); + } +} +""") + .ValidateAsync(); + } + + [Fact] + public async Task GetCustomAttributes_Count_ShouldReport() + { + await CreateProjectBuilder() + .WithSourceCode(""" +using System; +using System.Linq; +using System.Reflection; + +class TestClass +{ + void Test(MemberInfo member) + { + _ = [|member.GetCustomAttributes().Count() > 0|]; + } +} +""") + .ShouldFixCodeWith(""" +using System; +using System.Linq; +using System.Reflection; + +class TestClass +{ + void Test(MemberInfo member) + { + _ = Attribute.IsDefined(member, typeof(ObsoleteAttribute)); + } +} +""") + .ValidateAsync(); + } + + [Fact] + public async Task GetCustomAttributes_Count_WithPredicate_ShouldNotReport() + { + await CreateProjectBuilder() + .WithSourceCode(""" +using System; +using System.Linq; +using System.Reflection; + +class TestClass +{ + void Test(MemberInfo member) + { + _ = member.GetCustomAttributes().Count(a => a.Message != null) > 0; + } +} +""") + .ValidateAsync(); + } + + [Fact] + public async Task GetCustomAttribute_NullComparison_ReversedOrder() + { + await CreateProjectBuilder() + .WithSourceCode(""" +using System; +using System.Reflection; + +class TestClass +{ + void Test(MemberInfo member) + { + _ = [|null != member.GetCustomAttribute()|]; + } +} +""") + .ShouldFixCodeWith(""" +using System; +using System.Reflection; + +class TestClass +{ + void Test(MemberInfo member) + { + _ = Attribute.IsDefined(member, typeof(ObsoleteAttribute)); + } +} +""") + .ValidateAsync(); + } + + [Fact] + public async Task GetCustomAttributes_Length_GreaterThanZero() + { + await CreateProjectBuilder() + .WithSourceCode(""" +using System; +using System.Reflection; + +class TestClass +{ + void Test(MemberInfo member) + { + _ = [|member.GetCustomAttributes(typeof(ObsoleteAttribute), false).Length > 0|]; + } +} +""") + .ShouldFixCodeWith(""" +using System; +using System.Reflection; + +class TestClass +{ + void Test(MemberInfo member) + { + _ = Attribute.IsDefined(member, typeof(ObsoleteAttribute), false); + } +} +""") + .ValidateAsync(); + } + + [Fact] + public async Task GetCustomAttributes_Length_NotEqualZero() + { + await CreateProjectBuilder() + .WithSourceCode(""" +using System; +using System.Reflection; + +class TestClass +{ + void Test(MemberInfo member) + { + _ = [|member.GetCustomAttributes(typeof(ObsoleteAttribute), false).Length != 0|]; + } +} +""") + .ShouldFixCodeWith(""" +using System; +using System.Reflection; + +class TestClass +{ + void Test(MemberInfo member) + { + _ = Attribute.IsDefined(member, typeof(ObsoleteAttribute), false); + } +} +""") + .ValidateAsync(); + } + + [Fact] + public async Task GetCustomAttributes_Length_EqualZero() + { + await CreateProjectBuilder() + .WithSourceCode(""" +using System; +using System.Reflection; + +class TestClass +{ + void Test(MemberInfo member) + { + _ = [|member.GetCustomAttributes(typeof(ObsoleteAttribute), false).Length == 0|]; + } +} +""") + .ShouldFixCodeWith(""" +using System; +using System.Reflection; + +class TestClass +{ + void Test(MemberInfo member) + { + _ = !Attribute.IsDefined(member, typeof(ObsoleteAttribute), false); + } +} +""") + .ValidateAsync(); + } + + [Fact] + public async Task GetCustomAttributes_Length_GreaterThanOrEqualOne() + { + await CreateProjectBuilder() + .WithSourceCode(""" +using System; +using System.Reflection; + +class TestClass +{ + void Test(MemberInfo member) + { + _ = [|member.GetCustomAttributes(typeof(ObsoleteAttribute), false).Length >= 1|]; + } +} +""") + .ShouldFixCodeWith(""" +using System; +using System.Reflection; + +class TestClass +{ + void Test(MemberInfo member) + { + _ = Attribute.IsDefined(member, typeof(ObsoleteAttribute), false); + } +} +""") + .ValidateAsync(); + } + + [Fact] + public async Task Attribute_GetCustomAttributes_Length_GreaterThanZero() + { + await CreateProjectBuilder() + .WithSourceCode(""" +using System; +using System.Reflection; + +class TestClass +{ + void Test(MemberInfo member) + { + _ = [|Attribute.GetCustomAttributes(member, typeof(ObsoleteAttribute)).Length > 0|]; + } +} +""") + .ShouldFixCodeWith(""" +using System; +using System.Reflection; + +class TestClass +{ + void Test(MemberInfo member) + { + _ = Attribute.IsDefined(member, typeof(ObsoleteAttribute)); + } +} +""") + .ValidateAsync(); + } + + [Fact] + public async Task Attribute_GetCustomAttribute_NotEqualNull() + { + await CreateProjectBuilder() + .WithSourceCode(""" +using System; +using System.Reflection; + +class TestClass +{ + void Test(MemberInfo member) + { + _ = [|Attribute.GetCustomAttribute(member, typeof(ObsoleteAttribute)) != null|]; + } +} +""") + .ShouldFixCodeWith(""" +using System; +using System.Reflection; + +class TestClass +{ + void Test(MemberInfo member) + { + _ = Attribute.IsDefined(member, typeof(ObsoleteAttribute)); + } +} +""") + .ValidateAsync(); + } +}