From 05b8bab6f00a292db68a4142c8b2d318ee247334 Mon Sep 17 00:00:00 2001 From: Ivan Povazan <55002338+ivanpovazan@users.noreply.github.com> Date: Mon, 3 Oct 2022 18:14:48 +0200 Subject: [PATCH] Align parameter default value resolution between coreCLR, Mono and NativeAOT runtimes (#75612) Fixes https://github.com/dotnet/runtime/issues/49806 https://github.com/dotnet/runtime/issues/76174 Co-authored-by: Jan Kotas --- .../System/Reflection/RuntimeParameterInfo.cs | 163 +++++--------- .../Reflection/Runtime/General/Helpers.cs | 126 ++++++++--- .../tests/ParameterInfoTests.cs | 209 +++++++++++++++++- .../tests/System/DelegateTests.cs | 96 +++++++- .../System/Reflection/RuntimeParameterInfo.cs | 110 +++++++-- 5 files changed, 540 insertions(+), 164 deletions(-) diff --git a/src/coreclr/System.Private.CoreLib/src/System/Reflection/RuntimeParameterInfo.cs b/src/coreclr/System.Private.CoreLib/src/System/Reflection/RuntimeParameterInfo.cs index 078343e3cb6c1..cec302e60a014 100644 --- a/src/coreclr/System.Private.CoreLib/src/System/Reflection/RuntimeParameterInfo.cs +++ b/src/coreclr/System.Private.CoreLib/src/System/Reflection/RuntimeParameterInfo.cs @@ -293,6 +293,40 @@ public override bool HasDefaultValue return defaultValue; } + private object? GetDefaultValueFromCustomAttributeData() + { + foreach (CustomAttributeData attributeData in RuntimeCustomAttributeData.GetCustomAttributes(this)) + { + Type attributeType = attributeData.AttributeType; + if (attributeType == typeof(DecimalConstantAttribute)) + { + return GetRawDecimalConstant(attributeData); + } + else if (attributeType.IsSubclassOf(typeof(CustomConstantAttribute))) + { + if (attributeType == typeof(DateTimeConstantAttribute)) + { + return GetRawDateTimeConstant(attributeData); + } + return GetRawConstant(attributeData); + } + } + return DBNull.Value; + } + + private object? GetDefaultValueFromCustomAttributes() + { + object[] customAttributes = GetCustomAttributes(typeof(CustomConstantAttribute), false); + if (customAttributes.Length != 0) + return ((CustomConstantAttribute)customAttributes[0]).Value; + + customAttributes = GetCustomAttributes(typeof(DecimalConstantAttribute), false); + if (customAttributes.Length != 0) + return ((DecimalConstantAttribute)customAttributes[0]).Value; + + return DBNull.Value; + } + // returns DBNull.Value if the parameter doesn't have a default value private object? GetDefaultValueInternal(bool raw) { @@ -303,32 +337,7 @@ public override bool HasDefaultValue object? defaultValue = null; - // Why check the parameter type only for DateTime and only for the ctor arguments? - // No check on the parameter type is done for named args and for Decimal. - - // We should move this after MdToken.IsNullToken(m_tkParamDef) and combine it - // with the other custom attribute logic. But will that be a breaking change? - // For a DateTime parameter on which both an md constant and a ca constant are set, - // which one should win? - if (ParameterType == typeof(DateTime)) - { - if (raw) - { - CustomAttributeTypedArgument value = - RuntimeCustomAttributeData.Filter( - RuntimeCustomAttributeData.GetCustomAttributes(this), typeof(DateTimeConstantAttribute), 0); - - if (value.ArgumentType != null) - return new DateTime((long)value.Value!); - } - else - { - object[] dt = GetCustomAttributes(typeof(DateTimeConstantAttribute), false); - if (dt != null && dt.Length != 0) - return ((DateTimeConstantAttribute)dt[0]).Value; - } - } - + // Prioritize metadata constant over custom attribute constant #region Look for a default value in metadata if (!MdToken.IsNullToken(m_tkParamDef)) { @@ -337,46 +346,17 @@ public override bool HasDefaultValue } #endregion + // If default value is not specified in metadata, look for it in custom attributes if (defaultValue == DBNull.Value) { - #region Look for a default value in the custom attributes - if (raw) - { - foreach (CustomAttributeData attr in CustomAttributeData.GetCustomAttributes(this)) - { - Type? attrType = attr.Constructor.DeclaringType; - - if (attrType == typeof(DateTimeConstantAttribute)) - { - defaultValue = GetRawDateTimeConstant(attr); - } - else if (attrType == typeof(DecimalConstantAttribute)) - { - defaultValue = GetRawDecimalConstant(attr); - } - else if (attrType!.IsSubclassOf(typeof(CustomConstantAttribute))) - { - defaultValue = GetRawConstant(attr); - } - } - } - else - { - object[] CustomAttrs = GetCustomAttributes(typeof(CustomConstantAttribute), false); - if (CustomAttrs.Length != 0) - { - defaultValue = ((CustomConstantAttribute)CustomAttrs[0]).Value; - } - else - { - CustomAttrs = GetCustomAttributes(typeof(DecimalConstantAttribute), false); - if (CustomAttrs.Length != 0) - { - defaultValue = ((DecimalConstantAttribute)CustomAttrs[0]).Value; - } - } - } - #endregion + // The resolution of default value is done by following these rules: + // 1. For RawDefaultValue, we pick the first custom attribute holding the constant value + // in the following order: DecimalConstantAttribute, DateTimeConstantAttribute, CustomConstantAttribute + // 2. For DefaultValue, we first look for CustomConstantAttribute and pick the first occurrence. + // If none is found, then we repeat the same process searching for DecimalConstantAttribute. + // IMPORTANT: Please note that there is a subtle difference in order custom attributes are inspected for + // RawDefaultValue and DefaultValue. + defaultValue = raw ? GetDefaultValueFromCustomAttributeData() : GetDefaultValueFromCustomAttributes(); } if (defaultValue == DBNull.Value) @@ -388,44 +368,21 @@ public override bool HasDefaultValue private static decimal GetRawDecimalConstant(CustomAttributeData attr) { Debug.Assert(attr.Constructor.DeclaringType == typeof(DecimalConstantAttribute)); - - foreach (CustomAttributeNamedArgument namedArgument in attr.NamedArguments) - { - if (namedArgument.MemberInfo.Name.Equals("Value")) - { - // This is not possible because Decimal cannot be represented directly in the metadata. - Debug.Fail("Decimal cannot be represented directly in the metadata."); - return (decimal)namedArgument.TypedValue.Value!; - } - } - - ParameterInfo[] parameters = attr.Constructor.GetParameters(); - Debug.Assert(parameters.Length == 5); - System.Collections.Generic.IList args = attr.ConstructorArguments; Debug.Assert(args.Count == 5); - if (parameters[2].ParameterType == typeof(uint)) - { - // DecimalConstantAttribute(byte scale, byte sign, uint hi, uint mid, uint low) - int low = (int)(uint)args[4].Value!; - int mid = (int)(uint)args[3].Value!; - int hi = (int)(uint)args[2].Value!; - byte sign = (byte)args[1].Value!; - byte scale = (byte)args[0].Value!; - - return new decimal(low, mid, hi, sign != 0, scale); - } - else + return new decimal( + lo: GetConstructorArgument(args, 4), + mid: GetConstructorArgument(args, 3), + hi: GetConstructorArgument(args, 2), + isNegative: ((byte)args[1].Value!) != 0, + scale: (byte)args[0].Value!); + + static int GetConstructorArgument(IList args, int index) { - // DecimalConstantAttribute(byte scale, byte sign, int hi, int mid, int low) - int low = (int)args[4].Value!; - int mid = (int)args[3].Value!; - int hi = (int)args[2].Value!; - byte sign = (byte)args[1].Value!; - byte scale = (byte)args[0].Value!; - - return new decimal(low, mid, hi, sign != 0, scale); + // The constructor is overloaded to accept both signed and unsigned arguments + object obj = args[index].Value!; + return (obj is int value) ? value : (int)(uint)obj; } } @@ -434,20 +391,12 @@ private static DateTime GetRawDateTimeConstant(CustomAttributeData attr) Debug.Assert(attr.Constructor.DeclaringType == typeof(DateTimeConstantAttribute)); Debug.Assert(attr.ConstructorArguments.Count == 1); - foreach (CustomAttributeNamedArgument namedArgument in attr.NamedArguments) - { - if (namedArgument.MemberInfo.Name.Equals("Value")) - { - return new DateTime((long)namedArgument.TypedValue.Value!); - } - } - - // Look at the ctor argument if the "Value" property was not explicitly defined. return new DateTime((long)attr.ConstructorArguments[0].Value!); } private static object? GetRawConstant(CustomAttributeData attr) { + // We are relying only on named arguments for historical reasons foreach (CustomAttributeNamedArgument namedArgument in attr.NamedArguments) { if (namedArgument.MemberInfo.Name.Equals("Value")) diff --git a/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Reflection/Runtime/General/Helpers.cs b/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Reflection/Runtime/General/Helpers.cs index 51847c8e868c3..378d18db65a8f 100644 --- a/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Reflection/Runtime/General/Helpers.cs +++ b/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Reflection/Runtime/General/Helpers.cs @@ -203,46 +203,112 @@ public static object[] InstantiateAsArray(this IEnumerable return result; } - public static bool GetCustomAttributeDefaultValueIfAny(IEnumerable customAttributes, bool raw, out object? defaultValue) + private static object? GetRawDefaultValue(IEnumerable customAttributes) { - // Legacy: If there are multiple default value attribute, the desktop picks one at random (and so do we...) - foreach (CustomAttributeData cad in customAttributes) + foreach (CustomAttributeData attributeData in customAttributes) { - Type attributeType = cad.AttributeType; - if (attributeType.IsSubclassOf(typeof(CustomConstantAttribute))) + Type attributeType = attributeData.AttributeType; + if (attributeType == typeof(DecimalConstantAttribute)) { - if (raw) - { - foreach (CustomAttributeNamedArgument namedArgument in cad.NamedArguments) - { - if (namedArgument.MemberName.Equals("Value")) - { - defaultValue = namedArgument.TypedValue.Value; - return true; - } - } - defaultValue = null; - return false; - } - else + return GetRawDecimalConstant(attributeData); + } + else if (attributeType.IsSubclassOf(typeof(CustomConstantAttribute))) + { + if (attributeType == typeof(DateTimeConstantAttribute)) { - CustomConstantAttribute customConstantAttribute = (CustomConstantAttribute)(cad.Instantiate()); - defaultValue = customConstantAttribute.Value; - return true; + return GetRawDateTimeConstant(attributeData); } + return GetRawConstant(attributeData); } - if (attributeType.Equals(typeof(DecimalConstantAttribute))) + } + return DBNull.Value; + } + + private static decimal GetRawDecimalConstant(CustomAttributeData attr) + { + System.Collections.Generic.IList args = attr.ConstructorArguments; + + return new decimal( + lo: GetConstructorArgument(args, 4), + mid: GetConstructorArgument(args, 3), + hi: GetConstructorArgument(args, 2), + isNegative: ((byte)args[1].Value!) != 0, + scale: (byte)args[0].Value!); + + static int GetConstructorArgument(IList args, int index) + { + // The constructor is overloaded to accept both signed and unsigned arguments + object obj = args[index].Value!; + return (obj is int value) ? value : (int)(uint)obj; + } + } + + private static DateTime GetRawDateTimeConstant(CustomAttributeData attr) + { + return new DateTime((long)attr.ConstructorArguments[0].Value!); + } + + // We are relying only on named arguments for historical reasons + private static object? GetRawConstant(CustomAttributeData attr) + { + foreach (CustomAttributeNamedArgument namedArgument in attr.NamedArguments) + { + if (namedArgument.MemberInfo.Name.Equals("Value")) + return namedArgument.TypedValue.Value; + } + return DBNull.Value; + } + + private static object? GetDefaultValue(IEnumerable customAttributes) + { + // we first look for a CustomConstantAttribute, but we will save the first occurrence of DecimalConstantAttribute + // so we don't go through all custom attributes again + CustomAttributeData? firstDecimalConstantAttributeData = null; + foreach (CustomAttributeData attributeData in customAttributes) + { + Type attributeType = attributeData.AttributeType; + if (firstDecimalConstantAttributeData == null && attributeType == typeof(DecimalConstantAttribute)) + { + firstDecimalConstantAttributeData = attributeData; + } + else if (attributeType.IsSubclassOf(typeof(CustomConstantAttribute))) { - // We should really do a non-instanting check if "raw == false" but given that we don't support - // reflection-only loads, there isn't an observable difference. - DecimalConstantAttribute decimalConstantAttribute = (DecimalConstantAttribute)(cad.Instantiate()); - defaultValue = decimalConstantAttribute.Value; - return true; + CustomConstantAttribute customConstantAttribute = (CustomConstantAttribute)(attributeData.Instantiate()); + return customConstantAttribute.Value; } } - defaultValue = null; - return false; + if (firstDecimalConstantAttributeData != null) + { + DecimalConstantAttribute decimalConstantAttribute = (DecimalConstantAttribute)(firstDecimalConstantAttributeData.Instantiate()); + return decimalConstantAttribute.Value; + } + else + { + return DBNull.Value; + } + } + + public static bool GetCustomAttributeDefaultValueIfAny(IEnumerable customAttributes, bool raw, out object? defaultValue) + { + // The resolution of default value is done by following these rules: + // 1. For RawDefaultValue, we pick the first custom attribute holding the constant value + // in the following order: DecimalConstantAttribute, DateTimeConstantAttribute, CustomConstantAttribute + // 2. For DefaultValue, we first look for CustomConstantAttribute and pick the first occurrence. + // If none is found, then we repeat the same process searching for DecimalConstantAttribute. + // IMPORTANT: Please note that there is a subtle difference in order custom attributes are inspected for + // RawDefaultValue and DefaultValue. + object? resolvedValue = raw ? GetRawDefaultValue(customAttributes) : GetDefaultValue(customAttributes); + if (resolvedValue != DBNull.Value) + { + defaultValue = resolvedValue; + return true; + } + else + { + defaultValue = null; + return false; + } } } } diff --git a/src/libraries/System.Reflection/tests/ParameterInfoTests.cs b/src/libraries/System.Reflection/tests/ParameterInfoTests.cs index 65cb8a7624e30..81200b6040d36 100644 --- a/src/libraries/System.Reflection/tests/ParameterInfoTests.cs +++ b/src/libraries/System.Reflection/tests/ParameterInfoTests.cs @@ -108,7 +108,6 @@ public void RawDefaultValue_Enum() } [Fact] - [ActiveIssue("https://github.com/mono/mono/issues/15037", TestRuntimes.Mono)] public void RawDefaultValueFromAttribute() { ParameterInfo p = GetParameterInfo(typeof(ParameterInfoMetadata), "Foo2", 0); @@ -126,6 +125,167 @@ public void RawDefaultValue_MetadataTrumpsAttribute() Assert.Equal((int)BindingFlags.FlattenHierarchy, (int)raw); } + [Fact] + public void RawDefaultValue_DateTimeParamWithDateTimeConstantAttr() + { + ParameterInfo p = GetParameterInfo(typeof(ParameterInfoMetadata), "DateTimeParamWithDateTimeConstantAttr", 0); + object raw = p.RawDefaultValue; + Assert.Equal(typeof(DateTime), raw.GetType()); + Assert.Equal(new DateTime(42), (DateTime)raw); + } + + [Fact] + public void RawDefaultValue_DateTimeNullableParamWithDateTimeConstantAttr() + { + ParameterInfo p = GetParameterInfo(typeof(ParameterInfoMetadata), "DateTimeNullableParamWithDateTimeConstantAttr", 0); + object raw = p.RawDefaultValue; + Assert.Equal(typeof(DateTime), raw.GetType()); + Assert.Equal(new DateTime(42), (DateTime)raw); + } + + [Fact] + public void RawDefaultValue_DateTimeNullableParamWithDateTimeConstantAttrDefaultValue() + { + ParameterInfo p = GetParameterInfo(typeof(ParameterInfoMetadata), "DateTimeNullableParamWithDateTimeConstantAttrDefaultValue", 0); + object raw = p.RawDefaultValue; + Assert.Null(raw); + } + + [Fact] + public void RawDefaultValue_DateTimeParamWithCustomConstantAttrNoDefaultValue() + { + ParameterInfo p = GetParameterInfo(typeof(ParameterInfoMetadata), "DateTimeParamWithCustomConstantAttrNoDefaultValue", 0); + object raw = p.RawDefaultValue; + Assert.Equal(typeof(int), raw.GetType()); + Assert.Equal(42, raw); + } + + [Fact] + public void RawDefaultValue_DateTimeParamWithTwoCustomConstantAttrsNoDefaultValue1() + { + ParameterInfo p = GetParameterInfo(typeof(ParameterInfoMetadata), "DateTimeParamWithTwoCustomConstantAttrsNoDefaultValue1", 0); + object raw = p.RawDefaultValue; + Assert.Equal(typeof(int), raw.GetType()); + Assert.Equal(42, raw); + } + + [Fact] + public void RawDefaultValue_DateTimeParamWithTwoCustomConstantAttrsNoDefaultValue2() + { + ParameterInfo p = GetParameterInfo(typeof(ParameterInfoMetadata), "DateTimeParamWithTwoCustomConstantAttrsNoDefaultValue2", 0); + object raw = p.RawDefaultValue; + Assert.Equal(typeof(DateTime), raw.GetType()); + Assert.Equal(new DateTime(43), (DateTime)raw); + } + + [Fact] + public void RawDefaultValue_IntParamWithCustomConstantAttr() + { + ParameterInfo p = GetParameterInfo(typeof(ParameterInfoMetadata), "IntParamWithCustomConstantAttr", 0); + object raw = p.RawDefaultValue; + Assert.Equal(typeof(int), raw.GetType()); + Assert.Equal(42, raw); + } + + [Fact] + public void RawDefaultValue_IntParamWithCustomConstantAttrDefaultValue() + { + ParameterInfo p = GetParameterInfo(typeof(ParameterInfoMetadata), "IntParamWithCustomConstantAttrDefaultValue", 0); + object raw = p.RawDefaultValue; + Assert.Equal(typeof(int), raw.GetType()); + Assert.Equal(0, raw); + } + + [Fact] + public void RawDefaultValue_IntParamWithTwoCustomConstantAttrsDefaultValue() + { + ParameterInfo p = GetParameterInfo(typeof(ParameterInfoMetadata), "IntParamWithTwoCustomConstantAttrsDefaultValue", 0); + object raw = p.RawDefaultValue; + Assert.Equal(typeof(int), raw.GetType()); + Assert.Equal(42, raw); + } + + // Verify that RawDefaultValue is resolved through named arguments + [Fact] + public void RawDefaultValue_IntParamWithCustomConstantAttrConstructorValue() + { + ParameterInfo p = GetParameterInfo(typeof(ParameterInfoMetadata), "IntParamWithCustomConstantAttrConstructorValue", 0); + object raw = p.RawDefaultValue; + Assert.Equal(typeof(DBNull), raw.GetType()); + Assert.Equal(DBNull.Value, raw); + } + + [Fact] + public void RawDefaultValue_DecimalParamWithDateTimeConstantAttr() + { + ParameterInfo p = GetParameterInfo(typeof(ParameterInfoMetadata), "DecimalParamWithDateTimeConstantAttr", 0); + object raw = p.RawDefaultValue; + Assert.Equal(typeof(decimal), raw.GetType()); + Assert.Equal(new decimal(4, 3, 2, true, 1), raw); + } + + [Fact] + public void RawDefaultValue_DecimalNullableParamWithDateTimeConstantAttrDefaultValue() + { + ParameterInfo p = GetParameterInfo(typeof(ParameterInfoMetadata), "DecimalNullableParamWithDateTimeConstantAttrDefaultValue", 0); + object raw = p.RawDefaultValue; + Assert.Null(raw); + } + + [Fact] + public void RawDefaultValue_DecimalParamWithCustomConstantAttrNoDefaultValue() + { + ParameterInfo p = GetParameterInfo(typeof(ParameterInfoMetadata), "DecimalParamWithCustomConstantAttrNoDefaultValue", 0); + object raw = p.RawDefaultValue; + Assert.Equal(typeof(int), raw.GetType()); + Assert.Equal(42, raw); + } + + [Fact] + public void RawDefaultValue_DecimalParamWithTwoCustomConstantAttrsNoDefaultValue1() + { + ParameterInfo p = GetParameterInfo(typeof(ParameterInfoMetadata), "DecimalParamWithTwoCustomConstantAttrsNoDefaultValue1", 0); + object raw = p.RawDefaultValue; + Assert.Equal(typeof(int), raw.GetType()); + Assert.Equal(42, raw); + } + + [Fact] + public void RawDefaultValue_DecimalParamWithTwoCustomConstantAttrsNoDefaultValue2() + { + ParameterInfo p = GetParameterInfo(typeof(ParameterInfoMetadata), "DecimalParamWithTwoCustomConstantAttrsNoDefaultValue2", 0); + object raw = p.RawDefaultValue; + Assert.Equal(typeof(decimal), raw.GetType()); + Assert.Equal(new decimal(4, 3, 2, true, 1), raw); + } + + [Fact] + public void RawDefaultValue_ObjectParamWithDateTimeConstantAttr() + { + ParameterInfo p = GetParameterInfo(typeof(ParameterInfoMetadata), "ObjectParamWithDateTimeConstantAttr", 0); + object raw = p.RawDefaultValue; + Assert.Equal(typeof(DateTime), raw.GetType()); + Assert.Equal(new DateTime(42), raw); + } + + [Fact] + public void RawDefaultValue_ObjectParamWithDecimalConstantAttr() + { + ParameterInfo p = GetParameterInfo(typeof(ParameterInfoMetadata), "ObjectParamWithDecimalConstantAttr", 0); + object raw = p.RawDefaultValue; + Assert.Equal(typeof(decimal), raw.GetType()); + Assert.Equal(new decimal(4, 3, 2, true, 1), raw); + } + + [Fact] + public void RawDefaultValue_ObjectParamWithTwoCustomConstantAttr() + { + ParameterInfo p = GetParameterInfo(typeof(ParameterInfoMetadata), "ObjectParamWithTwoCustomConstantAttr", 0); + object raw = p.RawDefaultValue; + Assert.Equal(typeof(decimal), raw.GetType()); + Assert.Equal(new decimal(4, 3, 2, true, 1), raw); + } + [Theory] [InlineData(typeof(ParameterInfoMetadata), "MethodWithRefParameter", 0, false)] [InlineData(typeof(ParameterInfoMetadata), "MethodWithOutParameter", 1, true)] @@ -362,12 +522,59 @@ private static MethodInfo GetMethod(Type type, string name) return type.GetTypeInfo().DeclaredMethods.FirstOrDefault(methodInfo => methodInfo.Name.Equals(name)); } + private class CustomDateTimeConstantAttribute : UsableCustomConstantAttribute + { + public new object Value { get { return RealValue; } set { RealValue = new DateTime((long)value); } } + } + + private class CustomDecimalConstantAttribute : UsableCustomConstantAttribute + { + public new object Value { get { return RealValue; } set { RealValue = new decimal((long)value); } } + } + + private class CustomIntConstantAttribute : UsableCustomConstantAttribute + { + public new object Value { get { return RealValue; } set { RealValue = value; } } + } + + private class AnotherCustomIntConstant : UsableCustomConstantAttribute + { + public new object Value { get { return RealValue; } set { RealValue = value; } } + } + + private class CustomIntConstructorConstantAttribute : UsableCustomConstantAttribute + { + public CustomIntConstructorConstantAttribute(int value) + { + RealValue = value; + } + public new object Value { get { return RealValue; } set { RealValue = value; } } + } + // Metadata for reflection public class ParameterInfoMetadata { public void Foo1(BindingFlags bf = BindingFlags.DeclaredOnly) { } public void Foo2([CustomBindingFlags(Value = BindingFlags.IgnoreCase)] BindingFlags bf) { } public void Foo3([CustomBindingFlags(Value = BindingFlags.DeclaredOnly)] BindingFlags bf = BindingFlags.FlattenHierarchy ) { } + public void DateTimeParamWithDateTimeConstantAttr([DateTimeConstant(42)] DateTime dt) { } + public void DateTimeNullableParamWithDateTimeConstantAttr([DateTimeConstant(42)] DateTime? dt) { } + public void DateTimeNullableParamWithDateTimeConstantAttrDefaultValue([CustomDateTimeConstant(Value = 42)] DateTime? dt = null) { } + public void DateTimeParamWithCustomConstantAttrNoDefaultValue([CustomDateTimeConstant(Value = 42)] DateTime dt) { } + public void DateTimeParamWithTwoCustomConstantAttrsNoDefaultValue1([CustomDateTimeConstant(Value = 42)][DateTimeConstant(43)] DateTime dt) { } + public void DateTimeParamWithTwoCustomConstantAttrsNoDefaultValue2([DateTimeConstant(43)][CustomDateTimeConstant(Value = 42)] DateTime dt) { } + public void IntParamWithCustomConstantAttr([CustomIntConstant(Value = 42)] int number) { } + public void IntParamWithCustomConstantAttrDefaultValue([CustomIntConstant(Value = 42)] int number = 0) { } + public void IntParamWithTwoCustomConstantAttrsDefaultValue([CustomIntConstant(Value = 42)][AnotherCustomIntConstant(Value = 43)] int number) { } + public void IntParamWithCustomConstantAttrConstructorValue([CustomIntConstructorConstant(42)] int number) { } + public void DecimalParamWithDateTimeConstantAttr([DecimalConstant(1, 1, 2, 3, 4)] decimal dec) { } + public void DecimalNullableParamWithDateTimeConstantAttrDefaultValue([CustomDecimalConstant(Value = 42)] decimal? dec = null) { } + public void DecimalParamWithCustomConstantAttrNoDefaultValue([CustomDecimalConstant(Value = 42)] decimal dec) { } + public void DecimalParamWithTwoCustomConstantAttrsNoDefaultValue1([CustomDecimalConstant(Value = 42)][DecimalConstant(1, 1, 2, 3, 4)] decimal dec) { } + public void DecimalParamWithTwoCustomConstantAttrsNoDefaultValue2([DecimalConstant(1, 1, 2, 3, 4)][CustomDecimalConstant(Value = 42)] decimal dec) { } + public void ObjectParamWithDateTimeConstantAttr([DateTimeConstant(42)] object obj) { } + public void ObjectParamWithDecimalConstantAttr([DecimalConstant(1, 1, 2, 3, 4)] object obj) { } + public void ObjectParamWithTwoCustomConstantAttr([DecimalConstant(1, 1, 2, 3, 4)][CustomIntConstant(Value = 42)] object obj) { } public void MethodWithCustomAttribute([My(2)]string str, int iValue, long lValue) { } public virtual void VirtualMethodWithCustomAttributes([My(3)]int val1, [My(4)]int val2, int val3) { } diff --git a/src/libraries/System.Runtime/tests/System/DelegateTests.cs b/src/libraries/System.Runtime/tests/System/DelegateTests.cs index e24d5478fddc1..c6f86a626ca25 100644 --- a/src/libraries/System.Runtime/tests/System/DelegateTests.cs +++ b/src/libraries/System.Runtime/tests/System/DelegateTests.cs @@ -103,20 +103,22 @@ public static void DynamicInvoke() emptyDelegate.DynamicInvoke(null); } - private class SomeCustomConstantAttribute : CustomConstantAttribute + [Fact] + public static void DynamicInvoke_MissingTypeForCustomConstantAttribute_Succeeds() { - public static object Do(object o) => o; - - public override object Value => "SomeValue"; + Assert.Equal("SomeValue", (string)(new ObjectDelegateWithStringCustomConstantAttribute(ObjectMethod).DynamicInvoke(Type.Missing))); } - private delegate object ObjectDelegateWithSomeCustomConstantAttribute([SomeCustomConstant] object o); + [Fact] + public static void DynamicInvoke_MissingTypeForCustomConstantAttributeWithDefault_Succeeds() + { + Assert.Equal("DefaultValue", new StringDelegateWithStringCustomConstantAttributeWithDefault(StringMethod).DynamicInvoke(Type.Missing)); + } [Fact] - [SkipOnMono("https://github.com/dotnet/runtime/issues/49806")] - public static void DynamicInvoke_MissingTypeForCustomConstantAttribute_Succeeds() + public static void DynamicInvoke_MissingTypeForTwoCustomConstantAttributes_Succeeds() { - Assert.Equal("SomeValue", (string)(new ObjectDelegateWithSomeCustomConstantAttribute(SomeCustomConstantAttribute.Do).DynamicInvoke(Type.Missing))); + Assert.Equal("SomeValue", (string)(new ObjectDelegateWithTwoCustomConstantAttributes(ObjectMethod).DynamicInvoke(Type.Missing))); } [Fact] @@ -328,6 +330,30 @@ public static void DynamicInvoke_DefaultParameter_DateTimeParameterWithMissingVa (DateTime)(new DateTimeWithDefaultValueAttribute(DateTimeMethod)).DynamicInvoke(new object[] { Type.Missing })); } + [Fact] + public static void DynamicInvoke_DateTimeAndCustomConstantAttribute_DateTimeParameterWithMissingValue() + { + Assert.Equal( + new DateTime(42), + (DateTime)(new DateTimeDelegateWithDateTimeAndCustomConstantAttribute(DateTimeMethod)).DynamicInvoke(new object[] { Type.Missing })); + } + + [Fact] + public static void DynamicInvoke_CustomConstantAndDateTimeAttribute_DateTimeParameterWithMissingValue() + { + Assert.Equal( + new DateTime(43), + (DateTime)(new DateTimeDelegateWithCustomConstantAndDateTimeAttribute(DateTimeMethod)).DynamicInvoke(new object[] { Type.Missing })); + } + + [Fact] + public static void DynamicInvoke_CustomConstantAttribute_DateTimeParameterWithMissingValue() + { + Assert.Equal( + new DateTime(43), + (DateTime)(new DateTimeDelegateWithCustomConstantAttribute(DateTimeMethod)).DynamicInvoke(new object[] { Type.Missing })); + } + [Fact] public static void DynamicInvoke_DefaultParameter_DateTimeParameterWithExplicitValue() { @@ -344,6 +370,30 @@ public static void DynamicInvoke_DefaultParameter_DecimalParameterWithAttributeA (decimal)(new DecimalWithDefaultValueAttribute(DecimalMethod)).DynamicInvoke(new object[] { Type.Missing })); } + [Fact] + public static void DynamicInvoke_DecimalAndCustomConstantAttribute_DecimalParameterWithAttributeAndMissingValue() + { + Assert.Equal( + new decimal(12, 13, 14, true, 1), + (decimal)(new DecimalDelegateWithDecimalAndCustomConstantAttribute(DecimalMethod)).DynamicInvoke(new object[] { Type.Missing })); + } + + [Fact] + public static void DynamicInvoke_CustomConstantAndDecimalAttribute_DecimalParameterWithAttributeAndMissingValue() + { + Assert.Equal( + new decimal(12, 13, 14, true, 1), + (decimal)(new DecimalDelegateWithCustomConstantAndDecimalAttribute(DecimalMethod)).DynamicInvoke(new object[] { Type.Missing })); + } + + [Fact] + public static void DynamicInvoke_CustomConstantAttribute_DecimalParameterWithAttributeAndMissingValue() + { + Assert.Equal( + new decimal(12, 13, 14, true, 1), + (decimal)(new DecimalDelegateWithCustomConstantAttribute(DecimalMethod)).DynamicInvoke(new object[] { Type.Missing })); + } + [Fact] public static void DynamicInvoke_DefaultParameter_DecimalParameterWithAttributeAndExplicitValue() { @@ -630,6 +680,36 @@ private static object ObjectMethod(object parameter) } private delegate string OptionalStringParameter([Optional] string parameter); + + private class StringCustomConstantAttribute : CustomConstantAttribute + { + public override object Value => "SomeValue"; + } + + private class AnotherStringCustomConstantAttribute : CustomConstantAttribute + { + public override object Value => "SomeOtherValue"; + } + + private class AnotherDateTimeCustomConstantAttribute : CustomConstantAttribute + { + public override object Value => new DateTime(43); + } + + private class AnotherDecimalCustomConstantAttribute : CustomConstantAttribute + { + public override object Value => new decimal(12, 13, 14, true, 1); + } + + private delegate object ObjectDelegateWithStringCustomConstantAttribute([StringCustomConstant] object o); + private delegate object ObjectDelegateWithTwoCustomConstantAttributes([StringCustomConstant][AnotherStringCustomConstant] object o); + private delegate DateTime DateTimeDelegateWithDateTimeAndCustomConstantAttribute([DateTimeConstant(42)][AnotherDateTimeCustomConstant] DateTime parameter); + private delegate DateTime DateTimeDelegateWithCustomConstantAndDateTimeAttribute([AnotherDateTimeCustomConstant][DateTimeConstant(42)] DateTime parameter); + private delegate DateTime DateTimeDelegateWithCustomConstantAttribute([AnotherDateTimeCustomConstant] DateTime parameter); + private delegate decimal DecimalDelegateWithDecimalAndCustomConstantAttribute([DecimalConstant(1, 1, 2, 3, 4)][AnotherDecimalCustomConstant] decimal parameter); + private delegate decimal DecimalDelegateWithCustomConstantAndDecimalAttribute([AnotherDecimalCustomConstant][DecimalConstant(1, 1, 2, 3, 4)] decimal parameter); + private delegate decimal DecimalDelegateWithCustomConstantAttribute([AnotherDecimalCustomConstant] decimal parameter); + private delegate string StringDelegateWithStringCustomConstantAttributeWithDefault([StringCustomConstant] string o = "DefaultValue"); } public static class CreateDelegateTests diff --git a/src/mono/System.Private.CoreLib/src/System/Reflection/RuntimeParameterInfo.cs b/src/mono/System.Private.CoreLib/src/System/Reflection/RuntimeParameterInfo.cs index 32f798e1bec16..07c63015e4bfb 100644 --- a/src/mono/System.Private.CoreLib/src/System/Reflection/RuntimeParameterInfo.cs +++ b/src/mono/System.Private.CoreLib/src/System/Reflection/RuntimeParameterInfo.cs @@ -131,38 +131,112 @@ internal RuntimeParameterInfo(MethodInfo owner, string? name, Type parameterType AttrsImpl = ParameterAttributes.None; } - public override - object? DefaultValue + private object? GetDefaultValueFromCustomAttributeData() { - get + foreach (CustomAttributeData attributeData in RuntimeCustomAttributeData.GetCustomAttributes(this)) { - if (ClassImpl == typeof(decimal) || ClassImpl == typeof(decimal?)) + Type attributeType = attributeData.AttributeType; + if (attributeType == typeof(DecimalConstantAttribute)) { - /* default values for decimals are encoded using a custom attribute */ - DecimalConstantAttribute[] attrs = (DecimalConstantAttribute[])GetCustomAttributes(typeof(DecimalConstantAttribute), false); - if (attrs.Length > 0) - return attrs[0].Value; + return GetRawDecimalConstant(attributeData); } - else if (ClassImpl == typeof(DateTime) || ClassImpl == typeof(DateTime?)) + else if (attributeType.IsSubclassOf(typeof(CustomConstantAttribute))) { - /* default values for DateTime are encoded using a custom attribute */ - DateTimeConstantAttribute[] attrs = (DateTimeConstantAttribute[])GetCustomAttributes(typeof(DateTimeConstantAttribute), false); - if (attrs.Length > 0) - return attrs[0].Value; + if (attributeType == typeof(DateTimeConstantAttribute)) + { + return GetRawDateTimeConstant(attributeData); + } + return GetRawConstant(attributeData); } - return DefaultValueImpl; } + return DBNull.Value; + } + + private static decimal GetRawDecimalConstant(CustomAttributeData attr) + { + System.Collections.Generic.IList args = attr.ConstructorArguments; + + return new decimal( + lo: GetConstructorArgument(args, 4), + mid: GetConstructorArgument(args, 3), + hi: GetConstructorArgument(args, 2), + isNegative: ((byte)args[1].Value!) != 0, + scale: (byte)args[0].Value!); + + static int GetConstructorArgument(IList args, int index) + { + // The constructor is overloaded to accept both signed and unsigned arguments + object obj = args[index].Value!; + return (obj is int value) ? value : (int)(uint)obj; + } + } + + private static DateTime GetRawDateTimeConstant(CustomAttributeData attr) + { + return new DateTime((long)attr.ConstructorArguments[0].Value!); + } + + // We are relying only on named arguments for historical reasons + private static object? GetRawConstant(CustomAttributeData attr) + { + foreach (CustomAttributeNamedArgument namedArgument in attr.NamedArguments) + { + if (namedArgument.MemberInfo.Name.Equals("Value")) + return namedArgument.TypedValue.Value; + } + return DBNull.Value; + } + + private object? GetDefaultValueFromCustomAttributes() + { + object[] customAttributes = GetCustomAttributes(typeof(CustomConstantAttribute), false); + if (customAttributes.Length != 0) + return ((CustomConstantAttribute)customAttributes[0]).Value; + + customAttributes = GetCustomAttributes(typeof(DecimalConstantAttribute), false); + if (customAttributes.Length != 0) + return ((DecimalConstantAttribute)customAttributes[0]).Value; + + return DBNull.Value; + } + + private object? GetDefaultValue(bool raw) + { + // Prioritize metadata constant over custom attribute constant + object? defaultValue = DefaultValueImpl; + if (defaultValue != null && (defaultValue.GetType() == typeof(DBNull) || defaultValue.GetType() == typeof(Missing))) + { + // If default value is not specified in metadata, look for it in custom attributes + // The resolution of default value is done by following these rules: + // 1. For RawDefaultValue, we pick the first custom attribute holding the constant value + // in the following order: DecimalConstantAttribute, DateTimeConstantAttribute, CustomConstantAttribute + // 2. For DefaultValue, we first look for CustomConstantAttribute and pick the first occurrence. + // If none is found, then we repeat the same process searching for DecimalConstantAttribute. + // IMPORTANT: Please note that there is a subtle difference in order custom attributes are inspected for + // RawDefaultValue and DefaultValue. + defaultValue = raw ? GetDefaultValueFromCustomAttributeData() : GetDefaultValueFromCustomAttributes(); + } + + if (defaultValue == DBNull.Value && IsOptional) + { + // If the argument is marked as optional then the default value is Missing.Value. + defaultValue = Type.Missing; + } + + return defaultValue; } + public override object? DefaultValue { get => GetDefaultValue(false); } + public override object? RawDefaultValue { get { - if (DefaultValue != null && DefaultValue.GetType().IsEnum) - return ((Enum)DefaultValue).GetValue(); - /*FIXME right now DefaultValue doesn't throw for reflection-only assemblies. Change this once the former is fixed.*/ - return DefaultValue; + object? defaultValue = GetDefaultValue(true); + if (defaultValue is Enum en) + return en.GetValue(); + return defaultValue; } }