Skip to content
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

[CS2] CSX spread attributes: <div {props…} /> #4607

Merged
merged 19 commits into from
Aug 3, 2017

Conversation

zdenko
Copy link
Collaborator

@zdenko zdenko commented Jul 8, 2017

This PR addresses the issue with CSX spread attributes (#4551).

Since spread in JSX is special notation different from the ES6 generic spread syntax, CS will simply compile {props...} to {...props}.

# <div {props...}>hello</div>
<div {...props}>hello</div>

# <div {props1...} id="tagId" {props2...}>hello</div>
<div {...props1} id="tagId" {...props2}>hello</div>

There is also an "accidental" side effect and this:

<div {props..., a: "x"}>hello</div>
<div {props1..., props2...}>hello</div>

will compile to

<div {...props} a="x">hello</div>
<div {...props1} {...props2}>hello</div>

@GeoffreyBooth
Copy link
Collaborator

I wonder if we should be doing this checking in the lexer . . . shouldn’t the check if a previous interpolation is a spread be happening in nodes.coffee?

Other than that it looks good to me. @lydell or @connec?

@zdenko
Copy link
Collaborator Author

zdenko commented Jul 9, 2017

shouldn’t the check if a previous interpolation is a spread be happening in nodes.coffee?

@GeoffreyBooth I don't think this is currently possible because of #4600.

Lexer (originally) converts <div id="bar" {foo...} /> into
[CSX_TAG div] [CALL_START (] [{ {] [PROPERTY id] [: :] [STRING "bar"] [, ,] [( (] [{ {] [IDENTIFIER foo] [... ...] [} }] [) )] [, ,] [} }] [CALL_END )].

This PR converts the same code into
[CSX_TAG div] [CALL_START (] [{ {] [PROPERTY id] [: :] [STRING "bar"] [, ,] [IDENTIFIER foo] [... ...] [, ,] [} }] [CALL_END )].

@xixixao
Copy link
Contributor

xixixao commented Jul 9, 2017

This is indeed what I had in mind in terms of what it would compile to. Nice! No opinion on the implementation.

@GeoffreyBooth
Copy link
Collaborator

@zdenko Those tokens roll up into the Call node, where you have compileCSX that works with attributes and content. Is it not possible to look inside those arrays to check for duplicates there?

@zdenko
Copy link
Collaborator Author

zdenko commented Jul 9, 2017

@GeoffreyBooth I've tried, but compilation fails at parsing step (before the Call).
Since @xixixao's lexer method csxToken is already transforming CSX tokens, I just added a simple check for spread attribute and alter the transformation.

But, I'm also working on the PR for #4600 and it might be possible to move this into the compileCSX if necessary.

@GeoffreyBooth
Copy link
Collaborator

In general we try to do as much as possible in Nodes rather than in Lexer, because checks in Lexer are much more brittle and harder to maintain. If that’s not possible that’s fine.

Should we merge this in now or wait until your other PR is ready?

@GeoffreyBooth GeoffreyBooth changed the title CSX spread attributes: <div {props...} /> [CS2] CSX spread attributes: <div {props…} /> Jul 9, 2017
@GeoffreyBooth GeoffreyBooth added this to the 2.0.0 milestone Jul 9, 2017
@zdenko
Copy link
Collaborator Author

zdenko commented Jul 9, 2017

I would merge now. I'm not sure when the PR for #4600 will be ready.

@GeoffreyBooth
Copy link
Collaborator

Okay. @lydell and @connec, any notes?

@connec
Copy link
Collaborator

connec commented Jul 10, 2017

I guess syntactically it's a bit weird that <div {a: 1}> compiles but <div {object}> or <div {object = a: 1}> won't. Unfortunately my lexer-fu is weak, so I'm not sure if there's a clean way of blocking a: 1 whilst still allowing splats?

Edit: in fact there are a couple of edge cases I've found whilst playing around:

  • <div {obj}></div> compiles to <div obj></div>
  • <div {"#{a}": 1}></div> compiles to the invalid <div [${a}]=1></div>
  • <div {"#{a}"}></div> throws an error: TypeError: Cannot set property 'csxAttribute' of undefined - here.

I think it would be best if we could restrict the syntax to allowing a single spread expression only, as is the case with JSX.

@zdenko
Copy link
Collaborator Author

zdenko commented Jul 12, 2017

I added some validation for CSX attributes.
Valid CSX syntax:

<div id="value" name={someName} {props...} attr="#{attribute}" checked />

Since CSX attributes are transformed into Obj in the lexer (@xixixao might explain this better), the following CS syntax is also allowed and will produce the same result.

<div {id:"value"} {name:{someName}} {props...} {attr:"#{attribute}"} {checked} />

@zdenko
Copy link
Collaborator Author

zdenko commented Jul 19, 2017

@GeoffreyBooth I think my work is done here. Do I have to add anything else?

@GeoffreyBooth
Copy link
Collaborator

Assuming this now covers all the cases @connec mentioned, then hopefully this is ready. @connec, did you have any other notes?

@GeoffreyBooth
Copy link
Collaborator

@connec, any notes on this?

src/nodes.coffee Outdated
if @csx and prop instanceof Assign
prop.variable.error "Unexpected token" unless prop.variable.unwrap() instanceof PropertyName
propVal = prop.value.unwrap()
unless ((propVal instanceof Parens or propVal instanceof StringWithInterpolations) and propVal.body instanceof Block) or propVal instanceof StringLiteral or propVal instanceof Obj
Copy link
Collaborator

Choose a reason for hiding this comment

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

Could we reformat this line to something like:

unless propVal instanceof StringLiteral or propVal instanceof Obj or
    ((propVal instanceof Parens or propVal instanceof StringWithInterpolations) and propVal.body instanceof Block)
  prop.value.error "..."

It might also benefit from a comment stating what is allowed (e.g. what is (propVal instanceof Parens or propVal instanceof StringWithInterpolations) and propVal.body instanceof Block ensuring?).

@connec
Copy link
Collaborator

connec commented Jul 24, 2017

Thanks for the update @zdenko, that makes sense to me now!

Unfortunately, there are still a couple of 'regressions' in terms of what will compile now and the errors:

  • Given:

    <div {obj}></div>

    Used to throw:

    [stdin]:1:10: error: unexpected ,
    <div {obj}></div>
             ^
    

    Now compiles to:

    <div obj></div>;

    This definitely shouldn't compile. The {} in the source implies that obj is a variable name, but it is instead compiled to a boolean flag.

  • Given:

    <div {"#{a}"}></div>

    Used to throw:

    [stdin]:1:13: error: unexpected ,
    <div {"#{a}"}></div>
                ^
    

    Now throws:

    SyntaxError: Unexpected token
    

    For some reason this error has lost location data.

  • Given:

    <div {props..., a: "x"}>hello</div>
    <div {props1..., props2...}>hello</div>

    As you note in the PR description, these now compile to:

    <div {...props} a="x">hello</div>
    <div {...props1} {...props2}>hello</div>

    I'm not entirely comfortable with this for a couple of reasons:

    • From these examples you might infer that any object expression is valid, however <div {a: 1}></div> is invalid.
    • This leads to two ways of adding attributes to a CSX expression, should <div {a..., b: "c"}> or <div {a...} b="c"> be preferred?
      I don't want to belabour this point too much though, if someone else thinks it's acceptable (@GeoffreyBooth / @lydell / @vendethiel / @xixixao).

@microdou
Copy link

microdou commented Jul 24, 2017

@zdenko actually added new functionality to the original jsx spreads, by allowing the following cases:

<div {props..., a: "x"}>hello</div>
<div {props1..., props2...}>hello</div>

However, as @connec points out, the new syntax may be causing some problems, so it's debatable that whether these new syntax should be allowed in coffeescript v2.0.0.

# If this is allowed
<div {props..., a: "x"}>hello</div>

# Should this be allowed?
<div {a: "x"}>hello</div>

JSX only allows {...obj} inside the bracket, which is not a real spread at all. I think this syntax is only introduced into JSX so that an object won't be treated as a Boolean flag.

<div {...obj}>hello</div>
// transpiles to
React.createElement(
  "div",
  obj,
  "hello"
);
// obj is treated as an object

<div obj>hello</div>
// transpiles to
React.createElement(
  "div",
  { obj: true },
  "hello"
);
// obj is treated as a Boolean flag

Though ugly, the "jsx spread" obj can actually appear in a complex form and be correctly transpiled in babel.

<div {...{...obj, a: "x"}}>hello</div>
// transpiles to
React.createElement(
  "div",
  _extends({}, obj, { a: "x" }),
  "hello"
);
// a new object {...obj, a: "x"} is created and gets "jsx spread"
// babel correctly recognize the two different "..." here

I would suggest that CSX follows JSX syntax strictly and safely, by only allowing {obj...} inside < >. Nothing else should appear side-by-side withobj... inside {obj...}. Neither {obj..., a: "x"} nor {obj1..., obj2...} is valid. However, a complex form of obj can be allowed, which follows just regular coffeescript syntax, such as {{obj..., a: "x"}...}, {{obj1..., obj2...}...} and {{a: "x"}...}. {{obj1..., obj2...}...} is of course ugly, {obj1...} {obj2...} inside < > is a better form.

In all, I suggest only one object can be filled into the blank to the direct left of ..., nothing else inside {} should appear before the object or after the ...:

<div {___...}>hello</div>

@zdenko
Copy link
Collaborator Author

zdenko commented Jul 24, 2017

@connec, @microdou you're right. CSX syntax should follow JSX.
The main issue is that most of the CSX transformation happens inside lexer where attribute checked in <div checked /> or <div {checked} /> is transformed into Obj {checked} in both cases.
This means that most of the CSX syntax checking has to be placed inside the lexer.

@zdenko
Copy link
Collaborator Author

zdenko commented Jul 24, 2017

BTW this new "functionality" (e.g. <div {props..., a:1} ?>) was just side effect caused by enabling spread synatx in the lexer. It wasn't intentional.

src/lexer.coffee Outdated
@@ -10,7 +10,7 @@
# are read by jison in the `parser.lexer` function defined in coffeescript.coffee.

{Rewriter, INVERSES} = require './rewriter'

log = console.log
Copy link
Collaborator

Choose a reason for hiding this comment

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

This shouldn’t be here.

src/lexer.coffee Outdated
@@ -109,6 +109,20 @@ exports.Lexer = class Lexer
# though `is` means `===` otherwise.
identifierToken: ->
inCSXTag = @atCSXTag()
# Check CSX properties synatx.
Copy link
Collaborator

Choose a reason for hiding this comment

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

“syntax”

src/nodes.coffee Outdated
@@ -1207,7 +1207,7 @@ exports.Obj = class Obj extends Base
else
',\n'
indent = if isCompact or prop instanceof Comment then '' else idt

log = console.log
Copy link
Collaborator

Choose a reason for hiding this comment

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

Please remove.

@microdou
Copy link

@zdenko Perfect!

@microdou
Copy link

I just realized that {...props} is allowed.

<div {...props}></div>
# compiles to
<div {...props}></div>;

I thought it was an side effect until I found that it appears to be universally allowed in coffeescript now.

newObj = {...obj, a: "x"}
# compiles to
var newObj;
newObj = Object.assign({}, obj, {
  a: "x"
});

@GeoffreyBooth Is the left-sided-spreads officially supported by coffeescript now in addition to the legacy right-sided-spreads?

@GeoffreyBooth
Copy link
Collaborator

Is the left-sided-spreads officially supported by coffeescript now in addition to the legacy right-sided-spreads?

Yes, since #4606. Dots on the left are not the preferred style, so I don’t plan to add it to the docs, but it seemed like a minor enough convenience to provide for people copying and pasting ES code.

Copy link
Collaborator

@connec connec left a comment

Choose a reason for hiding this comment

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

This looks really good now, there's only one technical issue I can see which is a bit of an edge case anyway (how many people directly use JSX expressions in attributes, I wonder?)

src/lexer.coffee Outdated
@@ -500,25 +503,30 @@ exports.Lexer = class Lexer
[input, id, colon] = match
origin = @token 'CSX_TAG', id, 1, id.length
@token 'CALL_START', '('
@token '{', '{'
csxStart = @token '[', '['
Copy link
Collaborator

Choose a reason for hiding this comment

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

Unused assignment.

src/lexer.coffee Outdated
token = @token '(', '('
if prevChar is ':'
token = @token '(', '('
@csxObjAttribute = no
Copy link
Collaborator

Choose a reason for hiding this comment

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

Not sure what the right fix here is, but because this is limited to a single state, alternately nesting assignment and splats causes errors:

[stdin]:1:24: error: unexpected }
<A child={<B {...params} />} />
                       ^

I suppose that's what @pair is for, but that will currently throw if it doesn't match... It would be nice to avoid a new stack though...

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

@connec the latest commit fixes this bug.

@microdou
Copy link

@connec @zdenko Thanks for pointing out and the fix!
Yes, it's quite useful to use JSX expression as attributes for oneliners, for example in react-bootstrap:

<ButtonToolbar>
  <OverlayTrigger overlay={<Tooltip>Hello</Tooltip>}>
    <Button>Click me!</Button>
  </OverlayTrigger>
</ButtonToolbar>

The nested JSX expression can be refactored out though:

tooltip = <Tooltip>Hello</Tooltip>
<ButtonToolbar>
  <OverlayTrigger overlay={tooltip}>
    <Button>Click me!</Button>
  </OverlayTrigger>
</ButtonToolbar>

It may be unusual to have spreads in the nested expression, but it's awesome to have the edge case covered!

@GeoffreyBooth
Copy link
Collaborator

So @microdou does the current code cover all the relevant cases, in your opinion? In other words, is it ready to merge in?

@microdou
Copy link

microdou commented Aug 1, 2017

@GeoffreyBooth I couldn't find any problem. @zdenko did such an awesome job!

@GeoffreyBooth
Copy link
Collaborator

@connec, any more notes?

src/nodes.coffee Outdated
if not (attr instanceof Obj or attr instanceof IdentifierLiteral) or (attr instanceof Obj and not attr.generated and (attrProps.length > 1 or not (attrProps[0] instanceof Splat)))
obj.error """
Unexpected token. Allowed CSX attributes are: id="val", src={source}, {props...} or attribute.
Example: <div id="val" src={getTheSource()} {props...} checked>hello world</div>.
Copy link
Collaborator

Choose a reason for hiding this comment

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

We don’t normally put examples in error messages. Please update the docs with this if the docs are insufficient.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I'll remove it.

test/csx.coffee Outdated
@@ -724,3 +692,11 @@ test 'unspaced less than after CSX works but is not encouraged', ->

res = 2 < div;
'''

test 'CSX invalid attribute errors', ->
Copy link
Collaborator

Choose a reason for hiding this comment

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

All our error-checking tests are in error_messages.coffee. Perhaps they shouldn’t be, but that’s a refactor for another PR. Please move these checks over there and follow the form in that file, where you confirm that the error returned is as expected (i.e. not just that an exception is thrown).

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Agree. I'll move and improve the tests later today.

@GeoffreyBooth
Copy link
Collaborator

My notes are fairly minor. This is much cleaner than before, thanks for reworking it @zdenko!

@connec
Copy link
Collaborator

connec commented Aug 2, 2017

I'm now seeing the following issue, but it's getting very esoteric now:

<A {...{child: <B {...params} />}} />
# [stdin]:1:34: error: unexpected )
# <A {...{child: <B {...params} />}} />
#                                  ^

The same thing compiles with babel and also works fine if you extract the child into a variable:

child = <B {...params} />
<A {...{child}} />

I think the issue is still to do with that csxObjAttribute field - in the presence of arbitrary nested mixes of {...*} and =* we would need a stack to keep track of what the lexer is currently scanning in order to correctly match } or ).

@connec
Copy link
Collaborator

connec commented Aug 2, 2017

Another failing example:

<A {...{child: <B foo={bar} />}} />

The problem occurs because:

  1. The lexer sees { without : and sets csxObjAttribute = yes.
  2. The lexer sees { with : and sets csxObjAttribute = no.
  3. The lexer sees } with not csxObjAttribute and emits a ) to close the field.
  4. The lexer sees } with not csxObjAttribute and emits a ) to close the field - this is the incorrect step that needs to instead emit }.

The story with <A {...{child: <B {...params} />}} /> is similar:

  1. The lexer sees { without : and sets csxObjAttribute = yes.
  2. The lexer sees { without : and sets csxObjAttribute = yes.
  3. The lexer sees } with csxObjAttribute and emits a } to close the field, and sets csxObjAttribute = no.
  4. The lexer sees } with not csxObjAttribute and emits a ) to close the field - this is the incorrect step that needs to instead emit }.

@zdenko
Copy link
Collaborator Author

zdenko commented Aug 2, 2017

@connec good catch. I improved tracking of the splats inside the CSX tag.

@GeoffreyBooth
Copy link
Collaborator

@connec Looks like the latest commit fixed both examples:

// <A {...{child: <B {...params} />}} />
<A {...{
    child: <B {...params} />
  }} />;


// <A {...{child: <B foo={bar} />}} />
<A {...{
    child: <B foo={bar} />
  }} />;

And ditto with the dots on the right side.

Copy link
Collaborator

@connec connec left a comment

Choose a reason for hiding this comment

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

Nice 👍

# Conflicts:
#	lib/coffeescript/lexer.js
#	lib/coffeescript/nodes.js
#	src/nodes.coffee
@GeoffreyBooth GeoffreyBooth merged commit cbf035f into jashkenas:2 Aug 3, 2017
@GeoffreyBooth GeoffreyBooth deleted the csx-spreads branch August 3, 2017 04:00
@GeoffreyBooth
Copy link
Collaborator

Great work @zdenko!

@GeoffreyBooth GeoffreyBooth changed the title [WIP][CS2] CSX spread attributes: <div {props…} /> [CS2] CSX spread attributes: <div {props…} /> Aug 3, 2017
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants