Skip to content

Add public API to obtain a location specifier for a call #72133

@RikkiGibson

Description

@RikkiGibson

Continuation of #72093

Need for generators to obtain a location specifier for a call

We are making adjustments to the interceptors design in order to improve the following aspects:

  • Portability of generated code. (i.e. the ability to generate code on one machine and compile the generated code on another machine.)
  • Simplicity for generator authors, by introducing public API accessible for generators, in order to get the correct argument(s) for [InterceptsLocation], with minimal guesswork.
  • Evolvability of the representation of locations, transparent to generator authors, by introducing an [InterceptsLocation(int, string)] constructor overload, the encoding of which can be updated over time.

Proposed API

We plan to add the following well-known constructor to InterceptsLocationAttribute:

 namespace System.Runtime.CompilerServices;

 public class InterceptsLocationAttribute
 {
     public InterceptsLocationAttribute(string filePath, int line, int character) { } // plan to drop support prior to stable release of interceptors feature
+    public InterceptsLocationAttribute(int version, string data) { }
 }

We propose adding the following public API to Roslyn:

 namespace Microsoft.CodeAnalysis
 {
     public static class CSharpExtensions
     {
+        /// <summary>
+        /// If 'node' cannot be intercepted, returns null. Otherwise, returns an InterceptableLocation instance which can be used to intercept the call denoted by 'node'.
+        /// </summary>
+        [Experimental(RoslynExperiments.Interceptors)]
+        public InterceptableLocation? GetInterceptableLocation(this SemanticModel model, InvocationExpressionSyntax node);
     }
 
     namespace CSharp
     {
+        /// <summary>Location data which can be used to intercept a call.</summary>
+        [Experimental(RoslynExperiments.Interceptors)]
+        public sealed class InterceptableLocation : IEquatable<InterceptableLocation>
+        {
+            /// <summary>The version of the InterceptsLocationAttribute data encoding. Used as an argument for the parameter 'version' of InterceptsLocationAttribute.</summary>
+            public int Version { get; }
+
+            /// <summary>Opaque data which references an interceptable call. Used as an argument for the parameter 'data' of InterceptsLocationAttribute.</summary>
+            public string Data { get; }
+
+            /// <summary>Gets a human-readable location description for the interceptable call.</summary>
+            public string GetDisplayLocation();
+
+            /// <summary>Returns a source code fragment of the form `[global::System.Runtime.CompilerServices.InterceptsLocationAttribute(version, data)]`.</summary>
+            public string GetInterceptsLocationAttributeSyntax();
+        }
+    }
 }
  • We declare this as an extension method because we want to use C#-specific types in the signature (a la Add public API to determine if a call is being intercepted #72093).
  • We use SemanticModel as the receiver type of the extension, because we want to have the ability to depend on compilation-level information in the location encoding.

The InterceptsLocation(string, int, int) constructor will coexist with the new constructor while we work to get partner generators onboarded to it. Prior to stable release of the interceptors feature, support for InterceptsLocation(string, int, int) will be dropped.

Usage Examples

In order to produce an interceptor method like the following:

// C:\project\src\Program.cs(6,17)
[InterceptsLocation(1, "yPU5+1/pMuRHlz+XbnIQwQYAAAARAAAAUHJvZ3JhbS5jcw==")]
public static void Interceptor(this ReceiverType receiver, ParamType param) { ... }

We may employ a generator implemented like the following:

using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;

[Generator(LanguageNames.CSharp)]
public class MyGenerator : IIncrementalGenerator
{
    record InterceptorInfo(InterceptableLocation location, object data);

    public void Initialize(IncrementalGeneratorInitializationContext context)
    {
        var interceptorInfos = context.SyntaxProvider.CreateSyntaxProvider(
            predicate: IsInterceptableCall,
            transform: (GeneratorSyntaxContext context, CancellationToken token) => {
                var interceptableLocation = context.GetInterceptableLocation((InvocationExpressionSyntax)context.Node);
                if (interceptableLocation is null)
                {
                    return null; // generator wants to intercept call, but host thinks call is not interceptable. bug.
                }

                // generator is careful to propagate only equatable data (i.e., not syntax nodes or symbols).
                return new InterceptorInfo(interceptableLocation, GetData(context));
            })
            .Where(info => info != null)
            .Collect();

        context.RegisterSourceOutput(interceptorInfos, (context, interceptorInfos) => {
            var builder = new StringBuilder();
            // builder boilerplate..
            foreach (var interceptorInfo in interceptorInfos)
            {
                var (location, data) = interceptorInfo;
                builder.Add($$"""
                    // {{location.GetDisplayLocation()}}
                    [InterceptsLocation({{location.Version}}, "{{location.Data}}")]
                    public static void Interceptor(this ReceiverType receiver, ParamType param)
                    {
                        {{GetMethodBody(data)}}
                    }
                    """);

                // alternatively, author can use the convenience method, which fully qualifies the attribute name:
                // e.g. `[global::System.Runtime.CompilerServices.InterceptsLocationAttribute(...)]`.
                builder.Add($$"""
                    // {{location.GetDisplayLocation()}}
                    {{location.GetInterceptsLocationAttributeSyntax()}}
                    public static void Interceptor(this ReceiverType receiver, ParamType param)
                    {
                        {{GetMethodBody(data)}}
                    }
                    """);
            }
            // builder boilerplate..

            context.AddSource(builder.ToString(), hintName: "MyInterceptors.cs");
        });
    }
}

Alternative Designs

  • Do not add a new constructor to [InterceptsLocation], and instead expose an API which obtains the attribute arguments for the existing constructor. This would likely require knowing the output directory for the particular generator and accepting a hintName in order to produce a relative path.
 namespace Microsoft.CodeAnalysis;

 public readonly struct GeneratorSyntaxContext
 {
     public void AddSource(string hintName, string source);
+    /// <summary>If 'expression' cannot be intercepted, returns null. Otherwise, returns a location specifier which can be used to intercept 'expression'.</summary>
+    /// <remarks>Currently, only 'InvocationExpressionSyntax' can be intercepted.</remarks>
+    public (string filePath, int line, int character)? GetInterceptsLocationArguments(ExpressionSyntax expression, string interceptorFileHintName);
 }
  • Do not expose any API for obtaining InterceptsLocation arguments. Instead, document the location encoding and have generator authors create them on their own.

Risks

LDM has not yet reviewed the proposed checksum-based approach. LDM will review the design while there is time to address any feedback before shipping the feature. If the feedback requires adjusting the approach (e.g. the particular signature of the attribute constructor), then it is possible that corresponding public API changes will need to also be made prior to shipping.

Metadata

Metadata

Assignees

Labels

Area-CompilersConcept-APIThis issue involves adding, removing, clarification, or modification of an API.Feature - Interceptorsapi-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