未验证 提交 ee240567 编写于 作者: G github-actions[bot] 提交者: GitHub

[release/8.0] Options Source Gen Fixes (#91432)

* Options Source Gen Fixes

* Remove unnecessary interpolated string usage

* Address the feedback

* Remove repeated word in the comment

---------
Co-authored-by: NTarek Mahmoud Sayed <tarekms@microsoft.com>
上级 cd8b2cb8
......@@ -3,10 +3,12 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Threading;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.DotnetRuntime.Extensions;
namespace Microsoft.Extensions.Options.Generators
{
......@@ -25,6 +27,7 @@ internal sealed class Emitter : EmitterBase
private string _staticValidationAttributeHolderClassFQN;
private string _staticValidatorHolderClassFQN;
private string _modifier;
private string _TryGetValueNullableAnnotation;
private sealed record StaticFieldInfo(string FieldTypeFQN, int FieldOrder, string FieldName, IList<string> InstantiationLines);
......@@ -37,13 +40,14 @@ public Emitter(Compilation compilation, bool emitPreamble = true) : base(emitPre
else
{
_modifier = "internal";
string suffix = $"_{new Random().Next():X8}";
string suffix = $"_{GetNonRandomizedHashCode(compilation.SourceModule.Name):X8}";
_staticValidationAttributeHolderClassName += suffix;
_staticValidatorHolderClassName += suffix;
}
_staticValidationAttributeHolderClassFQN = $"global::{StaticFieldHolderClassesNamespace}.{_staticValidationAttributeHolderClassName}";
_staticValidatorHolderClassFQN = $"global::{StaticFieldHolderClassesNamespace}.{_staticValidatorHolderClassName}";
_TryGetValueNullableAnnotation = GetNullableAnnotationStringForTryValidateValueToUseInGeneratedCode(compilation);
}
public string Emit(
......@@ -65,6 +69,31 @@ public Emitter(Compilation compilation, bool emitPreamble = true) : base(emitPre
return Capture();
}
/// <summary>
/// Returns the nullable annotation string to use in the code generation according to the first parameter of
/// <see cref="System.ComponentModel.DataAnnotations.Validator.TryValidateValue(object, ValidationContext, ICollection{ValidationResult}, IEnumerable{ValidationAttribute})"/> is nullable annotated.
/// </summary>
/// <param name="compilation">The <see cref="Compilation"/> to consider for analysis.</param>
/// <returns>"!" if the first parameter is not nullable annotated, otherwise an empty string.</returns>
/// <remarks>
/// In .NET 8.0 we have changed the nullable annotation on first parameter of the method cref="System.ComponentModel.DataAnnotations.Validator.TryValidateValue(object, ValidationContext, ICollection{ValidationResult}, IEnumerable{ValidationAttribute})"/>
/// The source generator need to detect if we need to append "!" to the first parameter of the method call when running on down-level versions.
/// </remarks>
private static string GetNullableAnnotationStringForTryValidateValueToUseInGeneratedCode(Compilation compilation)
{
INamedTypeSymbol? validatorTypeSymbol = compilation.GetBestTypeByMetadataName("System.ComponentModel.DataAnnotations.Validator");
if (validatorTypeSymbol is not null)
{
ImmutableArray<ISymbol> members = validatorTypeSymbol.GetMembers("TryValidateValue");
if (members.Length == 1 && members[0] is IMethodSymbol tryValidateValueMethod)
{
return tryValidateValueMethod.Parameters[0].NullableAnnotation == NullableAnnotation.NotAnnotated ? "!" : string.Empty;
}
}
return "!";
}
private void GenValidatorType(ValidatorType vt, ref Dictionary<string, StaticFieldInfo> staticValidationAttributesDict, ref Dictionary<string, StaticFieldInfo> staticValidatorsDict)
{
if (vt.Namespace.Length > 0)
......@@ -161,7 +190,7 @@ private void GenModelSelfValidationIfNecessary(ValidatedModel modelToValidate)
{
if (modelToValidate.SelfValidates)
{
OutLn($"builder.AddResults(((global::System.ComponentModel.DataAnnotations.IValidatableObject)options).Validate(context));");
OutLn($"(builder ??= new()).AddResults(((global::System.ComponentModel.DataAnnotations.IValidatableObject)options).Validate(context));");
OutLn();
}
}
......@@ -182,8 +211,7 @@ private void GenModelSelfValidationIfNecessary(ValidatedModel modelToValidate)
OutLn($"public {(makeStatic ? "static " : string.Empty)}global::Microsoft.Extensions.Options.ValidateOptionsResult Validate(string? name, {modelToValidate.Name} options)");
OutOpenBrace();
OutLn($"var baseName = (string.IsNullOrEmpty(name) ? \"{modelToValidate.SimpleName}\" : name) + \".\";");
OutLn($"var builder = new global::Microsoft.Extensions.Options.ValidateOptionsResultBuilder();");
OutLn($"global::Microsoft.Extensions.Options.ValidateOptionsResultBuilder? builder = null;");
OutLn($"var context = new global::System.ComponentModel.DataAnnotations.ValidationContext(options);");
int capacity = modelToValidate.MembersToValidate.Max(static vm => vm.ValidationAttributes.Count);
......@@ -199,33 +227,33 @@ private void GenModelSelfValidationIfNecessary(ValidatedModel modelToValidate)
{
if (vm.ValidationAttributes.Count > 0)
{
GenMemberValidation(vm, ref staticValidationAttributesDict, cleanListsBeforeUse);
GenMemberValidation(vm, modelToValidate.SimpleName, ref staticValidationAttributesDict, cleanListsBeforeUse);
cleanListsBeforeUse = true;
OutLn();
}
if (vm.TransValidatorType is not null)
{
GenTransitiveValidation(vm, ref staticValidatorsDict);
GenTransitiveValidation(vm, modelToValidate.SimpleName, ref staticValidatorsDict);
OutLn();
}
if (vm.EnumerationValidatorType is not null)
{
GenEnumerationValidation(vm, ref staticValidatorsDict);
GenEnumerationValidation(vm, modelToValidate.SimpleName, ref staticValidatorsDict);
OutLn();
}
}
GenModelSelfValidationIfNecessary(modelToValidate);
OutLn($"return builder.Build();");
OutLn($"return builder is null ? global::Microsoft.Extensions.Options.ValidateOptionsResult.Success : builder.Build();");
OutCloseBrace();
}
private void GenMemberValidation(ValidatedMember vm, ref Dictionary<string, StaticFieldInfo> staticValidationAttributesDict, bool cleanListsBeforeUse)
private void GenMemberValidation(ValidatedMember vm, string modelName, ref Dictionary<string, StaticFieldInfo> staticValidationAttributesDict, bool cleanListsBeforeUse)
{
OutLn($"context.MemberName = \"{vm.Name}\";");
OutLn($"context.DisplayName = baseName + \"{vm.Name}\";");
OutLn($"context.DisplayName = string.IsNullOrEmpty(name) ? \"{modelName}.{vm.Name}\" : $\"{{name}}.{vm.Name}\";");
if (cleanListsBeforeUse)
{
......@@ -239,9 +267,9 @@ private void GenMemberValidation(ValidatedMember vm, ref Dictionary<string, Stat
OutLn($"validationAttributes.Add({_staticValidationAttributeHolderClassFQN}.{staticValidationAttributeInstance.FieldName});");
}
OutLn($"if (!global::System.ComponentModel.DataAnnotations.Validator.TryValidateValue(options.{vm.Name}!, context, validationResults, validationAttributes))");
OutLn($"if (!global::System.ComponentModel.DataAnnotations.Validator.TryValidateValue(options.{vm.Name}{_TryGetValueNullableAnnotation}, context, validationResults, validationAttributes))");
OutOpenBrace();
OutLn($"builder.AddResults(validationResults);");
OutLn($"(builder ??= new()).AddResults(validationResults);");
OutCloseBrace();
}
......@@ -305,7 +333,7 @@ private StaticFieldInfo GetOrAddStaticValidationAttribute(ref Dictionary<string,
return staticValidationAttributeInstance;
}
private void GenTransitiveValidation(ValidatedMember vm, ref Dictionary<string, StaticFieldInfo> staticValidatorsDict)
private void GenTransitiveValidation(ValidatedMember vm, string modelName, ref Dictionary<string, StaticFieldInfo> staticValidatorsDict)
{
string callSequence;
if (vm.TransValidateTypeIsSynthetic)
......@@ -321,20 +349,22 @@ private void GenTransitiveValidation(ValidatedMember vm, ref Dictionary<string,
var valueAccess = (vm.IsNullable && vm.IsValueType) ? ".Value" : string.Empty;
var baseName = $"string.IsNullOrEmpty(name) ? \"{modelName}.{vm.Name}\" : $\"{{name}}.{vm.Name}\"";
if (vm.IsNullable)
{
OutLn($"if (options.{vm.Name} is not null)");
OutOpenBrace();
OutLn($"builder.AddResult({callSequence}.Validate(baseName + \"{vm.Name}\", options.{vm.Name}{valueAccess}));");
OutLn($"(builder ??= new()).AddResult({callSequence}.Validate({baseName}, options.{vm.Name}{valueAccess}));");
OutCloseBrace();
}
else
{
OutLn($"builder.AddResult({callSequence}.Validate(baseName + \"{vm.Name}\", options.{vm.Name}{valueAccess}));");
OutLn($"(builder ??= new()).AddResult({callSequence}.Validate({baseName}, options.{vm.Name}{valueAccess}));");
}
}
private void GenEnumerationValidation(ValidatedMember vm, ref Dictionary<string, StaticFieldInfo> staticValidatorsDict)
private void GenEnumerationValidation(ValidatedMember vm, string modelName, ref Dictionary<string, StaticFieldInfo> staticValidatorsDict)
{
var valueAccess = (vm.IsValueType && vm.IsNullable) ? ".Value" : string.Empty;
var enumeratedValueAccess = (vm.EnumeratedIsNullable && vm.EnumeratedIsValueType) ? ".Value" : string.Empty;
......@@ -365,14 +395,16 @@ private void GenEnumerationValidation(ValidatedMember vm, ref Dictionary<string,
{
OutLn($"if (o is not null)");
OutOpenBrace();
OutLn($"builder.AddResult({callSequence}.Validate(baseName + $\"{vm.Name}[{{count}}]\", o{enumeratedValueAccess}));");
var propertyName = $"string.IsNullOrEmpty(name) ? $\"{modelName}.{vm.Name}[{{count}}]\" : $\"{{name}}.{vm.Name}[{{count}}]\"";
OutLn($"(builder ??= new()).AddResult({callSequence}.Validate({propertyName}, o{enumeratedValueAccess}));");
OutCloseBrace();
if (!vm.EnumeratedMayBeNull)
{
OutLn($"else");
OutOpenBrace();
OutLn($"builder.AddError(baseName + $\"{vm.Name}[{{count}}] is null\");");
var error = $"string.IsNullOrEmpty(name) ? $\"{modelName}.{vm.Name}[{{count}}] is null\" : $\"{{name}}.{vm.Name}[{{count}}] is null\"";
OutLn($"(builder ??= new()).AddError({error});");
OutCloseBrace();
}
......@@ -380,7 +412,8 @@ private void GenEnumerationValidation(ValidatedMember vm, ref Dictionary<string,
}
else
{
OutLn($"builder.AddResult({callSequence}.Validate(baseName + $\"{vm.Name}[{{count++}}]\", o{enumeratedValueAccess}));");
var propertyName = $"string.IsNullOrEmpty(name) ? $\"{modelName}.{vm.Name}[{{count++}}] is null\" : $\"{{name}}.{vm.Name}[{{count++}}] is null\"";
OutLn($"(builder ??= new()).AddResult({callSequence}.Validate({propertyName}, o{enumeratedValueAccess}));");
}
OutCloseBrace();
......@@ -405,5 +438,19 @@ private StaticFieldInfo GetOrAddStaticValidator(ref Dictionary<string, StaticFie
return staticValidatorInstance;
}
/// <summary>
/// Returns a non-randomized hash code for the given string.
/// We always return a positive value.
/// </summary>
internal static int GetNonRandomizedHashCode(string s)
{
uint result = 2166136261u;
foreach (char c in s)
{
result = (c ^ result) * 16777619;
}
return Math.Abs((int)result);
}
}
}
......@@ -20,6 +20,7 @@
<ItemGroup>
<Compile Include="$(CoreLibSharedDir)System\Runtime\CompilerServices\IsExternalInit.cs" Link="Common\System\Runtime\CompilerServices\IsExternalInit.cs" />
<Compile Include="$(CommonPath)\Roslyn\GetBestTypeByMetadataName.cs" Link="Common\Roslyn\GetBestTypeByMetadataName.cs" />
<Compile Include="DiagDescriptors.cs" />
<Compile Include="DiagDescriptorsBase.cs" />
<Compile Include="Emitter.cs" />
......
......@@ -240,6 +240,13 @@ private static bool HasOpenGenerics(ITypeSymbol type, out string genericType)
type = ((INamedTypeSymbol)type).TypeArguments[0];
}
// Check first if the type is IEnumerable<T> interface
if (SymbolEqualityComparer.Default.Equals(type.OriginalDefinition, _symbolHolder.GenericIEnumerableSymbol))
{
return ((INamedTypeSymbol)type).TypeArguments[0];
}
// Check first if the type implement IEnumerable<T> interface
foreach (var implementingInterface in type.AllInterfaces)
{
if (SymbolEqualityComparer.Default.Equals(implementingInterface.OriginalDefinition, _compilation.GetSpecialType(SpecialType.System_Collections_Generic_IEnumerable_T)))
......
......@@ -14,7 +14,8 @@ namespace Microsoft.Extensions.Options.Generators
INamedTypeSymbol DataTypeAttributeSymbol,
INamedTypeSymbol ValidateOptionsSymbol,
INamedTypeSymbol IValidatableObjectSymbol,
INamedTypeSymbol GenericIEnumerableSymbol,
INamedTypeSymbol TypeSymbol,
INamedTypeSymbol? ValidateObjectMembersAttributeSymbol,
INamedTypeSymbol? ValidateEnumeratedItemsAttributeSymbol);
INamedTypeSymbol ValidateObjectMembersAttributeSymbol,
INamedTypeSymbol ValidateEnumeratedItemsAttributeSymbol);
}
......@@ -15,19 +15,11 @@ internal static class SymbolLoader
internal const string TypeOfType = "System.Type";
internal const string ValidateObjectMembersAttribute = "Microsoft.Extensions.Options.ValidateObjectMembersAttribute";
internal const string ValidateEnumeratedItemsAttribute = "Microsoft.Extensions.Options.ValidateEnumeratedItemsAttribute";
internal const string GenericIEnumerableType = "System.Collections.Generic.IEnumerable`1";
public static bool TryLoad(Compilation compilation, out SymbolHolder? symbolHolder)
{
INamedTypeSymbol? GetSymbol(string metadataName, bool optional = false)
{
var symbol = compilation.GetTypeByMetadataName(metadataName);
if (symbol == null && !optional)
{
return null;
}
return symbol;
}
INamedTypeSymbol? GetSymbol(string metadataName) => compilation.GetTypeByMetadataName(metadataName);
// required
var optionsValidatorSymbol = GetSymbol(OptionsValidatorAttribute);
......@@ -35,7 +27,10 @@ public static bool TryLoad(Compilation compilation, out SymbolHolder? symbolHold
var dataTypeAttributeSymbol = GetSymbol(DataTypeAttribute);
var ivalidatableObjectSymbol = GetSymbol(IValidatableObjectType);
var validateOptionsSymbol = GetSymbol(IValidateOptionsType);
var genericIEnumerableSymbol = GetSymbol(GenericIEnumerableType);
var typeSymbol = GetSymbol(TypeOfType);
var validateObjectMembersAttribute = GetSymbol(ValidateObjectMembersAttribute);
var validateEnumeratedItemsAttribute = GetSymbol(ValidateEnumeratedItemsAttribute);
#pragma warning disable S1067 // Expressions should not be too complex
if (optionsValidatorSymbol == null ||
......@@ -43,7 +38,10 @@ public static bool TryLoad(Compilation compilation, out SymbolHolder? symbolHold
dataTypeAttributeSymbol == null ||
ivalidatableObjectSymbol == null ||
validateOptionsSymbol == null ||
typeSymbol == null)
genericIEnumerableSymbol == null ||
typeSymbol == null ||
validateObjectMembersAttribute == null ||
validateEnumeratedItemsAttribute == null)
{
symbolHolder = default;
return false;
......@@ -56,11 +54,10 @@ public static bool TryLoad(Compilation compilation, out SymbolHolder? symbolHold
dataTypeAttributeSymbol,
validateOptionsSymbol,
ivalidatableObjectSymbol,
genericIEnumerableSymbol,
typeSymbol,
// optional
GetSymbol(ValidateObjectMembersAttribute, optional: true),
GetSymbol(ValidateEnumeratedItemsAttribute, optional: true));
validateObjectMembersAttribute,
validateEnumeratedItemsAttribute);
return true;
}
......
......@@ -70,31 +70,30 @@ partial struct MyOptionsValidator
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Options.SourceGeneration", "42.42.42.42")]
public global::Microsoft.Extensions.Options.ValidateOptionsResult Validate(string? name, global::HelloWorld.MyOptions options)
{
var baseName = (string.IsNullOrEmpty(name) ? "MyOptions" : name) + ".";
var builder = new global::Microsoft.Extensions.Options.ValidateOptionsResultBuilder();
global::Microsoft.Extensions.Options.ValidateOptionsResultBuilder? builder = null;
var context = new global::System.ComponentModel.DataAnnotations.ValidationContext(options);
var validationResults = new global::System.Collections.Generic.List<global::System.ComponentModel.DataAnnotations.ValidationResult>();
var validationAttributes = new global::System.Collections.Generic.List<global::System.ComponentModel.DataAnnotations.ValidationAttribute>(1);
context.MemberName = "Val1";
context.DisplayName = baseName + "Val1";
context.DisplayName = string.IsNullOrEmpty(name) ? "MyOptions.Val1" : $"{name}.Val1";
validationAttributes.Add(global::__OptionValidationStaticInstances.__Attributes.A1);
if (!global::System.ComponentModel.DataAnnotations.Validator.TryValidateValue(options.Val1!, context, validationResults, validationAttributes))
if (!global::System.ComponentModel.DataAnnotations.Validator.TryValidateValue(options.Val1, context, validationResults, validationAttributes))
{
builder.AddResults(validationResults);
(builder ??= new()).AddResults(validationResults);
}
context.MemberName = "Val2";
context.DisplayName = baseName + "Val2";
context.DisplayName = string.IsNullOrEmpty(name) ? "MyOptions.Val2" : $"{name}.Val2";
validationResults.Clear();
validationAttributes.Clear();
validationAttributes.Add(global::__OptionValidationStaticInstances.__Attributes.A2);
if (!global::System.ComponentModel.DataAnnotations.Validator.TryValidateValue(options.Val2!, context, validationResults, validationAttributes))
if (!global::System.ComponentModel.DataAnnotations.Validator.TryValidateValue(options.Val2, context, validationResults, validationAttributes))
{
builder.AddResults(validationResults);
(builder ??= new()).AddResults(validationResults);
}
return builder.Build();
return builder is null ? global::Microsoft.Extensions.Options.ValidateOptionsResult.Success : builder.Build();
}
}
}
......
......@@ -25,10 +25,15 @@ public void TestValidationSuccessResults()
{
Tall = 10,
Id = "1",
Children = new()
Children1 = new()
{
new ChildOptions() { Name = "C1" },
new ChildOptions() { Name = "C2" }
new ChildOptions() { Name = "C1-1" },
new ChildOptions() { Name = "C1-2" }
},
Children2 = new List<ChildOptions>()
{
new ChildOptions() { Name = "C2-1" },
new ChildOptions() { Name = "C2-2" }
},
NestedList = new()
{
......@@ -126,12 +131,19 @@ public void TestValidationWithEnumeration()
{
Tall = 10,
Id = "1",
Children = new()
Children1 = new()
{
new ChildOptions(),
new ChildOptions(),
new ChildOptions()
}
},
Children2 = new List<ChildOptions>()
{
new ChildOptions(),
new ChildOptions(),
new ChildOptions()
},
}
};
......@@ -142,9 +154,12 @@ public void TestValidationWithEnumeration()
Assert.True(result1.Failed);
Assert.Equal(new List<string>
{
"Name: The MyOptions.Nested.Children[0].Name field is required.",
"Name: The MyOptions.Nested.Children[1].Name field is required.",
"Name: The MyOptions.Nested.Children[2].Name field is required.",
"Name: The MyOptions.Nested.Children1[0].Name field is required.",
"Name: The MyOptions.Nested.Children1[1].Name field is required.",
"Name: The MyOptions.Nested.Children1[2].Name field is required.",
"Name: The MyOptions.Nested.Children2[0].Name field is required.",
"Name: The MyOptions.Nested.Children2[1].Name field is required.",
"Name: The MyOptions.Nested.Children2[2].Name field is required.",
},
result1.Failures);
......@@ -152,9 +167,12 @@ public void TestValidationWithEnumeration()
Assert.True(result2.Failed);
Assert.Equal(new List<string>
{
"DataAnnotation validation failed for 'MyOptions.Nested.Children[0]' members: 'Name' with the error: 'The Name field is required.'.",
"DataAnnotation validation failed for 'MyOptions.Nested.Children[1]' members: 'Name' with the error: 'The Name field is required.'.",
"DataAnnotation validation failed for 'MyOptions.Nested.Children[2]' members: 'Name' with the error: 'The Name field is required.'.",
"DataAnnotation validation failed for 'MyOptions.Nested.Children1[0]' members: 'Name' with the error: 'The Name field is required.'.",
"DataAnnotation validation failed for 'MyOptions.Nested.Children1[1]' members: 'Name' with the error: 'The Name field is required.'.",
"DataAnnotation validation failed for 'MyOptions.Nested.Children1[2]' members: 'Name' with the error: 'The Name field is required.'.",
"DataAnnotation validation failed for 'MyOptions.Nested.Children2[0]' members: 'Name' with the error: 'The Name field is required.'.",
"DataAnnotation validation failed for 'MyOptions.Nested.Children2[1]' members: 'Name' with the error: 'The Name field is required.'.",
"DataAnnotation validation failed for 'MyOptions.Nested.Children2[2]' members: 'Name' with the error: 'The Name field is required.'.",
},
result2.Failures);
}
......@@ -219,7 +237,10 @@ public class NestedOptions
public string? Id { get; set; }
[ValidateEnumeratedItems]
public List<ChildOptions>? Children { get; set; }
public List<ChildOptions>? Children1 { get; set; }
[ValidateEnumeratedItems]
public IEnumerable<ChildOptions>? Children2 { get; set; }
#pragma warning disable SYSLIB1211 // Source gen does static analysis for circular reference. We need to disable it for this test.
[ValidateEnumeratedItems]
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册