Skip to content
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

Suggest inlining of simple lambda expressions #800

Draft
wants to merge 2 commits into
base: main
Choose a base branch
from
Draft
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
240 changes: 240 additions & 0 deletions Funcky.Analyzers/Funcky.Analyzers.Test/SimpleLambdaExpressionsTest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,240 @@
using Microsoft.CodeAnalysis;
using Xunit;
using VerifyCS = Funcky.Analyzers.Test.CSharpAnalyzerVerifier<Funcky.Analyzers.SimpleLambdaExpressionsAnalyzer>;

namespace Funcky.Analyzers.Test;

// TODO: Code Fix: Add cast for null literal
public sealed class SimpleLambdaExpressionsTest
{
private const string FuncWithAttributeCode =
"""
public static class F<T>
{
public static void TakesFunc(T value) { }
public static void TakesFunc(System.Func<T> func) { }
public static void TakesFunc(System.Func<T, T> func) { }
}
""";

[Fact]
public async Task Literal()
{
const string inputCode =
"""
public static class C
{
public static void M()
{
F<int>.TakesFunc(() => 10);
}
}
""";
await VerifyCS.VerifyAnalyzerAsync(inputCode + Environment.NewLine + FuncWithAttributeCode, VerifyCS.Diagnostic().WithSpan(5, 19, 5, 27));
}

[Fact]
public async Task Variable()
{
const string inputCode =
"""
public static class C
{
public static void M()
{
var variable = 10;
F<int>.TakesFunc(() => variable);
}
}
""";
await VerifyCS.VerifyAnalyzerAsync(inputCode + Environment.NewLine + FuncWithAttributeCode, VerifyCS.Diagnostic().WithSpan(6, 19, 6, 33));
}

[Fact]
public async Task ConstantValue()
{
const string inputCode =
"""
public static class C
{
public const int MemberConstant = 10;

public static void M()
{
const int localConstant = 10;
F<int>.TakesFunc(() => localConstant);
F<int>.TakesFunc(() => MemberConstant);
F<int>.TakesFunc(() => localConstant + 1);
}
}
""";
await VerifyCS.VerifyAnalyzerAsync(
inputCode + Environment.NewLine + FuncWithAttributeCode,
VerifyCS.Diagnostic().WithSpan(8, 19, 8, 38),
VerifyCS.Diagnostic().WithSpan(9, 19, 9, 39),
VerifyCS.Diagnostic().WithSpan(10, 19, 10, 42));
}

[Fact]
public async Task Parameter()
{
const string inputCode =
"""
public static class C
{
public static void M(int parameter)
{
F<int>.TakesFunc(() => parameter);
}
}
""";
await VerifyCS.VerifyAnalyzerAsync(inputCode + Environment.NewLine + FuncWithAttributeCode, VerifyCS.Diagnostic().WithSpan(5, 19, 5, 34));
}

[Fact]
public async Task FuncWithParameter()
{
const string inputCode =
"""
public static class C
{
public static void M(int parameter)
{
F<int>.TakesFunc(_ => parameter);
F<int>.TakesFunc(lambdaParameter => lambdaParameter);
}
}
""";
await VerifyCS.VerifyAnalyzerAsync(inputCode + Environment.NewLine + FuncWithAttributeCode, VerifyCS.Diagnostic().WithSpan(5, 19, 5, 39));
}

[Fact]
public async Task Cast()
{
const string inputCode =
"""
public static class C
{
public static void M(int parameter)
{
F<object>.TakesFunc(() => (object)parameter);
}
}
""";
await VerifyCS.VerifyAnalyzerAsync(inputCode + Environment.NewLine + FuncWithAttributeCode, VerifyCS.Diagnostic().WithSpan(5, 19, 5, 42));
}

[Fact]
public async Task ObjectCreation()
{
const string inputCode =
"""
public static class C
{
public static void M(int parameter)
{
F<object>.TakesFunc(() => new object());
}
}
""";
await VerifyCS.VerifyAnalyzerAsync(inputCode + Environment.NewLine + FuncWithAttributeCode, VerifyCS.Diagnostic().WithSeverity(DiagnosticSeverity.Info).WithSpan(5, 19, 5, 37));
}

[Fact]
public async Task AnonymousObjectCreation()
{
const string inputCode =
"""
public static class C
{
public static void M(int parameter)
{
F<object>.TakesFunc(() => new { X = 10 });
F<object>.TakesFunc(() => new { X = 10, Y = "foo" });
F<object>.TakesFunc(() => new { });
F<object>.TakesFunc(() => new { X = new object() });
F<object>.TakesFunc(() => new { X = 10, Y = GetBar() });
}

public static int GetBar() { return 0; }
}
""";

await VerifyCS.VerifyAnalyzerAsync(
inputCode + Environment.NewLine + FuncWithAttributeCode,
VerifyCS.Diagnostic().WithSpan(5, 19, 5, 39),
VerifyCS.Diagnostic().WithSpan(6, 19, 6, 50),
VerifyCS.Diagnostic().WithSpan(7, 19, 7, 32),
VerifyCS.Diagnostic().WithSeverity(DiagnosticSeverity.Info).WithSpan(8, 19, 8, 49));
}

[Fact]
public async Task ObjectCreationCounterExample()
{
const string inputCode =
"""
public static class C
{
public static void M(int parameter)
{
F<Foo>.TakesFunc(() => new Foo(GetBar()));
F<Foo>.TakesFunc(() => new Foo(0) { Bar = GetBar() });
}

public static int GetBar() { return 0; }

public sealed class Foo
{
public Foo() { }

public Foo(int bar) { }

public int Bar { get; set; }
}
}
""";
await VerifyCS.VerifyAnalyzerAsync(inputCode + Environment.NewLine + FuncWithAttributeCode);
}

[Fact]
public async Task StaticProperty()
{
const string inputCode =
"""
public static partial class C
{
public static void M(int parameter)
{
F<int>.TakesFunc(() => Foo.Value);
}

public static class Foo
{
public static int Value { get; }
}
}
""";
await VerifyCS.VerifyAnalyzerAsync(inputCode + Environment.NewLine + FuncWithAttributeCode, VerifyCS.Diagnostic().WithSeverity(DiagnosticSeverity.Info).WithSpan(5, 19, 5, 34));
}

[Fact]
public async Task StaticField()
{
const string inputCode =
"""
public static partial class C
{
public static void M(int parameter)
{
F<int>.TakesFunc(() => Foo.Value);
}

public static class Foo
{
public static readonly int Value;
}
}
""";
await VerifyCS.VerifyAnalyzerAsync(inputCode + Environment.NewLine + FuncWithAttributeCode, VerifyCS.Diagnostic().WithSpan(5, 19, 5, 34));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
using System.Collections.Immutable;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.Operations;

namespace Funcky.Analyzers;

[DiagnosticAnalyzer(LanguageNames.CSharp)]
public sealed class SimpleLambdaExpressionsAnalyzer : DiagnosticAnalyzer
{
public const string DiagnosticId = $"{DiagnosticName.Prefix}{DiagnosticName.Usage}05";

private const string AttributeFullName = "Funcky.CodeAnalysis.InlineSimpleLambdaExpressionsAttribute";

private static readonly DiagnosticDescriptor Descriptor = new DiagnosticDescriptor(
id: DiagnosticId,

Check failure on line 16 in Funcky.Analyzers/Funcky.Analyzers/SimpleLambdaExpressionsAnalyzer.cs

View workflow job for this annotation

GitHub Actions / Trimming and AOT Test

Diagnostic Id 'λ1005' is already used by analyzer 'AlternativeMonadAnalyzer'. Please use a different diagnostic ID.

Check warning on line 16 in Funcky.Analyzers/Funcky.Analyzers/SimpleLambdaExpressionsAnalyzer.cs

View workflow job for this annotation

GitHub Actions / Generate NuGet Packages

Diagnostic Id 'λ1005' is already used by analyzer 'AlternativeMonadAnalyzer'. Please use a different diagnostic ID.

Check failure on line 16 in Funcky.Analyzers/Funcky.Analyzers/SimpleLambdaExpressionsAnalyzer.cs

View workflow job for this annotation

GitHub Actions / Build and Test (ubuntu-latest, 5.0.0)

Diagnostic Id 'λ1005' is already used by analyzer 'AlternativeMonadAnalyzer'. Please use a different diagnostic ID.

Check failure on line 16 in Funcky.Analyzers/Funcky.Analyzers/SimpleLambdaExpressionsAnalyzer.cs

View workflow job for this annotation

GitHub Actions / Build and Test (ubuntu-latest, 5.0.0)

Diagnostic Id 'λ1005' is already used by analyzer 'AlternativeMonadAnalyzer'. Please use a different diagnostic ID.

Check failure on line 16 in Funcky.Analyzers/Funcky.Analyzers/SimpleLambdaExpressionsAnalyzer.cs

View workflow job for this annotation

GitHub Actions / Build and Test (ubuntu-latest, 6.0.1)

Diagnostic Id 'λ1005' is already used by analyzer 'AlternativeMonadAnalyzer'. Please use a different diagnostic ID.

Check failure on line 16 in Funcky.Analyzers/Funcky.Analyzers/SimpleLambdaExpressionsAnalyzer.cs

View workflow job for this annotation

GitHub Actions / Build and Test (ubuntu-latest, 6.0.1)

Diagnostic Id 'λ1005' is already used by analyzer 'AlternativeMonadAnalyzer'. Please use a different diagnostic ID.
title: "Simple lambda expression can be inlined",
messageFormat: "Simple lambda expression can be inlined",
description: "TODO.",
category: nameof(Funcky),
defaultSeverity: DiagnosticSeverity.Warning,
isEnabledByDefault: true);

public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => ImmutableArray.Create(Descriptor);

public override void Initialize(AnalysisContext context)
{
context.EnableConcurrentExecution();
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
context.RegisterCompilationStartAction(OnCompilationStarted);
}

private static void OnCompilationStarted(CompilationStartAnalysisContext context)
{
context.RegisterOperationAction(AnalyzeArgument, OperationKind.Argument);
}

private static void AnalyzeArgument(OperationAnalysisContext context)
{
var operation = (IArgumentOperation)context.Operation;
if (operation.Parameter is { } parameter
&& operation.Value is IDelegateCreationOperation { Target: IAnonymousFunctionOperation lambda }
&& parameter.ContainingSymbol is IMethodSymbol { ContainingType: var containingType } method
&& MatchBlockOperationWithSingleReturn(lambda.Body) is { } returnedValue
&& containingType.GetMembers().OfType<IMethodSymbol>().Any(m => m.Name == method.Name
&& SymbolEqualityComparer.IncludeNullability.Equals(m.ReturnType, method.ReturnType)
&& m.Parameters.Length == 1
&& SymbolEqualityComparer.IncludeNullability.Equals(m.Parameters[0].Type, returnedValue.Type)))
{
context.ReportDiagnostic(Diagnostic.Create(Descriptor, lambda.Syntax.GetLocation()));

// var kind = DetectSimpleOperation(returnedValue, lambda);
//
// switch (kind)
// {
// case SimpleOperationKind.Certain:
// context.ReportDiagnostic(Diagnostic.Create(Descriptor, lambda.Syntax.GetLocation()));
// break;
// case SimpleOperationKind.Maybe:
// context.ReportDiagnostic(Diagnostic.Create(Descriptor, lambda.Syntax.GetLocation(), DiagnosticSeverity.Info, null, null));
// break;
// }
}
}

private static IOperation? MatchBlockOperationWithSingleReturn(IOperation operation)
=> operation is IBlockOperation blockOperation
&& blockOperation.Operations.Length == 1
&& blockOperation.Operations[0] is IReturnOperation @return
? @return.ReturnedValue
: null;

private static SimpleOperationKind DetectSimpleOperation(IOperation operation, IAnonymousFunctionOperation lambda)
=> operation switch
{
_ when operation.ConstantValue.HasValue => SimpleOperationKind.Certain,
IParameterReferenceOperation parameterReference when !SymbolEqualityComparer.Default.Equals(parameterReference.Parameter.ContainingSymbol, lambda.Symbol) => SimpleOperationKind.Certain,
ILocalReferenceOperation or ILiteralOperation => SimpleOperationKind.Certain,
IConversionOperation conversion => DetectSimpleOperation(conversion.Operand, lambda),
IObjectCreationOperation objectCreation when objectCreation.Children.Any() => Min(objectCreation.Children.Min(c => DetectSimpleOperation(c, lambda)), SimpleOperationKind.Maybe),
IObjectCreationOperation => SimpleOperationKind.Maybe,
IAnonymousObjectCreationOperation creation when creation.Initializers.Any() => creation.Initializers.Cast<ISimpleAssignmentOperation>().Select(x => x.Value).Min(c => DetectSimpleOperation(c, lambda)),
IAnonymousObjectCreationOperation => SimpleOperationKind.Certain,
IFieldReferenceOperation fieldReference when fieldReference.Field.IsStatic => SimpleOperationKind.Certain,
IPropertyReferenceOperation propertyReference when propertyReference.Property.IsStatic => SimpleOperationKind.Maybe,
_ => SimpleOperationKind.None,
};

private static SimpleOperationKind Min(SimpleOperationKind lhs, SimpleOperationKind rhs) => lhs < rhs ? lhs : rhs;

#pragma warning disable SA1201
private enum SimpleOperationKind
#pragma warning restore SA1201
{
None,
Maybe,
Certain,
}
}
7 changes: 7 additions & 0 deletions Funcky/CodeAnalysis/InlineSimpleLambdaExpressionsAttribute.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
namespace Funcky.CodeAnalysis;

/// <summary>The analyzer will suggest inlining lambda expressions passed to this method when the lambda is «simple».</summary>
[AttributeUsage(AttributeTargets.Parameter)]
internal sealed class InlineSimpleLambdaExpressionsAttribute : Attribute
{
}
Loading