|
// 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.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.CodeStyle;
using Microsoft.CodeAnalysis.CSharp.CodeStyle;
using Microsoft.CodeAnalysis.CSharp.Extensions;
using Microsoft.CodeAnalysis.CSharp.Shared.Extensions;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.CSharp.Utilities;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.Text;
using Roslyn.Utilities;
namespace Microsoft.CodeAnalysis.CSharp.RemoveUnnecessaryLambdaExpression;
/// <summary>
/// DiagnosticAnalyzer that looks code like <c>Goo(() => Bar())</c> and offers to convert it to <c>Goo(Bar)</c>.
/// This is only offered on C# 11 and above where this delegate can be cached and will not cause allocations each
/// time.
/// </summary>
[DiagnosticAnalyzer(LanguageNames.CSharp)]
internal sealed class CSharpRemoveUnnecessaryLambdaExpressionDiagnosticAnalyzer()
: AbstractBuiltInUnnecessaryCodeStyleDiagnosticAnalyzer(
IDEDiagnosticIds.RemoveUnnecessaryLambdaExpressionDiagnosticId,
EnforceOnBuildValues.RemoveUnnecessaryLambdaExpression,
CSharpCodeStyleOptions.PreferMethodGroupConversion,
new LocalizableResourceString(nameof(CSharpAnalyzersResources.Remove_unnecessary_lambda_expression), CSharpAnalyzersResources.ResourceManager, typeof(CSharpAnalyzersResources)),
new LocalizableResourceString(nameof(CSharpAnalyzersResources.Lambda_expression_can_be_removed), CSharpAnalyzersResources.ResourceManager, typeof(CSharpAnalyzersResources)))
{
public override DiagnosticAnalyzerCategory GetAnalyzerCategory()
=> DiagnosticAnalyzerCategory.SemanticSpanAnalysis;
protected override void InitializeWorker(AnalysisContext context)
{
context.RegisterCompilationStartAction(context =>
{
if (context.Compilation.LanguageVersion().IsCSharp11OrAbove())
{
var expressionType = context.Compilation.ExpressionOfTType();
var conditionalAttributeType = context.Compilation.ConditionalAttribute();
context.RegisterSyntaxNodeAction(
context => AnalyzeSyntax(context, expressionType, conditionalAttributeType),
SyntaxKind.SimpleLambdaExpression, SyntaxKind.ParenthesizedLambdaExpression, SyntaxKind.AnonymousMethodExpression);
}
});
}
private void AnalyzeSyntax(SyntaxNodeAnalysisContext context, INamedTypeSymbol? expressionType, INamedTypeSymbol? conditionalAttributeType)
{
var cancellationToken = context.CancellationToken;
var semanticModel = context.SemanticModel;
var syntaxTree = semanticModel.SyntaxTree;
var preference = context.GetCSharpAnalyzerOptions().PreferMethodGroupConversion;
if (ShouldSkipAnalysis(context, preference.Notification))
{
// User doesn't care about this rule.
return;
}
var anonymousFunction = (AnonymousFunctionExpressionSyntax)context.Node;
// Syntax checks first.
// Don't simplify static lambdas. The user made them explicitly static to make it clear it must only cause
// a single allocation for the cached delegate. If we get rid of the lambda (and thus the static-keyword) it
// won't be clear anymore if the member-group-conversion allocation is cached or not.
if (anonymousFunction.Modifiers.Any(SyntaxKind.StaticKeyword))
return;
if (!TryGetAnonymousFunctionInvocation(anonymousFunction, out var invocation, out var wasAwaited))
return;
// If we had an async function, but we didn't await the expression inside then we can't convert this. The
// underlying value was wrapped into a task, and that won't work if directly referencing the function.
if (wasAwaited != anonymousFunction.Modifiers.Any(SyntaxKind.AsyncKeyword))
return;
// We have to have an invocation in the lambda like `() => X()` or `() => expr.X()`.
var invokedExpression = invocation.Expression;
if (invokedExpression is not SimpleNameSyntax and not MemberAccessExpressionSyntax)
return;
// lambda and invocation have to agree on number of parameters.
var parameters = GetParameters(anonymousFunction);
if (parameters.Count != invocation.ArgumentList.Arguments.Count)
return;
// parameters must be passed 1:1 from lambda to invocation.
for (int i = 0, n = parameters.Count; i < n; i++)
{
var parameter = parameters[i];
var argument = invocation.ArgumentList.Arguments[i];
if (argument.Expression is not IdentifierNameSyntax argumentIdentifier)
return;
if (parameter.Identifier.ValueText != argumentIdentifier.Identifier.ValueText)
return;
}
// if we have `() => new C().X()` then converting to `new C().X` very much changes the meaning.
if (MayHaveSideEffects(invokedExpression))
return;
// Looks like a reasonable candidate to simplify. Now switch to semantics to check for sure.
if (CSharpSemanticFacts.Instance.IsInExpressionTree(semanticModel, anonymousFunction, expressionType, cancellationToken))
return;
// If we have `object obj = x => Goo(x);` we don't want to simplify. The compiler warns if you write
// `object obj = Goo;` because of the conversion to a non-delegate type. While we could insert a cast here
// to make this work, that goes against the spirit of this analyzer/fixer just removing code.
var lambdaTypeInfo = semanticModel.GetTypeInfo(anonymousFunction, cancellationToken);
if (lambdaTypeInfo.ConvertedType == null || lambdaTypeInfo.ConvertedType.SpecialType is SpecialType.System_Object)
return;
var lambdaSymbolInfo = semanticModel.GetSymbolInfo(anonymousFunction, cancellationToken);
if (lambdaSymbolInfo.Symbol is not IMethodSymbol lambdaMethod)
return;
var invokedSymbolInfo = semanticModel.GetSymbolInfo(invokedExpression, cancellationToken);
if (invokedSymbolInfo.Symbol is not IMethodSymbol invokedMethod)
return;
// cannot convert a partial-definition to a delegate (unless there's an existing implementation part that can be used).
if (invokedMethod is { IsPartialDefinition: true, PartialImplementationPart: null })
return;
// Check if the invoked method is a member of a struct. If it is, and it is not readonly and the invoked method
// is not readonly, then we don't want to convert it to a method group. If we did, we may accidently change the
// behaviour of the code, since structs are value types and the method group will be copied rather than referenced.
if (invokedMethod is { ContainingType.TypeKind: TypeKind.Struct, IsReadOnly: false, ContainingType.IsReadOnly: false })
return;
// If we're calling a generic method, we have to have supplied type arguments. They cannot be inferred once
// we remove the arguments during simplification.
var invokedTypeArguments = invokedExpression.GetRightmostName() is GenericNameSyntax genericName
? genericName.TypeArgumentList.Arguments
: default;
if (invokedMethod.TypeArguments.Length != invokedTypeArguments.Count)
return;
// Methods have to be complimentary. That means the same number of parameters, with proper
// co-contravariance for the parameters and return type.
if (lambdaMethod.Parameters.Length != invokedMethod.Parameters.Length)
return;
var compilation = semanticModel.Compilation;
// Must be able to convert the invoked method return type to the lambda's return type.
if (!IsIdentityOrImplicitConversion(compilation, invokedMethod.ReturnType, lambdaMethod.ReturnType))
return;
for (int i = 0, n = lambdaMethod.Parameters.Length; i < n; i++)
{
var lambdaParameter = lambdaMethod.Parameters[i];
var invokedParameter = invokedMethod.Parameters[i];
if (lambdaParameter.RefKind != invokedParameter.RefKind)
return;
// All the lambda parameters must be convertible to the invoked method parameters.
if (!IsIdentityOrImplicitConversion(compilation, lambdaParameter.Type, invokedParameter.Type))
return;
}
// If invoked method is conditional, converting lambda to method group produces compiler error
if (invokedMethod.HasAttribute(conditionalAttributeType))
return;
// In the case where we have `() => expr.m()`, check if `expr` is overwritten anywhere. If so then we do not
// want to remove the lambda, as that will bind eagerly to the original `expr` and will not see the write
// that later happens
if (invokedExpression is MemberAccessExpressionSyntax { Expression: var accessedExpression })
{
// Limit the search space to the outermost code block that could contain references to this expr (or
// fall back to compilation unit for top level statements).
var outermostBody = invokedExpression.AncestorsAndSelf().LastOrDefault(
n => n is BlockSyntax or ArrowExpressionClauseSyntax or AnonymousFunctionExpressionSyntax or GlobalStatementSyntax);
if (outermostBody is null or GlobalStatementSyntax)
outermostBody = syntaxTree.GetRoot(cancellationToken);
foreach (var candidate in outermostBody.DescendantNodes().OfType<ExpressionSyntax>())
{
if (candidate != accessedExpression &&
SemanticEquivalence.AreEquivalent(semanticModel, candidate, accessedExpression) &&
candidate.IsWrittenTo(semanticModel, cancellationToken))
{
return;
}
}
}
// Semantically, this looks good to go. Now, do an actual speculative replacement to ensure that the
// non-invoked method reference refers to the same method symbol, and that it converts to the same type that
// the lambda was.
var analyzer = new SpeculationAnalyzer(anonymousFunction, invokedExpression, semanticModel, cancellationToken);
var rewrittenExpression = analyzer.ReplacedExpression;
var rewrittenSemanticModel = analyzer.SpeculativeSemanticModel;
var rewrittenSymbolInfo = rewrittenSemanticModel.GetSymbolInfo(rewrittenExpression, cancellationToken);
if (rewrittenSymbolInfo.Symbol is not IMethodSymbol rewrittenMethod ||
!invokedMethod.Equals(rewrittenMethod))
{
return;
}
var rewrittenConvertedType = rewrittenSemanticModel.GetTypeInfo(rewrittenExpression, cancellationToken).ConvertedType;
if (!lambdaTypeInfo.ConvertedType.Equals(rewrittenConvertedType, SymbolEqualityComparer.IncludeNullability))
return;
if (OverloadsChanged(
semanticModel, anonymousFunction.GetRequiredParent(),
rewrittenSemanticModel, rewrittenExpression.GetRequiredParent(), cancellationToken))
{
return;
}
var startReportSpan = TextSpan.FromBounds(anonymousFunction.SpanStart, invokedExpression.SpanStart);
var endReportSpan = TextSpan.FromBounds(invokedExpression.Span.End, anonymousFunction.Span.End);
context.ReportDiagnostic(DiagnosticHelper.CreateWithLocationTags(
Descriptor,
syntaxTree.GetLocation(startReportSpan),
preference.Notification,
context.Options,
additionalLocations: [anonymousFunction.GetLocation()],
additionalUnnecessaryLocations: [syntaxTree.GetLocation(startReportSpan), syntaxTree.GetLocation(endReportSpan)]));
}
private static bool OverloadsChanged(
SemanticModel semanticModel1,
SyntaxNode? node1,
SemanticModel semanticModel2,
SyntaxNode? node2,
CancellationToken cancellationToken)
{
while (node1 != null && node2 != null)
{
cancellationToken.ThrowIfCancellationRequested();
var method1 = semanticModel1.GetSymbolInfo(node1, cancellationToken).Symbol as IMethodSymbol;
var method2 = semanticModel2.GetSymbolInfo(node2, cancellationToken).Symbol as IMethodSymbol;
if (method1 is null != method2 is null)
return true;
if (method1 is not null && !method1.Equals(method2, SymbolEqualityComparer.IncludeNullability))
return true;
node1 = node1.Parent;
node2 = node2.Parent;
}
return false;
}
private static bool IsIdentityOrImplicitConversion(Compilation compilation, ITypeSymbol type1, ITypeSymbol type2)
{
// Dynamic can have an identity conversion between types. But it can have a very different effect on the
// generated code. Do not allow the change if these are not in agreement.
if (type1 is IDynamicTypeSymbol != type2 is IDynamicTypeSymbol)
return false;
var conversion = compilation.ClassifyConversion(type1, type2);
if (!conversion.IsIdentityOrImplicitReference())
return false;
// When the types are the same except for nullability annotations, we need to check
// if they're truly compatible. This prevents suggesting conversions that would introduce
// nullability warnings, such as converting Task<string> to Task<string?> on an invariant type.
// For types that differ (e.g., string vs object), we allow the conversion as that's proper
// covariance/contravariance and won't produce nullability warnings.
return !type1.Equals(type2, SymbolEqualityComparer.Default) ||
type1.Equals(type2, SymbolEqualityComparer.IncludeNullability);
}
private static bool MayHaveSideEffects(ExpressionSyntax expression)
{
// Checks to see if the expression being invoked looks side-effect free. If so, changing from executing
// each time in the lambda to only executing it once could have impact on the program.
return !expression.DescendantNodesAndSelf().All(
n => n is TypeSyntax or
TypeArgumentListSyntax or
MemberAccessExpressionSyntax or
InstanceExpressionSyntax or
LiteralExpressionSyntax);
}
private static SeparatedSyntaxList<ParameterSyntax> GetParameters(AnonymousFunctionExpressionSyntax expression)
=> expression switch
{
AnonymousMethodExpressionSyntax anonymousMethod => anonymousMethod.ParameterList?.Parameters ?? default,
SimpleLambdaExpressionSyntax simpleLambda => [simpleLambda.Parameter],
ParenthesizedLambdaExpressionSyntax parenthesizedLambda => parenthesizedLambda.ParameterList.Parameters,
_ => throw ExceptionUtilities.UnexpectedValue(expression.Kind()),
};
public static bool TryGetAnonymousFunctionInvocation(
AnonymousFunctionExpressionSyntax anonymousFunction,
[NotNullWhen(true)] out InvocationExpressionSyntax? invocation,
out bool wasAwaited)
{
if (anonymousFunction.ExpressionBody != null)
return TryGetInvocation(anonymousFunction.ExpressionBody, out invocation, out wasAwaited);
if (anonymousFunction.Block is { Statements.Count: 1 })
{
var statement = anonymousFunction.Block.Statements[0];
if (statement is ReturnStatementSyntax { Expression: { } expression })
return TryGetInvocation(expression, out invocation, out wasAwaited);
if (statement is ExpressionStatementSyntax expressionStatement)
return TryGetInvocation(expressionStatement.Expression, out invocation, out wasAwaited);
}
invocation = null;
wasAwaited = false;
return false;
}
private static bool TryGetInvocation(
ExpressionSyntax expression,
[NotNullWhen(true)] out InvocationExpressionSyntax? invocation,
out bool wasAwaited)
{
wasAwaited = false;
// if we have either `await Goo()` or `await Goo().ConfigureAwait` then unwrap to get at `Goo()`.
if (expression is AwaitExpressionSyntax awaitExpression)
{
wasAwaited = true;
expression = awaitExpression.Expression;
if (expression is InvocationExpressionSyntax
{
Expression: MemberAccessExpressionSyntax { Name.Identifier.ValueText: nameof(Task.ConfigureAwait), Expression: var underlying }
})
{
expression = underlying;
}
}
invocation = expression as InvocationExpressionSyntax;
return invocation != null;
}
}
|