Verify PCRE2 on every hosted test platform and coverage runner. Preserve dependency PATH for real scanner delegates and report missing prerequisites explicitly, keeping production fail-closed behavior and hermetic missing/error controls intact.
Isolate spawned Stop hooks through TMPDIR/TMP/TEMP, open transcript fixtures exclusively with private modes, and derive intentional stale-race fixture identity from its opened descriptor.
- Record whether each `{` opens a block or an object literal from the
token before it; only a block's `}` lets a regex follow, so `{} / 2`
divides again while `if (ok) {} /re/` stays a regex.
- Match regex-preceding keywords only when no member dot precedes them,
so `a.of / 2` divides.
- Exempt the last dot of a spread from the member-call guard, so
`...require('./x')` is a module load.
- Resolve a call's first argument up to its first top-level comma, so
`require('./x', null)` and `import('./x.json', { with })` name `./x`.
`import(path.join(__dirname, ...))`, previously skipped silently, now
warns as dynamic.
Rescanning all 802 repository JavaScript files against the previous head
changes no result. The real registry is unchanged: 35/35 resolved, zero
failures or warnings. The design doc records the fix and states the gate's
scope: static relative specifiers in planned JavaScript only.
Three open review findings still pass the gate with a missing dependency,
each reproduced against the real registry:
- const ratio = {} / 2; require('../x') -- `}` is treated as a statement
boundary, so the division reads as a regex that swallows the require.
a.of / 2 does the same through the keyword list.
- [...require('../x')] -- the member-call guard reads the last dot of the
spread as member access and skips the call.
- require('../x', null) -- a literal counts only when the call closes right
after it, so the specifier is demoted to a dynamic warning, which never
fails.
14 cases, 11 failing. Controls that already pass: a regex after a function
body, a class body and a nested block.
- Call patterns now skip a require/import preceded by a dot, with or
without whitespace or optional chaining, so records.require('./label')
and obj?.import('./x') are method calls rather than module loads.
- Replace the one-level dynamic-argument pattern with a balanced-paren
scan over the string-free skeleton, so require(path.join(__dirname,
getName())) and require(a(b(c()))) are reported as dynamic.
Rescanning all 802 repository JavaScript files against the previous head
changes 6 results, each a newly reported dynamic argument nested deeper
than one level (docker eval checks and one test); no specifier or package
result changes. The real registry is unchanged: 35/35 resolved, zero
failures or warnings.
Two first-round review findings were never addressed, and both reproduce
on the current head:
- records.require('./label'), obj?.import('./x') and obj . require('./y')
are method calls, but the call pattern's word boundary is satisfied by
the dot, so a self-contained object method fails every projection for a
file the script never loads.
- The dynamic-argument pattern allows one level of nested parentheses, so
require(path.join(__dirname, getName())) produces no dynamic warning
while require(getName()) does.
Control: a genuine require beside the member calls is still a dependency.
- An extensionless require now tries only the path, .js, .json, and a
directory's index.js/index.json, as Node does; .cjs and .mjs must be
named, so require('./helper') no longer passes on helper.cjs alone.
- Treat `}` as a statement boundary, so a regex after a block is read as
a regex and cannot swallow a later require.
- Decide whether a paren opens a condition from the token before the
keyword, ignoring whitespace, so `obj.\nif(x) / 2` and
`obj?. while(x) / 2` stay method calls followed by division.
- Record which slots came from template literals and skip them for
static `from` and side-effect `import`, which accept only a quoted
string; a tagged template named `from` is no longer an import.
A template passed to import() is still a dependency.
Rescanning all 802 repository JavaScript files against dd14493e changes
no result, and the real registry still reports 35/35 resolved with zero
failures or warnings.
Review on dd14493e found four more ways the closure gate misjudges a
carrier, each reproduced against that head:
- require('./helper') resolves when only helper.cjs or helper.mjs is
planned, but Node's CommonJS search tries only .js, .json and .node.
- In `if (ok) {} /"/.test(text); require(...)` the regex after the block
is read as division and its quote swallows the require.
- `obj.\nif(x) / 2` is a method call, but the condition check only saw
the character immediately before `if`, so the division read as a regex
and swallowed a later require. Introduced by the previous fix.
- A tagged template named `from` was recorded as an import; with exact
import resolution it now fails a carrier that never loads that path.
Introduced by the previous fix.
Controls: an extensionless require still resolves to a .json file, and a
template literal passed to import() is still a dependency.
- Track whether each relative specifier arrives through require or
through import()/static import, and resolve the import forms by exact
path. Node's ESM resolver adds no extension and rejects directory
imports even from a CommonJS file, so import('./helper') and
import('./data') no longer pass on helper.js or data/index.json alone.
require keeps the CommonJS extension and index candidates.
- Stop normalizing backslashes in relative specifiers. Linux and macOS
read them as filename characters, so require('./lib\helper.js') now
fails as non-portable-separator instead of resolving to lib/helper.js.
- Record whether each closing paren ends an if/while/for/with condition,
so a `/` right after one starts a regex. A method named `if` and calls
nested inside a condition still read as division.
extractReferences gains an `imports` list. Rescanning all 802 repository
JavaScript files against the previous scanner changes no result, and the
real registry still reports 35/35 resolved with zero failures.
Review on aec97150 found four ways the closure gate passes a carrier that
fails at run time. Each is reproduced here against the current head:
- import('./helper') with only helper.js planned is accepted, but Node's
ESM resolver does no extension search.
- import('./data') with only data/index.json (or index.js) planned is
accepted, but Node rejects directory imports even from CommonJS.
- require('./lib\helper.js') with lib/helper.js planned is accepted
because backslashes were normalized, but Linux and macOS read the
backslash as part of the filename.
- In `if (ok) /['"]/.test(text); require('./missing')` the regex after
the condition's closing paren is read as division, its quote opens a
fake string, and the require disappears.
Controls that must keep passing: an exact import() of a planned file, an
extensionless require through the CommonJS search, and division after a
method named `if` or a call nested inside a condition. extractReferences
gains an `imports` list so the resolver can tell the two forms apart.