Skip to content

Commit 0413759

Browse files
authored
Use new [Interceptable] ctor for source gen (#104180)
1 parent f657459 commit 0413759

File tree

124 files changed

+12584
-35
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

124 files changed

+12584
-35
lines changed

src/libraries/Microsoft.Extensions.Configuration.Binder/README.md

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -118,13 +118,36 @@ Sometimes the SDK uses stale bits of the generator. This can lead to unexpected
118118

119119
Some contributions might change the logic emitted by the generator. We maintain baseline [source files](https://github.com/dotnet/runtime/tree/e3e9758a10870a8f99a93a25e54ab2837d3abefc/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/SourceGenerationTests/Baselines) to track the code emitted to handle some core binding scenarios.
120120

121-
If the emitted code changes, these tests will fail locally and in continuous integration checks (for PRs) changes. You would need to update the baseline source files, manually or by using the following commands (PowerShell):
121+
If the emitted code changes, these tests will fail locally and\or during continuous integration checks. You would need to update the baseline source files, manually or by using a combination of:
122+
- The `/p:UpdateBaselines=true` switch when building `Microsoft.Extensions.Configuration.Binder` in order to use `InterceptableAttributeVersion` for testing and\or updating the baselines.
123+
- The `/p:UpdateBaselines=true` switch when building `Microsoft.Extensions.Configuration.Binder.SourceGeneration.Tests` in order to update the test baseline files.
124+
- The `RepoRootDir` environment variable.
125+
- The optional `InterceptableAttributeVersion` environment variable.
122126

127+
The `RepoRootDir environment variable needs to be specified to the root repo path.
128+
129+
The `InterceptableAttributeVersion` specifies what version of the `[Interceptable]` attribute should be generated. Currently there are two versions, both of which are experimental as of July 2024, and one is selected based on the local compiler. The original version ("version 0") is expected to be deprecated. Version 1 will be used for newer compilers automatically. However, if version 0 needs to be updated when newer compilers are present, version 0 can be forced by setting the environment variable to `0`.
130+
131+
Sample commands (PowerShell):
123132
```ps
124133
> $env:RepoRootDir = "D:\repos\dotnet_runtime"
125-
> dotnet build t:test -f /p:UpdateBaselines=true
134+
> $env:InterceptableAttributeVersion = 0 # NOTE: this is optional - see notes
135+
> cd D:/repros/dotnet_runtime/src/libraries/Microsoft.Extensions.Configuration.Binder
136+
> dotnet build /p:UpdateBaselines=true
137+
> cd tests/SourceGenerationTests
138+
> dotnet build -t:test /p:UpdateBaselines=true
126139
```
127140

128-
We have a [test helper](https://github.com/dotnet/runtime/blob/e3e9758a10870a8f99a93a25e54ab2837d3abefc/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/SourceGenerationTests/GeneratorTests.Helpers.cs#L105-L118) to update the baselines. It requires setting an environment variable called `RepoRootDir` to the root repo path. In additon, the `UpdateBaselines` MSBuild property needs to be set to `true`.
141+
Sample commands (command prompt):
142+
```
143+
set RepoRootDir = "D:\repos\dotnet_runtime"
144+
set InterceptableAttributeVersion = 0 REM NOTE: this is optional - see notes
145+
cd D:\repros\dotnet_runtime\src\libraries\Microsoft.Extensions.Configuration.Binder
146+
dotnet build /p:UpdateBaselines=true
147+
cd tests\SourceGenerationTests
148+
dotnet build -t:test /p:UpdateBaselines=true
149+
```
129150

130-
After updating the baselines, inspect the changes to verify that they are valid. Note that the baseline tests will fail if the new code causes errors when building the resulting compilation.
151+
After updating the baselines:
152+
- Inspect the changes to verify that they are valid. Note that the baseline tests will fail if the new code causes errors when building the resulting compilation.
153+
- Rebuild `Microsoft.Extensions.Configuration.Binder.SourceGeneration.Tests` without `/p:UpdateBaselines=true` so that the tests compare against the new baselines instead of being re-generated.

src/libraries/Microsoft.Extensions.Configuration.Binder/gen/ConfigurationBindingGenerator.Emitter.cs

Lines changed: 17 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -72,22 +72,28 @@ file static class {{Identifier.BindingExtensions}}
7272
private void EmitInterceptsLocationAttrDecl()
7373
{
7474
_writer.WriteLine();
75+
76+
string arguments = ConfigurationBindingGenerator.InterceptorVersion == 0 ?
77+
"string filePath, int line, int column" :
78+
"int version, string data";
79+
7580
_writer.WriteLine($$"""
76-
namespace System.Runtime.CompilerServices
77-
{
78-
using System;
79-
using System.CodeDom.Compiler;
81+
namespace System.Runtime.CompilerServices
82+
{
83+
using System;
84+
using System.CodeDom.Compiler;
8085
81-
{{Expression.GeneratedCodeAnnotation}}
82-
[AttributeUsage(AttributeTargets.Method, AllowMultiple = true)]
83-
file sealed class InterceptsLocationAttribute : Attribute
86+
{{Expression.GeneratedCodeAnnotation}}
87+
[AttributeUsage(AttributeTargets.Method, AllowMultiple = true)]
88+
file sealed class InterceptsLocationAttribute : Attribute
89+
{
90+
public InterceptsLocationAttribute({{arguments}})
8491
{
85-
public InterceptsLocationAttribute(string filePath, int line, int column)
86-
{
87-
}
8892
}
8993
}
90-
""");
94+
}
95+
""");
96+
9197
_writer.WriteLine();
9298
}
9399

src/libraries/Microsoft.Extensions.Configuration.Binder/gen/ConfigurationBindingGenerator.cs

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,12 @@
33

44
//#define LAUNCH_DEBUGGER
55
using System;
6+
using System.Diagnostics;
7+
using System.Reflection;
8+
using System.Threading;
69
using Microsoft.CodeAnalysis;
710
using Microsoft.CodeAnalysis.CSharp;
11+
using Microsoft.CodeAnalysis.CSharp.Syntax;
812
using SourceGenerators;
913

1014
namespace Microsoft.Extensions.Configuration.Binder.SourceGeneration
@@ -62,6 +66,69 @@ public void Initialize(IncrementalGeneratorInitializationContext context)
6266
.WithTrackingName(GenSpecTrackingName);
6367

6468
context.RegisterSourceOutput(genSpec, ReportDiagnosticsAndEmitSource);
69+
70+
if (!s_hasInitializedInterceptorVersion)
71+
{
72+
InterceptorVersion = DetermineInterceptableVersion();
73+
s_hasInitializedInterceptorVersion = true;
74+
}
75+
}
76+
77+
internal static int InterceptorVersion { get; private set; }
78+
79+
// Used with v1 interceptor lightup approach:
80+
private static bool s_hasInitializedInterceptorVersion;
81+
internal static Func<SemanticModel, InvocationExpressionSyntax, CancellationToken, object>? GetInterceptableLocationFunc { get; private set; }
82+
internal static MethodInfo? InterceptableLocationVersionGetDisplayLocation { get; private set; }
83+
internal static MethodInfo? InterceptableLocationDataGetter { get; private set; }
84+
internal static MethodInfo? InterceptableLocationVersionGetter { get; private set; }
85+
86+
internal static int DetermineInterceptableVersion()
87+
{
88+
MethodInfo? getInterceptableLocationMethod = null;
89+
int? interceptableVersion = null;
90+
91+
#if UPDATE_BASELINES
92+
#pragma warning disable RS1035 // Do not use APIs banned for analyzers
93+
string? interceptableVersionString = Environment.GetEnvironmentVariable("InterceptableAttributeVersion");
94+
#pragma warning restore RS1035
95+
if (interceptableVersionString is not null)
96+
{
97+
if (int.TryParse(interceptableVersionString, out int version) && (version == 0 || version == 1))
98+
{
99+
interceptableVersion = version;
100+
}
101+
else
102+
{
103+
throw new InvalidOperationException($"Invalid InterceptableAttributeVersion value: {interceptableVersionString}");
104+
}
105+
}
106+
107+
if (interceptableVersion is null || interceptableVersion == 1)
108+
#endif
109+
{
110+
getInterceptableLocationMethod = typeof(Microsoft.CodeAnalysis.CSharp.CSharpExtensions).GetMethod(
111+
"GetInterceptableLocation",
112+
BindingFlags.Static | BindingFlags.Public,
113+
binder: null,
114+
new Type[] { typeof(SemanticModel), typeof(InvocationExpressionSyntax), typeof(CancellationToken) },
115+
modifiers: Array.Empty<ParameterModifier>());
116+
117+
interceptableVersion = getInterceptableLocationMethod is null ? 0 : 1;
118+
}
119+
120+
if (interceptableVersion == 1)
121+
{
122+
GetInterceptableLocationFunc = (Func<SemanticModel, InvocationExpressionSyntax, CancellationToken, object>)
123+
getInterceptableLocationMethod.CreateDelegate(typeof(Func<SemanticModel, InvocationExpressionSyntax, CancellationToken, object>), target: null);
124+
125+
Type? interceptableLocationType = typeof(Microsoft.CodeAnalysis.CSharp.CSharpExtensions).Assembly.GetType("Microsoft.CodeAnalysis.CSharp.InterceptableLocation");
126+
InterceptableLocationVersionGetDisplayLocation = interceptableLocationType.GetMethod("GetDisplayLocation", BindingFlags.Instance | BindingFlags.Public);
127+
InterceptableLocationVersionGetter = interceptableLocationType.GetProperty("Version", BindingFlags.Instance | BindingFlags.Public).GetGetMethod();
128+
InterceptableLocationDataGetter = interceptableLocationType.GetProperty("Data", BindingFlags.Instance | BindingFlags.Public).GetGetMethod();
129+
}
130+
131+
return interceptableVersion.Value;
65132
}
66133

67134
/// <summary>

src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Emitter/Helpers.cs

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -158,9 +158,19 @@ overload is MethodsToGen.ServiceCollectionExt_Configure_T_name_BinderOptions ||
158158

159159
private void EmitInterceptsLocationAnnotations(IEnumerable<InvocationLocationInfo> infoList)
160160
{
161-
foreach (InvocationLocationInfo info in infoList)
161+
if (ConfigurationBindingGenerator.InterceptorVersion == 0)
162162
{
163-
_writer.WriteLine($@"[{Identifier.InterceptsLocation}(@""{info.FilePath}"", {info.LineNumber}, {info.CharacterNumber})]");
163+
foreach (InvocationLocationInfo info in infoList)
164+
{
165+
_writer.WriteLine($@"[{Identifier.InterceptsLocation}(@""{info.FilePath}"", {info.LineNumber}, {info.CharacterNumber})]");
166+
}
167+
}
168+
else
169+
{
170+
foreach (InvocationLocationInfo info in infoList)
171+
{
172+
_writer.WriteLine($@"[{Identifier.InterceptsLocation}({info.InterceptableLocationVersion}, ""{info.InterceptableLocationData}"")] // {info.InterceptableLocationGetDisplayLocation()}");
173+
}
164174
}
165175
}
166176

src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Microsoft.Extensions.Configuration.Binder.SourceGeneration.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
<AnalyzerLanguage>cs</AnalyzerLanguage>
88
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
99
<DefineConstants Condition="'$(LaunchDebugger)' == 'true'">$(DefineConstants);LAUNCH_DEBUGGER</DefineConstants>
10+
<DefineConstants Condition="'$(UpdateBaselines)' == 'true'">$(DefineConstants);UPDATE_BASELINES</DefineConstants>
1011
</PropertyGroup>
1112

1213
<PropertyGroup Condition="'$(DevBuild)' == 'true'">

src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Specs/InterceptorInfo.cs

Lines changed: 40 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
using System.Collections.Generic;
66
using System.Diagnostics;
77
using System.Linq;
8+
using System.Reflection;
9+
using System.Threading;
810
using Microsoft.CodeAnalysis;
911
using Microsoft.CodeAnalysis.CSharp.Syntax;
1012
using Microsoft.CodeAnalysis.Operations;
@@ -170,33 +172,56 @@ public InvocationLocationInfo(MethodsToGen interceptor, IInvocationOperation inv
170172
{
171173
Debug.Assert(BinderInvocation.IsBindingOperation(invocation));
172174

173-
if (invocation.Syntax is not InvocationExpressionSyntax { Expression: MemberAccessExpressionSyntax memberAccessExprSyntax })
175+
if (invocation.Syntax is not InvocationExpressionSyntax { Expression: MemberAccessExpressionSyntax memberAccessExprSyntax } invocationExpressionSyntax)
174176
{
175177
const string InvalidInvocationErrMsg = "The invocation should have been validated upstream when selecting invocations to emit interceptors for.";
176178
throw new ArgumentException(InvalidInvocationErrMsg, nameof(invocation));
177179
}
178180

179-
SyntaxTree operationSyntaxTree = invocation.Syntax.SyntaxTree;
180-
TextSpan memberNameSpan = memberAccessExprSyntax.Name.Span;
181-
FileLinePositionSpan linePosSpan = operationSyntaxTree.GetLineSpan(memberNameSpan);
182-
183-
Interceptor = interceptor;
184-
LineNumber = linePosSpan.StartLinePosition.Line + 1;
185-
CharacterNumber = linePosSpan.StartLinePosition.Character + 1;
186-
FilePath = GetInterceptorFilePath();
187-
188-
// Use the same logic used by the interceptors API for resolving the source mapped value of a path.
189-
// https://github.com/dotnet/roslyn/blob/f290437fcc75dad50a38c09e0977cce13a64f5ba/src/Compilers/CSharp/Portable/Compilation/CSharpCompilation.cs#L1063-L1064
190-
string GetInterceptorFilePath()
181+
if (ConfigurationBindingGenerator.InterceptorVersion == 0)
182+
{
183+
SyntaxTree operationSyntaxTree = invocation.Syntax.SyntaxTree;
184+
TextSpan memberNameSpan = memberAccessExprSyntax.Name.Span;
185+
FileLinePositionSpan linePosSpan = operationSyntaxTree.GetLineSpan(memberNameSpan);
186+
187+
Interceptor = interceptor;
188+
LineNumber = linePosSpan.StartLinePosition.Line + 1;
189+
CharacterNumber = linePosSpan.StartLinePosition.Character + 1;
190+
FilePath = GetInterceptorFilePath();
191+
192+
// Use the same logic used by the interceptors API for resolving the source mapped value of a path.
193+
// https://github.com/dotnet/roslyn/blob/f290437fcc75dad50a38c09e0977cce13a64f5ba/src/Compilers/CSharp/Portable/Compilation/CSharpCompilation.cs#L1063-L1064
194+
string GetInterceptorFilePath()
195+
{
196+
SourceReferenceResolver? sourceReferenceResolver = invocation.SemanticModel?.Compilation.Options.SourceReferenceResolver;
197+
return sourceReferenceResolver?.NormalizePath(operationSyntaxTree.FilePath, baseFilePath: null) ?? operationSyntaxTree.FilePath;
198+
}
199+
}
200+
else
191201
{
192-
SourceReferenceResolver? sourceReferenceResolver = invocation.SemanticModel?.Compilation.Options.SourceReferenceResolver;
193-
return sourceReferenceResolver?.NormalizePath(operationSyntaxTree.FilePath, baseFilePath: null) ?? operationSyntaxTree.FilePath;
202+
Debug.Assert(ConfigurationBindingGenerator.InterceptorVersion == 1);
203+
Interceptor = interceptor;
204+
InterceptableLocation = ConfigurationBindingGenerator.GetInterceptableLocationFunc(invocation.SemanticModel, invocationExpressionSyntax, default(CancellationToken));
194205
}
195206
}
196207

197208
public MethodsToGen Interceptor { get; }
209+
210+
// Used with v0 interceptor approach:
198211
public string FilePath { get; }
199212
public int LineNumber { get; }
200213
public int CharacterNumber { get; }
214+
215+
// Used with v1 interceptor approach:
216+
private object? InterceptableLocation { get; }
217+
218+
public string InterceptableLocationGetDisplayLocation() => InterceptableLocation is null ? "" :
219+
(string)ConfigurationBindingGenerator.InterceptableLocationVersionGetDisplayLocation.Invoke(InterceptableLocation, parameters: null);
220+
221+
public string InterceptableLocationData => InterceptableLocation is null ? "" :
222+
(string)ConfigurationBindingGenerator.InterceptableLocationDataGetter.Invoke(InterceptableLocation, parameters: null);
223+
224+
public int InterceptableLocationVersion => InterceptableLocation is null ? 0 :
225+
(int)ConfigurationBindingGenerator.InterceptableLocationVersionGetter.Invoke(InterceptableLocation, parameters: null);
201226
}
202227
}

0 commit comments

Comments
 (0)