-
Notifications
You must be signed in to change notification settings - Fork 5.2k
Options Source Gen Fixes #91363
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
Options Source Gen Fixes #91363
Changes from 3 commits
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 |
|---|---|---|
|
|
@@ -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 string Emit( | |
| return Capture(); | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Returns the nullable annotation string to use in the code generation according to the first 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 GenModelValidationMethod( | |
|
|
||
| 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 GenModelValidationMethod( | |
| { | ||
| 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,22 +395,25 @@ 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(); | ||
| } | ||
|
|
||
| OutLn($"count++;"); | ||
| } | ||
| 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); | ||
| } | ||
|
Comment on lines
+442
to
+454
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. We might want to consider using SHA256 here instead of FNV-1a. I think that's what the compiler itself uses when it needs uniqueness. 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. After offline discussion, we decided to stick with |
||
| } | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.