Version
v22.22.1
Platform
Microsoft Windows NT 10.0.26200.0 x64
Subsystem
module / cli (lib/internal/main/check_syntax.js)
What steps will reproduce the bug?
Create broken.js in a directory with no package.json:
import fs from "node:fs";
var = ;
Then:
$ node --check broken.js ; echo "exit: $?"
exit: 0
$ node broken.js
var = ;
^
SyntaxError: Unexpected token '='
--check reports nothing and exits 0 for a file that is not valid JavaScript under either goal
symbol.
How often does it reproduce? Is there a required condition?
Always, given the condition: the file's format must be ambiguous — a .js extension with no
"type" in the nearest package.json — and the source must contain import or export.
Everything that states its format is handled correctly:
| the file |
node --check |
.mjs, or "type": "module" |
correct — reports the error |
"type": "commonjs" |
correct — Cannot use import statement outside a module |
.js, no "type", containing import/export |
exit 0, nothing reported |
.js whose only ESM signal is top-level await |
correct — reports the error |
What is the expected behavior? Why is that the expected behavior?
A non-zero exit and the SyntaxError, as with every other invalid file. The documentation for
-c, --check is "Syntax check the script without executing", with no caveat about module
detection — and the same source reports the error correctly the moment its format is stated.
This matters in practice because node --check is a common cheap gate in CI, and a plain .js
file containing import is the shape most projects have — so the sources most likely to be
checked are the ones that silently pass.
What do you see instead?
Exit code 0 and no output.
Additional information
lib/internal/main/check_syntax.js parses under the Module goal only when defaultGetFormat
answers 'module', which an ambiguous .js does not:
if (format === 'module') {
const { ModuleWrap } = internalBinding('module_wrap');
new ModuleWrap(filename, undefined, source, 0, 0);
return;
}
wrapSafe(filename, source, undefined, format);
So the CommonJS branch runs. Inside wrapSafe, cjsModuleInstance is undefined here, so
shouldDetectModule falls through to --require-module — on by default — and
compileFunctionForCJSLoader detects ESM instead of throwing. Its result is then discarded, and
the source is never parsed under either goal.
Module.prototype._compile handles exactly this case correctly a little further down the same
file, by reading the flag the detection returns:
if (format !== 'module') {
const result = wrapSafe(filename, content, this, format);
compiledWrapper = result.function;
if (result.canParseAsESM) {
format = 'module';
}
}
if (format === 'module') {
loadESMFromCJS(this, filename, format, content);
return;
}
checkSyntax looks like it wants the same canParseAsESM branch, falling back to the ModuleWrap
parse it already has above rather than returning.
The relevant part of check_syntax.js is unchanged on main; the file was last touched
2024-12-02. The behaviour most likely dates from module: unflag detect-module (2024-07-20), which
made detection the default for this path.
Test coverage. test/sequential/test-cli-syntax-bad.js checks bad_syntax.js, bad_syntax,
bad_syntax_shebang.js and bad_syntax_shebang — all CommonJS.
test/fixtures/syntax/bad_syntax.mjs exists but is used only by test-esm-error-cache.js and
test-compile-cache-bad-syntax.js, not by either --check test. So neither a .mjs file nor a
detected-ESM .js file is currently asserted to fail --check.
I am happy to open a PR with the fix and a fixture plus test if that shape is welcome.
Found while differential-testing Node against
ViperJS, an embeddable JavaScript engine in safe Rust.
Sweeping eight repositories, the two disagreed about 18 files; every one of them turned out to be
genuinely invalid JavaScript, and node --check had reported nine as fine. V8 itself is right
about all eighteen — it is only --check that never asks it.
Version
v22.22.1
Platform
Subsystem
module / cli (
lib/internal/main/check_syntax.js)What steps will reproduce the bug?
Create
broken.jsin a directory with nopackage.json:Then:
--checkreports nothing and exits 0 for a file that is not valid JavaScript under either goalsymbol.
How often does it reproduce? Is there a required condition?
Always, given the condition: the file's format must be ambiguous — a
.jsextension with no"type"in the nearestpackage.json— and the source must containimportorexport.Everything that states its format is handled correctly:
node --check.mjs, or"type": "module""type": "commonjs"Cannot use import statement outside a module.js, no"type", containingimport/export.jswhose only ESM signal is top-levelawaitWhat is the expected behavior? Why is that the expected behavior?
A non-zero exit and the
SyntaxError, as with every other invalid file. The documentation for-c,--checkis "Syntax check the script without executing", with no caveat about moduledetection — and the same source reports the error correctly the moment its format is stated.
This matters in practice because
node --checkis a common cheap gate in CI, and a plain.jsfile containing
importis the shape most projects have — so the sources most likely to bechecked are the ones that silently pass.
What do you see instead?
Exit code 0 and no output.
Additional information
lib/internal/main/check_syntax.jsparses under the Module goal only whendefaultGetFormatanswers
'module', which an ambiguous.jsdoes not:So the CommonJS branch runs. Inside
wrapSafe,cjsModuleInstanceisundefinedhere, soshouldDetectModulefalls through to--require-module— on by default — andcompileFunctionForCJSLoaderdetects ESM instead of throwing. Its result is then discarded, andthe source is never parsed under either goal.
Module.prototype._compilehandles exactly this case correctly a little further down the samefile, by reading the flag the detection returns:
checkSyntaxlooks like it wants the samecanParseAsESMbranch, falling back to theModuleWrapparse it already has above rather than returning.
The relevant part of
check_syntax.jsis unchanged onmain; the file was last touched2024-12-02. The behaviour most likely dates from
module: unflag detect-module(2024-07-20), whichmade detection the default for this path.
Test coverage.
test/sequential/test-cli-syntax-bad.jschecksbad_syntax.js,bad_syntax,bad_syntax_shebang.jsandbad_syntax_shebang— all CommonJS.test/fixtures/syntax/bad_syntax.mjsexists but is used only bytest-esm-error-cache.jsandtest-compile-cache-bad-syntax.js, not by either--checktest. So neither a.mjsfile nor adetected-ESM
.jsfile is currently asserted to fail--check.I am happy to open a PR with the fix and a fixture plus test if that shape is welcome.
Found while differential-testing Node against
ViperJS, an embeddable JavaScript engine in safe Rust.
Sweeping eight repositories, the two disagreed about 18 files; every one of them turned out to be
genuinely invalid JavaScript, and
node --checkhad reported nine as fine. V8 itself is rightabout all eighteen — it is only
--checkthat never asks it.