Skip to content

Commit

Permalink
Use Roslyn interceptors feature in binder gen (#90340)
Browse files Browse the repository at this point in the history
* Use Roslyn interceptors feature in binder gen

* Fix polymorphic issue and address feedback

* Fix source build issue

* Revert changes to options gen

* Fix
  • Loading branch information
layomia authored Aug 19, 2023
1 parent a429b0c commit a5ff66c
Show file tree
Hide file tree
Showing 67 changed files with 1,580 additions and 1,189 deletions.
2 changes: 2 additions & 0 deletions docs/project/list-of-diagnostics.md
Original file line number Diff line number Diff line change
Expand Up @@ -270,3 +270,5 @@ The diagnostic id values reserved for .NET Libraries analyzer warnings are `SYSL
| Suppression ID | Suppressed Diagnostic ID | Description |
| :----------------------- | :----------------------- | :---------- |
| __`SYSLIBSUPPRESS0001`__ | CA1822 | Do not offer to make methods static when the methods need to be instance methods for a custom marshaller shape. |
| __`SYSLIBSUPPRESS0002`__ | IL2026 | ConfigurationBindingGenerator: suppress RequiresUnreferencedCode diagnostic for binding call that has been intercepted by a generated static variant. |
| __`SYSLIBSUPPRESS0003`__ | IL3050 | ConfigurationBindingGenerator: suppress RequiresDynamicCode diagnostic for binding call that has been intercepted by a generated static variant. |
1 change: 1 addition & 0 deletions eng/SourceBuildPrebuiltBaseline.xml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
<UsagePattern IdentityGlob="System.Composition*/*7.*" />
<UsagePattern IdentityGlob="Microsoft.CodeAnalysis*/*4.4.*" />
<UsagePattern IdentityGlob="Microsoft.CodeAnalysis*/*4.5.*" />
<UsagePattern IdentityGlob="Microsoft.CodeAnalysis*/*4.7.*" />

<!-- Allowed and pinned to major version due to https://github.com/dotnet/source-build/issues/3228 -->
<UsagePattern IdentityGlob="Microsoft.NETCore.App.Crossgen2.linux-x64/*8.*" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// The .NET Foundation licenses this file to you under the MIT license.

using System;
using System.Collections.Immutable;
using System.Diagnostics;
using System.Text.RegularExpressions;
using Microsoft.CodeAnalysis;
Expand All @@ -17,7 +18,6 @@ private sealed partial class Emitter
private readonly SourceGenerationSpec _sourceGenSpec;

private bool _emitBlankLineBeforeNextStatement;
private bool _useFullyQualifiedNames;
private int _valueSuffixIndex;

private static readonly Regex s_arrayBracketsRegex = new(Regex.Escape("[]"));
Expand All @@ -32,7 +32,7 @@ public Emitter(SourceProductionContext context, SourceGenerationSpec sourceGenSp

public void Emit()
{
if (!ShouldEmitBinders())
if (!ShouldEmitBindingExtensions())
{
return;
}
Expand All @@ -42,17 +42,26 @@ public void Emit()
#nullable enable
#pragma warning disable CS0612, CS0618 // Suppress warnings about [Obsolete] member usage in generated code.
""");
_writer.WriteLine();

_useFullyQualifiedNames = true;
EmitBinder_Extensions_IConfiguration();
EmitBinder_Extensions_OptionsBuilder();
EmitBinder_Extensions_IServiceCollection();
EmitInterceptsLocationAttrDecl();

EmitStartBlock($"namespace {ProjectName}");
EmitUsingStatements();

_writer.WriteLine();
EmitStartBlock($$"""
{{Expression.GeneratedCodeAnnotation}}
file static class {{Identifier.BindingExtensions}}
""");
EmitBindingExtensions_IConfiguration();
EmitBindingExtensions_OptionsBuilder();
EmitBindingExtensions_IServiceCollection();
EmitCoreBindingHelpers();
EmitEndBlock(); // BindingExtensions class

_useFullyQualifiedNames = false;
Emit_CoreBindingHelper();
EmitEndBlock(); // Binding namespace.

_context.AddSource($"{Identifier.GeneratedConfigurationBinder}.g.cs", _writer.ToSourceText());
_context.AddSource($"{Identifier.BindingExtensions}.g.cs", _writer.ToSourceText());
}

private void EmitBindCoreCall(
Expand All @@ -74,7 +83,7 @@ private void EmitBindCoreCall(
if (initKind is InitializationKind.AssignmentWithNullCheck)
{
Debug.Assert(!type.IsValueType);
_writer.WriteLine($"{type.MinimalDisplayString}? {tempIdentifier} = {memberAccessExpr};");
_writer.WriteLine($"{type.DisplayString}? {tempIdentifier} = {memberAccessExpr};");
EmitBindCoreCall(tempIdentifier, InitializationKind.AssignmentWithNullCheck);
}
else if (initKind is InitializationKind.None && type.IsValueType)
Expand All @@ -89,9 +98,7 @@ private void EmitBindCoreCall(

void EmitBindCoreCall(string objExpression, InitializationKind initKind)
{
string methodDisplayString = GetHelperMethodDisplayString(nameof(MethodsToGen_CoreBindingHelper.BindCore));
string bindCoreCall = $@"{methodDisplayString}({configArgExpr}, ref {objExpression}, {Identifier.binderOptions});";

string bindCoreCall = $@"{nameof(MethodsToGen_CoreBindingHelper.BindCore)}({configArgExpr}, ref {objExpression}, {Identifier.binderOptions});";
EmitObjectInit(objExpression, initKind);
_writer.WriteLine(bindCoreCall);
writeOnSuccess?.Invoke(objExpression);
Expand Down Expand Up @@ -127,12 +134,11 @@ private void EmitBindLogicFromString(
}
else if (typeKind is StringParsableTypeKind.Enum)
{
parsedValueExpr = $"ParseEnum<{type.MinimalDisplayString}>({stringValueToParse_Expr}, () => {sectionPathExpr})";
parsedValueExpr = $"ParseEnum<{type.DisplayString}>({stringValueToParse_Expr}, () => {sectionPathExpr})";
}
else
{
string helperMethodDisplayString = GetHelperMethodDisplayString(type.ParseMethodName);
parsedValueExpr = $"{helperMethodDisplayString}({stringValueToParse_Expr}, () => {sectionPathExpr})";
parsedValueExpr = $"{type.ParseMethodName}({stringValueToParse_Expr}, () => {sectionPathExpr})";
}

if (!checkForNullSectionValue)
Expand All @@ -156,7 +162,7 @@ private bool EmitObjectInit(TypeSpec type, string memberAccessExpr, Initializati
string initExpr;
CollectionSpec? collectionType = type as CollectionSpec;

string effectiveDisplayString = GetTypeDisplayString(type);
string effectiveDisplayString = type.DisplayString;
if (collectionType is not null)
{
if (collectionType is EnumerableSpec { InitializationStrategy: InitializationStrategy.Array })
Expand All @@ -165,7 +171,7 @@ private bool EmitObjectInit(TypeSpec type, string memberAccessExpr, Initializati
}
else
{
effectiveDisplayString = GetTypeDisplayString(collectionType.ConcreteType ?? collectionType);
effectiveDisplayString = (collectionType.ConcreteType ?? collectionType).DisplayString;
initExpr = $"new {effectiveDisplayString}()";
}
}
Expand Down Expand Up @@ -215,36 +221,41 @@ private bool EmitObjectInit(TypeSpec type, string memberAccessExpr, Initializati
return true;
}

private void EmitCastToIConfigurationSection()
private void EmitInterceptsLocationAttrDecl()
{
string sectionTypeDisplayString;
string exceptionTypeDisplayString;
if (_useFullyQualifiedNames)
{
sectionTypeDisplayString = "global::Microsoft.Extensions.Configuration.IConfigurationSection";
exceptionTypeDisplayString = FullyQualifiedDisplayString.InvalidOperationException;
}
else
{
sectionTypeDisplayString = Identifier.IConfigurationSection;
exceptionTypeDisplayString = nameof(InvalidOperationException);
}

_writer.WriteLine();
_writer.WriteLine($$"""
if ({{Identifier.configuration}} is not {{sectionTypeDisplayString}} {{Identifier.section}})
namespace System.Runtime.CompilerServices
{
throw new {{exceptionTypeDisplayString}}();
using System;
using System.CodeDom.Compiler;

{{Expression.GeneratedCodeAnnotation}}
[AttributeUsage(AttributeTargets.Method, AllowMultiple = true)]
file sealed class InterceptsLocationAttribute : Attribute
{
public InterceptsLocationAttribute(string filePath, int line, int column)
{
}
}
}
""");
_writer.WriteLine();
}

private void EmitUsingStatements()
{
foreach (string @namespace in _sourceGenSpec.Namespaces.ToImmutableSortedSet())
{
_writer.WriteLine($"using {@namespace};");
}
}

private void EmitIConfigurationHasValueOrChildrenCheck(bool voidReturn)
{
string returnPostfix = voidReturn ? string.Empty : " null";
string methodDisplayString = GetHelperMethodDisplayString(Identifier.HasValueOrChildren);

_writer.WriteLine($$"""
if (!{{methodDisplayString}}({{Identifier.configuration}}))
if (!{{Identifier.HasValueOrChildren}}({{Identifier.configuration}}))
{
return{{returnPostfix}};
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
// 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.Collections.Immutable;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Operations;

namespace Microsoft.Extensions.Configuration.Binder.SourceGeneration
{
Expand Down Expand Up @@ -147,7 +149,7 @@ type.TypeKind is TypeKind.TypeParameter or TypeKind.Pointer or TypeKind.Error ||
{
// List<string> is used in generated code as a temp holder for formatting
// an error for config properties that don't map to object properties.
_sourceGenSpec.TypeNamespaces.Add("System.Collections.Generic");
_sourceGenSpec.Namespaces.Add("System.Collections.Generic");

spec = CreateObjectSpec(namedType);
}
Expand All @@ -169,32 +171,54 @@ type.TypeKind is TypeKind.TypeParameter or TypeKind.Pointer or TypeKind.Error ||
string @namespace = spec.Namespace;
if (@namespace is not null and not "<global namespace>")
{
_sourceGenSpec.TypeNamespaces.Add(@namespace);
_sourceGenSpec.Namespaces.Add(@namespace);
}

return _createdSpecs[type] = spec;
}

private void RegisterTypeForMethodGen(MethodsToGen_CoreBindingHelper method, TypeSpec type)
private void RegisterTypeForBindCoreMainGen(TypeSpec typeSpec)
{
if (!_sourceGenSpec.TypesForGen_CoreBindingHelper_Methods.TryGetValue(method, out HashSet<TypeSpec>? types))
if (typeSpec.NeedsMemberBinding)
{
_sourceGenSpec.TypesForGen_CoreBindingHelper_Methods[method] = types = new HashSet<TypeSpec>();
RegisterTypeForMethodGen(MethodsToGen_CoreBindingHelper.BindCoreMain, typeSpec);
RegisterTypeForBindCoreGen(typeSpec);
_sourceGenSpec.MethodsToGen_CoreBindingHelper |= MethodsToGen_CoreBindingHelper.AsConfigWithChildren;
}

types.Add(type);
_sourceGenSpec.MethodsToGen_CoreBindingHelper |= method;
}

private void RegisterTypeForBindCoreUntypedGen(TypeSpec typeSpec)
private void RegisterTypeForBindCoreGen(TypeSpec typeSpec)
{
if (typeSpec.NeedsMemberBinding)
{
RegisterTypeForMethodGen(MethodsToGen_CoreBindingHelper.BindCore, typeSpec);
RegisterTypeForMethodGen(MethodsToGen_CoreBindingHelper.BindCoreUntyped, typeSpec);
}
}

private void RegisterTypeForGetCoreGen(TypeSpec typeSpec)
{
RegisterTypeForMethodGen(MethodsToGen_CoreBindingHelper.GetCore, typeSpec);
_sourceGenSpec.MethodsToGen_CoreBindingHelper |= MethodsToGen_CoreBindingHelper.AsConfigWithChildren;
}

private void RegisterTypeForMethodGen(MethodsToGen_CoreBindingHelper method, TypeSpec type)
{
if (!_sourceGenSpec.TypesForGen_CoreBindingHelper_Methods.TryGetValue(method, out HashSet<TypeSpec>? types))
{
_sourceGenSpec.TypesForGen_CoreBindingHelper_Methods[method] = types = new HashSet<TypeSpec>();
}

types.Add(type);
_sourceGenSpec.MethodsToGen_CoreBindingHelper |= method;
}

/// <summary>
/// Registers interceptors for root binding methods, except for ConfigurationBinder.Bind,
/// which is handled by <see cref="RegisterAsInterceptor_ConfigBinder_BindMethod"/>
/// </summary>
private void RegisterAsInterceptor(Enum method, IInvocationOperation operation) =>
_sourceGenSpec.InterceptionInfo.RegisterCacheEntry(method, new InterceptorLocationInfo(operation));

private static bool IsNullable(ITypeSymbol type, [NotNullWhen(true)] out ITypeSymbol? underlyingType)
{
if (type is INamedTypeSymbol { IsGenericType: true } genericType &&
Expand Down Expand Up @@ -335,7 +359,7 @@ private bool TryGetTypeSpec(ITypeSymbol type, DiagnosticDescriptor descriptor, o

// We want a BindCore method for List<TElement> as a temp holder for the array values. We know the element type is supported.
EnumerableSpec listSpec = (GetOrCreateTypeSpec(_typeSymbols.List.Construct(arrayType.ElementType)) as EnumerableSpec)!;
RegisterTypeForMethodGen(MethodsToGen_CoreBindingHelper.BindCore, listSpec);
RegisterTypeForBindCoreGen(listSpec);

EnumerableSpec spec = new EnumerableSpec(arrayType)
{
Expand All @@ -347,7 +371,7 @@ private bool TryGetTypeSpec(ITypeSymbol type, DiagnosticDescriptor descriptor, o
};

Debug.Assert(spec.CanInitialize);
RegisterTypeForMethodGen(MethodsToGen_CoreBindingHelper.BindCore, spec);
RegisterTypeForBindCoreGen(spec);

return spec;
}
Expand Down Expand Up @@ -383,7 +407,7 @@ private bool IsSupportedArrayType(ITypeSymbol type)

if (spec is not null)
{
RegisterTypeForMethodGen(MethodsToGen_CoreBindingHelper.BindCore, spec);
RegisterTypeForBindCoreGen(spec);
spec.InitExceptionMessage ??= spec.ElementType.InitExceptionMessage;
}

Expand Down Expand Up @@ -442,7 +466,7 @@ private DictionarySpec CreateDictionarySpec(INamedTypeSymbol type, ITypeSymbol k
constructionStrategy = InitializationStrategy.ToEnumerableMethod;
populationStrategy = CollectionPopulationStrategy.Cast_Then_Add;
toEnumerableMethodCall = "ToDictionary(pair => pair.Key, pair => pair.Value)";
_sourceGenSpec.TypeNamespaces.Add("System.Linq");
_sourceGenSpec.Namespaces.Add("System.Linq");
}
else
{
Expand Down Expand Up @@ -711,7 +735,7 @@ private DictionarySpec CreateDictionarySpec(INamedTypeSymbol type, ITypeSymbol k

if (objectSpec.NeedsMemberBinding)
{
RegisterTypeForMethodGen(MethodsToGen_CoreBindingHelper.BindCore, objectSpec);
RegisterTypeForBindCoreGen(objectSpec);
}

return objectSpec;
Expand Down Expand Up @@ -890,4 +914,19 @@ private void RegisterTypeDiagnostic(ITypeSymbol causingType, InvocationDiagnosti
}
}
}

public static class ParserExtensions
{
public static void RegisterCacheEntry<TKey, TValue, TEntry>(this Dictionary<TKey, TValue> cache, TKey key, TEntry entry)
where TKey : notnull
where TValue : ICollection<TEntry>, new()
{
if (!cache.TryGetValue(key, out TValue? entryCollection))
{
cache[key] = entryCollection = new TValue();
}

entryCollection.Add(entry);
}
}
}
Loading

0 comments on commit a5ff66c

Please sign in to comment.