File: Specs\InterceptorInfo.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.

using System;
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;
using Microsoft.CodeAnalysis.Text;
using SourceGenerators;

namespace Microsoft.Extensions.Configuration.Binder.SourceGeneration
{
    public sealed record InterceptorInfo
    {
        public required MethodsToGen MethodsToGen { get; init; }

        public required ImmutableEquatableArray<TypedInterceptorInvocationInfo>? ConfigBinder_Bind_instance { get; init; }
        public required ImmutableEquatableArray<TypedInterceptorInvocationInfo>? ConfigBinder_Bind_instance_BinderOptions { get; init; }
        public required ImmutableEquatableArray<TypedInterceptorInvocationInfo>? ConfigBinder_Bind_key_instance { get; init; }


        public required ImmutableEquatableArray<InvocationLocationInfo>? ConfigBinder { get; init; }
        public required ImmutableEquatableArray<InvocationLocationInfo>? OptionsBuilderExt { get; init; }
        public required ImmutableEquatableArray<InvocationLocationInfo>? ServiceCollectionExt { get; init; }

        public IEnumerable<InvocationLocationInfo>? GetInfo(MethodsToGen interceptor)
        {
            Debug.Assert((MethodsToGen.ConfigBinder_Bind & interceptor) is 0);

            ImmutableEquatableArray<InvocationLocationInfo>? infoList;
            if (((MethodsToGen.ConfigBinder_Any & ~MethodsToGen.ConfigBinder_Bind) & interceptor) is not 0)
            {
                infoList = ConfigBinder;
            }
            else if ((MethodsToGen.OptionsBuilderExt_Any & interceptor) is not 0)
            {
                infoList = OptionsBuilderExt;
            }
            else
            {
                Debug.Assert((MethodsToGen.ServiceCollectionExt_Any & interceptor) is not 0);
                infoList = ServiceCollectionExt;
            }

            return infoList?.Where(i => i.Interceptor == interceptor);
        }

        internal sealed class Builder
        {
            private TypedInterceptorInfoBuildler? _configBinder_InfoBuilder_Bind_instance;
            private TypedInterceptorInfoBuildler? _configBinder_InfoBuilder_Bind_instance_BinderOptions;
            private TypedInterceptorInfoBuildler? _configBinder_InfoBuilder_Bind_key_instance;

            private List<InvocationLocationInfo>? _interceptors_configBinder;
            private List<InvocationLocationInfo>? _interceptors_OptionsBuilderExt;
            private List<InvocationLocationInfo>? _interceptors_serviceCollectionExt;

            public MethodsToGen MethodsToGen { get; set; }

            public void RegisterInterceptor_ConfigBinder_Bind(MethodsToGen overload, ComplexTypeSpec type, IInvocationOperation invocation)
            {
                Debug.Assert((MethodsToGen.ConfigBinder_Bind & overload) is not 0);

                switch (overload)
                {
                    case MethodsToGen.ConfigBinder_Bind_instance:
                        RegisterInterceptor(ref _configBinder_InfoBuilder_Bind_instance);
                        break;
                    case MethodsToGen.ConfigBinder_Bind_instance_BinderOptions:
                        RegisterInterceptor(ref _configBinder_InfoBuilder_Bind_instance_BinderOptions);
                        break;
                    case MethodsToGen.ConfigBinder_Bind_key_instance:
                        RegisterInterceptor(ref _configBinder_InfoBuilder_Bind_key_instance);
                        break;
                }

                MethodsToGen |= overload;

                void RegisterInterceptor(ref TypedInterceptorInfoBuildler? infoBuilder)
                {
                    infoBuilder ??= new TypedInterceptorInfoBuildler();
                    infoBuilder.RegisterInterceptor(overload, type, invocation);
                }
            }

            public void RegisterInterceptor(MethodsToGen overload, IInvocationOperation operation)
            {
                Debug.Assert((MethodsToGen.ConfigBinder_Bind & overload) is 0);

                if (((MethodsToGen.ConfigBinder_Any & ~MethodsToGen.ConfigBinder_Bind) & overload) is not 0)
                {
                    RegisterInterceptor(ref _interceptors_configBinder);
                }
                else if ((MethodsToGen.OptionsBuilderExt_Any & overload) is not 0)
                {
                    RegisterInterceptor(ref _interceptors_OptionsBuilderExt);
                }
                else
                {
                    Debug.Assert((MethodsToGen.ServiceCollectionExt_Any & overload) is not 0);
                    RegisterInterceptor(ref _interceptors_serviceCollectionExt);
                }

                MethodsToGen |= overload;

                void RegisterInterceptor(ref List<InvocationLocationInfo>? infoList)
                {
                    infoList ??= new List<InvocationLocationInfo>();
                    infoList.Add(new InvocationLocationInfo(overload, operation));
                }
            }

            public InterceptorInfo ToIncrementalValue() =>
                new InterceptorInfo
                {
                    MethodsToGen = MethodsToGen,

                    ConfigBinder = _interceptors_configBinder?.ToImmutableEquatableArray(),
                    OptionsBuilderExt = _interceptors_OptionsBuilderExt?.ToImmutableEquatableArray(),
                    ServiceCollectionExt = _interceptors_serviceCollectionExt?.ToImmutableEquatableArray(),

                    ConfigBinder_Bind_instance = _configBinder_InfoBuilder_Bind_instance?.ToIncrementalValue(),
                    ConfigBinder_Bind_instance_BinderOptions = _configBinder_InfoBuilder_Bind_instance_BinderOptions?.ToIncrementalValue(),
                    ConfigBinder_Bind_key_instance = _configBinder_InfoBuilder_Bind_key_instance?.ToIncrementalValue(),
                };
        }
    }

    internal sealed class TypedInterceptorInfoBuildler
    {
        private readonly Dictionary<ComplexTypeSpec, TypedInterceptorInvocationInfo.Builder> _invocationInfoBuilderCache = new();

        public void RegisterInterceptor(MethodsToGen overload, ComplexTypeSpec type, IInvocationOperation invocation)
        {
            if (!_invocationInfoBuilderCache.TryGetValue(type, out TypedInterceptorInvocationInfo.Builder? invocationInfoBuilder))
            {
                _invocationInfoBuilderCache[type] = invocationInfoBuilder = new TypedInterceptorInvocationInfo.Builder(overload, type);
            }

            invocationInfoBuilder.RegisterInvocation(invocation);
        }

        public ImmutableEquatableArray<TypedInterceptorInvocationInfo>? ToIncrementalValue() =>
            _invocationInfoBuilderCache.Values
            .Select(b => b.ToIncrementalValue())
            .ToImmutableEquatableArray();
    }

    public sealed record TypedInterceptorInvocationInfo(ComplexTypeSpec TargetType, ImmutableEquatableArray<InvocationLocationInfo> Locations)
    {
        public sealed class Builder(MethodsToGen Overload, ComplexTypeSpec TargetType)
        {
            private readonly List<InvocationLocationInfo> _infoList = new();

            public void RegisterInvocation(IInvocationOperation invocation) =>
                _infoList.Add(new InvocationLocationInfo(Overload, invocation));

            public TypedInterceptorInvocationInfo ToIncrementalValue() => new(
                TargetType,
                Locations: _infoList.ToImmutableEquatableArray());
        }
    }

    public sealed record InvocationLocationInfo
    {
        public InvocationLocationInfo(MethodsToGen interceptor, IInvocationOperation invocation)
        {
            Debug.Assert(BinderInvocation.IsBindingOperation(invocation));

            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));
            }

            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
            {
                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);
    }
}