@vibe-agent-toolkit/utils 0.2.0-rc.1 → 0.2.0-rc.10
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 +69 -23
- package/dist/asset-reference.js +3 -11
- package/dist/asset-reference.js.map +1 -1
- package/dist/compare-code-units.d.ts +28 -0
- package/dist/compare-code-units.d.ts.map +1 -0
- package/dist/compare-code-units.js +33 -0
- package/dist/compare-code-units.js.map +1 -0
- package/dist/crawl-timing.d.ts +657 -0
- package/dist/crawl-timing.d.ts.map +1 -0
- package/dist/crawl-timing.js +765 -0
- package/dist/crawl-timing.js.map +1 -0
- package/dist/crawl.d.ts +2 -4
- package/dist/crawl.d.ts.map +1 -1
- package/dist/crawl.js +2 -4
- package/dist/crawl.js.map +1 -1
- package/dist/dirent-kind.d.ts +83 -0
- package/dist/dirent-kind.d.ts.map +1 -0
- package/dist/dirent-kind.js +129 -0
- package/dist/dirent-kind.js.map +1 -0
- package/dist/entrypoint.d.ts +54 -0
- package/dist/entrypoint.d.ts.map +1 -0
- package/dist/entrypoint.js +71 -0
- package/dist/entrypoint.js.map +1 -0
- package/dist/env-flag.d.ts +51 -0
- package/dist/env-flag.d.ts.map +1 -0
- package/dist/env-flag.js +62 -0
- package/dist/env-flag.js.map +1 -0
- package/dist/errors/errno.d.ts +51 -0
- package/dist/errors/errno.d.ts.map +1 -0
- package/dist/errors/errno.js +112 -0
- package/dist/errors/errno.js.map +1 -0
- package/dist/errors/vat-error.d.ts +60 -0
- package/dist/errors/vat-error.d.ts.map +1 -0
- package/dist/errors/vat-error.js +86 -0
- package/dist/errors/vat-error.js.map +1 -0
- package/dist/file-crawler.d.ts +60 -0
- package/dist/file-crawler.d.ts.map +1 -1
- package/dist/file-crawler.js +133 -29
- package/dist/file-crawler.js.map +1 -1
- package/dist/fs/file-hash.d.ts.map +1 -1
- package/dist/fs/file-hash.js +0 -1
- package/dist/fs/file-hash.js.map +1 -1
- package/dist/fs-utils.d.ts +422 -191
- package/dist/fs-utils.d.ts.map +1 -1
- package/dist/fs-utils.js +534 -204
- package/dist/fs-utils.js.map +1 -1
- package/dist/fs.d.ts +5 -2
- package/dist/fs.d.ts.map +1 -1
- package/dist/fs.js +9 -7
- package/dist/fs.js.map +1 -1
- package/dist/git-run.d.ts +167 -0
- package/dist/git-run.d.ts.map +1 -0
- package/dist/git-run.js +154 -0
- package/dist/git-run.js.map +1 -0
- package/dist/git-snapshot.d.ts +199 -0
- package/dist/git-snapshot.d.ts.map +1 -0
- package/dist/git-snapshot.js +250 -0
- package/dist/git-snapshot.js.map +1 -0
- package/dist/git-tracker.d.ts +166 -3
- package/dist/git-tracker.d.ts.map +1 -1
- package/dist/git-tracker.js +250 -22
- package/dist/git-tracker.js.map +1 -1
- package/dist/git-utils.d.ts +84 -1
- package/dist/git-utils.d.ts.map +1 -1
- package/dist/git-utils.js +208 -82
- package/dist/git-utils.js.map +1 -1
- package/dist/git.d.ts +13 -4
- package/dist/git.d.ts.map +1 -1
- package/dist/git.js +12 -4
- package/dist/git.js.map +1 -1
- package/dist/gitignore-checker.d.ts.map +1 -1
- package/dist/gitignore-checker.js +15 -6
- package/dist/gitignore-checker.js.map +1 -1
- package/dist/glob/glob-pattern.js +5 -5
- package/dist/glob/glob-pattern.js.map +1 -1
- package/dist/index.d.ts +32 -20
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +95 -50
- package/dist/index.js.map +1 -1
- package/dist/listing-refusal.d.ts +112 -0
- package/dist/listing-refusal.d.ts.map +1 -0
- package/dist/listing-refusal.js +105 -0
- package/dist/listing-refusal.js.map +1 -0
- package/dist/path-containment.d.ts +64 -0
- package/dist/path-containment.d.ts.map +1 -0
- package/dist/path-containment.js +111 -0
- package/dist/path-containment.js.map +1 -0
- package/dist/path-core.d.ts +113 -18
- package/dist/path-core.d.ts.map +1 -1
- package/dist/path-core.js +139 -23
- package/dist/path-core.js.map +1 -1
- package/dist/path-utils.d.ts.map +1 -1
- package/dist/path-utils.js +42 -47
- package/dist/path-utils.js.map +1 -1
- package/dist/path.d.ts +1 -1
- package/dist/path.d.ts.map +1 -1
- package/dist/path.js +1 -1
- package/dist/path.js.map +1 -1
- package/dist/process.d.ts +7 -0
- package/dist/process.d.ts.map +1 -1
- package/dist/process.js +7 -0
- package/dist/process.js.map +1 -1
- package/dist/project-utils.d.ts.map +1 -1
- package/dist/project-utils.js +15 -9
- package/dist/project-utils.js.map +1 -1
- package/dist/project.d.ts +7 -5
- package/dist/project.d.ts.map +1 -1
- package/dist/project.js +7 -5
- package/dist/project.js.map +1 -1
- package/dist/safe-exec.d.ts +40 -11
- package/dist/safe-exec.d.ts.map +1 -1
- package/dist/safe-exec.js +111 -25
- package/dist/safe-exec.js.map +1 -1
- package/dist/skill-targets.d.ts +2 -2
- package/dist/skill-targets.js +2 -2
- package/dist/skill-test/auth-resolver.d.ts +10 -2
- package/dist/skill-test/auth-resolver.d.ts.map +1 -1
- package/dist/skill-test/auth-resolver.js +26 -11
- package/dist/skill-test/auth-resolver.js.map +1 -1
- package/dist/skill-test/index.d.ts +1 -1
- package/dist/skill-test/index.d.ts.map +1 -1
- package/dist/skill-test/index.js +1 -1
- package/dist/skill-test/index.js.map +1 -1
- package/dist/skill-test/spawn-claude.d.ts +30 -1
- package/dist/skill-test/spawn-claude.d.ts.map +1 -1
- package/dist/skill-test/spawn-claude.js +71 -5
- package/dist/skill-test/spawn-claude.js.map +1 -1
- package/dist/skill-test/transcript.d.ts +41 -2
- package/dist/skill-test/transcript.d.ts.map +1 -1
- package/dist/skill-test/transcript.js +55 -10
- package/dist/skill-test/transcript.js.map +1 -1
- package/dist/spawn-hardened.js +0 -1
- package/dist/spawn-hardened.js.map +1 -1
- package/dist/stdio-blocking.js +13 -14
- package/dist/stdio-blocking.js.map +1 -1
- package/dist/test-helpers.d.ts +135 -100
- package/dist/test-helpers.d.ts.map +1 -1
- package/dist/test-helpers.js +224 -146
- package/dist/test-helpers.js.map +1 -1
- package/dist/testing/executables.d.ts +32 -0
- package/dist/testing/executables.d.ts.map +1 -0
- package/dist/testing/executables.js +70 -0
- package/dist/testing/executables.js.map +1 -0
- package/dist/testing/hostile-tree.d.ts +79 -0
- package/dist/testing/hostile-tree.d.ts.map +1 -0
- package/dist/testing/hostile-tree.js +164 -0
- package/dist/testing/hostile-tree.js.map +1 -0
- package/dist/testing/platform-gates.d.ts +31 -0
- package/dist/testing/platform-gates.d.ts.map +1 -0
- package/dist/testing/platform-gates.js +31 -0
- package/dist/testing/platform-gates.js.map +1 -0
- package/dist/testing/temp-dir.d.ts +255 -0
- package/dist/testing/temp-dir.d.ts.map +1 -0
- package/dist/testing/temp-dir.js +363 -0
- package/dist/testing/temp-dir.js.map +1 -0
- package/dist/testing.d.ts +94 -3
- package/dist/testing.d.ts.map +1 -1
- package/dist/testing.js +119 -3
- package/dist/testing.js.map +1 -1
- package/dist/text-content.d.ts +197 -0
- package/dist/text-content.d.ts.map +1 -0
- package/dist/text-content.js +354 -0
- package/dist/text-content.js.map +1 -0
- package/dist/text-file.d.ts +45 -0
- package/dist/text-file.d.ts.map +1 -0
- package/dist/text-file.js +51 -0
- package/dist/text-file.js.map +1 -0
- package/dist/text.d.ts +16 -0
- package/dist/text.d.ts.map +1 -0
- package/dist/text.js +16 -0
- package/dist/text.js.map +1 -0
- package/dist/timing-dump.d.ts +136 -0
- package/dist/timing-dump.d.ts.map +1 -0
- package/dist/timing-dump.js +229 -0
- package/dist/timing-dump.js.map +1 -0
- package/dist/windows-shell.d.ts +0 -1
- package/dist/windows-shell.d.ts.map +1 -1
- package/dist/windows-shell.js +0 -1
- package/dist/windows-shell.js.map +1 -1
- package/dist/zod-introspection.js +2 -2
- package/dist/zod-introspection.js.map +1 -1
- package/eslint/README.md +223 -49
- package/eslint/index.cjs +111 -97
- package/eslint/index.d.cts +16 -6
- package/eslint/rules/callee-name.cjs +23 -0
- package/eslint/rules/commands-import-boundary.cjs +148 -0
- package/eslint/rules/dead-import.cjs +1 -1
- package/eslint/rules/dirent-type-needs-symlink-check.cjs +282 -0
- package/eslint/rules/eslint-rule-factory.cjs +5 -2
- package/eslint/rules/exempt-path-matcher.cjs +2 -1
- package/eslint/rules/explicit-zod-strictness.cjs +120 -0
- package/eslint/rules/no-bare-dynamic-import-path.cjs +5 -1
- package/eslint/rules/no-bare-executable-spawn.cjs +70 -0
- package/eslint/rules/no-bare-symlink-in-tests.cjs +213 -0
- package/eslint/rules/no-blind-catch.cjs +171 -0
- package/eslint/rules/no-child-process-execSync.cjs +8 -0
- package/eslint/rules/no-decaying-referent.cjs +217 -0
- package/eslint/rules/no-dotdot-containment.cjs +160 -0
- package/eslint/rules/no-file-url-string-concat.cjs +4 -1
- package/eslint/rules/no-fragile-entrypoint-guard.cjs +228 -0
- package/eslint/rules/no-fs-mkdirSync.cjs +8 -0
- package/eslint/rules/no-fs-promises-cp.cjs +7 -0
- package/eslint/rules/no-fs-realpathSync.cjs +8 -0
- package/eslint/rules/no-hardcoded-path-split.cjs +29 -18
- package/eslint/rules/no-io-in-unit-tier.cjs +129 -0
- package/eslint/rules/no-literal-process-exit.cjs +129 -0
- package/eslint/rules/no-manual-path-normalize.cjs +136 -107
- package/eslint/rules/no-os-tmpdir.cjs +8 -0
- package/eslint/rules/no-path-operations-in-comparisons.cjs +7 -2
- package/eslint/rules/no-path-sep-in-strings.cjs +7 -2
- package/eslint/rules/no-path-startswith.cjs +7 -2
- package/eslint/rules/no-process-exit-in-phase.cjs +127 -0
- package/eslint/rules/no-raw-node-path.cjs +563 -0
- package/eslint/rules/no-raw-text-decode.cjs +305 -0
- package/eslint/rules/no-registry-count-pin.cjs +255 -0
- package/eslint/rules/no-self-package-import.cjs +168 -0
- package/eslint/rules/no-test-scoped-functions.cjs +10 -3
- package/eslint/rules/no-unix-shell-commands.cjs +6 -2
- package/eslint/rules/no-unsafe-root-join.cjs +23 -2
- package/eslint/rules/no-url-pathname-for-fs.cjs +5 -1
- package/eslint/rules/no-version-literal.cjs +151 -0
- package/eslint/rules/prefer-startswith-over-regex.cjs +9 -0
- package/eslint/rules/require-justified-skip.cjs +8 -2
- package/eslint/rules/safe-import.cjs +1 -1
- package/package.json +16 -16
- package/dist/link-auth/build-headers.d.ts +0 -34
- package/dist/link-auth/build-headers.d.ts.map +0 -1
- package/dist/link-auth/build-headers.js +0 -58
- package/dist/link-auth/build-headers.js.map +0 -1
- package/dist/link-auth/expand-macro.d.ts +0 -38
- package/dist/link-auth/expand-macro.d.ts.map +0 -1
- package/dist/link-auth/expand-macro.js +0 -133
- package/dist/link-auth/expand-macro.js.map +0 -1
- package/dist/link-auth/macros.yaml +0 -50
- package/dist/link-auth/resolve-token.d.ts +0 -83
- package/dist/link-auth/resolve-token.d.ts.map +0 -1
- package/dist/link-auth/resolve-token.js +0 -101
- package/dist/link-auth/resolve-token.js.map +0 -1
- package/dist/link-auth/resolve.d.ts +0 -102
- package/dist/link-auth/resolve.d.ts.map +0 -1
- package/dist/link-auth/resolve.js +0 -66
- package/dist/link-auth/resolve.js.map +0 -1
- package/dist/link-auth/rewrite.d.ts +0 -52
- package/dist/link-auth/rewrite.d.ts.map +0 -1
- package/dist/link-auth/rewrite.js +0 -102
- package/dist/link-auth/rewrite.js.map +0 -1
- package/dist/link-auth/select-provider.d.ts +0 -30
- package/dist/link-auth/select-provider.d.ts.map +0 -1
- package/dist/link-auth/select-provider.js +0 -55
- package/dist/link-auth/select-provider.js.map +0 -1
- package/dist/link-auth/template.d.ts +0 -40
- package/dist/link-auth/template.d.ts.map +0 -1
- package/dist/link-auth/template.js +0 -89
- package/dist/link-auth/template.js.map +0 -1
- package/dist/link-auth/transforms.d.ts +0 -46
- package/dist/link-auth/transforms.d.ts.map +0 -1
- package/dist/link-auth/transforms.js +0 -52
- package/dist/link-auth/transforms.js.map +0 -1
- package/dist/template-entry.d.ts +0 -10
- package/dist/template-entry.d.ts.map +0 -1
- package/dist/template-entry.js +0 -10
- package/dist/template-entry.js.map +0 -1
- package/dist/template.d.ts +0 -7
- package/dist/template.d.ts.map +0 -1
- package/dist/template.js +0 -18
- package/dist/template.js.map +0 -1
- package/eslint/rules/no-path-join.cjs +0 -14
- package/eslint/rules/no-path-relative.cjs +0 -14
- package/eslint/rules/no-path-resolve.cjs +0 -14
- package/eslint/rules/path-function-rule-factory.cjs +0 -496
|
@@ -1,496 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* ESLint Rule Factory for banning specific path functions from node:path
|
|
3
|
-
*
|
|
4
|
-
* Handles both import styles:
|
|
5
|
-
* - Named: import { join } from 'node:path' → join(...)
|
|
6
|
-
* - Default/namespace: import path from 'node:path' → path.join(...)
|
|
7
|
-
*
|
|
8
|
-
* Auto-fixes to safePath.fn() from `@vibe-agent-toolkit/utils/path` — the narrow
|
|
9
|
-
* subpath that owns `safePath`, NOT the barrel. See `safe-import.cjs`.
|
|
10
|
-
*/
|
|
11
|
-
|
|
12
|
-
const {
|
|
13
|
-
DEAD_UNSAFE_IMPORT,
|
|
14
|
-
DEAD_UNSAFE_IMPORT_MESSAGE,
|
|
15
|
-
reportDeadUnsafeImports,
|
|
16
|
-
} = require('./dead-import.cjs');
|
|
17
|
-
const {
|
|
18
|
-
UNANCHORED_EXEMPT_FILE,
|
|
19
|
-
UNANCHORED_EXEMPT_MESSAGE,
|
|
20
|
-
createConfigurableExemptPathMatcher,
|
|
21
|
-
reportUnanchoredExemptEntries,
|
|
22
|
-
} = require('./exempt-path-matcher.cjs');
|
|
23
|
-
const {
|
|
24
|
-
EXEMPT_AND_SAFE_MODULE_SCHEMA,
|
|
25
|
-
SAFE_PATH_MODULE,
|
|
26
|
-
insertAboveWithComments,
|
|
27
|
-
isNameAlreadyBound,
|
|
28
|
-
resolveSafeModule,
|
|
29
|
-
} = require('./safe-import.cjs');
|
|
30
|
-
|
|
31
|
-
const PATH_MODULES = new Set(['node:path', 'path']);
|
|
32
|
-
const SAFE_OBJECT = 'safePath';
|
|
33
|
-
|
|
34
|
-
/**
|
|
35
|
-
* The files allowed to call raw `node:path` functions are whichever ones the
|
|
36
|
-
* CONSUMING repo says implement (or assert) its `safePath` wrappers — declared
|
|
37
|
-
* per-rule as `{ exemptFiles: [...] }`, matched at a path-segment boundary (see
|
|
38
|
-
* `exempt-path-matcher.cjs` for why these are not substrings).
|
|
39
|
-
*
|
|
40
|
-
* This list used to be hardcoded to VAT's own `packages/utils/src/path-core.ts`
|
|
41
|
-
* et al. Those paths are meaningless in an adopter's tree and actively harmful
|
|
42
|
-
* as a default — a same-named file at the same repo-relative path would inherit
|
|
43
|
-
* an exemption it never declared. Default: nothing is exempt.
|
|
44
|
-
*/
|
|
45
|
-
const exemptMatcherFor = createConfigurableExemptPathMatcher();
|
|
46
|
-
|
|
47
|
-
/**
|
|
48
|
-
* Remove a named import specifier, handling comma cleanup.
|
|
49
|
-
*/
|
|
50
|
-
function removeSpecifier(fixer, sourceCode, importNode, spec) {
|
|
51
|
-
if (importNode.specifiers.length === 1) {
|
|
52
|
-
return [fixer.remove(importNode)];
|
|
53
|
-
}
|
|
54
|
-
const comma = sourceCode.getTokenAfter(spec);
|
|
55
|
-
if (comma?.value === ',') {
|
|
56
|
-
return [fixer.removeRange([spec.range[0], comma.range[1]])];
|
|
57
|
-
}
|
|
58
|
-
const commaBefore = sourceCode.getTokenBefore(spec);
|
|
59
|
-
if (commaBefore?.value === ',') {
|
|
60
|
-
return [fixer.removeRange([commaBefore.range[0], spec.range[1]])];
|
|
61
|
-
}
|
|
62
|
-
return [fixer.remove(spec)];
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
/**
|
|
66
|
-
* Track path module specifiers from an import declaration.
|
|
67
|
-
*
|
|
68
|
-
* Two specifier shapes are deliberately NOT tracked, because tracking them is
|
|
69
|
-
* what let the fixer delete them:
|
|
70
|
-
*
|
|
71
|
-
* - **Type-only** (`import { type join, … }` / `import type { join }`). The
|
|
72
|
-
* binding exists only for the type checker; there is no call to rewrite, and
|
|
73
|
-
* removing the specifier silently breaks every `typeof join` that referenced
|
|
74
|
-
* it. `no-undef` cannot see the damage — it is a TYPE reference.
|
|
75
|
-
* - **Aliased** (`import { join as pathJoin }`). The rule never reported
|
|
76
|
-
* `pathJoin(...)` in the first place — `classifyCall` matches on the callee's
|
|
77
|
-
* name — so tracking the specifier bought nothing and cost the whole import:
|
|
78
|
-
* an unrelated unbound `join(` elsewhere in the file made the fixer remove
|
|
79
|
-
* the alias, breaking every working `pathJoin` call site.
|
|
80
|
-
*/
|
|
81
|
-
function trackPathImport(node, unsafeFn, state) {
|
|
82
|
-
if (node.importKind === 'type') {
|
|
83
|
-
return;
|
|
84
|
-
}
|
|
85
|
-
for (const spec of node.specifiers) {
|
|
86
|
-
if (
|
|
87
|
-
spec.type === 'ImportSpecifier' &&
|
|
88
|
-
spec.importKind !== 'type' &&
|
|
89
|
-
spec.imported.name === unsafeFn &&
|
|
90
|
-
spec.local.name === unsafeFn
|
|
91
|
-
) {
|
|
92
|
-
state.namedImportSpec = spec;
|
|
93
|
-
state.namedImportNode = node;
|
|
94
|
-
}
|
|
95
|
-
if (spec.type === 'ImportDefaultSpecifier' || spec.type === 'ImportNamespaceSpecifier') {
|
|
96
|
-
state.defaultImportName = spec.local.name;
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
/**
|
|
102
|
-
* Is `name` re-exported by a bare `export { name }` in this file?
|
|
103
|
-
*
|
|
104
|
-
* Removing the import specifier then leaves the export naming nothing, and the
|
|
105
|
-
* result does not PARSE — `Export 'join' is not defined`. An autofix whose
|
|
106
|
-
* output cannot be parsed is the worst outcome available, so the specifier
|
|
107
|
-
* stays and the call sites are still rewritten. Whatever is left is a lint
|
|
108
|
-
* finding a human can read, not a broken file.
|
|
109
|
-
*/
|
|
110
|
-
function isReExported(sourceCode, name) {
|
|
111
|
-
return sourceCode.ast.body.some(
|
|
112
|
-
(node) =>
|
|
113
|
-
node.type === 'ExportNamedDeclaration' &&
|
|
114
|
-
!node.source &&
|
|
115
|
-
node.specifiers.some((spec) => spec.local?.name === name),
|
|
116
|
-
);
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
/**
|
|
120
|
-
* Track safe module import from an import declaration.
|
|
121
|
-
*/
|
|
122
|
-
function trackSafeImport(node, state) {
|
|
123
|
-
state.safeImportNode = node;
|
|
124
|
-
for (const spec of node.specifiers) {
|
|
125
|
-
if (spec.type === 'ImportSpecifier' && spec.imported.name === SAFE_OBJECT) {
|
|
126
|
-
state.hasSafePathImport = true;
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
/**
|
|
132
|
-
* Is `name` resolvable from `node`'s scope outward — a parameter, a local, an
|
|
133
|
-
* import, or a configured global?
|
|
134
|
-
*
|
|
135
|
-
* Used only to decide whether a bare `join(...)` with no `node:path` import is
|
|
136
|
-
* OUR `join` or somebody else's. `import { join } from 'lodash'` binds the name
|
|
137
|
-
* and is not our business; an unbound `join` is a ReferenceError waiting to
|
|
138
|
-
* happen, and — see `classifyCall` — is exactly what a half-applied autofix
|
|
139
|
-
* leaves behind.
|
|
140
|
-
*/
|
|
141
|
-
function isIdentifierBound(sourceCode, node, name) {
|
|
142
|
-
for (let scope = sourceCode.getScope(node); scope; scope = scope.upper) {
|
|
143
|
-
if (scope.variables.some((variable) => variable.name === name)) {
|
|
144
|
-
return true;
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
return false;
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
/**
|
|
151
|
-
* Is this the call THIS rule's fixer writes — `safePath.<unsafeFn>(…)`?
|
|
152
|
-
*
|
|
153
|
-
* The evidence that this rule migrated something in this file, and the reason it
|
|
154
|
-
* is not enough to ask whether `safePath` is merely in scope. Read by BOTH legs
|
|
155
|
-
* that would otherwise mistake a file-wide fact for a claim about `unsafeFn`:
|
|
156
|
-
* the dead-import leg, and the bare-call repair leg in `classifyCall`.
|
|
157
|
-
* Matched whether or not `safePath` is bound: an ORPHANED `safePath.join(…)` —
|
|
158
|
-
* the repair leg's own subject — is still this fixer's handiwork.
|
|
159
|
-
*
|
|
160
|
-
* @param {object} node - A `CallExpression`.
|
|
161
|
-
* @param {string} unsafeFn - The member this rule migrates.
|
|
162
|
-
* @returns {boolean} True if the callee is `safePath.<unsafeFn>`.
|
|
163
|
-
*/
|
|
164
|
-
function isSafeReplacementCall(node, unsafeFn) {
|
|
165
|
-
return (
|
|
166
|
-
node.callee.type === 'MemberExpression' &&
|
|
167
|
-
node.callee.object.type === 'Identifier' &&
|
|
168
|
-
node.callee.object.name === SAFE_OBJECT &&
|
|
169
|
-
node.callee.property.type === 'Identifier' &&
|
|
170
|
-
node.callee.property.name === unsafeFn
|
|
171
|
-
);
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
/**
|
|
175
|
-
* Check if a call expression is an unsafe path function call.
|
|
176
|
-
*
|
|
177
|
-
* Returns `{ isNamed }` — or `{ importOnly: true }` for a call that is already
|
|
178
|
-
* correct and merely missing its import — or `{ isRepair: true }` for a bare call
|
|
179
|
-
* that is ours only if the file also shows THIS function mid-migration, which
|
|
180
|
-
* `Program:exit` decides once the whole file has been seen — or null if not a
|
|
181
|
-
* match.
|
|
182
|
-
*/
|
|
183
|
-
function classifyCall(node, unsafeFn, state, sourceCode) {
|
|
184
|
-
const isMember = node.callee.type === 'MemberExpression' && node.callee.property.type === 'Identifier';
|
|
185
|
-
|
|
186
|
-
// Direct call: join(...)
|
|
187
|
-
if (node.callee.type === 'Identifier' && node.callee.name === unsafeFn) {
|
|
188
|
-
if (state.namedImportSpec) {
|
|
189
|
-
return { isNamed: true };
|
|
190
|
-
}
|
|
191
|
-
// REPAIR LEG. Keying detection on "did I see the import?" made this rule
|
|
192
|
-
// stop reporting the moment a fix removed the specifier, so a partial
|
|
193
|
-
// `--fix` reached a stable fixpoint over source that no longer compiles and
|
|
194
|
-
// exited clean.
|
|
195
|
-
//
|
|
196
|
-
// An unbound `join` is NOT reliably our `join`: ESLint scope analysis does
|
|
197
|
-
// not bind `declare global { function join() }`, and it cannot see an ambient
|
|
198
|
-
// global from a `globals.d.ts`, an `@types` package, or a bundler — `resolve`
|
|
199
|
-
// and `relative` are entirely plausible as those. So the leg needs a second
|
|
200
|
-
// condition, and `safePathBoundInSource` alone was the wrong one: it is a
|
|
201
|
-
// fact about the FILE, not about `unsafeFn`, and a SIBLING instance of this
|
|
202
|
-
// factory hands it over for free. `no-path-resolve` rewrites a
|
|
203
|
-
// `path.resolve(...)` and imports `safePath`; on the next `--fix` pass the
|
|
204
|
-
// `join` instance sees a file with `safePath` bound and an ambient-global
|
|
205
|
-
// `join(...)` that predates any of this pack's involvement, and rewrites it
|
|
206
|
-
// to a different function. Measured with both rules enabled over one file.
|
|
207
|
-
//
|
|
208
|
-
// So this is only a CANDIDATE. `Program:exit` admits it once the file has
|
|
209
|
-
// been seen to contain a `safePath.<unsafeFn>(...)` call — the same positive
|
|
210
|
-
// evidence the dead-import leg reads, and the only in-file signal that THIS
|
|
211
|
-
// function is mid-migration HERE. Every genuine strand carries it:
|
|
212
|
-
// `removeSpecifier` only ever ships inside the same report's fix as the
|
|
213
|
-
// callee rewrite that consumed the specifier, and ESLint merges a report's
|
|
214
|
-
// fixes into one all-or-nothing range — so a file cannot lose the `node:path`
|
|
215
|
-
// specifier without gaining a `safePath.<unsafeFn>(` call.
|
|
216
|
-
//
|
|
217
|
-
// What this gives up: a file a HUMAN half-migrated by hand — import deleted,
|
|
218
|
-
// not one call rewritten — is no longer finished by `--fix`. That file is a
|
|
219
|
-
// loud `no-undef`/`tsc` error rather than a silent one, and it was never this
|
|
220
|
-
// pack's doing. Silently redirecting a live call to another function is.
|
|
221
|
-
if (state.safePathBoundInSource && !isIdentifierBound(sourceCode, node.callee, unsafeFn)) {
|
|
222
|
-
return { isNamed: false, isRepair: true };
|
|
223
|
-
}
|
|
224
|
-
return null;
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
// Namespace call: path.join(...)
|
|
228
|
-
if (
|
|
229
|
-
isMember &&
|
|
230
|
-
node.callee.object.type === 'Identifier' &&
|
|
231
|
-
node.callee.object.name === state.defaultImportName &&
|
|
232
|
-
node.callee.property.name === unsafeFn
|
|
233
|
-
) {
|
|
234
|
-
return { isNamed: false };
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
// REPAIR LEG, the other half: `safePath.join(...)` with no `safePath` in
|
|
238
|
-
// scope. This is what a partially-applied fix leaves — and without it, that
|
|
239
|
-
// state is PERMANENT rather than transient.
|
|
240
|
-
//
|
|
241
|
-
// ESLint runs `fix()` for a problem BEFORE the `eslint-disable` filter
|
|
242
|
-
// discards it, so a suppressed report on the first call site consumes the
|
|
243
|
-
// once-per-file import edit and then throws it away. Every other call is
|
|
244
|
-
// rewritten to `safePath.join`, nothing imports `safePath`, and no report
|
|
245
|
-
// survives to carry the import on any later pass. Recognising the orphaned
|
|
246
|
-
// call is what closes that loop; it costs one extra pass, and only in a file
|
|
247
|
-
// that is already broken.
|
|
248
|
-
if (
|
|
249
|
-
isMember &&
|
|
250
|
-
node.callee.object.type === 'Identifier' &&
|
|
251
|
-
node.callee.object.name === SAFE_OBJECT &&
|
|
252
|
-
node.callee.property.name === unsafeFn &&
|
|
253
|
-
!isIdentifierBound(sourceCode, node.callee.object, SAFE_OBJECT)
|
|
254
|
-
) {
|
|
255
|
-
return { importOnly: true };
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
return null;
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
/**
|
|
262
|
-
* Build auto-fix for an unsafe path function call.
|
|
263
|
-
*
|
|
264
|
-
* ## Why the import edits are emitted at most ONCE per file
|
|
265
|
-
*
|
|
266
|
-
* ESLint merges the fixes one `fix()` yields into a SINGLE range spanning
|
|
267
|
-
* `min..max`, and applies only non-overlapping ranges per pass. A fix that
|
|
268
|
-
* touches both the import and its own call site therefore spans everything in
|
|
269
|
-
* between — so N such reports produce N nested ranges, ESLint keeps the
|
|
270
|
-
* shortest and DISCARDS THE REST.
|
|
271
|
-
*
|
|
272
|
-
* That is not an edge case, it is every file with more than one call site. The
|
|
273
|
-
* import edit landed, the other calls did not, and (before `classifyCall` grew
|
|
274
|
-
* its bare-call leg) the next pass could no longer see them because the
|
|
275
|
-
* specifier it keyed on was gone. `--fix` reached a stable fixpoint over source
|
|
276
|
-
* that does not compile and exited clean. An adopter measured 146 files left
|
|
277
|
-
* with a dangling reference across one sweep — worst single file, 75 call sites.
|
|
278
|
-
*
|
|
279
|
-
* So: the shared edits belong to the first report, and every later report emits
|
|
280
|
-
* a fix LOCAL to its own callee. Nothing overlaps, and one pass fixes the file.
|
|
281
|
-
* `no-manual-path-normalize.cjs` carries the same guard for the same reason.
|
|
282
|
-
*
|
|
283
|
-
* Only the FIRST report's fix is self-sufficient, and that is load-bearing:
|
|
284
|
-
* applying a later one ALONE — an editor's "fix this problem", or an
|
|
285
|
-
* `eslint-disable` on the first call site — rewrites the call without adding
|
|
286
|
-
* the import. ESLint runs `fix()` before the disable filter, so a suppressed
|
|
287
|
-
* report consumes the once-per-file edit and then discards it.
|
|
288
|
-
*
|
|
289
|
-
* That state is recoverable rather than permanent ONLY because `classifyCall`
|
|
290
|
-
* has a repair leg for an orphaned `safePath.join(...)`. Without it the file
|
|
291
|
-
* stays broken through every subsequent `--fix`, because no report is left to
|
|
292
|
-
* carry the import — measured, not reasoned about. An earlier draft of this
|
|
293
|
-
* comment asserted the recovery came free from `hasSafePathImport` being seeded
|
|
294
|
-
* from scope; that was wrong, and an adversarial run produced the stable broken
|
|
295
|
-
* fixpoint to prove it.
|
|
296
|
-
*
|
|
297
|
-
* The shared edits still cannot be hoisted onto their own report: removing
|
|
298
|
-
* `join` from the import while a suppressed `join(...)` call survives is the
|
|
299
|
-
* same broken output reached a different way. `exemptFiles` opts a whole file
|
|
300
|
-
* out.
|
|
301
|
-
*/
|
|
302
|
-
function importSafePath(fixer, sourceCode, state) {
|
|
303
|
-
if (state.safeImportNode) {
|
|
304
|
-
const lastSpec = state.safeImportNode.specifiers.at(-1);
|
|
305
|
-
return fixer.insertTextAfter(lastSpec, `, ${SAFE_OBJECT}`);
|
|
306
|
-
}
|
|
307
|
-
const targetNode = state.namedImportNode || sourceCode.ast.body[0];
|
|
308
|
-
const declaration = `import { ${SAFE_OBJECT} } from '${state.safeModule}';`;
|
|
309
|
-
// Land the new import next to the imports, not after arbitrary code. A file
|
|
310
|
-
// reported only through a repair leg may have no path import at all, and
|
|
311
|
-
// `insertTextAfter(body[0])` would push the declaration below the statement
|
|
312
|
-
// that needs it — legal, since imports hoist, but it reads as though the
|
|
313
|
-
// fixer lost track of the file.
|
|
314
|
-
return targetNode.type === 'ImportDeclaration'
|
|
315
|
-
? fixer.insertTextAfter(targetNode, `\n${declaration}`)
|
|
316
|
-
: insertAboveWithComments(fixer, sourceCode, targetNode, `${declaration}\n`);
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
function buildFix(fixer, node, unsafeFn, classification, sourceCode, state) {
|
|
320
|
-
// REPAIR: an orphaned `safePath.join(...)` is already the call we want, and
|
|
321
|
-
// the only thing missing is the import that a discarded report was carrying.
|
|
322
|
-
//
|
|
323
|
-
// This deliberately ignores `state.hasSafePathImport`. That flag is mutated
|
|
324
|
-
// inside `fix()`, and ESLint runs `fix()` for a SUPPRESSED problem before the
|
|
325
|
-
// disable filter throws it away — so on every pass the suppressed report
|
|
326
|
-
// spends the flag first and the repair emits nothing. The file then never
|
|
327
|
-
// recovers, which is precisely the stable broken fixpoint this leg exists to
|
|
328
|
-
// break. The gate that makes ignoring the flag safe is immutable: this
|
|
329
|
-
// classification is only reached when `safePath` is unbound in the SOURCE.
|
|
330
|
-
//
|
|
331
|
-
// Several orphaned calls yield the identical insert at the identical anchor,
|
|
332
|
-
// so ESLint applies one and drops the rest as overlapping — which is the
|
|
333
|
-
// desired outcome, not a hazard.
|
|
334
|
-
if (classification.importOnly) {
|
|
335
|
-
return [importSafePath(fixer, sourceCode, state)];
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
const fixes = [fixer.replaceText(node.callee, `${SAFE_OBJECT}.${unsafeFn}`)];
|
|
339
|
-
|
|
340
|
-
if (!state.hasSafePathImport) {
|
|
341
|
-
fixes.push(importSafePath(fixer, sourceCode, state));
|
|
342
|
-
state.hasSafePathImport = true;
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
if (
|
|
346
|
-
classification.isNamed &&
|
|
347
|
-
state.namedImportNode &&
|
|
348
|
-
!state.namedImportRemoved &&
|
|
349
|
-
!isReExported(sourceCode, unsafeFn)
|
|
350
|
-
) {
|
|
351
|
-
fixes.push(...removeSpecifier(fixer, sourceCode, state.namedImportNode, state.namedImportSpec));
|
|
352
|
-
state.namedImportRemoved = true;
|
|
353
|
-
}
|
|
354
|
-
|
|
355
|
-
return fixes;
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
module.exports = function createPathFunctionRule(config) {
|
|
359
|
-
const { unsafeFn, message } = config;
|
|
360
|
-
|
|
361
|
-
return {
|
|
362
|
-
meta: {
|
|
363
|
-
type: 'problem',
|
|
364
|
-
docs: {
|
|
365
|
-
description: `Enforce safePath.${unsafeFn}() instead of path.${unsafeFn}()`,
|
|
366
|
-
category: 'Cross-platform compatibility',
|
|
367
|
-
recommended: true,
|
|
368
|
-
},
|
|
369
|
-
fixable: 'code',
|
|
370
|
-
schema: [EXEMPT_AND_SAFE_MODULE_SCHEMA],
|
|
371
|
-
messages: {
|
|
372
|
-
noUnsafePathFn: message,
|
|
373
|
-
[DEAD_UNSAFE_IMPORT]: DEAD_UNSAFE_IMPORT_MESSAGE,
|
|
374
|
-
[UNANCHORED_EXEMPT_FILE]: UNANCHORED_EXEMPT_MESSAGE,
|
|
375
|
-
},
|
|
376
|
-
},
|
|
377
|
-
|
|
378
|
-
create(context) {
|
|
379
|
-
if (exemptMatcherFor(context)(context.getFilename())) {
|
|
380
|
-
// Still surface a malformed exemption list: the file we are standing in
|
|
381
|
-
// may be exempt only BECAUSE the entry is unanchored.
|
|
382
|
-
return {
|
|
383
|
-
Program(node) {
|
|
384
|
-
reportUnanchoredExemptEntries(context, node);
|
|
385
|
-
},
|
|
386
|
-
};
|
|
387
|
-
}
|
|
388
|
-
|
|
389
|
-
const sourceCode = context.getSourceCode();
|
|
390
|
-
const state = {
|
|
391
|
-
// Resolved per invocation — the option belongs to the consuming repo,
|
|
392
|
-
// which may point different rules at different re-export entries.
|
|
393
|
-
safeModule: resolveSafeModule(context, SAFE_PATH_MODULE),
|
|
394
|
-
namedImportSpec: null,
|
|
395
|
-
namedImportNode: null,
|
|
396
|
-
// Both of these guard a SHARED edit against being emitted by more than
|
|
397
|
-
// one report — see `buildFix` for what ESLint does with the overlap.
|
|
398
|
-
namedImportRemoved: false,
|
|
399
|
-
defaultImportName: null,
|
|
400
|
-
// Seeded from SCOPE, not from "did I see an import from SAFE_MODULE?".
|
|
401
|
-
// A file already importing `safePath` from the barrel needs the call
|
|
402
|
-
// rewritten but must NOT gain a second binding of the same name.
|
|
403
|
-
hasSafePathImport: isNameAlreadyBound(sourceCode, SAFE_OBJECT),
|
|
404
|
-
// The SAME question, answered once and never mutated. `hasSafePathImport`
|
|
405
|
-
// flips to true the moment a fix inserts the import, and gating the
|
|
406
|
-
// repair leg on a flag that the first report can flip would arm it for
|
|
407
|
-
// the rest of THIS pass — re-admitting the ambient-global false positive
|
|
408
|
-
// in any file that also has a `path.join()` to fix.
|
|
409
|
-
safePathBoundInSource: isNameAlreadyBound(sourceCode, SAFE_OBJECT),
|
|
410
|
-
safeImportNode: null,
|
|
411
|
-
// Set from the SOURCE as traversal walks it, and read only at
|
|
412
|
-
// `Program:exit` — so it is a fact about the text being linted, not
|
|
413
|
-
// about what a `fix()` intends to write. See `dead-import.cjs`, and the
|
|
414
|
-
// repair leg in `classifyCall`, which gates on the same flag.
|
|
415
|
-
safeReplacementCalled: false,
|
|
416
|
-
// Bare unbound calls awaiting that evidence. Held rather than reported,
|
|
417
|
-
// because a candidate can precede the migrated call that vouches for it.
|
|
418
|
-
deferredRepairs: [],
|
|
419
|
-
// EVERY path-module declaration, not just the one carrying `unsafeFn`.
|
|
420
|
-
// A file's dead binding is `import path from 'node:path'`, which
|
|
421
|
-
// `trackPathImport` only ever recorded as a NAME. See `dead-import.cjs`.
|
|
422
|
-
pathImportNodes: [],
|
|
423
|
-
};
|
|
424
|
-
|
|
425
|
-
const report = (node, classification) => {
|
|
426
|
-
context.report({
|
|
427
|
-
node,
|
|
428
|
-
messageId: 'noUnsafePathFn',
|
|
429
|
-
// The module name reaches the message through `{{safeModule}}` rather
|
|
430
|
-
// than being spelled out in each rule's string, so the advice cannot
|
|
431
|
-
// drift from where the fixer actually writes the import.
|
|
432
|
-
data: { safeModule: state.safeModule },
|
|
433
|
-
fix(fixer) {
|
|
434
|
-
return buildFix(fixer, node, unsafeFn, classification, sourceCode, state);
|
|
435
|
-
},
|
|
436
|
-
});
|
|
437
|
-
};
|
|
438
|
-
|
|
439
|
-
return {
|
|
440
|
-
Program(node) {
|
|
441
|
-
reportUnanchoredExemptEntries(context, node);
|
|
442
|
-
},
|
|
443
|
-
|
|
444
|
-
'Program:exit'() {
|
|
445
|
-
// Holding these back cannot change any FIX, only when it is computed.
|
|
446
|
-
// A repair candidate is classified only when `safePath` is bound in the
|
|
447
|
-
// source, so `hasSafePathImport` starts true and `namedImportSpec` is
|
|
448
|
-
// null: the report carries no import insert and no specifier removal,
|
|
449
|
-
// and therefore nothing shared that an earlier report could have spent
|
|
450
|
-
// first. (`importOnly` is the mirror case — it requires `safePath`
|
|
451
|
-
// UNBOUND — so the two can never arise in the same file.)
|
|
452
|
-
if (state.safeReplacementCalled) {
|
|
453
|
-
for (const candidate of state.deferredRepairs) {
|
|
454
|
-
report(candidate, { isNamed: false });
|
|
455
|
-
}
|
|
456
|
-
}
|
|
457
|
-
reportDeadUnsafeImports(
|
|
458
|
-
context,
|
|
459
|
-
sourceCode,
|
|
460
|
-
state.pathImportNodes,
|
|
461
|
-
state.safePathBoundInSource,
|
|
462
|
-
state.safeReplacementCalled,
|
|
463
|
-
);
|
|
464
|
-
},
|
|
465
|
-
|
|
466
|
-
ImportDeclaration(node) {
|
|
467
|
-
if (PATH_MODULES.has(node.source.value)) {
|
|
468
|
-
state.pathImportNodes.push(node);
|
|
469
|
-
trackPathImport(node, unsafeFn, state);
|
|
470
|
-
}
|
|
471
|
-
if (node.source.value === state.safeModule) {
|
|
472
|
-
trackSafeImport(node, state);
|
|
473
|
-
}
|
|
474
|
-
},
|
|
475
|
-
|
|
476
|
-
CallExpression(node) {
|
|
477
|
-
if (isSafeReplacementCall(node, unsafeFn)) {
|
|
478
|
-
state.safeReplacementCalled = true;
|
|
479
|
-
}
|
|
480
|
-
|
|
481
|
-
const classification = classifyCall(node, unsafeFn, state, sourceCode);
|
|
482
|
-
if (!classification) {
|
|
483
|
-
return;
|
|
484
|
-
}
|
|
485
|
-
|
|
486
|
-
if (classification.isRepair) {
|
|
487
|
-
state.deferredRepairs.push(node);
|
|
488
|
-
return;
|
|
489
|
-
}
|
|
490
|
-
|
|
491
|
-
report(node, classification);
|
|
492
|
-
},
|
|
493
|
-
};
|
|
494
|
-
},
|
|
495
|
-
};
|
|
496
|
-
};
|