-
Notifications
You must be signed in to change notification settings - Fork 4.7k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[release/8.0-rc1] Use Roslyn interceptors feature in binder gen #90835
Changes from 5 commits
b764c56
8bd9571
e17fe3e
4596e82
72bdf7a
5e52ba8
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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; | ||
|
@@ -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("[]")); | ||
|
@@ -32,7 +32,7 @@ public Emitter(SourceProductionContext context, SourceGenerationSpec sourceGenSp | |
|
||
public void Emit() | ||
{ | ||
if (!ShouldEmitBinders()) | ||
if (!ShouldEmitBindingExtensions()) | ||
{ | ||
return; | ||
} | ||
|
@@ -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( | ||
|
@@ -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) | ||
|
@@ -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); | ||
|
@@ -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})"; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. When emitting lambdas in generated code consider using fully specified signatures: makes the code less subject to interference and reduces build / IDE analysis time. |
||
} | ||
|
||
if (!checkForNullSectionValue) | ||
|
@@ -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 }) | ||
|
@@ -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}()"; | ||
} | ||
} | ||
|
@@ -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}}; | ||
} | ||
|
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 | ||
{ | ||
|
@@ -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"); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thanks, I have pending 9.0 work to fix this #89587. |
||
|
||
spec = CreateObjectSpec(namedType); | ||
} | ||
|
@@ -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 && | ||
|
@@ -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) | ||
{ | ||
|
@@ -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; | ||
} | ||
|
@@ -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; | ||
} | ||
|
||
|
@@ -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 | ||
{ | ||
|
@@ -711,7 +735,7 @@ private DictionarySpec CreateDictionarySpec(INamedTypeSymbol type, ITypeSymbol k | |
|
||
if (objectSpec.NeedsMemberBinding) | ||
{ | ||
RegisterTypeForMethodGen(MethodsToGen_CoreBindingHelper.BindCore, objectSpec); | ||
RegisterTypeForBindCoreGen(objectSpec); | ||
} | ||
|
||
return objectSpec; | ||
|
@@ -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); | ||
} | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When emitting lambdas in generated code consider using fully specified signatures: makes the code less subject to interference and reduces build / IDE analysis time.