Skip to content

Commit

Permalink
Add support for TS declare modifier on fields (#10545)
Browse files Browse the repository at this point in the history
* [parser] Add support for TS declare modifier on fields (#10484)

* [parser] Add support for TS declare modifier on fields

* Use Object.create(null)

* Comment

* Add support for TS declare types to types and generator (#10544)

* Transform TypeScript "declare" fields (#10546)

* Transform TypeScript "declare" fields

* Remove multiple spaces

* declareFields -> allowDeclareFields

* Update after rebase
  • Loading branch information
nicolo-ribaudo committed Nov 5, 2019
1 parent 87feda7 commit e9c1bce
Show file tree
Hide file tree
Showing 38 changed files with 985 additions and 166 deletions.
39 changes: 2 additions & 37 deletions packages/babel-generator/src/generators/classes.js
Expand Up @@ -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);
Expand Down Expand Up @@ -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);
}
23 changes: 23 additions & 0 deletions packages/babel-generator/src/generators/typescript.js
Expand Up @@ -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();
}
}
@@ -0,0 +1,5 @@
class A {
declare foo;
declare bar: string;
declare readonly bax: number;
}
@@ -0,0 +1,4 @@
{
"sourceType": "module",
"plugins": ["typescript", "classProperties"]
}
@@ -0,0 +1,5 @@
class A {
declare foo;
declare bar: string;
declare readonly bax: number;
}
Expand Up @@ -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) {
Expand Down Expand Up @@ -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();
Expand Down
@@ -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`,
);
}
}
115 changes: 81 additions & 34 deletions packages/babel-parser/src/plugins/typescript/index.js
Expand Up @@ -28,6 +28,7 @@ import * as charCodes from "charcodes";
type TsModifier =
| "readonly"
| "abstract"
| "declare"
| "static"
| "public"
| "private"
Expand Down Expand Up @@ -129,6 +130,31 @@ export default (superClass: Class<Parser>): Class<Parser> =>
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<T: TsModifier>(
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":
Expand Down Expand Up @@ -405,7 +431,7 @@ export default (superClass: Class<Parser>): Class<Parser> =>
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) &&
Expand Down Expand Up @@ -1844,50 +1870,49 @@ export default (superClass: Class<Parser>): Class<Parser> =>

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,
Expand All @@ -1902,6 +1927,20 @@ export default (superClass: Class<Parser>): Class<Parser> =>
): 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`
Expand Down Expand Up @@ -2048,6 +2087,14 @@ export default (superClass: Class<Parser>): Class<Parser> =>

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);
}

Expand Down
21 changes: 11 additions & 10 deletions packages/babel-parser/src/types.js
Expand Up @@ -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",
Expand Down
@@ -0,0 +1,3 @@
class A {
declare bar: string = "test";
}

0 comments on commit e9c1bce

Please sign in to comment.