From c1b6e4658f10f7bb3743c263380ad563efd47a8e Mon Sep 17 00:00:00 2001 From: sosukesuzuki Date: Sat, 6 Feb 2021 22:19:33 +0900 Subject: [PATCH 1/4] Support parsing abstract interface --- .../src/plugins/typescript/index.js | 50 +++++++++++++------ .../typescript/interface/abstract/input.ts | 3 ++ .../typescript/interface/abstract/output.json | 50 +++++++++++++++++++ .../export-abstract-interface/options.json | 3 -- .../export-abstract-interface/output.json | 38 ++++++++++++++ 5 files changed, 127 insertions(+), 17 deletions(-) create mode 100644 packages/babel-parser/test/fixtures/typescript/interface/abstract/input.ts create mode 100644 packages/babel-parser/test/fixtures/typescript/interface/abstract/output.json delete mode 100644 packages/babel-parser/test/fixtures/typescript/interface/export-abstract-interface/options.json create mode 100644 packages/babel-parser/test/fixtures/typescript/interface/export-abstract-interface/output.json diff --git a/packages/babel-parser/src/plugins/typescript/index.js b/packages/babel-parser/src/plugins/typescript/index.js index 9447dd03256e..48d19ad3fbb0 100644 --- a/packages/babel-parser/src/plugins/typescript/index.js +++ b/packages/babel-parser/src/plugins/typescript/index.js @@ -89,6 +89,8 @@ const TSErrors = Object.freeze({ "Tuple members must all have names or all not have names.", NonAbstractClassHasAbstractMethod: "Abstract methods can only appear within an abstract class.", + NonClassMethodPropertyHasAbstractModifer: + "'abstract' modifier can only appear on a class, method, or property declaration.", OptionalTypeBeforeRequired: "A required element cannot follow an optional element.", PatternIsOptional: @@ -1585,20 +1587,13 @@ export default (superClass: Class): Class => ): ?N.Declaration { switch (value) { case "abstract": - if (this.tsCheckLineTerminatorAndMatch(tt._class, next)) { - const cls: N.ClassDeclaration = node; - cls.abstract = true; - if (next) { - this.next(); - if (!this.match(tt._class)) { - this.unexpected(null, tt._class); - } - } - return this.parseClass( - cls, - /* isStatement */ true, - /* optionalId */ false, - ); + if ( + this.tsCheckLineTerminatorAndMatch(tt._class, next) || + // for interface + this.tsCheckLineTerminatorAndMatch(tt.name, next) + ) { + if (next) this.next(); + return this.tsParseAbstractDeclaration(node); } break; @@ -2849,4 +2844,31 @@ export default (superClass: Class): Class => this.state.inAbstractClass = oldInAbstractClass; } } + + tsParseAbstractDeclaration( + node: any, + ): N.ClassDeclaration | N.TsInterfaceDeclaration { + const isClass = this.match(tt._class); + if (!isClass && !this.isContextual("interface")) { + this.unexpected(null, tt._class); + } + node.abstract = true; + if (isClass) { + return this.parseClass( + (node: N.ClassDeclaration), + /* isStatement */ true, + /* optionalId */ false, + ); + } else { + // for invalid abstract interface + this.raise( + node.start, + TSErrors.NonClassMethodPropertyHasAbstractModifer, + ); + this.next(); + return this.tsParseInterfaceDeclaration( + (node: N.TsInterfaceDeclaration), + ); + } + } }; diff --git a/packages/babel-parser/test/fixtures/typescript/interface/abstract/input.ts b/packages/babel-parser/test/fixtures/typescript/interface/abstract/input.ts new file mode 100644 index 000000000000..38a2e02394ad --- /dev/null +++ b/packages/babel-parser/test/fixtures/typescript/interface/abstract/input.ts @@ -0,0 +1,3 @@ +abstract interface Foo { + foo: string; +} diff --git a/packages/babel-parser/test/fixtures/typescript/interface/abstract/output.json b/packages/babel-parser/test/fixtures/typescript/interface/abstract/output.json new file mode 100644 index 000000000000..dd5eecede71b --- /dev/null +++ b/packages/babel-parser/test/fixtures/typescript/interface/abstract/output.json @@ -0,0 +1,50 @@ +{ + "type": "File", + "start":0,"end":41,"loc":{"start":{"line":1,"column":0},"end":{"line":3,"column":1}}, + "errors": [ + "SyntaxError: 'abstract' modifier can only appear on a class, method, or property declaration. (1:0)" + ], + "program": { + "type": "Program", + "start":0,"end":41,"loc":{"start":{"line":1,"column":0},"end":{"line":3,"column":1}}, + "sourceType": "module", + "interpreter": null, + "body": [ + { + "type": "TSInterfaceDeclaration", + "start":0,"end":41,"loc":{"start":{"line":1,"column":0},"end":{"line":3,"column":1}}, + "abstract": true, + "id": { + "type": "Identifier", + "start":19,"end":22,"loc":{"start":{"line":1,"column":19},"end":{"line":1,"column":22},"identifierName":"Foo"}, + "name": "Foo" + }, + "body": { + "type": "TSInterfaceBody", + "start":23,"end":41,"loc":{"start":{"line":1,"column":23},"end":{"line":3,"column":1}}, + "body": [ + { + "type": "TSPropertySignature", + "start":27,"end":39,"loc":{"start":{"line":2,"column":2},"end":{"line":2,"column":14}}, + "key": { + "type": "Identifier", + "start":27,"end":30,"loc":{"start":{"line":2,"column":2},"end":{"line":2,"column":5},"identifierName":"foo"}, + "name": "foo" + }, + "computed": false, + "typeAnnotation": { + "type": "TSTypeAnnotation", + "start":30,"end":38,"loc":{"start":{"line":2,"column":5},"end":{"line":2,"column":13}}, + "typeAnnotation": { + "type": "TSStringKeyword", + "start":32,"end":38,"loc":{"start":{"line":2,"column":7},"end":{"line":2,"column":13}} + } + } + } + ] + } + } + ], + "directives": [] + } +} \ No newline at end of file diff --git a/packages/babel-parser/test/fixtures/typescript/interface/export-abstract-interface/options.json b/packages/babel-parser/test/fixtures/typescript/interface/export-abstract-interface/options.json deleted file mode 100644 index e94c31509714..000000000000 --- a/packages/babel-parser/test/fixtures/typescript/interface/export-abstract-interface/options.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "throws": "Unexpected token, expected \"class\" (1:16)" -} diff --git a/packages/babel-parser/test/fixtures/typescript/interface/export-abstract-interface/output.json b/packages/babel-parser/test/fixtures/typescript/interface/export-abstract-interface/output.json new file mode 100644 index 000000000000..d1172b3d8d5e --- /dev/null +++ b/packages/babel-parser/test/fixtures/typescript/interface/export-abstract-interface/output.json @@ -0,0 +1,38 @@ +{ + "type": "File", + "start":0,"end":32,"loc":{"start":{"line":1,"column":0},"end":{"line":3,"column":1}}, + "errors": [ + "SyntaxError: 'abstract' modifier can only appear on a class, method, or property declaration. (1:7)" + ], + "program": { + "type": "Program", + "start":0,"end":32,"loc":{"start":{"line":1,"column":0},"end":{"line":3,"column":1}}, + "sourceType": "module", + "interpreter": null, + "body": [ + { + "type": "ExportNamedDeclaration", + "start":0,"end":32,"loc":{"start":{"line":1,"column":0},"end":{"line":3,"column":1}}, + "exportKind": "type", + "specifiers": [], + "source": null, + "declaration": { + "type": "TSInterfaceDeclaration", + "start":7,"end":32,"loc":{"start":{"line":1,"column":7},"end":{"line":3,"column":1}}, + "abstract": true, + "id": { + "type": "Identifier", + "start":26,"end":27,"loc":{"start":{"line":1,"column":26},"end":{"line":1,"column":27},"identifierName":"I"}, + "name": "I" + }, + "body": { + "type": "TSInterfaceBody", + "start":28,"end":32,"loc":{"start":{"line":1,"column":28},"end":{"line":3,"column":1}}, + "body": [] + } + } + } + ], + "directives": [] + } +} \ No newline at end of file From 7d1e3d85e1807c521f169ad572471831b43cb524 Mon Sep 17 00:00:00 2001 From: sosukesuzuki Date: Tue, 9 Feb 2021 04:42:56 +0900 Subject: [PATCH 2/4] Address review Address reviews Address reviews --- packages/babel-parser/src/parser/util.js | 6 +-- .../src/plugins/typescript/index.js | 35 +++++++------ .../invalid-abstract-interface/input.ts | 2 + .../invalid-abstract-interface/output.json | 50 +++++++++++++++++++ 4 files changed, 74 insertions(+), 19 deletions(-) create mode 100644 packages/babel-parser/test/fixtures/typescript/interface/invalid-abstract-interface/input.ts create mode 100644 packages/babel-parser/test/fixtures/typescript/interface/invalid-abstract-interface/output.json diff --git a/packages/babel-parser/src/parser/util.js b/packages/babel-parser/src/parser/util.js index 5dbf618ee4d5..c60ad571bde2 100644 --- a/packages/babel-parser/src/parser/util.js +++ b/packages/babel-parser/src/parser/util.js @@ -90,10 +90,8 @@ export default class UtilParser extends Tokenizer { ); } - hasPrecedingLineBreak(): boolean { - return lineBreak.test( - this.input.slice(this.state.lastTokEnd, this.state.start), - ); + hasPrecedingLineBreak(state: State = this.state): boolean { + return lineBreak.test(this.input.slice(state.lastTokEnd, state.start)); } // TODO diff --git a/packages/babel-parser/src/plugins/typescript/index.js b/packages/babel-parser/src/plugins/typescript/index.js index 48d19ad3fbb0..1f2c3f2152d4 100644 --- a/packages/babel-parser/src/plugins/typescript/index.js +++ b/packages/babel-parser/src/plugins/typescript/index.js @@ -2848,27 +2848,32 @@ export default (superClass: Class): Class => tsParseAbstractDeclaration( node: any, ): N.ClassDeclaration | N.TsInterfaceDeclaration { - const isClass = this.match(tt._class); - if (!isClass && !this.isContextual("interface")) { - this.unexpected(null, tt._class); - } - node.abstract = true; - if (isClass) { + if (this.match(tt._class)) { + node.abstract = true; return this.parseClass( (node: N.ClassDeclaration), /* isStatement */ true, /* optionalId */ false, ); - } else { + } else if (this.isContextual("interface")) { // for invalid abstract interface - this.raise( - node.start, - TSErrors.NonClassMethodPropertyHasAbstractModifer, - ); - this.next(); - return this.tsParseInterfaceDeclaration( - (node: N.TsInterfaceDeclaration), - ); + + // To avoid + // abstract interface + // Foo {} + if (!this.hasPrecedingLineBreak(this.lookahead())) { + node.abstract = true; + this.raise( + node.start, + TSErrors.NonClassMethodPropertyHasAbstractModifer, + ); + this.next(); + return this.tsParseInterfaceDeclaration( + (node: N.TsInterfaceDeclaration), + ); + } + } else { + this.unexpected(null, tt._class); } } }; diff --git a/packages/babel-parser/test/fixtures/typescript/interface/invalid-abstract-interface/input.ts b/packages/babel-parser/test/fixtures/typescript/interface/invalid-abstract-interface/input.ts new file mode 100644 index 000000000000..7d319ac6926f --- /dev/null +++ b/packages/babel-parser/test/fixtures/typescript/interface/invalid-abstract-interface/input.ts @@ -0,0 +1,2 @@ +abstract interface +Foo {} diff --git a/packages/babel-parser/test/fixtures/typescript/interface/invalid-abstract-interface/output.json b/packages/babel-parser/test/fixtures/typescript/interface/invalid-abstract-interface/output.json new file mode 100644 index 000000000000..9e1d3aec785f --- /dev/null +++ b/packages/babel-parser/test/fixtures/typescript/interface/invalid-abstract-interface/output.json @@ -0,0 +1,50 @@ +{ + "type": "File", + "start":0,"end":25,"loc":{"start":{"line":1,"column":0},"end":{"line":2,"column":6}}, + "errors": [ + "SyntaxError: Missing semicolon (1:8)", + "SyntaxError: Missing semicolon (2:3)" + ], + "program": { + "type": "Program", + "start":0,"end":25,"loc":{"start":{"line":1,"column":0},"end":{"line":2,"column":6}}, + "sourceType": "module", + "interpreter": null, + "body": [ + { + "type": "ExpressionStatement", + "start":0,"end":8,"loc":{"start":{"line":1,"column":0},"end":{"line":1,"column":8}}, + "expression": { + "type": "Identifier", + "start":0,"end":8,"loc":{"start":{"line":1,"column":0},"end":{"line":1,"column":8},"identifierName":"abstract"}, + "name": "abstract" + } + }, + { + "type": "ExpressionStatement", + "start":9,"end":18,"loc":{"start":{"line":1,"column":9},"end":{"line":1,"column":18}}, + "expression": { + "type": "Identifier", + "start":9,"end":18,"loc":{"start":{"line":1,"column":9},"end":{"line":1,"column":18},"identifierName":"interface"}, + "name": "interface" + } + }, + { + "type": "ExpressionStatement", + "start":19,"end":22,"loc":{"start":{"line":2,"column":0},"end":{"line":2,"column":3}}, + "expression": { + "type": "Identifier", + "start":19,"end":22,"loc":{"start":{"line":2,"column":0},"end":{"line":2,"column":3},"identifierName":"Foo"}, + "name": "Foo" + } + }, + { + "type": "BlockStatement", + "start":23,"end":25,"loc":{"start":{"line":2,"column":4},"end":{"line":2,"column":6}}, + "body": [], + "directives": [] + } + ], + "directives": [] + } +} \ No newline at end of file From c6e4386be081db1efbc05ec062c3319b63e9bf7e Mon Sep 17 00:00:00 2001 From: sosukesuzuki Date: Tue, 9 Feb 2021 06:30:13 +0900 Subject: [PATCH 3/4] Fix types --- packages/babel-parser/src/plugins/typescript/index.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/babel-parser/src/plugins/typescript/index.js b/packages/babel-parser/src/plugins/typescript/index.js index 1f2c3f2152d4..172026c760c5 100644 --- a/packages/babel-parser/src/plugins/typescript/index.js +++ b/packages/babel-parser/src/plugins/typescript/index.js @@ -2847,10 +2847,10 @@ export default (superClass: Class): Class => tsParseAbstractDeclaration( node: any, - ): N.ClassDeclaration | N.TsInterfaceDeclaration { + ): N.ClassDeclaration | N.TsInterfaceDeclaration | typeof undefined { if (this.match(tt._class)) { node.abstract = true; - return this.parseClass( + return this.parseClass( (node: N.ClassDeclaration), /* isStatement */ true, /* optionalId */ false, From da8ce696449b8dd9803f46e49d5d867bb65d26e6 Mon Sep 17 00:00:00 2001 From: sosukesuzuki Date: Tue, 9 Feb 2021 07:34:29 +0900 Subject: [PATCH 4/4] Add hasFollowingLineBreak --- packages/babel-parser/src/parser/util.js | 12 ++++++++++-- .../babel-parser/src/plugins/typescript/index.js | 2 +- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/packages/babel-parser/src/parser/util.js b/packages/babel-parser/src/parser/util.js index c60ad571bde2..d3a83b18c2ec 100644 --- a/packages/babel-parser/src/parser/util.js +++ b/packages/babel-parser/src/parser/util.js @@ -90,8 +90,16 @@ export default class UtilParser extends Tokenizer { ); } - hasPrecedingLineBreak(state: State = this.state): boolean { - return lineBreak.test(this.input.slice(state.lastTokEnd, state.start)); + hasPrecedingLineBreak(): boolean { + return lineBreak.test( + this.input.slice(this.state.lastTokEnd, this.state.start), + ); + } + + hasFollowingLineBreak(): boolean { + return lineBreak.test( + this.input.slice(this.state.end, this.nextTokenStart()), + ); } // TODO diff --git a/packages/babel-parser/src/plugins/typescript/index.js b/packages/babel-parser/src/plugins/typescript/index.js index 172026c760c5..98220d47d85d 100644 --- a/packages/babel-parser/src/plugins/typescript/index.js +++ b/packages/babel-parser/src/plugins/typescript/index.js @@ -2861,7 +2861,7 @@ export default (superClass: Class): Class => // To avoid // abstract interface // Foo {} - if (!this.hasPrecedingLineBreak(this.lookahead())) { + if (!this.hasFollowingLineBreak()) { node.abstract = true; this.raise( node.start,