diff --git a/acorn-loose/src/statement.js b/acorn-loose/src/statement.js index a55e843a..3e08de20 100644 --- a/acorn-loose/src/statement.js +++ b/acorn-loose/src/statement.js @@ -10,7 +10,7 @@ lp.parseTopLevel = function() { while (this.tok.type !== tt.eof) node.body.push(this.parseStatement()) this.toks.adaptDirectivePrologue(node.body) this.last = this.tok - node.sourceType = this.options.sourceType + node.sourceType = this.options.sourceType === "commonjs" ? "script" : this.options.sourceType return this.finishNode(node, "Program") } diff --git a/acorn/README.md b/acorn/README.md index f7ff9662..13f00627 100644 --- a/acorn/README.md +++ b/acorn/README.md @@ -61,11 +61,12 @@ required): implemented through plugins. - **sourceType**: Indicate the mode the code should be parsed in. Can be - either `"script"` or `"module"`. This influences global strict mode + either `"script"`, `"module"` or `"commonjs"`. This influences global strict mode and parsing of `import` and `export` declarations. **NOTE**: If set to `"module"`, then static `import` / `export` syntax - will be valid, even if `ecmaVersion` is less than 6. + will be valid, even if `ecmaVersion` is less than 6. If set to `"commonjs"`, + it is the same as `"script"` except that the top-level scope behaves like a function. - **onInsertedSemicolon**: If given a callback, that callback will be called whenever a missing semicolon is inserted by the parser. The @@ -97,7 +98,7 @@ required): for `ecmaVersion` 2022 and later, `false` for lower versions. Setting this option to `true` allows to have top-level `await` expressions. They are still not allowed in non-`async` functions, - though. + though. Setting this option to `true` is not allowed when `sourceType: "commonjs"`. - **allowSuperOutsideMethod**: By default, `super` outside a method raises an error. Set this to `true` to accept such code. diff --git a/acorn/src/acorn.d.ts b/acorn/src/acorn.d.ts index f2ec5243..afbd9139 100644 --- a/acorn/src/acorn.d.ts +++ b/acorn/src/acorn.d.ts @@ -614,10 +614,10 @@ export interface Options { /** * `sourceType` indicates the mode the code should be parsed in. - * Can be either `"script"` or `"module"`. This influences global + * Can be either `"script"`, `"module"` or `"commonjs"`. This influences global * strict mode and parsing of `import` and `export` declarations. */ - sourceType?: "script" | "module" + sourceType?: "script" | "module" | "commonjs" /** * a callback that will be called when a semicolon is automatically inserted. diff --git a/acorn/src/options.js b/acorn/src/options.js index b87864c8..67312504 100644 --- a/acorn/src/options.js +++ b/acorn/src/options.js @@ -13,7 +13,7 @@ export const defaultOptions = { // for new syntax features. ecmaVersion: null, // `sourceType` indicates the mode the code should be parsed in. - // Can be either `"script"` or `"module"`. This influences global + // Can be either `"script"`, `"module"` or `"commonjs"`. This influences global // strict mode and parsing of `import` and `export` declarations. sourceType: "script", // `onInsertedSemicolon` can be a callback that will be called when @@ -137,6 +137,9 @@ export function getOptions(opts) { if (isArray(options.onComment)) options.onComment = pushComment(options, options.onComment) + if (options.sourceType === "commonjs" && options.allowAwaitOutsideFunction) + throw new Error("Cannot use allowAwaitOutsideFunction with sourceType: commonjs") + return options } diff --git a/acorn/src/state.js b/acorn/src/state.js index c3744f7d..7d0ac315 100644 --- a/acorn/src/state.js +++ b/acorn/src/state.js @@ -83,7 +83,12 @@ export class Parser { // Scope tracking for duplicate variable names (see scope.js) this.scopeStack = [] - this.enterScope(SCOPE_TOP) + this.enterScope( + this.options.sourceType === "commonjs" + // In commonjs, the top-level scope behaves like a function scope + ? SCOPE_FUNCTION + : SCOPE_TOP + ) // For RegExp validation this.regexpState = null diff --git a/acorn/src/statement.js b/acorn/src/statement.js index ace386ab..c9cf260e 100644 --- a/acorn/src/statement.js +++ b/acorn/src/statement.js @@ -27,7 +27,7 @@ pp.parseTopLevel = function(node) { this.raiseRecoverable(this.undefinedExports[name].start, `Export '${name}' is not defined`) this.adaptDirectivePrologue(node.body) this.next() - node.sourceType = this.options.sourceType + node.sourceType = this.options.sourceType === "commonjs" ? "script" : this.options.sourceType return this.finishNode(node, "Program") } diff --git a/test/driver.js b/test/driver.js index 57d7810f..0ac2f6e7 100644 --- a/test/driver.js +++ b/test/driver.js @@ -27,13 +27,13 @@ exports.runTests = function(config, callback) { try { var ast = parse(test.code, testOpts); } catch(e) { - if (!(e instanceof SyntaxError)) { console.log(e.stack); throw e; } if (test.error) { if (test.error.charAt(0) === "~" ? e.message.indexOf(test.error.slice(1)) > -1 : e.message === test.error) callback("ok", test.code); else callback("fail", test.code, "Expected error message: " + test.error + "\nGot error message: " + e.message); } else { + if (!(e instanceof SyntaxError)) { console.log(e.stack); throw e; } callback("error", test.code, e.message || e.toString()); } continue diff --git a/test/run.js b/test/run.js index 6af8a2b9..7156fd06 100644 --- a/test/run.js +++ b/test/run.js @@ -31,6 +31,7 @@ require("./tests-module-string-names.js"); require("./tests-import-attributes.js"); require("./tests-using.js"); + require("./tests-commonjs.js"); var acorn = require("../acorn") var acorn_loose = require("../acorn-loose") @@ -84,6 +85,28 @@ return opts.loose !== false; } } + }, + + // Test whether the test for `sourceType: 'script'` produces the same result for `'commonjs'`. + 'Normal with sourceType: commonjs': { + config: { + parse: (code, option) => acorn.parse(code, Object.assign({}, option, { sourceType: 'commonjs' })), + filter: function (test) { + var opts = test.options || {}; + return opts.commonjs !== false && !opts.allowAwaitOutsideFunction && (!opts.sourceType || opts.sourceType === 'script'); + } + } + }, + 'Loose with sourceType: commonjs': { + config: { + parse: (code, option) => acorn_loose.parse(code, Object.assign({}, option, { sourceType: 'commonjs' })), + loose: true, + filter: function (test) { + var opts = test.options || {}; + if (opts.loose === false) return false; + return opts.commonjs !== false && !opts.allowAwaitOutsideFunction && (!opts.sourceType || opts.sourceType === 'script'); + } + } } }; diff --git a/test/tests-commonjs.js b/test/tests-commonjs.js new file mode 100644 index 00000000..1a8b7d2a --- /dev/null +++ b/test/tests-commonjs.js @@ -0,0 +1,97 @@ +if (typeof exports !== "undefined") { + var test = require("./driver.js").test + var testFail = require("./driver.js").testFail +} + +// Top-level using declaration with commonjs +test("using x = resource;", { + type: "Program", + body: [{ + type: "VariableDeclaration", + declarations: [{ + type: "VariableDeclarator", + id: { + type: "Identifier", + name: "x" + }, + init: { + type: "Identifier", + name: "resource" + } + }], + kind: "using" + }], + sourceType: "script" +}, {ecmaVersion: 17, sourceType: "commonjs"}); + +// Top-level new.target with commonjs +test("new.target", { + type: "Program", + body: [{ + type: "ExpressionStatement", + expression: { + type: "MetaProperty", + meta: {type: "Identifier", name: "new"}, + property: {type: "Identifier", name: "target"} + } + }], + sourceType: "script" +}, {ecmaVersion: 6, sourceType: "commonjs"}); + +// new.target at top-level scope with commonjs +test("let y = () => new.target", { + type: "Program", + body: [{ + type: "VariableDeclaration", + declarations: [{ + type: "VariableDeclarator", + id: { + type: "Identifier", + name: "y" + }, + init: { + type: "ArrowFunctionExpression", + body: { + type: "MetaProperty", + meta: {type: "Identifier", name: "new"}, + property: {type: "Identifier", name: "target"} + }, + params: [] + } + }], + kind: "let" + }], + sourceType: "script" +}, {ecmaVersion: 6, sourceType: "commonjs"}); + +// Top-level return statement with commonjs +test("return {} / 2", { + type: "Program", + body: [{ + type: "ReturnStatement", + argument: { + type: "BinaryExpression", + left: { + type: "ObjectExpression", + properties: [] + }, + right: { + type: "Literal", + value: 2 + }, + operator: "/" + } + }], + sourceType: "script" +}, {sourceType: "commonjs"}); + +// Illegal return statement with commonjs +testFail(`class X { static { return; } }`, "'return' outside of function (1:19)", {ecmaVersion: 13, sourceType: "commonjs"}); + +// Top-level await using declaration with commonjs +testFail("await using x = resource;", "Await using cannot appear outside of async function (1:0)", {ecmaVersion: 17, sourceType: "commonjs"}); + +// Disallowing allowAwaitOutsideFunction with commonjs +testFail("await 1", "Cannot use allowAwaitOutsideFunction with sourceType: commonjs", {allowAwaitOutsideFunction: true, sourceType: "commonjs"}); +testFail("x", "Cannot use allowAwaitOutsideFunction with sourceType: commonjs", {allowAwaitOutsideFunction: true, sourceType: "commonjs"}); +test("x", {}, {allowAwaitOutsideFunction: false, sourceType: "commonjs"}); diff --git a/test/tests-harmony.js b/test/tests-harmony.js index b6d46610..dfd1ba11 100644 --- a/test/tests-harmony.js +++ b/test/tests-harmony.js @@ -14873,9 +14873,9 @@ test("function foo() { new.target; }", { }, {ecmaVersion: 6}); testFail("new.prop", "The only valid meta property for new is 'new.target' (1:4)", {ecmaVersion: 6}); -testFail("new.target", "'new.target' can only be used in functions and class static block (1:0)", {ecmaVersion: 6}); +testFail("new.target", "'new.target' can only be used in functions and class static block (1:0)", {ecmaVersion: 6, commonjs: false}); test("function x() { return () => new.target }", {}, {ecmaVersion: 6}); -testFail("let y = () => new.target", "'new.target' can only be used in functions and class static block (1:14)", {ecmaVersion: 6}); +testFail("let y = () => new.target", "'new.target' can only be used in functions and class static block (1:14)", {ecmaVersion: 6, commonjs: false}); test("export default function foo() {} false", { body: [ diff --git a/test/tests-using.js b/test/tests-using.js index 6040abed..5d9ec80f 100644 --- a/test/tests-using.js +++ b/test/tests-using.js @@ -1102,14 +1102,14 @@ testFail("let await using x = resource;", "Cannot use keyword 'await' outside an // let using is not allowed testFail("let using x = resource;", "Unexpected token (1:10)", {ecmaVersion: 17, sourceType: "module"}); // top level using is not allowed -testFail("using x = resource;", "Using declaration cannot appear in the top level when source type is `script` or in the bare case statement (1:0)", {ecmaVersion: 17, sourceType: "script"}); +testFail("using x = resource;", "Using declaration cannot appear in the top level when source type is `script` or in the bare case statement (1:0)", {ecmaVersion: 17, sourceType: "script", commonjs: false}); // BoundNames contains "let" testFail("async function test() { await using let = resource; }", "The keyword 'let' is reserved (1:36)", {ecmaVersion: 17, sourceType: "module"}); // BoundNames contains duplicate entries testFail("async function test() { await using x = resource1, x = resource2; }", "Identifier 'x' has already been declared (1:51)", {ecmaVersion: 17, sourceType: "module"}); // top level await using is not allowed -testFail("await using x = resource;", "Using declaration cannot appear in the top level when source type is `script` or in the bare case statement (1:0)", {ecmaVersion: 17, sourceType: "script"}); +testFail("await using x = resource;", "Using declaration cannot appear in the top level when source type is `script` or in the bare case statement (1:0)", {ecmaVersion: 17, sourceType: "script", commonjs: false}); // Basic missing initializer testFail("{ using x; }", "Missing initializer in using declaration (1:9)", {ecmaVersion: 17, sourceType: "module"}); diff --git a/test/tests.js b/test/tests.js index 11bc4cb2..4e91d7b2 100644 --- a/test/tests.js +++ b/test/tests.js @@ -27458,7 +27458,7 @@ testFail("\"\\u", "Bad character escape sequence (1:3)"); testFail("return", - "'return' outside of function (1:0)"); + "'return' outside of function (1:0)", { commonjs: false }); testFail("break", "Unsyntactic break (1:0)");