-
Notifications
You must be signed in to change notification settings - Fork 468
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Analyzer: Prefer .Length/Count/IsEmpty over Any() #6236
Conversation
...zers/Core/Microsoft.NetCore.Analyzers/Performance/PreferLengthCountIsEmptyOverAnyAnalyzer.cs
Outdated
Show resolved
Hide resolved
...zers/Core/Microsoft.NetCore.Analyzers/Performance/PreferLengthCountIsEmptyOverAnyAnalyzer.cs
Outdated
Show resolved
Hide resolved
...zers/Core/Microsoft.NetCore.Analyzers/Performance/PreferLengthCountIsEmptyOverAnyAnalyzer.cs
Outdated
Show resolved
Hide resolved
...zers/Core/Microsoft.NetCore.Analyzers/Performance/PreferLengthCountIsEmptyOverAnyAnalyzer.cs
Outdated
Show resolved
Hide resolved
...zers/Core/Microsoft.NetCore.Analyzers/Performance/PreferLengthCountIsEmptyOverAnyAnalyzer.cs
Outdated
Show resolved
Hide resolved
...zers/Core/Microsoft.NetCore.Analyzers/Performance/PreferLengthCountIsEmptyOverAnyAnalyzer.cs
Outdated
Show resolved
Hide resolved
...Sharp/Microsoft.NetCore.Analyzers/Performance/CSharpPreferLengthCountIsEmptyOverAny.Fixer.cs
Show resolved
Hide resolved
...alBasic/Microsoft.NetCore.Analyzers/Performance/BasicPreferLengthCountIsEmptyOverAnyFixer.vb
Show resolved
Hide resolved
# Conflicts: # src/NetAnalyzers/Core/AnalyzerReleases.Unshipped.md
I ran the |
# Conflicts: # src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/MicrosoftNetCoreAnalyzersResources.resx # src/NetAnalyzers/Microsoft.CodeAnalysis.NetAnalyzers.md # src/NetAnalyzers/Microsoft.CodeAnalysis.NetAnalyzers.sarif # src/NetAnalyzers/RulesMissingDocumentation.md
@buyaa-n Thanks for the hint! I am using Rider, so I ran |
...alBasic/Microsoft.NetCore.Analyzers/Performance/BasicPreferLengthCountIsEmptyOverAnyFixer.vb
Show resolved
Hide resolved
…rmance/BasicPreferLengthCountIsEmptyOverAnyFixer.vb
Looks like only my VB tests are failing now. Although I am still clueless as to where that indentation issue is coming from. |
Can't you repro it locally? I can, somehow fixer produced source has an extra spacing before
Looks like VB fixer issue, @mavasani @jmarolf is this a known issue? Is there any work around except adjusting the spacing? |
return method.Name == AnyText | ||
&& method.ReturnType.SpecialType == SpecialType.System_Boolean | ||
&& method.Language == LanguageNames.CSharp && method.Parameters.Length == 1 | ||
|| (method.Language == LanguageNames.VisualBasic && (method.Parameters.Length == 0 || method.Parameters.Length == 1 && method.Parameters[0].Name == SourceParameterName)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Something looks smelly here. In VB, you don't check the method name and return type at all.
In C#, you don't check the parameter name.
In general, I think this should be a symbol comparison.
NOTE: My suggestions below addresses this.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In general, I think this should be a symbol comparison.
In case we want to restrict to a specific symbol like Enumerable.Any()
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Turns out we want to flag for any type, not only IEnumerable.Any()
var firstArgument = invocation.Instance ?? invocation.Arguments[0].Value; | ||
var type = (firstArgument as IConversionOperation)?.Operand.Type ?? firstArgument.Type; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@mavasani Is this logic correct? The logic in GetReceiverType
seems more expensive (involves semantic model calls).
for now, I think this should use GetReceiverType
, and if this logic is correct and more efficient, then GetReceiverType
should be updated as a follow up. What do you think?
...zers/Core/Microsoft.NetCore.Analyzers/Performance/PreferLengthCountIsEmptyOverAnyAnalyzer.cs
Show resolved
Hide resolved
...zers/Core/Microsoft.NetCore.Analyzers/Performance/PreferLengthCountIsEmptyOverAnyAnalyzer.cs
Outdated
Show resolved
Hide resolved
...zers/Core/Microsoft.NetCore.Analyzers/Performance/PreferLengthCountIsEmptyOverAnyAnalyzer.cs
Show resolved
Hide resolved
{ | ||
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); | ||
context.EnableConcurrentExecution(); | ||
context.RegisterOperationAction(OnInvocationAnalysis, OperationKind.Invocation); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
context.RegisterOperationAction(OnInvocationAnalysis, OperationKind.Invocation); | |
context.RegisterCompilationStartAction(context => | |
{ | |
var anyMethod = WellKnownTypeProvider.GetOrCreate(context.Compilation).GetOrCreateTypeByMetadataName(WellKnownTypeNames.SystemLinqEnumerable) | |
?.GetMembers(AnyText).OfType<IMethodSymbol>().FirstOrDefault(m => m.IsExtensionMethod && m.Parameters.Length == 1); | |
if (anyMethod is not null) | |
{ | |
context.RegisterOperationAction(context => OnInvocationAnalysis(context, anyMethod), OperationKind.Invocation); | |
} | |
}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The problem with this approach is that we miss things like https://learn.microsoft.com/en-us/dotnet/api/system.linq.immutablearrayextensions.any?view=net-7.0#system-linq-immutablearrayextensions-any-1(system-collections-immutable-immutablearray((-0)))
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@CollinAlpert I see. Then consider not adding anything to compilation start, but check the method correctly.
The code should be language-agnostic (no C#/VB checks), and parameter name shouldn't be checked. Just check method name, whether it's extension method, whether it has a single parameter, and whether its return type is boolean.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I remember having problems with eliminating language checks, as I believe Roslyn considers Any
to be an extension method in C# and not an extension method in VB.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@CollinAlpert That suggestion to use ReducedFrom
if MethodKind
is ReducedExtension
should probably fix the problems you were seeing.
...zers/Core/Microsoft.NetCore.Analyzers/Performance/PreferLengthCountIsEmptyOverAnyAnalyzer.cs
Outdated
Show resolved
Hide resolved
...zers/Core/Microsoft.NetCore.Analyzers/Performance/PreferLengthCountIsEmptyOverAnyAnalyzer.cs
Outdated
Show resolved
Hide resolved
...zers/Core/Microsoft.NetCore.Analyzers/Performance/PreferLengthCountIsEmptyOverAnyAnalyzer.cs
Outdated
Show resolved
Hide resolved
...zers/Core/Microsoft.NetCore.Analyzers/Performance/PreferLengthCountIsEmptyOverAnyAnalyzer.cs
Outdated
Show resolved
Hide resolved
|
||
private static bool IsEligibleAnyMethod(IMethodSymbol method) | ||
{ | ||
return method.Name == AnyText | ||
&& method.ReturnType.SpecialType == SpecialType.System_Boolean | ||
&& method.Language == LanguageNames.CSharp && method.Parameters.Length == 1 | ||
|| (method.Language == LanguageNames.VisualBasic && (method.Parameters.Length == 0 || method.Parameters.Length == 1 && method.Parameters[0].Name == SourceParameterName)); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
private static bool IsEligibleAnyMethod(IMethodSymbol method) | |
{ | |
return method.Name == AnyText | |
&& method.ReturnType.SpecialType == SpecialType.System_Boolean | |
&& method.Language == LanguageNames.CSharp && method.Parameters.Length == 1 | |
|| (method.Language == LanguageNames.VisualBasic && (method.Parameters.Length == 0 || method.Parameters.Length == 1 && method.Parameters[0].Name == SourceParameterName)); | |
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Given that we need to catch any Any
method. The check should be:
return method is
{
Name: AnyText,
ReturnType.SpecialType: SpecialType.System_Boolean,
IsExtensionMethod: true,
Parameters: [_]
};
On the callsite, the passed method should be:
var targetMethod = invocation.TargetMethod;
if (targetMethod.MethodKind == MethodKind.ReducedExtension)
{
targetMethod = targetMethod.ReducedFrom;
}
// pass targetMethod to IsEligibleAnyMethod at this point.
|
||
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics { get; } = ImmutableArray.Create( | ||
LengthDescriptor, | ||
CountDescriptor, | ||
IsEmptyDescriptor | ||
); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics { get; } = ImmutableArray.Create( | |
LengthDescriptor, | |
CountDescriptor, | |
IsEmptyDescriptor | |
); |
expression, | ||
IdentifierName(PreferLengthCountIsEmptyOverAnyAnalyzer.IsEmptyText) | ||
); | ||
if (invocation.Parent is PrefixUnaryExpressionSyntax prefixExpression && prefixExpression.IsKind(SyntaxKind.LogicalNotExpression)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
if (invocation.Parent is PrefixUnaryExpressionSyntax prefixExpression && prefixExpression.IsKind(SyntaxKind.LogicalNotExpression)) | |
if (invocation.Parent.IsKind(SyntaxKind.LogicalNotExpression)) |
...zers/Core/Microsoft.NetCore.Analyzers/Performance/PreferLengthCountIsEmptyOverAnyAnalyzer.cs
Show resolved
Hide resolved
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks so much for all the great work here. I would like us to
- Verify that the
Any
extension method we get is the one defined in the framework by checking the namespace that it is defined in. - Verify that the type inherits from either
IEnumerable
orIEnumerable<T>
With those additional checks in place I would be happy to merge this.
Thanks for the feedback! I have incorporated the changes and now check that the |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks good! thanks for all the work here.
Happy to be able to contribute! |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@CollinAlpert the different ids not suggested in the API review and I do not see need of using 3 ids, the id ranges are limited, we better reuse same id for all diagnostics. Sorry for requesting it now, in case you are busy, I will try to contribute to the PR with the change
No worries, I can change it. However then we need a new message which encapsulates all three properties. Do you have a suggestion? |
Thank you!
I think you can create each diagnostic with existing different messages, no? Though not sure which title/description will be on doc internal const string RuleId = "CA1860";
internal static readonly DiagnosticDescriptor IsEmptyDescriptor = DiagnosticDescriptorHelper.Create(
RuleId,
CreateLocalizableResourceString(nameof(PreferIsEmptyOverAnyTitle)),
CreateLocalizableResourceString(nameof(PreferIsEmptyOverAnyMessage)),
DiagnosticCategory.Performance,
RuleLevel.IdeSuggestion,
CreateLocalizableResourceString(nameof(PreferIsEmptyOverAnyDescription)),
isPortedFxCopRule: false,
isDataflowRule: false
);
internal static readonly DiagnosticDescriptor LengthDescriptor = DiagnosticDescriptorHelper.Create(
RuleId,
CreateLocalizableResourceString(nameof(PreferLengthOverAnyTitle)),
CreateLocalizableResourceString(nameof(PreferLengthOverAnyMessage)),
DiagnosticCategory.Performance,
RuleLevel.IdeSuggestion,
CreateLocalizableResourceString(nameof(PreferLengthOverAnyDescription)),
isPortedFxCopRule: false,
isDataflowRule: false
);
... |
The generated docs just using the 1st diagnostic description and title, for me that is OK as integrated title and/or description doesn't look good to me. For example: I prefer For distinguishing the diagnostics in fixer It seems better to use the properties as checking the DiagnosticDescriptiors somehow not triggering the fixer: roslyn-analyzers/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/Performance/UseCountProperly.cs Line 377 in 9b5d130
|
Diagnostic Ids are most important when thinking about how a developer is going to suppress this analyzer. Because I cannot think of a good reason to suppress some of these cases and not others, I agree with @buyaa-n that we should just have once id. For documentation I think we should have the same Title for all the diagnostics with different descriptions depending on the situation. |
# Conflicts: # src/NetAnalyzers/Core/AnalyzerReleases.Unshipped.md # src/NetAnalyzers/RulesMissingDocumentation.md
Hm, it seems the other title/descriptions are not used/visible anywhere else.
Right, it seems we should use one title, is the different descriptions will be used anywhere? How is this title sounds? And a description could be something like: |
...zers/Core/Microsoft.NetCore.Analyzers/Performance/PreferLengthCountIsEmptyOverAnyAnalyzer.cs
Outdated
Show resolved
Hide resolved
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM thanks!
* Add analyzer and fixer. * Update src/NetAnalyzers/VisualBasic/Microsoft.NetCore.Analyzers/Performance/BasicPreferLengthCountIsEmptyOverAnyFixer.vb * Extended type check for 'Length' and changed wording of message. * Updated wording on message descriptions. * Change 'GetReceiverType' to return an 'ITypeSymbol'.
The main discussion thread its closed, but this is a bad change, at least its a warning we can disable, but this is favoring performance over legibility. |
It's not even a warning, it defaults to info / suggestion. If you're seeing it as a warning, it's because you're doing something to bump up its severity. (And legibility is in the eye of the beholder; personally, I find explicit use of the properties more readable/understandable.) |
This PR adds an analyzer which flags usages of an
.Any()
call which could be replaced with aCount
,Length
orIsEmpty
check.My VB tests are currently failing due to an indentation issue. Maybe someone more versed in VB than me could take a look and tell me what I am missing?
Fixes dotnet/runtime#75933