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

Support TimeSpan with RangeAttribute in Options validation source generator #94798

Merged
merged 1 commit into from
Nov 16, 2023
Merged
Show file tree
Hide file tree
Changes from all 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
91 changes: 78 additions & 13 deletions src/libraries/Microsoft.Extensions.Options/gen/Emitter.cs
Original file line number Diff line number Diff line change
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)
eiriktsarpalis marked this conversation as resolved.
Show resolved Hide resolved
{
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) ||
tarekgh marked this conversation as resolved.
Show resolved Hide resolved
!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 override string FormatErrorMessage(string name) =>
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.";
tarekgh marked this conversation as resolved.
Show resolved Hide resolved

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
Original file line number Diff line number Diff line change
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 _);
tarekgh marked this conversation as resolved.
Show resolved Hide resolved
attributeFullQualifiedName = $"{Emitter.StaticGeneratedValidationAttributesClassesNamespace}.{Emitter.StaticAttributeClassNamePrefix}{_optionsSourceGenContext.Suffix}_{attribute.AttributeClass!.Name}";
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ internal sealed record class SymbolHolder(
INamedTypeSymbol IValidatableObjectSymbol,
INamedTypeSymbol GenericIEnumerableSymbol,
INamedTypeSymbol TypeSymbol,
INamedTypeSymbol TimeSpanSymbol,
INamedTypeSymbol ValidateObjectMembersAttributeSymbol,
INamedTypeSymbol ValidateEnumeratedItemsAttributeSymbol);
}
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
Expand Up @@ -99,19 +99,21 @@ public override string FormatErrorMessage(string name) =>
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
Original file line number Diff line number Diff line change
Expand Up @@ -97,19 +97,21 @@ public override string FormatErrorMessage(string name) =>
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
Loading