diff --git a/src/Fable.Transforms/Beam/Replacements.fs b/src/Fable.Transforms/Beam/Replacements.fs index be0961e2e2..beca74e4e7 100644 --- a/src/Fable.Transforms/Beam/Replacements.fs +++ b/src/Fable.Transforms/Beam/Replacements.fs @@ -5525,6 +5525,7 @@ let tryCall | "System.Exception" -> match info.CompiledName, thisArg, args with | ".ctor", None, [ msg ] -> emitExpr r t [ msg ] "#{message => $0}" |> Some + | ".ctor", None, [ msg; inner ] -> emitExpr r t [ msg; inner ] "#{message => $0, inner_exception => $1}" |> Some | ".ctor", None, [] -> emitExpr r t [] "#{message => <<\"Exception of type 'System.Exception' was thrown.\">>}" |> Some @@ -5536,6 +5537,14 @@ let tryCall [ c ] "case erlang:is_reference($0) of true -> maps:get(message, erlang:get($0), $0); false -> maps:get(message, $0, $0) end" |> Some + | "get_InnerException", Some c, _ -> + // Handle both map exceptions and reference-based class exceptions + emitExpr + r + t + [ c ] + "case erlang:is_reference($0) of true -> maps:get(inner_exception, erlang:get($0), undefined); false -> maps:get(inner_exception, $0, undefined) end" + |> Some | _ -> None // Built-in .NET exception types — all become #{message => Msg} maps in Erlang | BuiltinSystemException _ @@ -5543,11 +5552,21 @@ let tryCall | "System.OperationCanceledException" -> match info.CompiledName, thisArg, args with | ".ctor", None, [ msg ] -> emitExpr r t [ msg ] "#{message => $0}" |> Some + | ".ctor", None, [ msg; second ] -> + match info.SignatureArgTypes with + // (message, paramName): paramName is not modelled, only the message is kept + | [ _; String ] -> emitExpr r t [ msg ] "#{message => $0}" |> Some + // (message, innerException) + | _ -> emitExpr r t [ msg; second ] "#{message => $0, inner_exception => $1}" |> Some + // (message, paramName, innerException) + | ".ctor", None, [ msg; _paramName; inner ] -> + emitExpr r t [ msg; inner ] "#{message => $0, inner_exception => $1}" |> Some | ".ctor", None, [] -> let typeName = info.DeclaringEntityFullName let msg = $"Exception of type '%s{typeName}' was thrown." emitExpr r t [] $"#{{message => <<\"%s{msg}\">>}}" |> 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 | _ -> 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 e9744b646b..b1e787f770 100644 --- a/src/Fable.Transforms/Dart/Replacements.fs +++ b/src/Fable.Transforms/Dart/Replacements.fs @@ -2606,6 +2606,7 @@ let exceptions (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr Helper.ConstructorCall(e, t, args, ?loc = r) |> Some | "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 | _ -> 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 7225f246e9..c137a9611f 100644 --- a/src/Fable.Transforms/Python/Replacements.fs +++ b/src/Fable.Transforms/Python/Replacements.fs @@ -2789,9 +2789,12 @@ let exceptions (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr match i.DeclaringEntityFullName with // | "System.Collections.Generic.KeyNotFoundException" | BuiltinSystemException _ -> bclType com ctx r t i thisArg args - | _ -> Helper.ConstructorCall(makeIdentExpr "Exception", t, args, ?loc = r) |> Some + | _ -> + let e = makeImportLib com Any "ExceptionBase" "Types" + Helper.ConstructorCall(e, t, args, ?loc = r) |> Some | "get_Message", Some e -> Helper.GlobalCall("str", t, [ thisArg.Value ], ?loc = r) |> Some | "get_StackTrace", Some e -> getFieldWith r t e "stack" |> Some + | "get_InnerException", Some e -> getFieldWith r t e "inner_exception" |> Some | _ -> None let unchecked (com: ICompiler) (ctx: Context) r t (i: CallInfo) (_: Expr option) (args: Expr list) = @@ -4228,7 +4231,7 @@ let tryCall (com: ICompiler) (ctx: Context) r t (info: CallInfo) (thisArg: Expr let tryBaseConstructor com ctx (ent: EntityRef) (argTypes: Lazy) genArgs args = match ent.FullName with - | Types.exception_ -> Some(makeIdentExpr ("Exception"), args) + | Types.exception_ -> Some(makeImportLib com Any "ExceptionBase" "Types", args) | Types.attribute -> Some(makeImportLib com Any "Attribute" "Types", args) | Types.dictionary -> let args = diff --git a/src/Fable.Transforms/Replacements.fs b/src/Fable.Transforms/Replacements.fs index d8f178c070..e45bb07aa5 100644 --- a/src/Fable.Transforms/Replacements.fs +++ b/src/Fable.Transforms/Replacements.fs @@ -3049,6 +3049,7 @@ let exceptions (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr |> Some | "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 | _ -> None let unchecked (com: ICompiler) (ctx: Context) r t (i: CallInfo) (_: Expr option) (args: Expr list) = diff --git a/src/Fable.Transforms/Rust/Replacements.fs b/src/Fable.Transforms/Rust/Replacements.fs index 6d70882214..a614ebd0a5 100644 --- a/src/Fable.Transforms/Rust/Replacements.fs +++ b/src/Fable.Transforms/Rust/Replacements.fs @@ -2411,6 +2411,7 @@ let exceptions (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr | ".ctor", None -> bclType com ctx r t i thisArg args | "get_Message", Some ex -> makeInstanceCall r t i ex i.CompiledName args |> Some | "get_StackTrace", Some ex -> makeInstanceCall r t i ex i.CompiledName args |> Some + | "get_InnerException", Some ex -> makeInstanceCall r t i ex i.CompiledName args |> Some | _ -> None let unchecked (com: ICompiler) (ctx: Context) r t (i: CallInfo) (_: Expr option) (args: Expr list) = diff --git a/src/fable-library-dart/System.fs b/src/fable-library-dart/System.fs index 91ee0bfd55..37fd60c8c5 100644 --- a/src/fable-library-dart/System.fs +++ b/src/fable-library-dart/System.fs @@ -64,25 +64,28 @@ type TimeoutException(message: string) = inherit Exception(message) new() = TimeoutException(SR.Arg_TimeoutException) -type ArgumentException(message: string, paramName: string) = +type ArgumentException(message: string, paramName: string, innerException: exn) = inherit Exception( - if System.String.IsNullOrEmpty(paramName) then - message - else - message + SR.Arg_ParamName_Name + paramName + "')" + (if System.String.IsNullOrEmpty(paramName) then + message + else + message + SR.Arg_ParamName_Name + paramName + "')"), + innerException ) - new() = ArgumentException(SR.Arg_ArgumentException, "") - new(message) = ArgumentException(message, "") + new() = ArgumentException(SR.Arg_ArgumentException, "", null) + new(message) = ArgumentException(message, "", null) + new(message: string, paramName: string) = ArgumentException(message, paramName, null) + new(message: string, innerException: exn) = ArgumentException(message, "", innerException) member _.ParamName = paramName type ArgumentNullException(paramName: string, message: string) = - inherit ArgumentException(message, paramName) + inherit ArgumentException(message, paramName, null) new(paramName) = ArgumentNullException(paramName, SR.ArgumentNull_Generic) new() = ArgumentNullException("") type ArgumentOutOfRangeException(paramName: string, message: string) = - inherit ArgumentException(message, paramName) + inherit ArgumentException(message, paramName, null) new(paramName) = ArgumentOutOfRangeException(paramName, SR.Arg_ArgumentOutOfRangeException) new() = ArgumentOutOfRangeException("") diff --git a/src/fable-library-dart/Types.dart b/src/fable-library-dart/Types.dart index a563b18303..5aadafb078 100644 --- a/src/fable-library-dart/Types.dart +++ b/src/fable-library-dart/Types.dart @@ -7,7 +7,8 @@ final Expando _setComparerExpando = Expando('fable.setComparer') class ExceptionBase implements Exception { final String message; - const ExceptionBase([this.message = ""]); + final Object? innerException; + const ExceptionBase([this.message = "", this.innerException]); @override String toString() => this.message; diff --git a/src/fable-library-py/fable_library/types.py b/src/fable-library-py/fable_library/types.py index ba3afb191f..6ae62d5e5c 100644 --- a/src/fable-library-py/fable_library/types.py +++ b/src/fable-library-py/fable_library/types.py @@ -42,6 +42,22 @@ class Attribute: ... +class ExceptionBase(Exception): + """Base class for .NET ``System.Exception`` and its subclasses. + + Subclasses the built-in ``Exception`` so ``raise``/``except``/``isinstance`` + keep working as before. Only the message is forwarded to the built-in + initializer, so ``str(exc)`` still returns the message even when an inner + exception is supplied (the built-in would otherwise stringify the whole + argument tuple). The inner exception is kept on a dedicated attribute so it + can be read back through ``System.Exception.InnerException``. + """ + + def __init__(self, message: str | None = None, inner_exception: Exception | None = None) -> None: + super().__init__(message if message is not None else "") + self.inner_exception: Exception | None = inner_exception + + # We don't use type aliases here because we need to do isinstance checks IntegerTypes = int | byte | sbyte | int16 | uint16 | int32 | uint32 | int64 | uint64 FloatTypes = float | float32 | float64 @@ -50,6 +66,7 @@ class Attribute: __all__ = [ "UNIT", "Attribute", + "ExceptionBase", "FSharpRef", "FloatTypes", "IntegerTypes", diff --git a/src/fable-library-rust/src/System.fs b/src/fable-library-rust/src/System.fs index 776b2fde78..65e5200f47 100644 --- a/src/fable-library-rust/src/System.fs +++ b/src/fable-library-rust/src/System.fs @@ -8,84 +8,89 @@ type Attribute() = class end type Enum() = class end -type Exception(message: string) = - new() = Exception("") +[] +type Exception(message: string, innerException: Exception) = + new() = Exception("", null) + new(message) = Exception(message, null) member _.Message = message member _.StackTrace = "" + member _.InnerException = innerException interface System.Collections.IStructuralEquatable with member x.Equals(y, comparer) = false member x.GetHashCode(comparer) = 0 type SystemException(message: string) = - inherit Exception(message) + inherit Exception(message, null) new() = SystemException(SR.Arg_SystemException) type ApplicationException(message: string) = - inherit Exception(message) + inherit Exception(message, null) new() = ApplicationException(SR.Arg_ApplicationException) type ArithmeticException(message: string) = - inherit Exception(message) + inherit Exception(message, null) new() = ArithmeticException(SR.Arg_ArithmeticException) type DivideByZeroException(message: string) = - inherit Exception(message) + inherit Exception(message, null) new() = DivideByZeroException(SR.Arg_DivideByZero) type FormatException(message: string) = - inherit Exception(message) + inherit Exception(message, null) new() = FormatException(SR.Arg_FormatException) type IndexOutOfRangeException(message: string) = - inherit Exception(message) + inherit Exception(message, null) new() = IndexOutOfRangeException(SR.Arg_IndexOutOfRangeException) type InvalidOperationException(message: string) = - inherit Exception(message) + inherit Exception(message, null) new() = InvalidOperationException(SR.Arg_InvalidOperationException) type NotFiniteNumberException(message: string) = - inherit Exception(message) + inherit Exception(message, null) new() = NotFiniteNumberException(SR.Arg_NotFiniteNumberException) type NotImplementedException(message: string) = - inherit Exception(message) + inherit Exception(message, null) new() = NotImplementedException(SR.Arg_NotImplementedException) type NotSupportedException(message: string) = - inherit Exception(message) + inherit Exception(message, null) new() = NotSupportedException(SR.Arg_NotSupportedException) type NullReferenceException(message: string) = - inherit Exception(message) + inherit Exception(message, null) new() = NullReferenceException(SR.Arg_NullReferenceException) type OutOfMemoryException(message: string) = - inherit Exception(message) + inherit Exception(message, null) new() = OutOfMemoryException(SR.Arg_OutOfMemoryException) type OverflowException(message: string) = - inherit Exception(message) + inherit Exception(message, null) new() = OverflowException(SR.Arg_OverflowException) type RankException(message: string) = - inherit Exception(message) + inherit Exception(message, null) new() = RankException(SR.Arg_RankException) type StackOverflowException(message: string) = - inherit Exception(message) + inherit Exception(message, null) new() = StackOverflowException(SR.Arg_StackOverflowException) type TimeoutException(message: string) = - inherit Exception(message) + inherit Exception(message, null) new() = TimeoutException(SR.Arg_TimeoutException) -type ArgumentException(message: string, paramName: string) = - inherit Exception(message) +type ArgumentException(message: string, paramName: string, innerException: Exception) = + inherit Exception(message, innerException) - new() = ArgumentException(SR.Arg_ArgumentException, "") - new(message) = ArgumentException(message, "") + new() = ArgumentException(SR.Arg_ArgumentException, "", null) + new(message) = ArgumentException(message, "", null) + new(message: string, paramName: string) = ArgumentException(message, paramName, null) + new(message: string, innerException: Exception) = ArgumentException(message, "", innerException) member _.Message = if System.String.IsNullOrEmpty(paramName) then @@ -96,12 +101,12 @@ type ArgumentException(message: string, paramName: string) = member _.ParamName = paramName type ArgumentNullException(paramName: string, message: string) = - inherit ArgumentException(message, paramName) + inherit ArgumentException(message, paramName, null) new(paramName) = ArgumentNullException(paramName, SR.ArgumentNull_Generic) new() = ArgumentNullException("") type ArgumentOutOfRangeException(paramName: string, message: string) = - inherit ArgumentException(message, paramName) + inherit ArgumentException(message, paramName, null) new(paramName) = ArgumentOutOfRangeException(paramName, SR.Arg_ArgumentOutOfRangeException) new() = ArgumentOutOfRangeException("") diff --git a/src/fable-library-ts/System.fs b/src/fable-library-ts/System.fs index 91ee0bfd55..03cb86a83d 100644 --- a/src/fable-library-ts/System.fs +++ b/src/fable-library-ts/System.fs @@ -64,25 +64,31 @@ type TimeoutException(message: string) = inherit Exception(message) new() = TimeoutException(SR.Arg_TimeoutException) -type ArgumentException(message: string, paramName: string) = +type ArgumentException(message: string, paramName: string, innerException: exn | null) = inherit Exception( - if System.String.IsNullOrEmpty(paramName) then - message - else - message + SR.Arg_ParamName_Name + paramName + "')" + (if System.String.IsNullOrEmpty(paramName) then + message + else + message + SR.Arg_ParamName_Name + paramName + "')"), + innerException ) - new() = ArgumentException(SR.Arg_ArgumentException, "") - new(message) = ArgumentException(message, "") + // Use Unchecked.defaultof rather than a `null` literal for the absent 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(message: string, paramName: string) = ArgumentException(message, paramName, Unchecked.defaultof) + new(message: string, innerException: exn | null) = ArgumentException(message, "", innerException) member _.ParamName = paramName type ArgumentNullException(paramName: string, message: string) = - inherit ArgumentException(message, paramName) + inherit ArgumentException(message, paramName, Unchecked.defaultof) new(paramName) = ArgumentNullException(paramName, SR.ArgumentNull_Generic) new() = ArgumentNullException("") type ArgumentOutOfRangeException(paramName: string, message: string) = - inherit ArgumentException(message, paramName) + inherit ArgumentException(message, paramName, Unchecked.defaultof) new(paramName) = ArgumentOutOfRangeException(paramName, SR.Arg_ArgumentOutOfRangeException) new() = ArgumentOutOfRangeException("") diff --git a/src/fable-library-ts/Util.ts b/src/fable-library-ts/Util.ts index 2ced013e02..f8f0bb0663 100644 --- a/src/fable-library-ts/Util.ts +++ b/src/fable-library-ts/Util.ts @@ -80,9 +80,15 @@ export interface ICollection extends IEnumerable { export class Exception { public message: string; public stack?: string; + // Typed non-nullable to match how Fable models .NET reference types: nullability + // annotations are erased, so consumers (and `get_InnerException`) see `Exception`. + // At runtime this is `undefined` when no inner exception was provided, just like + // a `defaultOf()` value, which is fine for code that reads `.InnerException`. + public innerException!: Exception; - constructor(msg?: string) { + constructor(msg?: string, innerException?: Exception) { this.message = msg ?? ""; + this.innerException = innerException as Exception; } toString() { diff --git a/tests/Beam/MiscTests.fs b/tests/Beam/MiscTests.fs index d3c232ef5b..48ee8a155b 100644 --- a/tests/Beam/MiscTests.fs +++ b/tests/Beam/MiscTests.fs @@ -733,6 +733,18 @@ let ``test try-with with unmatched exception type reraises`` () = | _ -> caught <- "other" caught |> equal "arg" +[] +let ``test ArgumentException with message and inner exception works`` () = + let inner = exn "the inner cause" + let ex = System.ArgumentException("outer message", inner) + ex.Message |> equal "outer message" + ex.InnerException.Message |> equal "the inner cause" + +[] +let ``test Exception InnerException is null when not provided`` () = + let ex = System.ArgumentException("no inner") + isNull (box ex.InnerException) |> equal true + // -- General / Misc -- [] diff --git a/tests/Dart/src/TypeTests.fs b/tests/Dart/src/TypeTests.fs index 42b9eb81d9..5a498c02f4 100644 --- a/tests/Dart/src/TypeTests.fs +++ b/tests/Dart/src/TypeTests.fs @@ -608,6 +608,12 @@ let tests() = let z = MyOptionalClass(?arg3 = Some 2) (z.P1, z.P2, z.P3) |> equal (1.0, "1", 2) + testCase "ArgumentException with message and inner exception works" <| fun () -> + let inner = exn "the inner cause" + let ex = System.ArgumentException("outer message", inner) + ex.Message |> equal "outer message" + ex.InnerException.Message |> equal "the inner cause" + // testCase "Can implement interface optional properties" <| fun () -> // let veryOptionalValue = VeryOptionalClass() :> VeryOptionalInterface // veryOptionalValue.Bar |> equal (Some 3) diff --git a/tests/Js/Main/TypeTests.fs b/tests/Js/Main/TypeTests.fs index 0377816ead..5bf93644e9 100644 --- a/tests/Js/Main/TypeTests.fs +++ b/tests/Js/Main/TypeTests.fs @@ -1414,6 +1414,12 @@ let tests = with ex -> ex.Message |> equal "Will I be reraised?" + testCase "ArgumentException with message and inner exception works" <| fun () -> + let inner = exn "the inner cause" + let ex = System.ArgumentException("outer message", inner) + ex.Message |> equal "outer message" + ex.InnerException.Message |> equal "the inner cause" + testCase "This context is not lost in closures within implicit constructor" <| fun () -> // See #1444 ThisContextInConstructor(7).Value() |> equal 7 diff --git a/tests/Python/TestMisc.fs b/tests/Python/TestMisc.fs index d9e2a8a62b..95994941dc 100644 --- a/tests/Python/TestMisc.fs +++ b/tests/Python/TestMisc.fs @@ -1066,6 +1066,18 @@ let ``test try-with with unmatched exception type reraises`` () = caught |> equal "arg" +[] +let ``test ArgumentException with message and inner exception works`` () = + let inner = exn "the inner cause" + let ex = System.ArgumentException("outer message", inner) + ex.Message |> equal "outer message" + ex.InnerException.Message |> equal "the inner cause" + +[] +let ``test Exception InnerException is null when not provided`` () = + let ex = System.ArgumentException("no inner") + isNull (box ex.InnerException) |> equal true + [] let ``test use doesn't return on finally clause`` () = // See #211 let foo() = diff --git a/tests/Rust/tests/src/TypeTests.fs b/tests/Rust/tests/src/TypeTests.fs index 520564c7b7..752315a872 100644 --- a/tests/Rust/tests/src/TypeTests.fs +++ b/tests/Rust/tests/src/TypeTests.fs @@ -1405,3 +1405,15 @@ let ``Super call works correctly in multi-level generic class hierarchy`` () = obj.Attach("hello") // Each override delegates to base before logging, so the chain unwinds Base -> Mid -> Leaf List.ofSeq log |> equal [ "Base"; "Mid"; "Leaf" ] + +[] +let ``ArgumentException with message and inner exception works`` () = + let inner = exn "the inner cause" + let ex = System.ArgumentException("outer message", inner) + ex.Message |> equal "outer message" + ex.InnerException.Message |> equal "the inner cause" + +[] +let ``Exception InnerException is null when not provided`` () = + let ex = System.ArgumentException("no inner") + isNull (box ex.InnerException) |> equal true