Skip to content

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

Merged
GeoffreyBooth merged 19 commits into
jashkenas:2from
GeoffreyBooth:csx-spreads
Aug 3, 2017
Merged

[CS2] CSX spread attributes: <div {props…} />#4607
GeoffreyBooth merged 19 commits into
jashkenas:2from
GeoffreyBooth:csx-spreads

Conversation

@zdenko

@zdenko zdenko commented Jul 8, 2017

Copy link
Copy Markdown
Collaborator

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
Copy Markdown
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

zdenko commented Jul 9, 2017

Copy link
Copy Markdown
Collaborator Author

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

xixixao commented Jul 9, 2017

Copy link
Copy Markdown
Contributor

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
Copy Markdown
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

zdenko commented Jul 9, 2017

Copy link
Copy Markdown
Collaborator Author

@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
Copy Markdown
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

zdenko commented Jul 9, 2017

Copy link
Copy Markdown
Collaborator Author

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

@GeoffreyBooth

Copy link
Copy Markdown
Collaborator

Okay. @lydell and @connec, any notes?

@connec

connec commented Jul 10, 2017

Copy link
Copy Markdown
Collaborator

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

zdenko commented Jul 12, 2017

Copy link
Copy Markdown
Collaborator Author

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

zdenko commented Jul 19, 2017

Copy link
Copy Markdown
Collaborator Author

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

@GeoffreyBooth

Copy link
Copy Markdown
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
Copy Markdown
Collaborator

@connec, any notes on this?

Comment thread 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
Copy Markdown
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

connec commented Jul 24, 2017

Copy link
Copy Markdown
Collaborator

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

microdou commented Jul 24, 2017

Copy link
Copy Markdown

@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

zdenko commented Jul 24, 2017

Copy link
Copy Markdown
Collaborator Author

@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

zdenko commented Jul 24, 2017

Copy link
Copy Markdown
Collaborator Author

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.

Comment thread src/lexer.coffee Outdated

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

log = console.log

Copy link
Copy Markdown
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.

Comment thread src/lexer.coffee Outdated
# though `is` means `===` otherwise.
identifierToken: ->
inCSXTag = @atCSXTag()
# Check CSX properties synatx.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

“syntax”

Comment thread src/nodes.coffee Outdated
',\n'
indent = if isCompact or prop instanceof Comment then '' else idt

log = console.log

Copy link
Copy Markdown
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
Copy Markdown

@zdenko Perfect!

@microdou

Copy link
Copy Markdown

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
Copy Markdown
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.

@connec connec left a comment

Copy link
Copy Markdown
Collaborator

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?)

Comment thread src/lexer.coffee Outdated
origin = @token 'CSX_TAG', id, 1, id.length
@token 'CALL_START', '('
@token '{', '{'
csxStart = @token '[', '['

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Unused assignment.

Comment thread src/lexer.coffee Outdated
token = @token '(', '('
if prevChar is ':'
token = @token '(', '('
@csxObjAttribute = no

Copy link
Copy Markdown
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
Copy Markdown
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
Copy Markdown

@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
Copy Markdown
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

microdou commented Aug 1, 2017

Copy link
Copy Markdown

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

@GeoffreyBooth

Copy link
Copy Markdown
Collaborator

@connec, any more notes?

Comment thread 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
Copy Markdown
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
Copy Markdown
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.

Comment thread test/csx.coffee Outdated
res = 2 < div;
'''

test 'CSX invalid attribute errors', ->

Copy link
Copy Markdown
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
Copy Markdown
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
Copy Markdown
Collaborator

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

@connec

connec commented Aug 2, 2017

Copy link
Copy Markdown
Collaborator

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

connec commented Aug 2, 2017

Copy link
Copy Markdown
Collaborator

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

zdenko commented Aug 2, 2017

Copy link
Copy Markdown
Collaborator Author

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

@GeoffreyBooth

Copy link
Copy Markdown
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.

@connec connec left a comment

Copy link
Copy Markdown
Collaborator

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
Copy Markdown
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