diff --git a/src/Fable.Transforms/Beam/Replacements.fs b/src/Fable.Transforms/Beam/Replacements.fs index c0c94f7b81..11b28ecf45 100644 --- a/src/Fable.Transforms/Beam/Replacements.fs +++ b/src/Fable.Transforms/Beam/Replacements.fs @@ -334,19 +334,49 @@ let private operators makeThrow r _t (emitExpr r Any [ msg ] "#{exn_type => invalid_operation_exception, message => $0}") |> Some | "InvalidArg", [ argName; msg ] -> - let msg = - add msg (add (add (Value(StringConstant " (Parameter '", None)) argName) (Value(StringConstant "')", None))) - - makeThrow r _t (emitExpr r Any [ msg ] "#{exn_type => argument_exception, message => $0}") + captureExprOnce + com + "param_name" + argName + (fun argName -> + let msg = + add + msg + (add + (add (Value(StringConstant " (Parameter '", None)) argName) + (Value(StringConstant "')", None))) + + makeThrow + r + _t + (emitExpr + r + Any + [ msg; argName ] + "#{exn_type => argument_exception, message => $0, param_name => $1}") + ) |> Some | "Raise", [ arg ] -> makeThrow r _t arg |> Some | "NullArg", [ arg ] -> - let msg = - add - (Value(StringConstant "Value cannot be null. (Parameter '", None)) - (add arg (Value(StringConstant "')", None))) - - makeThrow r _t (emitExpr r Any [ msg ] "#{exn_type => argument_null_exception, message => $0}") + captureExprOnce + com + "param_name" + arg + (fun arg -> + let msg = + add + (Value(StringConstant "Value cannot be null. (Parameter '", None)) + (add arg (Value(StringConstant "')", None))) + + makeThrow + r + _t + (emitExpr + r + Any + [ msg; arg ] + "#{exn_type => argument_null_exception, message => $0, param_name => $1}") + ) |> Some | "IsNull", [ arg ] -> emitExpr r _t [ arg ] "($0 =:= undefined)" |> Some | "IsNotNull", [ arg ] -> emitExpr r _t [ arg ] "($0 =/= undefined)" |> Some @@ -6274,9 +6304,6 @@ let tryCall | Some inner -> emitExpr r t [ msg; inner ] $"#{{exn_type => %s{typeTag}, message => $0, inner_exception => $1}}" - let withParamName msg paramName = - Helper.LibCall(com, "fable_utils", "argument_exception_message", String, [ msg; paramName ]) - let isArgumentException = typeName = "System.ArgumentException" // ArgumentNullException and ArgumentOutOfRangeException take (paramName, message), the others (message, paramName) @@ -6284,6 +6311,31 @@ let tryCall typeName = "System.ArgumentNullException" || typeName = "System.ArgumentOutOfRangeException" + // message and paramName are evaluated once, in source order + let makeArg msg paramName inner = + let build msg paramName = + let msg = + Helper.LibCall(com, "fable_utils", "argument_exception_message", String, [ msg; paramName ]) + + match inner with + | None -> + emitExpr r t [ msg; paramName ] $"#{{exn_type => %s{typeTag}, message => $0, param_name => $1}}" + | Some inner -> + emitExpr + r + t + [ msg; paramName; inner ] + $"#{{exn_type => %s{typeTag}, message => $0, param_name => $1, inner_exception => $2}}" + + if paramNameFirst then + captureExprs2Once + com + ("param_name", paramName) + ("message", msg) + (fun paramName msg -> build msg paramName) + else + captureExprs2Once com ("message", msg) ("param_name", paramName) build + let defaultMessage = match typeName with | "System.ArgumentException" -> "Value does not fall within the expected range." @@ -6294,19 +6346,21 @@ let tryCall match info.CompiledName, thisArg, args with | ".ctor", None, [] -> make (makeStrConst defaultMessage) None |> Some | ".ctor", None, [ paramName ] when paramNameFirst -> - make (withParamName (makeStrConst defaultMessage) paramName) None |> Some + makeArg (makeStrConst defaultMessage) paramName None |> Some | ".ctor", None, [ msg ] -> make msg None |> Some | ".ctor", None, [ first; second ] -> match info.SignatureArgTypes with - | [ _; String ] when paramNameFirst -> make (withParamName second first) None |> Some - | [ _; String ] when isArgumentException -> make (withParamName first second) None |> Some + | [ _; String ] when paramNameFirst -> makeArg second first None |> Some + | [ _; String ] when isArgumentException -> makeArg first second None |> Some | [ _; String ] -> make first None |> Some | _ -> make first (Some second) |> Some | ".ctor", None, [ msg; paramName; inner ] when isArgumentException -> - make (withParamName msg paramName) (Some inner) |> Some + makeArg msg paramName (Some inner) |> Some + | ".ctor", None, [ paramName; _; msg ] when paramNameFirst -> makeArg msg paramName None |> Some | ".ctor", None, [ msg; _; inner ] -> make msg (Some inner) |> Some | "get_Message", Some c, _ -> emitExpr r t [ c ] "maps:get(message, $0, $0)" |> Some | "get_InnerException", Some c, _ -> emitExpr r t [ c ] "maps:get(inner_exception, $0, undefined)" |> Some + | "get_ParamName", Some c, _ -> emitExpr r t [ c ] "maps:get(param_name, $0, undefined)" |> Some | _ -> None // System.Type (reflection) — type info is a map #{fullname => ..., generics => [...]} | "System.Type" -> diff --git a/src/Fable.Transforms/Dart/Replacements.fs b/src/Fable.Transforms/Dart/Replacements.fs index 354372048a..ce6705fd06 100644 --- a/src/Fable.Transforms/Dart/Replacements.fs +++ b/src/Fable.Transforms/Dart/Replacements.fs @@ -2721,6 +2721,7 @@ let exceptions (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr | "get_Message", Some e -> Helper.InstanceCall(e, "toString", t, [], ?loc = r) |> Some // | "get_StackTrace", Some e -> getFieldWith r t e "stack" |> Some | "get_InnerException", Some e -> getFieldWith r t e "innerException" |> Some + | "get_ParamName", Some _ -> bclType com ctx r t i thisArg args | _ -> None let unchecked (com: ICompiler) (ctx: Context) r t (i: CallInfo) (_: Expr option) (args: Expr list) = diff --git a/src/Fable.Transforms/Python/Replacements.fs b/src/Fable.Transforms/Python/Replacements.fs index 3da6ef0dfd..0f9ae88991 100644 --- a/src/Fable.Transforms/Python/Replacements.fs +++ b/src/Fable.Transforms/Python/Replacements.fs @@ -2922,6 +2922,7 @@ let exceptions (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr // Caught exceptions are typed as Python's built-in Exception, which lacks this attribute. Helper.GlobalCall("getattr", t, [ e; makeStrConst "inner_exception" ], ?loc = r) |> Some + | "get_ParamName", Some _ -> bclType com ctx r t i thisArg args | _ -> None let unchecked (com: ICompiler) (ctx: Context) r t (i: CallInfo) (_: Expr option) (args: Expr list) = diff --git a/src/Fable.Transforms/Replacements.fs b/src/Fable.Transforms/Replacements.fs index bb1331fb10..9a930256d5 100644 --- a/src/Fable.Transforms/Replacements.fs +++ b/src/Fable.Transforms/Replacements.fs @@ -3334,6 +3334,7 @@ let exceptions (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr | "get_Message", Some e -> getFieldWith r t e "message" |> Some | "get_StackTrace", Some e -> getFieldWith r t e "stack" |> Some | "get_InnerException", Some e -> getFieldWith r t e "innerException" |> Some + | "get_ParamName", Some _ -> bclType com ctx r t i thisArg args | _ -> None let unchecked (com: ICompiler) (ctx: Context) r t (i: CallInfo) (_: Expr option) (args: Expr list) = diff --git a/src/fable-library-beam/fable_int.erl b/src/fable-library-beam/fable_int.erl index 9113fa354b..d75ed94194 100644 --- a/src/fable-library-beam/fable_int.erl +++ b/src/fable-library-beam/fable_int.erl @@ -87,7 +87,8 @@ wrap_u64(N) -> log2(N) when N < 0 -> erlang:error(#{ exn_type => argument_out_of_range_exception, - message => <<"Non-negative number required. (Parameter 'value')">> + message => <<"Non-negative number required. (Parameter 'value')">>, + param_name => <<"value">> }); log2(0) -> 0; @@ -140,7 +141,8 @@ bigint_from_byte_array(BytesValue) -> bigint_pow(_Base, Exponent) when Exponent < 0 -> erlang:error(#{ exn_type => argument_out_of_range_exception, - message => <<"The number must be greater than or equal to zero. (Parameter 'exponent')">> + message => <<"The number must be greater than or equal to zero. (Parameter 'exponent')">>, + param_name => <<"exponent">> }); bigint_pow(Base, Exponent) -> bigint_pow(Base, Exponent, 1). diff --git a/src/fable-library-beam/fable_list.erl b/src/fable-library-beam/fable_list.erl index ea7d918ecb..1cc2b2c97e 100644 --- a/src/fable-library-beam/fable_list.erl +++ b/src/fable-library-beam/fable_list.erl @@ -909,19 +909,22 @@ randomizer_out_of_range(R) -> "The index is outside the legal range.~nrandomizer returned ~w, should be in range [0.0, 1.0). (Parameter 'randomizer')", [R] ) - ) + ), + param_name => <<"randomizer">> }). invalid_arg(ParamName, Message) -> erlang:error(#{ exn_type => argument_exception, - message => fable_utils:argument_exception_message(Message, ParamName) + message => fable_utils:argument_exception_message(Message, ParamName), + param_name => ParamName }). argument_out_of_range(ParamName, Message) -> erlang:error(#{ exn_type => argument_out_of_range_exception, - message => fable_utils:argument_exception_message(Message, ParamName) + message => fable_utils:argument_exception_message(Message, ParamName), + param_name => ParamName }). lists_had_different_lengths(ParamName) -> diff --git a/src/fable-library-beam/fable_resize_array.erl b/src/fable-library-beam/fable_resize_array.erl index c24a6df568..ec6335786d 100644 --- a/src/fable-library-beam/fable_resize_array.erl +++ b/src/fable-library-beam/fable_resize_array.erl @@ -168,7 +168,8 @@ non_negative_number_required(ParamName) -> erlang:error(#{ exn_type => argument_out_of_range_exception, message => - fable_utils:argument_exception_message(<<"Non-negative number required.">>, ParamName) + fable_utils:argument_exception_message(<<"Non-negative number required.">>, ParamName), + param_name => ParamName }). %% Find first matching predicate, return Default when not found diff --git a/src/fable-library-py/fable_library/System.fs b/src/fable-library-py/fable_library/System.fs index a85c921c46..ca77ee92ae 100644 --- a/src/fable-library-py/fable_library/System.fs +++ b/src/fable-library-py/fable_library/System.fs @@ -76,21 +76,25 @@ type ArgumentException(message: string, paramName: string, innerException: exn | innerException ) - // Use Unchecked.defaultof rather than a `null` literal for the absent inner - // exception: Fable erases nullable reference annotations, so a `null` literal + // Use Unchecked.defaultof rather than a `null` literal for the absent parameter name + // and inner exception: Fable erases nullable reference annotations, so a `null` literal // would be emitted against a non-nullable parameter type and fail type checking. - new() = ArgumentException(SR.Arg_ArgumentException, "", Unchecked.defaultof) - new(message) = ArgumentException(message, "", Unchecked.defaultof) + new() = ArgumentException(SR.Arg_ArgumentException, Unchecked.defaultof, Unchecked.defaultof) + new(message) = ArgumentException(message, Unchecked.defaultof, Unchecked.defaultof) new(message: string, paramName: string) = ArgumentException(message, paramName, Unchecked.defaultof) - new(message: string, innerException: exn | null) = ArgumentException(message, "", innerException) + + new(message: string, innerException: exn | null) + = + ArgumentException(message, Unchecked.defaultof, innerException) + member _.ParamName = paramName type ArgumentNullException(paramName: string, message: string) = inherit ArgumentException(message, paramName, Unchecked.defaultof) new(paramName) = ArgumentNullException(paramName, SR.ArgumentNull_Generic) - new() = ArgumentNullException("") + new() = ArgumentNullException(Unchecked.defaultof) type ArgumentOutOfRangeException(paramName: string, message: string) = inherit ArgumentException(message, paramName, Unchecked.defaultof) new(paramName) = ArgumentOutOfRangeException(paramName, SR.Arg_ArgumentOutOfRangeException) - new() = ArgumentOutOfRangeException("") + new() = ArgumentOutOfRangeException(Unchecked.defaultof) diff --git a/src/fable-library-ts/System.fs b/src/fable-library-ts/System.fs index 03cb86a83d..86182aa495 100644 --- a/src/fable-library-ts/System.fs +++ b/src/fable-library-ts/System.fs @@ -74,21 +74,25 @@ type ArgumentException(message: string, paramName: string, innerException: exn | innerException ) - // Use Unchecked.defaultof rather than a `null` literal for the absent inner - // exception: Fable erases nullable reference annotations, so a `null` literal + // Use Unchecked.defaultof rather than a `null` literal for the absent parameter name + // and inner exception: Fable erases nullable reference annotations, so a `null` literal // would be emitted against a non-nullable parameter type and fail type checking. - new() = ArgumentException(SR.Arg_ArgumentException, "", Unchecked.defaultof) - new(message) = ArgumentException(message, "", Unchecked.defaultof) + new() = ArgumentException(SR.Arg_ArgumentException, Unchecked.defaultof, Unchecked.defaultof) + new(message) = ArgumentException(message, Unchecked.defaultof, Unchecked.defaultof) new(message: string, paramName: string) = ArgumentException(message, paramName, Unchecked.defaultof) - new(message: string, innerException: exn | null) = ArgumentException(message, "", innerException) + + new(message: string, innerException: exn | null) + = + ArgumentException(message, Unchecked.defaultof, innerException) + member _.ParamName = paramName type ArgumentNullException(paramName: string, message: string) = inherit ArgumentException(message, paramName, Unchecked.defaultof) new(paramName) = ArgumentNullException(paramName, SR.ArgumentNull_Generic) - new() = ArgumentNullException("") + new() = ArgumentNullException(Unchecked.defaultof) type ArgumentOutOfRangeException(paramName: string, message: string) = inherit ArgumentException(message, paramName, Unchecked.defaultof) new(paramName) = ArgumentOutOfRangeException(paramName, SR.Arg_ArgumentOutOfRangeException) - new() = ArgumentOutOfRangeException("") + new() = ArgumentOutOfRangeException(Unchecked.defaultof) diff --git a/tests/Beam/ExceptionTests.fs b/tests/Beam/ExceptionTests.fs index d6e9119c2b..b9bf615daa 100644 --- a/tests/Beam/ExceptionTests.fs +++ b/tests/Beam/ExceptionTests.fs @@ -200,3 +200,46 @@ let ``test nullArg raises ArgumentNullException`` () = | :? System.ArgumentNullException as e -> e.Message | e -> "Unexpected exception: " + e.Message caught |> equal "Value cannot be null. (Parameter 'arg')" + +let private paramName (f: unit -> unit) = + try + f () + "no exception" + with :? System.ArgumentException as e -> e.ParamName + +[] +let ``test ArgumentException.ParamName works`` () = + paramName (fun () -> invalidArg "x" "m") |> equal "x" + paramName (fun () -> nullArg "x") |> equal "x" + System.ArgumentException("m", "x").ParamName |> equal "x" + System.ArgumentException("m").ParamName |> isNull |> equal true + System.ArgumentException().ParamName |> isNull |> equal true + System.ArgumentException("m", exn "i").ParamName |> isNull |> equal true + System.ArgumentException("m", "x", exn "i").ParamName |> equal "x" + System.ArgumentNullException("x").ParamName |> equal "x" + System.ArgumentNullException("x", "m").ParamName |> equal "x" + System.ArgumentNullException().ParamName |> isNull |> equal true + System.ArgumentOutOfRangeException("x").ParamName |> equal "x" + System.ArgumentOutOfRangeException("x", "m").ParamName |> equal "x" + System.ArgumentOutOfRangeException("x", box 5, "m").ParamName |> equal "x" + System.ArgumentOutOfRangeException().ParamName |> isNull |> equal true + +[] +let ``test ArgumentException.ParamName of FSharp.Core errors works`` () = + paramName (fun () -> List.exactlyOne [ 1; 2 ] |> ignore) |> equal "source" + paramName (fun () -> Seq.head Seq.empty |> ignore) |> equal "source" + paramName (fun () -> System.Array.BinarySearch([| 1; 3; 5 |], -1, 2, 5) |> ignore) |> equal "index" + paramName (fun () -> Array.sub [| 1 |] 0 5 |> ignore) |> equal "count" + paramName (fun () -> Array.sub [| 1 |] -1 1 |> ignore) |> equal "startIndex" + +[] +let ``test ArgumentException parameter name is evaluated once`` () = + let calls = ref 0 + let name () = + calls.Value <- calls.Value + 1 + "x" + paramName (fun () -> invalidArg (name ()) "m") |> equal "x" + paramName (fun () -> nullArg (name ())) |> equal "x" + System.ArgumentException("m", name ()).ParamName |> equal "x" + System.ArgumentNullException(name (), "m").ParamName |> equal "x" + calls.Value |> equal 4 diff --git a/tests/Dart/src/MiscTests.fs b/tests/Dart/src/MiscTests.fs index fb0c2a645e..e532689428 100644 --- a/tests/Dart/src/MiscTests.fs +++ b/tests/Dart/src/MiscTests.fs @@ -63,6 +63,12 @@ type ElmishWidget<'Model, 'View, 'Msg>(view: 'Model -> ('Msg -> unit) -> 'View) let _view = view member _.Render(model, dispatch) = _view model dispatch +let paramName (f: unit -> unit) = + try + f () + "no exception" + with :? System.ArgumentException as e -> e.ParamName + let tests() = testCase "ref works" <| fun () -> let x = ref 5 @@ -164,6 +170,27 @@ let tests() = | e -> "Unexpected exception: " + e.Message equal "This is invalid (Parameter 'arg')" caught + testCase "ArgumentException.ParamName works" <| fun () -> + paramName (fun () -> invalidArg "x" "m") |> equal "x" + paramName (fun () -> nullArg "x") |> equal "x" + System.ArgumentException("m", "x").ParamName |> equal "x" + // Dart strings are not nullable, so an absent name reads as an empty string + System.ArgumentException("m").ParamName |> System.String.IsNullOrEmpty |> equal true + System.ArgumentException().ParamName |> System.String.IsNullOrEmpty |> equal true + System.ArgumentException("m", exn "i").ParamName |> System.String.IsNullOrEmpty |> equal true + System.ArgumentException("m", "x", exn "i").ParamName |> equal "x" + System.ArgumentNullException("x").ParamName |> equal "x" + System.ArgumentNullException("x", "m").ParamName |> equal "x" + System.ArgumentNullException().ParamName |> System.String.IsNullOrEmpty |> equal true + System.ArgumentOutOfRangeException("x").ParamName |> equal "x" + System.ArgumentOutOfRangeException("x", "m").ParamName |> equal "x" + System.ArgumentOutOfRangeException().ParamName |> System.String.IsNullOrEmpty |> equal true + + testCase "ArgumentException.ParamName of FSharp.Core errors works" <| fun () -> + paramName (fun () -> List.head ([]: int list) |> ignore) |> equal "list" + paramName (fun () -> List.take -1 [ 1 ] |> ignore) |> equal "count" + paramName (fun () -> List.reduce (+) ([]: int list) |> ignore) |> equal "list" + testCase "invalidOp raises InvalidOperationException" <| fun () -> let caught = try diff --git a/tests/Js/Main/MiscTests.fs b/tests/Js/Main/MiscTests.fs index 6df4c81f2c..94153e6d16 100644 --- a/tests/Js/Main/MiscTests.fs +++ b/tests/Js/Main/MiscTests.fs @@ -510,6 +510,13 @@ let buildRecordWithMutableFnWithFor (xs: RecordWithMutableFn list) = let x = xs[i] items <- (x.MutableFn >>* ()) :: items items + +let paramName (f: unit -> unit) = + try + f () + "no exception" + with :? System.ArgumentException as e -> e.ParamName + let tests = testList "Miscellaneous" [ testCase "Hoisted vars don't conflict with while loop" <| fun () -> // see #4031 @@ -1269,6 +1276,26 @@ let tests = | e -> "Unexpected exception: " + e.Message equal "This is invalid (Parameter 'arg')" caught + testCase "ArgumentException.ParamName works" <| fun () -> + paramName (fun () -> invalidArg "x" "m") |> equal "x" + paramName (fun () -> nullArg "x") |> equal "x" + System.ArgumentException("m", "x").ParamName |> equal "x" + System.ArgumentException("m").ParamName |> isNull |> equal true + System.ArgumentException().ParamName |> isNull |> equal true + System.ArgumentException("m", exn "i").ParamName |> isNull |> equal true + System.ArgumentException("m", "x", exn "i").ParamName |> equal "x" + System.ArgumentNullException("x").ParamName |> equal "x" + System.ArgumentNullException("x", "m").ParamName |> equal "x" + System.ArgumentNullException().ParamName |> isNull |> equal true + System.ArgumentOutOfRangeException("x").ParamName |> equal "x" + System.ArgumentOutOfRangeException("x", "m").ParamName |> equal "x" + System.ArgumentOutOfRangeException().ParamName |> isNull |> equal true + + testCase "ArgumentException.ParamName of FSharp.Core errors works" <| fun () -> + paramName (fun () -> List.head ([]: int list) |> ignore) |> equal "list" + paramName (fun () -> List.take -1 [ 1 ] |> ignore) |> equal "count" + paramName (fun () -> List.reduce (+) ([]: int list) |> ignore) |> equal "list" + testCase "invalidOp raises InvalidOperationException" <| fun () -> let caught = try diff --git a/tests/Python/TestMisc.fs b/tests/Python/TestMisc.fs index 0fe681a1f1..ab47fc7125 100644 --- a/tests/Python/TestMisc.fs +++ b/tests/Python/TestMisc.fs @@ -1287,6 +1287,34 @@ let ``test invalidArg raises ArgumentException`` () = | e -> "Unexpected exception: " + e.Message caught |> equal "This is invalid (Parameter 'arg')" +let private paramName (f: unit -> unit) = + try + f () + "no exception" + with :? System.ArgumentException as e -> e.ParamName + +[] +let ``test ArgumentException.ParamName works`` () = + paramName (fun () -> invalidArg "x" "m") |> equal "x" + paramName (fun () -> nullArg "x") |> equal "x" + System.ArgumentException("m", "x").ParamName |> equal "x" + System.ArgumentException("m").ParamName |> isNull |> equal true + System.ArgumentException().ParamName |> isNull |> equal true + System.ArgumentException("m", exn "i").ParamName |> isNull |> equal true + System.ArgumentException("m", "x", exn "i").ParamName |> equal "x" + System.ArgumentNullException("x").ParamName |> equal "x" + System.ArgumentNullException("x", "m").ParamName |> equal "x" + System.ArgumentNullException().ParamName |> isNull |> equal true + System.ArgumentOutOfRangeException("x").ParamName |> equal "x" + System.ArgumentOutOfRangeException("x", "m").ParamName |> equal "x" + System.ArgumentOutOfRangeException().ParamName |> isNull |> equal true + +[] +let ``test ArgumentException.ParamName of FSharp.Core errors works`` () = + paramName (fun () -> List.head ([]: int list) |> ignore) |> equal "list" + paramName (fun () -> List.take -1 [ 1 ] |> ignore) |> equal "count" + paramName (fun () -> List.reduce (+) ([]: int list) |> ignore) |> equal "list" + [] let ``test invalidOp raises InvalidOperationException`` () = let caught =