Skip to content

Commit

Permalink
[release/8.0-staging] Support TimeSpan with RangeAttribute in Options…
Browse files Browse the repository at this point in the history
… validation source generator (#94857)

Co-authored-by: Tarek Mahmoud Sayed <tarekms@microsoft.com>
  • Loading branch information
github-actions[bot] and tarekgh committed Nov 17, 2023
1 parent ad96636 commit 1f53b81
Show file tree
Hide file tree
Showing 17 changed files with 713 additions and 82 deletions.
91 changes: 78 additions & 13 deletions src/libraries/Microsoft.Extensions.Options/gen/Emitter.cs
Expand Up @@ -374,12 +374,83 @@ public void EmitCompareAttribute(string modifier, string prefix, string classNam
""");
}

public void EmitRangeAttribute(string modifier, string prefix, string className, string suffix)
public void EmitRangeAttribute(string modifier, string prefix, string className, string suffix, bool emitTimeSpanSupport)
{
OutGeneratedCodeAttribute();

string qualifiedClassName = $"{prefix}{suffix}_{className}";

string initializationString = emitTimeSpanSupport ?
"""
if (OperandType == typeof(global::System.TimeSpan))
{
if (!global::System.TimeSpan.TryParse((string)Minimum, culture, out global::System.TimeSpan timeSpanMinimum) ||
!global::System.TimeSpan.TryParse((string)Maximum, culture, out global::System.TimeSpan timeSpanMaximum))
{
throw new global::System.InvalidOperationException(c_minMaxError);
}
Minimum = timeSpanMinimum;
Maximum = timeSpanMaximum;
}
else
{
Minimum = ConvertValue(Minimum, culture) ?? throw new global::System.InvalidOperationException(c_minMaxError);
Maximum = ConvertValue(Maximum, culture) ?? throw new global::System.InvalidOperationException(c_minMaxError);
}
"""
:
"""
Minimum = ConvertValue(Minimum, culture) ?? throw new global::System.InvalidOperationException(c_minMaxError);
Maximum = ConvertValue(Maximum, culture) ?? throw new global::System.InvalidOperationException(c_minMaxError);
""";

string convertValue = emitTimeSpanSupport ?
"""
if (OperandType == typeof(global::System.TimeSpan))
{
if (value is global::System.TimeSpan)
{
convertedValue = value;
}
else if (value is string)
{
if (!global::System.TimeSpan.TryParse((string)value, formatProvider, out global::System.TimeSpan timeSpanValue))
{
return false;
}
convertedValue = timeSpanValue;
}
else
{
throw new global::System.InvalidOperationException($"A value type {value.GetType()} that is not a TimeSpan or a string has been given. This might indicate a problem with the source generator.");
}
}
else
{
try
{
convertedValue = ConvertValue(value, formatProvider);
}
catch (global::System.Exception e) when (e is global::System.FormatException or global::System.InvalidCastException or global::System.NotSupportedException)
{
return false;
}
}
"""
:
"""
try
{
convertedValue = ConvertValue(value, formatProvider);
}
catch (global::System.Exception e) when (e is global::System.FormatException or global::System.InvalidCastException or global::System.NotSupportedException)
{
return false;
}
""";



OutLn($$"""
[global::System.AttributeUsage(global::System.AttributeTargets.Property | global::System.AttributeTargets.Field | global::System.AttributeTargets.Parameter, AllowMultiple = false)]
{{modifier}} class {{qualifiedClassName}} : {{StaticValidationAttributeType}}
Expand Down Expand Up @@ -414,19 +485,20 @@ public void EmitRangeAttribute(string modifier, string prefix, string className,
string.Format(global::System.Globalization.CultureInfo.CurrentCulture, GetValidationErrorMessage(), name, Minimum, Maximum);
private bool NeedToConvertMinMax { get; }
private bool Initialized { get; set; }
private const string c_minMaxError = "The minimum and maximum values must be set to valid values.";

public override bool IsValid(object? value)
{
if (!Initialized)
{
if (Minimum is null || Maximum is null)
{
throw new global::System.InvalidOperationException("The minimum and maximum values must be set to valid values.");
throw new global::System.InvalidOperationException(c_minMaxError);
}
if (NeedToConvertMinMax)
{
System.Globalization.CultureInfo culture = ParseLimitsInInvariantCulture ? global::System.Globalization.CultureInfo.InvariantCulture : global::System.Globalization.CultureInfo.CurrentCulture;
Minimum = ConvertValue(Minimum, culture) ?? throw new global::System.InvalidOperationException("The minimum and maximum values must be set to valid values.");
Maximum = ConvertValue(Maximum, culture) ?? throw new global::System.InvalidOperationException("The minimum and maximum values must be set to valid values.");
{{initializationString}}
}
int cmp = ((global::System.IComparable)Minimum).CompareTo((global::System.IComparable)Maximum);
if (cmp > 0)
Expand All @@ -448,14 +520,7 @@ public override bool IsValid(object? value)
System.Globalization.CultureInfo formatProvider = ConvertValueInInvariantCulture ? global::System.Globalization.CultureInfo.InvariantCulture : global::System.Globalization.CultureInfo.CurrentCulture;
object? convertedValue;

try
{
convertedValue = ConvertValue(value, formatProvider);
}
catch (global::System.Exception e) when (e is global::System.FormatException or global::System.InvalidCastException or global::System.NotSupportedException)
{
return false;
}
{{convertValue}}

var min = (global::System.IComparable)Minimum;
var max = (global::System.IComparable)Maximum;
Expand Down Expand Up @@ -574,7 +639,7 @@ private void GenValidationAttributesClasses()
}
else if (attributeData.Key == _symbolHolder.RangeAttributeSymbol.Name)
{
EmitRangeAttribute(_optionsSourceGenContext.ClassModifier, Emitter.StaticAttributeClassNamePrefix, attributeData.Key, _optionsSourceGenContext.Suffix);
EmitRangeAttribute(_optionsSourceGenContext.ClassModifier, Emitter.StaticAttributeClassNamePrefix, attributeData.Key, _optionsSourceGenContext.Suffix, attributeData.Value is not null);
}
}

Expand Down
41 changes: 27 additions & 14 deletions src/libraries/Microsoft.Extensions.Options/gen/Parser.cs
Expand Up @@ -624,10 +624,21 @@ private void TrackCompareAttributeForSubstitution(AttributeData attribute, IType
private void TrackRangeAttributeForSubstitution(AttributeData attribute, ITypeSymbol memberType, ref string attributeFullQualifiedName)
{
ImmutableArray<IParameterSymbol> constructorParameters = attribute.AttributeConstructor?.Parameters ?? ImmutableArray<IParameterSymbol>.Empty;
SpecialType argumentSpecialType = SpecialType.None;
ITypeSymbol? argumentType = null;
bool hasTimeSpanType = false;

ITypeSymbol typeSymbol = memberType;
if (typeSymbol.OriginalDefinition.SpecialType == SpecialType.System_Nullable_T)
{
typeSymbol = ((INamedTypeSymbol)typeSymbol).TypeArguments[0];
}

if (constructorParameters.Length == 2)
{
argumentSpecialType = constructorParameters[0].Type.SpecialType;
if (OptionsSourceGenContext.IsConvertibleBasicType(typeSymbol))
{
argumentType = constructorParameters[0].Type;
}
}
else if (constructorParameters.Length == 3)
{
Expand All @@ -641,23 +652,25 @@ private void TrackRangeAttributeForSubstitution(AttributeData attribute, ITypeSy
}
}

if (argumentValue is INamedTypeSymbol namedTypeSymbol && OptionsSourceGenContext.IsConvertibleBasicType(namedTypeSymbol))
if (argumentValue is INamedTypeSymbol namedTypeSymbol)
{
argumentSpecialType = namedTypeSymbol.SpecialType;
// When type is provided as a parameter, it has to match the property type.
if (OptionsSourceGenContext.IsConvertibleBasicType(namedTypeSymbol) && typeSymbol.SpecialType == namedTypeSymbol.SpecialType)
{
argumentType = namedTypeSymbol;
}
else if (SymbolEqualityComparer.Default.Equals(namedTypeSymbol, _symbolHolder.TimeSpanSymbol) &&
(SymbolEqualityComparer.Default.Equals(typeSymbol, _symbolHolder.TimeSpanSymbol) || typeSymbol.SpecialType == SpecialType.System_String))
{
hasTimeSpanType = true;
argumentType = _symbolHolder.TimeSpanSymbol;
}
}
}

ITypeSymbol typeSymbol = memberType;
if (typeSymbol.OriginalDefinition.SpecialType == SpecialType.System_Nullable_T)
{
typeSymbol = ((INamedTypeSymbol)typeSymbol).TypeArguments[0];
}

if (argumentSpecialType != SpecialType.None &&
OptionsSourceGenContext.IsConvertibleBasicType(typeSymbol) &&
(constructorParameters.Length != 3 || typeSymbol.SpecialType == argumentSpecialType)) // When type is provided as a parameter, it has to match the property type.
if (argumentType is not null)
{
_optionsSourceGenContext.EnsureTrackingAttribute(attribute.AttributeClass!.Name, createValue: false, out _);
_optionsSourceGenContext.EnsureTrackingAttribute(attribute.AttributeClass!.Name, createValue: hasTimeSpanType, out _);
attributeFullQualifiedName = $"{Emitter.StaticGeneratedValidationAttributesClassesNamespace}.{Emitter.StaticAttributeClassNamePrefix}{_optionsSourceGenContext.Suffix}_{attribute.AttributeClass!.Name}";
}
}
Expand Down
Expand Up @@ -23,6 +23,7 @@ internal sealed record class SymbolHolder(
INamedTypeSymbol IValidatableObjectSymbol,
INamedTypeSymbol GenericIEnumerableSymbol,
INamedTypeSymbol TypeSymbol,
INamedTypeSymbol TimeSpanSymbol,
INamedTypeSymbol ValidateObjectMembersAttributeSymbol,
INamedTypeSymbol ValidateEnumeratedItemsAttributeSymbol);
}
Expand Up @@ -19,6 +19,7 @@ internal static class SymbolLoader
internal const string IValidatableObjectType = "System.ComponentModel.DataAnnotations.IValidatableObject";
internal const string IValidateOptionsType = "Microsoft.Extensions.Options.IValidateOptions`1";
internal const string TypeOfType = "System.Type";
internal const string TimeSpanType = "System.TimeSpan";
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";
Expand All @@ -42,6 +43,7 @@ public static bool TryLoad(Compilation compilation, out SymbolHolder? symbolHold
var validateOptionsSymbol = GetSymbol(IValidateOptionsType);
var genericIEnumerableSymbol = GetSymbol(GenericIEnumerableType);
var typeSymbol = GetSymbol(TypeOfType);
var timeSpanSymbol = GetSymbol(TimeSpanType);
var validateObjectMembersAttribute = GetSymbol(ValidateObjectMembersAttribute);
var validateEnumeratedItemsAttribute = GetSymbol(ValidateEnumeratedItemsAttribute);
var unconditionalSuppressMessageAttributeSymbol = GetSymbol(UnconditionalSuppressMessageAttributeType);
Expand Down Expand Up @@ -70,6 +72,7 @@ public static bool TryLoad(Compilation compilation, out SymbolHolder? symbolHold
validateOptionsSymbol == null ||
genericIEnumerableSymbol == null ||
typeSymbol == null ||
timeSpanSymbol == null ||
validateObjectMembersAttribute == null ||
validateEnumeratedItemsAttribute == null)
{
Expand All @@ -93,6 +96,7 @@ public static bool TryLoad(Compilation compilation, out SymbolHolder? symbolHold
ivalidatableObjectSymbol,
genericIEnumerableSymbol,
typeSymbol,
timeSpanSymbol,
validateObjectMembersAttribute,
validateEnumeratedItemsAttribute);

Expand Down
Expand Up @@ -5,7 +5,7 @@
<EnableDefaultItems>true</EnableDefaultItems>
<IsPackable>true</IsPackable>
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
<ServicingVersion>1</ServicingVersion>
<ServicingVersion>2</ServicingVersion>
<PackageDescription>Provides a strongly typed way of specifying and accessing settings using dependency injection.</PackageDescription>
</PropertyGroup>

Expand Down
Expand Up @@ -99,19 +99,21 @@ public __SourceGen__RangeAttribute(global::System.Type type, string minimum, str
string.Format(global::System.Globalization.CultureInfo.CurrentCulture, GetValidationErrorMessage(), name, Minimum, Maximum);
private bool NeedToConvertMinMax { get; }
private bool Initialized { get; set; }
private const string c_minMaxError = "The minimum and maximum values must be set to valid values.";

public override bool IsValid(object? value)
{
if (!Initialized)
{
if (Minimum is null || Maximum is null)
{
throw new global::System.InvalidOperationException("The minimum and maximum values must be set to valid values.");
throw new global::System.InvalidOperationException(c_minMaxError);
}
if (NeedToConvertMinMax)
{
System.Globalization.CultureInfo culture = ParseLimitsInInvariantCulture ? global::System.Globalization.CultureInfo.InvariantCulture : global::System.Globalization.CultureInfo.CurrentCulture;
Minimum = ConvertValue(Minimum, culture) ?? throw new global::System.InvalidOperationException("The minimum and maximum values must be set to valid values.");
Maximum = ConvertValue(Maximum, culture) ?? throw new global::System.InvalidOperationException("The minimum and maximum values must be set to valid values.");
Minimum = ConvertValue(Minimum, culture) ?? throw new global::System.InvalidOperationException(c_minMaxError);
Maximum = ConvertValue(Maximum, culture) ?? throw new global::System.InvalidOperationException(c_minMaxError);
}
int cmp = ((global::System.IComparable)Minimum).CompareTo((global::System.IComparable)Maximum);
if (cmp > 0)
Expand Down
Expand Up @@ -97,19 +97,21 @@ public __SourceGen__RangeAttribute(global::System.Type type, string minimum, str
string.Format(global::System.Globalization.CultureInfo.CurrentCulture, GetValidationErrorMessage(), name, Minimum, Maximum);
private bool NeedToConvertMinMax { get; }
private bool Initialized { get; set; }
private const string c_minMaxError = "The minimum and maximum values must be set to valid values.";

public override bool IsValid(object? value)
{
if (!Initialized)
{
if (Minimum is null || Maximum is null)
{
throw new global::System.InvalidOperationException("The minimum and maximum values must be set to valid values.");
throw new global::System.InvalidOperationException(c_minMaxError);
}
if (NeedToConvertMinMax)
{
System.Globalization.CultureInfo culture = ParseLimitsInInvariantCulture ? global::System.Globalization.CultureInfo.InvariantCulture : global::System.Globalization.CultureInfo.CurrentCulture;
Minimum = ConvertValue(Minimum, culture) ?? throw new global::System.InvalidOperationException("The minimum and maximum values must be set to valid values.");
Maximum = ConvertValue(Maximum, culture) ?? throw new global::System.InvalidOperationException("The minimum and maximum values must be set to valid values.");
Minimum = ConvertValue(Minimum, culture) ?? throw new global::System.InvalidOperationException(c_minMaxError);
Maximum = ConvertValue(Maximum, culture) ?? throw new global::System.InvalidOperationException(c_minMaxError);
}
int cmp = ((global::System.IComparable)Minimum).CompareTo((global::System.IComparable)Maximum);
if (cmp > 0)
Expand Down

0 comments on commit 1f53b81

Please sign in to comment.