diff --git a/Funcky.Analyzers/Funcky.Analyzers.Test/SimpleLambdaExpressionsTest.cs b/Funcky.Analyzers/Funcky.Analyzers.Test/SimpleLambdaExpressionsTest.cs new file mode 100644 index 00000000..ecc27e06 --- /dev/null +++ b/Funcky.Analyzers/Funcky.Analyzers.Test/SimpleLambdaExpressionsTest.cs @@ -0,0 +1,240 @@ +using Microsoft.CodeAnalysis; +using Xunit; +using VerifyCS = Funcky.Analyzers.Test.CSharpAnalyzerVerifier; + +namespace Funcky.Analyzers.Test; + +// TODO: Code Fix: Add cast for null literal +public sealed class SimpleLambdaExpressionsTest +{ + private const string FuncWithAttributeCode = + """ + public static class F + { + public static void TakesFunc(T value) { } + public static void TakesFunc(System.Func func) { } + public static void TakesFunc(System.Func func) { } + } + """; + + [Fact] + public async Task Literal() + { + const string inputCode = + """ + public static class C + { + public static void M() + { + F.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.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.TakesFunc(() => localConstant); + F.TakesFunc(() => MemberConstant); + F.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.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.TakesFunc(_ => parameter); + F.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.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.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.TakesFunc(() => new { X = 10 }); + F.TakesFunc(() => new { X = 10, Y = "foo" }); + F.TakesFunc(() => new { }); + F.TakesFunc(() => new { X = new object() }); + F.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.TakesFunc(() => new Foo(GetBar())); + F.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.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.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)); + } +} diff --git a/Funcky.Analyzers/Funcky.Analyzers/SimpleLambdaExpressionsAnalyzer.cs b/Funcky.Analyzers/Funcky.Analyzers/SimpleLambdaExpressionsAnalyzer.cs new file mode 100644 index 00000000..ee5e4e8b --- /dev/null +++ b/Funcky.Analyzers/Funcky.Analyzers/SimpleLambdaExpressionsAnalyzer.cs @@ -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, + 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 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().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().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, + } +} diff --git a/Funcky/CodeAnalysis/InlineSimpleLambdaExpressionsAttribute.cs b/Funcky/CodeAnalysis/InlineSimpleLambdaExpressionsAttribute.cs new file mode 100644 index 00000000..d58471bb --- /dev/null +++ b/Funcky/CodeAnalysis/InlineSimpleLambdaExpressionsAttribute.cs @@ -0,0 +1,7 @@ +namespace Funcky.CodeAnalysis; + +/// The analyzer will suggest inlining lambda expressions passed to this method when the lambda is «simple». +[AttributeUsage(AttributeTargets.Parameter)] +internal sealed class InlineSimpleLambdaExpressionsAttribute : Attribute +{ +}