Skip to content

Commit

Permalink
Merge pull request #1859 from riganti/fix-DotVVM04-inLinqExpr
Browse files Browse the repository at this point in the history
Supress DotVVM04 in Linq.Expressions
  • Loading branch information
exyi authored Oct 10, 2024
2 parents d92e70b + a2621b5 commit f73922b
Show file tree
Hide file tree
Showing 4 changed files with 120 additions and 2 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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<Action> fn = () => Target();
Expression<Func<int, int>> fn2 = arg => Target();
}
}
}
""",
expected: []
);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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(
Expand Down
3 changes: 1 addition & 2 deletions src/Analyzers/Analyzers/DiagnosticCategory.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
using System;
using System.Collections.Generic;
using System.Collections.Generic;
using System.Text;

namespace DotVVM.Analyzers
Expand Down
51 changes: 51 additions & 0 deletions src/Analyzers/Analyzers/RoslynTreeHelper.cs
Original file line number Diff line number Diff line change
@@ -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<OperationKind> s_LambdaAndLocalFunctionKinds =
ImmutableArray.Create(OperationKind.AnonymousFunction, OperationKind.LocalFunction);

/// <summary>
/// Gets the first ancestor of this operation with:
/// 1. Any OperationKind from the specified <paramref name="ancestorKinds"/>.
/// 2. If <paramref name="predicate"/> is non-null, it succeeds for the ancestor.
/// Returns null if there is no such ancestor.
/// </summary>
public static IOperation? GetAncestor(this IOperation root, ImmutableArray<OperationKind> ancestorKinds, Func<IOperation, bool>? 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);
}
}

0 comments on commit f73922b

Please sign in to comment.