Skip to content

Commit a5b8f7a

Browse files
authored
More error messages (#7522)
* refactor * explicit error for while condition * explicit for loop condition * assert condition * refactor * fix * try tracking record field type checking * iron out a few more error messages * track optional function arguments * format * cleanup * PR comments * update test output * change wording a bit * change wording * changelog * explicit error message for optional component props, since they have another syntax
1 parent af1eaa8 commit a5b8f7a

33 files changed

+672
-210
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
- Improve a few error messages around various subtyping issues. https://github.com/rescript-lang/rescript/pull/7404
4040
- In module declarations, accept the invalid syntax `M = {...}` and format it to `M : {...}`. https://github.com/rescript-lang/rescript/pull/7527
4141
- Improve doc comment formatting to match the style of multiline comments. https://github.com/rescript-lang/rescript/pull/7529
42+
- Improve error messages around type mismatches for try/catch, if, for, while, and optional record fields + optional function arguments. https://github.com/rescript-lang/rescript/pull/7522
4243

4344
#### :house: Internal
4445

compiler/ml/error_message_utils.ml

Lines changed: 165 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -69,23 +69,58 @@ 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 =
74-
| SetRecordField
80+
| SetRecordField 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+
}
7587
| ArrayValue
7688
| MaybeUnwrapOption
7789
| IfCondition
90+
| AssertCondition
7891
| IfReturn
79-
| Switch
92+
| SwitchReturn
93+
| TryReturn
8094
| StringConcat
8195
| ComparisonOperator
96+
| WhileCondition
8297
| MathOperator of {
8398
for_float: bool;
8499
operator: string;
85100
is_constant: string option;
86101
}
87-
| FunctionArgument
102+
| FunctionArgument of {optional: bool; name: string option}
88103
| Statement of type_clash_statement
104+
| ForLoopCondition
105+
106+
let context_to_string = function
107+
| Some WhileCondition -> "WhileCondition"
108+
| Some ForLoopCondition -> "ForLoopCondition"
109+
| Some AssertCondition -> "AssertCondition"
110+
| Some IfCondition -> "IfCondition"
111+
| Some (Statement _) -> "Statement"
112+
| Some (MathOperator _) -> "MathOperator"
113+
| Some ArrayValue -> "ArrayValue"
114+
| Some (SetRecordField _) -> "SetRecordField"
115+
| Some (RecordField _) -> "RecordField"
116+
| Some MaybeUnwrapOption -> "MaybeUnwrapOption"
117+
| Some SwitchReturn -> "SwitchReturn"
118+
| Some TryReturn -> "TryReturn"
119+
| Some StringConcat -> "StringConcat"
120+
| Some (FunctionArgument _) -> "FunctionArgument"
121+
| Some ComparisonOperator -> "ComparisonOperator"
122+
| Some IfReturn -> "IfReturn"
123+
| None -> "None"
89124
90125
let fprintf = Format.fprintf
91126
@@ -95,33 +130,63 @@ let error_type_text ppf type_clash_context =
95130
| Some (Statement FunctionCall) -> "This function call returns:"
96131
| Some (MathOperator {is_constant = Some _}) -> "This value has type:"
97132
| Some ArrayValue -> "This array item has type:"
98-
| Some SetRecordField ->
133+
| Some (SetRecordField _) ->
99134
"You're assigning something to this field that has type:"
100135
| _ -> "This has type:"
101136
in
102137
fprintf ppf "%s" text
103138
104139
let error_expected_type_text ppf type_clash_context =
105140
match type_clash_context with
106-
| Some FunctionArgument ->
107-
fprintf ppf "But this function argument is expecting:"
141+
| Some (FunctionArgument {optional; name}) ->
142+
fprintf ppf "But this%s function argument"
143+
(match optional with
144+
| false -> ""
145+
| true -> " optional");
146+
147+
(match name with
148+
| Some name -> fprintf ppf " @{<info>~%s@}" name
149+
| None -> ());
150+
151+
fprintf ppf " is expecting:"
108152
| Some ComparisonOperator ->
109153
fprintf ppf "But it's being compared to something of type:"
110-
| Some Switch -> fprintf ppf "But this switch is expected to return:"
154+
| Some SwitchReturn -> fprintf ppf "But this switch is expected to return:"
155+
| Some TryReturn -> fprintf ppf "But this try/catch is expected to return:"
156+
| Some WhileCondition ->
157+
fprintf ppf "But a @{<info>while@} loop condition must always be of type:"
158+
| Some ForLoopCondition ->
159+
fprintf ppf "But a @{<info>for@} loop bounds must always be of type:"
111160
| Some IfCondition ->
112161
fprintf ppf "But @{<info>if@} conditions must always be of type:"
162+
| Some AssertCondition -> fprintf ppf "But assertions must always be of type:"
113163
| Some IfReturn ->
114164
fprintf ppf "But this @{<info>if@} statement is expected to return:"
115165
| Some ArrayValue ->
116166
fprintf ppf "But this array is expected to have items of type:"
117-
| Some SetRecordField -> fprintf ppf "But this record field is of type:"
167+
| Some (SetRecordField _) -> fprintf ppf "But the record field is of type:"
168+
| Some
169+
(RecordField {field_name = "children"; jsx = Some {jsx_type = `Fragment}})
170+
->
171+
fprintf ppf "But children of JSX fragments must be of type:"
172+
| Some
173+
(RecordField
174+
{field_name = "children"; jsx = Some {jsx_type = `CustomComponent}}) ->
175+
fprintf ppf "But children passed to this component must be of type:"
176+
| Some (RecordField {field_name; jsx = Some _}) ->
177+
fprintf ppf "But the component prop @{<info>%s@} is expected to have type:"
178+
field_name
179+
| Some (RecordField {field_name}) ->
180+
fprintf ppf "But the record field @{<info>%s@} is expected to have type:"
181+
field_name
118182
| Some (Statement FunctionCall) -> fprintf ppf "But it's expected to return:"
119183
| Some (MathOperator {operator}) ->
120184
fprintf ppf
121185
"But it's being used with the @{<info>%s@} operator, which works on:"
122186
operator
123187
| Some StringConcat -> fprintf ppf "But string concatenation is expecting:"
124-
| _ -> fprintf ppf "But it's expected to have type:"
188+
| Some MaybeUnwrapOption | None ->
189+
fprintf ppf "But it's expected to have type:"
125190
126191
let is_record_type ~extract_concrete_typedecl ~env ty =
127192
try
@@ -201,11 +266,17 @@ let print_extra_type_clash_help ~extract_concrete_typedecl ~env loc ppf
201266
(if for_float then "int" else "float")
202267
| _ -> ())
203268
| _ -> ())
204-
| Some Switch, _ ->
269+
| Some SwitchReturn, _ ->
205270
fprintf ppf
206271
"\n\n\
207-
\ All branches in a @{<info>switch@} must return the same type. To fix \
208-
this, change your branch to return the expected type."
272+
\ All branches in a @{<info>switch@} must return the same type.@,\
273+
To fix this, change your branch to return the expected type."
274+
| Some TryReturn, _ ->
275+
fprintf ppf
276+
"\n\n\
277+
\ The @{<info>try@} body and the @{<info>catch@} block must return the \
278+
same type.@,\
279+
To fix this, change your try/catch blocks to return the expected type."
209280
| Some IfCondition, _ ->
210281
fprintf ppf
211282
"\n\n\
@@ -355,6 +426,58 @@ let print_extra_type_clash_help ~extract_concrete_typedecl ~env loc ppf
355426
single JSX element.@,"
356427
(with_configured_jsx_module "array")
357428
| _ -> ())
429+
| ( Some (RecordField {optional = true; field_name; jsx = None}),
430+
Some ({desc = Tconstr (p, _, _)}, _) )
431+
when Path.same Predef.path_option p ->
432+
fprintf ppf
433+
"@,\
434+
@,\
435+
@{<info>%s@} is an optional record field, and you're passing an \
436+
optional value to it.@,\
437+
Values passed to an optional record field don't need to be wrapped in \
438+
an option. You might need to adjust the type of the value supplied.\n\
439+
\ @,\
440+
Possible solutions: @,\
441+
- Unwrap the option from the value you're passing in@,\
442+
- If you really do want to pass the optional value, prepend the value \
443+
with @{<info>?@} to show you want to pass the option, like: \
444+
@{<info>{%s: ?%s@}}"
445+
field_name field_name
446+
(Parser.extract_text_at_loc loc)
447+
| ( Some (RecordField {optional = true; field_name; jsx = Some _}),
448+
Some ({desc = Tconstr (p, _, _)}, _) )
449+
when Path.same Predef.path_option p ->
450+
fprintf ppf
451+
"@,\
452+
@,\
453+
@{<info>%s@} is an optional component prop, and you're passing an \
454+
optional value to it.@,\
455+
Values passed to an optional component prop don't need to be wrapped in \
456+
an option. You might need to adjust the type of the value supplied.\n\
457+
\ @,\
458+
Possible solutions: @,\
459+
- Unwrap the option from the value you're passing in@,\
460+
- If you really do want to pass the optional value, prepend the value \
461+
with @{<info>?@} to show you want to pass the option, like: \
462+
@{<info>%s=?%s@}"
463+
field_name field_name
464+
(Parser.extract_text_at_loc loc)
465+
| ( Some (FunctionArgument {optional = true}),
466+
Some ({desc = Tconstr (p, _, _)}, _) )
467+
when Path.same Predef.path_option p ->
468+
fprintf ppf
469+
"@,\
470+
@,\
471+
You're passing an optional value into an optional function argument.@,\
472+
Values passed to an optional function argument don't need to be wrapped \
473+
in an option. You might need to adjust the type of the value supplied.\n\
474+
\ @,\
475+
Possible solutions: @,\
476+
- Unwrap the option from the value you're passing in@,\
477+
- If you really do want to pass the optional value, prepend the value \
478+
with @{<info>?@} to show you want to pass the option, like: \
479+
@{<info>?%s@}"
480+
(Parser.extract_text_at_loc loc)
358481
| _, Some (t1, t2) ->
359482
let is_subtype =
360483
try
@@ -410,9 +533,9 @@ let type_clash_context_from_function sexp sfunct =
410533
Some (MathOperator {for_float = true; operator; is_constant})
411534
| Pexp_ident {txt = Lident (("/" | "*" | "+" | "-") as operator)} ->
412535
Some (MathOperator {for_float = false; operator; is_constant})
413-
| _ -> Some FunctionArgument
536+
| _ -> None
414537
415-
let type_clash_context_for_function_argument type_clash_context sarg0 =
538+
let type_clash_context_for_function_argument ~label type_clash_context sarg0 =
416539
match type_clash_context with
417540
| Some (MathOperator {for_float; operator}) ->
418541
Some
@@ -427,6 +550,16 @@ let type_clash_context_for_function_argument type_clash_context sarg0 =
427550
Some txt
428551
| _ -> None);
429552
})
553+
| None ->
554+
Some
555+
(FunctionArgument
556+
{
557+
optional = false;
558+
name =
559+
(match label with
560+
| Asttypes.Nolabel -> None
561+
| Optional {txt = l} | Labelled {txt = l} -> Some l);
562+
})
430563
| type_clash_context -> type_clash_context
431564
432565
let type_clash_context_maybe_option ty_expected ty_res =
@@ -468,11 +601,6 @@ let print_contextual_unification_error ppf t1 t2 =
468601
the highlighted pattern in @{<info>Some()@} to make it work.@]"
469602
| _ -> ()
470603
471-
type jsx_prop_error_info = {
472-
fields: Types.label_declaration list;
473-
props_record_path: Path.t;
474-
}
475-
476604
let attributes_include_jsx_component_props (attrs : Parsetree.attributes) =
477605
attrs
478606
|> List.exists (fun ({Location.txt}, _) -> txt = "res.jsxComponentProps")
@@ -484,18 +612,24 @@ let path_to_jsx_component_name p =
484612
485613
let get_jsx_component_props
486614
~(extract_concrete_typedecl : extract_concrete_typedecl) env ty p =
487-
match Path.last p with
488-
| "props" -> (
489-
try
490-
match extract_concrete_typedecl env ty with
491-
| ( _p0,
492-
_p,
493-
{Types.type_kind = Type_record (fields, _repr); type_attributes} )
494-
when attributes_include_jsx_component_props type_attributes ->
495-
Some {props_record_path = p; fields}
496-
| _ -> None
497-
with _ -> None)
498-
| _ -> None
615+
match p with
616+
| Path.Pdot (Path.Pident {Ident.name = jsx_module_name}, "fragmentProps", _)
617+
when Some jsx_module_name = !configured_jsx_module ->
618+
Some {props_record_path = p; fields = []; jsx_type = `Fragment}
619+
| _ -> (
620+
(* TODO: handle lowercase components using JSXDOM.domProps *)
621+
match Path.last p with
622+
| "props" -> (
623+
try
624+
match extract_concrete_typedecl env ty with
625+
| ( _p0,
626+
_p,
627+
{Types.type_kind = Type_record (fields, _repr); type_attributes} )
628+
when attributes_include_jsx_component_props type_attributes ->
629+
Some {props_record_path = p; fields; jsx_type = `CustomComponent}
630+
| _ -> None
631+
with _ -> None)
632+
| _ -> None)
499633
500634
let print_component_name ppf (p : Path.t) =
501635
match path_to_jsx_component_name p with

0 commit comments

Comments
 (0)