From 6faf79a98b6aba9fdff61e1eba45851b9ad70d69 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicol=C3=B2=20Ribaudo?= Date: Sun, 13 Oct 2019 11:24:46 +0200 Subject: [PATCH] [parser] Add support for TS declare modifier on fields (#10484) * [parser] Add support for TS declare modifier on fields * Use Object.create(null) * Comment --- .../src/plugins/typescript/index.js | 117 +++++++---- packages/babel-parser/src/types.js | 21 +- .../class/declare-field-initializer/input.ts | 3 + .../declare-field-initializer/output.json | 170 ++++++++++++++++ .../typescript/class/declare-field/input.ts | 4 + .../class/declare-field/output.json | 187 ++++++++++++++++++ .../typescript/class/declare-method/input.ts | 3 + .../class/declare-method/options.json | 3 + .../class/method-readonly/options.json | 2 +- 9 files changed, 465 insertions(+), 45 deletions(-) create mode 100644 packages/babel-parser/test/fixtures/typescript/class/declare-field-initializer/input.ts create mode 100644 packages/babel-parser/test/fixtures/typescript/class/declare-field-initializer/output.json create mode 100644 packages/babel-parser/test/fixtures/typescript/class/declare-field/input.ts create mode 100644 packages/babel-parser/test/fixtures/typescript/class/declare-field/output.json create mode 100644 packages/babel-parser/test/fixtures/typescript/class/declare-method/input.ts create mode 100644 packages/babel-parser/test/fixtures/typescript/class/declare-method/options.json diff --git a/packages/babel-parser/src/plugins/typescript/index.js b/packages/babel-parser/src/plugins/typescript/index.js index 8adf82f1568a..7cbc1268c8ee 100644 --- a/packages/babel-parser/src/plugins/typescript/index.js +++ b/packages/babel-parser/src/plugins/typescript/index.js @@ -1,5 +1,7 @@ // @flow +/*:: declare var invariant; */ + import type { TokenType } from "../../tokenizer/types"; import { types as tt } from "../../tokenizer/types"; import { types as ct } from "../../tokenizer/context"; @@ -25,6 +27,7 @@ import * as charCodes from "charcodes"; type TsModifier = | "readonly" | "abstract" + | "declare" | "static" | "public" | "private" @@ -126,6 +129,31 @@ export default (superClass: Class): Class => return undefined; } + /** Parses a list of modifiers, in any order. + * If you need a specific order, you must call this function multiple times: + * this.tsParseModifiers(["public"]); + * this.tsParseModifiers(["abstract", "readonly"]); + */ + tsParseModifiers( + allowedModifiers: T[], + ): { [key: TsModifier]: ?true, __proto__: null } { + const modifiers = Object.create(null); + + while (true) { + const startPos = this.state.start; + const modifier: ?T = this.tsParseModifier(allowedModifiers); + + if (!modifier) break; + + if (Object.hasOwnProperty.call(modifiers, modifier)) { + this.raise(startPos, `Duplicate modifier: '${modifier}'`); + } + modifiers[modifier] = true; + } + + return modifiers; + } + tsIsListTerminator(kind: ParsingContext): boolean { switch (kind) { case "EnumMembers": @@ -402,7 +430,7 @@ export default (superClass: Class): Class => return this.eat(tt.name) && this.match(tt.colon); } - tsTryParseIndexSignature(node: N.TsIndexSignature): ?N.TsIndexSignature { + tsTryParseIndexSignature(node: N.Node): ?N.TsIndexSignature { if ( !( this.match(tt.bracketL) && @@ -1814,50 +1842,49 @@ export default (superClass: Class): Class => parseClassMemberWithIsStatic( classBody: N.ClassBody, - member: any, + member: N.ClassMember | N.TsIndexSignature, state: { hadConstructor: boolean }, isStatic: boolean, constructorAllowsSuper: boolean, ): void { - const methodOrProp: N.ClassMethod | N.ClassProperty = member; - const prop: N.ClassProperty = member; - const propOrIdx: N.ClassProperty | N.TsIndexSignature = member; - - let abstract = false, - readonly = false; - - const mod = this.tsParseModifier(["abstract", "readonly"]); - switch (mod) { - case "readonly": - readonly = true; - abstract = !!this.tsParseModifier(["abstract"]); - break; - case "abstract": - abstract = true; - readonly = !!this.tsParseModifier(["readonly"]); - break; - } + const modifiers = this.tsParseModifiers([ + "abstract", + "readonly", + "declare", + ]); - if (abstract) methodOrProp.abstract = true; - if (readonly) propOrIdx.readonly = true; + Object.assign(member, modifiers); - if (!abstract && !isStatic && !methodOrProp.accessibility) { - const idx = this.tsTryParseIndexSignature(member); - if (idx) { - classBody.body.push(idx); - return; + const idx = this.tsTryParseIndexSignature(member); + if (idx) { + classBody.body.push(idx); + + if (modifiers.abstract) { + this.raise( + member.start, + "Index signatures cannot have the 'abstract' modifier", + ); + } + if (isStatic) { + this.raise( + member.start, + "Index signatures cannot have the 'static' modifier", + ); + } + if ((member: any).accessibility) { + this.raise( + member.start, + `Index signatures cannot have an accessibility modifier ('${ + (member: any).accessibility + }')`, + ); } - } - if (readonly) { - // Must be a property (if not an index signature). - methodOrProp.static = isStatic; - this.parseClassPropertyName(prop); - this.parsePostMemberNameModifiers(methodOrProp); - this.pushClassProperty(classBody, prop); return; } + /*:: invariant(member.type !== "TSIndexSignature") */ + super.parseClassMemberWithIsStatic( classBody, member, @@ -1872,6 +1899,20 @@ export default (superClass: Class): Class => ): void { const optional = this.eat(tt.question); if (optional) methodOrProp.optional = true; + + if ((methodOrProp: any).readonly && this.match(tt.parenL)) { + this.raise( + methodOrProp.start, + "Class methods cannot have the 'readonly' modifier", + ); + } + + if ((methodOrProp: any).declare && this.match(tt.parenL)) { + this.raise( + methodOrProp.start, + "Class methods cannot have the 'declare' modifier", + ); + } } // Note: The reason we do this in `parseExpressionStatement` and not `parseStatement` @@ -2014,6 +2055,14 @@ export default (superClass: Class): Class => const type = this.tsTryParseTypeAnnotation(); if (type) node.typeAnnotation = type; + + if (node.declare && this.match(tt.equal)) { + this.raise( + this.state.start, + "'declare' class fields cannot have an initializer", + ); + } + return super.parseClassProperty(node); } diff --git a/packages/babel-parser/src/types.js b/packages/babel-parser/src/types.js index 8560da3a3f73..9ee76ab1c7b9 100644 --- a/packages/babel-parser/src/types.js +++ b/packages/babel-parser/src/types.js @@ -743,18 +743,19 @@ export type ClassPrivateMethod = NodeBase & computed: false, }; -export type ClassProperty = ClassMemberBase & { - type: "ClassProperty", - key: Expression, - value: ?Expression, // TODO: Not in spec that this is nullable. +export type ClassProperty = ClassMemberBase & + DeclarationBase & { + type: "ClassProperty", + key: Expression, + value: ?Expression, // TODO: Not in spec that this is nullable. - typeAnnotation?: ?TypeAnnotationBase, // TODO: Not in spec - variance?: ?FlowVariance, // TODO: Not in spec + typeAnnotation?: ?TypeAnnotationBase, // TODO: Not in spec + variance?: ?FlowVariance, // TODO: Not in spec - // TypeScript only: (TODO: Not in spec) - readonly?: true, - definite?: true, -}; + // TypeScript only: (TODO: Not in spec) + readonly?: true, + definite?: true, + }; export type ClassPrivateProperty = NodeBase & { type: "ClassPrivateProperty", diff --git a/packages/babel-parser/test/fixtures/typescript/class/declare-field-initializer/input.ts b/packages/babel-parser/test/fixtures/typescript/class/declare-field-initializer/input.ts new file mode 100644 index 000000000000..b77a17738bf5 --- /dev/null +++ b/packages/babel-parser/test/fixtures/typescript/class/declare-field-initializer/input.ts @@ -0,0 +1,3 @@ +class A { + declare bar: string = "test"; +} diff --git a/packages/babel-parser/test/fixtures/typescript/class/declare-field-initializer/output.json b/packages/babel-parser/test/fixtures/typescript/class/declare-field-initializer/output.json new file mode 100644 index 000000000000..fb27f3055064 --- /dev/null +++ b/packages/babel-parser/test/fixtures/typescript/class/declare-field-initializer/output.json @@ -0,0 +1,170 @@ +{ + "type": "File", + "start": 0, + "end": 43, + "loc": { + "start": { + "line": 1, + "column": 0 + }, + "end": { + "line": 3, + "column": 1 + } + }, + "program": { + "type": "Program", + "start": 0, + "end": 43, + "loc": { + "start": { + "line": 1, + "column": 0 + }, + "end": { + "line": 3, + "column": 1 + } + }, + "sourceType": "module", + "interpreter": null, + "body": [ + { + "type": "ClassDeclaration", + "start": 0, + "end": 43, + "loc": { + "start": { + "line": 1, + "column": 0 + }, + "end": { + "line": 3, + "column": 1 + } + }, + "id": { + "type": "Identifier", + "start": 6, + "end": 7, + "loc": { + "start": { + "line": 1, + "column": 6 + }, + "end": { + "line": 1, + "column": 7 + }, + "identifierName": "A" + }, + "name": "A" + }, + "superClass": null, + "body": { + "type": "ClassBody", + "start": 8, + "end": 43, + "loc": { + "start": { + "line": 1, + "column": 8 + }, + "end": { + "line": 3, + "column": 1 + } + }, + "body": [ + { + "type": "ClassProperty", + "start": 12, + "end": 41, + "loc": { + "start": { + "line": 2, + "column": 2 + }, + "end": { + "line": 2, + "column": 31 + } + }, + "declare": true, + "static": false, + "key": { + "type": "Identifier", + "start": 20, + "end": 23, + "loc": { + "start": { + "line": 2, + "column": 10 + }, + "end": { + "line": 2, + "column": 13 + }, + "identifierName": "bar" + }, + "name": "bar" + }, + "computed": false, + "typeAnnotation": { + "type": "TSTypeAnnotation", + "start": 23, + "end": 31, + "loc": { + "start": { + "line": 2, + "column": 13 + }, + "end": { + "line": 2, + "column": 21 + } + }, + "typeAnnotation": { + "type": "TSStringKeyword", + "start": 25, + "end": 31, + "loc": { + "start": { + "line": 2, + "column": 15 + }, + "end": { + "line": 2, + "column": 21 + } + } + } + }, + "value": { + "type": "StringLiteral", + "start": 34, + "end": 40, + "loc": { + "start": { + "line": 2, + "column": 24 + }, + "end": { + "line": 2, + "column": 30 + } + }, + "extra": { + "rawValue": "test", + "raw": "\"test\"" + }, + "value": "test" + } + } + ] + } + } + ], + "directives": [] + } +} \ No newline at end of file diff --git a/packages/babel-parser/test/fixtures/typescript/class/declare-field/input.ts b/packages/babel-parser/test/fixtures/typescript/class/declare-field/input.ts new file mode 100644 index 000000000000..a7a8b2d36280 --- /dev/null +++ b/packages/babel-parser/test/fixtures/typescript/class/declare-field/input.ts @@ -0,0 +1,4 @@ +class A { + declare foo; + declare bar: string; +} diff --git a/packages/babel-parser/test/fixtures/typescript/class/declare-field/output.json b/packages/babel-parser/test/fixtures/typescript/class/declare-field/output.json new file mode 100644 index 000000000000..746e2964f438 --- /dev/null +++ b/packages/babel-parser/test/fixtures/typescript/class/declare-field/output.json @@ -0,0 +1,187 @@ +{ + "type": "File", + "start": 0, + "end": 49, + "loc": { + "start": { + "line": 1, + "column": 0 + }, + "end": { + "line": 4, + "column": 1 + } + }, + "program": { + "type": "Program", + "start": 0, + "end": 49, + "loc": { + "start": { + "line": 1, + "column": 0 + }, + "end": { + "line": 4, + "column": 1 + } + }, + "sourceType": "module", + "interpreter": null, + "body": [ + { + "type": "ClassDeclaration", + "start": 0, + "end": 49, + "loc": { + "start": { + "line": 1, + "column": 0 + }, + "end": { + "line": 4, + "column": 1 + } + }, + "id": { + "type": "Identifier", + "start": 6, + "end": 7, + "loc": { + "start": { + "line": 1, + "column": 6 + }, + "end": { + "line": 1, + "column": 7 + }, + "identifierName": "A" + }, + "name": "A" + }, + "superClass": null, + "body": { + "type": "ClassBody", + "start": 8, + "end": 49, + "loc": { + "start": { + "line": 1, + "column": 8 + }, + "end": { + "line": 4, + "column": 1 + } + }, + "body": [ + { + "type": "ClassProperty", + "start": 12, + "end": 24, + "loc": { + "start": { + "line": 2, + "column": 2 + }, + "end": { + "line": 2, + "column": 14 + } + }, + "declare": true, + "static": false, + "key": { + "type": "Identifier", + "start": 20, + "end": 23, + "loc": { + "start": { + "line": 2, + "column": 10 + }, + "end": { + "line": 2, + "column": 13 + }, + "identifierName": "foo" + }, + "name": "foo" + }, + "computed": false, + "value": null + }, + { + "type": "ClassProperty", + "start": 27, + "end": 47, + "loc": { + "start": { + "line": 3, + "column": 2 + }, + "end": { + "line": 3, + "column": 22 + } + }, + "declare": true, + "static": false, + "key": { + "type": "Identifier", + "start": 35, + "end": 38, + "loc": { + "start": { + "line": 3, + "column": 10 + }, + "end": { + "line": 3, + "column": 13 + }, + "identifierName": "bar" + }, + "name": "bar" + }, + "computed": false, + "typeAnnotation": { + "type": "TSTypeAnnotation", + "start": 38, + "end": 46, + "loc": { + "start": { + "line": 3, + "column": 13 + }, + "end": { + "line": 3, + "column": 21 + } + }, + "typeAnnotation": { + "type": "TSStringKeyword", + "start": 40, + "end": 46, + "loc": { + "start": { + "line": 3, + "column": 15 + }, + "end": { + "line": 3, + "column": 21 + } + } + } + }, + "value": null + } + ] + } + } + ], + "directives": [] + } +} \ No newline at end of file diff --git a/packages/babel-parser/test/fixtures/typescript/class/declare-method/input.ts b/packages/babel-parser/test/fixtures/typescript/class/declare-method/input.ts new file mode 100644 index 000000000000..0e539a84a2a5 --- /dev/null +++ b/packages/babel-parser/test/fixtures/typescript/class/declare-method/input.ts @@ -0,0 +1,3 @@ +class A { + declare foo() {} +} diff --git a/packages/babel-parser/test/fixtures/typescript/class/declare-method/options.json b/packages/babel-parser/test/fixtures/typescript/class/declare-method/options.json new file mode 100644 index 000000000000..29a88dae9e6e --- /dev/null +++ b/packages/babel-parser/test/fixtures/typescript/class/declare-method/options.json @@ -0,0 +1,3 @@ +{ + "throws": "Class methods cannot have the 'declare' modifier (2:2)" +} \ No newline at end of file diff --git a/packages/babel-parser/test/fixtures/typescript/class/method-readonly/options.json b/packages/babel-parser/test/fixtures/typescript/class/method-readonly/options.json index f7dd2e27ef2c..c8df2376c0db 100644 --- a/packages/babel-parser/test/fixtures/typescript/class/method-readonly/options.json +++ b/packages/babel-parser/test/fixtures/typescript/class/method-readonly/options.json @@ -1,3 +1,3 @@ { - "throws": "Unexpected token, expected \";\" (2:14)" + "throws": "Class methods cannot have the 'readonly' modifier (2:4)" }