Skip to content
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

Merged
merged 6 commits into from
Aug 19, 2023
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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})";
Copy link
Member

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.

}
else
{
string helperMethodDisplayString = GetHelperMethodDisplayString(type.ParseMethodName);
parsedValueExpr = $"{helperMethodDisplayString}({stringValueToParse_Expr}, () => {sectionPathExpr})";
parsedValueExpr = $"{type.ParseMethodName}({stringValueToParse_Expr}, () => {sectionPathExpr})";
Copy link
Member

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.

}

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");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The record type InvocationDiagnosticInfo at the start of this file also has broken equality semantics. Can't comment on it directly.

Copy link
Contributor

Choose a reason for hiding this comment

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