Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)|ℹ️|✔️|✔️|

<!-- rules -->

Expand Down
7 changes: 7 additions & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,7 @@
|[MA0176](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0176.md)|Performance|Optimize guid creation|<span title='Info'>ℹ️</span>|✔️|✔️|
|[MA0177](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0177.md)|Style|Use single-line XML comment syntax when possible|<span title='Info'>ℹ️</span>|❌|✔️|
|[MA0178](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0178.md)|Design|Use TimeSpan.Zero instead of TimeSpan.FromXXX(0)|<span title='Info'>ℹ️</span>|✔️|✔️|
|[MA0179](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0179.md)|Performance|Use Attribute.IsDefined instead of GetCustomAttribute(s)|<span title='Info'>ℹ️</span>|✔️|✔️|

|Id|Suppressed rule|Justification|
|--|---------------|-------------|
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
```
42 changes: 42 additions & 0 deletions docs/Rules/MA0179.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# MA0179 - Use Attribute.IsDefined instead of GetCustomAttribute(s)
<!-- sources -->
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)
<!-- sources -->

`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<ObsoleteAttribute>() != null) { }
if (type.GetCustomAttribute<ObsoleteAttribute>() == null) { }
if (type.GetCustomAttribute<ObsoleteAttribute>() is null) { }
if (type.GetCustomAttribute<ObsoleteAttribute>() is not null) { }
if (member.GetCustomAttributes<ObsoleteAttribute>().Any()) { }
if (member.GetCustomAttributes<ObsoleteAttribute>().Count() > 0) { }
if (member.GetCustomAttributes<ObsoleteAttribute>().Length > 0) { }
if (member.GetCustomAttributes<ObsoleteAttribute>().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<ObsoleteAttribute>().Any(a => a.Message != null)) { }
if (member.GetCustomAttributes<ObsoleteAttribute>().Count(a => a.Message != null) > 0) { }

// compliant - accessing attribute properties
var attr = type.GetCustomAttribute<ObsoleteAttribute>();
if (attr != null)
{
_ = attr.Message;
}
````
Original file line number Diff line number Diff line change
@@ -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<string> 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<Document> 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<T>(IEnumerable<T>) 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<T>(IEnumerable<T>) 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<SyntaxNode>();
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;
}
}
Loading