From fefe7c1a2d851a63ba42c0f43f99f2728ec291ff Mon Sep 17 00:00:00 2001 From: Tobiasz Laskowski Date: Tue, 18 Feb 2025 02:32:24 +0000 Subject: [PATCH 1/2] Run toString(null) for explicit nullable abstract If the abstract is declared over Null, then null is part of the type so the user should be able to handle the null case within the toString method. It is also useful to be able to have custom empty values for an abstract that wraps a nullable type. This is also in-line what null safety has to say, as it forces the user to add a null check to toString when the concrete type is Null. --- src/typing/calls.ml | 4 ++-- src/typing/operators.ml | 4 ++-- src/typing/typer.ml | 7 ++++--- 3 files changed, 8 insertions(+), 7 deletions(-) diff --git a/src/typing/calls.ml b/src/typing/calls.ml index 2d6bbf0fb7d..d5b5882dda8 100644 --- a/src/typing/calls.ml +++ b/src/typing/calls.ml @@ -322,7 +322,7 @@ let rec needs_temp_var e = | TField (e, _) | TParenthesis e -> needs_temp_var e | _ -> true -let call_to_string ctx ?(resume=false) e = +let call_to_string ctx ?(resume=false) ?(no_null_check=false) e = if not ctx.allow_transform then { e with etype = ctx.t.tstring } else @@ -333,7 +333,7 @@ let call_to_string ctx ?(resume=false) e = ctx.f.meta <- List.tl ctx.f.meta; build_call ctx acc [] (WithType.with_type ctx.t.tstring) e.epos in - if ctx.com.config.pf_static && not (is_nullable e.etype) then + if no_null_check || (ctx.com.config.pf_static && not (is_nullable e.etype)) then gen_to_string e else begin (* generate `if(e == null) 'null' else e.toString()` *) let string_null = mk (TConst (TString "null")) ctx.t.tstring e.epos in diff --git a/src/typing/operators.ml b/src/typing/operators.ml index 8be57597129..3828ffce3dd 100644 --- a/src/typing/operators.ml +++ b/src/typing/operators.ml @@ -197,8 +197,8 @@ let make_binop ctx op e1 e2 is_assign_op p = let tstring = ctx.t.tstring in let to_string e = let rec loop t = match classify t with - | KAbstract ({a_impl = Some c},_) when PMap.mem "toString" c.cl_statics -> - call_to_string ctx e + | KAbstract ({a_impl = Some c; a_this},_) when PMap.mem "toString" c.cl_statics -> + call_to_string ~no_null_check:(is_explicit_null a_this) ctx e | KInt | KFloat | KString -> e | KUnk | KDyn | KNumParam _ | KStrParam _ | KOther -> Texpr.Builder.resolve_and_make_static_call ctx.com.std "string" [e] e.epos diff --git a/src/typing/typer.ml b/src/typing/typer.ml index d40c70c2cbd..2c4709d0b0c 100644 --- a/src/typing/typer.ml +++ b/src/typing/typer.ml @@ -1603,7 +1603,8 @@ and type_meta ?(mode=MGet) ctx m e1 with_type p = | (Meta.ToString,_,_) -> let e = e() in (match follow e.etype with - | TAbstract({a_impl = Some c},_) when PMap.mem "toString" c.cl_statics -> call_to_string ctx e + | TAbstract({a_impl = Some c; a_this},_) when PMap.mem "toString" c.cl_statics -> + call_to_string ~no_null_check:(is_explicit_null a_this) ctx e | _ -> e) | (Meta.Markup,_,_) -> raise_typing_error "Markup literals must be processed by a macro" p @@ -1725,8 +1726,8 @@ and type_call_builtin ctx e el mode with_type p = let e = type_expr ctx e WithType.value in let infos = type_expr ctx infos WithType.value in let e = match follow e.etype with - | TAbstract({a_impl = Some c},_) when PMap.mem "toString" c.cl_statics -> - call_to_string ctx e + | TAbstract({a_impl = Some c; a_this},_) when PMap.mem "toString" c.cl_statics -> + call_to_string ~no_null_check:(is_explicit_null a_this) ctx e | _ -> e in From 83915ec95f19ae3c8d764d719f97840170ea2cd5 Mon Sep 17 00:00:00 2001 From: Tobiasz Laskowski Date: Tue, 18 Feb 2025 18:21:37 +0000 Subject: [PATCH 2/2] Add test for toString of nullable abstract --- tests/unit/src/unit/issues/Issue12019.hx | 48 ++++++++++++++++++++++++ 1 file changed, 48 insertions(+) create mode 100644 tests/unit/src/unit/issues/Issue12019.hx diff --git a/tests/unit/src/unit/issues/Issue12019.hx b/tests/unit/src/unit/issues/Issue12019.hx new file mode 100644 index 00000000000..4c3a7e75cc1 --- /dev/null +++ b/tests/unit/src/unit/issues/Issue12019.hx @@ -0,0 +1,48 @@ +package unit.issues; + +private abstract MyStringA(Null) from Null { + function toString() { + if (this == null) + return "EMPTY"; + return this; + } +} + +private typedef NullableString = Null; + +private abstract MyStringB(NullableString) from NullableString { + function toString() { + if (this == null) + return "EMPTY"; + return this; + } +} + +class Issue12019 extends unit.Test { + final a:MyStringA = null; + final b:MyStringB = null; + + var oldTrace:(Dynamic, ?Null) -> Void; + + function setup() { + oldTrace = haxe.Log.trace; + } + + function teardown() { + haxe.Log.trace = oldTrace; + } + + function testTrace() { + haxe.Log.trace = function(v, ?infos) { + eq("EMPTY", v); + }; + + trace(a); + trace(b); + } + + function testConcatenate() { + eq("Concatenated: EMPTY", "Concatenated: " + a); + eq("Concatenated: EMPTY", "Concatenated: " + b); + } +}