Skip to content

Commit 394c9b5

Browse files
committed
feat(tools/prim): add lint subcommand + Node bootstrap surface support
Three additions: 1. **`prim lint`** — new structural lint subcommand for primordials destructure blocks. Currently encodes one rule: - `ctor-rename`: constructor primordials (`Array`, `Set`, `TypeError`, etc.) MUST be aliased `<Name>: <Name>Ctor` when destructured from `primordials` or any configured primordials-shaped source. Bare `{ Array } = primordials` shadows the global and is reported as a violation. The set of primordials-shaped sources is configurable via `--primordials-source <name>` (repeatable). Defaults cover `primordials` (Node bootstrap global) and the `internal/socketsecurity/safe-references` re-export module used in socket-btm additions. Exits 1 if any violation found. 2. **`--surface <path>`** flag on coverage/gaps/audit. Lets users point prim at any primordials source file, overriding the default sibling/installed lookup. Use case: auditing socket-btm's additions against Node's `lib/internal/per_context/primordials.js` surface instead of socket-lib's surface. 3. **Node bootstrap surface derivation**. When `--surface` points at a `per_context/primordials.js`-style file, the loader recognizes the path and dynamically computes the full surface by enumerating the static + prototype methods of the upstream globals (Array, Object, String, Number, Map, Set, Error, RegExp, JSON, Math, Reflect, etc.) the same way Node does at bootstrap. This goes from ~20 detected names (text-only regex) to 541 detected names (matches what Node actually installs). Also: `audit.mts` now resolves constructor primordial names against the surface — picks `<Name>Ctor` if the surface uses socket-lib's convention, falls back to bare `<Name>` if the surface uses Node's bootstrap convention. Eliminates spurious "ArrayCtor missing" gaps when auditing against Node's surface.
1 parent 5d83ce7 commit 394c9b5

5 files changed

Lines changed: 615 additions & 22 deletions

File tree

tools/prim/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ pnpm prim mod --target . --dir src --include-guessed --apply
7777
| `prim gaps` | Report call sites that need a primordial that doesn't exist yet — the input list for expanding `socket-lib/src/primordials.ts`. |
7878
| `prim audit` | Run `coverage` + `gaps` and (optionally) persist the snapshot to the state file. |
7979
| `prim state` | Inspect the persisted state file. |
80+
| `prim lint` | Structural lint rules for primordials destructure blocks. Currently: `ctor-rename` — constructor primordials (`Array`, `Set`, `TypeError`, …) must be aliased `<Name>: <Name>Ctor` when destructured from `primordials` (or any configured primordials-shaped source like `safe-references`). Exits 1 on violations. |
8081
| `prim mod` | Codemod **JavaScript** source files to use primordials. Dry-run by default; pass `--apply` to write. TypeScript is out of scope (rewriting `.ts` requires source-mapping between stripped-types and original byte offsets) — `prim audit` still walks TS, so candidates are visible. |
8182

8283
## How it knows what's covered

tools/prim/src/audit.mts

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -263,6 +263,23 @@ export function auditDirectory({
263263
return false
264264
}
265265

266+
// Constructor naming differs between surfaces:
267+
// socket-lib uses `<Name>Ctor` (e.g. `ArrayCtor`, `SetCtor`)
268+
// Node bootstrap uses bare `<Name>` (e.g. `Array`, `Set`)
269+
// Pick whichever variant the surface actually exports; if neither
270+
// is present we report the socket-lib convention as the gap so the
271+
// expansion target is clear.
272+
function resolveCtorName(globalName) {
273+
const sktName = ctorPrimordialName(globalName)
274+
if (exported.has(sktName)) {
275+
return sktName
276+
}
277+
if (exported.has(globalName)) {
278+
return globalName
279+
}
280+
return sktName
281+
}
282+
266283
const visitors = {
267284
NewExpression(node, _ancestors) {
268285
if (
@@ -275,7 +292,7 @@ export function auditDirectory({
275292
currentFile.relPath,
276293
node.start,
277294
`new ${node.callee.name}(...)`,
278-
ctorPrimordialName(node.callee.name),
295+
resolveCtorName(node.callee.name),
279296
)
280297
},
281298
CallExpression(node, _ancestors) {

tools/prim/src/cli.mts

Lines changed: 66 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,19 @@
22
* @fileoverview `prim` CLI entry point.
33
*
44
* Subcommands:
5-
* audit — full report: coverage + gaps in one pass.
6-
* coverage — only call sites where a primordial already exists
7-
* (migration candidates).
8-
* gaps — only call sites where no primordial exists yet
9-
* (surface-expansion candidates).
10-
* mod — rewrite call sites to use primordials. Dry-run by default;
11-
* pass `--apply` to write changes. Adds the import block.
12-
* state — show or diff the persisted state file.
5+
* audit — full report: coverage + gaps in one pass.
6+
* coverage — only call sites where a primordial already exists
7+
* (migration candidates).
8+
* gaps — only call sites where no primordial exists yet
9+
* (surface-expansion candidates).
10+
* mod — rewrite call sites to use primordials. Dry-run by default;
11+
* pass `--apply` to write changes. Adds the import block.
12+
* lint — structural lint rules for primordials usage. Currently:
13+
* ctor-rename (constructor primordials must be aliased
14+
* `<Name>: <Name>Ctor` when destructured from
15+
* `primordials` or any configured primordials-shaped
16+
* source). Exits 1 if violations are found.
17+
* state — show or diff the persisted state file.
1318
*
1419
* Common flags:
1520
* --target <path> The repo to audit. Defaults to cwd.
@@ -36,6 +41,7 @@ import { parseArgs } from 'node:util'
3641
import { auditDirectory } from './audit.mts'
3742
import { applyCodemod } from './codemod.mts'
3843
import { formatHuman, formatJson } from './format.mts'
44+
import { formatLintFindings, lintSource } from './lint.mts'
3945
import { defaultStatePath, loadState, rollup, saveState } from './state.mts'
4046
import { loadPrimordialsSurface } from './surface.mts'
4147

@@ -50,6 +56,9 @@ COMMANDS
5056
coverage Only show migration candidates — existing primordials.
5157
gaps Only show surface gaps — uncovered patterns.
5258
mod Rewrite call sites to use primordials. Dry-run by default.
59+
lint Structural lint rules for primordials usage. Currently:
60+
ctor-rename (constructor primordials must be aliased
61+
\`<Name>: <Name>Ctor\`). Exits 1 if violations are found.
5362
state Show the persisted state file.
5463
5564
COMMON OPTIONS
@@ -58,6 +67,15 @@ COMMON OPTIONS
5867
commands (audit/coverage/gaps); default \`src\` for \`mod\`.
5968
--json JSON output instead of human-readable text.
6069
--state <path> State file path (default: <cwd>/.prim-state.json).
70+
--surface <path> Explicit primordials source file (overrides the default
71+
sibling/installed lookup). Use this to audit against
72+
Node's lib/internal/per_context/primordials.js or any
73+
other primordials-shaped source.
74+
--primordials-source <name> (lint only, repeatable) Identifier or require()
75+
specifier to treat as a primordials-shaped source.
76+
Defaults: \`primordials\`,
77+
\`internal/socketsecurity/safe-references\`,
78+
\`safe-references\`.
6179
--update-state Persist findings into the state file.
6280
--help, -h Show this help.
6381
@@ -92,6 +110,8 @@ const ARG_OPTIONS = {
92110
dir: { type: 'string' },
93111
json: { type: 'boolean', default: false },
94112
state: { type: 'string' },
113+
surface: { type: 'string' },
114+
'primordials-source': { type: 'string', multiple: true },
95115
'update-state': { type: 'boolean', default: false },
96116
apply: { type: 'boolean', default: false },
97117
'include-guessed': { type: 'boolean', default: false },
@@ -152,9 +172,31 @@ export async function runCli(argv) {
152172
)
153173
}
154174

175+
// `lint` is purely structural — it doesn't need a primordials surface.
176+
// Handle it before the surface load so users don't need to pass
177+
// --surface for a lint-only check. The list of "primordials-shaped"
178+
// sources can be extended via --primordials-source (repeatable).
179+
if (command === 'lint') {
180+
const primordialSources = values['primordials-source']
181+
const findings = lintSource({
182+
targetRoot,
183+
scanDir,
184+
primordialSources: Array.isArray(primordialSources)
185+
? primordialSources
186+
: primordialSources
187+
? [primordialSources]
188+
: undefined,
189+
})
190+
reportLint(findings, json, path.basename(targetRoot))
191+
if (findings.length > 0) {
192+
process.exitCode = 1
193+
}
194+
return
195+
}
196+
155197
let surface
156198
try {
157-
surface = loadPrimordialsSurface(targetRoot)
199+
surface = loadPrimordialsSurface(targetRoot, values.surface)
158200
} catch (e) {
159201
fail(e.message)
160202
}
@@ -220,6 +262,21 @@ function report(findings, json, targetName, mode) {
220262
}
221263
}
222264

265+
function reportLint(findings, json, targetName) {
266+
if (json) {
267+
process.stdout.write(
268+
formatJson({
269+
targetName,
270+
mode: 'lint',
271+
count: findings.length,
272+
findings,
273+
}) + '\n',
274+
)
275+
return
276+
}
277+
process.stdout.write(formatLintFindings(findings, { targetName }))
278+
}
279+
223280
function reportMod(result, json, applied) {
224281
if (json) {
225282
process.stdout.write(

0 commit comments

Comments
 (0)