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:
importfsfrom"node:fs";var=;
Then:
$ node --check broken.js ;echo"exit: $?"exit: 0
$ node broken.jsvar = ; ^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');newModuleWrap(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
compileFunctionForCJSLoaderdetects 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'){constresult=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.