diff --git a/packages/babel-parser/src/parser/util.js b/packages/babel-parser/src/parser/util.js index 5dbf618ee4..d3a83b18c2 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 9447dd0325..98220d47d8 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 0000000000..38a2e02394 --- /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 0000000000..dd5eecede7 --- /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 e94c315097..0000000000 --- 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 0000000000..d1172b3d8d --- /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 0000000000..7d319ac692 --- /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 0000000000..9e1d3aec78 --- /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