Skip to content

Recover from broken JSX prop #6660

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
106 changes: 94 additions & 12 deletions jscomp/syntax/src/res_core.ml
Original file line number Diff line number Diff line change
Expand Up @@ -982,6 +982,75 @@ let parseRegion p ~grammar ~f =
Parser.eatBreadcrumb p;
nodes

let isJsxPropWellFormed p =
(* jsx-prop ::= prop = value *)
(* Possible tokens after `=` *)
(* value ::= *)
(* | True *)
(* | False *)
(* | #Variant *)
(* | `string` *)
(* | [array] *)
(* | list{} *)
(* | () *)
(* | ?value *)
(* | <element /> *)
(* | %raw *)
(* | module(P) *)
(* | string-literal *)
(* | int-literal *)
(* | float-literal *)
(* | variable *)
(* | A.B *)
let isPossibleAfterEqual token =
match token with
| Token.True | False | Hash | Backtick | Lbracket | List | Lparen | Question
| LessThan | Percent | Module | String _ | Int _ | Float _ | Lident _
| Uident _ ->
true
| _ -> false
in
(* jsx-prop ::= prop = {value} *)
(* Possible tokens after `}` *)
(* prop={{expr}} *)
(* prop={value} > ...children </> *)
(* prop={value} /> *)
(* prop={value} prop2={value2} /> *)
(* prop={value} ?prop2 /> *)
let isPossibleAfterRbrace token =
match token with
| Token.Rbrace | GreaterThan | Forwardslash | Lident _ | Question | Eof ->
true
| _ -> false
in
let res =
Parser.lookahead p (fun state ->
match state.Parser.token with
(* arrived at k1= *)
| Equal -> (
Parser.next state;
match state.Parser.token with
(* arrived at k1={ *)
| Lbrace -> (
Parser.next state;
match state.Parser.token with
| Rbrace -> false
| _ ->
goToClosing Rbrace state;
isPossibleAfterRbrace state.Parser.token)
(* arrived at k1=v1 *)
| token when isPossibleAfterEqual token -> (
Parser.next state;
match state.Parser.token with
(* arrived at k1=v1 =v2 *)
| Equal -> false
| _ -> true)
(* arrived at k1=x *)
| _ -> false)
| _ -> false)
Comment on lines +1030 to +1050
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When looking ahead, there are two possibilities when we encounter a prop,

  1. prop where the value is wrapped with {} like prop={...}.
    • any expression can live inside this {}
  2. prop where the value is specified without {} like prop=true.
    • this can be number literal, string literal, list, array, module, etc

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think for the first case, we could just check for { (Lbrace) but for completeness’s sake, it's probably a good idea to be this explicit.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To keep things simple, I would do nothing in the { case, and avoid long look aheads searching for }.
Then keep the changes and additional code to a minimum.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In fact I can't think of a case other than x = y = that one needs to handle.
The situation is one starts typing x = in a jsx context and expects completion.
This currently does not happen if it's before something of the form y = and I don't know if there are other cases to consider in this scenario.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One more case that is not covered is prop punning. If we have just y instead of y=e and the user types x=, then we get x=y with no parse error at all. It's also totally not obvious that the intention is to autocomplete.
In other words, this approach does not work at all with prop pinning.

@zth perhaps one can try with a different approach directly in the vscode extension: in case of x=y if the cursor is after = and y is a valid prop name for the component, then infer the intent to autocomplete x=. This would cover punned as well as non punned cases. Thoughts?

in
res

(* let-binding ::= pattern = expr *)
(* ∣ value-name { parameter } [: typexpr] [:> typexpr] = expr *)
(* ∣ value-name : poly-typexpr = expr *)
Expand Down Expand Up @@ -2745,18 +2814,31 @@ and parseJsxProp p =
else
match p.Parser.token with
| Equal ->
Parser.next p;
(* no punning *)
let optional = Parser.optional p Question in
Scanner.popMode p.scanner Jsx;
let attrExpr =
let e = parsePrimaryExpr ~operand:(parseAtomicExpr p) p in
{e with pexp_attributes = propLocAttr :: e.pexp_attributes}
in
let label =
if optional then Asttypes.Optional name else Asttypes.Labelled name
in
Some (label, attrExpr)
let wellFormed = isJsxPropWellFormed p in
if wellFormed then (
(* no punning *)
Parser.next p;
let optional = Parser.optional p Question in
Scanner.popMode p.scanner Jsx;
let attrExpr =
let e = parsePrimaryExpr ~operand:(parseAtomicExpr p) p in
{e with pexp_attributes = propLocAttr :: e.pexp_attributes}
in
let label =
if optional then Asttypes.Optional name else Asttypes.Labelled name
in
Some (label, attrExpr))
else
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This branch should probably raise an error. Otherwise a hole is added and the parser can continue as if the parsing was successful.

let label =
if optional then Asttypes.Optional name else Asttypes.Labelled name
in
let id = Location.mknoloc "rescript.exprhole" in
let attrExpr =
Ast_helper.Exp.mk
(Pexp_extension (id, PStr []))
~attrs:[propLocAttr]
in
Some (label, attrExpr)
| _ ->
let attrExpr =
Ast_helper.Exp.ident ~loc ~attrs:[propLocAttr]
Expand Down
111 changes: 110 additions & 1 deletion jscomp/syntax/tests/parsing/recovery/expression/expected/jsx.res.txt
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,118 @@

1 │ let x = <div @ @@@ />
2 │
3 │ let x = <Component prop1= prop2="value2" prop3=<C /> {...props} props=mo
│ dule(Foo) />

I'm not sure what to parse here when looking at "@".


Syntax error!
tests/parsing/recovery/expression/jsx.res:3:25

1 │ let x = <div @ @@@ />
2 │
3 │ let x = <Component prop1= prop2="value2" prop3=<C /> {...props} props=mo
│ dule(Foo) />
4 │
5 │ let x = <Component ?prop0 prop1=[1,2,3] prop2= prop3={value3} prop4=?val
│ ue4 />

I'm not sure what to parse here when looking at "=".


Syntax error!
tests/parsing/recovery/expression/jsx.res:5:46

3 │ let x = <Component prop1= prop2="value2" prop3=<C /> {...props} props=mo
│ dule(Foo) />
4 │
5 │ let x = <Component ?prop0 prop1=[1,2,3] prop2= prop3={value3} prop4=?val
│ ue4 />
6 │
7 │ let x = <Component prop1=value1 prop2="value2" prop3=<C /> {...props} pr
│ ops4= />

I'm not sure what to parse here when looking at "=".


Syntax error!
tests/parsing/recovery/expression/jsx.res:7:77

5 │ let x = <Component ?prop0 prop1=[1,2,3] prop2= prop3={value3} prop4=?val
│ ue4 />
6 │
7 │ let x = <Component prop1=value1 prop2="value2" prop3=<C /> {...props} pr
│ ops4= />
8 │
9 │ let x = <Component className=Styles.something prop2={v => {f(v + 1)}} pr
│ op1= prop3=1 />

I'm not sure what to parse here when looking at "=".


Syntax error!
tests/parsing/recovery/expression/jsx.res:9:76

7 │ let x = <Component prop1=value1 prop2="value2" prop3=<C /> {...props} p
│ rops4= />
8 │
9 │ let x = <Component className=Styles.something prop2={v => {f(v + 1)}} p
│ rop1= prop3=1 />
10 │
11 │ let x =

I'm not sure what to parse here when looking at "=".


Syntax error!
tests/parsing/recovery/expression/jsx.res:12:10

10 │
11 │ let x =
12 │ <a prop= href={j`https://$txExplererUrl/tx/$txHash`} target="_blank"
│ rel="noopener noreferrer">
13 │ {("View the transaction on " ++ txExplererUrl)->restr}
14 │ </a>

I'm not sure what to parse here when looking at "=".

let x = ((div ~children:[] ())[@JSX ])
[@@@ ]
;;[%rescript.exprhole ][@@ ]
;;[%rescript.exprhole ][@@ ]
let x =
((Component.createElement ~prop1:(([%rescript.exprhole ])
[@res.namedArgLoc ]) ~prop2:(({js|value2|js})[@res.namedArgLoc ])
~prop3:((C.createElement ~children:[] ())[@res.namedArgLoc ][@JSX ])
~_spreadProps:((props)[@res.namedArgLoc ]) ~props:(((module Foo))
[@res.namedArgLoc ]) ~children:[] ())
[@JSX ])
let x =
((Component.createElement ?prop0:((prop0)[@res.namedArgLoc ])
~prop1:(([|1;2;3|])[@res.namedArgLoc ]) ~prop2:(([%rescript.exprhole ])
[@res.namedArgLoc ]) ~prop3:((value3)[@res.namedArgLoc ][@res.braces ])
?prop4:((value4)[@res.namedArgLoc ]) ~children:[] ())
[@JSX ])
let x =
((Component.createElement ~prop1:((value1)[@res.namedArgLoc ])
~prop2:(({js|value2|js})[@res.namedArgLoc ])
~prop3:((C.createElement ~children:[] ())[@res.namedArgLoc ][@JSX ])
~_spreadProps:((props)[@res.namedArgLoc ])
~props4:(([%rescript.exprhole ])[@res.namedArgLoc ]) ~children:[] ())
[@JSX ])
let x =
((Component.createElement ~className:((Styles.something)
[@res.namedArgLoc ]) ~prop2:((fun v -> ((f (v + 1))[@res.braces ]))
[@res.namedArgLoc ][@res.braces ]) ~prop1:(([%rescript.exprhole ])
[@res.namedArgLoc ]) ~prop3:((1)[@res.namedArgLoc ]) ~children:[] ())
[@JSX ])
let x =
((a ~prop:(([%rescript.exprhole ])[@res.namedArgLoc ])
~href:(({j|https://$txExplererUrl/tx/$txHash|j})
[@res.namedArgLoc ][@res.braces ][@res.template ])
~target:(({js|_blank|js})[@res.namedArgLoc ])
~rel:(({js|noopener noreferrer|js})[@res.namedArgLoc ])
~children:[((({js|View the transaction on |js} ^ txExplererUrl) |.
restr)
[@res.braces ])] ())
[@JSX ])
13 changes: 13 additions & 0 deletions jscomp/syntax/tests/parsing/recovery/expression/jsx.res
Original file line number Diff line number Diff line change
@@ -1 +1,14 @@
let x = <div @ @@@ />

let x = <Component prop1= prop2="value2" prop3=<C /> {...props} props=module(Foo) />

let x = <Component ?prop0 prop1=[1,2,3] prop2= prop3={value3} prop4=?value4 />

let x = <Component prop1=value1 prop2="value2" prop3=<C /> {...props} props4= />

let x = <Component className=Styles.something prop2={v => {f(v + 1)}} prop1= prop3=1 />

let x =
<a prop= href={j`https://$txExplererUrl/tx/$txHash`} target="_blank" rel="noopener noreferrer">
{("View the transaction on " ++ txExplererUrl)->restr}
</a>