diff --git a/src/System.Management.Automation/engine/CoreAdapter.cs b/src/System.Management.Automation/engine/CoreAdapter.cs index 0d58cb525..5dbc2e601 100644 --- a/src/System.Management.Automation/engine/CoreAdapter.cs +++ b/src/System.Management.Automation/engine/CoreAdapter.cs @@ -708,28 +708,46 @@ namespace System.Management.Automation #region Internal Helper Methods - private static Type GetArgumentType(object argument) + private static Type GetArgumentType(object argument, bool isByRefParameter) { if (argument == null) { return typeof(LanguagePrimitives.Null); } - PSReference psref = argument as PSReference; - if (psref != null) + + if (isByRefParameter && argument is PSReference psref) { - return GetArgumentType(PSObject.Base(psref.Value)); + return GetArgumentType(PSObject.Base(psref.Value), isByRefParameter: false); } + return argument.GetType(); } - internal static ConversionRank GetArgumentConversionRank(object argument, Type parameterType) + internal static ConversionRank GetArgumentConversionRank(object argument, Type parameterType, bool isByRef, bool allowCastingToByRefLikeType) { - Type fromType = GetArgumentType(argument); - ConversionRank rank = LanguagePrimitives.GetConversionRank(fromType, parameterType); + Type fromType = null; + ConversionRank rank = ConversionRank.None; + + if (allowCastingToByRefLikeType && parameterType.IsByRefLike) + { + // When resolving best method for use in binders, we can accept implicit/explicit casting conversions to + // a ByRef-like target type, because when generating IL from a call site with the binder, the IL includes + // the casting operation. However, we don't accept such conversions when it's for invoking the method via + // reflection, because reflection just doesn't support ByRef-like type. + fromType = GetArgumentType(PSObject.Base(argument), isByRefParameter: false); + if (fromType != typeof(LanguagePrimitives.Null)) + { + LanguagePrimitives.FigureCastConversion(fromType, parameterType, ref rank); + } + return rank; + } + + fromType = GetArgumentType(argument, isByRef); + rank = LanguagePrimitives.GetConversionRank(fromType, parameterType); if (rank == ConversionRank.None) { - fromType = GetArgumentType(PSObject.Base(argument)); + fromType = GetArgumentType(PSObject.Base(argument), isByRef); rank = LanguagePrimitives.GetConversionRank(fromType, parameterType); } @@ -1186,6 +1204,7 @@ namespace System.Management.Automation /// /// different overloads for a method /// invocation constraints + /// true if we accept implicit/explicit casting conversion to a ByRef-like parameter type for method resolution /// arguments to check against the overloads /// if no best method, the error id to use in the error message /// if no best method, the error message (format string) to use in the error message @@ -1194,6 +1213,7 @@ namespace System.Management.Automation internal static MethodInformation FindBestMethod( MethodInformation[] methods, PSMethodInvocationConstraints invocationConstraints, + bool allowCastingToByRefLikeType, object[] arguments, ref string errorId, ref string errorMsg, @@ -1201,7 +1221,7 @@ namespace System.Management.Automation out bool callNonVirtually) { callNonVirtually = false; - var methodInfo = FindBestMethodImpl(methods, invocationConstraints, arguments, ref errorId, ref errorMsg, out expandParamsOnBest); + var methodInfo = FindBestMethodImpl(methods, invocationConstraints, allowCastingToByRefLikeType, arguments, ref errorId, ref errorMsg, out expandParamsOnBest); if (methodInfo == null) { return null; @@ -1243,6 +1263,7 @@ namespace System.Management.Automation private static MethodInformation FindBestMethodImpl( MethodInformation[] methods, PSMethodInvocationConstraints invocationConstraints, + bool allowCastingToByRefLikeType, object[] arguments, ref string errorId, ref string errorMsg, @@ -1364,8 +1385,18 @@ namespace System.Management.Automation Type elementType = parameter.parameterType.GetElementType(); if (parameters.Length == arguments.Length) { - ConversionRank arrayConv = GetArgumentConversionRank(arguments[j], parameter.parameterType); - ConversionRank elemConv = GetArgumentConversionRank(arguments[j], elementType); + ConversionRank arrayConv = GetArgumentConversionRank( + arguments[j], + parameter.parameterType, + isByRef: false, + allowCastingToByRefLikeType: false); + + ConversionRank elemConv = GetArgumentConversionRank( + arguments[j], + elementType, + isByRef: false, + allowCastingToByRefLikeType: false); + if (elemConv > arrayConv) { candidate.expandedParameters = ExpandParameters(arguments.Length, parameters, elementType); @@ -1387,7 +1418,12 @@ namespace System.Management.Automation // Note that we go through here when the param array parameter has no argument. for (int k = j; k < arguments.Length; k++) { - candidate.conversionRanks[k] = GetArgumentConversionRank(arguments[k], elementType); + candidate.conversionRanks[k] = GetArgumentConversionRank( + arguments[k], + elementType, + isByRef: false, + allowCastingToByRefLikeType: false); + if (candidate.conversionRanks[k] == ConversionRank.None) { // No longer a candidate @@ -1404,7 +1440,11 @@ namespace System.Management.Automation } else { - candidate.conversionRanks[j] = GetArgumentConversionRank(arguments[j], parameter.parameterType); + candidate.conversionRanks[j] = GetArgumentConversionRank( + arguments[j], + parameter.parameterType, + parameter.isByRef, + allowCastingToByRefLikeType); if (candidate.conversionRanks[j] == ConversionRank.None) { @@ -1540,7 +1580,17 @@ namespace System.Management.Automation bool callNonVirtually; string errorId = null; string errorMsg = null; - MethodInformation bestMethod = FindBestMethod(methods, invocationConstraints, arguments, ref errorId, ref errorMsg, out expandParamsOnBest, out callNonVirtually); + + MethodInformation bestMethod = FindBestMethod( + methods, + invocationConstraints, + allowCastingToByRefLikeType: false, + arguments, + ref errorId, + ref errorMsg, + out expandParamsOnBest, + out callNonVirtually); + if (bestMethod == null) { throw new MethodException(errorId, null, errorMsg, methodName, arguments.Length); diff --git a/src/System.Management.Automation/engine/LanguagePrimitives.cs b/src/System.Management.Automation/engine/LanguagePrimitives.cs index 0c954de68..13ecf6e12 100644 --- a/src/System.Management.Automation/engine/LanguagePrimitives.cs +++ b/src/System.Management.Automation/engine/LanguagePrimitives.cs @@ -1410,6 +1410,7 @@ namespace System.Management.Automation #region type converter internal static PSTraceSource typeConversion = PSTraceSource.GetTracer("TypeConversion", "Traces the type conversion algorithm", false); + internal static ConversionData NoConversion = new ConversionData(ConvertNoConversion, ConversionRank.None); private static TypeConverter GetIntegerSystemConverter(Type type) { diff --git a/src/System.Management.Automation/engine/parser/PSType.cs b/src/System.Management.Automation/engine/parser/PSType.cs index bc75fbdb9..475f0cebb 100644 --- a/src/System.Management.Automation/engine/parser/PSType.cs +++ b/src/System.Management.Automation/engine/parser/PSType.cs @@ -70,7 +70,17 @@ namespace System.Management.Automation.Language bool expandParamsOnBest; bool callNonVirtually; var positionalArgCount = positionalArgs.Length; - var bestMethod = Adapter.FindBestMethod(newConstructors, null, positionalArgs, ref errorId, ref errorMsg, out expandParamsOnBest, out callNonVirtually); + + var bestMethod = Adapter.FindBestMethod( + newConstructors, + invocationConstraints: null, + allowCastingToByRefLikeType: false, + positionalArgs, + ref errorId, + ref errorMsg, + out expandParamsOnBest, + out callNonVirtually); + if (bestMethod == null) { parser.ReportError(new ParseError(attributeAst.Extent, errorId, diff --git a/src/System.Management.Automation/engine/runtime/Binding/Binders.cs b/src/System.Management.Automation/engine/runtime/Binding/Binders.cs index 67e2fb8b8..c8c6810c6 100644 --- a/src/System.Management.Automation/engine/runtime/Binding/Binders.cs +++ b/src/System.Management.Automation/engine/runtime/Binding/Binders.cs @@ -247,6 +247,7 @@ namespace System.Management.Automation.Language Type parameterType, string parameterName, string methodName, + bool allowCastingToByRefLikeType, List temps, List initTemps) { @@ -267,13 +268,26 @@ namespace System.Management.Automation.Language return target.Expression.Cast(parameterType); } + ConversionRank? rank = null; + if (parameterType.IsByRefLike && allowCastingToByRefLikeType) + { + var conversionResult = PSConvertBinder.ConvertToByRefLikeTypeViaCasting(target, parameterType); + if (conversionResult != null) + { + return conversionResult; + } + rank = ConversionRank.None; + } + var exceptionParam = Expression.Variable(typeof(Exception)); var targetTemp = Expression.Variable(target.Expression.Type); - bool debase; + bool debase = false; // ConstrainedLanguage note - calls to this conversion are covered by the method resolution algorithm // (which ignores method arguments with disallowed types) - var conversion = LanguagePrimitives.FigureConversion(target.Value, parameterType, out debase); + var conversion = rank == ConversionRank.None + ? LanguagePrimitives.NoConversion + : LanguagePrimitives.FigureConversion(target.Value, parameterType, out debase); var invokeConverter = PSConvertBinder.InvokeConverter(conversion, targetTemp, parameterType, debase, ExpressionCache.InvariantCulture); var expr = Expression.Block(new[] { targetTemp }, @@ -1621,8 +1635,17 @@ namespace System.Management.Automation.Language bool expandParamsOnBest; bool callNonVirtually; var positionalArgCount = CallInfo.ArgumentCount - CallInfo.ArgumentNames.Count; - var bestMethod = Adapter.FindBestMethod(newConstructors, null, - args.Take(positionalArgCount).Select(arg => arg.Value).ToArray(), ref errorId, ref errorMsg, out expandParamsOnBest, out callNonVirtually); + + var bestMethod = Adapter.FindBestMethod( + newConstructors, + invocationConstraints: null, + allowCastingToByRefLikeType: false, + args.Take(positionalArgCount).Select(arg => arg.Value).ToArray(), + ref errorId, + ref errorMsg, + out expandParamsOnBest, + out callNonVirtually); + if (bestMethod == null) { return errorSuggestion ?? new DynamicMetaObject( @@ -1676,14 +1699,7 @@ namespace System.Management.Automation.Language } else { - bool debase; - - // ConstrainedLanguage note - calls to this conversion are done by constructors with params arguments. - // Protection against conversions are covered by the method resolution algorithm - // (which ignores method arguments with disallowed types) - var conversion = LanguagePrimitives.FigureConversion(args[argIndex].Value, resultType, out debase); - Diagnostics.Assert(conversion.Rank != ConversionRank.None, "FindBestMethod should have failed if there is no conversion"); - + var conversion = LanguagePrimitives.FigureConversion(args[argIndex].Value, resultType, out bool debase); ctorArgs[argIndex] = PSConvertBinder.InvokeConverter(conversion, args[argIndex].Expression, resultType, debase, ExpressionCache.InvariantCulture); } } @@ -3771,6 +3787,39 @@ namespace System.Management.Automation.Language return new DynamicMetaObject(expr, bindingRestrictions); } + /// + /// Convert argument to a ByRef-like type via implicit or explicit conversion. + /// + /// + /// The argument to be converted to a ByRef-like type. + /// + /// + /// The ByRef-like type to convert to. + /// + internal static Expression ConvertToByRefLikeTypeViaCasting(DynamicMetaObject argument, Type resultType) + { + var baseObject = PSObject.Base(argument.Value); + + // Source value cannot be null or AutomationNull, and it cannot be a pure PSObject. + if (baseObject != null && !(baseObject is PSObject)) + { + Type fromType = baseObject.GetType(); + ConversionRank rank = ConversionRank.None; + + LanguagePrimitives.FigureCastConversion(fromType, resultType, ref rank); + if (rank != ConversionRank.None) + { + var valueToConvert = baseObject == argument.Value + ? argument.Expression + : Expression.Call(CachedReflectionInfo.PSObject_Base, argument.Expression); + + return Expression.Convert(valueToConvert.Cast(fromType), resultType); + } + } + + return null; + } + internal static Expression InvokeConverter(LanguagePrimitives.ConversionData conversion, Expression value, Type resultType, @@ -4270,22 +4319,9 @@ namespace System.Management.Automation.Language } } - Expression[] indexExprs = new Expression[getterParams.Length]; - for (int i = 0; i < getterParams.Length; ++i) - { - var parameterType = getterParams[i].ParameterType; - - indexExprs[i] = ConvertIndex(indexes[i], parameterType); - if (indexExprs[i] == null) - { - // Calling the indexer will fail because we can't convert an index to the correct type. - return errorSuggestion ?? PSConvertBinder.ThrowNoConversion(target, parameterType, this, _version, indexes); - } - } - - // Check return type after the argument conversion, so any no-conversion error can be thrown. if (getter.ReturnType.IsByRefLike) { + // We cannot return a ByRef-like value in PowerShell, so we disallow getting such an indexer. return errorSuggestion ?? new DynamicMetaObject( Expression.Block( Expression.IfThen( @@ -4296,7 +4332,22 @@ namespace System.Management.Automation.Language Expression.Constant(target.LimitType, typeof(Type)), Expression.Constant(getter.ReturnType, typeof(Type)))), GetNullResult()), - target.CombineRestrictions(indexes)); + target.PSGetTypeRestriction()); + } + + Expression[] indexExprs = new Expression[getterParams.Length]; + for (int i = 0; i < getterParams.Length; ++i) + { + var parameterType = getterParams[i].ParameterType; + indexExprs[i] = parameterType.IsByRefLike + ? PSConvertBinder.ConvertToByRefLikeTypeViaCasting(indexes[i], parameterType) + : ConvertIndex(indexes[i], parameterType); + + if (indexExprs[i] == null) + { + // Calling the indexer will fail because we can't convert an index to the correct type. + return errorSuggestion ?? PSConvertBinder.ThrowNoConversion(target, parameterType, this, _version, indexes); + } } if (CanIndexFromEndWithNegativeIndex(target, getter, getterParams)) @@ -4333,10 +4384,8 @@ namespace System.Management.Automation.Language internal static Expression ConvertIndex(DynamicMetaObject index, Type resultType) { - bool debase; - // ConstrainedLanguage note - Calls to this conversion are protected by the binding rules that call it. - var conversion = LanguagePrimitives.FigureConversion(index.Value, resultType, out debase); + var conversion = LanguagePrimitives.FigureConversion(index.Value, resultType, out bool debase); return conversion.Rank == ConversionRank.None ? null : PSConvertBinder.InvokeConverter(conversion, index.Expression, resultType, debase, ExpressionCache.InvariantCulture); @@ -4543,37 +4592,38 @@ namespace System.Management.Automation.Language } var setterParams = setter.GetParameters(); - if (setterParams.Length != indexes.Length + 1) - { -#if false - if (setterParams.Length == 1 && _allowSlicing) - { - // We have a slicing operation. - return InvokeSlicingIndexer(target, indexes); - } -#endif + int paramLength = setterParams.Length; + if (paramLength != indexes.Length + 1) + { // Calling the indexer will fail because there are either too many or too few indices. return errorSuggestion ?? CannotIndexTarget(target, indexes, value); } -#if false - if (setterParams.Length == 1) + if (setterParams[paramLength - 1].ParameterType.IsByRefLike) { - // The getter takes a single argument, so first check if we're slicing. - var slicingResult = CheckForSlicing(target, indexes); - if (slicingResult != null) - { - return slicingResult; - } + // In theory, it's possible to call the setter with a value that can be implicitly/explicitly casted to the target ByRef-like type. + // However, the set-property/set-indexer semantics in PowerShell requires returning the value after the setting operation. We cannot + // return a ByRef-like value back, so we just disallow setting an indexer that takes a ByRef-like type value. + return errorSuggestion ?? new DynamicMetaObject( + Compiler.ThrowRuntimeError( + nameof(ParserStrings.CannotIndexWithByRefLikeReturnType), + ParserStrings.CannotIndexWithByRefLikeReturnType, + Expression.Constant(target.LimitType, typeof(Type)), + Expression.Constant(setterParams[paramLength - 1].ParameterType, typeof(Type))), + target.PSGetTypeRestriction()); } -#endif - Expression[] indexExprs = new Expression[setterParams.Length]; - for (int i = 0; i < setterParams.Length; ++i) + Expression[] indexExprs = new Expression[paramLength]; + for (int i = 0; i < paramLength; ++i) { var parameterType = setterParams[i].ParameterType; - indexExprs[i] = PSGetIndexBinder.ConvertIndex(i == setterParams.Length - 1 ? value : indexes[i], parameterType); + var argument = (i == paramLength - 1) ? value : indexes[i]; + + indexExprs[i] = parameterType.IsByRefLike + ? PSConvertBinder.ConvertToByRefLikeTypeViaCasting(argument, parameterType) + : PSGetIndexBinder.ConvertIndex(argument, parameterType); + if (indexExprs[i] == null) { // Calling the indexer will fail because we can't convert an index to the correct type. @@ -4581,7 +4631,7 @@ namespace System.Management.Automation.Language } } - if (setterParams.Length == 2 && setterParams[0].ParameterType == typeof(int) && !(target.Value is IDictionary)) + if (paramLength == 2 && setterParams[0].ParameterType == typeof(int) && !(target.Value is IDictionary)) { // PowerShell supports negative indexing for some types (specifically, those with a single // int parameter to the indexer, and also have either a Length or Count property.) For @@ -6110,6 +6160,9 @@ namespace System.Management.Automation.Language if (data.propertyType.IsByRefLike) { + // In theory, it's possible to call the setter with a value that can be implicitly/explicitly casted to the target ByRef-like type. + // However, the set-property/set-indexer semantics in PowerShell requires returning the value after the setting operation. We cannot + // return a ByRef-like value back, so we just disallow setting a member that takes a ByRef-like type value. expr = Expression.Throw( Expression.New( CachedReflectionInfo.SetValueException_ctor, @@ -6227,13 +6280,38 @@ namespace System.Management.Automation.Language var codeProperty = psPropertyInfo as PSCodeProperty; if (codeProperty != null) { + var setterMethod = codeProperty.SetterCodeReference; + var parameters = setterMethod.GetParameters(); + var propertyType = parameters[parameters.Length - 1].ParameterType; + + if (propertyType.IsByRefLike) + { + var expr = Expression.Throw( + Expression.New( + CachedReflectionInfo.SetValueException_ctor, + Expression.Constant(nameof(ExtendedTypeSystem.CannotAccessByRefLikePropertyOrField)), + Expression.Constant(null, typeof(Exception)), + Expression.Constant(ExtendedTypeSystem.CannotAccessByRefLikePropertyOrField), + Expression.NewArrayInit( + typeof(object), + Expression.Constant(codeProperty.Name), + Expression.Constant(propertyType, typeof(Type)))), + this.ReturnType); + + return new DynamicMetaObject(expr, restrictions).WriteToDebugLog(this); + } + var temp = Expression.Variable(typeof(object)); return new DynamicMetaObject( Expression.Block( new[] { temp }, Expression.Assign(temp, value.CastOrConvert(temp.Type)), - PSInvokeMemberBinder.InvokeMethod(codeProperty.SetterCodeReference, null, new[] { target, value }, - false, PSInvokeMemberBinder.MethodInvocationType.Setter), + PSInvokeMemberBinder.InvokeMethod( + setterMethod, + target: null, + new[] { target, value }, + expandParameters: false, + PSInvokeMemberBinder.MethodInvocationType.Setter), temp), restrictions).WriteToDebugLog(this); } @@ -6743,8 +6821,16 @@ namespace System.Management.Automation.Language psMethodInvocationConstraints = new PSMethodInvocationConstraints(target.Value.GetType(), argTypes); } - var result = Adapter.FindBestMethod(mi, psMethodInvocationConstraints, - argValues, ref errorId, ref errorMsg, out expandParamsOnBest, out callNonVirtually); + var result = Adapter.FindBestMethod( + mi, + psMethodInvocationConstraints, + allowCastingToByRefLikeType: true, + argValues, + ref errorId, + ref errorMsg, + out expandParamsOnBest, + out callNonVirtually); + if (callNonVirtually && methodInvocationType != MethodInvocationType.BaseCtor) { methodInvocationType = MethodInvocationType.NonVirtual; @@ -6834,9 +6920,17 @@ namespace System.Management.Automation.Language string errorMsg = null; bool expandParameters; bool callNonVirtually; - var mi = Adapter.FindBestMethod(data.methodInformationStructures, invocationConstraints, - args.Select(arg => arg.Value == AutomationNull.Value ? null : arg.Value).ToArray(), - ref errorId, ref errorMsg, out expandParameters, out callNonVirtually); + + var mi = Adapter.FindBestMethod( + data.methodInformationStructures, + invocationConstraints, + allowCastingToByRefLikeType: true, + args.Select(arg => arg.Value == AutomationNull.Value ? null : arg.Value).ToArray(), + ref errorId, + ref errorMsg, + out expandParameters, + out callNonVirtually); + if (mi != null) { result = (MethodInfo)mi.method; @@ -6845,7 +6939,7 @@ namespace System.Management.Automation.Language return result; } - internal static Expression InvokeMethod(MethodBase mi, DynamicMetaObject target, DynamicMetaObject[] args, bool expandParameters, MethodInvocationType invocationInvocationType) + internal static Expression InvokeMethod(MethodBase mi, DynamicMetaObject target, DynamicMetaObject[] args, bool expandParameters, MethodInvocationType invocationType) { List temps = new List(); List initTemps = new List(); @@ -6858,9 +6952,23 @@ namespace System.Management.Automation.Language Type returnType = methodInfo.ReturnType; if (returnType.IsByRefLike) { + ConstructorInfo exceptionCtorInfo; + switch (invocationType) + { + case MethodInvocationType.Getter: + exceptionCtorInfo = CachedReflectionInfo.GetValueException_ctor; + break; + case MethodInvocationType.Setter: + exceptionCtorInfo = CachedReflectionInfo.SetValueException_ctor; + break; + default: + exceptionCtorInfo = CachedReflectionInfo.MethodException_ctor; + break; + } + return Expression.Throw( Expression.New( - CachedReflectionInfo.MethodException_ctor, + exceptionCtorInfo, Expression.Constant(nameof(ExtendedTypeSystem.CannotCallMethodWithByRefLikeReturnType)), Expression.Constant(null, typeof(Exception)), Expression.Constant(ExtendedTypeSystem.CannotCallMethodWithByRefLikeReturnType), @@ -6890,12 +6998,20 @@ namespace System.Management.Automation.Language } } + // Invoking a base constructor or a base method (non-virtual call) depends reflection invocation + // via helper methods, and thus all arguments need to be casted to 'object'. The ByRef-like types + // cannot be boxed and won't work with reflection. + bool allowCastingToByRefLikeType = + invocationType != MethodInvocationType.BaseCtor && + invocationType != MethodInvocationType.NonVirtual; var parameters = mi.GetParameters(); var argExprs = new Expression[parameters.Length]; + for (int i = 0; i < parameters.Length; ++i) { + Type parameterType = parameters[i].ParameterType; string paramName = parameters[i].Name; - if (string.IsNullOrWhiteSpace(parameters[i].Name)) + if (string.IsNullOrWhiteSpace(paramName)) { paramName = i.ToString(CultureInfo.InvariantCulture); } @@ -6910,17 +7026,30 @@ namespace System.Management.Automation.Language if (paramArrayAttrs != null && paramArrayAttrs.Any()) { Diagnostics.Assert(i == parameters.Length - 1, "vararg parameter is not the last"); - var paramElementType = parameters[i].ParameterType.GetElementType(); + var paramElementType = parameterType.GetElementType(); if (expandParameters) { - argExprs[i] = Expression.NewArrayInit(paramElementType, - args.Skip(i).Select( - a => a.CastOrConvertMethodArgument(paramElementType, paramName, mi.Name, temps, initTemps))); + argExprs[i] = Expression.NewArrayInit( + paramElementType, + args.Skip(i).Select( + a => a.CastOrConvertMethodArgument( + paramElementType, + paramName, + mi.Name, + allowCastingToByRefLikeType: false, + temps, + initTemps))); } else { - var arg = args[i].CastOrConvertMethodArgument(parameters[i].ParameterType, paramName, mi.Name, temps, initTemps); + var arg = args[i].CastOrConvertMethodArgument( + parameterType, + paramName, + mi.Name, + allowCastingToByRefLikeType: false, + temps, + initTemps); argExprs[i] = arg; } } @@ -6931,7 +7060,7 @@ namespace System.Management.Automation.Language var argValue = parameters[i].DefaultValue; if (argValue == null) { - argExprs[i] = Expression.Default(parameters[i].ParameterType); + argExprs[i] = Expression.Default(parameterType); } else { @@ -6943,7 +7072,7 @@ namespace System.Management.Automation.Language } else { - if (parameters[i].ParameterType.IsByRef) + if (parameterType.IsByRef) { if (!(args[i].Value is PSReference)) { @@ -6953,7 +7082,7 @@ namespace System.Management.Automation.Language new object[] { i + 1, typeof(PSReference).FullName, "[ref]" }); } - var temp = Expression.Variable(parameters[i].ParameterType.GetElementType()); + var temp = Expression.Variable(parameterType.GetElementType()); temps.Add(temp); var psRefValue = Expression.Property(args[i].Expression.Cast(typeof(PSReference)), CachedReflectionInfo.PSReference_Value); initTemps.Add(Expression.Assign(temp, psRefValue.Convert(temp.Type))); @@ -6962,7 +7091,14 @@ namespace System.Management.Automation.Language } else { - argExprs[i] = args[i].CastOrConvertMethodArgument(parameters[i].ParameterType, paramName, mi.Name, temps, initTemps); + + argExprs[i] = args[i].CastOrConvertMethodArgument( + parameterType, + paramName, + mi.Name, + allowCastingToByRefLikeType, + temps, + initTemps); } } } @@ -6970,9 +7106,9 @@ namespace System.Management.Automation.Language Expression call; if (constructorInfo != null) { - if (invocationInvocationType == MethodInvocationType.BaseCtor) + if (invocationType == MethodInvocationType.BaseCtor) { - var targetExpr = target.Value is PSObject + var targetExpr = target.Value is PSObject ? target.Expression.Cast(constructorInfo.DeclaringType) : PSGetMemberBinder.GetTargetExpr(target, constructorInfo.DeclaringType); call = Expression.Call( @@ -6988,7 +7124,7 @@ namespace System.Management.Automation.Language } else { - if (invocationInvocationType == MethodInvocationType.NonVirtual && !methodInfo.IsStatic) + if (invocationType == MethodInvocationType.NonVirtual && !methodInfo.IsStatic) { call = Expression.Call( methodInfo.ReturnType == typeof(void) diff --git a/test/powershell/Language/Interop/DotNet/DotNetInterop.Tests.ps1 b/test/powershell/Language/Interop/DotNet/DotNetInterop.Tests.ps1 index cf7214ab5..fd85105b5 100644 --- a/test/powershell/Language/Interop/DotNet/DotNetInterop.Tests.ps1 +++ b/test/powershell/Language/Interop/DotNet/DotNetInterop.Tests.ps1 @@ -6,6 +6,7 @@ Describe "Handle ByRef-like types gracefully" -Tags "CI" { BeforeAll { $code = @' using System; +using System.Management.Automation; namespace DotNetInterop { public class Test @@ -32,17 +33,60 @@ namespace DotNetInterop { } - public string PrintMySpan(string str, Span mySpan = default) + public string PrintMySpan(string str, ReadOnlySpan mySpan = default) { - return str; + if (mySpan.Length == 0) + { + return str; + } + else + { + return str + mySpan.Length; + } } - + public Span GetSpan(int[] array) { return array.AsSpan(); } } + public class Test2 + { + public Test2(ReadOnlySpan span) + { + name = $"Number of chars: {span.Length}"; + } + + public Test2() {} + + private string name = "Hello World"; + public string this[ReadOnlySpan span] + { + get { return name; } + set { name = value; } + } + + public string Name => name; + } + + public class CodeMethods + { + public static ReadOnlySpan GetProperty(PSObject instance) + { + return default(ReadOnlySpan); + } + + public static void SetProperty(PSObject instance, ReadOnlySpan span) + { + } + + public static string RunMethod(PSObject instance, string str, ReadOnlySpan span) + { + return str + span.Length; + } + } + public ref struct MyByRefLikeType { public MyByRefLikeType(int i) { } @@ -56,6 +100,7 @@ namespace DotNetInterop } $testObj = [DotNetInterop.Test]::new() + $test2Obj = [DotNetInterop.Test2]::new() } It "New-Object should fail gracefully when used for a ByRef-like type" { @@ -180,7 +225,82 @@ namespace DotNetInterop } } - It "Set access of an indexer that accepts ByRef-like type should fail gracefully" { - { $testObj[1] = 1 } | Should -Throw -ErrorId "InvalidCastToByRefLikeType" + It "Set access of an indexer that accepts ByRef-like type value should fail gracefully" { + { $testObj[1] = 1 } | Should -Throw -ErrorId "CannotIndexWithByRefLikeReturnType" + } + + Context "Passing value that is implicitly/explicitly castable to ByRef-like parameter in method invocation" { + + BeforeAll { + $ps = [powershell]::Create() + + # Define the CodeMethod 'RunTest' + $ps.AddCommand("Update-TypeData"). + AddParameter("TypeName", "DotNetInterop.Test2"). + AddParameter("MemberType", "CodeMethod"). + AddParameter("MemberName", "RunTest"). + AddParameter("Value", [DotNetInterop.CodeMethods].GetMethod('RunMethod')).Invoke() + $ps.Commands.Clear() + + # Define the CodeProperty 'TestName' + $ps.AddCommand("Update-TypeData"). + AddParameter("TypeName", "DotNetInterop.Test2"). + AddParameter("MemberType", "CodeProperty"). + AddParameter("MemberName", "TestName"). + AddParameter("Value", [DotNetInterop.CodeMethods].GetMethod('GetProperty')). + AddParameter("SecondValue", [DotNetInterop.CodeMethods].GetMethod('SetProperty')).Invoke() + $ps.Commands.Clear() + + $ps.AddScript('$test = [DotNetInterop.Test2]::new()').Invoke() + $ps.Commands.Clear() + } + + AfterAll { + $ps.Dispose() + } + + It "Support method calls with ByRef-like parameter as long as the argument can be casted to the ByRef-like type" { + $testObj.PrintMySpan("abc", "def") | Should -BeExactly "abc3" + $testObj.PrintMySpan("abc", "Hello".ToCharArray()) | Should -BeExactly "abc5" + { $testObj.PrintMySpan("abc", 12) } | Should -Throw -ErrorId "MethodArgumentConversionInvalidCastArgument" + + $path = [System.IO.Path]::GetTempPath() + [System.IO.Path]::IsPathRooted($path.ToCharArray()) | Should -Be $true + } + + It "Support constructor calls with ByRef-like parameter as long as the argument can be casted to the ByRef-like type" { + $result = [DotNetInterop.Test2]::new("abc") + $result.Name | Should -BeExactly "Number of chars: 3" + + { [DotNetInterop.Test2]::new(12) } | Should -Throw -ErrorId "MethodCountCouldNotFindBest" + } + + It "Support indexing operation with ByRef-like index as long as the argument can be casted to the ByRef-like type" { + $test2Obj["abc"] | Should -BeExactly "Hello World" + $test2Obj["abc"] = "pwsh" + $test2Obj["abc"] | Should -BeExactly "pwsh" + } + + It "Support CodeMethod with ByRef-like parameter as long as the argument can be casted to the ByRef-like type" { + $result = $ps.AddScript('$test.RunTest("Hello", "World".ToCharArray())').Invoke() + $ps.Commands.Clear() + $result.Count | Should -Be 1 + $result[0] | Should -Be 'Hello5' + } + + It "Return null for getter access of a CodeProperty that returns a ByRef-like type, even in strict mode" { + $result = $ps.AddScript( + 'try { Set-StrictMode -Version latest; $test.TestName } finally { Set-StrictMode -Off }').Invoke() + $ps.Commands.Clear() + $result.Count | Should -Be 1 + $result[0] | Should -Be $null + } + + It "Fail gracefully for setter access of a CodeProperty that returns a ByRef-like type" { + $result = $ps.AddScript('$test.TestName = "Hello"').Invoke() + $ps.Commands.Clear() + $result.Count | Should -Be 0 + $ps.Streams.Error[0].FullyQualifiedErrorId | Should -Be "CannotAccessByRefLikePropertyOrField" + } } }