Skip to content

Commit

Permalink
Use new [Interceptable] ctor for source gen (#104180)
Browse files Browse the repository at this point in the history
  • Loading branch information
steveharter authored Jul 10, 2024
1 parent f657459 commit 0413759
Show file tree
Hide file tree
Showing 124 changed files with 12,584 additions and 35 deletions.
31 changes: 27 additions & 4 deletions src/libraries/Microsoft.Extensions.Configuration.Binder/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -118,13 +118,36 @@ Sometimes the SDK uses stale bits of the generator. This can lead to unexpected

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.

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):
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:
- The `/p:UpdateBaselines=true` switch when building `Microsoft.Extensions.Configuration.Binder` in order to use `InterceptableAttributeVersion` for testing and\or updating the baselines.
- The `/p:UpdateBaselines=true` switch when building `Microsoft.Extensions.Configuration.Binder.SourceGeneration.Tests` in order to update the test baseline files.
- The `RepoRootDir` environment variable.
- The optional `InterceptableAttributeVersion` environment variable.

The `RepoRootDir environment variable needs to be specified to the root repo path.

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`.

Sample commands (PowerShell):
```ps
> $env:RepoRootDir = "D:\repos\dotnet_runtime"
> dotnet build t:test -f /p:UpdateBaselines=true
> $env:InterceptableAttributeVersion = 0 # NOTE: this is optional - see notes
> cd D:/repros/dotnet_runtime/src/libraries/Microsoft.Extensions.Configuration.Binder
> dotnet build /p:UpdateBaselines=true
> cd tests/SourceGenerationTests
> dotnet build -t:test /p:UpdateBaselines=true
```

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`.
Sample commands (command prompt):
```
set RepoRootDir = "D:\repos\dotnet_runtime"
set InterceptableAttributeVersion = 0 REM NOTE: this is optional - see notes
cd D:\repros\dotnet_runtime\src\libraries\Microsoft.Extensions.Configuration.Binder
dotnet build /p:UpdateBaselines=true
cd tests\SourceGenerationTests
dotnet build -t:test /p:UpdateBaselines=true
```

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.
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.
- 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.
Original file line number Diff line number Diff line change
Expand Up @@ -72,22 +72,28 @@ file static class {{Identifier.BindingExtensions}}
private void EmitInterceptsLocationAttrDecl()
{
_writer.WriteLine();

string arguments = ConfigurationBindingGenerator.InterceptorVersion == 0 ?
"string filePath, int line, int column" :
"int version, string data";

_writer.WriteLine($$"""
namespace System.Runtime.CompilerServices
{
using System;
using System.CodeDom.Compiler;
namespace System.Runtime.CompilerServices
{
using System;
using System.CodeDom.Compiler;
{{Expression.GeneratedCodeAnnotation}}
[AttributeUsage(AttributeTargets.Method, AllowMultiple = true)]
file sealed class InterceptsLocationAttribute : Attribute
{{Expression.GeneratedCodeAnnotation}}
[AttributeUsage(AttributeTargets.Method, AllowMultiple = true)]
file sealed class InterceptsLocationAttribute : Attribute
{
public InterceptsLocationAttribute({{arguments}})
{
public InterceptsLocationAttribute(string filePath, int line, int column)
{
}
}
}
""");
}
""");

_writer.WriteLine();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,12 @@

//#define LAUNCH_DEBUGGER
using System;
using System.Diagnostics;
using System.Reflection;
using System.Threading;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using SourceGenerators;

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

context.RegisterSourceOutput(genSpec, ReportDiagnosticsAndEmitSource);

if (!s_hasInitializedInterceptorVersion)
{
InterceptorVersion = DetermineInterceptableVersion();
s_hasInitializedInterceptorVersion = true;
}
}

internal static int InterceptorVersion { get; private set; }

// Used with v1 interceptor lightup approach:
private static bool s_hasInitializedInterceptorVersion;
internal static Func<SemanticModel, InvocationExpressionSyntax, CancellationToken, object>? GetInterceptableLocationFunc { get; private set; }
internal static MethodInfo? InterceptableLocationVersionGetDisplayLocation { get; private set; }
internal static MethodInfo? InterceptableLocationDataGetter { get; private set; }
internal static MethodInfo? InterceptableLocationVersionGetter { get; private set; }

internal static int DetermineInterceptableVersion()
{
MethodInfo? getInterceptableLocationMethod = null;
int? interceptableVersion = null;

#if UPDATE_BASELINES
#pragma warning disable RS1035 // Do not use APIs banned for analyzers
string? interceptableVersionString = Environment.GetEnvironmentVariable("InterceptableAttributeVersion");
#pragma warning restore RS1035
if (interceptableVersionString is not null)
{
if (int.TryParse(interceptableVersionString, out int version) && (version == 0 || version == 1))
{
interceptableVersion = version;
}
else
{
throw new InvalidOperationException($"Invalid InterceptableAttributeVersion value: {interceptableVersionString}");
}
}

if (interceptableVersion is null || interceptableVersion == 1)
#endif
{
getInterceptableLocationMethod = typeof(Microsoft.CodeAnalysis.CSharp.CSharpExtensions).GetMethod(
"GetInterceptableLocation",
BindingFlags.Static | BindingFlags.Public,
binder: null,
new Type[] { typeof(SemanticModel), typeof(InvocationExpressionSyntax), typeof(CancellationToken) },
modifiers: Array.Empty<ParameterModifier>());

interceptableVersion = getInterceptableLocationMethod is null ? 0 : 1;
}

if (interceptableVersion == 1)
{
GetInterceptableLocationFunc = (Func<SemanticModel, InvocationExpressionSyntax, CancellationToken, object>)
getInterceptableLocationMethod.CreateDelegate(typeof(Func<SemanticModel, InvocationExpressionSyntax, CancellationToken, object>), target: null);

Type? interceptableLocationType = typeof(Microsoft.CodeAnalysis.CSharp.CSharpExtensions).Assembly.GetType("Microsoft.CodeAnalysis.CSharp.InterceptableLocation");
InterceptableLocationVersionGetDisplayLocation = interceptableLocationType.GetMethod("GetDisplayLocation", BindingFlags.Instance | BindingFlags.Public);
InterceptableLocationVersionGetter = interceptableLocationType.GetProperty("Version", BindingFlags.Instance | BindingFlags.Public).GetGetMethod();
InterceptableLocationDataGetter = interceptableLocationType.GetProperty("Data", BindingFlags.Instance | BindingFlags.Public).GetGetMethod();
}

return interceptableVersion.Value;
}

/// <summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -158,9 +158,19 @@ overload is MethodsToGen.ServiceCollectionExt_Configure_T_name_BinderOptions ||

private void EmitInterceptsLocationAnnotations(IEnumerable<InvocationLocationInfo> infoList)
{
foreach (InvocationLocationInfo info in infoList)
if (ConfigurationBindingGenerator.InterceptorVersion == 0)
{
_writer.WriteLine($@"[{Identifier.InterceptsLocation}(@""{info.FilePath}"", {info.LineNumber}, {info.CharacterNumber})]");
foreach (InvocationLocationInfo info in infoList)
{
_writer.WriteLine($@"[{Identifier.InterceptsLocation}(@""{info.FilePath}"", {info.LineNumber}, {info.CharacterNumber})]");
}
}
else
{
foreach (InvocationLocationInfo info in infoList)
{
_writer.WriteLine($@"[{Identifier.InterceptsLocation}({info.InterceptableLocationVersion}, ""{info.InterceptableLocationData}"")] // {info.InterceptableLocationGetDisplayLocation()}");
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
<AnalyzerLanguage>cs</AnalyzerLanguage>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<DefineConstants Condition="'$(LaunchDebugger)' == 'true'">$(DefineConstants);LAUNCH_DEBUGGER</DefineConstants>
<DefineConstants Condition="'$(UpdateBaselines)' == 'true'">$(DefineConstants);UPDATE_BASELINES</DefineConstants>
</PropertyGroup>

<PropertyGroup Condition="'$(DevBuild)' == 'true'">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Reflection;
using System.Threading;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Operations;
Expand Down Expand Up @@ -170,33 +172,56 @@ public InvocationLocationInfo(MethodsToGen interceptor, IInvocationOperation inv
{
Debug.Assert(BinderInvocation.IsBindingOperation(invocation));

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

SyntaxTree operationSyntaxTree = invocation.Syntax.SyntaxTree;
TextSpan memberNameSpan = memberAccessExprSyntax.Name.Span;
FileLinePositionSpan linePosSpan = operationSyntaxTree.GetLineSpan(memberNameSpan);

Interceptor = interceptor;
LineNumber = linePosSpan.StartLinePosition.Line + 1;
CharacterNumber = linePosSpan.StartLinePosition.Character + 1;
FilePath = GetInterceptorFilePath();

// Use the same logic used by the interceptors API for resolving the source mapped value of a path.
// https://github.com/dotnet/roslyn/blob/f290437fcc75dad50a38c09e0977cce13a64f5ba/src/Compilers/CSharp/Portable/Compilation/CSharpCompilation.cs#L1063-L1064
string GetInterceptorFilePath()
if (ConfigurationBindingGenerator.InterceptorVersion == 0)
{
SyntaxTree operationSyntaxTree = invocation.Syntax.SyntaxTree;
TextSpan memberNameSpan = memberAccessExprSyntax.Name.Span;
FileLinePositionSpan linePosSpan = operationSyntaxTree.GetLineSpan(memberNameSpan);

Interceptor = interceptor;
LineNumber = linePosSpan.StartLinePosition.Line + 1;
CharacterNumber = linePosSpan.StartLinePosition.Character + 1;
FilePath = GetInterceptorFilePath();

// Use the same logic used by the interceptors API for resolving the source mapped value of a path.
// https://github.com/dotnet/roslyn/blob/f290437fcc75dad50a38c09e0977cce13a64f5ba/src/Compilers/CSharp/Portable/Compilation/CSharpCompilation.cs#L1063-L1064
string GetInterceptorFilePath()
{
SourceReferenceResolver? sourceReferenceResolver = invocation.SemanticModel?.Compilation.Options.SourceReferenceResolver;
return sourceReferenceResolver?.NormalizePath(operationSyntaxTree.FilePath, baseFilePath: null) ?? operationSyntaxTree.FilePath;
}
}
else
{
SourceReferenceResolver? sourceReferenceResolver = invocation.SemanticModel?.Compilation.Options.SourceReferenceResolver;
return sourceReferenceResolver?.NormalizePath(operationSyntaxTree.FilePath, baseFilePath: null) ?? operationSyntaxTree.FilePath;
Debug.Assert(ConfigurationBindingGenerator.InterceptorVersion == 1);
Interceptor = interceptor;
InterceptableLocation = ConfigurationBindingGenerator.GetInterceptableLocationFunc(invocation.SemanticModel, invocationExpressionSyntax, default(CancellationToken));
}
}

public MethodsToGen Interceptor { get; }

// Used with v0 interceptor approach:
public string FilePath { get; }
public int LineNumber { get; }
public int CharacterNumber { get; }

// Used with v1 interceptor approach:
private object? InterceptableLocation { get; }

public string InterceptableLocationGetDisplayLocation() => InterceptableLocation is null ? "" :
(string)ConfigurationBindingGenerator.InterceptableLocationVersionGetDisplayLocation.Invoke(InterceptableLocation, parameters: null);

public string InterceptableLocationData => InterceptableLocation is null ? "" :
(string)ConfigurationBindingGenerator.InterceptableLocationDataGetter.Invoke(InterceptableLocation, parameters: null);

public int InterceptableLocationVersion => InterceptableLocation is null ? 0 :
(int)ConfigurationBindingGenerator.InterceptableLocationVersionGetter.Invoke(InterceptableLocation, parameters: null);
}
}
Loading

0 comments on commit 0413759

Please sign in to comment.