| File: ConfigurationBindingGenerator.cs | Web Access |
| Project: src\runtime\src\libraries\Microsoft.Extensions.Configuration.Binder\gen\Microsoft.Extensions.Configuration.Binder.SourceGeneration.csproj (Microsoft.Extensions.Configuration.Binder.SourceGeneration) |
// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. //#define LAUNCH_DEBUGGER using System; using System.Diagnostics; using System.Reflection; using System.Threading; using System.Collections.Immutable; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; namespace Microsoft.Extensions.Configuration.Binder.SourceGeneration { /// <summary> /// Generates source code to optimize binding with ConfigurationBinder. /// </summary> [Generator] public sealed partial class ConfigurationBindingGenerator : IIncrementalGenerator { private static readonly string ProjectName = Emitter.s_assemblyName.Name!; public const string GenSpecTrackingName = nameof(SourceGenerationSpec); public void Initialize(IncrementalGeneratorInitializationContext context) { #if LAUNCH_DEBUGGER if (!System.Diagnostics.Debugger.IsAttached) { System.Diagnostics.Debugger.Launch(); } #endif IncrementalValueProvider<CompilationData?> compilationData = context.CompilationProvider .Select((compilation, _) => compilation.Options is CSharpCompilationOptions options ? new CompilationData((CSharpCompilation)compilation) : null); IncrementalValueProvider<(SourceGenerationSpec?, ImmutableArray<Diagnostic>)> genSpec = context.SyntaxProvider .CreateSyntaxProvider( (node, _) => BinderInvocation.IsCandidateSyntaxNode(node), BinderInvocation.Create) .Where(invocation => invocation is not null) .Collect() .Combine(compilationData) .Select((tuple, cancellationToken) => { if (tuple.Right is not CompilationData compilationData) { return (null, ImmutableArray<Diagnostic>.Empty); } try { Parser parser = new(compilationData); SourceGenerationSpec? spec = parser.GetSourceGenerationSpec(tuple.Left, cancellationToken); ImmutableArray<Diagnostic> diagnostics = parser.Diagnostics is { } diags ? diags.ToImmutableArray() : ImmutableArray<Diagnostic>.Empty; return (spec, diagnostics); } catch (Exception ex) { throw ex; } }) .WithTrackingName(GenSpecTrackingName); // Project the combined pipeline result to just the equatable model, discarding diagnostics. // SourceGenerationSpec implements value equality, so Roslyn's Select operator will compare // successive model snapshots and only propagate changes downstream when the model structurally // differs. This ensures source generation is fully incremental: re-emitting code only when // the binding spec actually changes, not on every keystroke or positional shift. IncrementalValueProvider<SourceGenerationSpec?> sourceGenerationSpec = genSpec.Select(static (t, _) => t.Item1); context.RegisterSourceOutput(sourceGenerationSpec, EmitSource); // Project to just the diagnostics, discarding the model. ImmutableArray<Diagnostic> does not // implement value equality, so Roslyn's incremental pipeline uses reference equality for these // values — the callback fires on every compilation change. This is by design: diagnostic // emission is cheap, and we need fresh SourceLocation instances that are pragma-suppressible // (cf. https://github.com/dotnet/runtime/issues/92509). // No source code is generated from this pipeline — it exists solely to report diagnostics. IncrementalValueProvider<ImmutableArray<Diagnostic>> diagnostics = genSpec.Select(static (t, _) => t.Item2); context.RegisterSourceOutput(diagnostics, EmitDiagnostics); 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> /// Instrumentation helper for unit tests. /// </summary> public Action<SourceGenerationSpec>? OnSourceEmitting { get; init; } private static void EmitDiagnostics(SourceProductionContext context, ImmutableArray<Diagnostic> diagnostics) { foreach (Diagnostic diagnostic in diagnostics) { context.ReportDiagnostic(diagnostic); } } private void EmitSource(SourceProductionContext sourceProductionContext, SourceGenerationSpec? spec) { if (spec is not null) { OnSourceEmitting?.Invoke(spec); Emitter emitter = new(spec); emitter.Emit(sourceProductionContext); } } internal sealed class CompilationData { public bool LanguageVersionIsSupported { get; } public KnownTypeSymbols? TypeSymbols { get; } public CompilationData(CSharpCompilation compilation) { // We don't have a CSharp21 value available yet. Polyfill the value here for forward compat, rather than use the LanguageVersion.Preview enum value. // https://github.com/dotnet/roslyn/blob/168689931cb4e3150641ec2fb188a64ce4b3b790/src/Compilers/CSharp/Portable/LanguageVersion.cs#L218-L232 const int LangVersion_CSharp12 = 1200; LanguageVersionIsSupported = (int)compilation.LanguageVersion >= LangVersion_CSharp12; if (LanguageVersionIsSupported) { TypeSymbols = new KnownTypeSymbols(compilation); } } } } }