From 650e33376a564135b9cc0b2460d37163ee5cb672 Mon Sep 17 00:00:00 2001 From: Kai Cataldo Date: Thu, 22 Sep 2016 10:20:47 -0400 Subject: [PATCH] Disallow duplicate named exports (#107) fixes #69 --- src/parser/statement.js | 44 +++++++++++++++++-- src/tokenizer/state.js | 6 +++ .../actual.js | 2 + .../options.json | 3 ++ .../duplicate-export-default/actual.js | 2 + .../duplicate-export-default/options.json | 3 ++ .../actual.js | 2 + .../options.json | 4 ++ .../actual.js | 2 + .../options.json | 3 ++ .../actual.js | 2 + .../options.json | 3 ++ .../modules/duplicate-named-export/actual.js | 2 + .../duplicate-named-export/options.json | 3 ++ .../flow/type-exports/interface/actual.js | 2 +- .../flow/type-exports/interface/expected.json | 4 +- 16 files changed, 81 insertions(+), 6 deletions(-) create mode 100644 test/fixtures/es2015/modules/duplicate-export-default-and-export-as-default/actual.js create mode 100644 test/fixtures/es2015/modules/duplicate-export-default-and-export-as-default/options.json create mode 100644 test/fixtures/es2015/modules/duplicate-export-default/actual.js create mode 100644 test/fixtures/es2015/modules/duplicate-export-default/options.json create mode 100644 test/fixtures/es2015/modules/duplicate-named-export-class-declaration/actual.js create mode 100644 test/fixtures/es2015/modules/duplicate-named-export-class-declaration/options.json create mode 100644 test/fixtures/es2015/modules/duplicate-named-export-function-declaration/actual.js create mode 100644 test/fixtures/es2015/modules/duplicate-named-export-function-declaration/options.json create mode 100644 test/fixtures/es2015/modules/duplicate-named-export-variable-declaration/actual.js create mode 100644 test/fixtures/es2015/modules/duplicate-named-export-variable-declaration/options.json create mode 100644 test/fixtures/es2015/modules/duplicate-named-export/actual.js create mode 100644 test/fixtures/es2015/modules/duplicate-named-export/options.json diff --git a/src/parser/statement.js b/src/parser/statement.js index d5ff1cc175..2412180403 100644 --- a/src/parser/statement.js +++ b/src/parser/statement.js @@ -844,7 +844,7 @@ pp.parseExport = function (node) { } node.declaration = expr; if (needsSemi) this.semicolon(); - this.checkExport(node); + this.checkExport(node, true, true); return this.finishNode(node, "ExportDefaultDeclaration"); } else if (this.state.type.keyword || this.shouldParseExportDeclaration()) { node.specifiers = []; @@ -855,7 +855,7 @@ pp.parseExport = function (node) { node.specifiers = this.parseExportSpecifiers(); this.parseExportFrom(node); } - this.checkExport(node); + this.checkExport(node, true); return this.finishNode(node, "ExportNamedDeclaration"); }; @@ -903,7 +903,31 @@ pp.shouldParseExportDeclaration = function () { return this.isContextual("async"); }; -pp.checkExport = function (node) { +pp.checkExport = function (node, checkNames, isDefault) { + if (checkNames) { + // Check for duplicate exports + if (isDefault) { + // Default exports + this.checkDuplicateExports(node, "default", isDefault); + } else if (node.specifiers && node.specifiers.length) { + // Named exports + for (let specifier of node.specifiers) { + const name = specifier.exported.name; + if (name === "default") isDefault = true; + this.checkDuplicateExports(specifier, name, isDefault); + } + } else if (node.declaration) { + // Exported declarations + if (node.declaration.type === "FunctionDeclaration" || node.declaration.type === "ClassDeclaration") { + this.checkDuplicateExports(node, node.declaration.id.name, isDefault); + } else if (node.declaration.type === "VariableDeclaration") { + for (let declaration of node.declaration.declarations) { + this.checkDuplicateExports(declaration, declaration.id.name, isDefault); + } + } + } + } + if (this.state.decorators.length) { let isClass = node.declaration && (node.declaration.type === "ClassDeclaration" || node.declaration.type === "ClassExpression"); if (!node.declaration || !isClass) { @@ -913,6 +937,20 @@ pp.checkExport = function (node) { } }; +pp.checkDuplicateExports = function(node, name, isDefault) { + if (this.state.exportedIdentifiers[name]) { + this.raiseDuplicateExportError(node, name, isDefault); + } + this.state.exportedIdentifiers[name] = true; +}; + +pp.raiseDuplicateExportError = function(node, name, isDefault) { + this.raise(node.start, isDefault ? + "Only one default export allowed per module." : + `\`${name}\` has already been exported. Exported identifiers must be unique.` + ); +}; + // Parses a comma-separated list of module exports. pp.parseExportSpecifiers = function () { diff --git a/src/tokenizer/state.js b/src/tokenizer/state.js index 942c10413e..77c2a7407e 100644 --- a/src/tokenizer/state.js +++ b/src/tokenizer/state.js @@ -43,6 +43,8 @@ export default class State { this.containsEsc = this.containsOctal = false; this.octalPosition = null; + this.exportedIdentifiers = {}; + return this; } @@ -119,6 +121,10 @@ export default class State { containsOctal: boolean; octalPosition: ?number; + // Names of exports store. `default` is stored as a name for both + // `export default foo;` and `export { foo as default };`. + exportedIdentifiers: {[id:string]: boolean}; + curPosition() { return new Position(this.curLine, this.pos - this.lineStart); } diff --git a/test/fixtures/es2015/modules/duplicate-export-default-and-export-as-default/actual.js b/test/fixtures/es2015/modules/duplicate-export-default-and-export-as-default/actual.js new file mode 100644 index 0000000000..05d104a1bc --- /dev/null +++ b/test/fixtures/es2015/modules/duplicate-export-default-and-export-as-default/actual.js @@ -0,0 +1,2 @@ +export default function() {}; +export { foo as default }; diff --git a/test/fixtures/es2015/modules/duplicate-export-default-and-export-as-default/options.json b/test/fixtures/es2015/modules/duplicate-export-default-and-export-as-default/options.json new file mode 100644 index 0000000000..275e6a57f9 --- /dev/null +++ b/test/fixtures/es2015/modules/duplicate-export-default-and-export-as-default/options.json @@ -0,0 +1,3 @@ +{ + "throws": "Only one default export allowed per module. (2:9)" +} diff --git a/test/fixtures/es2015/modules/duplicate-export-default/actual.js b/test/fixtures/es2015/modules/duplicate-export-default/actual.js new file mode 100644 index 0000000000..9b09f3c850 --- /dev/null +++ b/test/fixtures/es2015/modules/duplicate-export-default/actual.js @@ -0,0 +1,2 @@ +export default {}; +export default function() {}; diff --git a/test/fixtures/es2015/modules/duplicate-export-default/options.json b/test/fixtures/es2015/modules/duplicate-export-default/options.json new file mode 100644 index 0000000000..1ec6a5168f --- /dev/null +++ b/test/fixtures/es2015/modules/duplicate-export-default/options.json @@ -0,0 +1,3 @@ +{ + "throws": "Only one default export allowed per module. (2:0)" +} diff --git a/test/fixtures/es2015/modules/duplicate-named-export-class-declaration/actual.js b/test/fixtures/es2015/modules/duplicate-named-export-class-declaration/actual.js new file mode 100644 index 0000000000..c6dbde6fd9 --- /dev/null +++ b/test/fixtures/es2015/modules/duplicate-named-export-class-declaration/actual.js @@ -0,0 +1,2 @@ +export { Foo }; +export class Foo {}; diff --git a/test/fixtures/es2015/modules/duplicate-named-export-class-declaration/options.json b/test/fixtures/es2015/modules/duplicate-named-export-class-declaration/options.json new file mode 100644 index 0000000000..c58e1ea64f --- /dev/null +++ b/test/fixtures/es2015/modules/duplicate-named-export-class-declaration/options.json @@ -0,0 +1,4 @@ +{ + "throws": "`Foo` has already been exported. Exported identifiers must be unique. (2:0)" +} + diff --git a/test/fixtures/es2015/modules/duplicate-named-export-function-declaration/actual.js b/test/fixtures/es2015/modules/duplicate-named-export-function-declaration/actual.js new file mode 100644 index 0000000000..4b52a67515 --- /dev/null +++ b/test/fixtures/es2015/modules/duplicate-named-export-function-declaration/actual.js @@ -0,0 +1,2 @@ +export { foo }; +export function foo() {}; diff --git a/test/fixtures/es2015/modules/duplicate-named-export-function-declaration/options.json b/test/fixtures/es2015/modules/duplicate-named-export-function-declaration/options.json new file mode 100644 index 0000000000..1923e47523 --- /dev/null +++ b/test/fixtures/es2015/modules/duplicate-named-export-function-declaration/options.json @@ -0,0 +1,3 @@ +{ + "throws": "`foo` has already been exported. Exported identifiers must be unique. (2:0)" +} diff --git a/test/fixtures/es2015/modules/duplicate-named-export-variable-declaration/actual.js b/test/fixtures/es2015/modules/duplicate-named-export-variable-declaration/actual.js new file mode 100644 index 0000000000..673097f72c --- /dev/null +++ b/test/fixtures/es2015/modules/duplicate-named-export-variable-declaration/actual.js @@ -0,0 +1,2 @@ +export { foo }; +export const foo = bar; diff --git a/test/fixtures/es2015/modules/duplicate-named-export-variable-declaration/options.json b/test/fixtures/es2015/modules/duplicate-named-export-variable-declaration/options.json new file mode 100644 index 0000000000..9fdf7082da --- /dev/null +++ b/test/fixtures/es2015/modules/duplicate-named-export-variable-declaration/options.json @@ -0,0 +1,3 @@ +{ + "throws": "`foo` has already been exported. Exported identifiers must be unique. (2:13)" +} diff --git a/test/fixtures/es2015/modules/duplicate-named-export/actual.js b/test/fixtures/es2015/modules/duplicate-named-export/actual.js new file mode 100644 index 0000000000..9731059958 --- /dev/null +++ b/test/fixtures/es2015/modules/duplicate-named-export/actual.js @@ -0,0 +1,2 @@ +export { foo }; +export { bar as foo }; diff --git a/test/fixtures/es2015/modules/duplicate-named-export/options.json b/test/fixtures/es2015/modules/duplicate-named-export/options.json new file mode 100644 index 0000000000..9bb46f4778 --- /dev/null +++ b/test/fixtures/es2015/modules/duplicate-named-export/options.json @@ -0,0 +1,3 @@ +{ + "throws": "`foo` has already been exported. Exported identifiers must be unique. (2:9)" +} diff --git a/test/fixtures/flow/type-exports/interface/actual.js b/test/fixtures/flow/type-exports/interface/actual.js index b3512f17ab..f5a4bc1441 100644 --- a/test/fixtures/flow/type-exports/interface/actual.js +++ b/test/fixtures/flow/type-exports/interface/actual.js @@ -1,2 +1,2 @@ export interface foo { p: number }; -export interface foo { p: T }; +export interface bar { p: T }; diff --git a/test/fixtures/flow/type-exports/interface/expected.json b/test/fixtures/flow/type-exports/interface/expected.json index 6a3db14e47..49aeb479a6 100644 --- a/test/fixtures/flow/type-exports/interface/expected.json +++ b/test/fixtures/flow/type-exports/interface/expected.json @@ -206,7 +206,7 @@ "column": 20 } }, - "name": "foo" + "name": "bar" }, "typeParameters": { "type": "TypeParameterDeclaration", @@ -346,4 +346,4 @@ ], "directives": [] } -} \ No newline at end of file +}