| File: src\roslyn\src\Analyzers\Core\Analyzers\SimplifyLinqExpression\AbstractSimplifyLinqExpressionDiagnosticAnalyzer.cs | Web Access |
| Project: src\roslyn\src\CodeStyle\Core\Analyzers\Microsoft.CodeAnalysis.CodeStyle.csproj (Microsoft.CodeAnalysis.CodeStyle) |
// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. using System.Collections.Immutable; using System.Diagnostics.CodeAnalysis; using System.Linq; using Microsoft.CodeAnalysis.CodeStyle; using Microsoft.CodeAnalysis.Collections; using Microsoft.CodeAnalysis.Diagnostics; using Microsoft.CodeAnalysis.LanguageService; using Microsoft.CodeAnalysis.Operations; using Microsoft.CodeAnalysis.PooledObjects; using Microsoft.CodeAnalysis.Text; namespace Microsoft.CodeAnalysis.SimplifyLinqExpression; internal abstract class AbstractSimplifyLinqExpressionDiagnosticAnalyzer< TExpressionSyntax, TSimpleNameSyntax, TInvocationExpressionSyntax, TMemberAccessExpressionSyntax>() : AbstractBuiltInUnnecessaryCodeStyleDiagnosticAnalyzer( IDEDiagnosticIds.SimplifyLinqExpressionDiagnosticId, EnforceOnBuildValues.SimplifyLinqExpression, option: null, title: new LocalizableResourceString(nameof(AnalyzersResources.Simplify_LINQ_expression), AnalyzersResources.ResourceManager, typeof(AnalyzersResources))) where TExpressionSyntax : SyntaxNode where TSimpleNameSyntax : TExpressionSyntax where TInvocationExpressionSyntax : TExpressionSyntax where TMemberAccessExpressionSyntax : TExpressionSyntax { private static readonly ImmutableHashSet<string> s_nonEnumerableReturningLinqPredicateMethodNames = [ nameof(Enumerable.First), nameof(Enumerable.Last), nameof(Enumerable.Single), nameof(Enumerable.Any), nameof(Enumerable.Count), nameof(Enumerable.SingleOrDefault), nameof(Enumerable.FirstOrDefault), nameof(Enumerable.LastOrDefault)]; private static readonly ImmutableHashSet<string> s_nonEnumerableReturningLinqSelectorMethodNames = [ nameof(Enumerable.Average), nameof(Enumerable.Sum), nameof(Enumerable.Min), nameof(Enumerable.Max)]; protected abstract ISyntaxFacts SyntaxFacts { get; } protected abstract bool ConflictsWithMemberByNameOnly { get; } protected abstract IInvocationOperation? TryGetNextInvocationInChain(IInvocationOperation invocation); public override DiagnosticAnalyzerCategory GetAnalyzerCategory() => DiagnosticAnalyzerCategory.SemanticSpanAnalysis; protected override void InitializeWorker(AnalysisContext context) => context.RegisterCompilationStartAction(OnCompilationStart); private void OnCompilationStart(CompilationStartAnalysisContext context) { if (TryGetEnumerableTypeSymbol(context.Compilation, out var enumerableType) && TryGetLinqWhereExtensionMethod(enumerableType, out var whereMethodSymbol) && TryGetLinqSelectExtensionMethod(enumerableType, out var selectMethodSymbol) && TryGetLinqMethodsThatDoNotReturnEnumerables(enumerableType, out var linqMethods)) { context.RegisterOperationAction(AnalyzeInvocationOperation, OperationKind.Invocation); } return; static bool TryGetEnumerableTypeSymbol(Compilation compilation, [NotNullWhen(true)] out INamedTypeSymbol? enumerableType) { enumerableType = compilation.GetTypeByMetadataName(typeof(Enumerable)?.FullName!); return enumerableType is not null; } static bool TryGetLinqWhereExtensionMethod(INamedTypeSymbol enumerableType, [NotNullWhen(true)] out IMethodSymbol? linqMethod) => TryGetLinqExtensionMethod(enumerableType, nameof(Enumerable.Where), out linqMethod); static bool TryGetLinqSelectExtensionMethod(INamedTypeSymbol enumerableType, [NotNullWhen(true)] out IMethodSymbol? linqMethod) => TryGetLinqExtensionMethod(enumerableType, nameof(Enumerable.Select), out linqMethod); static bool TryGetLinqExtensionMethod(INamedTypeSymbol enumerableType, string name, [NotNullWhen(true)] out IMethodSymbol? linqMethod) { foreach (var linqMethodSymbol in enumerableType.GetMembers(name).OfType<IMethodSymbol>()) { if (linqMethodSymbol.Parameters is [_, { Type: INamedTypeSymbol { Arity: 2 } }]) { // This is the Where/Select overload that does not take and index (i.e. Where(source, Func<T, bool>) // vs Where(source, Func<T, int, bool>)) linqMethod = linqMethodSymbol; return true; } } linqMethod = null; return false; } static bool TryGetLinqMethodsThatDoNotReturnEnumerables(INamedTypeSymbol enumerableType, out ImmutableArray<IMethodSymbol> linqMethods) { using var _ = ArrayBuilder<IMethodSymbol>.GetInstance(out var linqMethodSymbolsBuilder); foreach (var method in enumerableType.GetMembers().OfType<IMethodSymbol>()) { if (method.Parameters.Length != 1) continue; if (s_nonEnumerableReturningLinqPredicateMethodNames.Contains(method.Name) || s_nonEnumerableReturningLinqSelectorMethodNames.Contains(method.Name)) { linqMethodSymbolsBuilder.AddRange(method); } } linqMethods = linqMethodSymbolsBuilder.ToImmutable(); return linqMethods.Any(); } void AnalyzeInvocationOperation(OperationAnalysisContext context) { if (ShouldSkipAnalysis(context, notification: null)) return; // Do not analyze linq methods that contain diagnostics. if (context.Operation.Syntax.GetDiagnostics().Any(diagnostic => diagnostic.Severity == DiagnosticSeverity.Error)) return; // we only care about Where/Select invocation methods on linq expressions if (context.Operation is not IInvocationOperation invocation) return; var isWhereMethod = IsWhereLinqMethod(invocation); var isSelectMethod = IsSelectLinqMethod(invocation); if (!isWhereMethod && !isSelectMethod) return; if (TryGetNextInvocationInChain(invocation) is not IInvocationOperation nextInvocation || !IsInvocationNonEnumerableReturningLinqMethod(nextInvocation)) { // Invocation is not part of a chain of invocations (i.e. Where(x => x is not null).First()) return; } if (TryGetSymbolOfMemberAccess(invocation) is not ITypeSymbol targetTypeSymbol) return; if (nextInvocation.Syntax is not TInvocationExpressionSyntax nextInvocationNode || TryGetMethodName(nextInvocationNode) is not TSimpleNameSyntax memberName) { return; } var name = SyntaxFacts.GetIdentifierOfSimpleName(memberName).ValueText; if (isWhereMethod && !s_nonEnumerableReturningLinqPredicateMethodNames.Contains(name)) return; if (isSelectMethod && !s_nonEnumerableReturningLinqSelectorMethodNames.Contains(name)) return; // Do not offer to transpose if there is already a method on the collection named the same as the linq extension // method. This would cause us to call the instance method after the transformation, not the extension method. if (!targetTypeSymbol.Equals(enumerableType, SymbolEqualityComparer.Default)) { var members = targetTypeSymbol.GetMembers(name); if (members.Length > 0) { // VB conflicts if any member has the same name (like a Count property vs Count extension method). if (this.ConflictsWithMemberByNameOnly) return; // C# conflicts only if it is a method as well. So a Count property will not conflict with a Count // extension method. if (members.Any(m => m is IMethodSymbol)) return; } } // For `Where(x => x is not null).First()` fade out the `First()` portion as we're effectively trimming off // the last unnecessary chunk. using var additionalUnnecessaryLocations = TemporaryArray<Location>.Empty; additionalUnnecessaryLocations.Add(nextInvocationNode.SyntaxTree.GetLocation( TextSpan.FromBounds(memberName.SpanStart, nextInvocationNode.Span.End))); context.ReportDiagnostic(DiagnosticHelper.CreateWithLocationTags( Descriptor, nextInvocation.Syntax.GetLocation(), NotificationOption2.ForSeverity(Descriptor.DefaultSeverity), context.Options, additionalLocations: [], additionalUnnecessaryLocations.ToImmutableAndClear())); } bool IsWhereLinqMethod(IInvocationOperation invocation) => whereMethodSymbol.Equals(invocation.TargetMethod.ReducedFrom ?? invocation.TargetMethod.OriginalDefinition, SymbolEqualityComparer.Default); bool IsSelectLinqMethod(IInvocationOperation invocation) => selectMethodSymbol.Equals(invocation.TargetMethod.ReducedFrom ?? invocation.TargetMethod.OriginalDefinition, SymbolEqualityComparer.Default); bool IsInvocationNonEnumerableReturningLinqMethod(IInvocationOperation invocation) => linqMethods.Any(static (m, invocation) => m.Equals(invocation.TargetMethod.ReducedFrom ?? invocation.TargetMethod.OriginalDefinition, SymbolEqualityComparer.Default), invocation); ITypeSymbol? TryGetSymbolOfMemberAccess(IInvocationOperation invocation) { if (invocation.Syntax is not TInvocationExpressionSyntax invocationNode || SyntaxFacts.GetExpressionOfInvocationExpression(invocationNode) is not TMemberAccessExpressionSyntax memberAccess || SyntaxFacts.GetExpressionOfMemberAccessExpression(memberAccess) is not SyntaxNode expression) { return null; } return invocation.SemanticModel?.GetTypeInfo(expression).Type; } TSimpleNameSyntax? TryGetMethodName(TInvocationExpressionSyntax invocationNode) { if (SyntaxFacts.GetExpressionOfInvocationExpression(invocationNode) is not TMemberAccessExpressionSyntax memberAccess) return null; var memberName = SyntaxFacts.GetNameOfMemberAccessExpression(memberAccess); return memberName as TSimpleNameSyntax; } } }