From 2194842d11466cf1a3dcdd165e8f1e6505e5d05f Mon Sep 17 00:00:00 2001 From: Retsam Date: Tue, 6 Nov 2018 02:19:34 -0500 Subject: [PATCH] Typescript: Validate tuple type element positions (#8828) * 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 * Fix spread after optional case; add test case --- .../babel-parser/src/plugins/typescript.js | 23 ++ .../types/tuple-optional-invalid/input.js | 1 + .../types/tuple-optional-invalid/options.json | 7 + .../types/tuple-rest-after-optional/input.js | 1 + .../tuple-rest-after-optional/output.json | 242 ++++++++++++++++++ .../types/tuple-rest-invalid/input.js | 1 + .../types/tuple-rest-invalid/options.json | 7 + 7 files changed, 282 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-after-optional/input.js create mode 100644 packages/babel-parser/test/fixtures/typescript/types/tuple-rest-after-optional/output.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 bd63e93407c2..63fd484a8988 100644 --- a/packages/babel-parser/src/plugins/typescript.js +++ b/packages/babel-parser/src/plugins/typescript.js @@ -507,6 +507,29 @@ 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") { + 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) { + 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-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 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