Skip to content
This repository has been archived by the owner on May 19, 2018. It is now read-only.

ClassFields: Allowing comma separated grammar #608

Closed
wants to merge 2 commits into from

Conversation

diervo
Copy link
Contributor

@diervo diervo commented Jul 2, 2017

Refactor for classFields:

Q A
Bug fix? yes
Breaking change? yes
New feature? yes
Deprecations? yes
Spec compliancy? yes
Tests added/pass? yes
Fixed tickets #540
License MIT

this.state.inCommaSeparatedFields = true;
// Copy decorators for cases like `@foo x, y;`
if (decorators.length) {
member.decorators = decorators;
Copy link
Member

Choose a reason for hiding this comment

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

Let's slice here. It'll be really weird if a transform pushes a decorator and two fields get it.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good call.

@@ -899,7 +918,7 @@ export default class StatementParser extends ExpressionParser {
}
}

if (decorators.length) {
if (!this.state.inCommaSeparatedFields && decorators.length) {
Copy link
Member

Choose a reason for hiding this comment

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

I don't understand why inCommaSeparatedFields affects this. Maybe it should be a separate check instead (if fields can't have a trailing comma)?

@@ -1071,7 +1090,7 @@ export default class StatementParser extends ExpressionParser {
/* isConstructor */ false,
);
this.checkGetterSetterParamCount(method);
} else if (this.isLineTerminator()) {
} else if (this.isLineTerminator() || this.match(tt.comma)) {
Copy link
Member

Choose a reason for hiding this comment

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

Does this mean methods can have trailing commas?

class Foo {
  method() {
  },
}

I don't see a test for it anywhere.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Will add more tests.


if (isClassField) {
if (this.eat(tt.comma)) {
this.state.inCommaSeparatedFields = true;
Copy link
Member

Choose a reason for hiding this comment

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

We can probably avoid this state entirely.

Copy link
Contributor Author

@diervo diervo Jul 2, 2017

Choose a reason for hiding this comment

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

I don't think we can completely get away without knowing we are within the context of a coma separated production due to ASI and explicit checks for ;
Im revising it nonetheless.

@nicolo-ribaudo
Copy link
Member

Fixes location for publicFields (should not contain semi-colon)

Why? Usually the semicolon is included:

var a = 2; // start: 0, end: 10
foo(); // start: 0, end: 6

@diervo
Copy link
Contributor Author

diervo commented Jul 2, 2017

@nicolo-ribaudo Not for comma separated statements, which is what I'm refering here.

import { a, b, c } from "x" // location do not include comma
var x = 1, b = 2, z = 3; // location do not include comma

Will revisit, and will try to to make it consistent, but it might be tricky.

- Fixes babel#540
- Allow decorators for comma separated fields
- Added tests
@diervo
Copy link
Contributor Author

diervo commented Jul 2, 2017

Ok based on the comments I refactor pretty much the whole thing (sorry I had to rebase for simplicity of sake):

  • Added more tests
  • All locations should be correct now (@nicolo-ribaudo)
  • Refactor for better parse errors when we a re in the comma separated case.

@jridgewell We need to know whether or not we are in a "comma-separated" statement inside parseClassMember to check for specific unexpected characters depending on the case (method vs. prop)

@nicolo-ribaudo
Copy link
Member

I have a doubt about decorators:

class A {
  @dec x, y;
}
class B {
  @dec x;
  y;
}
class C {
  @dec x;
  @dec y;
}

Is A more like B or like C? According to this pr, it is like C; Typescript interprets it like B.
The decorators2 plugin doesn't have this problem because it disallows decorators on class prpoerties, but we should decide how must decrators behave,

@diervo
Copy link
Contributor Author

diervo commented Jul 3, 2017

@nicolo-ribaudo

A and C are equivalent.
Decorators should be applied to all the comma separated fields (keep me honest @littledan)

@peey should revisit this logic once this PR lands in master.

@littledan
Copy link

@diervo Agree with what you're saying about equivalency (except it matters how many times dec is evaluated--imagine it's a getter on the global object, or inside a with). A decorator applying to multiple comma-separated elements is the entire reason comma-separated elements were proposed by @wycats; maybe he can provide more clarification.

Some test ideas:

  • Static fields that are comma-separated: class C { static x, y }
  • Invalid to put a comma between two methods class C { a(){}, b() {} }
  • Mixed public and private fields that are comma-separated class C { #x, y }

cc @bakkot

@nicolo-ribaudo
Copy link
Member

nicolo-ribaudo commented Jul 4, 2017

except it matters how many times dec is evaluated

Then the AST must somehow reflect the difference between a decorator applied to a list of properties and the same decorator applied to different properties. Otherwise how could babel-generator/prettier/any other code generator know what code to emit?


e.g.

AST
Program {
  body: [
    ClassDeclaration {
      id: Identifier { name: "Foo" },
      body: ClassBody {
        body: [
          ClassProperty {
            decorators: [
              Decorator {
                expression: CallExpression {
                  callee: Identifier { name: "dec" },
                  arguments: []
                }
              }
            ],
            key: Identifier { name: "a" }
          },
          ClassProperty {
            decorators: [
              Decorator {
                expression: CallExpression {
                  callee: Identifier { name: "dec" },
                  arguments: []
                }
              }
            ],
            key: Identifier { name: "b" }
          }
        ],
      },
    },
    FunctionDeclaration {
      params: [],
      body: BlockStatement {
        body: [
          "dec = () => { throw new Error(); }",
          "return /* super cool decorator */"
        ]
      }
    }
  ]
}

This ast represents to different programs: one works and the other throws.

class Foo {
  @dec() a, b; // works
}

function dec() {
  dec = () => { throw new Error(); };
  return decorator;
}
class Foo {
  @dec() a;
  @dec() b; // Throws
}

function dec() {
  dec = () => { throw new Error(); };
  return decorator;
}

A possible solution is to represent a comma-separated list of properties actually as a list of properties, similar to a VariableDeclaration:

class Foo {
  @dec() a, b;
}
AST
ClassDeclaration {
  id: Identifier { name: "Foo" },
  body: ClassBody {
    body: [
      ClassPropertiesList {
        decorators: [
          Decorator {
            expression: CallExpression {
              callee: Identifier { name: "dec" },
              arguments: []
            }
          }
        ],
        static: false,
        properties: [
          ClassProperty {
            key: Identifier { name: "a" }
          },
          ClassProperty {
            key: Identifier { name: "b" }
          }
        ],
      }
    ],
  },
}


This approach has other two possible little advantages:

  • It prevents the inconsistent locations due to the commas and semicolons: ClassPropertiesList would contain the semicolon, which for now is included in the last property.
  • The location of the decorators nodes will be included in the parent node (currently, the decorators are child of every property in the list but the location of the property does not include the decorators')

I'm sorry if I may sound quite pedantic, but I'm trying to highlight possible problems 😶

@diervo
Copy link
Contributor Author

diervo commented Jul 4, 2017

@littledan @nicolo-ribaudo I think you are both right, but this will require some interesting refactor, and probably a somewhat big breaking change on the transformers for publicFields.

Is everyone ok then moving to the following productions (analogous to VariableDeclaration):

FieldDefinition:
+decorators: []
+definitions: [PublicField | PrivateField]

Basically any public/private fields will be enclosed by FieldDefinition, and no more decorators in the Fields itself.

If we do this, we might as well refactor the whole ClassBody to make it future proof as @jridgewell was suggested to do in #609.

If you give me thumbs up I can give it a run today, otherwise will have to wait til next weekend most likely or someone else can take over.

Thoughts?

@nicolo-ribaudo
Copy link
Member

and probably a somewhat big breaking change

In #609 @xtuc suggested to merge the classProperties and classPrivateProperties. We can enable the changes introduced by this pr only under the new plugin to avoid breaking changes

@diervo
Copy link
Contributor Author

diervo commented Jul 4, 2017

@nicolo-ribaudo The problem is not creating a new plugin, is the fact that we are changing the shape of the Nodes (for example no longer ClassProperty will have decorators, or potentially a change on the node name ClassProperty =>PublicField ) that transformers expect.

And I'm very reluctant to have a fork in the code that generate different Node types based on the syntax plugin that is enabled, specially since we are working in babel7 where we have more room for error. If we do this changes, my vote is to do it in a non backward compatibility way.

@hzoo what you think?

@littledan
Copy link

If this is for Babel 7, could the legacy decorator syntax and transform also be updated to the new AST as well? It seems like the new form you're proposing is simply more general. Are there a lot of transforms out there which depend on the current shape of decorators?

@diervo
Copy link
Contributor Author

diervo commented Aug 13, 2017

Closing this since comma separated proposal was dropped from the original classField proposal and will be advancing separately.

@diervo diervo closed this Aug 13, 2017
@littledan
Copy link

If someone wants to reopen the PR and pick it up, you can find the new proposal at https://github.com/littledan/proposal-comma-separated-fields . The default class fields flag needs to prohibit comma separation, with a separate flag to permit it; I am not sure whether this flag behavior should be implemented at the babylon or transform level.

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Class Fields (combined) - Stage 2
5 participants