Skip to content

Commit 86453bb

Browse files
cknittclaude
andcommitted
Stop warning 20 on calls of open JS object methods
Why: Calling a method read from an open JS object type, such as `obj["getData"]("text/plain")` with `obj: {..}`, reported warning 20 "this argument will not be used by the function". The read adds the method to the object's open row with a fresh type variable as its type, and type_application takes a callee whose type is a fresh variable as a function that never returns. The JS method does receive its arguments. Fixes #8159. The read of a field that the open row gains now records that field's type, and the application skips the warning only when its callee is that read. Callees that cannot return still warn, including field reads on them: `throw(e)(1)`, `throw(e)["m"](x)`, `{"m": throw(e)}["m"](x)`. Diagnostic removed (warning 20 on open object methods): Example: @get external clipboardData: string => {..} = "clipboardData" let text = clipboardData("event")["getData"]("text/plain") let fromThrow = () => throw(Failure("boom"))["m"](Console.log("b")) Before (diagnostic): Warning number 20 Ex.res:2:46-57 this argument will not be used by the function. Warning number 20 Ex.res:3:51-66 this argument will not be used by the function. After (diagnostic): Warning number 20 Ex.res:3:51-66 this argument will not be used by the function. Review: Why does the read record the field type instead of the typed tree marking the callee? The Pexp_apply case clears the record, types the callee and reads the record right away, comparing it with the callee's type by physical equality, so only the callee's own read can match. It passes the result to type_application as ~open_row_method. A typed-tree flag would change Texp_object_get and the .cmt format for a warning. Review: Can a read inside an argument replace the callee's record? No: the check runs before any argument is typed. The fixture covers arguments that read closed and open object fields, first or last, and nested method calls. Review: Why a Request_local key rather than a plain ref? Requests type-checked concurrently on separate domains would share a ref, and one could overwrite another's record between typing a callee and checking it. An ounit test compiles open-object method calls in two concurrent requests with warning 20 on and expects none; it failed on every run with a shared ref. Review: Does a method read before the call, `let o = f("e")` then `o["m"]` and `o["m"]("used")`, need a record of every field? No: the binding keeps the object type at the binding's level, below the level of the later call, so the fresh-variable check does not apply. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Kbf3gJavp7rWUAfzV9rjFj Signed-off-by: Christoph Knittel <ck@cca.io>
1 parent f88294f commit 86453bb

5 files changed

Lines changed: 146 additions & 4 deletions

File tree

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
- Fix warning 20 ("this argument will not be used by the function") firing on calls of methods of open JS object types, such as `obj["getData"]("text/plain")`.

‎compiler/ml/typecore.ml‎

Lines changed: 43 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2542,6 +2542,15 @@ let should_unify_expected_result_before_typing_lowered_apply funct sargs =
25422542
25432543
type targs = (Asttypes.arg_label * Typedtree.expression option) list
25442544
2545+
(* The type of the most recent object field read if that read added the field
2546+
to an open object row ([obj["m"]] with [obj: {..}]), [None] after any other
2547+
read. Only the [Pexp_apply] case reads it: it resets it before typing the
2548+
callee and checks it right after, where it is only meaningful when the
2549+
callee itself is such a read. Request-local, so requests type-checked
2550+
concurrently on other domains cannot overwrite it in between. *)
2551+
let open_row_field_read : type_expr option ref Request_local.key =
2552+
Request_local.new_key (fun () -> ref None)
2553+
25452554
(* Eliminate an object-field scheme for a read. A declared field payload is
25462555
wrapped in [Tpoly]: an empty binder list needs ordinary instantiation, while
25472556
a nonempty list needs polymorphic instantiation. An inferred field whose
@@ -2810,9 +2819,19 @@ and type_expect_ ?deprecated_context ~context ?(recarg = Rejected)
28102819
assert (sargs <> []);
28112820
begin_def ();
28122821
(* one more level for non-returning functions *)
2822+
Request_local.get open_row_field_read := None;
28132823
let funct =
28142824
type_exp ~deprecated_context:FunctionCall ~context:None env sfunct
28152825
in
2826+
(* Whether the callee is a JS object method that its read added to an open
2827+
object row ([obj["m"]] with [obj: {..}]). Its type is a fresh variable
2828+
like that of a callee that never returns, but the method receives its
2829+
arguments, so [type_application] must not report them as unused. *)
2830+
let open_row_method =
2831+
match (funct.exp_desc, !(Request_local.get open_row_field_read)) with
2832+
| Texp_object_get _, Some ty -> repr ty == repr funct.exp_type
2833+
| _ -> false
2834+
in
28162835
(if should_unify_expected_result_before_typing_lowered_apply funct sargs
28172836
then
28182837
(* The async wrapper is lowered to a regular application, so thread the
@@ -2835,7 +2854,8 @@ and type_expect_ ?deprecated_context ~context ?(recarg = Rejected)
28352854
let args, ty_res, fully_applied =
28362855
match translate_unified_ops env funct sargs with
28372856
| Some (targs, result_type) -> (targs, result_type, true)
2838-
| None -> type_application ~context total_app env funct sargs
2857+
| None ->
2858+
type_application ~context ~open_row_method total_app env funct sargs
28392859
in
28402860
end_def ();
28412861
if transformed_jsx then check_jsx_props ~loc env args;
@@ -3666,11 +3686,23 @@ and type_expect_ ?deprecated_context ~context ?(recarg = Rejected)
36663686
}
36673687
| Pexp_object_get (e, ({txt = met} as met_loc)) -> (
36683688
let obj = type_exp ~context:None env e in
3689+
(* An object type without the field gets it from its open row (or the
3690+
lookup fails). An object whose type is still a variable, such as
3691+
[failwith("x")], does not count. *)
3692+
let adds_field_to_row =
3693+
match (expand_head env obj.exp_type).desc with
3694+
| Tobject fields ->
3695+
let fields, _ = Ctype.flatten_fields fields in
3696+
not (List.exists (fun (f : Ctype.field_info) -> f.f_name = met) fields)
3697+
| _ -> false
3698+
in
36693699
try
36703700
(* Lookup constrains an open object row to contain the field, without
36713701
requiring or adding write capability. *)
36723702
let typ = filter_method env met obj.exp_type in
36733703
let typ = object_field_use_type env typ in
3704+
Request_local.get open_row_field_read
3705+
:= if adds_field_to_row then Some typ else None;
36743706
rue
36753707
{
36763708
exp_desc = Texp_object_get (obj, met_loc);
@@ -4475,8 +4507,8 @@ and translate_unified_ops (env : Env.t) (funct : Typedtree.expression)
44754507
| _ -> None)
44764508
| _ -> None
44774509
4478-
and type_application ~context total_app env funct (sargs : sargs) :
4479-
targs * Types.type_expr * bool =
4510+
and type_application ~context ~open_row_method total_app env funct
4511+
(sargs : sargs) : targs * Types.type_expr * bool =
44804512
match sargs with
44814513
(* Special case for ignore: avoid discarding warning *)
44824514
| [(Nolabel, sarg)] when is_ignore ~env funct ->
@@ -4505,9 +4537,16 @@ and type_application ~context total_app env funct (sargs : sargs) :
45054537
List.map (fun (l1, _) -> {Types.lbl = l1; typ = newvar ()}) sargs
45064538
in
45074539
let ty_ret = newvar () in
4540+
(* A fresh type variable as function type usually means the function
4541+
never returns (e.g. [failwith("x")(1)]), so the arguments are
4542+
unused. A JS object method that the read added to an open object
4543+
row ([obj["m"]] with [obj: {..}]) also gets a fresh variable, but it
4544+
is an ordinary JS method that receives its arguments
4545+
([open_row_method]). *)
45084546
if
45094547
ty_fun.level >= ty_ret.level
4510-
&& not (is_identity_coercion funct.exp_desc)
4548+
&& (not (is_identity_coercion funct.exp_desc))
4549+
&& not open_row_method
45114550
then
45124551
List.iter
45134552
(fun (_, sarg1) ->
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
2+
Warning number 20
3+
/.../fixtures/unused_argument_object_method.res:29:51
4+
5+
27 │
6+
28 │ // A function that never returns ignores its arguments: warning 20.
7+
29 │ let ignored = () => throw(Failure("unreachable"))(1)
8+
30 │
9+
31 │ // A field read that cannot return ignores its arguments: warning 20.
10+
11+
this argument will not be used by the function.
12+
13+
14+
Warning number 20
15+
/.../fixtures/unused_argument_object_method.res:32:62-77
16+
17+
30 │
18+
31 │ // A field read that cannot return ignores its arguments: warning 20.
19+
32 │ let fromLiteral = () => ({"m": throw(Failure("boom"))})["m"](Console.lo
20+
│ g("a"))
21+
33 │ let fromThrow = () => throw(Failure("boom"))["m"](Console.log("b"))
22+
34 │
23+
24+
this argument will not be used by the function.
25+
26+
27+
Warning number 20
28+
/.../fixtures/unused_argument_object_method.res:33:51-66
29+
30+
31 │ // A field read that cannot return ignores its arguments: warning 20.
31+
32 │ let fromLiteral = () => ({"m": throw(Failure("boom"))})["m"](Console.lo
32+
│ g("a"))
33+
33 │ let fromThrow = () => throw(Failure("boom"))["m"](Console.log("b"))
34+
34 │
35+
36+
this argument will not be used by the function.
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
@get external clipboardData: string => {..} = "clipboardData"
2+
3+
// A method of an open JS object type receives its arguments: no warning 20.
4+
let text = clipboardData("event")["getData"]("text/plain")
5+
6+
// Arguments that read object fields do not change that.
7+
let closed: {"x": string} = {"x": "x"}
8+
let closedArg = clipboardData("e")["getData"](closed["x"])
9+
let openArg = clipboardData("e")["getData"](clipboardData("o")["x"])
10+
let closedLast = clipboardData("e")["m"]("a", closed["x"])
11+
let closedFirst = clipboardData("e")["m"](closed["x"], "a")
12+
let openLast = clipboardData("e")["m"]("a", clipboardData("o")["x"])
13+
let openFirst = clipboardData("e")["m"](clipboardData("o")["x"], "a")
14+
let nested = clipboardData("e")["m"](clipboardData("f")["n"]("y"))
15+
16+
// Reading the method before calling it does not change that either.
17+
let retained = clipboardData("e")
18+
let first = retained["m"]
19+
let second = retained["m"]("used")
20+
21+
let readElsewhere = () => {
22+
let o = clipboardData("e")
23+
let get = () => o["m"]
24+
o["m"]("used")->ignore
25+
get
26+
}
27+
28+
// A function that never returns ignores its arguments: warning 20.
29+
let ignored = () => throw(Failure("unreachable"))(1)
30+
31+
// A field read that cannot return ignores its arguments: warning 20.
32+
let fromLiteral = () => ({"m": throw(Failure("boom"))})["m"](Console.log("a"))
33+
let fromThrow = () => throw(Failure("boom"))["m"](Console.log("b"))

‎tests/ounit_tests/ounit_compiler_request_tests.ml‎

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1505,6 +1505,38 @@ let concurrent_jsx_diagnostic_tests _context =
15051505
assert_equal right_expected.stderr (Domain.join right).stderr
15061506
done)
15071507

1508+
(* Calls of open JS object methods mark their callee in request-local typing
1509+
state. Another request typing applications at the same time must not
1510+
overwrite that mark, which would report the method's arguments as unused
1511+
(warning 20, off by default and enabled here). *)
1512+
let concurrent_open_object_method_tests _context =
1513+
with_temp_dir "rewatch-driver-open-method-" (fun root ->
1514+
let source =
1515+
"@get external obj: string => {..} = \"obj\"\n"
1516+
^ String.concat ""
1517+
(List.init 200 (fun i ->
1518+
Printf.sprintf "let v%d = obj(\"e\")[\"m\"](\"a\", \"b\")\n" i))
1519+
in
1520+
let setup name =
1521+
let directory = Filename.concat root name in
1522+
ensure_dir directory;
1523+
write directory "Calls.res" source;
1524+
fun () -> snd (run directory ~extra:["-w"; "+20"] "Calls.res")
1525+
in
1526+
let left_request = setup "left" in
1527+
let right_request = setup "right" in
1528+
let expect_clean result =
1529+
expect_code 0 result;
1530+
assert_equal ~printer:Fun.id "" result.Rescript_compiler_driver.stderr
1531+
in
1532+
expect_clean (left_request ());
1533+
for _ = 1 to 5 do
1534+
let left = Domain.spawn left_request in
1535+
let right = Domain.spawn right_request in
1536+
expect_clean (Domain.join left);
1537+
expect_clean (Domain.join right)
1538+
done)
1539+
15081540
(* Exercise concurrent compiler requests without the process-worker boundary. *)
15091541
let concurrent_request_isolation_tests _context =
15101542
with_temp_dir "rewatch-driver-domains-" (fun root ->
@@ -1710,6 +1742,7 @@ let suites =
17101742
"concurrent_diagnostic_recovery"
17111743
>:: concurrent_diagnostic_recovery_tests;
17121744
"concurrent_jsx_diagnostic" >:: concurrent_jsx_diagnostic_tests;
1745+
"concurrent_open_object_method" >:: concurrent_open_object_method_tests;
17131746
"concurrent_request_isolation" >:: concurrent_request_isolation_tests;
17141747
"concurrent_async_context" >:: concurrent_async_context_tests;
17151748
]

0 commit comments

Comments
 (0)