Skip to content

Commit

Permalink
Add feature to convert from an explicitly typed lambda to an implicit…
Browse files Browse the repository at this point in the history
…ly typed one. (#76770)
  • Loading branch information
CyrusNajmabadi authored Jan 16, 2025
2 parents 8c05cc8 + e5d47df commit 4edc471
Show file tree
Hide file tree
Showing 46 changed files with 944 additions and 13 deletions.
1 change: 1 addition & 0 deletions src/Analyzers/CSharp/Analyzers/CSharpAnalyzers.projitems
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,7 @@
<Compile Include="$(MSBuildThisFileDirectory)RemoveUnusedMembers\CSharpRemoveUnusedMembersDiagnosticAnalyzer.cs" />
<Compile Include="$(MSBuildThisFileDirectory)RemoveUnusedParametersAndValues\CSharpRemoveUnusedParametersAndValuesDiagnosticAnalyzer.cs" />
<Compile Include="$(MSBuildThisFileDirectory)UseCollectionInitializer\CSharpUseCollectionInitializerDiagnosticAnalyzer.cs" />
<Compile Include="$(MSBuildThisFileDirectory)UseImplicitlyTypedLambdaExpression\CSharpUseImplicitlyTypedLambdaExpressionDiagnosticAnalyzer.cs" />
<Compile Include="$(MSBuildThisFileDirectory)UseImplicitObjectCreation\CSharpUseImplicitObjectCreationDiagnosticAnalyzer.cs" />
<Compile Include="$(MSBuildThisFileDirectory)UseImplicitOrExplicitType\CSharpTypeStyleDiagnosticAnalyzerBase.cs" />
<Compile Include="$(MSBuildThisFileDirectory)UseImplicitOrExplicitType\CSharpUseExplicitTypeDiagnosticAnalyzer.cs" />
Expand Down
6 changes: 6 additions & 0 deletions src/Analyzers/CSharp/Analyzers/CSharpAnalyzersResources.resx
Original file line number Diff line number Diff line change
Expand Up @@ -305,6 +305,12 @@
<data name="new_expression_can_be_simplified" xml:space="preserve">
<value>'new' expression can be simplified</value>
</data>
<data name="Lambda_expression_can_be_simplified" xml:space="preserve">
<value>Lambda expression can be simplified</value>
</data>
<data name="Use_implicitly_typed_lambda" xml:space="preserve">
<value>Use implicitly typed lambda</value>
</data>
<data name="Discard_can_be_removed" xml:space="preserve">
<value>Discard can be removed</value>
</data>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ internal CSharpSimplifierOptions GetSimplifierOptions()
public CodeStyleOption2<bool> PreferPatternMatchingOverIsWithCastCheck => GetOption(CSharpCodeStyleOptions.PreferPatternMatchingOverIsWithCastCheck);
public CodeStyleOption2<bool> PreferNotPattern => GetOption(CSharpCodeStyleOptions.PreferNotPattern);
public CodeStyleOption2<bool> PreferExtendedPropertyPattern => GetOption(CSharpCodeStyleOptions.PreferExtendedPropertyPattern);
public CodeStyleOption2<bool> PreferImplicitlyTypedLambdaExpression => GetOption(CSharpCodeStyleOptions.PreferImplicitlyTypedLambdaExpression);
public CodeStyleOption2<bool> PreferInlinedVariableDeclaration => GetOption(CSharpCodeStyleOptions.PreferInlinedVariableDeclaration);
public CodeStyleOption2<bool> PreferDeconstructedVariableDeclaration => GetOption(CSharpCodeStyleOptions.PreferDeconstructedVariableDeclaration);
public CodeStyleOption2<bool> PreferIndexOperator => GetOption(CSharpCodeStyleOptions.PreferIndexOperator);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
// 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;
using System.Linq;
using System.Threading;
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.Operations;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.Shared.Utilities;

namespace Microsoft.CodeAnalysis.CSharp.UseImplicitlyTypedLambdaExpression;

using static SyntaxFactory;

[DiagnosticAnalyzer(LanguageNames.CSharp)]
internal sealed class CSharpUseImplicitlyTypedLambdaExpressionDiagnosticAnalyzer()
: AbstractBuiltInCodeStyleDiagnosticAnalyzer(IDEDiagnosticIds.UseImplicitlyTypedLambdaExpressionDiagnosticId,
EnforceOnBuildValues.UseImplicitObjectCreation,
CSharpCodeStyleOptions.PreferImplicitlyTypedLambdaExpression,
new LocalizableResourceString(nameof(CSharpAnalyzersResources.Use_implicitly_typed_lambda), CSharpAnalyzersResources.ResourceManager, typeof(CSharpAnalyzersResources)),
new LocalizableResourceString(nameof(CSharpAnalyzersResources.Lambda_expression_can_be_simplified), CSharpAnalyzersResources.ResourceManager, typeof(CSharpAnalyzersResources)))
{
public override DiagnosticAnalyzerCategory GetAnalyzerCategory()
=> DiagnosticAnalyzerCategory.SemanticSpanAnalysis;

protected override void InitializeWorker(AnalysisContext context)
=> context.RegisterSyntaxNodeAction(AnalyzeIfEnabled,
SyntaxKind.ParenthesizedLambdaExpression);

private void AnalyzeIfEnabled(SyntaxNodeAnalysisContext context)
{
var cancellationToken = context.CancellationToken;
var analyzerOptions = context.Options;
var semanticModel = context.SemanticModel;
var option = analyzerOptions.GetCSharpAnalyzerOptions(semanticModel.SyntaxTree).PreferImplicitlyTypedLambdaExpression;
if (!option.Value || ShouldSkipAnalysis(context, option.Notification))
return;

var explicitLambda = (ParenthesizedLambdaExpressionSyntax)context.Node;
if (!Analyze(semanticModel, explicitLambda, cancellationToken))
return;

context.ReportDiagnostic(DiagnosticHelper.Create(
Descriptor,
explicitLambda.ParameterList.OpenParenToken.GetLocation(),
option.Notification,
context.Options,
[explicitLambda.GetLocation()],
properties: null));
}

public static bool Analyze(
SemanticModel semanticModel,
ParenthesizedLambdaExpressionSyntax explicitLambda,
CancellationToken cancellationToken)
{
// If the lambda has an explicit return type, then do not offer the feature. Explicit return types are used to
// provide full semantic information to the compiler so it does not need to perform speculative lambda binding.
// Removing may cause code compilation performance to regress.
if (explicitLambda.ReturnType != null)
return false;

// Needs to have at least one parameter, all parameters need to have a provided type, and no parameters can have a
// default value provided.
if (explicitLambda.ParameterList.Parameters.Count == 0 ||
explicitLambda.ParameterList.Parameters.Any(p => p.Type is null || p.Default != null))
{
return false;
}

// Prior to C# 14, implicitly typed lambdas can't have modifiers on parameters.
var languageVersion = semanticModel.Compilation.LanguageVersion();
if (!languageVersion.IsCSharp14OrAbove() && explicitLambda.ParameterList.Parameters.Any(p => p.Modifiers.Count > 0))
return false;

var implicitLambda = ConvertToImplicitlyTypedLambda(explicitLambda);

var analyzer = new SpeculationAnalyzer(
explicitLambda, implicitLambda, semanticModel, cancellationToken);
if (analyzer.ReplacementChangesSemantics())
return false;

if (semanticModel.GetSymbolInfo(explicitLambda, cancellationToken).Symbol is not IMethodSymbol explicitLambdaMethod ||
analyzer.SpeculativeSemanticModel.GetSymbolInfo(analyzer.ReplacedExpression, cancellationToken).Symbol is not IMethodSymbol implicitLambdaMethod)
{
return false;
}

if (!SignatureComparer.Instance.HaveSameSignature(explicitLambdaMethod, implicitLambdaMethod, caseSensitive: true))
return false;

return true;
}

public static LambdaExpressionSyntax ConvertToImplicitlyTypedLambda(ParenthesizedLambdaExpressionSyntax explicitLambda)
{
var implicitLambda = explicitLambda.ReplaceNodes(
explicitLambda.ParameterList.Parameters,
(parameter, _) => RemoveParamsModifier(
parameter.WithType(null)
.WithIdentifier(parameter.Identifier.WithPrependedLeadingTrivia(parameter.Type!.GetLeadingTrivia()))));

// If the lambda only has one parameter, then convert it to the non-parenthesized form.
if (implicitLambda.ParameterList.Parameters is not ([{ AttributeLists.Count: 0, Modifiers.Count: 0 } parameter]))
return implicitLambda;

return SimpleLambdaExpression(
explicitLambda.AttributeLists,
explicitLambda.Modifiers,
parameter.WithTriviaFrom(explicitLambda.ParameterList),
explicitLambda.Block,
explicitLambda.ExpressionBody);
}

private static ParameterSyntax RemoveParamsModifier(ParameterSyntax parameter)
{
// Implicitly typed lambdas aren't ever allowed to have the 'params' modifier.
var paramsModifierIndex = parameter.Modifiers.IndexOf(SyntaxKind.ParamsKeyword);
return paramsModifierIndex >= 0 ? parameter.WithModifiers(parameter.Modifiers.RemoveAt(paramsModifierIndex)) : parameter;
}
}
10 changes: 10 additions & 0 deletions src/Analyzers/CSharp/Analyzers/xlf/CSharpAnalyzersResources.cs.xlf

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 10 additions & 0 deletions src/Analyzers/CSharp/Analyzers/xlf/CSharpAnalyzersResources.de.xlf

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 10 additions & 0 deletions src/Analyzers/CSharp/Analyzers/xlf/CSharpAnalyzersResources.es.xlf

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 10 additions & 0 deletions src/Analyzers/CSharp/Analyzers/xlf/CSharpAnalyzersResources.fr.xlf

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 10 additions & 0 deletions src/Analyzers/CSharp/Analyzers/xlf/CSharpAnalyzersResources.it.xlf

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 10 additions & 0 deletions src/Analyzers/CSharp/Analyzers/xlf/CSharpAnalyzersResources.ja.xlf

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 10 additions & 0 deletions src/Analyzers/CSharp/Analyzers/xlf/CSharpAnalyzersResources.ko.xlf

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 10 additions & 0 deletions src/Analyzers/CSharp/Analyzers/xlf/CSharpAnalyzersResources.pl.xlf

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 10 additions & 0 deletions src/Analyzers/CSharp/Analyzers/xlf/CSharpAnalyzersResources.ru.xlf

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading

0 comments on commit 4edc471

Please sign in to comment.