Skip to content

Add public API to determine if a call is being intercepted #72093

@RikkiGibson

Description

@RikkiGibson

Background and Motivation

As we work to stabilize the interceptors feature, we have public API additions which are needed in order to deliver a fully coherent and usable feature experience.

Need for interceptor semantic info in IDE

See also https://github.com/dotnet/csharplang/blob/interceptors-2024-01/proposals/interceptors-issues-2024-01.md#need-for-interceptor-semantic-info-in-ide

Currently, the Roslyn public APIs provide no indication of whether a method call is being intercepted. After shipping the interceptors experimental feature, we found that some analyzers had a need to know when a call is being intercepted, in order to produce correct diagnostics.

routes.MapGet("/products/", () => { ... }); // warning: MapGet is not NativeAOT-compatible

static class Extensions
{
    // Original method signature looks like:
    [RequiresUnreferencedCode]
    public static IRouteEndpointBuilder MapGet(this IRouteEndpointBuilder builder, string route, Delegate handler) => ...;

    // Interceptor signature looks like (i.e. lacks '[RequiresUnreferencedCode]'):
    [InterceptsLocation(/* routes.MapGet(...) */)]
    public static IRouteEndpointBuilder Interceptor(this IRouteEndpointBuilder builder, string route, Delegate handler) => ...;
}

In the above sample, the ILLinker analyzer will warn about use of a method marked with [RequiresUnreferencedCode]. If it knew that the call was intercepted, and could inspect the interceptor method instead, then it would be able to give more accurate diagnostics (i.e., it would know that it does not need to warn on the above call.)

In .NET 8, we worked around the spurious warning in the above sample by having the generator author ship a diagnostic suppressor. We think this is problematic as a practice because it requires the generator author to understand the semantics of the warning deeply enough to be able to claim "actually, this doesn't apply to me". If, for example, the interceptor method also had [RequiresUnreferencedCode], we wouldn't want the warning to be suppressed, and it seems like a layering violation for the source generator to need to know these kinds of in-depth details about another component.

Proposed API

 namespace Microsoft.CodeAnalysis;

 public static class CSharpExtensions
 {
+    /// <summary>If the call represented by <paramref name="node"/> is referenced in an InterceptsLocationAttribute, returns the original definition symbol which is decorated with that attribute. Otherwise, returns null.</summary>
+    public static IMethodSymbol? GetInterceptorMethod(this SemanticModel model, InvocationExpressionSyntax node, CancellationToken cancellationToken = default);
 }

This API shape is intended to reflect the fact that the first time an analyzer asks about interceptors, a certain amount of fixed-overhead work will need to be done and cached on the compilation. Specifically, we will need to search declarations in the compilation for any attributes which might be [InterceptsLocation] (taking aliases into account), and bind them. We expect a quite small subset of attributes on methods to be bound in this process.

Usage Examples

public sealed override void Initialize(AnalysisContext context)
{
    context.RegisterOperationAction(handleInvocation, OperationKind.Invocation);

    void handleInvocation(OperationAnalysisContext context)
    {
        var invocationOperation = (IInvocationOperation)context.Operation;
        if (invocationOperation.Syntax is InvocationExpressionSyntax invocationSyntax
            && context.SemanticModel.GetInterceptorMethod(invocationSyntax, context.CancellationToken) is IMethodSymbol interceptor)
        {
            AnalyzeInterceptor(interceptor);
        }
    }
}

Alternative Designs

We rejected a design which added a member to IInvocationOperation to get the interceptor--either a method which takes CancellationToken or a property accessor.

Risks

This API adds complexity to analyzers: they need to decide if they care about intercepted calls or not. In most cases, we believe analyzers do not need to care about intercepted calls. Looking at the original call should be good enough.

The essential case where an analyzer may need to look at the interceptor of a call is when legal signature differences between the interceptor and original methods might influence the analyzer's behavior. In the ILLinker analyzer case, its behavior differs based on presence or absence of the [RequiresUnreferencedCode] attribute. This makes interceptors of interest to that analyzer.

Metadata

Metadata

Assignees

Labels

Area-CompilersConcept-APIThis issue involves adding, removing, clarification, or modification of an API.Feature - InterceptorsFeature Requestapi-approvedAPI was approved in API review, it can be implementedblockingAPI needs to reviewed with priority to unblock work

Type

No type

Projects

No projects

Milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions