@vibe-agent-toolkit/utils 0.2.0-rc.11 → 0.2.0-rc.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +111 -9
- package/dist/archive/archive.d.ts +129 -0
- package/dist/archive/archive.d.ts.map +1 -0
- package/dist/archive/archive.js +223 -0
- package/dist/archive/archive.js.map +1 -0
- package/dist/archive/entries.d.ts +132 -0
- package/dist/archive/entries.d.ts.map +1 -0
- package/dist/archive/entries.js +223 -0
- package/dist/archive/entries.js.map +1 -0
- package/dist/archive/limits.d.ts +39 -0
- package/dist/archive/limits.d.ts.map +1 -0
- package/dist/archive/limits.js +59 -0
- package/dist/archive/limits.js.map +1 -0
- package/dist/archive/records.d.ts +14 -0
- package/dist/archive/records.d.ts.map +1 -0
- package/dist/archive/records.js +3 -0
- package/dist/archive/records.js.map +1 -0
- package/dist/archive/tar-reader.d.ts +18 -0
- package/dist/archive/tar-reader.d.ts.map +1 -0
- package/dist/archive/tar-reader.js +173 -0
- package/dist/archive/tar-reader.js.map +1 -0
- package/dist/archive/zip-reader.d.ts +76 -0
- package/dist/archive/zip-reader.d.ts.map +1 -0
- package/dist/archive/zip-reader.js +376 -0
- package/dist/archive/zip-reader.js.map +1 -0
- package/dist/archive.d.ts +14 -0
- package/dist/archive.d.ts.map +1 -0
- package/dist/archive.js +12 -0
- package/dist/archive.js.map +1 -0
- package/dist/asset-reference.d.ts +17 -1
- package/dist/asset-reference.d.ts.map +1 -1
- package/dist/asset-reference.js +31 -7
- package/dist/asset-reference.js.map +1 -1
- package/dist/dirent-kind.d.ts +13 -40
- package/dist/dirent-kind.d.ts.map +1 -1
- package/dist/dirent-kind.js +21 -69
- package/dist/dirent-kind.js.map +1 -1
- package/dist/entrypoint.d.ts +2 -2
- package/dist/entrypoint.d.ts.map +1 -1
- package/dist/entrypoint.js +4 -3
- package/dist/entrypoint.js.map +1 -1
- package/dist/env-flag.d.ts +2 -0
- package/dist/env-flag.d.ts.map +1 -1
- package/dist/env-flag.js +2 -0
- package/dist/env-flag.js.map +1 -1
- package/dist/errors/confirmed-absent.d.ts +41 -0
- package/dist/errors/confirmed-absent.d.ts.map +1 -0
- package/dist/errors/confirmed-absent.js +43 -0
- package/dist/errors/confirmed-absent.js.map +1 -0
- package/dist/errors/errno-table.d.ts +177 -0
- package/dist/errors/errno-table.d.ts.map +1 -0
- package/dist/errors/errno-table.js +343 -0
- package/dist/errors/errno-table.js.map +1 -0
- package/dist/errors/fs-boundary.d.ts +36 -0
- package/dist/errors/fs-boundary.d.ts.map +1 -0
- package/dist/errors/fs-boundary.js +121 -0
- package/dist/errors/fs-boundary.js.map +1 -0
- package/dist/errors/fs-fault.d.ts +176 -0
- package/dist/errors/fs-fault.d.ts.map +1 -0
- package/dist/errors/fs-fault.js +215 -0
- package/dist/errors/fs-fault.js.map +1 -0
- package/dist/errors/git-errno.d.ts +13 -0
- package/dist/errors/git-errno.d.ts.map +1 -0
- package/dist/errors/git-errno.js +45 -0
- package/dist/errors/git-errno.js.map +1 -0
- package/dist/errors/path-present.d.ts +72 -0
- package/dist/errors/path-present.d.ts.map +1 -0
- package/dist/errors/path-present.js +110 -0
- package/dist/errors/path-present.js.map +1 -0
- package/dist/errors/rag-index-empty.d.ts +16 -0
- package/dist/errors/rag-index-empty.d.ts.map +1 -0
- package/dist/errors/rag-index-empty.js +16 -0
- package/dist/errors/rag-index-empty.js.map +1 -0
- package/dist/errors/suppressed-faults.d.ts +34 -0
- package/dist/errors/suppressed-faults.d.ts.map +1 -0
- package/dist/errors/suppressed-faults.js +66 -0
- package/dist/errors/suppressed-faults.js.map +1 -0
- package/dist/file-crawler.d.ts +13 -18
- package/dist/file-crawler.d.ts.map +1 -1
- package/dist/file-crawler.js +140 -24
- package/dist/file-crawler.js.map +1 -1
- package/dist/fs-utils.d.ts +75 -50
- package/dist/fs-utils.d.ts.map +1 -1
- package/dist/fs-utils.js +148 -158
- package/dist/fs-utils.js.map +1 -1
- package/dist/fs.d.ts +4 -4
- package/dist/fs.d.ts.map +1 -1
- package/dist/fs.js +4 -4
- package/dist/fs.js.map +1 -1
- package/dist/git-snapshot.d.ts +25 -17
- package/dist/git-snapshot.d.ts.map +1 -1
- package/dist/git-snapshot.js +65 -11
- package/dist/git-snapshot.js.map +1 -1
- package/dist/git-tracker.d.ts +11 -3
- package/dist/git-tracker.d.ts.map +1 -1
- package/dist/git-tracker.js +29 -6
- package/dist/git-tracker.js.map +1 -1
- package/dist/git-utils.d.ts.map +1 -1
- package/dist/git-utils.js +7 -35
- package/dist/git-utils.js.map +1 -1
- package/dist/git.d.ts +1 -1
- package/dist/git.d.ts.map +1 -1
- package/dist/git.js +1 -1
- package/dist/git.js.map +1 -1
- package/dist/gitignore-checker.d.ts.map +1 -1
- package/dist/gitignore-checker.js +5 -4
- package/dist/gitignore-checker.js.map +1 -1
- package/dist/in-order.d.ts +93 -0
- package/dist/in-order.d.ts.map +1 -0
- package/dist/in-order.js +165 -0
- package/dist/in-order.js.map +1 -0
- package/dist/index.d.ts +20 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +30 -4
- package/dist/index.js.map +1 -1
- package/dist/kill-process-tree.d.ts +43 -0
- package/dist/kill-process-tree.d.ts.map +1 -0
- package/dist/kill-process-tree.js +48 -0
- package/dist/kill-process-tree.js.map +1 -0
- package/dist/link-following.d.ts +115 -0
- package/dist/link-following.d.ts.map +1 -0
- package/dist/link-following.js +283 -0
- package/dist/link-following.js.map +1 -0
- package/dist/listing-refusal.d.ts +44 -0
- package/dist/listing-refusal.d.ts.map +1 -1
- package/dist/listing-refusal.js +79 -2
- package/dist/listing-refusal.js.map +1 -1
- package/dist/name-fold.d.ts +25 -0
- package/dist/name-fold.d.ts.map +1 -0
- package/dist/name-fold.js +40 -0
- package/dist/name-fold.js.map +1 -0
- package/dist/path-containment.d.ts +3 -1
- package/dist/path-containment.d.ts.map +1 -1
- package/dist/path-containment.js +13 -7
- package/dist/path-containment.js.map +1 -1
- package/dist/path-utils.d.ts +14 -17
- package/dist/path-utils.d.ts.map +1 -1
- package/dist/path-utils.js +37 -55
- package/dist/path-utils.js.map +1 -1
- package/dist/process.d.ts +2 -0
- package/dist/process.d.ts.map +1 -1
- package/dist/process.js +2 -0
- package/dist/process.js.map +1 -1
- package/dist/project-utils.d.ts.map +1 -1
- package/dist/project-utils.js +8 -9
- package/dist/project-utils.js.map +1 -1
- package/dist/safe-exec.d.ts +6 -2
- package/dist/safe-exec.d.ts.map +1 -1
- package/dist/safe-exec.js +1 -1
- package/dist/safe-exec.js.map +1 -1
- package/dist/skill-targets.d.ts.map +1 -1
- package/dist/skill-targets.js +3 -1
- package/dist/skill-targets.js.map +1 -1
- package/dist/skill-test/spawn-claude.d.ts +0 -15
- package/dist/skill-test/spawn-claude.d.ts.map +1 -1
- package/dist/skill-test/spawn-claude.js +11 -50
- package/dist/skill-test/spawn-claude.js.map +1 -1
- package/dist/special-file.d.ts +18 -0
- package/dist/special-file.d.ts.map +1 -0
- package/dist/special-file.js +21 -0
- package/dist/special-file.js.map +1 -0
- package/dist/terminating-signals.d.ts +129 -0
- package/dist/terminating-signals.d.ts.map +1 -0
- package/dist/terminating-signals.js +210 -0
- package/dist/terminating-signals.js.map +1 -0
- package/dist/test-helpers.d.ts +9 -5
- package/dist/test-helpers.d.ts.map +1 -1
- package/dist/test-helpers.js +18 -21
- package/dist/test-helpers.js.map +1 -1
- package/dist/test-scratch-guard.d.ts +34 -0
- package/dist/test-scratch-guard.d.ts.map +1 -0
- package/dist/test-scratch-guard.js +45 -0
- package/dist/test-scratch-guard.js.map +1 -0
- package/dist/testing/archive-fixtures.d.ts +147 -0
- package/dist/testing/archive-fixtures.d.ts.map +1 -0
- package/dist/testing/archive-fixtures.js +449 -0
- package/dist/testing/archive-fixtures.js.map +1 -0
- package/dist/testing/executables.d.ts +6 -0
- package/dist/testing/executables.d.ts.map +1 -1
- package/dist/testing/executables.js +20 -9
- package/dist/testing/executables.js.map +1 -1
- package/dist/testing/fault-fs-preload.d.ts +16 -0
- package/dist/testing/fault-fs-preload.d.ts.map +1 -0
- package/dist/testing/fault-fs-preload.js +22 -0
- package/dist/testing/fault-fs-preload.js.map +1 -0
- package/dist/testing/fault-fs.d.ts +115 -0
- package/dist/testing/fault-fs.d.ts.map +1 -0
- package/dist/testing/fault-fs.js +411 -0
- package/dist/testing/fault-fs.js.map +1 -0
- package/dist/testing/fault-spec.d.ts +83 -0
- package/dist/testing/fault-spec.d.ts.map +1 -0
- package/dist/testing/fault-spec.js +132 -0
- package/dist/testing/fault-spec.js.map +1 -0
- package/dist/testing/hostile-tree.d.ts.map +1 -1
- package/dist/testing/hostile-tree.js +3 -4
- package/dist/testing/hostile-tree.js.map +1 -1
- package/dist/testing/platform-gates.d.ts +8 -0
- package/dist/testing/platform-gates.d.ts.map +1 -1
- package/dist/testing/platform-gates.js +24 -0
- package/dist/testing/platform-gates.js.map +1 -1
- package/dist/testing/temp-dir.d.ts +31 -0
- package/dist/testing/temp-dir.d.ts.map +1 -1
- package/dist/testing/temp-dir.js +63 -0
- package/dist/testing/temp-dir.js.map +1 -1
- package/dist/testing/tier-timeout.d.ts +34 -0
- package/dist/testing/tier-timeout.d.ts.map +1 -0
- package/dist/testing/tier-timeout.js +45 -0
- package/dist/testing/tier-timeout.js.map +1 -0
- package/dist/testing/tree-snapshot.d.ts +71 -0
- package/dist/testing/tree-snapshot.d.ts.map +1 -0
- package/dist/testing/tree-snapshot.js +163 -0
- package/dist/testing/tree-snapshot.js.map +1 -0
- package/dist/testing.d.ts +6 -0
- package/dist/testing.d.ts.map +1 -1
- package/dist/testing.js +8 -1
- package/dist/testing.js.map +1 -1
- package/dist/text-file.d.ts +14 -1
- package/dist/text-file.d.ts.map +1 -1
- package/dist/text-file.js +41 -8
- package/dist/text-file.js.map +1 -1
- package/dist/timing-dump.d.ts.map +1 -1
- package/dist/timing-dump.js +2 -10
- package/dist/timing-dump.js.map +1 -1
- package/dist/tree-change/apply-decisions.d.ts +101 -0
- package/dist/tree-change/apply-decisions.d.ts.map +1 -0
- package/dist/tree-change/apply-decisions.js +139 -0
- package/dist/tree-change/apply-decisions.js.map +1 -0
- package/dist/tree-change/apply.d.ts +98 -0
- package/dist/tree-change/apply.d.ts.map +1 -0
- package/dist/tree-change/apply.js +364 -0
- package/dist/tree-change/apply.js.map +1 -0
- package/dist/tree-change/copy-decisions.d.ts +58 -0
- package/dist/tree-change/copy-decisions.d.ts.map +1 -0
- package/dist/tree-change/copy-decisions.js +66 -0
- package/dist/tree-change/copy-decisions.js.map +1 -0
- package/dist/tree-change/copy-tree.d.ts +125 -0
- package/dist/tree-change/copy-tree.d.ts.map +1 -0
- package/dist/tree-change/copy-tree.js +279 -0
- package/dist/tree-change/copy-tree.js.map +1 -0
- package/dist/tree-change/files.d.ts +167 -0
- package/dist/tree-change/files.d.ts.map +1 -0
- package/dist/tree-change/files.js +423 -0
- package/dist/tree-change/files.js.map +1 -0
- package/dist/tree-change/followed-walk.d.ts +54 -0
- package/dist/tree-change/followed-walk.d.ts.map +1 -0
- package/dist/tree-change/followed-walk.js +81 -0
- package/dist/tree-change/followed-walk.js.map +1 -0
- package/dist/tree-change/host-chain.d.ts +48 -0
- package/dist/tree-change/host-chain.d.ts.map +1 -0
- package/dist/tree-change/host-chain.js +85 -0
- package/dist/tree-change/host-chain.js.map +1 -0
- package/dist/tree-change/identity-compare.d.ts +61 -0
- package/dist/tree-change/identity-compare.d.ts.map +1 -0
- package/dist/tree-change/identity-compare.js +115 -0
- package/dist/tree-change/identity-compare.js.map +1 -0
- package/dist/tree-change/identity.d.ts +62 -0
- package/dist/tree-change/identity.d.ts.map +1 -0
- package/dist/tree-change/identity.js +152 -0
- package/dist/tree-change/identity.js.map +1 -0
- package/dist/tree-change/index.d.ts +24 -0
- package/dist/tree-change/index.d.ts.map +1 -0
- package/dist/tree-change/index.js +14 -0
- package/dist/tree-change/index.js.map +1 -0
- package/dist/tree-change/link-chain.d.ts +180 -0
- package/dist/tree-change/link-chain.d.ts.map +1 -0
- package/dist/tree-change/link-chain.js +377 -0
- package/dist/tree-change/link-chain.js.map +1 -0
- package/dist/tree-change/link-containment.d.ts +105 -0
- package/dist/tree-change/link-containment.d.ts.map +1 -0
- package/dist/tree-change/link-containment.js +196 -0
- package/dist/tree-change/link-containment.js.map +1 -0
- package/dist/tree-change/link-walk.d.ts +49 -0
- package/dist/tree-change/link-walk.d.ts.map +1 -0
- package/dist/tree-change/link-walk.js +69 -0
- package/dist/tree-change/link-walk.js.map +1 -0
- package/dist/tree-change/plan.d.ts +206 -0
- package/dist/tree-change/plan.d.ts.map +1 -0
- package/dist/tree-change/plan.js +320 -0
- package/dist/tree-change/plan.js.map +1 -0
- package/dist/tree-change/readable-tree.d.ts +44 -0
- package/dist/tree-change/readable-tree.d.ts.map +1 -0
- package/dist/tree-change/readable-tree.js +52 -0
- package/dist/tree-change/readable-tree.js.map +1 -0
- package/dist/tree-change/rollback-error.d.ts +29 -0
- package/dist/tree-change/rollback-error.d.ts.map +1 -0
- package/dist/tree-change/rollback-error.js +31 -0
- package/dist/tree-change/rollback-error.js.map +1 -0
- package/dist/tree-change/staging-names.d.ts +27 -0
- package/dist/tree-change/staging-names.d.ts.map +1 -0
- package/dist/tree-change/staging-names.js +40 -0
- package/dist/tree-change/staging-names.js.map +1 -0
- package/dist/tree-change/tree-walk.d.ts +81 -0
- package/dist/tree-change/tree-walk.d.ts.map +1 -0
- package/dist/tree-change/tree-walk.js +212 -0
- package/dist/tree-change/tree-walk.js.map +1 -0
- package/dist/win32-namespace.d.ts +24 -0
- package/dist/win32-namespace.d.ts.map +1 -0
- package/dist/win32-namespace.js +31 -0
- package/dist/win32-namespace.js.map +1 -0
- package/dist/yaml/surgical-yaml.d.ts +12 -3
- package/dist/yaml/surgical-yaml.d.ts.map +1 -1
- package/dist/yaml/surgical-yaml.js +16 -6
- package/dist/yaml/surgical-yaml.js.map +1 -1
- package/eslint/README.md +12 -4
- package/eslint/rules/fs-binding-scan.cjs +278 -0
- package/eslint/rules/no-adhoc-errno.cjs +364 -0
- package/eslint/rules/no-bare-symlink-in-tests.cjs +2 -3
- package/eslint/rules/no-blind-catch.cjs +1 -1
- package/eslint/rules/no-destructive-fs.cjs +359 -0
- package/eslint/rules/no-existssync.cjs +70 -0
- package/eslint/rules/no-fragile-entrypoint-guard.cjs +3 -2
- package/eslint/rules/no-link-following-fs.cjs +68 -0
- package/eslint/rules/no-literal-process-exit.cjs +3 -16
- package/eslint/rules/no-literal-test-timeout.cjs +214 -0
- package/eslint/rules/no-raw-signal-listener.cjs +224 -0
- package/eslint/rules/no-stdout-outside-writer.cjs +135 -0
- package/package.json +10 -2
- package/dist/errors/errno.d.ts +0 -51
- package/dist/errors/errno.d.ts.map +0 -1
- package/dist/errors/errno.js +0 -112
- package/dist/errors/errno.js.map +0 -1
- package/eslint/rules/no-fs-promises-cp.cjs +0 -43
|
@@ -0,0 +1,359 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ESLint rule: no-destructive-fs
|
|
3
|
+
*
|
|
4
|
+
* Bans every recursive remove, rename and copy of a filesystem tree outside the one
|
|
5
|
+
* module that owns them. A hand-rolled `rm` + `rename` + `cp` sequence is a private
|
|
6
|
+
* swap protocol, and each one decided differently what happens when the disk fills, a
|
|
7
|
+
* file is unreadable, a name differs only by case, or the process dies between two
|
|
8
|
+
* steps. The tree-change primitive (`planTreeChanges` / `applyTreePlan`, plus
|
|
9
|
+
* `replaceFile`, `renameFileAtomic`, `withTempDir`, `disposeTempDir`, `copyTree`) is
|
|
10
|
+
* the single protocol; everything else asks it.
|
|
11
|
+
*
|
|
12
|
+
* The rule judges REFERENCES to a banned function, not only calls, using the scope
|
|
13
|
+
* manager. A binding is banned when it comes from `node:fs` / `node:fs/promises` (or
|
|
14
|
+
* the unprefixed names) by named import (`import { rmSync as r }`), by destructuring a
|
|
15
|
+
* namespace (`const { rmSync } = fs`, `const { promises: { rm } } = fs`), or by a
|
|
16
|
+
* dynamic `import()` / `require()`; a namespace is a default or namespace import, a
|
|
17
|
+
* `promises` binding, `fs.promises`, an alias of one, or a dynamic `import()` /
|
|
18
|
+
* `require()`. A local that shadows an imported name is not the fs function.
|
|
19
|
+
*
|
|
20
|
+
* - `recursiveRm` — `rm` / `rmSync`, unless the reference is a call whose options are
|
|
21
|
+
* provably non-recursive: an object literal with no `recursive` key or `recursive:
|
|
22
|
+
* false`. An identifier, a call, a spread (`rmSync(...args)`), a computed key, a
|
|
23
|
+
* non-literal value, or a use as a value (`names.map(rmSync)`, `.call`, `.apply`)
|
|
24
|
+
* counts as recursive. `rm(file)` and `rm(file, { force: true })` are one file.
|
|
25
|
+
* - `rmdir`, `rename`, `cp`, `copyFile` — every reference: a call, a value use
|
|
26
|
+
* (`names.map(fs.renameSync)`, `fs['renameSync']`), `.call` / `.apply` / `.bind`, a
|
|
27
|
+
* re-export (`export { renameSync } from 'node:fs'`). A file rename goes through
|
|
28
|
+
* `renameFileAtomic`, which owns the Windows retry.
|
|
29
|
+
* - `reexport` — `export * from 'node:fs'`, which hands every banned function on.
|
|
30
|
+
*
|
|
31
|
+
* `unlink` is allowed: removing one file is not a tree change.
|
|
32
|
+
*
|
|
33
|
+
* Exempt: test files, and the entries of `exemptFiles`. An entry ending in `/` is a
|
|
34
|
+
* DIRECTORY (anchored at a path-segment boundary: the primitive's directory, the test
|
|
35
|
+
* infrastructure that lives in `src`); any other entry is one file. A bare name (`x.ts`
|
|
36
|
+
* or `tree-change/`) is reported as unanchored, because it would exempt that name in
|
|
37
|
+
* every package. No default exemption ships, so a consumer declares its own.
|
|
38
|
+
*/
|
|
39
|
+
|
|
40
|
+
const {
|
|
41
|
+
EXEMPT_FILES_SCHEMA,
|
|
42
|
+
UNANCHORED_EXEMPT_FILE,
|
|
43
|
+
UNANCHORED_EXEMPT_MESSAGE,
|
|
44
|
+
createExemptDirectoryMatcher,
|
|
45
|
+
createExemptPathMatcher,
|
|
46
|
+
isTestFile,
|
|
47
|
+
normalizeForMatch,
|
|
48
|
+
reportUnanchoredExemptEntries,
|
|
49
|
+
} = require('./exempt-path-matcher.cjs');
|
|
50
|
+
|
|
51
|
+
const FS_MODULES = new Set(['fs', 'node:fs', 'fs/promises', 'node:fs/promises']);
|
|
52
|
+
|
|
53
|
+
/** `fs` function name -> the messageId that reports it. `rm` is judged by its options. */
|
|
54
|
+
const MESSAGE_FOR = new Map([
|
|
55
|
+
['rm', 'recursiveRm'],
|
|
56
|
+
['rmSync', 'recursiveRm'],
|
|
57
|
+
['rmdir', 'rmdir'],
|
|
58
|
+
['rmdirSync', 'rmdir'],
|
|
59
|
+
['rename', 'rename'],
|
|
60
|
+
['renameSync', 'rename'],
|
|
61
|
+
['cp', 'cp'],
|
|
62
|
+
['cpSync', 'cp'],
|
|
63
|
+
['copyFile', 'copyFile'],
|
|
64
|
+
['copyFileSync', 'copyFile'],
|
|
65
|
+
]);
|
|
66
|
+
|
|
67
|
+
const UNANCHORED_EXEMPT_DIRECTORY = 'unanchoredExemptDirectory';
|
|
68
|
+
const FUNCTION_TYPES = new Set(['FunctionExpression', 'ArrowFunctionExpression']);
|
|
69
|
+
/** Wrappers that pass a value through unchanged. */
|
|
70
|
+
const TRANSPARENT_TYPES = new Set(['AwaitExpression', 'TSAsExpression', 'TSNonNullExpression', 'TSSatisfiesExpression']);
|
|
71
|
+
|
|
72
|
+
/** The string a property key or member property spells, or undefined when not static. */
|
|
73
|
+
function staticName(node, computed) {
|
|
74
|
+
if (!computed) {
|
|
75
|
+
return node.type === 'Identifier' ? node.name : undefined;
|
|
76
|
+
}
|
|
77
|
+
return node.type === 'Literal' && typeof node.value === 'string' ? node.value : undefined;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** True when `node` is a string literal naming an fs module. */
|
|
81
|
+
function isFsModuleLiteral(node) {
|
|
82
|
+
return node?.type === 'Literal' && FS_MODULES.has(node.value);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* True when the `rm` options (the call's arguments) are, or may be, recursive. Only an
|
|
87
|
+
* object literal that provably does not turn recursion on is safe.
|
|
88
|
+
*/
|
|
89
|
+
function mayBeRecursive(args) {
|
|
90
|
+
if (args.slice(0, 2).some((argument) => argument.type === 'SpreadElement')) {
|
|
91
|
+
return true;
|
|
92
|
+
}
|
|
93
|
+
const options = args[1];
|
|
94
|
+
if (options === undefined || FUNCTION_TYPES.has(options.type)) {
|
|
95
|
+
return false;
|
|
96
|
+
}
|
|
97
|
+
if (options.type !== 'ObjectExpression') {
|
|
98
|
+
return true;
|
|
99
|
+
}
|
|
100
|
+
return options.properties.some((property) => {
|
|
101
|
+
if (property.type !== 'Property') {
|
|
102
|
+
return true;
|
|
103
|
+
}
|
|
104
|
+
const key = staticName(property.key, property.computed);
|
|
105
|
+
if (key === undefined) {
|
|
106
|
+
return true;
|
|
107
|
+
}
|
|
108
|
+
return key === 'recursive' && !(property.value.type === 'Literal' && property.value.value === false);
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** `require('fs')` or `import('fs')`: an expression whose value is an fs namespace (a promise, for import). */
|
|
113
|
+
function isDynamicFsSource(node) {
|
|
114
|
+
if (node.type === 'ImportExpression') {
|
|
115
|
+
return isFsModuleLiteral(node.source);
|
|
116
|
+
}
|
|
117
|
+
return (
|
|
118
|
+
node.type === 'CallExpression' &&
|
|
119
|
+
node.callee.type === 'Identifier' &&
|
|
120
|
+
node.callee.name === 'require' &&
|
|
121
|
+
isFsModuleLiteral(node.arguments[0])
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** The directory entries of `exemptFiles` that name no parent: `tree-change/`. */
|
|
126
|
+
function bareDirectoryEntries(exemptFiles) {
|
|
127
|
+
return exemptFiles.filter((entry) => {
|
|
128
|
+
const normalized = normalizeForMatch(entry).replace(/^\/+/, '');
|
|
129
|
+
return normalized.endsWith('/') && !normalized.slice(0, -1).includes('/');
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** Match each exemption entry to the matcher its shape asks for. */
|
|
134
|
+
function createExemptMatcher(exemptFiles) {
|
|
135
|
+
const normalized = exemptFiles.map((entry) => normalizeForMatch(entry));
|
|
136
|
+
const isUnderDirectory = createExemptDirectoryMatcher(normalized.filter((entry) => entry.endsWith('/')));
|
|
137
|
+
const isFile = createExemptPathMatcher(normalized.filter((entry) => !entry.endsWith('/')));
|
|
138
|
+
return (filename) => isUnderDirectory(filename) || isFile(filename);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Judges every use of the fs names in one file, through the scope manager.
|
|
143
|
+
*
|
|
144
|
+
* `namespaces` holds the variables whose value is an fs namespace; `banned` maps a
|
|
145
|
+
* variable to the fs function name it holds. Both grow as destructures and aliases are
|
|
146
|
+
* followed, and each variable is judged once.
|
|
147
|
+
*/
|
|
148
|
+
class FsUseJudge {
|
|
149
|
+
constructor(context) {
|
|
150
|
+
this.context = context;
|
|
151
|
+
this.sourceCode = context.sourceCode;
|
|
152
|
+
this.namespaces = new Set();
|
|
153
|
+
this.banned = new Map();
|
|
154
|
+
this.pending = [];
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/** Seed from the imports and the dynamic sources, then follow every binding to a fixed point. */
|
|
158
|
+
run(programNode, dynamicSources) {
|
|
159
|
+
for (const statement of programNode.body) {
|
|
160
|
+
if (statement.type === 'ImportDeclaration' && isFsModuleLiteral(statement.source)) {
|
|
161
|
+
for (const specifier of statement.specifiers) this.seedImport(specifier);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
for (const source of dynamicSources) this.useNamespace(source);
|
|
165
|
+
while (this.pending.length > 0) {
|
|
166
|
+
this.followReferences(this.pending.pop());
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
seedImport(specifier) {
|
|
171
|
+
const variable = this.sourceCode.getDeclaredVariables(specifier)[0];
|
|
172
|
+
const imported = specifier.type === 'ImportSpecifier' ? (specifier.imported.name ?? specifier.imported.value) : undefined;
|
|
173
|
+
if (imported === undefined || imported === 'promises') {
|
|
174
|
+
this.markNamespace(variable);
|
|
175
|
+
} else if (MESSAGE_FOR.has(imported)) {
|
|
176
|
+
this.markBanned(variable, imported);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
followReferences(variable) {
|
|
181
|
+
const isNamespace = this.namespaces.has(variable);
|
|
182
|
+
for (const reference of variable.references.filter((candidate) => !candidate.init)) {
|
|
183
|
+
if (isNamespace) {
|
|
184
|
+
this.useNamespace(reference.identifier);
|
|
185
|
+
} else {
|
|
186
|
+
this.judgeBanned(reference.identifier, this.banned.get(variable));
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
markNamespace(variable) {
|
|
192
|
+
if (variable && !this.namespaces.has(variable)) {
|
|
193
|
+
this.namespaces.add(variable);
|
|
194
|
+
this.pending.push(variable);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
markBanned(variable, name) {
|
|
199
|
+
if (variable && !this.banned.has(variable)) {
|
|
200
|
+
this.banned.set(variable, name);
|
|
201
|
+
this.pending.push(variable);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
variableNamed(declaration, name) {
|
|
206
|
+
return this.sourceCode.getDeclaredVariables(declaration).find((variable) => variable.name === name);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/** Bind the names a destructuring pattern takes from an fs namespace. */
|
|
210
|
+
bindPattern(pattern, declaration) {
|
|
211
|
+
if (pattern.type === 'Identifier') {
|
|
212
|
+
this.markNamespace(this.variableNamed(declaration, pattern.name));
|
|
213
|
+
} else if (pattern.type === 'ObjectPattern') {
|
|
214
|
+
for (const property of pattern.properties) this.bindProperty(property, declaration);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
bindProperty(property, declaration) {
|
|
219
|
+
if (property.type === 'RestElement') {
|
|
220
|
+
this.bindPattern(property.argument, declaration);
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
const key = staticName(property.key, property.computed);
|
|
224
|
+
const target = property.value.type === 'AssignmentPattern' ? property.value.left : property.value;
|
|
225
|
+
if (key === 'promises') {
|
|
226
|
+
this.bindPattern(target, declaration);
|
|
227
|
+
} else if (MESSAGE_FOR.has(key) && target.type === 'Identifier') {
|
|
228
|
+
this.markBanned(this.variableNamed(declaration, target.name), key);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/** A banned function reached as `node`: report anything but a provably non-recursive `rm` call. */
|
|
233
|
+
judgeBanned(node, name) {
|
|
234
|
+
const parent = node.parent;
|
|
235
|
+
const isCall = parent.type === 'CallExpression' && parent.callee === node;
|
|
236
|
+
const isRm = MESSAGE_FOR.get(name) === 'recursiveRm';
|
|
237
|
+
if (isCall && isRm && !mayBeRecursive(parent.arguments)) {
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
this.context.report({ node, messageId: MESSAGE_FOR.get(name), data: { name } });
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/** `node` evaluates to an fs namespace: follow what its parent does with it. */
|
|
244
|
+
useNamespace(node) {
|
|
245
|
+
let current = node;
|
|
246
|
+
while (TRANSPARENT_TYPES.has(current.parent?.type)) {
|
|
247
|
+
current = current.parent;
|
|
248
|
+
}
|
|
249
|
+
const parent = current.parent;
|
|
250
|
+
if (parent?.type === 'MemberExpression' && parent.object === current) {
|
|
251
|
+
this.useMember(parent);
|
|
252
|
+
} else if (parent?.type === 'VariableDeclarator' && parent.init === current) {
|
|
253
|
+
this.bindPattern(parent.id, parent);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
useMember(member) {
|
|
258
|
+
const name = staticName(member.property, member.computed);
|
|
259
|
+
if (name === 'promises') {
|
|
260
|
+
this.useNamespace(member);
|
|
261
|
+
} else if (MESSAGE_FOR.has(name)) {
|
|
262
|
+
this.judgeBanned(member, name);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
module.exports = {
|
|
268
|
+
meta: {
|
|
269
|
+
type: 'problem',
|
|
270
|
+
docs: {
|
|
271
|
+
description:
|
|
272
|
+
'Ban recursive rm, rmdir, rename, cp and copyFile outside the tree-change primitive: every tree change goes through one plan/apply protocol',
|
|
273
|
+
category: 'Filesystem and process',
|
|
274
|
+
bans: 'recursive `rm`/`rmSync`, `rmdir`, `rename`, `cp` and `copyFile` from `node:fs` / `node:fs/promises` (and their `Sync` forms), called or passed as values',
|
|
275
|
+
useInstead: '`planTreeChanges()` / `applyTreePlan()`, `renameFileAtomic()`, `replaceFile()`, `copyTree()`, `withTempDir()` / `disposeTempDir()`',
|
|
276
|
+
// Not in `recommended`: the remedy names this repo's own tree-change primitive,
|
|
277
|
+
// which an adopter does not have. VAT enables it explicitly, naming the
|
|
278
|
+
// primitive's directory and the test infrastructure that lives in `src`.
|
|
279
|
+
recommended: false,
|
|
280
|
+
recommendedSeverity: 'error',
|
|
281
|
+
},
|
|
282
|
+
fixable: null,
|
|
283
|
+
schema: [EXEMPT_FILES_SCHEMA],
|
|
284
|
+
messages: {
|
|
285
|
+
recursiveRm:
|
|
286
|
+
'{{name}} removes a tree, or may (its options are not a literal that rules recursion out). ' +
|
|
287
|
+
'Plan the removal with planTreeChanges() / applyTreePlan(), or dispose a temp dir with withTempDir() / disposeTempDir(). ' +
|
|
288
|
+
'Removing one file is fine: rm(file, { force: true }) or unlink().',
|
|
289
|
+
rmdir: '{{name}} is a tree removal. Use planTreeChanges() / applyTreePlan() (op: remove) or disposeTempDir().',
|
|
290
|
+
rename:
|
|
291
|
+
'{{name}} is half a swap protocol. Replace a tree with planTreeChanges() / applyTreePlan(); ' +
|
|
292
|
+
'rename a single file with renameFileAtomic(), which owns the Windows retry.',
|
|
293
|
+
cp: '{{name}} copies a tree outside the primitive. Use copyTree(), or a replace change in planTreeChanges().',
|
|
294
|
+
copyFile: '{{name}} copies a file outside the primitive. Use copyTree() or replaceFile().',
|
|
295
|
+
reexport: 'Re-exporting every node:fs function hands on rm, rename, cp and copyFile. Export the names you need.',
|
|
296
|
+
[UNANCHORED_EXEMPT_FILE]: UNANCHORED_EXEMPT_MESSAGE,
|
|
297
|
+
[UNANCHORED_EXEMPT_DIRECTORY]:
|
|
298
|
+
'exemptFiles entry "{{entry}}" is a bare directory name, so it exempts EVERY directory named ' +
|
|
299
|
+
'"{{entry}}" in the repo — including ones added later. Give the repo-relative path instead ' +
|
|
300
|
+
'(e.g. "packages/utils/src/{{entry}}").',
|
|
301
|
+
},
|
|
302
|
+
},
|
|
303
|
+
|
|
304
|
+
create(context) {
|
|
305
|
+
const filename = context.getFilename();
|
|
306
|
+
const configured = context.options?.[0]?.exemptFiles;
|
|
307
|
+
const exemptFiles = Array.isArray(configured) ? configured : [];
|
|
308
|
+
const isExempt = createExemptMatcher(exemptFiles);
|
|
309
|
+
|
|
310
|
+
/** Surface a malformed exemption list even for an exempt file: it may be exempt only BECAUSE of it. */
|
|
311
|
+
const reportExemptions = (node) => {
|
|
312
|
+
reportUnanchoredExemptEntries(context, node);
|
|
313
|
+
for (const entry of bareDirectoryEntries(exemptFiles)) {
|
|
314
|
+
context.report({ node, messageId: UNANCHORED_EXEMPT_DIRECTORY, data: { entry } });
|
|
315
|
+
}
|
|
316
|
+
};
|
|
317
|
+
|
|
318
|
+
if (isExempt(filename)) {
|
|
319
|
+
return { Program: reportExemptions };
|
|
320
|
+
}
|
|
321
|
+
if (isTestFile(filename)) {
|
|
322
|
+
return {};
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
const dynamicSources = [];
|
|
326
|
+
|
|
327
|
+
return {
|
|
328
|
+
Program: reportExemptions,
|
|
329
|
+
|
|
330
|
+
'CallExpression, ImportExpression'(node) {
|
|
331
|
+
if (isDynamicFsSource(node)) {
|
|
332
|
+
dynamicSources.push(node);
|
|
333
|
+
}
|
|
334
|
+
},
|
|
335
|
+
|
|
336
|
+
ExportNamedDeclaration(node) {
|
|
337
|
+
if (!isFsModuleLiteral(node.source)) {
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
340
|
+
for (const specifier of node.specifiers) {
|
|
341
|
+
const name = specifier.local.name ?? specifier.local.value;
|
|
342
|
+
if (MESSAGE_FOR.has(name)) {
|
|
343
|
+
context.report({ node: specifier, messageId: MESSAGE_FOR.get(name), data: { name } });
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
},
|
|
347
|
+
|
|
348
|
+
ExportAllDeclaration(node) {
|
|
349
|
+
if (isFsModuleLiteral(node.source)) {
|
|
350
|
+
context.report({ node, messageId: 'reexport' });
|
|
351
|
+
}
|
|
352
|
+
},
|
|
353
|
+
|
|
354
|
+
'Program:exit'(node) {
|
|
355
|
+
new FsUseJudge(context).run(node, dynamicSources);
|
|
356
|
+
},
|
|
357
|
+
};
|
|
358
|
+
},
|
|
359
|
+
};
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ESLint rule: no-existssync
|
|
3
|
+
*
|
|
4
|
+
* In product source, flags every use of `existsSync` from `node:fs` / `fs`:
|
|
5
|
+
* a named import (aliased too) that is called or handed on as a value, a
|
|
6
|
+
* namespace, default, `require()` or `await import()` binding read as
|
|
7
|
+
* `fs.existsSync`, the same read straight off the loading expression
|
|
8
|
+
* (`require('node:fs').existsSync`, `(await import('node:fs')).existsSync`), a
|
|
9
|
+
* destructure of it out of any of those, and a re-export.
|
|
10
|
+
*
|
|
11
|
+
* `existsSync` answers `false` for `ENOENT` — and equally for `EACCES` (a
|
|
12
|
+
* parent the process may not search), `ELOOP` and every other errno. So a
|
|
13
|
+
* path the OS would not let VAT look at reads as "absent", and the run goes on
|
|
14
|
+
* as if it were: a LICENSE is skipped, a manifest is "not found", a plugin
|
|
15
|
+
* directory is missing. The replacement asks the one errno classifier:
|
|
16
|
+
* `pathPresent(path, mode, sideOf, absence)` from `@vibe-agent-toolkit/utils` answers
|
|
17
|
+
* `false` only for an absence, and throws every other errno as a classified
|
|
18
|
+
* `FS_FAULT` on the caller's side.
|
|
19
|
+
*
|
|
20
|
+
* ## Scope
|
|
21
|
+
*
|
|
22
|
+
* Product source: a file under `packages/<pkg>/src/` that is not itself a test
|
|
23
|
+
* file. Tests and test-support (`packages/<pkg>/test/`) are not linted — a
|
|
24
|
+
* fixture asking whether its own scratch file exists is not a run deciding
|
|
25
|
+
* what to do. Like `no-io-in-unit-tier`, the layout is this repo's, which is
|
|
26
|
+
* why the rule is not in `configs.recommended`.
|
|
27
|
+
*
|
|
28
|
+
* ## The floor — what this rule cannot see
|
|
29
|
+
*
|
|
30
|
+
* It follows bindings through scope, not values through data flow:
|
|
31
|
+
* - an fs namespace copied first (`const f = fs; f.existsSync(p)`) or passed
|
|
32
|
+
* as an argument;
|
|
33
|
+
* - `import('node:fs').then((fs) => fs.existsSync(p))`, and a `require`
|
|
34
|
+
* built by `createRequire`;
|
|
35
|
+
* - a project module that re-exports it under another name is caught at its
|
|
36
|
+
* own `export { existsSync } from 'node:fs'`, not at the importers of that
|
|
37
|
+
* module.
|
|
38
|
+
*
|
|
39
|
+
* Option `allowFiles: string[]` — repo-relative paths of today's offenders,
|
|
40
|
+
* the ratchet. Name files, never directories.
|
|
41
|
+
*
|
|
42
|
+
* @example
|
|
43
|
+
* // BAD — an EACCES parent reads as "no manifest"
|
|
44
|
+
* if (!existsSync(manifestPath)) return emptyInventory();
|
|
45
|
+
*
|
|
46
|
+
* // GOOD — absent is false; a refused stat throws a classified FS_FAULT
|
|
47
|
+
* if (!pathPresent(manifestPath, { follow: pluginDir }, everywhere('source'), 'probe')) return emptyInventory();
|
|
48
|
+
*/
|
|
49
|
+
|
|
50
|
+
'use strict';
|
|
51
|
+
|
|
52
|
+
const { productSourceFsBan } = require('./fs-binding-scan.cjs');
|
|
53
|
+
|
|
54
|
+
module.exports = productSourceFsBan({
|
|
55
|
+
docs: {
|
|
56
|
+
description:
|
|
57
|
+
'Disallow existsSync from node:fs in product source — it answers false for a path it could not ' +
|
|
58
|
+
'look at (EACCES, ELOOP) exactly as for one that is not there',
|
|
59
|
+
bans: '`existsSync()` from `node:fs` — called, handed on, destructured or re-exported — under `packages/*/src/`',
|
|
60
|
+
useInstead: '`pathPresent(path, mode, sideOf, absence)` (absent only on an absence; every other errno a classified `FS_FAULT`)',
|
|
61
|
+
},
|
|
62
|
+
messageId: 'existsSync',
|
|
63
|
+
message:
|
|
64
|
+
'existsSync answers false for a path it could not look at (EACCES, ELOOP) exactly as for one that is ' +
|
|
65
|
+
'not there, so "could not look" goes on as "absent". Use pathPresent(path, mode, sideOf, absence) from ' +
|
|
66
|
+
'@vibe-agent-toolkit/utils: false only for an absence, every other errno a classified FS_FAULT.',
|
|
67
|
+
modules: new Set(['node:fs', 'fs']),
|
|
68
|
+
banned: new Set(['existsSync']),
|
|
69
|
+
untyped: null,
|
|
70
|
+
});
|
|
@@ -25,7 +25,8 @@
|
|
|
25
25
|
* every gate:
|
|
26
26
|
*
|
|
27
27
|
* - `import.meta.main` was added in Node **24.2 / 22.18**. Measured on the exact
|
|
28
|
-
* floor this repo declares — `>=22.13.0
|
|
28
|
+
* floor this repo declares — `>=22.16.0`, and on 22.13.0 before it — the
|
|
29
|
+
* property is `undefined`:
|
|
29
30
|
*
|
|
30
31
|
* ```
|
|
31
32
|
* $ node-v22.13.0 --input-type=module -e "console.log(import.meta.main)" -> undefined
|
|
@@ -186,7 +187,7 @@ module.exports = {
|
|
|
186
187
|
// Node floor rather than a portable fact. `import.meta.main` shipped in Node
|
|
187
188
|
// 24.2 / 22.18; an adopter whose floor is at or above that writes it
|
|
188
189
|
// correctly and would get a finding they cannot act on. This package's own
|
|
189
|
-
// floor spans 22.
|
|
190
|
+
// floor spans 22.16–22.17 where the property is `undefined`, so the hazard
|
|
190
191
|
// is real for some adopters and absent for others, and only they know which.
|
|
191
192
|
// The other half (`rawEntrypointCompare`) has no such dependency — a raw
|
|
192
193
|
// string compare misses a symlinked entry on every Node there has ever been
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ESLint rule: no-link-following-fs
|
|
3
|
+
*
|
|
4
|
+
* In product source, flags every use of an fs call that FOLLOWS a symbolic link —
|
|
5
|
+
* `stat`, `statSync`, `existsSync`, `realpath`, `realpathSync` (and their
|
|
6
|
+
* `.native` forms), from `node:fs`, `fs`, `node:fs/promises` or `fs/promises`,
|
|
7
|
+
* however reached (see `fs-binding-scan.cjs`).
|
|
8
|
+
*
|
|
9
|
+
* A Windows kernel asked to follow a link to `\\host\share` opens an SMB session
|
|
10
|
+
* and offers the operator's NTLM credentials to that host before any answer comes
|
|
11
|
+
* back. So the kernel may be asked to follow a link only once the chain to it has
|
|
12
|
+
* been read by its text, hop by hop, and shown local — which the one guarded module
|
|
13
|
+
* does (`@vibe-agent-toolkit/utils` `followedStatSync`, `followedKindSync`,
|
|
14
|
+
* `realpathNativeSync`, `pathPresent(path, { follow: within }, …)`). Three rounds of
|
|
15
|
+
* hand sweeps for "the sites that resolve links" each missed sites; this rule is
|
|
16
|
+
* the sweep.
|
|
17
|
+
*
|
|
18
|
+
* `symlink` and `symlinkSync` are flagged unless CALLED with a third argument (the
|
|
19
|
+
* link's type): with none, Node on Windows `stat`s the target to pick `file` or `dir`,
|
|
20
|
+
* so creating a link is a follow of its unread text. The type is read syntactically —
|
|
21
|
+
* a callback passed by name in the third place reads as a type.
|
|
22
|
+
*
|
|
23
|
+
* `lstat`, `fstat`, a file handle's `stat()` and `readlink` follow nothing and are
|
|
24
|
+
* not flagged. Opening or reading a file through a link is outside this rule.
|
|
25
|
+
*
|
|
26
|
+
* ## Scope
|
|
27
|
+
*
|
|
28
|
+
* Product source: a file under `packages/<pkg>/src/` that is not itself a test
|
|
29
|
+
* file — the layout is this repo's, which is why the rule is not in
|
|
30
|
+
* `configs.recommended`.
|
|
31
|
+
*
|
|
32
|
+
* Option `allowFiles: string[]` — repo-relative paths: the guarded module itself,
|
|
33
|
+
* and files whose every such call acts on the operator's own path, each with its
|
|
34
|
+
* reason beside it in the config. The repo's config derives the list from
|
|
35
|
+
* `LINK_FOLLOWING_FS_CALLS`, which pins how many calls each listed file holds;
|
|
36
|
+
* `dev-tools/test/eslint-allowlist-ratchets.test.ts` asserts that count exactly —
|
|
37
|
+
* one more call in a listed file is red, and so is one fewer.
|
|
38
|
+
*/
|
|
39
|
+
|
|
40
|
+
'use strict';
|
|
41
|
+
|
|
42
|
+
const { productSourceFsBan } = require('./fs-binding-scan.cjs');
|
|
43
|
+
|
|
44
|
+
module.exports = productSourceFsBan({
|
|
45
|
+
docs: {
|
|
46
|
+
description:
|
|
47
|
+
'Disallow fs calls that follow a symbolic link (stat, existsSync, realpath) in product source outside the ' +
|
|
48
|
+
'guarded module — the kernel would follow a link no walk has read',
|
|
49
|
+
bans: '`stat`/`statSync`/`existsSync`/`realpath`/`realpathSync`, and `symlink`/`symlinkSync` called with no type, from `node:fs` or `node:fs/promises` under `packages/*/src/`',
|
|
50
|
+
useInstead: '`followedStatSync`/`followedKindSync`/`realpathNativeSync(path, within)` or `pathPresent(path, { follow: within }, …)`',
|
|
51
|
+
},
|
|
52
|
+
messageId: 'follows',
|
|
53
|
+
message:
|
|
54
|
+
'`{{name}}` follows a symbolic link with no walk of its chain first: on Windows a link to \\\\host\\share ' +
|
|
55
|
+
'sends the operator\'s credentials to that host. Use followedStatSync / followedKindSync / ' +
|
|
56
|
+
'realpathNativeSync(path, within) or pathPresent(path, { follow: within }, …) from @vibe-agent-toolkit/utils.',
|
|
57
|
+
modules: new Set(['node:fs', 'fs', 'node:fs/promises', 'fs/promises']),
|
|
58
|
+
banned: new Set(['stat', 'statSync', 'existsSync', 'realpath', 'realpathSync']),
|
|
59
|
+
untyped: {
|
|
60
|
+
names: new Set(['symlink', 'symlinkSync']),
|
|
61
|
+
messageId: 'untypedLink',
|
|
62
|
+
message:
|
|
63
|
+
'`{{name}}` with no type has Node on Windows `stat` the target to pick `file` or `dir` — the kernel ' +
|
|
64
|
+
'follows a link text no walk has read, and a text naming a network share sends the operator\'s ' +
|
|
65
|
+
'credentials to the share\'s host. Pass the type, decided from what VAT already read — for a link being ' +
|
|
66
|
+
'copied, from its chain read by text (`copyTree` asks `linkCopyType` in utils `link-following.ts`).',
|
|
67
|
+
},
|
|
68
|
+
});
|
|
@@ -35,9 +35,8 @@
|
|
|
35
35
|
* `ExitCode.ERROR`, or a call to one of `derived.calls` (the derivations:
|
|
36
36
|
* `exitCodeForReport`, `exitCodeOfChild`, …). A ternary, a variable or a
|
|
37
37
|
* forwarded field is a code decided somewhere this rule cannot see.
|
|
38
|
-
* - `derived.
|
|
39
|
-
*
|
|
40
|
-
* error (`staleLegacy`), so the list can only shrink and never holds a dead entry.
|
|
38
|
+
* - No file under `derived.paths` is exempt: the migration ratchet that once
|
|
39
|
+
* listed the undelivered verbs reached zero and went, and the option with it.
|
|
41
40
|
*
|
|
42
41
|
* ⚠️ What this cannot see: a document verb ending on a bare `ExitCode.OK` over a
|
|
43
42
|
* document that says `error`. That is a runtime fact, pinned by the exit-code
|
|
@@ -141,9 +140,8 @@ module.exports = {
|
|
|
141
140
|
properties: {
|
|
142
141
|
paths: { type: 'array', items: { type: 'string' }, uniqueItems: true },
|
|
143
142
|
calls: { type: 'array', items: { type: 'string' }, uniqueItems: true },
|
|
144
|
-
legacy: { type: 'array', items: { type: 'string' }, uniqueItems: true },
|
|
145
143
|
},
|
|
146
|
-
required: ['paths', 'calls'
|
|
144
|
+
required: ['paths', 'calls'],
|
|
147
145
|
additionalProperties: false,
|
|
148
146
|
},
|
|
149
147
|
},
|
|
@@ -164,9 +162,6 @@ module.exports = {
|
|
|
164
162
|
exitNotDerived:
|
|
165
163
|
'This exit code is decided here, beside the document, where it can disagree with it. Exit ' +
|
|
166
164
|
'with ExitCode.OK, ExitCode.ERROR, or a derivation: {{calls}}.',
|
|
167
|
-
staleLegacy:
|
|
168
|
-
'This file is listed in no-literal-process-exit derived.legacy but decides no exit code by ' +
|
|
169
|
-
'hand any more. Remove it from the list — the ratchet only shrinks.',
|
|
170
165
|
},
|
|
171
166
|
},
|
|
172
167
|
|
|
@@ -181,14 +176,9 @@ module.exports = {
|
|
|
181
176
|
const inDerived = derived !== undefined
|
|
182
177
|
&& !isTestFile(filename)
|
|
183
178
|
&& createExemptDirectoryMatcher(derived.paths)(filename);
|
|
184
|
-
const isLegacy = inDerived && createExemptPathMatcher(derived.legacy)(filename);
|
|
185
179
|
const derivations = new Set(derived?.calls ?? []);
|
|
186
|
-
let handDecided = 0;
|
|
187
180
|
|
|
188
|
-
/** Report a derivation violation — or, in a legacy file, only count it. */
|
|
189
181
|
function reportDerived(node, messageId) {
|
|
190
|
-
handDecided += 1;
|
|
191
|
-
if (isLegacy) return;
|
|
192
182
|
context.report({ node, messageId, data: { calls: [...derivations].join(', ') } });
|
|
193
183
|
}
|
|
194
184
|
|
|
@@ -222,9 +212,6 @@ module.exports = {
|
|
|
222
212
|
MemberExpression(node) {
|
|
223
213
|
if (inDerived && isExitCodeMember(node, 'FINDINGS')) reportDerived(node, 'findingsNotDerived');
|
|
224
214
|
},
|
|
225
|
-
'Program:exit'(node) {
|
|
226
|
-
if (isLegacy && handDecided === 0) context.report({ node, messageId: 'staleLegacy' });
|
|
227
|
-
},
|
|
228
215
|
};
|
|
229
216
|
},
|
|
230
217
|
};
|