diff --git a/packages/babel-generator/src/generators/classes.js b/packages/babel-generator/src/generators/classes.js index e8c62339a6d2..1713293369d4 100644 --- a/packages/babel-generator/src/generators/classes.js +++ b/packages/babel-generator/src/generators/classes.js @@ -71,26 +71,8 @@ export function ClassBody(node: Object) { export function ClassProperty(node: Object) { this.printJoin(node.decorators, node); + this.tsPrintClassMemberModifiers(node, /* isField */ true); - if (node.accessibility) { - // TS - this.word(node.accessibility); - this.space(); - } - if (node.static) { - this.word("static"); - this.space(); - } - if (node.abstract) { - // TS - this.word("abstract"); - this.space(); - } - if (node.readonly) { - // TS - this.word("readonly"); - this.space(); - } if (node.computed) { this.token("["); this.print(node.key, node); @@ -148,23 +130,6 @@ export function ClassPrivateMethod(node: Object) { export function _classMethodHead(node) { this.printJoin(node.decorators, node); - - if (node.accessibility) { - // TS - this.word(node.accessibility); - this.space(); - } - - if (node.abstract) { - // TS - this.word("abstract"); - this.space(); - } - - if (node.static) { - this.word("static"); - this.space(); - } - + this.tsPrintClassMemberModifiers(node, /* isField */ false); this._methodHead(node); } diff --git a/packages/babel-generator/src/generators/typescript.js b/packages/babel-generator/src/generators/typescript.js index a201a5d5d658..39c580d74d2f 100644 --- a/packages/babel-generator/src/generators/typescript.js +++ b/packages/babel-generator/src/generators/typescript.js @@ -556,3 +556,26 @@ export function tsPrintSignatureDeclarationBase(node) { this.token(")"); this.print(node.typeAnnotation, node); } + +export function tsPrintClassMemberModifiers(node, isField) { + if (isField && node.declare) { + this.word("declare"); + this.space(); + } + if (node.accessibility) { + this.word(node.accessibility); + this.space(); + } + if (node.static) { + this.word("static"); + this.space(); + } + if (node.abstract) { + this.word("abstract"); + this.space(); + } + if (isField && node.readonly) { + this.word("readonly"); + this.space(); + } +} diff --git a/packages/babel-generator/test/fixtures/typescript/class-field-declare/input.js b/packages/babel-generator/test/fixtures/typescript/class-field-declare/input.js new file mode 100644 index 000000000000..0e524a92bb8a --- /dev/null +++ b/packages/babel-generator/test/fixtures/typescript/class-field-declare/input.js @@ -0,0 +1,5 @@ +class A { + declare foo; + declare bar: string; + declare readonly bax: number; +} diff --git a/packages/babel-generator/test/fixtures/typescript/class-field-declare/options.json b/packages/babel-generator/test/fixtures/typescript/class-field-declare/options.json new file mode 100644 index 000000000000..9f3a0c2c0ff1 --- /dev/null +++ b/packages/babel-generator/test/fixtures/typescript/class-field-declare/options.json @@ -0,0 +1,4 @@ +{ + "sourceType": "module", + "plugins": ["typescript", "classProperties"] +} diff --git a/packages/babel-generator/test/fixtures/typescript/class-field-declare/output.js b/packages/babel-generator/test/fixtures/typescript/class-field-declare/output.js new file mode 100644 index 000000000000..d1d7b1be56aa --- /dev/null +++ b/packages/babel-generator/test/fixtures/typescript/class-field-declare/output.js @@ -0,0 +1,5 @@ +class A { + declare foo; + declare bar: string; + declare readonly bax: number; +} \ No newline at end of file diff --git a/packages/babel-helper-create-class-features-plugin/src/fields.js b/packages/babel-helper-create-class-features-plugin/src/fields.js index 4cdaccd1bbb5..167e894cae33 100644 --- a/packages/babel-helper-create-class-features-plugin/src/fields.js +++ b/packages/babel-helper-create-class-features-plugin/src/fields.js @@ -5,6 +5,8 @@ import ReplaceSupers, { import memberExpressionToFunctions from "@babel/helper-member-expression-to-functions"; import optimiseCall from "@babel/helper-optimise-call-expression"; +import * as ts from "./typescript"; + export function buildPrivateNamesMap(props) { const privateNamesMap = new Map(); for (const prop of props) { @@ -556,6 +558,8 @@ export function buildFieldsInitNodes( let needsClassRef = false; for (const prop of props) { + ts.assertFieldTransformed(prop); + const isStatic = prop.node.static; const isInstance = !isStatic; const isPrivate = prop.isPrivate(); diff --git a/packages/babel-helper-create-class-features-plugin/src/typescript.js b/packages/babel-helper-create-class-features-plugin/src/typescript.js new file mode 100644 index 000000000000..c18f0ea17389 --- /dev/null +++ b/packages/babel-helper-create-class-features-plugin/src/typescript.js @@ -0,0 +1,19 @@ +// @flow + +import type { NodePath } from "@babel/traverse"; + +export function assertFieldTransformed(path: NodePath) { + // TODO (Babel 8): Also check path.node.definite + + if (path.node.declare) { + throw path.buildCodeFrameError( + `TypeScript 'declare' fields must first be transformed by ` + + `@babel/plugin-transform-typescript.\n` + + `If you have already enabled that plugin (or '@babel/preset-typescript'), make sure ` + + `that it runs before any plugin related to additional class features:\n` + + ` - @babel/plugin-proposal-class-properties\n` + + ` - @babel/plugin-proposal-private-methods\n` + + ` - @babel/plugin-proposal-decorators`, + ); + } +} diff --git a/packages/babel-parser/src/plugins/typescript/index.js b/packages/babel-parser/src/plugins/typescript/index.js index e5d461b55e46..9fabb50cd0a2 100644 --- a/packages/babel-parser/src/plugins/typescript/index.js +++ b/packages/babel-parser/src/plugins/typescript/index.js @@ -28,6 +28,7 @@ import * as charCodes from "charcodes"; type TsModifier = | "readonly" | "abstract" + | "declare" | "static" | "public" | "private" @@ -129,6 +130,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": @@ -405,7 +431,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) && @@ -1844,50 +1870,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, @@ -1902,6 +1927,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` @@ -2048,6 +2087,14 @@ export default (superClass: Class): Class => parseClassProperty(node: N.ClassProperty): N.ClassProperty { this.parseClassPropertyAnnotation(node); + + 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 f5b34a2db0a3..8392a58db0f2 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/output.json b/packages/babel-parser/test/fixtures/typescript/class/declare-method/output.json new file mode 100644 index 000000000000..0219c43119d2 --- /dev/null +++ b/packages/babel-parser/test/fixtures/typescript/class/declare-method/output.json @@ -0,0 +1,145 @@ +{ + "type": "File", + "start": 0, + "end": 30, + "loc": { + "start": { + "line": 1, + "column": 0 + }, + "end": { + "line": 3, + "column": 1 + } + }, + "errors": [ + "SyntaxError: Class methods cannot have the 'declare' modifier (2:2)" + ], + "program": { + "type": "Program", + "start": 0, + "end": 30, + "loc": { + "start": { + "line": 1, + "column": 0 + }, + "end": { + "line": 3, + "column": 1 + } + }, + "sourceType": "module", + "interpreter": null, + "body": [ + { + "type": "ClassDeclaration", + "start": 0, + "end": 30, + "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": 30, + "loc": { + "start": { + "line": 1, + "column": 8 + }, + "end": { + "line": 3, + "column": 1 + } + }, + "body": [ + { + "type": "ClassMethod", + "start": 12, + "end": 28, + "loc": { + "start": { + "line": 2, + "column": 2 + }, + "end": { + "line": 2, + "column": 18 + } + }, + "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, + "kind": "method", + "id": null, + "generator": false, + "async": false, + "params": [], + "body": { + "type": "BlockStatement", + "start": 26, + "end": 28, + "loc": { + "start": { + "line": 2, + "column": 16 + }, + "end": { + "line": 2, + "column": 18 + } + }, + "body": [], + "directives": [] + } + } + ] + } + } + ], + "directives": [] + } +} \ 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 deleted file mode 100644 index f7dd2e27ef2c..000000000000 --- a/packages/babel-parser/test/fixtures/typescript/class/method-readonly/options.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "throws": "Unexpected token, expected \";\" (2:14)" -} diff --git a/packages/babel-parser/test/fixtures/typescript/class/method-readonly/output.json b/packages/babel-parser/test/fixtures/typescript/class/method-readonly/output.json new file mode 100644 index 000000000000..198e2e1cf0c9 --- /dev/null +++ b/packages/babel-parser/test/fixtures/typescript/class/method-readonly/output.json @@ -0,0 +1,145 @@ +{ + "type": "File", + "start": 0, + "end": 31, + "loc": { + "start": { + "line": 1, + "column": 0 + }, + "end": { + "line": 3, + "column": 1 + } + }, + "errors": [ + "SyntaxError: Class methods cannot have the 'readonly' modifier (2:4)" + ], + "program": { + "type": "Program", + "start": 0, + "end": 31, + "loc": { + "start": { + "line": 1, + "column": 0 + }, + "end": { + "line": 3, + "column": 1 + } + }, + "sourceType": "module", + "interpreter": null, + "body": [ + { + "type": "ClassDeclaration", + "start": 0, + "end": 31, + "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": "C" + }, + "name": "C" + }, + "superClass": null, + "body": { + "type": "ClassBody", + "start": 8, + "end": 31, + "loc": { + "start": { + "line": 1, + "column": 8 + }, + "end": { + "line": 3, + "column": 1 + } + }, + "body": [ + { + "type": "ClassMethod", + "start": 14, + "end": 29, + "loc": { + "start": { + "line": 2, + "column": 4 + }, + "end": { + "line": 2, + "column": 19 + } + }, + "readonly": true, + "static": false, + "key": { + "type": "Identifier", + "start": 23, + "end": 24, + "loc": { + "start": { + "line": 2, + "column": 13 + }, + "end": { + "line": 2, + "column": 14 + }, + "identifierName": "m" + }, + "name": "m" + }, + "computed": false, + "kind": "method", + "id": null, + "generator": false, + "async": false, + "params": [], + "body": { + "type": "BlockStatement", + "start": 27, + "end": 29, + "loc": { + "start": { + "line": 2, + "column": 17 + }, + "end": { + "line": 2, + "column": 19 + } + }, + "body": [], + "directives": [] + } + } + ] + } + } + ], + "directives": [] + } +} \ No newline at end of file diff --git a/packages/babel-parser/test/fixtures/typescript/class/private-fields-modifier-readonly/output.json b/packages/babel-parser/test/fixtures/typescript/class/private-fields-modifier-readonly/output.json index f883e0b00b2a..456ca59ae4ae 100644 --- a/packages/babel-parser/test/fixtures/typescript/class/private-fields-modifier-readonly/output.json +++ b/packages/babel-parser/test/fixtures/typescript/class/private-fields-modifier-readonly/output.json @@ -77,7 +77,7 @@ }, "body": [ { - "type": "ClassProperty", + "type": "ClassPrivateProperty", "start": 12, "end": 24, "loc": { @@ -127,7 +127,7 @@ "value": null }, { - "type": "ClassProperty", + "type": "ClassPrivateProperty", "start": 27, "end": 47, "loc": { diff --git a/packages/babel-plugin-transform-typescript/src/index.js b/packages/babel-plugin-transform-typescript/src/index.js index c89e2dbe8185..ed7d927fe595 100644 --- a/packages/babel-plugin-transform-typescript/src/index.js +++ b/packages/babel-plugin-transform-typescript/src/index.js @@ -44,11 +44,94 @@ function registerGlobalType(programScope, name) { } export default declare( - (api, { jsxPragma = "React", allowNamespaces = false }) => { + ( + api, + { + jsxPragma = "React", + allowNamespaces = false, + allowDeclareFields = false, + }, + ) => { api.assertVersion(7); const JSX_ANNOTATION_REGEX = /\*?\s*@jsx\s+([^\s]+)/; + const classMemberVisitors = { + field(path) { + const { node } = path; + + if (!allowDeclareFields && node.declare) { + throw path.buildCodeFrameError( + `The 'declare' modifier is only allowed when the 'allowDeclareFields' option of ` + + `@babel/plugin-transform-typescript or @babel/preset-typescript is enabled.`, + ); + } + if (node.definite || node.declare) { + if (node.value) { + throw path.buildCodeFrameError( + `Definietly assigned fields and fields with the 'declare' modifier cannot` + + ` be initialized here, but only in the constructor`, + ); + } + + path.remove(); + } else if (!allowDeclareFields && !node.value && !node.decorators) { + path.remove(); + } + + if (node.accessibility) node.accessibility = null; + if (node.abstract) node.abstract = null; + if (node.readonly) node.readonly = null; + if (node.optional) node.optional = null; + if (node.typeAnnotation) node.typeAnnotation = null; + }, + method({ node }) { + if (node.accessibility) node.accessibility = null; + if (node.abstract) node.abstract = null; + if (node.optional) node.optional = null; + + // Rest handled by Function visitor + }, + constructor(path, classPath) { + // Collects parameter properties so that we can add an assignment + // for each of them in the constructor body + // + // We use a WeakSet to ensure an assignment for a parameter + // property is only added once. This is necessary for cases like + // using `transform-classes`, which causes this visitor to run + // twice. + const parameterProperties = []; + for (const param of path.node.params) { + if ( + param.type === "TSParameterProperty" && + !PARSED_PARAMS.has(param.parameter) + ) { + PARSED_PARAMS.add(param.parameter); + parameterProperties.push(param.parameter); + } + } + + if (parameterProperties.length) { + const assigns = parameterProperties.map(p => { + let id; + if (t.isIdentifier(p)) { + id = p; + } else if (t.isAssignmentPattern(p) && t.isIdentifier(p.left)) { + id = p.left; + } else { + throw path.buildCodeFrameError( + "Parameter properties can not be destructuring patterns.", + ); + } + + return template.statement.ast`this.${id} = ${id}`; + }); + + injectInitialization(classPath, path, assigns); + } + }, + }; + return { name: "transform-typescript", inherits: syntaxTypeScript, @@ -192,27 +275,6 @@ export default declare( if (node.definite) node.definite = null; }, - ClassMethod(path) { - const { node } = path; - - if (node.accessibility) node.accessibility = null; - if (node.abstract) node.abstract = null; - if (node.optional) node.optional = null; - - // Rest handled by Function visitor - }, - - ClassProperty(path) { - const { node } = path; - - if (node.accessibility) node.accessibility = null; - if (node.abstract) node.abstract = null; - if (node.readonly) node.readonly = null; - if (node.optional) node.optional = null; - if (node.definite) node.definite = null; - if (node.typeAnnotation) node.typeAnnotation = null; - }, - TSIndexSignature(path) { path.remove(); }, @@ -238,54 +300,14 @@ export default declare( // class transform would transform the class, causing more specific // visitors to not run. path.get("body.body").forEach(child => { - const childNode = child.node; - - if (t.isClassMethod(childNode, { kind: "constructor" })) { - // Collects parameter properties so that we can add an assignment - // for each of them in the constructor body - // - // We use a WeakSet to ensure an assignment for a parameter - // property is only added once. This is necessary for cases like - // using `transform-classes`, which causes this visitor to run - // twice. - const parameterProperties = []; - for (const param of childNode.params) { - if ( - param.type === "TSParameterProperty" && - !PARSED_PARAMS.has(param.parameter) - ) { - PARSED_PARAMS.add(param.parameter); - parameterProperties.push(param.parameter); - } - } - - if (parameterProperties.length) { - const assigns = parameterProperties.map(p => { - let id; - if (t.isIdentifier(p)) { - id = p; - } else if ( - t.isAssignmentPattern(p) && - t.isIdentifier(p.left) - ) { - id = p.left; - } else { - throw path.buildCodeFrameError( - "Parameter properties can not be destructuring patterns.", - ); - } - - return template.statement.ast`this.${id} = ${id}`; - }); - - injectInitialization(path, child, assigns); + if (child.isClassMethod()) { + if (child.node.kind === "constructor") { + classMemberVisitors.constructor(child, path); + } else { + classMemberVisitors.method(child, path); } } else if (child.isClassProperty()) { - childNode.typeAnnotation = null; - - if (!childNode.value && !childNode.decorators) { - child.remove(); - } + classMemberVisitors.field(child, path); } }); }, diff --git a/packages/babel-plugin-transform-typescript/test/fixtures/class/declare-not-enabled/input.ts b/packages/babel-plugin-transform-typescript/test/fixtures/class/declare-not-enabled/input.ts new file mode 100644 index 000000000000..3cb9d54665fa --- /dev/null +++ b/packages/babel-plugin-transform-typescript/test/fixtures/class/declare-not-enabled/input.ts @@ -0,0 +1,3 @@ +class A { + declare x; +} diff --git a/packages/babel-plugin-transform-typescript/test/fixtures/class/declare-not-enabled/options.json b/packages/babel-plugin-transform-typescript/test/fixtures/class/declare-not-enabled/options.json new file mode 100644 index 000000000000..c844b0ad736c --- /dev/null +++ b/packages/babel-plugin-transform-typescript/test/fixtures/class/declare-not-enabled/options.json @@ -0,0 +1,4 @@ +{ + "plugins": ["transform-typescript"], + "throws": "The 'declare' modifier is only allowed when the 'allowDeclareFields' option of @babel/plugin-transform-typescript or @babel/preset-typescript is enabled." +} diff --git a/packages/babel-plugin-transform-typescript/test/fixtures/class/declare-not-initialized/input.ts b/packages/babel-plugin-transform-typescript/test/fixtures/class/declare-not-initialized/input.ts new file mode 100644 index 000000000000..c7c42323a390 --- /dev/null +++ b/packages/babel-plugin-transform-typescript/test/fixtures/class/declare-not-initialized/input.ts @@ -0,0 +1,3 @@ +class A { + x; +} \ No newline at end of file diff --git a/packages/babel-plugin-transform-typescript/test/fixtures/class/declare-not-initialized/options.json b/packages/babel-plugin-transform-typescript/test/fixtures/class/declare-not-initialized/options.json new file mode 100644 index 000000000000..a6d406a50e49 --- /dev/null +++ b/packages/babel-plugin-transform-typescript/test/fixtures/class/declare-not-initialized/options.json @@ -0,0 +1,3 @@ +{ + "plugins": [["transform-typescript", { "allowDeclareFields": true }]] +} diff --git a/packages/babel-plugin-transform-typescript/test/fixtures/class/declare-not-initialized/output.js b/packages/babel-plugin-transform-typescript/test/fixtures/class/declare-not-initialized/output.js new file mode 100644 index 000000000000..eebf5c5c33db --- /dev/null +++ b/packages/babel-plugin-transform-typescript/test/fixtures/class/declare-not-initialized/output.js @@ -0,0 +1,3 @@ +class A { + x; +} diff --git a/packages/babel-plugin-transform-typescript/test/fixtures/class/declare/input.ts b/packages/babel-plugin-transform-typescript/test/fixtures/class/declare/input.ts new file mode 100644 index 000000000000..7ab37b71fb39 --- /dev/null +++ b/packages/babel-plugin-transform-typescript/test/fixtures/class/declare/input.ts @@ -0,0 +1,3 @@ +class A { + declare x; +} \ No newline at end of file diff --git a/packages/babel-plugin-transform-typescript/test/fixtures/class/declare/options.json b/packages/babel-plugin-transform-typescript/test/fixtures/class/declare/options.json new file mode 100644 index 000000000000..a6d406a50e49 --- /dev/null +++ b/packages/babel-plugin-transform-typescript/test/fixtures/class/declare/options.json @@ -0,0 +1,3 @@ +{ + "plugins": [["transform-typescript", { "allowDeclareFields": true }]] +} diff --git a/packages/babel-plugin-transform-typescript/test/fixtures/class/declare/output.js b/packages/babel-plugin-transform-typescript/test/fixtures/class/declare/output.js new file mode 100644 index 000000000000..a869c2849526 --- /dev/null +++ b/packages/babel-plugin-transform-typescript/test/fixtures/class/declare/output.js @@ -0,0 +1 @@ +class A {} diff --git a/packages/babel-plugin-transform-typescript/test/fixtures/class/properties/input.ts b/packages/babel-plugin-transform-typescript/test/fixtures/class/properties/input.ts index abb53ac69bc3..b59f26e0c387 100644 --- a/packages/babel-plugin-transform-typescript/test/fixtures/class/properties/input.ts +++ b/packages/babel-plugin-transform-typescript/test/fixtures/class/properties/input.ts @@ -1,7 +1,8 @@ class C { public a?: number; private b: number = 0; - readonly c!: number = 1; + readonly c: number = 1; @foo d: number; @foo e: number = 3; + f!: number; } diff --git a/packages/babel-plugin-transform-typescript/test/fixtures/class/transform-properties-declare-wrong-order/input.ts b/packages/babel-plugin-transform-typescript/test/fixtures/class/transform-properties-declare-wrong-order/input.ts new file mode 100644 index 000000000000..afc220470140 --- /dev/null +++ b/packages/babel-plugin-transform-typescript/test/fixtures/class/transform-properties-declare-wrong-order/input.ts @@ -0,0 +1,4 @@ +class A { + declare x; + y; +} \ No newline at end of file diff --git a/packages/babel-plugin-transform-typescript/test/fixtures/class/transform-properties-declare-wrong-order/options.json b/packages/babel-plugin-transform-typescript/test/fixtures/class/transform-properties-declare-wrong-order/options.json new file mode 100644 index 000000000000..8a07f1cc2003 --- /dev/null +++ b/packages/babel-plugin-transform-typescript/test/fixtures/class/transform-properties-declare-wrong-order/options.json @@ -0,0 +1,7 @@ +{ + "plugins": [ + "proposal-class-properties", + ["transform-typescript", { "allowDeclareFields": true }] + ], + "throws": "TypeScript 'declare' fields must first be transformed by @babel/plugin-transform-typescript.\nIf you have already enabled that plugin (or '@babel/preset-typescript'), make sure that it runs before any plugin related to additional class features:\n - @babel/plugin-proposal-class-properties\n - @babel/plugin-proposal-private-methods\n - @babel/plugin-proposal-decorators" +} diff --git a/packages/babel-plugin-transform-typescript/test/fixtures/class/transform-properties-declare/input.ts b/packages/babel-plugin-transform-typescript/test/fixtures/class/transform-properties-declare/input.ts new file mode 100644 index 000000000000..afc220470140 --- /dev/null +++ b/packages/babel-plugin-transform-typescript/test/fixtures/class/transform-properties-declare/input.ts @@ -0,0 +1,4 @@ +class A { + declare x; + y; +} \ No newline at end of file diff --git a/packages/babel-plugin-transform-typescript/test/fixtures/class/transform-properties-declare/options.json b/packages/babel-plugin-transform-typescript/test/fixtures/class/transform-properties-declare/options.json new file mode 100644 index 000000000000..18a1de9a1531 --- /dev/null +++ b/packages/babel-plugin-transform-typescript/test/fixtures/class/transform-properties-declare/options.json @@ -0,0 +1,6 @@ +{ + "plugins": [ + ["transform-typescript", { "allowDeclareFields": true }], + "proposal-class-properties" + ] +} \ No newline at end of file diff --git a/packages/babel-plugin-transform-typescript/test/fixtures/class/transform-properties-declare/output.js b/packages/babel-plugin-transform-typescript/test/fixtures/class/transform-properties-declare/output.js new file mode 100644 index 000000000000..44b56f160013 --- /dev/null +++ b/packages/babel-plugin-transform-typescript/test/fixtures/class/transform-properties-declare/output.js @@ -0,0 +1,8 @@ +function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } + +class A { + constructor() { + _defineProperty(this, "y", void 0); + } + +} diff --git a/packages/babel-plugin-transform-typescript/test/fixtures/class/uninitialized-definite-with-declare-enabled/input.ts b/packages/babel-plugin-transform-typescript/test/fixtures/class/uninitialized-definite-with-declare-enabled/input.ts new file mode 100644 index 000000000000..4bb8da7936c6 --- /dev/null +++ b/packages/babel-plugin-transform-typescript/test/fixtures/class/uninitialized-definite-with-declare-enabled/input.ts @@ -0,0 +1,3 @@ +class A { + x!; +} \ No newline at end of file diff --git a/packages/babel-plugin-transform-typescript/test/fixtures/class/uninitialized-definite-with-declare-enabled/options.json b/packages/babel-plugin-transform-typescript/test/fixtures/class/uninitialized-definite-with-declare-enabled/options.json new file mode 100644 index 000000000000..a6d406a50e49 --- /dev/null +++ b/packages/babel-plugin-transform-typescript/test/fixtures/class/uninitialized-definite-with-declare-enabled/options.json @@ -0,0 +1,3 @@ +{ + "plugins": [["transform-typescript", { "allowDeclareFields": true }]] +} diff --git a/packages/babel-plugin-transform-typescript/test/fixtures/class/uninitialized-definite-with-declare-enabled/output.js b/packages/babel-plugin-transform-typescript/test/fixtures/class/uninitialized-definite-with-declare-enabled/output.js new file mode 100644 index 000000000000..a869c2849526 --- /dev/null +++ b/packages/babel-plugin-transform-typescript/test/fixtures/class/uninitialized-definite-with-declare-enabled/output.js @@ -0,0 +1 @@ +class A {} diff --git a/packages/babel-preset-typescript/src/index.js b/packages/babel-preset-typescript/src/index.js index bb1687e623c4..f7dc5eefae00 100644 --- a/packages/babel-preset-typescript/src/index.js +++ b/packages/babel-preset-typescript/src/index.js @@ -4,7 +4,13 @@ import transformTypeScript from "@babel/plugin-transform-typescript"; export default declare( ( api, - { jsxPragma, allExtensions = false, isTSX = false, allowNamespaces }, + { + jsxPragma, + allExtensions = false, + isTSX = false, + allowNamespaces, + allowDeclareFields, + }, ) => { api.assertVersion(7); @@ -19,13 +25,18 @@ export default declare( throw new Error("isTSX:true requires allExtensions:true"); } + const pluginOptions = isTSX => ({ + jsxPragma, + isTSX, + allowNamespaces, + allowDeclareFields, + }); + return { overrides: allExtensions ? [ { - plugins: [ - [transformTypeScript, { jsxPragma, isTSX, allowNamespaces }], - ], + plugins: [[transformTypeScript, pluginOptions(isTSX)]], }, ] : [ @@ -33,18 +44,13 @@ export default declare( // Only set 'test' if explicitly requested, since it requires that // Babel is being called` test: /\.ts$/, - plugins: [[transformTypeScript, { jsxPragma, allowNamespaces }]], + plugins: [[transformTypeScript, pluginOptions(false)]], }, { // Only set 'test' if explicitly requested, since it requires that // Babel is being called` test: /\.tsx$/, - plugins: [ - [ - transformTypeScript, - { jsxPragma, isTSX: true, allowNamespaces }, - ], - ], + plugins: [[transformTypeScript, pluginOptions(true)]], }, ], }; diff --git a/packages/babel-types/src/definitions/experimental.js b/packages/babel-types/src/definitions/experimental.js index d344630dcc75..f4e110e1002f 100644 --- a/packages/babel-types/src/definitions/experimental.js +++ b/packages/babel-types/src/definitions/experimental.js @@ -67,6 +67,10 @@ defineType("ClassProperty", { validate: assertValueType("boolean"), optional: true, }, + declare: { + validate: assertValueType("boolean"), + optional: true, + }, }, });