From e48253601ce514fbce1d1c68a5f181ed38bb8878 Mon Sep 17 00:00:00 2001 From: Tau <4602612+bash@users.noreply.github.com> Date: Sun, 24 Sep 2023 21:40:18 +0200 Subject: [PATCH 1/6] Do not report argument names warning inside expression trees --- .../UseWithArgumentNamesTest.cs | 34 ++++++++ .../FunckyWellKnownTypeNames.cs | 2 + .../Funcky.Analyzers/SyntaxNodeExtensions.cs | 78 +++++++++++++++++++ .../UseWithArgumentNamesAnalyzer.cs | 9 ++- 4 files changed, 120 insertions(+), 3 deletions(-) create mode 100644 Funcky.Analyzers/Funcky.Analyzers/SyntaxNodeExtensions.cs diff --git a/Funcky.Analyzers/Funcky.Analyzers.Test/UseWithArgumentNamesTest.cs b/Funcky.Analyzers/Funcky.Analyzers.Test/UseWithArgumentNamesTest.cs index 9ee8e70e..d1b0e117 100644 --- a/Funcky.Analyzers/Funcky.Analyzers.Test/UseWithArgumentNamesTest.cs +++ b/Funcky.Analyzers/Funcky.Analyzers.Test/UseWithArgumentNamesTest.cs @@ -5,6 +5,15 @@ namespace Funcky.Analyzers.Test; public sealed class UseWithArgumentNamesTest { + private const string AttributeSource = + """ + namespace Funcky.CodeAnalysis + { + [System.AttributeUsage(System.AttributeTargets.Method)] + internal sealed class UseWithArgumentNamesAttribute : System.Attribute { } + } + """; + [Fact] public async Task ArgumentsThatAlreadyUseArgumentNamesGetNoDiagnostic() { @@ -36,4 +45,29 @@ public async Task UsagesOfMethodsAnnotatedWithShouldUseNamedArgumentsAttributeGe await VerifyWithSourceExample.VerifyDiagnosticAndCodeFix(expectedDiagnostics, "UseWithArgumentNames"); } + + [Fact] + public async Task IgnoresCallsToMethodsInsideExpressionTrees() + { + var inputCode = + $$""" + using System; + using System.Linq.Expressions; + using Funcky.CodeAnalysis; + + class Test + { + private void Syntax() + { + Expression expr = () => Method(10, 20); + } + + [UseWithArgumentNames] + private void Method(int x, int y) { } + } + + {{AttributeSource}} + """; + await VerifyCS.VerifyAnalyzerAsync(inputCode); + } } diff --git a/Funcky.Analyzers/Funcky.Analyzers/FunckyWellKnownTypeNames.cs b/Funcky.Analyzers/Funcky.Analyzers/FunckyWellKnownTypeNames.cs index b97bc43c..351354e3 100644 --- a/Funcky.Analyzers/Funcky.Analyzers/FunckyWellKnownTypeNames.cs +++ b/Funcky.Analyzers/Funcky.Analyzers/FunckyWellKnownTypeNames.cs @@ -25,4 +25,6 @@ public static class FunckyWellKnownTypeNames public static INamedTypeSymbol? GetSequenceType(this Compilation compilation) => compilation.GetTypeByMetadataName("Funcky.Sequence"); public static INamedTypeSymbol? GetFunctionalType(this Compilation compilation) => compilation.GetTypeByMetadataName("Funcky.Functional"); + + public static INamedTypeSymbol? GetExpressionOfTType(this Compilation compilation) => compilation.GetTypeByMetadataName("System.Linq.Expressions.Expression`1"); } diff --git a/Funcky.Analyzers/Funcky.Analyzers/SyntaxNodeExtensions.cs b/Funcky.Analyzers/Funcky.Analyzers/SyntaxNodeExtensions.cs new file mode 100644 index 00000000..65a289ba --- /dev/null +++ b/Funcky.Analyzers/Funcky.Analyzers/SyntaxNodeExtensions.cs @@ -0,0 +1,78 @@ +using System.Collections.Immutable; +using System.Diagnostics.CodeAnalysis; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; + +namespace Funcky.Analyzers; + +internal static class SyntaxNodeExtensions +{ + // Copied from Roslyn's source code as this API is not public: + // https://github.com/dotnet/roslyn/blob/232f7afa4966411958759c880de3a1765bdb28a0/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/CSharp/Extensions/SyntaxNodeExtensions.cs#L925 + public static bool IsInExpressionTree( + [NotNullWhen(returnValue: true)] this SyntaxNode? node, + SemanticModel semanticModel, + [NotNullWhen(returnValue: true)] INamedTypeSymbol? expressionTypeOpt, + CancellationToken cancellationToken) + { + if (expressionTypeOpt != null) + { + for (var current = node; current != null; current = current.Parent) + { + if (current.IsAnyLambda()) + { + var typeInfo = semanticModel.GetTypeInfo(current, cancellationToken); + if (SymbolEqualityComparer.Default.Equals(expressionTypeOpt, typeInfo.ConvertedType?.OriginalDefinition)) + { + return true; + } + } + else if (current is SelectOrGroupClauseSyntax or OrderingSyntax) + { + var info = semanticModel.GetSymbolInfo(current, cancellationToken); + if (TakesExpressionTree(info, expressionTypeOpt)) + { + return true; + } + } + else if (current is QueryClauseSyntax queryClause) + { + var info = semanticModel.GetQueryClauseInfo(queryClause, cancellationToken); + if (TakesExpressionTree(info.CastInfo, expressionTypeOpt) || + TakesExpressionTree(info.OperationInfo, expressionTypeOpt)) + { + return true; + } + } + } + } + + return false; + + static bool TakesExpressionTree(SymbolInfo info, INamedTypeSymbol expressionType) + { + foreach (var symbol in GetAllSymbols(info)) + { + if (symbol is IMethodSymbol method && + method.Parameters.Length > 0 && + SymbolEqualityComparer.Default.Equals(expressionType, method.Parameters[0].Type?.OriginalDefinition)) + { + return true; + } + } + + return false; + } + } + + internal static ImmutableArray GetAllSymbols(SymbolInfo info) + => info.Symbol == null + ? info.CandidateSymbols + : ImmutableArray.Create(info.Symbol); + + // Copied from Roslyn's source code as this API is not public: + // https://github.com/dotnet/roslyn/blob/232f7afa4966411958759c880de3a1765bdb28a0/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/CSharp/Extensions/SyntaxNodeExtensions.cs#L925 + internal static bool IsAnyLambda([NotNullWhen(returnValue: true)] this SyntaxNode? node) + => node?.Kind() is SyntaxKind.ParenthesizedLambdaExpression or SyntaxKind.SimpleLambdaExpression; +} diff --git a/Funcky.Analyzers/Funcky.Analyzers/UseWithArgumentNamesAnalyzer.cs b/Funcky.Analyzers/Funcky.Analyzers/UseWithArgumentNamesAnalyzer.cs index c0cb7d16..6a8d75a8 100644 --- a/Funcky.Analyzers/Funcky.Analyzers/UseWithArgumentNamesAnalyzer.cs +++ b/Funcky.Analyzers/Funcky.Analyzers/UseWithArgumentNamesAnalyzer.cs @@ -33,17 +33,20 @@ public override void Initialize(AnalysisContext context) { if (context.Compilation.GetTypeByMetadataName(AttributeFullName) is { } attributeSymbol) { - context.RegisterOperationAction(AnalyzeInvocation(attributeSymbol), OperationKind.Invocation); + var expressionOfTType = context.Compilation.GetExpressionOfTType(); + context.RegisterOperationAction(AnalyzeInvocation(attributeSymbol, expressionOfTType), OperationKind.Invocation); } }); } - private static Action AnalyzeInvocation(INamedTypeSymbol attributeSymbol) + private static Action AnalyzeInvocation(INamedTypeSymbol attributeSymbol, INamedTypeSymbol? expressionOfTType) => context => { var invocation = (IInvocationOperation)context.Operation; + var semanticModel = invocation.SemanticModel ?? throw new InvalidOperationException("Semantic model is never be null for operations passed to an analyzer (according to docs)"); - if (invocation.TargetMethod.GetAttributes().Any(attribute => SymbolEqualityComparer.Default.Equals(attribute.AttributeClass, attributeSymbol))) + if (invocation.TargetMethod.GetAttributes().Any(attribute => SymbolEqualityComparer.Default.Equals(attribute.AttributeClass, attributeSymbol)) + && !invocation.Syntax.IsInExpressionTree(semanticModel, expressionOfTType, context.CancellationToken)) { foreach (var argument in invocation.Arguments) { From 3c62e154e7647efca923058a601b70e5388a351b Mon Sep 17 00:00:00 2001 From: Tau <4602612+bash@users.noreply.github.com> Date: Sun, 24 Sep 2023 23:11:14 +0200 Subject: [PATCH 2/6] Inline and simplify analyzer test files --- .../TestCode/UseWithArgumentNames.expected | 33 ------ .../TestCode/UseWithArgumentNames.input | 33 ------ .../TestCode/ValidUseWithArgumentNames.input | 23 ---- ...ValidUseWithArgumentNamesNoAttribute.input | 22 ---- .../UseWithArgumentNamesTest.cs | 112 +++++++++++++++--- 5 files changed, 95 insertions(+), 128 deletions(-) delete mode 100644 Funcky.Analyzers/Funcky.Analyzers.Test/TestCode/UseWithArgumentNames.expected delete mode 100644 Funcky.Analyzers/Funcky.Analyzers.Test/TestCode/UseWithArgumentNames.input delete mode 100644 Funcky.Analyzers/Funcky.Analyzers.Test/TestCode/ValidUseWithArgumentNames.input delete mode 100644 Funcky.Analyzers/Funcky.Analyzers.Test/TestCode/ValidUseWithArgumentNamesNoAttribute.input diff --git a/Funcky.Analyzers/Funcky.Analyzers.Test/TestCode/UseWithArgumentNames.expected b/Funcky.Analyzers/Funcky.Analyzers.Test/TestCode/UseWithArgumentNames.expected deleted file mode 100644 index 23e363ca..00000000 --- a/Funcky.Analyzers/Funcky.Analyzers.Test/TestCode/UseWithArgumentNames.expected +++ /dev/null @@ -1,33 +0,0 @@ -using System; -using System.Linq; -using Funcky.CodeAnalysis; - -namespace ConsoleApplication1 -{ - class Program - { - private void Syntax() - { - Method(x: 10, y: 20); - Method(x: 10, y: 20); - Method( - x: 10, y: 20); - Method( - x: 10, - y: 20); - MethodWithKeywordAsArgument(@int: 10); - } - - [UseWithArgumentNames] - private void Method(int x, int y) { } - - [UseWithArgumentNames] - private void MethodWithKeywordAsArgument(int @int) { } - } -} - -namespace Funcky.CodeAnalysis -{ - [AttributeUsage(AttributeTargets.Method)] - internal sealed class UseWithArgumentNamesAttribute : Attribute { } -} diff --git a/Funcky.Analyzers/Funcky.Analyzers.Test/TestCode/UseWithArgumentNames.input b/Funcky.Analyzers/Funcky.Analyzers.Test/TestCode/UseWithArgumentNames.input deleted file mode 100644 index c47c1b46..00000000 --- a/Funcky.Analyzers/Funcky.Analyzers.Test/TestCode/UseWithArgumentNames.input +++ /dev/null @@ -1,33 +0,0 @@ -using System; -using System.Linq; -using Funcky.CodeAnalysis; - -namespace ConsoleApplication1 -{ - class Program - { - private void Syntax() - { - Method(x: 10, 20); - Method(10, 20); - Method( - 10, 20); - Method( - 10, - 20); - MethodWithKeywordAsArgument(10); - } - - [UseWithArgumentNames] - private void Method(int x, int y) { } - - [UseWithArgumentNames] - private void MethodWithKeywordAsArgument(int @int) { } - } -} - -namespace Funcky.CodeAnalysis -{ - [AttributeUsage(AttributeTargets.Method)] - internal sealed class UseWithArgumentNamesAttribute : Attribute { } -} diff --git a/Funcky.Analyzers/Funcky.Analyzers.Test/TestCode/ValidUseWithArgumentNames.input b/Funcky.Analyzers/Funcky.Analyzers.Test/TestCode/ValidUseWithArgumentNames.input deleted file mode 100644 index b961cae6..00000000 --- a/Funcky.Analyzers/Funcky.Analyzers.Test/TestCode/ValidUseWithArgumentNames.input +++ /dev/null @@ -1,23 +0,0 @@ -using System; -using System.Linq; -using Funcky.CodeAnalysis; - -namespace ConsoleApplication1 -{ - class Program - { - private void Syntax() - { - Method(x: 10, y: 20); - } - - [UseWithArgumentNames] - private void Method(int x, int y) { } - } -} - -namespace Funcky.CodeAnalysis -{ - [AttributeUsage(AttributeTargets.Method)] - internal sealed class UseWithArgumentNamesAttribute : Attribute { } -} diff --git a/Funcky.Analyzers/Funcky.Analyzers.Test/TestCode/ValidUseWithArgumentNamesNoAttribute.input b/Funcky.Analyzers/Funcky.Analyzers.Test/TestCode/ValidUseWithArgumentNamesNoAttribute.input deleted file mode 100644 index 4642ebd4..00000000 --- a/Funcky.Analyzers/Funcky.Analyzers.Test/TestCode/ValidUseWithArgumentNamesNoAttribute.input +++ /dev/null @@ -1,22 +0,0 @@ -using System; -using System.Linq; -using Funcky.CodeAnalysis; - -namespace ConsoleApplication1 -{ - class Program - { - private void Syntax() - { - Method(10, 20); - } - - private void Method(int x, int y) { } - } -} - -namespace Funcky.CodeAnalysis -{ - [AttributeUsage(AttributeTargets.Method)] - internal sealed class UseWithArgumentNamesAttribute : Attribute { } -} diff --git a/Funcky.Analyzers/Funcky.Analyzers.Test/UseWithArgumentNamesTest.cs b/Funcky.Analyzers/Funcky.Analyzers.Test/UseWithArgumentNamesTest.cs index d1b0e117..60371b1c 100644 --- a/Funcky.Analyzers/Funcky.Analyzers.Test/UseWithArgumentNamesTest.cs +++ b/Funcky.Analyzers/Funcky.Analyzers.Test/UseWithArgumentNamesTest.cs @@ -17,40 +17,120 @@ internal sealed class UseWithArgumentNamesAttribute : System.Attribute { } [Fact] public async Task ArgumentsThatAlreadyUseArgumentNamesGetNoDiagnostic() { - var inputCode = await File.ReadAllTextAsync("TestCode/ValidUseWithArgumentNames.input"); - await VerifyCS.VerifyAnalyzerAsync(inputCode); + var inputCode = + """ + using Funcky.CodeAnalysis; + + class Test + { + private void Syntax() + { + Method(x: 10, y: 20); + } + + [UseWithArgumentNames] + private void Method(int x, int y) { } + } + """; + await VerifyCS.VerifyAnalyzerAsync(inputCode + AttributeSource); } [Fact] public async Task ArgumentsForCallsToMethodsWithoutAttributeGetNoDiagnostic() { - var inputCode = await File.ReadAllTextAsync("TestCode/ValidUseWithArgumentNamesNoAttribute.input"); - await VerifyCS.VerifyAnalyzerAsync(inputCode); + var inputCode = + """ + using Funcky.CodeAnalysis; + + class Test + { + private void Syntax() + { + Method(10, 20); + } + + private void Method(int x, int y) { } + } + """; + await VerifyCS.VerifyAnalyzerAsync(inputCode + AttributeSource); } [Fact] public async Task UsagesOfMethodsAnnotatedWithShouldUseNamedArgumentsAttributeGetWarningAndAreFixed() { + const string inputCode = + """ + using Funcky.CodeAnalysis; + + class Test + { + private void Syntax() + { + Method(x: 10, 20); + Method(10, 20); + Method( + 10, 20); + Method( + 10, + 20); + MethodWithKeywordAsArgument(10); + } + + [UseWithArgumentNames] + private void Method(int x, int y) { } + + [UseWithArgumentNames] + private void MethodWithKeywordAsArgument(int @int) { } + } + """; + + const string fixedCode = + """ + using Funcky.CodeAnalysis; + + class Test + { + private void Syntax() + { + Method(x: 10, y: 20); + Method(x: 10, y: 20); + Method( + x: 10, y: 20); + Method( + x: 10, + y: 20); + MethodWithKeywordAsArgument(@int: 10); + } + + [UseWithArgumentNames] + private void Method(int x, int y) { } + + [UseWithArgumentNames] + private void MethodWithKeywordAsArgument(int @int) { } + } + """; + var expectedDiagnostics = new[] { - VerifyCS.Diagnostic().WithSpan(11, 27, 11, 29).WithArguments("y"), - VerifyCS.Diagnostic().WithSpan(12, 20, 12, 22).WithArguments("x"), - VerifyCS.Diagnostic().WithSpan(12, 24, 12, 26).WithArguments("y"), - VerifyCS.Diagnostic().WithSpan(14, 17, 14, 19).WithArguments("x"), - VerifyCS.Diagnostic().WithSpan(14, 21, 14, 23).WithArguments("y"), - VerifyCS.Diagnostic().WithSpan(16, 17, 16, 19).WithArguments("x"), - VerifyCS.Diagnostic().WithSpan(17, 17, 17, 19).WithArguments("y"), - VerifyCS.Diagnostic().WithSpan(18, 41, 18, 43).WithArguments("int"), + VerifyCS.Diagnostic().WithSpan(7, 23, 7, 25).WithArguments("y"), + VerifyCS.Diagnostic().WithSpan(8, 16, 8, 18).WithArguments("x"), + VerifyCS.Diagnostic().WithSpan(8, 20, 8, 22).WithArguments("y"), + VerifyCS.Diagnostic().WithSpan(10, 13, 10, 15).WithArguments("x"), + VerifyCS.Diagnostic().WithSpan(10, 17, 10, 19).WithArguments("y"), + VerifyCS.Diagnostic().WithSpan(12, 13, 12, 15).WithArguments("x"), + VerifyCS.Diagnostic().WithSpan(13, 13, 13, 15).WithArguments("y"), + VerifyCS.Diagnostic().WithSpan(14, 37, 14, 39).WithArguments("int"), }; - await VerifyWithSourceExample.VerifyDiagnosticAndCodeFix(expectedDiagnostics, "UseWithArgumentNames"); + await VerifyCS.VerifyAnalyzerAsync(inputCode + AttributeSource, expectedDiagnostics); + await VerifyCS.VerifyCodeFixAsync(inputCode + AttributeSource, expectedDiagnostics, fixedCode + AttributeSource); } [Fact] public async Task IgnoresCallsToMethodsInsideExpressionTrees() { var inputCode = - $$""" + """ using System; using System.Linq.Expressions; using Funcky.CodeAnalysis; @@ -65,9 +145,7 @@ private void Syntax() [UseWithArgumentNames] private void Method(int x, int y) { } } - - {{AttributeSource}} """; - await VerifyCS.VerifyAnalyzerAsync(inputCode); + await VerifyCS.VerifyAnalyzerAsync(inputCode + AttributeSource); } } From e6097efb7042321f21c76b85c1dcd7e3edb6fb91 Mon Sep 17 00:00:00 2001 From: Tau <4602612+bash@users.noreply.github.com> Date: Mon, 25 Sep 2023 11:47:13 +0200 Subject: [PATCH 3/6] Refactor IsInExpressionTree function to use LINQ --- .../Funcky.Analyzers/SyntaxNodeExtensions.cs | 64 +++++++++---------- 1 file changed, 31 insertions(+), 33 deletions(-) diff --git a/Funcky.Analyzers/Funcky.Analyzers/SyntaxNodeExtensions.cs b/Funcky.Analyzers/Funcky.Analyzers/SyntaxNodeExtensions.cs index 65a289ba..f5d1a2e8 100644 --- a/Funcky.Analyzers/Funcky.Analyzers/SyntaxNodeExtensions.cs +++ b/Funcky.Analyzers/Funcky.Analyzers/SyntaxNodeExtensions.cs @@ -16,47 +16,37 @@ public static bool IsInExpressionTree( [NotNullWhen(returnValue: true)] INamedTypeSymbol? expressionTypeOpt, CancellationToken cancellationToken) { - if (expressionTypeOpt != null) + return expressionTypeOpt is not null + && node is not null + && node.AncestorsAndSelf().Any(current => IsExpressionTree(current, expressionTypeOpt, semanticModel, cancellationToken)); + + static bool IsExpressionTree( + SyntaxNode current, + INamedTypeSymbol expressionType, + SemanticModel semanticModel, + CancellationToken cancellationToken) { - for (var current = node; current != null; current = current.Parent) + if (current.IsAnyLambda()) { - if (current.IsAnyLambda()) - { - var typeInfo = semanticModel.GetTypeInfo(current, cancellationToken); - if (SymbolEqualityComparer.Default.Equals(expressionTypeOpt, typeInfo.ConvertedType?.OriginalDefinition)) - { - return true; - } - } - else if (current is SelectOrGroupClauseSyntax or OrderingSyntax) + var typeInfo = semanticModel.GetTypeInfo(current, cancellationToken); + if (SymbolEqualityComparer.Default.Equals(expressionType, typeInfo.ConvertedType?.OriginalDefinition)) { - var info = semanticModel.GetSymbolInfo(current, cancellationToken); - if (TakesExpressionTree(info, expressionTypeOpt)) - { - return true; - } + return true; } - else if (current is QueryClauseSyntax queryClause) + } + else if (current is SelectOrGroupClauseSyntax or OrderingSyntax) + { + var info = semanticModel.GetSymbolInfo(current, cancellationToken); + if (TakesExpressionTree(info, expressionType)) { - var info = semanticModel.GetQueryClauseInfo(queryClause, cancellationToken); - if (TakesExpressionTree(info.CastInfo, expressionTypeOpt) || - TakesExpressionTree(info.OperationInfo, expressionTypeOpt)) - { - return true; - } + return true; } } - } - - return false; - - static bool TakesExpressionTree(SymbolInfo info, INamedTypeSymbol expressionType) - { - foreach (var symbol in GetAllSymbols(info)) + else if (current is QueryClauseSyntax queryClause) { - if (symbol is IMethodSymbol method && - method.Parameters.Length > 0 && - SymbolEqualityComparer.Default.Equals(expressionType, method.Parameters[0].Type?.OriginalDefinition)) + var info = semanticModel.GetQueryClauseInfo(queryClause, cancellationToken); + if (TakesExpressionTree(info.CastInfo, expressionType) || + TakesExpressionTree(info.OperationInfo, expressionType)) { return true; } @@ -64,6 +54,14 @@ static bool TakesExpressionTree(SymbolInfo info, INamedTypeSymbol expressionType return false; } + + static bool TakesExpressionTree(SymbolInfo info, INamedTypeSymbol expressionType) + => GetAllSymbols(info).Any(symbol => TakesExpressionTreeAsFirstArgument(symbol, expressionType)); + + static bool TakesExpressionTreeAsFirstArgument(ISymbol symbol, INamedTypeSymbol expressionType) + => symbol is IMethodSymbol method + && method.Parameters.Length > 0 + && SymbolEqualityComparer.Default.Equals(expressionType, method.Parameters[0].Type?.OriginalDefinition); } internal static ImmutableArray GetAllSymbols(SymbolInfo info) From 0e9a7161638031759912ff420e78b547c86366dc Mon Sep 17 00:00:00 2001 From: Tau <4602612+bash@users.noreply.github.com> Date: Mon, 25 Sep 2023 15:45:06 +0200 Subject: [PATCH 4/6] Refactor if / elseif chain into switch expression --- .../Funcky.Analyzers/SyntaxNodeExtensions.cs | 36 +++++++++---------- 1 file changed, 17 insertions(+), 19 deletions(-) diff --git a/Funcky.Analyzers/Funcky.Analyzers/SyntaxNodeExtensions.cs b/Funcky.Analyzers/Funcky.Analyzers/SyntaxNodeExtensions.cs index f5d1a2e8..7e9874da 100644 --- a/Funcky.Analyzers/Funcky.Analyzers/SyntaxNodeExtensions.cs +++ b/Funcky.Analyzers/Funcky.Analyzers/SyntaxNodeExtensions.cs @@ -8,7 +8,7 @@ namespace Funcky.Analyzers; internal static class SyntaxNodeExtensions { - // Copied from Roslyn's source code as this API is not public: + // Adapted from Roslyn's source code as this API is not public: // https://github.com/dotnet/roslyn/blob/232f7afa4966411958759c880de3a1765bdb28a0/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/CSharp/Extensions/SyntaxNodeExtensions.cs#L925 public static bool IsInExpressionTree( [NotNullWhen(returnValue: true)] this SyntaxNode? node, @@ -26,33 +26,31 @@ static bool IsExpressionTree( SemanticModel semanticModel, CancellationToken cancellationToken) { - if (current.IsAnyLambda()) + return current switch { + _ when current.IsAnyLambda() => LambdaIsExpressionTree(), + SelectOrGroupClauseSyntax or OrderingSyntax => QueryExpressionIsExpressionTree(), + QueryClauseSyntax queryClause => QueryClauseIsExpressionTree(queryClause), + _ => false, + }; + + bool LambdaIsExpressionTree() { var typeInfo = semanticModel.GetTypeInfo(current, cancellationToken); - if (SymbolEqualityComparer.Default.Equals(expressionType, typeInfo.ConvertedType?.OriginalDefinition)) - { - return true; - } + return SymbolEqualityComparer.Default.Equals(expressionType, typeInfo.ConvertedType?.OriginalDefinition); } - else if (current is SelectOrGroupClauseSyntax or OrderingSyntax) + + bool QueryExpressionIsExpressionTree() { var info = semanticModel.GetSymbolInfo(current, cancellationToken); - if (TakesExpressionTree(info, expressionType)) - { - return true; - } + return TakesExpressionTree(info, expressionType); } - else if (current is QueryClauseSyntax queryClause) + + bool QueryClauseIsExpressionTree(QueryClauseSyntax queryClause) { var info = semanticModel.GetQueryClauseInfo(queryClause, cancellationToken); - if (TakesExpressionTree(info.CastInfo, expressionType) || - TakesExpressionTree(info.OperationInfo, expressionType)) - { - return true; - } + return TakesExpressionTree(info.CastInfo, expressionType) + || TakesExpressionTree(info.OperationInfo, expressionType); } - - return false; } static bool TakesExpressionTree(SymbolInfo info, INamedTypeSymbol expressionType) From 423edb0dfe24ff01ec3feb30afb73c1c97a392db Mon Sep 17 00:00:00 2001 From: Tau <4602612+bash@users.noreply.github.com> Date: Mon, 25 Sep 2023 15:50:44 +0200 Subject: [PATCH 5/6] Make functions static by providing the context as arguments --- .../Funcky.Analyzers/SyntaxNodeExtensions.cs | 54 ++++++++++--------- 1 file changed, 28 insertions(+), 26 deletions(-) diff --git a/Funcky.Analyzers/Funcky.Analyzers/SyntaxNodeExtensions.cs b/Funcky.Analyzers/Funcky.Analyzers/SyntaxNodeExtensions.cs index 7e9874da..fb7451d6 100644 --- a/Funcky.Analyzers/Funcky.Analyzers/SyntaxNodeExtensions.cs +++ b/Funcky.Analyzers/Funcky.Analyzers/SyntaxNodeExtensions.cs @@ -18,39 +18,35 @@ public static bool IsInExpressionTree( { return expressionTypeOpt is not null && node is not null - && node.AncestorsAndSelf().Any(current => IsExpressionTree(current, expressionTypeOpt, semanticModel, cancellationToken)); + && node.AncestorsAndSelf().Any(current => IsExpressionTree(new IsExpressionTreeContext(current, expressionTypeOpt, semanticModel, cancellationToken))); - static bool IsExpressionTree( - SyntaxNode current, - INamedTypeSymbol expressionType, - SemanticModel semanticModel, - CancellationToken cancellationToken) + static bool IsExpressionTree(IsExpressionTreeContext context) { - return current switch { - _ when current.IsAnyLambda() => LambdaIsExpressionTree(), - SelectOrGroupClauseSyntax or OrderingSyntax => QueryExpressionIsExpressionTree(), - QueryClauseSyntax queryClause => QueryClauseIsExpressionTree(queryClause), + return context.Syntax switch { + var node when node.IsAnyLambda() => LambdaIsExpressionTree(context), + SelectOrGroupClauseSyntax or OrderingSyntax => QueryExpressionIsExpressionTree(context), + QueryClauseSyntax queryClause => QueryClauseIsExpressionTree(context, queryClause), _ => false, }; + } - bool LambdaIsExpressionTree() - { - var typeInfo = semanticModel.GetTypeInfo(current, cancellationToken); - return SymbolEqualityComparer.Default.Equals(expressionType, typeInfo.ConvertedType?.OriginalDefinition); - } + static bool LambdaIsExpressionTree(IsExpressionTreeContext context) + { + var typeInfo = context.SemanticModel.GetTypeInfo(context.Syntax, context.CancellationToken); + return SymbolEqualityComparer.Default.Equals(context.ExpressionType, typeInfo.ConvertedType?.OriginalDefinition); + } - bool QueryExpressionIsExpressionTree() - { - var info = semanticModel.GetSymbolInfo(current, cancellationToken); - return TakesExpressionTree(info, expressionType); - } + static bool QueryExpressionIsExpressionTree(IsExpressionTreeContext context) + { + var info = context.SemanticModel.GetSymbolInfo(context.Syntax, context.CancellationToken); + return TakesExpressionTree(info, context.ExpressionType); + } - bool QueryClauseIsExpressionTree(QueryClauseSyntax queryClause) - { - var info = semanticModel.GetQueryClauseInfo(queryClause, cancellationToken); - return TakesExpressionTree(info.CastInfo, expressionType) - || TakesExpressionTree(info.OperationInfo, expressionType); - } + static bool QueryClauseIsExpressionTree(IsExpressionTreeContext context, QueryClauseSyntax queryClause) + { + var info = context.SemanticModel.GetQueryClauseInfo(queryClause, context.CancellationToken); + return TakesExpressionTree(info.CastInfo, context.ExpressionType) + || TakesExpressionTree(info.OperationInfo, context.ExpressionType); } static bool TakesExpressionTree(SymbolInfo info, INamedTypeSymbol expressionType) @@ -71,4 +67,10 @@ internal static ImmutableArray GetAllSymbols(SymbolInfo info) // https://github.com/dotnet/roslyn/blob/232f7afa4966411958759c880de3a1765bdb28a0/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/CSharp/Extensions/SyntaxNodeExtensions.cs#L925 internal static bool IsAnyLambda([NotNullWhen(returnValue: true)] this SyntaxNode? node) => node?.Kind() is SyntaxKind.ParenthesizedLambdaExpression or SyntaxKind.SimpleLambdaExpression; + + private sealed record IsExpressionTreeContext( + SyntaxNode Syntax, + INamedTypeSymbol ExpressionType, + SemanticModel SemanticModel, + CancellationToken CancellationToken); } From ab08f0de78298ce03e9ca38d1c6025877f650fbf Mon Sep 17 00:00:00 2001 From: Tau <4602612+bash@users.noreply.github.com> Date: Mon, 25 Sep 2023 15:55:42 +0200 Subject: [PATCH 6/6] Extract to a separate file --- .../SyntaxNodeExtensions.ExpressionTree.cs | 64 +++++++++++++++++++ .../Funcky.Analyzers/SyntaxNodeExtensions.cs | 59 +---------------- 2 files changed, 65 insertions(+), 58 deletions(-) create mode 100644 Funcky.Analyzers/Funcky.Analyzers/SyntaxNodeExtensions.ExpressionTree.cs diff --git a/Funcky.Analyzers/Funcky.Analyzers/SyntaxNodeExtensions.ExpressionTree.cs b/Funcky.Analyzers/Funcky.Analyzers/SyntaxNodeExtensions.ExpressionTree.cs new file mode 100644 index 00000000..84dc0a7d --- /dev/null +++ b/Funcky.Analyzers/Funcky.Analyzers/SyntaxNodeExtensions.ExpressionTree.cs @@ -0,0 +1,64 @@ +using System.Diagnostics.CodeAnalysis; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; + +namespace Funcky.Analyzers; + +internal static partial class SyntaxNodeExtensions +{ + // Adapted from Roslyn's source code as this API is not public: + // https://github.com/dotnet/roslyn/blob/232f7afa4966411958759c880de3a1765bdb28a0/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/CSharp/Extensions/SyntaxNodeExtensions.cs#L925 + public static bool IsInExpressionTree( + [NotNullWhen(returnValue: true)] this SyntaxNode? node, + SemanticModel semanticModel, + [NotNullWhen(returnValue: true)] INamedTypeSymbol? expressionType, + CancellationToken cancellationToken) + => expressionType is not null + && node is not null + && node + .AncestorsAndSelf() + .Any(current => IsExpressionTree(new(current, expressionType, semanticModel, cancellationToken))); + + private static bool IsExpressionTree(IsExpressionTreeContext context) + => context.Syntax switch + { + var node when node.IsAnyLambda() => LambdaIsExpressionTree(context), + SelectOrGroupClauseSyntax or OrderingSyntax => QueryExpressionIsExpressionTree(context), + QueryClauseSyntax queryClause => QueryClauseIsExpressionTree(context, queryClause), + _ => false, + }; + + private static bool LambdaIsExpressionTree(IsExpressionTreeContext context) + { + var typeInfo = context.SemanticModel.GetTypeInfo(context.Syntax, context.CancellationToken); + return SymbolEqualityComparer.Default.Equals(context.ExpressionType, typeInfo.ConvertedType?.OriginalDefinition); + } + + private static bool QueryExpressionIsExpressionTree(IsExpressionTreeContext context) + { + var info = context.SemanticModel.GetSymbolInfo(context.Syntax, context.CancellationToken); + return TakesExpressionTree(info, context.ExpressionType); + } + + private static bool QueryClauseIsExpressionTree(IsExpressionTreeContext context, QueryClauseSyntax queryClause) + { + var info = context.SemanticModel.GetQueryClauseInfo(queryClause, context.CancellationToken); + return TakesExpressionTree(info.CastInfo, context.ExpressionType) + || TakesExpressionTree(info.OperationInfo, context.ExpressionType); + } + + private static bool TakesExpressionTree(SymbolInfo info, INamedTypeSymbol expressionType) + => GetAllSymbols(info).Any(symbol => TakesExpressionTreeAsFirstArgument(symbol, expressionType)); + + private static bool TakesExpressionTreeAsFirstArgument(ISymbol symbol, INamedTypeSymbol expressionType) + => symbol is IMethodSymbol method + && method.Parameters.Length > 0 + && SymbolEqualityComparer.Default.Equals(expressionType, method.Parameters[0].Type?.OriginalDefinition); + + private sealed record IsExpressionTreeContext( + SyntaxNode Syntax, + INamedTypeSymbol ExpressionType, + SemanticModel SemanticModel, + CancellationToken CancellationToken); +} diff --git a/Funcky.Analyzers/Funcky.Analyzers/SyntaxNodeExtensions.cs b/Funcky.Analyzers/Funcky.Analyzers/SyntaxNodeExtensions.cs index fb7451d6..ddce46ae 100644 --- a/Funcky.Analyzers/Funcky.Analyzers/SyntaxNodeExtensions.cs +++ b/Funcky.Analyzers/Funcky.Analyzers/SyntaxNodeExtensions.cs @@ -2,62 +2,11 @@ using System.Diagnostics.CodeAnalysis; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; -using Microsoft.CodeAnalysis.CSharp.Syntax; namespace Funcky.Analyzers; -internal static class SyntaxNodeExtensions +internal static partial class SyntaxNodeExtensions { - // Adapted from Roslyn's source code as this API is not public: - // https://github.com/dotnet/roslyn/blob/232f7afa4966411958759c880de3a1765bdb28a0/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/CSharp/Extensions/SyntaxNodeExtensions.cs#L925 - public static bool IsInExpressionTree( - [NotNullWhen(returnValue: true)] this SyntaxNode? node, - SemanticModel semanticModel, - [NotNullWhen(returnValue: true)] INamedTypeSymbol? expressionTypeOpt, - CancellationToken cancellationToken) - { - return expressionTypeOpt is not null - && node is not null - && node.AncestorsAndSelf().Any(current => IsExpressionTree(new IsExpressionTreeContext(current, expressionTypeOpt, semanticModel, cancellationToken))); - - static bool IsExpressionTree(IsExpressionTreeContext context) - { - return context.Syntax switch { - var node when node.IsAnyLambda() => LambdaIsExpressionTree(context), - SelectOrGroupClauseSyntax or OrderingSyntax => QueryExpressionIsExpressionTree(context), - QueryClauseSyntax queryClause => QueryClauseIsExpressionTree(context, queryClause), - _ => false, - }; - } - - static bool LambdaIsExpressionTree(IsExpressionTreeContext context) - { - var typeInfo = context.SemanticModel.GetTypeInfo(context.Syntax, context.CancellationToken); - return SymbolEqualityComparer.Default.Equals(context.ExpressionType, typeInfo.ConvertedType?.OriginalDefinition); - } - - static bool QueryExpressionIsExpressionTree(IsExpressionTreeContext context) - { - var info = context.SemanticModel.GetSymbolInfo(context.Syntax, context.CancellationToken); - return TakesExpressionTree(info, context.ExpressionType); - } - - static bool QueryClauseIsExpressionTree(IsExpressionTreeContext context, QueryClauseSyntax queryClause) - { - var info = context.SemanticModel.GetQueryClauseInfo(queryClause, context.CancellationToken); - return TakesExpressionTree(info.CastInfo, context.ExpressionType) - || TakesExpressionTree(info.OperationInfo, context.ExpressionType); - } - - static bool TakesExpressionTree(SymbolInfo info, INamedTypeSymbol expressionType) - => GetAllSymbols(info).Any(symbol => TakesExpressionTreeAsFirstArgument(symbol, expressionType)); - - static bool TakesExpressionTreeAsFirstArgument(ISymbol symbol, INamedTypeSymbol expressionType) - => symbol is IMethodSymbol method - && method.Parameters.Length > 0 - && SymbolEqualityComparer.Default.Equals(expressionType, method.Parameters[0].Type?.OriginalDefinition); - } - internal static ImmutableArray GetAllSymbols(SymbolInfo info) => info.Symbol == null ? info.CandidateSymbols @@ -67,10 +16,4 @@ internal static ImmutableArray GetAllSymbols(SymbolInfo info) // https://github.com/dotnet/roslyn/blob/232f7afa4966411958759c880de3a1765bdb28a0/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/CSharp/Extensions/SyntaxNodeExtensions.cs#L925 internal static bool IsAnyLambda([NotNullWhen(returnValue: true)] this SyntaxNode? node) => node?.Kind() is SyntaxKind.ParenthesizedLambdaExpression or SyntaxKind.SimpleLambdaExpression; - - private sealed record IsExpressionTreeContext( - SyntaxNode Syntax, - INamedTypeSymbol ExpressionType, - SemanticModel SemanticModel, - CancellationToken CancellationToken); }