Skip to content

Commit b3fbd49

Browse files
committed
iron out a few more error messages
1 parent f35a3c3 commit b3fbd49

11 files changed

+228
-39
lines changed

compiler/ml/error_message_utils.ml

Lines changed: 85 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -69,10 +69,21 @@ let type_expr ppf typ =
6969
Printtyp.reset_and_mark_loops typ;
7070
Printtyp.type_expr ppf typ
7171
72+
type jsx_prop_error_info = {
73+
fields: Types.label_declaration list;
74+
props_record_path: Path.t;
75+
jsx_type: [`Fragment | `CustomComponent | `LowercaseComponent];
76+
}
77+
7278
type type_clash_statement = FunctionCall
7379
type type_clash_context =
7480
| SetRecordField of string (* field name *)
75-
| RecordField of string (* field name *)
81+
| RecordField of {
82+
jsx: jsx_prop_error_info option;
83+
record_type: Types.type_expr;
84+
field_name: string;
85+
optional: bool;
86+
}
7687
| ArrayValue
7788
| MaybeUnwrapOption
7889
| IfCondition
@@ -88,7 +99,7 @@ type type_clash_context =
8899
operator: string;
89100
is_constant: string option;
90101
}
91-
| FunctionArgument
102+
| FunctionArgument of {optional: bool}
92103
| Statement of type_clash_statement
93104
| ForLoopCondition
94105
@@ -106,7 +117,7 @@ let context_to_string = function
106117
| Some SwitchReturn -> "SwitchReturn"
107118
| Some TryReturn -> "TryReturn"
108119
| Some StringConcat -> "StringConcat"
109-
| Some FunctionArgument -> "FunctionArgument"
120+
| Some (FunctionArgument _) -> "FunctionArgument"
110121
| Some ComparisonOperator -> "ComparisonOperator"
111122
| Some IfReturn -> "IfReturn"
112123
| None -> "None"
@@ -127,8 +138,11 @@ let error_type_text ppf type_clash_context =
127138
128139
let error_expected_type_text ppf type_clash_context =
129140
match type_clash_context with
130-
| Some FunctionArgument ->
131-
fprintf ppf "But this function argument is expecting:"
141+
| Some (FunctionArgument {optional}) ->
142+
fprintf ppf "But this%s function argument is expecting:"
143+
(match optional with
144+
| false -> ""
145+
| true -> " optional")
132146
| Some ComparisonOperator ->
133147
fprintf ppf "But it's being compared to something of type:"
134148
| Some SwitchReturn -> fprintf ppf "But this switch is expected to return:"
@@ -145,7 +159,19 @@ let error_expected_type_text ppf type_clash_context =
145159
| Some ArrayValue ->
146160
fprintf ppf "But this array is expected to have items of type:"
147161
| Some (SetRecordField _) -> fprintf ppf "But this record field is of type:"
148-
| Some (RecordField field_name) ->
162+
| Some
163+
(RecordField {field_name = "children"; jsx = Some {jsx_type = `Fragment}})
164+
->
165+
fprintf ppf "But children of JSX fragments is expected to have type:"
166+
| Some
167+
(RecordField
168+
{field_name = "children"; jsx = Some {jsx_type = `CustomComponent}}) ->
169+
fprintf ppf
170+
"But children passed to this component is expected to have type:"
171+
| Some (RecordField {field_name; jsx = Some _}) ->
172+
fprintf ppf "But this component prop @{<info>%s@} is expected to have type:"
173+
field_name
174+
| Some (RecordField {field_name}) ->
149175
fprintf ppf "But this record field @{<info>%s@} is expected to have type:"
150176
field_name
151177
| Some (Statement FunctionCall) -> fprintf ppf "But it's expected to return:"
@@ -395,6 +421,40 @@ let print_extra_type_clash_help ~extract_concrete_typedecl ~env loc ppf
395421
single JSX element.@,"
396422
(with_configured_jsx_module "array")
397423
| _ -> ())
424+
| ( Some (RecordField {optional = true; field_name}),
425+
Some ({desc = Tconstr (p, _, _)}, _) )
426+
when Path.same Predef.path_option p ->
427+
fprintf ppf
428+
"@,\
429+
@,\
430+
@{<info>%s@} is an optional record field, and you're passing an \
431+
optional value to it.@,\
432+
Optional fields expect you to pass the concrete value, not an option, \
433+
when passed directly.\n\
434+
\ @,\
435+
Possible solutions: @,\
436+
- Unwrap the option and pass a concrete value directly@,\
437+
- If you really do want to pass the optional value, prepend the value \
438+
with @{<info>?@} to show you want to pass the option, like: \
439+
@{<info>{%s: ?%s@}}"
440+
field_name field_name
441+
(Parser.extract_text_at_loc loc)
442+
| ( Some (FunctionArgument {optional = true}),
443+
Some ({desc = Tconstr (p, _, _)}, _) )
444+
when Path.same Predef.path_option p ->
445+
fprintf ppf
446+
"@,\
447+
@,\
448+
You're passing an optional value into an optional function argument.@,\
449+
Optional function arguments expect you to pass the concrete value, not \
450+
an option, when passed directly.\n\
451+
\ @,\
452+
Possible solutions: @,\
453+
- Unwrap the option and pass a concrete value directly@,\
454+
- If you really do want to pass the optional value, prepend the value \
455+
with @{<info>?@} to show you want to pass the option, like: \
456+
@{<info>?%s@}"
457+
(Parser.extract_text_at_loc loc)
398458
| _, Some (t1, t2) ->
399459
let is_subtype =
400460
try
@@ -450,7 +510,7 @@ let type_clash_context_from_function sexp sfunct =
450510
Some (MathOperator {for_float = true; operator; is_constant})
451511
| Pexp_ident {txt = Lident (("/" | "*" | "+" | "-") as operator)} ->
452512
Some (MathOperator {for_float = false; operator; is_constant})
453-
| _ -> Some FunctionArgument
513+
| _ -> Some (FunctionArgument {optional = false})
454514
455515
let type_clash_context_for_function_argument type_clash_context sarg0 =
456516
match type_clash_context with
@@ -508,11 +568,6 @@ let print_contextual_unification_error ppf t1 t2 =
508568
the highlighted pattern in @{<info>Some()@} to make it work.@]"
509569
| _ -> ()
510570
511-
type jsx_prop_error_info = {
512-
fields: Types.label_declaration list;
513-
props_record_path: Path.t;
514-
}
515-
516571
let attributes_include_jsx_component_props (attrs : Parsetree.attributes) =
517572
attrs
518573
|> List.exists (fun ({Location.txt}, _) -> txt = "res.jsxComponentProps")
@@ -524,18 +579,24 @@ let path_to_jsx_component_name p =
524579
525580
let get_jsx_component_props
526581
~(extract_concrete_typedecl : extract_concrete_typedecl) env ty p =
527-
match Path.last p with
528-
| "props" -> (
529-
try
530-
match extract_concrete_typedecl env ty with
531-
| ( _p0,
532-
_p,
533-
{Types.type_kind = Type_record (fields, _repr); type_attributes} )
534-
when attributes_include_jsx_component_props type_attributes ->
535-
Some {props_record_path = p; fields}
536-
| _ -> None
537-
with _ -> None)
538-
| _ -> None
582+
match p with
583+
| Path.Pdot (Path.Pident {Ident.name = jsx_module_name}, "fragmentProps", _)
584+
when Some jsx_module_name = !configured_jsx_module ->
585+
Some {props_record_path = p; fields = []; jsx_type = `Fragment}
586+
| _ -> (
587+
(* TODO: handle lowercase components using JSXDOM.domProps *)
588+
match Path.last p with
589+
| "props" -> (
590+
try
591+
match extract_concrete_typedecl env ty with
592+
| ( _p0,
593+
_p,
594+
{Types.type_kind = Type_record (fields, _repr); type_attributes} )
595+
when attributes_include_jsx_component_props type_attributes ->
596+
Some {props_record_path = p; fields; jsx_type = `CustomComponent}
597+
| _ -> None
598+
with _ -> None)
599+
| _ -> None)
539600
540601
let print_component_name ppf (p : Path.t) =
541602
match path_to_jsx_component_name p with

compiler/ml/typecore.ml

Lines changed: 32 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2503,7 +2503,7 @@ and type_expect_ ~context ?in_function ?(recarg = Rejected) env sexp ty_expected
25032503
exp_env = env;
25042504
}
25052505
| Pexp_construct (lid, sarg) ->
2506-
type_construct env loc lid sarg ty_expected sexp.pexp_attributes
2506+
type_construct ~context env loc lid sarg ty_expected sexp.pexp_attributes
25072507
| Pexp_variant (l, sarg) -> (
25082508
(* Keep sharing *)
25092509
let ty_expected0 = instance env ty_expected in
@@ -2570,13 +2570,15 @@ and type_expect_ ~context ?in_function ?(recarg = Rejected) env sexp ty_expected
25702570
get_jsx_component_props ~extract_concrete_typedecl env ty_record p
25712571
| None -> None
25722572
in
2573+
(* React.fragmentProps, JSXDOM.domProps *)
2574+
let jsx_component_error_info = get_jsx_component_error_info () in
25732575
let lbl_exp_list =
25742576
wrap_disambiguate "This record expression is expected to have" ty_record
25752577
(type_record_elem_list loc true env
25762578
(fun e k ->
25772579
k
2578-
(type_label_exp ~call_context:`Regular true env loc ty_record
2579-
(process_optional_label e)))
2580+
(type_label_exp ~call_context:(`Regular jsx_component_error_info)
2581+
true env loc ty_record (process_optional_label e)))
25802582
opath lid_sexp_list)
25812583
(fun x -> x)
25822584
in
@@ -2601,7 +2603,7 @@ and type_expect_ ~context ?in_function ?(recarg = Rejected) env sexp ty_expected
26012603
Labels_missing
26022604
{
26032605
labels = labels_missing;
2604-
jsx_component_info = get_jsx_component_error_info ();
2606+
jsx_component_info = jsx_component_error_info;
26052607
} ));
26062608
([||], representation)
26072609
| [], _ ->
@@ -2634,7 +2636,7 @@ and type_expect_ ~context ?in_function ?(recarg = Rejected) env sexp ty_expected
26342636
Labels_missing
26352637
{
26362638
labels = List.rev !labels_missing;
2637-
jsx_component_info = get_jsx_component_error_info ();
2639+
jsx_component_info = jsx_component_error_info;
26382640
} ));
26392641
let fields =
26402642
Array.map2
@@ -2680,13 +2682,14 @@ and type_expect_ ~context ?in_function ?(recarg = Rejected) env sexp ty_expected
26802682
ty_record
26812683
in
26822684
let closed = false in
2685+
let jsx_component_error_info = get_jsx_component_error_info () in
26832686
let lbl_exp_list =
26842687
wrap_disambiguate "This record expression is expected to have" ty_record
26852688
(type_record_elem_list loc closed env
26862689
(fun e k ->
26872690
k
2688-
(type_label_exp ~call_context:`Regular true env loc ty_record
2689-
(process_optional_label e)))
2691+
(type_label_exp ~call_context:(`Regular jsx_component_error_info)
2692+
true env loc ty_record (process_optional_label e)))
26902693
opath lid_sexp_list)
26912694
(fun x -> x)
26922695
in
@@ -3296,8 +3299,8 @@ and type_label_access env srecord lid =
32963299
(* Typing format strings for printing or reading.
32973300
These formats are used by functions in modules Printf, Format, and Scanf.
32983301
(Handling of * modifiers contributed by Thorsten Ohl.) *)
3299-
and type_label_exp ~(call_context : [`SetRecordField | `Regular]) create env loc
3300-
ty_expected (lid, label, sarg, opt) =
3302+
and type_label_exp ~call_context create env loc ty_expected
3303+
(lid, label, sarg, opt) =
33013304
(* Here also ty_expected may be at generic_level *)
33023305
begin_def ();
33033306
let separate = Env.has_local_constraints env in
@@ -3328,7 +3331,10 @@ and type_label_exp ~(call_context : [`SetRecordField | `Regular]) create env loc
33283331
let field_context =
33293332
match call_context with
33303333
| `SetRecordField -> Some (Error_message_utils.SetRecordField field_name)
3331-
| `Regular -> Some (Error_message_utils.RecordField field_name)
3334+
| `Regular jsx ->
3335+
Some
3336+
(Error_message_utils.RecordField
3337+
{jsx; record_type = ty_expected; field_name; optional = false})
33323338
in
33333339
let arg =
33343340
type_argument ~context:field_context env sarg ty_arg (instance env ty_arg)
@@ -3692,7 +3698,7 @@ and type_application ~context total_app env funct (sargs : sargs) :
36923698
in
36933699
(targs, ret_t, fully_applied)
36943700
3695-
and type_construct env loc lid sarg ty_expected attrs =
3701+
and type_construct ~context env loc lid sarg ty_expected attrs =
36963702
let opath =
36973703
try
36983704
let p0, p, _ = extract_concrete_variant env ty_expected in
@@ -3739,7 +3745,21 @@ and type_construct env loc lid sarg ty_expected attrs =
37393745
exp_env = env;
37403746
}
37413747
in
3742-
let context = type_clash_context_maybe_option ty_expected ty_res in
3748+
(* Forward context if this is a Some constructor injected (meaning it's
3749+
an optional field or an optional argument) *)
3750+
let context =
3751+
match lid.txt with
3752+
| Longident.Ldot (Lident "*predef*", "Some") -> (
3753+
match context with
3754+
| Some (RecordField {record_type; jsx; field_name}) ->
3755+
Some
3756+
(Error_message_utils.RecordField
3757+
{record_type; jsx; field_name; optional = true})
3758+
| Some (FunctionArgument _) ->
3759+
Some (Error_message_utils.FunctionArgument {optional = true})
3760+
| _ -> None)
3761+
| _ -> None
3762+
in
37433763
if separate then (
37443764
end_def ();
37453765
generalize_structure ty_res;
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
2+
We've found a bug for you!
3+
/.../fixtures/jsx_custom_component_children.res:24:28-29
4+
5+
22 │ }
6+
23 │
7+
24 │ let x = <CustomComponent> {1.} </CustomComponent>
8+
25 │
9+
10+
This has type: float
11+
But children passed to this component is expected to have type:
12+
React.element (defined as Jsx.element)
13+
14+
In JSX, all content must be JSX elements. You can convert float to a JSX element with React.float.
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
2+
We've found a bug for you!
3+
/.../fixtures/jsx_custom_component_optional.res:31:34-40
4+
5+
29 │ }
6+
30 │
7+
31 │ let x = <CustomComponent someOpt="hello" />
8+
32 │
9+
10+
This has type: string
11+
But this component prop someOpt is expected to have type: float
12+
13+
You can convert string to float with Float.fromString.

tests/build_tests/super_errors/expected/jsx_type_mismatch_float.res.expected

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
18 │
99

1010
This has type: float
11-
But it's expected to have type: React.element (defined as Jsx.element)
11+
But children of JSX fragments is expected to have type:
12+
React.element (defined as Jsx.element)
1213

1314
In JSX, all content must be JSX elements. You can convert float to a JSX element with React.float.

tests/build_tests/super_errors/expected/jsx_type_mismatch_int.res.expected

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
18 │
99

1010
This has type: int
11-
But it's expected to have type: React.element (defined as Jsx.element)
11+
But children of JSX fragments is expected to have type:
12+
React.element (defined as Jsx.element)
1213

1314
In JSX, all content must be JSX elements. You can convert int to a JSX element with React.int.

tests/build_tests/super_errors/expected/jsx_type_mismatch_string.res.expected

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
18 │
99

1010
This has type: string
11-
But it's expected to have type: React.element (defined as Jsx.element)
11+
But children of JSX fragments is expected to have type:
12+
React.element (defined as Jsx.element)
1213

1314
In JSX, all content must be JSX elements. You can convert string to a JSX element with React.string.
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/optional_record_field_pass_option.res:6:16
4+
5+
4 │ let t = Some(true)
6+
5 │
7+
6 │ let x = {test: t}
8+
9+
This has type: option<bool>
10+
But this record field test is expected to have type: bool
11+
12+
test is an optional record field, and you're passing an optional value to it.
13+
Optional fields expect you to pass the concrete value, not an option, when passed directly.
14+
15+
Possible solutions:
16+
- Unwrap the option and pass a concrete value directly
17+
- If you really do want to pass the optional value, prepend the value with ? to show you want to pass the option, like: {test: ?t}
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+
10+
@module("react/jsx-runtime")
11+
external jsx: (component<'props>, 'props) => element = "jsx"
12+
13+
type fragmentProps = {children?: element}
14+
@module("react/jsx-runtime") external jsxFragment: component<fragmentProps> = "Fragment"
15+
}
16+
17+
module CustomComponent = {
18+
@react.component
19+
let make = (~children) => {
20+
<> {children} </>
21+
}
22+
}
23+
24+
let x = <CustomComponent> {1.} </CustomComponent>

0 commit comments

Comments
 (0)