Skip to content

Commit f7220bf

Browse files
cknittclaude
andcommitted
Explain a JSX tag whose module has no make
`<Button />` refers to `Button.make`. When `Button` exists but defines no `make` value, the type checker now says that the module can't be used as a JSX component and shows what a component module looks like, instead of the generic "The value make can't be found in Button". The spellcheck hint for a near miss such as `mak` is kept. A missing module, a functor, and a `make` that is not a component report as before. Example (rescript): module Ui = { module Button = { let label = "x" } } let x = <Ui.Button /> Before (text): The value make can't be found in Ui.Button After (text): The module Ui.Button can't be used as a JSX component, because it has no make value. A component module defines its component as make, for example: @react.component let make = (~label) => ... Review: How does the type checker know the identifier is a JSX tag? The JSX transform passes the tag as the first argument of a `transformed_jsx` application, which the type checker already types with the `JsxComponent` context. The new error replaces only an unbound `<Module>.make` in that context; the lookup reports an unbound module before it gets there. Area: typechecker tests Output: unchanged Formats: none Editor: editor diagnostics show the new message; no analysis fixture changes Errors: +Jsx_component_without_make (jsx_component_without_make.res, jsx_component_without_make_typo.res) Tests: +tests/build_tests/super_errors/fixtures/jsx_component_without_make.res +tests/build_tests/super_errors/fixtures/jsx_component_without_make_typo.res PR: #8885 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014hcyfimX2ss8qj5MZwdMdP Signed-off-by: Christoph Knittel <ck@cca.io>
1 parent 9a1c88b commit f7220bf

10 files changed

Lines changed: 119 additions & 7 deletions
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
- Explain that a JSX tag needs a component module, with a `make` value, when the module it names has none. https://github.com/rescript-lang/rescript/pull/8885

‎compiler/ml/typecore.ml‎

Lines changed: 28 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,7 @@ type error =
9898
| Type_params_not_supported of Longident.t
9999
| Field_access_on_dict_type
100100
| Jsx_not_enabled
101+
| Jsx_component_without_make of Longident.t
101102
| Jsx_unboxed_props of type_expr
102103
| Tagged_template_non_tag of type_expr
103104
| Record_rest of Typecore_record_rest.error
@@ -2530,12 +2531,20 @@ and type_expect_ ?deprecated_context ~context ?(recarg = Rejected) env sexp
25302531
{exp with exp_attributes = sexp.pexp_attributes @ exp.exp_attributes}
25312532
| Pexp_ident lid ->
25322533
let path, desc =
2533-
Typetexp.find_value
2534-
?deprecated_context:
2535-
(match deprecated_context with
2536-
| None -> Some Reference
2537-
| v -> v)
2538-
env lid.loc lid.txt
2534+
try
2535+
Typetexp.find_value
2536+
?deprecated_context:
2537+
(match deprecated_context with
2538+
| None -> Some Reference
2539+
| v -> v)
2540+
env lid.loc lid.txt
2541+
with
2542+
| Typetexp.Error (_, _, Unbound_value (Ldot (module_lid, "make")))
2543+
when context = Some JsxComponent
2544+
->
2545+
(* [<Button />] refers to [Button.make]. The module exists (otherwise
2546+
the lookup reports it as unbound), but it defines no component. *)
2547+
raise (Error (lid.loc, env, Jsx_component_without_make module_lid))
25392548
in
25402549
let is_recarg =
25412550
match (repr desc.val_type).desc with
@@ -5570,6 +5579,19 @@ let report_error env loc ppf error =
55705579
fprintf ppf
55715580
"Cannot compile JSX expression because JSX support is not enabled. Add \
55725581
\"jsx\" settings to rescript.json to enable JSX support."
5582+
| Jsx_component_without_make module_lid ->
5583+
fprintf ppf
5584+
"@[<v>The module @{<info>%a@} can't be used as a JSX component, because \
5585+
it has no @{<info>make@} value.@,\
5586+
@,\
5587+
A component module defines its component as @{<info>make@}, for \
5588+
example:@,\
5589+
@,\
5590+
\ @{<info>@react.component@}@,\
5591+
\ let make = (~label) => ...@]"
5592+
longident module_lid;
5593+
ignore
5594+
(Typetexp.spellcheck_value ppf env (Longident.Ldot (module_lid, "make")))
55735595
| Jsx_unboxed_props typ ->
55745596
fprintf ppf
55755597
"@[<v>The props of this JSX element have type %a, which is an \

‎compiler/ml/typecore.mli‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,7 @@ type error =
114114
| Type_params_not_supported of Longident.t
115115
| Field_access_on_dict_type
116116
| Jsx_not_enabled
117+
| Jsx_component_without_make of Longident.t
117118
| Jsx_unboxed_props of type_expr
118119
| Tagged_template_non_tag of type_expr
119120
| Record_rest of Typecore_record_rest.error

‎compiler/ml/typetexp.ml‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -785,6 +785,8 @@ let super_spellcheck ppf fold env lid =
785785
| Longident.Lident s -> did_you_mean ppf (fun _ -> choices None s)
786786
| Longident.Ldot (r, s) -> did_you_mean ppf (fun _ -> choices (Some r) s)
787787

788+
let spellcheck_value ppf env lid = super_spellcheck ppf Env.fold_values env lid
789+
788790
let spellcheck ppf fold env lid =
789791
let choices ~path name =
790792
let env = fold (fun x xs -> x :: xs) path env [] in
@@ -890,7 +892,7 @@ let report_error env ppf = function
890892
| other_ident ->
891893
Format.fprintf ppf "The value %a can't be found" Printtyp.longident
892894
other_ident);
893-
let did_spellcheck = super_spellcheck ppf Env.fold_values env lid in
895+
let did_spellcheck = spellcheck_value ppf env lid in
894896
(* For cases such as when the user refers to something that's a value with
895897
a lowercase identifier in JS but a module in ReScript.
896898

‎compiler/ml/typetexp.mli‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,10 @@ exception Error of Location.t * Env.t * error
6464

6565
val report_error : Env.t -> Format.formatter -> error -> unit
6666

67+
val spellcheck_value : Format.formatter -> Env.t -> Longident.t -> bool
68+
(** Print a hint naming the values in scope whose names are close to the given
69+
one. Returns whether it printed one. *)
70+
6771
(* Support for first-class modules. *)
6872
val transl_modtype_longident :
6973
(* from Typemod *)

‎tests/ERROR_VARIANTS.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -252,6 +252,7 @@ Source: `type error` in [typecore.ml](../compiler/ml/typecore.ml).
252252
| `Type_params_not_supported` | ✓ | `variant_spread_pattern_type_params.res` | Pattern-level variant spread (`| ...a as v`) where `a` has type params; typedecl path covered by `variant_spread_type_parameters.res`. |
253253
| `Field_access_on_dict_type` | ✓ | `field_access_on_dict_type.res` | |
254254
| `Jsx_not_enabled` | ☐ (needs harness flag) | — | typecore.ml, the `Pexp_jsx_element` cases of `iter_expression` and `type_expect_`. Fires when JSX is used without `-bs-jsx N`. The `super_errors` runner hard-codes `-bs-jsx 4` in `bscFlags`; adding a per-fixture opt-out (e.g. a `.opts` sidecar) would expose this. Until then, it's reachable in real code but blocked at the harness level. |
255+
| `Jsx_component_without_make` | ✓ | `jsx_component_without_make.res`, `jsx_component_without_make_typo.res` | A JSX tag naming a module that has no `make` value; the typo fixture pins the spellcheck hint. |
255256
| `Jsx_unboxed_props` | ✓ | `jsx_unboxed_props.res`, `jsx_unboxed_props_spread.res` | A JSX element whose props have an `@unboxed` record type, given as attributes or as a spread. |
256257
| `Tagged_template_non_tag` | ✓ | `tagged_template_non_tag.res` | Backtick tagged-template syntax used on a value whose type is not `taggedTemplate<'param, 'output>`. |
257258

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
2+
We've found a bug for you!
3+
/.../fixtures/jsx_component_without_make.res:24:10-18
4+
5+
22 │ }
6+
23 │
7+
24 │ let x = <Ui.Button />
8+
25 │
9+
10+
The module Ui.Button can't be used as a JSX component, because it has no make value.
11+
12+
A component module defines its component as make, for example:
13+
14+
@react.component
15+
let make = (~label) => ...
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
2+
We've found a bug for you!
3+
/.../fixtures/jsx_component_without_make_typo.res:25:10-15
4+
5+
23 │ }
6+
24 │
7+
25 │ let x = <Button label="a" />
8+
26 │
9+
10+
The module Button can't be used as a JSX component, because it has no make value.
11+
12+
A component module defines its component as make, for example:
13+
14+
@react.component
15+
let make = (~label) => ...
16+
17+
Hint: Did you mean mak?
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
@@config({
2+
flags: ["-bs-jsx", "4"],
3+
})
4+
5+
module React = {
6+
type element = Jsx.element
7+
type componentLike<'props, 'return> = 'props => 'return
8+
type component<'props> = Jsx.component<'props>
9+
external component: componentLike<'props, element> => component<'props> = "%component_identity"
10+
11+
@module("react/jsx-runtime")
12+
external jsx: (component<'props>, 'props) => element = "jsx"
13+
14+
type fragmentProps = {children?: element}
15+
@module("react/jsx-runtime") external jsxFragment: component<fragmentProps> = "Fragment"
16+
}
17+
18+
module Ui = {
19+
module Button = {
20+
let label = "x"
21+
}
22+
}
23+
24+
let x = <Ui.Button />
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
@@config({
2+
flags: ["-bs-jsx", "4"],
3+
})
4+
5+
module React = {
6+
type element = Jsx.element
7+
type componentLike<'props, 'return> = 'props => 'return
8+
type component<'props> = Jsx.component<'props>
9+
external component: componentLike<'props, element> => component<'props> = "%component_identity"
10+
11+
@module("react/jsx-runtime")
12+
external jsx: (component<'props>, 'props) => element = "jsx"
13+
14+
type fragmentProps = {children?: element}
15+
@module("react/jsx-runtime") external jsxFragment: component<fragmentProps> = "Fragment"
16+
17+
external string: string => element = "%identity"
18+
}
19+
20+
module Button = {
21+
@react.component
22+
let mak = (~label) => React.string(label)
23+
}
24+
25+
let x = <Button label="a" />

0 commit comments

Comments
 (0)