-
Notifications
You must be signed in to change notification settings - Fork 4.2k
Description
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
SemanticModelas 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 ahintNamein 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.