diff --git a/src/System.Management.Automation/engine/LanguagePrimitives.cs b/src/System.Management.Automation/engine/LanguagePrimitives.cs index 253cfd4a6..93ecdbeff 100644 --- a/src/System.Management.Automation/engine/LanguagePrimitives.cs +++ b/src/System.Management.Automation/engine/LanguagePrimitives.cs @@ -2885,6 +2885,34 @@ namespace System.Management.Automation } } + /// + /// Attempts to use Parser.ScanNumber to get the value of a numeric string. + /// + /// The string to convert to a number. + /// The resulting value type to convert to. + /// The resulting numeric value. + /// + /// True if the parse succeeds, false if a parse exception arises. + /// In all other cases, an exception will be thrown. + /// + private static bool TryScanNumber(string strToConvert, Type resultType, out object result) + { + try + { + result = Convert.ChangeType( + Parser.ScanNumber(strToConvert, resultType, shouldTryCoercion: false), + resultType, + System.Globalization.CultureInfo.InvariantCulture.NumberFormat); + return true; + } + catch (Exception) + { + // Parse or convert failed + result = null; + return false; + } + } + private static object ConvertStringToInteger(object valueToConvert, Type resultType, bool recursion, @@ -2907,7 +2935,14 @@ namespace System.Management.Automation TypeConverter integerConverter = LanguagePrimitives.GetIntegerSystemConverter(resultType); try { - return integerConverter.ConvertFrom(strToConvert); + if (TryScanNumber(strToConvert, resultType, out object result)) + { + return result; + } + else + { + return integerConverter.ConvertFrom(strToConvert); + } } catch (Exception e) { @@ -2946,8 +2981,9 @@ namespace System.Management.Automation TypeTable backupTable) { Diagnostics.Assert(valueToConvert is string, "Value to convert must be a string"); + var strToConvert = valueToConvert as string; - if (((string)valueToConvert).Length == 0) + if (strToConvert.Length == 0) { typeConversion.WriteLine("Returning numeric zero."); // This is not wrapped in a try/catch because it can't fail. @@ -2957,8 +2993,15 @@ namespace System.Management.Automation typeConversion.WriteLine("Converting to decimal."); try { - return Convert.ChangeType(valueToConvert, resultType, - System.Globalization.CultureInfo.InvariantCulture.NumberFormat); + typeConversion.WriteLine("Parsing string value to account for multipliers and type suffixes"); + if (TryScanNumber(strToConvert, resultType, out object result)) + { + return result; + } + else + { + return Convert.ChangeType(strToConvert, resultType, CultureInfo.InvariantCulture.NumberFormat); + } } catch (Exception e) { @@ -2967,7 +3010,7 @@ namespace System.Management.Automation { try { - return ConvertNumericThroughDouble(valueToConvert, resultType); + return ConvertNumericThroughDouble(strToConvert, resultType); } catch (Exception ex) { @@ -2977,7 +3020,7 @@ namespace System.Management.Automation throw new PSInvalidCastException("InvalidCastFromStringToDecimal", e, ExtendedTypeSystem.InvalidCastExceptionWithInnerException, - valueToConvert.ToString(), resultType.ToString(), e.Message); + strToConvert, resultType.ToString(), e.Message); } } @@ -2989,8 +3032,9 @@ namespace System.Management.Automation TypeTable backupTable) { Diagnostics.Assert(valueToConvert is string, "Value to convert must be a string"); + var strToConvert = valueToConvert as string; - if (((string)valueToConvert).Length == 0) + if (strToConvert.Length == 0) { typeConversion.WriteLine("Returning numeric zero."); // This is not wrapped in a try/catch because it can't fail. @@ -3000,15 +3044,23 @@ namespace System.Management.Automation typeConversion.WriteLine("Converting to double or single."); try { - return Convert.ChangeType(valueToConvert, resultType, - System.Globalization.CultureInfo.InvariantCulture.NumberFormat); + typeConversion.WriteLine("Parsing string value to account for multipliers and type suffixes"); + + if (TryScanNumber(strToConvert, resultType, out object result)) + { + return result; + } + else + { + return Convert.ChangeType(strToConvert, resultType, CultureInfo.InvariantCulture.NumberFormat); + } } catch (Exception e) { typeConversion.WriteLine("Exception converting to double or single: \"{0}\".", e.Message); throw new PSInvalidCastException("InvalidCastFromStringToDoubleOrSingle", e, ExtendedTypeSystem.InvalidCastExceptionWithInnerException, - valueToConvert.ToString(), resultType.ToString(), e.Message); + strToConvert, resultType.ToString(), e.Message); } } diff --git a/src/System.Management.Automation/engine/parser/Parser.cs b/src/System.Management.Automation/engine/parser/Parser.cs index 7af775f56..273c4d561 100644 --- a/src/System.Management.Automation/engine/parser/Parser.cs +++ b/src/System.Management.Automation/engine/parser/Parser.cs @@ -225,7 +225,7 @@ namespace System.Management.Automation.Language } // This helper routine is used from the runtime to convert a string to a number. - internal static object ScanNumber(string str, Type toType) + internal static object ScanNumber(string str, Type toType, bool shouldTryCoercion = true) { str = str.Trim(); if (str.Length == 0) @@ -249,11 +249,17 @@ namespace System.Management.Automation.Language if (token == null || !tokenizer.IsAtEndOfScript(token.Extent)) { - // We call ConvertTo, primarily because we expect it will throw an exception, - // but it's possible it could succeed, e.g. if the string had commas, our lexer - // will fail, but Convert.ChangeType could succeed. - - return LanguagePrimitives.ConvertTo(str, toType, CultureInfo.InvariantCulture); + if (shouldTryCoercion) + { + // We call ConvertTo, primarily because we expect it will throw an exception, + // but it's possible it could succeed, e.g. if the string had commas, our lexer + // will fail, but Convert.ChangeType could succeed. + return LanguagePrimitives.ConvertTo(str, toType, CultureInfo.InvariantCulture); + } + else + { + throw new ParseException(); + } } return token.Value; diff --git a/src/System.Management.Automation/engine/runtime/Binding/Binders.cs b/src/System.Management.Automation/engine/runtime/Binding/Binders.cs index d78de553e..81156098b 100644 --- a/src/System.Management.Automation/engine/runtime/Binding/Binders.cs +++ b/src/System.Management.Automation/engine/runtime/Binding/Binders.cs @@ -477,7 +477,7 @@ namespace System.Management.Automation.Language return BindingRestrictions.GetExpressionRestriction( Expression.Block( - new[] {tmp}, + new[] { tmp }, Expression.Assign(tmp, ExpressionCache.GetExecutionContextFromTLS), test)); } @@ -2587,7 +2587,11 @@ namespace System.Management.Automation.Language toType = typeof(int); } - return Expression.Call(CachedReflectionInfo.Parser_ScanNumber, expr.Cast(typeof(string)), Expression.Constant(toType, typeof(Type))); + return Expression.Call( + CachedReflectionInfo.Parser_ScanNumber, + expr.Cast(typeof(string)), + Expression.Constant(toType, typeof(Type)), + Expression.Constant(true)); } private static DynamicMetaObject GetArgAsNumericOrPrimitive(DynamicMetaObject arg, Type targetType) @@ -7414,7 +7418,7 @@ namespace System.Management.Automation.Language // ([pscustomobject]@{ foo = 'bar' }).Where({1}) if (string.Equals(methodName, "Where", StringComparison.OrdinalIgnoreCase)) { - var enumerator = (new object[] {obj}).GetEnumerator(); + var enumerator = (new object[] { obj }).GetEnumerator(); switch (args.Length) { case 1: @@ -7430,7 +7434,7 @@ namespace System.Management.Automation.Language if (string.Equals(methodName, "Foreach", StringComparison.OrdinalIgnoreCase)) { - var enumerator = (new object[] {obj}).GetEnumerator(); + var enumerator = (new object[] { obj }).GetEnumerator(); return EnumerableOps.ForEach(enumerator, args[0], Utils.EmptyArray()); } diff --git a/test/powershell/Language/Parser/Conversions.Tests.ps1 b/test/powershell/Language/Parser/Conversions.Tests.ps1 index ced48d9cf..efe016c7d 100644 --- a/test/powershell/Language/Parser/Conversions.Tests.ps1 +++ b/test/powershell/Language/Parser/Conversions.Tests.ps1 @@ -486,4 +486,35 @@ Describe 'method conversion' -Tags 'CI' { $n = [N]::new() { [System.Management.Automation.LanguagePrimitives]::ConvertTo($n.GetC, [Func[[int], [object]]]) } | Should -Throw -ErrorId "PSInvalidCastException" } + + $TestCases = @( + @{ Number = "100y"; Value = "100"; Type = [int] } + @{ Number = "100uy"; Value = "100"; Type = [double] } + @{ Number = "1200u"; Value = "1200"; Type = [short] } + @{ Number = "1200L"; Value = "1200"; Type = [int] } + @{ Number = "127ul"; Value = "127"; Type = [ulong] } + @{ Number = "127d"; Value = "127"; Type = [byte] } + @{ Number = "127s"; Value = "127"; Type = [sbyte] } + @{ Number = "127y"; Value = "127"; Type = [uint] } + ) + It "Correctly casts to value as type " -TestCases $TestCases { + param($Number, $Value, $Type) + + $Result = $Number -as $Type + $Result | Should -Be $Value + $Result | Should -BeOfType $Type + } + + $TestCases = @( + @{ Number = "200y" } + @{ Number = "300uy" } + @{ Number = "70000us" } + @{ Number = "40000s" } + ) + It "Fails to cast invalid PowerShell-Style suffixed numeral " -TestCases $TestCases { + param($Number) + + $Result = $Number -as [int] + $Result | Should -BeNullOrEmpty + } }