-
Notifications
You must be signed in to change notification settings - Fork 470
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
shulhi
wants to merge
10
commits into
rescript-lang:11.0_release
from
shulhi:fix/recover-broken-jsx-prop
Closed
Changes from all commits
Commits
Show all changes
10 commits
Select commit
Hold shift + click to select a range
623f1aa
Lookahead for valid jsx prop
shulhi 538c12d
Attach rescript.exprhole
shulhi 8fb30d8
Handle more possible tokens
shulhi 3252203
Improve token lookahead
shulhi d49c2a1
WIP
shulhi 64c5158
Check for expression in {}
shulhi 16108f5
WIP
shulhi 432fefa
Add comments
shulhi c93a48e
Simplify
shulhi 7d7cf16
Add tests
shulhi File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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) | ||
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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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] | ||
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> |
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
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,
{}
likeprop={...}
.{}
{}
likeprop=true
.There was a problem hiding this comment.
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.There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.There was a problem hiding this comment.
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 ofy=e
and the user typesx=
, then we getx=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=
andy
is a valid prop name for the component, then infer the intent to autocompletex=
. This would cover punned as well as non punned cases. Thoughts?