Skip to content

module: correctly detect top-level await in ambiguous contexts #58646

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions src/node_contextify.cc
Original file line number Diff line number Diff line change
Expand Up @@ -1647,6 +1647,12 @@ static const auto throws_only_in_cjs_error_messages =
"await is only valid in async functions and "
"the top level bodies of modules"};

static const auto maybe_top_level_await_errors =
std::array<std::string_view, 2>{
"missing ) after argument list", // example: `func(await 1);`
"SyntaxError: Unexpected" // example: `if(await 1)`
};

// If cached_data is provided, it would be used for the compilation and
// the on-disk compilation cache from NODE_COMPILE_CACHE (if configured)
// would be ignored.
Expand Down Expand Up @@ -1877,6 +1883,16 @@ bool ShouldRetryAsESM(Realm* realm,
break;
}
}

for (const auto& error_message : maybe_top_level_await_errors) {
if (message_view.find(error_message) != std::string_view::npos) {
// If the error message is related to top-level await, we can try to
// compile it as ESM.
maybe_valid_in_esm = true;
break;
}
}

if (!maybe_valid_in_esm) {
return false;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import { spawnPromisified } from '../common/index.mjs';
import { describe, it } from 'node:test';
import { strictEqual, match } from 'node:assert';

describe('maybe top-level await syntax errors that are not recognized as top-level await errors', () => {
const expressions = [
// string
{ expression: '""' },
// number
{ expression: '0' },
// boolean
{ expression: 'true' },
// null
{ expression: 'null' },
// undefined
{ expression: 'undefined' },
// object
{ expression: '{}' },
// array
{ expression: '[]' },
// new
{ expression: 'new Date()' },
// identifier
{ initialize: 'const a = 2;', expression: 'a' },
];
it('should not crash the process', async () => {
for (const { expression, initialize } of expressions) {
const wrapperExpressions = [
`function callAwait() {}; callAwait(await ${expression});`,
`if (await ${expression}) {}`,
`{ key: await ${expression} }`,
`[await ${expression}]`,
`(await ${expression})`,
];
for (const wrapperExpression of wrapperExpressions) {
const { code, signal, stdout, stderr } = await spawnPromisified(process.execPath, [
'--eval',
`
${initialize || ''}
${wrapperExpression}
`,
]);

strictEqual(stderr, '');
strictEqual(stdout, '');
strictEqual(code, 0);
strictEqual(signal, null);
}
}
});

it('should crash when the expression is not valid', async () => {
let { code, signal, stdout, stderr } = await spawnPromisified(process.execPath, [
'--eval',
`
function callAwait() {}
callAwait(await "" "");
`,
]);
match(stderr, /SyntaxError: missing \) after argument list/);
strictEqual(stdout, '');
strictEqual(code, 1);
strictEqual(signal, null);

({ code, signal, stdout, stderr } = await spawnPromisified(process.execPath, [
'--eval',
`
function callAwait() {}
if (a "") {}
`,
]));
match(stderr, /SyntaxError: Unexpected string/);
strictEqual(stdout, '');
strictEqual(code, 1);
strictEqual(signal, null);
});
});
Loading