Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
88 changes: 71 additions & 17 deletions src/Fable.Transforms/Beam/Replacements.fs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -6274,16 +6304,38 @@ 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)
let paramNameFirst =
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."
Expand All @@ -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" ->
Expand Down
1 change: 1 addition & 0 deletions src/Fable.Transforms/Dart/Replacements.fs
Original file line number Diff line number Diff line change
Expand Up @@ -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) =
Expand Down
1 change: 1 addition & 0 deletions src/Fable.Transforms/Python/Replacements.fs
Original file line number Diff line number Diff line change
Expand Up @@ -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) =
Expand Down
1 change: 1 addition & 0 deletions src/Fable.Transforms/Replacements.fs
Original file line number Diff line number Diff line change
Expand Up @@ -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) =
Expand Down
6 changes: 4 additions & 2 deletions src/fable-library-beam/fable_int.erl
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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).
Expand Down
9 changes: 6 additions & 3 deletions src/fable-library-beam/fable_list.erl
Original file line number Diff line number Diff line change
Expand Up @@ -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) ->
Expand Down
3 changes: 2 additions & 1 deletion src/fable-library-beam/fable_resize_array.erl
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
18 changes: 11 additions & 7 deletions src/fable-library-py/fable_library/System.fs
Original file line number Diff line number Diff line change
Expand Up @@ -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<exn>)
new(message) = ArgumentException(message, "", Unchecked.defaultof<exn>)
new() = ArgumentException(SR.Arg_ArgumentException, Unchecked.defaultof<string>, Unchecked.defaultof<exn>)
new(message) = ArgumentException(message, Unchecked.defaultof<string>, Unchecked.defaultof<exn>)
new(message: string, paramName: string) = ArgumentException(message, paramName, Unchecked.defaultof<exn>)
new(message: string, innerException: exn | null) = ArgumentException(message, "", innerException)

new(message: string, innerException: exn | null)
=
ArgumentException(message, Unchecked.defaultof<string>, innerException)

member _.ParamName = paramName

type ArgumentNullException(paramName: string, message: string) =
inherit ArgumentException(message, paramName, Unchecked.defaultof<exn>)
new(paramName) = ArgumentNullException(paramName, SR.ArgumentNull_Generic)
new() = ArgumentNullException("")
new() = ArgumentNullException(Unchecked.defaultof<string>)

type ArgumentOutOfRangeException(paramName: string, message: string) =
inherit ArgumentException(message, paramName, Unchecked.defaultof<exn>)
new(paramName) = ArgumentOutOfRangeException(paramName, SR.Arg_ArgumentOutOfRangeException)
new() = ArgumentOutOfRangeException("")
new() = ArgumentOutOfRangeException(Unchecked.defaultof<string>)
18 changes: 11 additions & 7 deletions src/fable-library-ts/System.fs
Original file line number Diff line number Diff line change
Expand Up @@ -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<exn>)
new(message) = ArgumentException(message, "", Unchecked.defaultof<exn>)
new() = ArgumentException(SR.Arg_ArgumentException, Unchecked.defaultof<string>, Unchecked.defaultof<exn>)
new(message) = ArgumentException(message, Unchecked.defaultof<string>, Unchecked.defaultof<exn>)
new(message: string, paramName: string) = ArgumentException(message, paramName, Unchecked.defaultof<exn>)
new(message: string, innerException: exn | null) = ArgumentException(message, "", innerException)

new(message: string, innerException: exn | null)
=
ArgumentException(message, Unchecked.defaultof<string>, innerException)

member _.ParamName = paramName

type ArgumentNullException(paramName: string, message: string) =
inherit ArgumentException(message, paramName, Unchecked.defaultof<exn>)
new(paramName) = ArgumentNullException(paramName, SR.ArgumentNull_Generic)
new() = ArgumentNullException("")
new() = ArgumentNullException(Unchecked.defaultof<string>)

type ArgumentOutOfRangeException(paramName: string, message: string) =
inherit ArgumentException(message, paramName, Unchecked.defaultof<exn>)
new(paramName) = ArgumentOutOfRangeException(paramName, SR.Arg_ArgumentOutOfRangeException)
new() = ArgumentOutOfRangeException("")
new() = ArgumentOutOfRangeException(Unchecked.defaultof<string>)
43 changes: 43 additions & 0 deletions tests/Beam/ExceptionTests.fs
Original file line number Diff line number Diff line change
Expand Up @@ -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

[<Fact>]
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

[<Fact>]
let ``test ArgumentException.ParamName of FSharp.Core errors works`` () =
paramName (fun () -> List.exactlyOne [ 1; 2 ] |> ignore) |> equal "source"
paramName (fun () -> Seq.head Seq.empty<int> |> 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"

[<Fact>]
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
27 changes: 27 additions & 0 deletions tests/Dart/src/MiscTests.fs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
Loading
Loading