From d242ea04c8feb2b0012c707d9cfff408ccf1184d Mon Sep 17 00:00:00 2001 From: Sosuke Suzuki Date: Tue, 9 Feb 2021 17:56:18 +0900 Subject: [PATCH] babel-parser(ts): Raise recoverable error for abstract interface (#12771) * Support parsing abstract interface * Address review Address reviews Address reviews * Fix types * Add hasFollowingLineBreak --- packages/babel-parser/src/parser/util.js | 6 ++ .../src/plugins/typescript/index.js | 55 ++++++++++++++----- .../typescript/interface/abstract/input.ts | 3 + .../typescript/interface/abstract/output.json | 50 +++++++++++++++++ .../export-abstract-interface/options.json | 3 - .../export-abstract-interface/output.json | 38 +++++++++++++ .../invalid-abstract-interface/input.ts | 2 + .../invalid-abstract-interface/output.json | 50 +++++++++++++++++ 8 files changed, 190 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 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..d3a83b18c2ec 100644 --- a/packages/babel-parser/src/parser/util.js +++ b/packages/babel-parser/src/parser/util.js @@ -96,6 +96,12 @@ export default class UtilParser extends Tokenizer { ); } + hasFollowingLineBreak(): boolean { + return lineBreak.test( + this.input.slice(this.state.end, this.nextTokenStart()), + ); + } + // TODO isLineTerminator(): boolean { diff --git a/packages/babel-parser/src/plugins/typescript/index.js b/packages/babel-parser/src/plugins/typescript/index.js index 9447dd03256e..98220d47d85d 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,36 @@ export default (superClass: Class): Class => this.state.inAbstractClass = oldInAbstractClass; } } + + tsParseAbstractDeclaration( + node: any, + ): N.ClassDeclaration | N.TsInterfaceDeclaration | typeof undefined { + if (this.match(tt._class)) { + node.abstract = true; + return this.parseClass( + (node: N.ClassDeclaration), + /* isStatement */ true, + /* optionalId */ false, + ); + } else if (this.isContextual("interface")) { + // for invalid abstract interface + + // To avoid + // abstract interface + // Foo {} + if (!this.hasFollowingLineBreak()) { + 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/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 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