Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support TypeScript 4.3 get/set type members #13089

5 changes: 5 additions & 0 deletions packages/babel-generator/src/generators/typescript.ts
Expand Up @@ -127,6 +127,11 @@ export function tsPrintPropertyOrMethodName(this: Printer, node) {
}

export function TSMethodSignature(this: Printer, node: t.TSMethodSignature) {
const { kind } = node;
if (kind === "set" || kind === "get") {
this.word(kind);
this.space();
}
this.tsPrintPropertyOrMethodName(node);
this.tsPrintSignatureDeclarationBase(node);
this.token(";");
Expand Down
@@ -0,0 +1,4 @@
interface Foo {
get foo();
set bar(v);
}
@@ -0,0 +1,4 @@
interface Foo {
get foo();
set bar(v);
}
8 changes: 8 additions & 0 deletions packages/babel-parser/src/parser/statement.js
Expand Up @@ -2384,4 +2384,12 @@ export default class StatementParser extends ExpressionParser {
this.checkLVal(specifier.local, "import specifier", BIND_LEXICAL);
node.specifiers.push(this.finishNode(specifier, "ImportSpecifier"));
}

// This is used in flow and typescript plugin
// Determine whether a parameter is a this param
isThisParam(
param: N.Pattern | N.Identifier | N.TSParameterProperty,
): boolean {
return param.type === "Identifier" && param.name === "this";
}
}
5 changes: 0 additions & 5 deletions packages/babel-parser/src/plugins/flow/index.js
Expand Up @@ -2392,11 +2392,6 @@ export default (superClass: Class<Parser>): Class<Parser> =>
return !this.match(tt.colon) && super.isNonstaticConstructor(method);
}

// determine whether a parameter is a this param
isThisParam(param) {
return param.type === "Identifier" && param.name === "this";
}

// parse type parameters for class methods
pushClassMethod(
classBody: N.ClassBody,
Expand Down
87 changes: 79 additions & 8 deletions packages/babel-parser/src/plugins/typescript/index.js
Expand Up @@ -70,6 +70,9 @@ const TSErrors = makeErrorTemplates(
{
AbstractMethodHasImplementation:
"Method '%0' cannot have an implementation because it is marked abstract.",
AccesorCannotDeclareThisParameter:
"'get' and 'set' accessors cannot declare 'this' parameters.",
AccesorCannotHaveTypeParameters: "An accessor cannot have type parameters.",
ClassMethodHasDeclare: "Class methods cannot have the 'declare' modifier.",
ClassMethodHasReadonly:
"Class methods cannot have the 'readonly' modifier.",
Expand Down Expand Up @@ -121,6 +124,12 @@ const TSErrors = makeErrorTemplates(
"Private elements cannot have an accessibility modifier ('%0').",
ReadonlyForMethodSignature:
"'readonly' modifier can only appear on a property declaration or index signature.",
SetAccesorCannotHaveOptionalParameter:
"A 'set' accessor cannot have an optional parameter.",
SetAccesorCannotHaveRestParameter:
"A 'set' accessor cannot have rest parameter.",
SetAccesorCannotHaveReturnType:
"A 'set' accessor cannot have a return type annotation.",
TypeAnnotationAfterAssign:
"Type annotations must come before default assignments, e.g. instead of `age = 25: number` use `age: number = 25`.",
TypeImportCannotSpecifyDefaultAndNamed:
Expand Down Expand Up @@ -192,12 +201,7 @@ export default (superClass: Class<Parser>): Class<Parser> =>
return this.match(tt.name);
}

tsNextTokenCanFollowModifier() {
// Note: TypeScript's implementation is much more complicated because
// more things are considered modifiers there.
// This implementation only handles modifiers not handled by @babel/parser itself. And "static".
// TODO: Would be nice to avoid lookahead. Want a hasLineBreakUpNext() method...
this.next();
tsTokenCanFollowModifier() {
return (
(this.match(tt.bracketL) ||
this.match(tt.braceL) ||
Expand All @@ -209,6 +213,15 @@ export default (superClass: Class<Parser>): Class<Parser> =>
);
}

tsNextTokenCanFollowModifier() {
// Note: TypeScript's implementation is much more complicated because
// more things are considered modifiers there.
// This implementation only handles modifiers not handled by @babel/parser itself. And "static".
// TODO: Would be nice to avoid lookahead. Want a hasLineBreakUpNext() method...
this.next();
return this.tsTokenCanFollowModifier();
}

/** Parses a modifier matching one the given modifier names. */
tsParseModifier<T: TsModifier>(allowedModifiers: T[]): ?T {
if (!this.match(tt.name)) {
Expand Down Expand Up @@ -546,8 +559,8 @@ export default (superClass: Class<Parser>): Class<Parser> =>
}

tsParseTypeMemberSemicolon(): void {
if (!this.eat(tt.comma)) {
this.semicolon();
if (!this.eat(tt.comma) && !this.isLineTerminator()) {
this.expect(tt.semi);
}
}

Expand Down Expand Up @@ -601,8 +614,57 @@ export default (superClass: Class<Parser>): Class<Parser> =>
this.raise(node.start, TSErrors.ReadonlyForMethodSignature);
}
const method: N.TsMethodSignature = nodeAny;
if (method.kind && this.isRelational("<")) {
this.raise(this.state.pos, TSErrors.AccesorCannotHaveTypeParameters);
}
this.tsFillSignature(tt.colon, method);
this.tsParseTypeMemberSemicolon();
if (method.kind === "get") {
if (method.parameters.length > 0) {
this.raise(this.state.pos, Errors.BadGetterArity);
if (this.isThisParam(method.parameters[0])) {
this.raise(
this.state.pos,
TSErrors.AccesorCannotDeclareThisParameter,
);
}
}
} else if (method.kind === "set") {
if (method.parameters.length !== 1) {
this.raise(this.state.pos, Errors.BadSetterArity);
} else {
const firstParameter = method.parameters[0];
if (this.isThisParam(firstParameter)) {
this.raise(
this.state.pos,
TSErrors.AccesorCannotDeclareThisParameter,
);
}
if (
firstParameter.type === "Identifier" &&
firstParameter.optional
) {
this.raise(
this.state.pos,
TSErrors.SetAccesorCannotHaveOptionalParameter,
);
}
if (firstParameter.type === "RestElement") {
this.raise(
this.state.pos,
TSErrors.SetAccesorCannotHaveRestParameter,
);
}
}
if (method.typeAnnotation) {
this.raise(
method.typeAnnotation.start,
TSErrors.SetAccesorCannotHaveReturnType,
);
}
} else {
method.kind = "method";
}
return this.finishNode(method, "TSMethodSignature");
} else {
const property: N.TsPropertySignature = nodeAny;
Expand Down Expand Up @@ -656,6 +718,15 @@ export default (superClass: Class<Parser>): Class<Parser> =>
}

this.parsePropertyName(node, /* isPrivateNameAllowed */ false);
Copy link
Member

Choose a reason for hiding this comment

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

Instead of using lookahead (which is expensive) in tsParseMethodSignatureKind, can we:

  • Call this.parsePropertyName
  • If not computed, not private, does not have type parameters, and the identifier name is get or set:
    • If tsNextTokenCanFollowModifier, then we are parsing an accessor and we call this.parsePropertyName again
    • Otherwise, it's a method/property named get or set
  • Otherwise, it's a method/property

Copy link
Contributor

Choose a reason for hiding this comment

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

Yeah we parse class accessors using the same strategy mentioned above.

Copy link
Member Author

Choose a reason for hiding this comment

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

What the AST babel should return for below code?

interface Foo {
    set foo;
}

In current behaviour, two properties named set and foo are returned (with recoverable error Missing semicolon). If we should keep it, we should detect if the property is a method signature after first this.parsePropertyName call. But then the token is the name of the property, so I don't know how to do that without lookahead...

Copy link
Member

Choose a reason for hiding this comment

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

It's ok to throw "Expected (":

  • It's what we already do for
    class Foo {
      set foo
    }
  • It's more likely that someone didn't finish writing a setter, than putting two properties on the same line without ; and that the name of the first one was exactly set.

if (
!node.computed &&
node.key.type === "Identifier" &&
(node.key.name === "get" || node.key.name === "set") &&
this.tsTokenCanFollowModifier()
) {
node.kind = node.key.name;
this.parsePropertyName(node, /* isPrivateNameAllowed */ false);
}
return this.tsParsePropertyOrMethodSignature(node, !!node.readonly);
}

Expand Down
1 change: 1 addition & 0 deletions packages/babel-parser/src/types.js
Expand Up @@ -1208,6 +1208,7 @@ export type TsPropertySignature = TsNamedTypeElementBase & {
export type TsMethodSignature = TsSignatureDeclarationBase &
TsNamedTypeElementBase & {
type: "TSMethodSignature",
kind: "method" | "get" | "set",
};

// *Not* a ClassMemberBase: Can't have accessibility, can't be abstract, can't be optional.
Expand Down
Expand Up @@ -43,7 +43,8 @@
"type": "TSVoidKeyword",
"start":34,"end":38,"loc":{"start":{"line":2,"column":12},"end":{"line":2,"column":16}}
}
}
},
"kind": "method"
},
{
"type": "TSMethodSignature",
Expand Down Expand Up @@ -74,7 +75,8 @@
"type": "TSAnyKeyword",
"start":56,"end":59,"loc":{"start":{"line":3,"column":16},"end":{"line":3,"column":19}}
}
}
},
"kind": "method"
},
{
"type": "TSMethodSignature",
Expand Down Expand Up @@ -126,7 +128,8 @@
"type": "TSVoidKeyword",
"start":95,"end":99,"loc":{"start":{"line":4,"column":34},"end":{"line":4,"column":38}}
}
}
},
"kind": "method"
}
]
},
Expand Down
@@ -0,0 +1,6 @@
interface Foo {
get
foo(): string;
set
bar(v);
}
@@ -0,0 +1,86 @@
{
"type": "File",
"start":0,"end":56,"loc":{"start":{"line":1,"column":0},"end":{"line":6,"column":1}},
"program": {
"type": "Program",
"start":0,"end":56,"loc":{"start":{"line":1,"column":0},"end":{"line":6,"column":1}},
"sourceType": "module",
"interpreter": null,
"body": [
{
"type": "TSInterfaceDeclaration",
"start":0,"end":56,"loc":{"start":{"line":1,"column":0},"end":{"line":6,"column":1}},
"id": {
"type": "Identifier",
"start":10,"end":13,"loc":{"start":{"line":1,"column":10},"end":{"line":1,"column":13},"identifierName":"Foo"},
"name": "Foo"
},
"body": {
"type": "TSInterfaceBody",
"start":14,"end":56,"loc":{"start":{"line":1,"column":14},"end":{"line":6,"column":1}},
"body": [
{
"type": "TSPropertySignature",
"start":18,"end":21,"loc":{"start":{"line":2,"column":2},"end":{"line":2,"column":5}},
"key": {
"type": "Identifier",
"start":18,"end":21,"loc":{"start":{"line":2,"column":2},"end":{"line":2,"column":5},"identifierName":"get"},
"name": "get"
},
"computed": false
},
{
"type": "TSMethodSignature",
"start":24,"end":38,"loc":{"start":{"line":3,"column":2},"end":{"line":3,"column":16}},
"key": {
"type": "Identifier",
"start":24,"end":27,"loc":{"start":{"line":3,"column":2},"end":{"line":3,"column":5},"identifierName":"foo"},
"name": "foo"
},
"computed": false,
"parameters": [],
"typeAnnotation": {
"type": "TSTypeAnnotation",
"start":29,"end":37,"loc":{"start":{"line":3,"column":7},"end":{"line":3,"column":15}},
"typeAnnotation": {
"type": "TSStringKeyword",
"start":31,"end":37,"loc":{"start":{"line":3,"column":9},"end":{"line":3,"column":15}}
}
},
"kind": "method"
},
{
"type": "TSPropertySignature",
"start":41,"end":44,"loc":{"start":{"line":4,"column":2},"end":{"line":4,"column":5}},
"key": {
"type": "Identifier",
"start":41,"end":44,"loc":{"start":{"line":4,"column":2},"end":{"line":4,"column":5},"identifierName":"set"},
"name": "set"
},
"computed": false
},
{
"type": "TSMethodSignature",
"start":47,"end":54,"loc":{"start":{"line":5,"column":2},"end":{"line":5,"column":9}},
"key": {
"type": "Identifier",
"start":47,"end":50,"loc":{"start":{"line":5,"column":2},"end":{"line":5,"column":5},"identifierName":"bar"},
"name": "bar"
},
"computed": false,
"parameters": [
{
"type": "Identifier",
"start":51,"end":52,"loc":{"start":{"line":5,"column":6},"end":{"line":5,"column":7},"identifierName":"v"},
"name": "v"
}
],
"kind": "method"
}
]
}
}
],
"directives": []
}
}
@@ -0,0 +1,3 @@
interface Foo {
set bar(foo?: string);
}
@@ -0,0 +1,58 @@
{
"type": "File",
"start":0,"end":42,"loc":{"start":{"line":1,"column":0},"end":{"line":3,"column":1}},
"errors": [
"SyntaxError: A 'set' accessor cannot have an optional parameter. (3:1)"
],
"program": {
"type": "Program",
"start":0,"end":42,"loc":{"start":{"line":1,"column":0},"end":{"line":3,"column":1}},
"sourceType": "module",
"interpreter": null,
"body": [
{
"type": "TSInterfaceDeclaration",
"start":0,"end":42,"loc":{"start":{"line":1,"column":0},"end":{"line":3,"column":1}},
"id": {
"type": "Identifier",
"start":10,"end":13,"loc":{"start":{"line":1,"column":10},"end":{"line":1,"column":13},"identifierName":"Foo"},
"name": "Foo"
},
"body": {
"type": "TSInterfaceBody",
"start":14,"end":42,"loc":{"start":{"line":1,"column":14},"end":{"line":3,"column":1}},
"body": [
{
"type": "TSMethodSignature",
"start":18,"end":40,"loc":{"start":{"line":2,"column":2},"end":{"line":2,"column":24}},
"key": {
"type": "Identifier",
"start":22,"end":25,"loc":{"start":{"line":2,"column":6},"end":{"line":2,"column":9},"identifierName":"bar"},
"name": "bar"
},
"computed": false,
"kind": "set",
"parameters": [
{
"type": "Identifier",
"start":26,"end":38,"loc":{"start":{"line":2,"column":10},"end":{"line":2,"column":22},"identifierName":"foo"},
"name": "foo",
"optional": true,
"typeAnnotation": {
"type": "TSTypeAnnotation",
"start":30,"end":38,"loc":{"start":{"line":2,"column":14},"end":{"line":2,"column":22}},
"typeAnnotation": {
"type": "TSStringKeyword",
"start":32,"end":38,"loc":{"start":{"line":2,"column":16},"end":{"line":2,"column":22}}
}
}
}
]
}
]
}
}
],
"directives": []
}
}
@@ -0,0 +1,4 @@
interface Foo {
get foo(param): string;
set foo();
}