babel icon indicating copy to clipboard operation
babel copied to clipboard

[breaking] Make Identifier an "atomic" node

Open nicolo-ribaudo opened this issue 6 years ago • 11 comments

Identifiers can have different properties totally unrelated to their "Identifierness":

  • optional (e.g. in function f(a?) {} with Flow)
  • typeAnnotation (e.g. in var a: string; with Flow/TypeScript)
  • decorators (e.g. in function f(@dec a) {} with TypeScript)

Every other "atom" (e.g. strings, numbers, ...) don't have child nodes. Additionally, it makes it impossible to get the actual identifier name start/end position, because in foo: string foo isn't represented by anything. Example: AST Explorer.

Also, these properties don't make sense in many places: what does it mean if I add typeAnnotation to prop in obj.prop? Or optional to label in break label;?

I propose creating a new node, AnnotatedPattern (name suggestions are welcome), like this:

defineType("AnnotatedPattern", {
  builder: ["pattern", "typeAnnotation", "optional", "decorators"],
  visitor: ["pattern", "typeAnnotation", "decorators"],
  aliases: ["PatternLike", "LVal"],
  fields: {
    pattern: {
      validate: assertNodeType("PatternLike"),
    },
    typeAnnotation: {
      validate: assertNodeType("TypeAnnotation", "TSTypeAnnotation"),
      optional: true,
    },
    optional: {
      validate: assertValueType("boolean"), // Only valid if pattern is an identifier
      optional: true,
    },
    decorators: {
      validate: chain(
        assertValueType("array"),
        assertEach(assertNodeType("Decorator")),
      ),
      optional: true,
    },
  },
});

AnnotatedPattern would be accepted in function parameters and in variable declarations. typeAnnotation & co would then be removed from destructuring patterns.

nicolo-ribaudo avatar Feb 19 '19 22:02 nicolo-ribaudo

Are function params and variable declarations the only cases where we currently know there are issues?

One alternative would be to move the typeAnnotation field from Identifier to VariableDeclarator, and also add a new FunctionParam node type that would wrap the Identifier and have optional, decorators, and typeAnnotation.

I worry that AnnotatedPattern is too overloaded by having to work in both cases, and having a consistent FunctionParam node would make the AST less polymorphic.

loganfsmyth avatar Sep 26 '19 13:09 loganfsmyth

That would work. I don't think that "would make the AST less polymorphic" is a good argument because it just moves the polymorphism down to the child of FunctionParam, but I prefer your suggestion because of optional and decorators.

Also, TypeScript matches this proposal: https://astexplorer.net/#/gist/8ab88f26fe8714059fdc05d3387d5810/d97b0ed36c10c8101b6dec439cc1171624ec0b62

We also have the TSParameterProperty node, which would probably be merged to FunctionParam.

nicolo-ribaudo avatar Nov 04 '19 14:11 nicolo-ribaudo

Defining a new node type represents a significant breaking change for us when compared to the base ESTree spec, which can cause us all manner of issues with existing ESLint rules expecting a certain structure. It's not an insurmountable problem, but it's definitely something to consider.

I prefer the idea of hoisting the type annotation up to where it belongs, as opposed to defining a new node type. This is how typescript does it, and it seems to work well:

var foo: string;
// VariableDeclaration:
// - name: Identifier
// - type: StringKeyword

var {bar}: {bar: string};
// VariableDeclaration:
// - name: ObjectBindingPattern
// - type: TypeLiteral

var [bar]: [string];
// VariableDeclaration:
// - name: ArrayBindingPattern
// - type: TupleType

function foo(
  @deco arg?: number,
  // Parameter:
  // - name: Identifier
  // - type: NumberKeyword
  // - optional: true
  // - decorators: [Decorator]
   
  arg2?,
  // Parameter:
  // - name: Identifier
  // - optional: true
) {}

@deco
class Foo {
  // ClassDeclaration
  // - name: Identifier
  // - decorators: [Decorator]

  @deco
  constructor(@deco private a: string) {}
  // Parameter:
  // - name: Identifier
  // - type: StringKeyword
  // - modifiers: [PrivateKeyword]
  // - decorators: [Decorator]

  @deco
  prop: string;
  // PropertyDeclaration:
  // - name: Identifier
  // - type: StringKeyword
  // - decorators: [Decorator]

  @deco
  prop?: string;
  // PropertyDeclaration:
  // - name: Identifier
  // - type: StringKeyword
  // - optional: true
  // - decorators: [Decorator]

  prop?;
  // PropertyDeclaration:
  // - name: Identifier
  // - optional: true
}
  
interface Foo {
  prop: string;
  // PropertySignature:
  // name: Identifier
  // type: StringKeyword
  
  prop?: string;
  // PropertySignature:
  // - name: Identifier
  // - type: StringKeyword
  // - optional: true
}

bradzacher avatar Nov 04 '19 19:11 bradzacher

Defining a new node type represents a significant breaking change for us when compared to the base ESTree spec

Our AST is not compatible with ESTree already. We have a separate parser mode that we use when parsing for ESLint and that can always keep the existing behavior.

I prefer the idea of hoisting the type annotation up to where it belongs, as opposed to defining a new node type.

The issue at the moment is that, for function params, there is no place to hoist to, we only have Identifier. The Parameter node type in your example is what we're proposing adding. The question is basically whether Parameter should only exist when there is an optional or decorators or typeAnnotation or whether it should be expected in all situations.

loganfsmyth avatar Nov 05 '19 09:11 loganfsmyth

Note that, when the parser plugin estree is enabled, we should keep the old AST anyway.

nicolo-ribaudo avatar Nov 05 '19 10:11 nicolo-ribaudo

I was experimenting with this plugin and found a problem with flow:

let [a: T] = []

where do you suggest to store the type annotation in this case?

nicolo-ribaudo avatar Nov 12 '19 00:11 nicolo-ribaudo

what in the sam hill is that syntax?!?

It looks like flow completely ignores the type that's provided there:

const [a: number, b: string, c: boolean] = ['a', true, 1];
a.toLocaleLowerCase(); // a === string
b - 1 // b === boolean
c.toFixed(); // c === number

const arr: string[] = [];
const [d: number] = arr;
d.toLocaleLowerCase();

https://flow.org/try/#0PQKgBAAgZgNg9gdzCYAoVBjOA7AzgFzAG0BDALjGwFcBbAIwFMAnAGjDooKYEtsBzNhgp04cGAxLYAumAC8xAOQkFbfEyoM2ARikBuVCQB0+OABk4GEuPMJmAYRK4GACgCUusMGBgSc2fK5ePlQ6MABaMC1Pb1D-eRExCWxMYzgAMW4ADwYAEzcPLzAMP3lqemZ0LDxCEiYmTjUgohl5Zv0qgmIcijLGJhafOv0c1PNLa0R7Rxd3VCA

I think that's probably some legacy syntax or something that the parser allows "just because"? You know what - i'll ask the flow team.

bradzacher avatar Nov 12 '19 01:11 bradzacher

Quoting response from someone on the flow team:

It's not intentional. You're correct that the annotation is ignored. We should at least fix the parser to error here, and ideally change the AST as well, since the AST representation re-uses a node that supports annotations.


Adding a bit more after some discussion:

They're going to discuss this internally and figure out what they want to do. Right now, as stated, it's currently technically "a parser bug" because the parser reuses the variable decl syntax there. However, they are considering adding support for it properly, so that it is easier to type destructuring (they would then do a similar thing for object destructuring).

So right now - consider it "valid syntax", as it could either turn into an error, or supported as valid type information.

If they go down the latter route, they don't want to make it an error only to make it valid not long after, for obvious reasons.

bradzacher avatar Nov 12 '19 01:11 bradzacher

@bradzacher thanks for investigating!

existentialism avatar Nov 12 '19 20:11 existentialism

I was thinking again about this in the context of https://github.com/babel/babel/pull/14694. Either having a new node as I proposed in the original issue, or something like https://github.com/babel/babel/issues/9545#issuecomment-549514519, would have probably prevented that problem from happening.

nicolo-ribaudo avatar Jun 26 '22 20:06 nicolo-ribaudo

Quoting response from someone on the flow team:

It's not intentional. You're correct that the annotation is ignored. We should at least fix the parser to error here, and ideally change the AST as well, since the AST representation re-uses a node that supports annotations.

Adding a bit more after some discussion:

They're going to discuss this internally and figure out what they want to do. Right now, as stated, it's currently technically "a parser bug" because the parser reuses the variable decl syntax there. However, they are considering adding support for it properly, so that it is easier to type destructuring (they would then do a similar thing for object destructuring).

So right now - consider it "valid syntax", as it could either turn into an error, or supported as valid type information.

If they go down the latter route, they don't want to make it an error only to make it valid not long after, for obvious reasons.

Now it is no longer a valid syntax and is reported as an error in flow.js (from v0.176.0 to the latest). The typeAnnotation syntax of flow.js is very similar to TS. (Are they still going to change it? Please correct me if I'm wrong.) Maybe it's time to move on to this topic.

magic-akari avatar Aug 08 '22 08:08 magic-akari