diff --git a/src/Analyzers/Analyzers.Tests/ApiUsage/UnsupportedCallSiteAttributeTests.cs b/src/Analyzers/Analyzers.Tests/ApiUsage/UnsupportedCallSiteAttributeTests.cs index a96458816d..1cb5141670 100644 --- a/src/Analyzers/Analyzers.Tests/ApiUsage/UnsupportedCallSiteAttributeTests.cs +++ b/src/Analyzers/Analyzers.Tests/ApiUsage/UnsupportedCallSiteAttributeTests.cs @@ -91,5 +91,68 @@ public void CallSite() VerifyCS.Diagnostic(UnsupportedCallSiteAttributeAnalyzer.DoNotInvokeMethodFromUnsupportedCallSite) .WithLocation(0).WithArguments("Target", "due to: \"REASON\"")); } + + [Fact] + public async Task Test_Warning_InvokeMethod_WithUnsupportedCallSiteAttribute_InLambda() + { + await VerifyCS.VerifyAnalyzerAsync(""" + using System; + using System.IO; + using DotVVM.Framework.CodeAnalysis; + + namespace ConsoleApplication1 + { + public class RegularClass + { + [UnsupportedCallSite(CallSiteType.ServerSide)] + public void Target() + { + + } + + public void CallSite() + { + Action fn = () => {|#0:Target()|}; + } + } + } + """, + + VerifyCS.Diagnostic(UnsupportedCallSiteAttributeAnalyzer.DoNotInvokeMethodFromUnsupportedCallSite) + .WithLocation(0).WithArguments("Target", string.Empty) + ); + } + + [Fact] + public async Task Test_Warning_InvokeMethod_WithUnsupportedCallSiteAttribute_InLinqExpression() + { + // supressed in Linq.Expressions + await VerifyCS.VerifyAnalyzerAsync(""" + using System; + using System.IO; + using System.Linq.Expressions; + using DotVVM.Framework.CodeAnalysis; + + namespace ConsoleApplication1 + { + public class RegularClass + { + [UnsupportedCallSite(CallSiteType.ServerSide)] + public int Target() + { + return 0; + } + + public void CallSite() + { + Expression fn = () => Target(); + Expression> fn2 = arg => Target(); + } + } + } + """, + expected: [] + ); + } } } diff --git a/src/Analyzers/Analyzers/ApiUsage/UnsupportedCallSiteAttributeAnalyzer.cs b/src/Analyzers/Analyzers/ApiUsage/UnsupportedCallSiteAttributeAnalyzer.cs index eb9c81c81b..18270a92a9 100644 --- a/src/Analyzers/Analyzers/ApiUsage/UnsupportedCallSiteAttributeAnalyzer.cs +++ b/src/Analyzers/Analyzers/ApiUsage/UnsupportedCallSiteAttributeAnalyzer.cs @@ -13,6 +13,7 @@ public sealed class UnsupportedCallSiteAttributeAnalyzer : DiagnosticAnalyzer private static readonly LocalizableResourceString unsupportedCallSiteMessage = new(nameof(Resources.ApiUsage_UnsupportedCallSite_Message), Resources.ResourceManager, typeof(Resources)); private static readonly LocalizableResourceString unsupportedCallSiteDescription = new(nameof(Resources.ApiUsage_UnsupportedCallSite_Description), Resources.ResourceManager, typeof(Resources)); private const string unsupportedCallSiteAttributeMetadataName = "DotVVM.Framework.CodeAnalysis.UnsupportedCallSiteAttribute"; + private const string linqExpressionsExpression1MetadataName = "System.Linq.Expressions.Expression`1"; private const int callSiteTypeServerUnderlyingValue = 0; public static DiagnosticDescriptor DoNotInvokeMethodFromUnsupportedCallSite = new DiagnosticDescriptor( @@ -48,6 +49,10 @@ public override void Initialize(AnalysisContext context) if (attribute.ConstructorArguments.First().Value is not int callSiteType || callSiteTypeServerUnderlyingValue != callSiteType) return; + if (context.Operation.IsWithinExpressionTree(context.Compilation.GetTypeByMetadataName(linqExpressionsExpression1MetadataName))) + // supress in Linq.Expression trees, such as in ValueOrBinding.Select + return; + var reason = (string?)attribute.ConstructorArguments.Skip(1).First().Value; context.ReportDiagnostic( Diagnostic.Create( diff --git a/src/Analyzers/Analyzers/DiagnosticCategory.cs b/src/Analyzers/Analyzers/DiagnosticCategory.cs index 60e78aaa99..dda6d3ed3f 100644 --- a/src/Analyzers/Analyzers/DiagnosticCategory.cs +++ b/src/Analyzers/Analyzers/DiagnosticCategory.cs @@ -1,5 +1,4 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using System.Text; namespace DotVVM.Analyzers diff --git a/src/Analyzers/Analyzers/RoslynTreeHelper.cs b/src/Analyzers/Analyzers/RoslynTreeHelper.cs new file mode 100644 index 0000000000..9a8044de40 --- /dev/null +++ b/src/Analyzers/Analyzers/RoslynTreeHelper.cs @@ -0,0 +1,51 @@ +using System; +using System.Collections.Immutable; +using Microsoft.CodeAnalysis; + +namespace DotVVM.Analyzers +{ + internal static class RoslynTreeHelper + { + // from https://github.com/Evangelink/roslyn-analyzers/blob/48424637e03e48bbbd8e02862c940e7eb5436817/src/Utilities/Compiler/Extensions/IOperationExtensions.cs + private static readonly ImmutableArray s_LambdaAndLocalFunctionKinds = + ImmutableArray.Create(OperationKind.AnonymousFunction, OperationKind.LocalFunction); + + /// + /// Gets the first ancestor of this operation with: + /// 1. Any OperationKind from the specified . + /// 2. If is non-null, it succeeds for the ancestor. + /// Returns null if there is no such ancestor. + /// + public static IOperation? GetAncestor(this IOperation root, ImmutableArray ancestorKinds, Func? predicate = null) + { + if (root == null) + { + throw new ArgumentNullException(nameof(root)); + } + + var ancestor = root; + do + { + ancestor = ancestor.Parent; + } while (ancestor != null && !ancestorKinds.Contains(ancestor.Kind)); + + if (ancestor != null) + { + if (predicate != null && !predicate(ancestor)) + { + return GetAncestor(ancestor, ancestorKinds, predicate); + } + return ancestor; + } + else + { + return default; + } + } + + public static bool IsWithinExpressionTree(this IOperation operation, INamedTypeSymbol? linqExpressionTreeType) + => linqExpressionTreeType != null + && operation.GetAncestor(s_LambdaAndLocalFunctionKinds)?.Parent?.Type?.OriginalDefinition is { } lambdaType + && SymbolEqualityComparer.Default.Equals(linqExpressionTreeType, lambdaType); + } +}