From 9dc2aeef449bee17332a07ab9a15478fd2467903 Mon Sep 17 00:00:00 2001 From: Retsam Date: Mon, 8 Oct 2018 13:04:07 -0400 Subject: [PATCH 1/2] feat: validate the positions of rest elements and optional elements in tuple types Adds a validation step to the parser which raises syntax errors if a rest param is not at the end of a tuple, or if a mandatory param follows an optional parameter --- .../babel-parser/src/plugins/typescript.js | 24 +++++++++++++++++++ .../types/tuple-optional-invalid/input.js | 1 + .../types/tuple-optional-invalid/options.json | 7 ++++++ .../types/tuple-rest-invalid/input.js | 1 + .../types/tuple-rest-invalid/options.json | 7 ++++++ 5 files changed, 40 insertions(+) create mode 100644 packages/babel-parser/test/fixtures/typescript/types/tuple-optional-invalid/input.js create mode 100644 packages/babel-parser/test/fixtures/typescript/types/tuple-optional-invalid/options.json create mode 100644 packages/babel-parser/test/fixtures/typescript/types/tuple-rest-invalid/input.js create mode 100644 packages/babel-parser/test/fixtures/typescript/types/tuple-rest-invalid/options.json diff --git a/packages/babel-parser/src/plugins/typescript.js b/packages/babel-parser/src/plugins/typescript.js index bd3d88d9ac1f..ef6e2ce7be14 100644 --- a/packages/babel-parser/src/plugins/typescript.js +++ b/packages/babel-parser/src/plugins/typescript.js @@ -507,6 +507,30 @@ export default (superClass: Class): Class => /* bracket */ true, /* skipFirstToken */ false, ); + + // Validate the elementTypes to ensure: + // No mandatory elements may follow optional elements + // If there's a rest element, it must be at the end of the tuple + let seenOptionalElement = false; + node.elementTypes.forEach((elementNode, i) => { + if ( + elementNode.type === "TSRestType" && + i !== node.elementTypes.length - 1 + ) { + this.raise( + elementNode.start, + "A rest element must be last in a tuple type.", + ); + } else if (elementNode.type === "TSOptionalType") { + seenOptionalElement = true; + } else if (seenOptionalElement) { + this.raise( + elementNode.start, + "A required element cannot follow an optional element.", + ); + } + }); + return this.finishNode(node, "TSTupleType"); } diff --git a/packages/babel-parser/test/fixtures/typescript/types/tuple-optional-invalid/input.js b/packages/babel-parser/test/fixtures/typescript/types/tuple-optional-invalid/input.js new file mode 100644 index 000000000000..6a4e23361c7e --- /dev/null +++ b/packages/babel-parser/test/fixtures/typescript/types/tuple-optional-invalid/input.js @@ -0,0 +1 @@ +let x: [string?, number] diff --git a/packages/babel-parser/test/fixtures/typescript/types/tuple-optional-invalid/options.json b/packages/babel-parser/test/fixtures/typescript/types/tuple-optional-invalid/options.json new file mode 100644 index 000000000000..92616d0020b4 --- /dev/null +++ b/packages/babel-parser/test/fixtures/typescript/types/tuple-optional-invalid/options.json @@ -0,0 +1,7 @@ +{ + "sourceType": "module", + "plugins": [ + "typescript" + ], + "throws": "A required element cannot follow an optional element. (1:17)" +} \ No newline at end of file diff --git a/packages/babel-parser/test/fixtures/typescript/types/tuple-rest-invalid/input.js b/packages/babel-parser/test/fixtures/typescript/types/tuple-rest-invalid/input.js new file mode 100644 index 000000000000..2bfeae2e87eb --- /dev/null +++ b/packages/babel-parser/test/fixtures/typescript/types/tuple-rest-invalid/input.js @@ -0,0 +1 @@ +let x: [...number[], string] diff --git a/packages/babel-parser/test/fixtures/typescript/types/tuple-rest-invalid/options.json b/packages/babel-parser/test/fixtures/typescript/types/tuple-rest-invalid/options.json new file mode 100644 index 000000000000..e2d7f7a6781e --- /dev/null +++ b/packages/babel-parser/test/fixtures/typescript/types/tuple-rest-invalid/options.json @@ -0,0 +1,7 @@ +{ + "sourceType": "module", + "plugins": [ + "typescript" + ], + "throws": "A rest element must be last in a tuple type. (1:8)" +} \ No newline at end of file From 1ea3108cf9ad5cbf55f80d27577e7be248306f2b Mon Sep 17 00:00:00 2001 From: Retsam Date: Tue, 9 Oct 2018 14:06:19 -0400 Subject: [PATCH 2/2] Fix spread after optional case; add test case --- .../babel-parser/src/plugins/typescript.js | 15 +- .../types/tuple-rest-after-optional/input.js | 1 + .../tuple-rest-after-optional/output.json | 242 ++++++++++++++++++ 3 files changed, 250 insertions(+), 8 deletions(-) create mode 100644 packages/babel-parser/test/fixtures/typescript/types/tuple-rest-after-optional/input.js create mode 100644 packages/babel-parser/test/fixtures/typescript/types/tuple-rest-after-optional/output.json diff --git a/packages/babel-parser/src/plugins/typescript.js b/packages/babel-parser/src/plugins/typescript.js index ef6e2ce7be14..480d59272f39 100644 --- a/packages/babel-parser/src/plugins/typescript.js +++ b/packages/babel-parser/src/plugins/typescript.js @@ -513,14 +513,13 @@ export default (superClass: Class): Class => // If there's a rest element, it must be at the end of the tuple let seenOptionalElement = false; node.elementTypes.forEach((elementNode, i) => { - if ( - elementNode.type === "TSRestType" && - i !== node.elementTypes.length - 1 - ) { - this.raise( - elementNode.start, - "A rest element must be last in a tuple type.", - ); + if (elementNode.type === "TSRestType") { + if (i !== node.elementTypes.length - 1) { + this.raise( + elementNode.start, + "A rest element must be last in a tuple type.", + ); + } } else if (elementNode.type === "TSOptionalType") { seenOptionalElement = true; } else if (seenOptionalElement) { diff --git a/packages/babel-parser/test/fixtures/typescript/types/tuple-rest-after-optional/input.js b/packages/babel-parser/test/fixtures/typescript/types/tuple-rest-after-optional/input.js new file mode 100644 index 000000000000..954c72a82aaa --- /dev/null +++ b/packages/babel-parser/test/fixtures/typescript/types/tuple-rest-after-optional/input.js @@ -0,0 +1 @@ +function foo(...args: [number, string?, ...number[]]) {} diff --git a/packages/babel-parser/test/fixtures/typescript/types/tuple-rest-after-optional/output.json b/packages/babel-parser/test/fixtures/typescript/types/tuple-rest-after-optional/output.json new file mode 100644 index 000000000000..1d71afdaae49 --- /dev/null +++ b/packages/babel-parser/test/fixtures/typescript/types/tuple-rest-after-optional/output.json @@ -0,0 +1,242 @@ +{ + "type": "File", + "start": 0, + "end": 56, + "loc": { + "start": { + "line": 1, + "column": 0 + }, + "end": { + "line": 1, + "column": 56 + } + }, + "program": { + "type": "Program", + "start": 0, + "end": 56, + "loc": { + "start": { + "line": 1, + "column": 0 + }, + "end": { + "line": 1, + "column": 56 + } + }, + "sourceType": "module", + "interpreter": null, + "body": [ + { + "type": "FunctionDeclaration", + "start": 0, + "end": 56, + "loc": { + "start": { + "line": 1, + "column": 0 + }, + "end": { + "line": 1, + "column": 56 + } + }, + "id": { + "type": "Identifier", + "start": 9, + "end": 12, + "loc": { + "start": { + "line": 1, + "column": 9 + }, + "end": { + "line": 1, + "column": 12 + }, + "identifierName": "foo" + }, + "name": "foo" + }, + "generator": false, + "async": false, + "params": [ + { + "type": "RestElement", + "start": 13, + "end": 52, + "loc": { + "start": { + "line": 1, + "column": 13 + }, + "end": { + "line": 1, + "column": 52 + } + }, + "argument": { + "type": "Identifier", + "start": 16, + "end": 20, + "loc": { + "start": { + "line": 1, + "column": 16 + }, + "end": { + "line": 1, + "column": 20 + }, + "identifierName": "args" + }, + "name": "args" + }, + "typeAnnotation": { + "type": "TSTypeAnnotation", + "start": 20, + "end": 52, + "loc": { + "start": { + "line": 1, + "column": 20 + }, + "end": { + "line": 1, + "column": 52 + } + }, + "typeAnnotation": { + "type": "TSTupleType", + "start": 22, + "end": 52, + "loc": { + "start": { + "line": 1, + "column": 22 + }, + "end": { + "line": 1, + "column": 52 + } + }, + "elementTypes": [ + { + "type": "TSNumberKeyword", + "start": 23, + "end": 29, + "loc": { + "start": { + "line": 1, + "column": 23 + }, + "end": { + "line": 1, + "column": 29 + } + } + }, + { + "type": "TSOptionalType", + "start": 31, + "end": 38, + "loc": { + "start": { + "line": 1, + "column": 31 + }, + "end": { + "line": 1, + "column": 38 + } + }, + "typeAnnotation": { + "type": "TSStringKeyword", + "start": 31, + "end": 37, + "loc": { + "start": { + "line": 1, + "column": 31 + }, + "end": { + "line": 1, + "column": 37 + } + } + } + }, + { + "type": "TSRestType", + "start": 40, + "end": 51, + "loc": { + "start": { + "line": 1, + "column": 40 + }, + "end": { + "line": 1, + "column": 51 + } + }, + "typeAnnotation": { + "type": "TSArrayType", + "start": 43, + "end": 51, + "loc": { + "start": { + "line": 1, + "column": 43 + }, + "end": { + "line": 1, + "column": 51 + } + }, + "elementType": { + "type": "TSNumberKeyword", + "start": 43, + "end": 49, + "loc": { + "start": { + "line": 1, + "column": 43 + }, + "end": { + "line": 1, + "column": 49 + } + } + } + } + } + ] + } + } + } + ], + "body": { + "type": "BlockStatement", + "start": 54, + "end": 56, + "loc": { + "start": { + "line": 1, + "column": 54 + }, + "end": { + "line": 1, + "column": 56 + } + }, + "body": [], + "directives": [] + } + } + ], + "directives": [] + } +} \ No newline at end of file