diff --git a/jscomp/syntax/src/res_core.ml b/jscomp/syntax/src/res_core.ml
index b88a9fd4cc..95ab19e2ba 100644
--- a/jscomp/syntax/src/res_core.ml
+++ b/jscomp/syntax/src/res_core.ml
@@ -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 *)
+ (* | *)
+ (* | %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)
+ in
+ res
+
(* let-binding ::= pattern = expr *)
(* ∣ value-name { parameter } [: typexpr] [:> typexpr] = expr *)
(* ∣ value-name : poly-typexpr = expr *)
@@ -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
+ 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]
diff --git a/jscomp/syntax/tests/parsing/recovery/expression/expected/jsx.res.txt b/jscomp/syntax/tests/parsing/recovery/expression/expected/jsx.res.txt
index 428af6eb03..8ba8b74cbb 100644
--- a/jscomp/syntax/tests/parsing/recovery/expression/expected/jsx.res.txt
+++ b/jscomp/syntax/tests/parsing/recovery/expression/expected/jsx.res.txt
@@ -4,9 +4,118 @@
1 │ let x =
2 │
+ 3 │ let x = {...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 =
+ 2 │
+ 3 │ let x = {...props} props=mo
+ │ dule(Foo) />
+ 4 │
+ 5 │ let x =
+
+ I'm not sure what to parse here when looking at "=".
+
+
+ Syntax error!
+ tests/parsing/recovery/expression/jsx.res:5:46
+
+ 3 │ let x = {...props} props=mo
+ │ dule(Foo) />
+ 4 │
+ 5 │ let x =
+ 6 │
+ 7 │ let x = {...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 =
+ 6 │
+ 7 │ let x = {...props} pr
+ │ ops4= />
+ 8 │
+ 9 │ let x = {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 = {...props} p
+ │ rops4= />
+ 8 │
+ 9 │ let x = {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 │
+ 13 │ {("View the transaction on " ++ txExplererUrl)->restr}
+ 14 │
+
+ I'm not sure what to parse here when looking at "=".
+
let x = ((div ~children:[] ())[@JSX ])
[@@@ ]
-;;[%rescript.exprhole ][@@ ]
\ No newline at end of file
+;;[%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 ])
\ No newline at end of file
diff --git a/jscomp/syntax/tests/parsing/recovery/expression/jsx.res b/jscomp/syntax/tests/parsing/recovery/expression/jsx.res
index c20ee242a0..1f5850ba42 100644
--- a/jscomp/syntax/tests/parsing/recovery/expression/jsx.res
+++ b/jscomp/syntax/tests/parsing/recovery/expression/jsx.res
@@ -1 +1,14 @@
let x =
+
+let x = {...props} props=module(Foo) />
+
+let x =
+
+let x = {...props} props4= />
+
+let x = {f(v + 1)}} prop1= prop3=1 />
+
+let x =
+
+ {("View the transaction on " ++ txExplererUrl)->restr}
+