@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
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ESLint rule: dirent-type-needs-symlink-check
|
|
3
|
+
*
|
|
4
|
+
* Flags `.isFile()` / `.isDirectory()` on a Dirent whose binding is never
|
|
5
|
+
* asked `.isSymbolicLink()`.
|
|
6
|
+
*
|
|
7
|
+
* A Dirent describes the entry itself, not what it points at, so for a
|
|
8
|
+
* symlink BOTH `isFile()` and `isDirectory()` are false. A walk written as
|
|
9
|
+
* `if (e.isDirectory()) recurse(); else if (e.isFile()) read();` therefore
|
|
10
|
+
* does not refuse a link, and does not follow it either — it drops the entry
|
|
11
|
+
* on the floor without a word. The sweep found staged skill trees, size
|
|
12
|
+
* accounting and packaging walks with exactly that shape, and every one
|
|
13
|
+
* reported a tree with a symlink in it as clean. The decision about a link —
|
|
14
|
+
* refuse, follow, record — has to be made where it can be seen, on the same
|
|
15
|
+
* binding, before the type test.
|
|
16
|
+
*
|
|
17
|
+
* ## How a binding is known to be a Dirent
|
|
18
|
+
*
|
|
19
|
+
* - the result of `readdir` / `readdirSync` called with `{ withFileTypes: true }`
|
|
20
|
+
* (any receiver: bare, `fs.`, `fs.promises.`), stored in a variable, assigned
|
|
21
|
+
* later, or iterated directly by `for…of`;
|
|
22
|
+
* - the result of `opendir` / `opendirSync`, iterated by `for await`;
|
|
23
|
+
* - an element of such a collection, via `for…of` or a callback to `filter` /
|
|
24
|
+
* `map` / `some` / `every` / `find` / `forEach` / `flatMap` — whether the
|
|
25
|
+
* collection is a variable or the `readdir` call itself, chained
|
|
26
|
+
* (`readdirSync(d, { withFileTypes: true }).filter(e => e.isDirectory())`),
|
|
27
|
+
* including through a `filter` / `toSorted` / `slice` that keeps the
|
|
28
|
+
* collection a Dirent collection;
|
|
29
|
+
* - a parameter or variable annotated `Dirent`, `Dirent[]`,
|
|
30
|
+
* `readonly Dirent[]` or `Array<Dirent>`.
|
|
31
|
+
*
|
|
32
|
+
* A Stats object from `stat()` / `lstat()` is NOT a Dirent and is never
|
|
33
|
+
* flagged: `stat` already followed the link, and `lstat` callers are asking a
|
|
34
|
+
* different question. A `readdir` without `withFileTypes` yields strings.
|
|
35
|
+
*
|
|
36
|
+
* The guard must be on the SAME binding: an `lstat(e.name).isSymbolicLink()`
|
|
37
|
+
* beside an unguarded `e.isDirectory()` is two questions about two objects,
|
|
38
|
+
* and the rule cannot know they agree.
|
|
39
|
+
*
|
|
40
|
+
* @example
|
|
41
|
+
* // BAD — a symlinked directory is neither, so it silently vanishes
|
|
42
|
+
* for (const e of readdirSync(dir, { withFileTypes: true })) {
|
|
43
|
+
* if (e.isDirectory()) walk(e); else if (e.isFile()) read(e);
|
|
44
|
+
* }
|
|
45
|
+
*
|
|
46
|
+
* // GOOD — the link is decided first, visibly
|
|
47
|
+
* for (const e of readdirSync(dir, { withFileTypes: true })) {
|
|
48
|
+
* if (e.isSymbolicLink()) { refuse(e); continue; }
|
|
49
|
+
* if (e.isDirectory()) walk(e); else if (e.isFile()) read(e);
|
|
50
|
+
* }
|
|
51
|
+
*/
|
|
52
|
+
|
|
53
|
+
'use strict';
|
|
54
|
+
|
|
55
|
+
const { calleeName } = require('./callee-name.cjs');
|
|
56
|
+
|
|
57
|
+
const READDIR_FUNCTIONS = new Set(['readdir', 'readdirSync']);
|
|
58
|
+
const OPENDIR_FUNCTIONS = new Set(['opendir', 'opendirSync']);
|
|
59
|
+
const ELEMENT_CALLBACK_METHODS = new Set([
|
|
60
|
+
'filter', 'map', 'some', 'every', 'find', 'forEach', 'flatMap', 'findIndex', 'findLast',
|
|
61
|
+
]);
|
|
62
|
+
/** Array methods whose RESULT is still the same Dirent collection (a `map` is not: it produced something else). */
|
|
63
|
+
const COLLECTION_PRESERVING_METHODS = new Set(['filter', 'toSorted', 'toReversed', 'slice', 'reverse', 'sort']);
|
|
64
|
+
const TYPE_TESTS = new Set(['isFile', 'isDirectory']);
|
|
65
|
+
const FUNCTION_TYPES = ['FunctionDeclaration', 'FunctionExpression', 'ArrowFunctionExpression'];
|
|
66
|
+
|
|
67
|
+
/** Whether some argument is an object literal carrying `withFileTypes: true`. */
|
|
68
|
+
function hasWithFileTypes(call) {
|
|
69
|
+
return call.arguments.some(
|
|
70
|
+
(arg) =>
|
|
71
|
+
arg.type === 'ObjectExpression' &&
|
|
72
|
+
arg.properties.some(
|
|
73
|
+
(prop) =>
|
|
74
|
+
prop.type === 'Property' &&
|
|
75
|
+
!prop.computed &&
|
|
76
|
+
(prop.key.name ?? prop.key.value) === 'withFileTypes' &&
|
|
77
|
+
prop.value.type === 'Literal' &&
|
|
78
|
+
prop.value.value === true,
|
|
79
|
+
),
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** Whether `expr` (possibly awaited) produces Dirents when stored or iterated. */
|
|
84
|
+
function isDirentCollectionCall(expr) {
|
|
85
|
+
const call = expr?.type === 'AwaitExpression' ? expr.argument : expr;
|
|
86
|
+
if (call?.type !== 'CallExpression') {
|
|
87
|
+
return false;
|
|
88
|
+
}
|
|
89
|
+
const name = calleeName(call);
|
|
90
|
+
return OPENDIR_FUNCTIONS.has(name) || (READDIR_FUNCTIONS.has(name) && hasWithFileTypes(call));
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** Whether a TS type node is the bare `Dirent` reference. */
|
|
94
|
+
function isDirentType(typeNode) {
|
|
95
|
+
return (
|
|
96
|
+
typeNode?.type === 'TSTypeReference' &&
|
|
97
|
+
typeNode.typeName.type === 'Identifier' &&
|
|
98
|
+
typeNode.typeName.name === 'Dirent'
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* 'element' for `Dirent`, 'collection' for `Dirent[]` / `readonly Dirent[]` /
|
|
104
|
+
* `Array<Dirent>` / `ReadonlyArray<Dirent>`, null otherwise.
|
|
105
|
+
*/
|
|
106
|
+
function direntKindOfAnnotation(annotation) {
|
|
107
|
+
const typeNode = annotation?.typeAnnotation;
|
|
108
|
+
if (!typeNode) {
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
if (isDirentType(typeNode)) {
|
|
112
|
+
return 'element';
|
|
113
|
+
}
|
|
114
|
+
if (typeNode.type === 'TSTypeOperator') {
|
|
115
|
+
return direntKindOfAnnotation({ typeAnnotation: typeNode.typeAnnotation }) === 'collection' ? 'collection' : null;
|
|
116
|
+
}
|
|
117
|
+
if (typeNode.type === 'TSArrayType') {
|
|
118
|
+
return isDirentType(typeNode.elementType) ? 'collection' : null;
|
|
119
|
+
}
|
|
120
|
+
const generic = typeNode.type === 'TSTypeReference' ? typeNode.typeArguments ?? typeNode.typeParameters : null;
|
|
121
|
+
return generic && isDirentType(generic.params[0]) ? 'collection' : null;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** The kind of member call a reference participates in: `{ name, call }` or null. */
|
|
125
|
+
function memberCallOf(identifier) {
|
|
126
|
+
const member = identifier.parent;
|
|
127
|
+
if (member?.type !== 'MemberExpression' || member.object !== identifier || member.computed) {
|
|
128
|
+
return null;
|
|
129
|
+
}
|
|
130
|
+
const call = member.parent;
|
|
131
|
+
if (call?.type !== 'CallExpression' || call.callee !== member || member.property.type !== 'Identifier') {
|
|
132
|
+
return null;
|
|
133
|
+
}
|
|
134
|
+
return { name: member.property.name, call };
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
module.exports = {
|
|
138
|
+
meta: {
|
|
139
|
+
type: 'problem',
|
|
140
|
+
docs: {
|
|
141
|
+
description:
|
|
142
|
+
'Require an isSymbolicLink() check on a Dirent before isFile()/isDirectory() — both are ' +
|
|
143
|
+
'false for a symlink, so an unchecked walk drops links silently',
|
|
144
|
+
recommended: false,
|
|
145
|
+
recommendedSeverity: 'warn',
|
|
146
|
+
},
|
|
147
|
+
schema: [],
|
|
148
|
+
messages: {
|
|
149
|
+
direntTypeWithoutSymlinkCheck:
|
|
150
|
+
'A symlink answers false to BOTH isFile() and isDirectory(), so this walk drops every link ' +
|
|
151
|
+
'without a word. Decide the link on this same binding first — ' +
|
|
152
|
+
'if ({{name}}.isSymbolicLink()) { refuse / follow / record } — then test the type.',
|
|
153
|
+
},
|
|
154
|
+
},
|
|
155
|
+
|
|
156
|
+
create(context) {
|
|
157
|
+
const sourceCode = context.sourceCode ?? context.getSourceCode();
|
|
158
|
+
const collections = new Set();
|
|
159
|
+
const elements = new Set();
|
|
160
|
+
|
|
161
|
+
/** The variable an identifier REFERENCE resolves to, or null. */
|
|
162
|
+
function resolvedVariable(identifier) {
|
|
163
|
+
const reference = sourceCode.getScope(identifier).references.find((ref) => ref.identifier === identifier);
|
|
164
|
+
return reference?.resolved ?? null;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function markDeclared(node, kind) {
|
|
168
|
+
for (const variable of sourceCode.getDeclaredVariables(node)) {
|
|
169
|
+
(kind === 'element' ? elements : collections).add(variable);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function markAnnotatedParams(fn) {
|
|
174
|
+
const declared = sourceCode.getDeclaredVariables(fn);
|
|
175
|
+
for (const param of fn.params) {
|
|
176
|
+
const kind = param.type === 'Identifier' ? direntKindOfAnnotation(param.typeAnnotation) : null;
|
|
177
|
+
const variable = kind ? declared.find((candidate) => candidate.name === param.name) : null;
|
|
178
|
+
if (variable) {
|
|
179
|
+
(kind === 'element' ? elements : collections).add(variable);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function isCollectionIdentifier(expr) {
|
|
185
|
+
return expr.type === 'Identifier' && collections.has(resolvedVariable(expr));
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Whether `expr` is a Dirent collection: a bound variable, the `readdir`
|
|
190
|
+
* call itself (possibly awaited or parenthesised), or a chain of
|
|
191
|
+
* collection-preserving array methods on one of those.
|
|
192
|
+
*/
|
|
193
|
+
function isDirentCollection(expr) {
|
|
194
|
+
if (!expr) {
|
|
195
|
+
return false;
|
|
196
|
+
}
|
|
197
|
+
if (isCollectionIdentifier(expr) || isDirentCollectionCall(expr)) {
|
|
198
|
+
return true;
|
|
199
|
+
}
|
|
200
|
+
const { callee } = expr.type === 'CallExpression' ? expr : {};
|
|
201
|
+
return (
|
|
202
|
+
callee?.type === 'MemberExpression' &&
|
|
203
|
+
callee.property.type === 'Identifier' &&
|
|
204
|
+
COLLECTION_PRESERVING_METHODS.has(callee.property.name) &&
|
|
205
|
+
isDirentCollection(callee.object)
|
|
206
|
+
);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function reportUnguarded(variable) {
|
|
210
|
+
let guarded = false;
|
|
211
|
+
const typeTests = [];
|
|
212
|
+
for (const reference of variable.references) {
|
|
213
|
+
const use = memberCallOf(reference.identifier);
|
|
214
|
+
if (use?.name === 'isSymbolicLink') {
|
|
215
|
+
guarded = true;
|
|
216
|
+
} else if (use && TYPE_TESTS.has(use.name)) {
|
|
217
|
+
typeTests.push(use.call);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
if (guarded) {
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
for (const call of typeTests) {
|
|
224
|
+
context.report({ node: call, messageId: 'direntTypeWithoutSymlinkCheck', data: { name: variable.name } });
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
return {
|
|
229
|
+
VariableDeclarator(node) {
|
|
230
|
+
if (isDirentCollectionCall(node.init)) {
|
|
231
|
+
markDeclared(node, 'collection');
|
|
232
|
+
} else if (node.id.type === 'Identifier') {
|
|
233
|
+
const kind = direntKindOfAnnotation(node.id.typeAnnotation);
|
|
234
|
+
if (kind) {
|
|
235
|
+
markDeclared(node, kind);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
},
|
|
239
|
+
AssignmentExpression(node) {
|
|
240
|
+
if (node.left.type === 'Identifier' && isDirentCollectionCall(node.right)) {
|
|
241
|
+
const variable = resolvedVariable(node.left);
|
|
242
|
+
if (variable) {
|
|
243
|
+
collections.add(variable);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
},
|
|
247
|
+
ForOfStatement(node) {
|
|
248
|
+
if (node.left.type !== 'VariableDeclaration') {
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
if (isDirentCollection(node.right)) {
|
|
252
|
+
markDeclared(node.left, 'element');
|
|
253
|
+
}
|
|
254
|
+
},
|
|
255
|
+
CallExpression(node) {
|
|
256
|
+
const { callee } = node;
|
|
257
|
+
const callback = node.arguments[0];
|
|
258
|
+
if (
|
|
259
|
+
callee.type === 'MemberExpression' &&
|
|
260
|
+
callee.property.type === 'Identifier' &&
|
|
261
|
+
ELEMENT_CALLBACK_METHODS.has(callee.property.name) &&
|
|
262
|
+
isDirentCollection(callee.object) &&
|
|
263
|
+
callback &&
|
|
264
|
+
FUNCTION_TYPES.includes(callback.type) &&
|
|
265
|
+
callback.params[0]?.type === 'Identifier'
|
|
266
|
+
) {
|
|
267
|
+
const declared = sourceCode.getDeclaredVariables(callback);
|
|
268
|
+
const first = declared.find((variable) => variable.name === callback.params[0].name);
|
|
269
|
+
if (first) {
|
|
270
|
+
elements.add(first);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
},
|
|
274
|
+
[FUNCTION_TYPES.join(',')]: markAnnotatedParams,
|
|
275
|
+
'Program:exit'() {
|
|
276
|
+
for (const variable of elements) {
|
|
277
|
+
reportUnguarded(variable);
|
|
278
|
+
}
|
|
279
|
+
},
|
|
280
|
+
};
|
|
281
|
+
},
|
|
282
|
+
};
|
|
@@ -18,6 +18,9 @@
|
|
|
18
18
|
* in one repo, so a baked-in default is a hole in every OTHER repo. Consumers
|
|
19
19
|
* declare their own via the rule option (see `exempt-path-matcher.cjs`).
|
|
20
20
|
* @param {boolean} [config.checkMemberExpression] - Check for obj.method() calls (default: false)
|
|
21
|
+
* @param {Object} config.docs - The rule's `meta.docs` manifest entry: `category`,
|
|
22
|
+
* `bans`, `useInstead`, optional `subpath`, and `recommended` /
|
|
23
|
+
* `recommendedSeverity` (what `index.cjs` builds `configs.recommended` from).
|
|
21
24
|
* @returns {Object} ESLint rule definition
|
|
22
25
|
*
|
|
23
26
|
* @example
|
|
@@ -147,6 +150,7 @@ module.exports = function createNoUnsafeRule(config) {
|
|
|
147
150
|
safeFn,
|
|
148
151
|
safeModule,
|
|
149
152
|
message,
|
|
153
|
+
docs,
|
|
150
154
|
exemptFiles = [],
|
|
151
155
|
checkMemberExpression = false,
|
|
152
156
|
} = config;
|
|
@@ -166,8 +170,7 @@ module.exports = function createNoUnsafeRule(config) {
|
|
|
166
170
|
type: 'problem',
|
|
167
171
|
docs: {
|
|
168
172
|
description: `Enforce use of ${safeFn}() instead of ${unsafeFn}()`,
|
|
169
|
-
|
|
170
|
-
recommended: true,
|
|
173
|
+
...docs,
|
|
171
174
|
},
|
|
172
175
|
fixable: 'code',
|
|
173
176
|
schema: [EXEMPT_AND_SAFE_MODULE_SCHEMA],
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
* - `isTestFile` — "is this a test file?" (a category, anchored on the basename's
|
|
24
24
|
* extension rather than on a path segment)
|
|
25
25
|
*
|
|
26
|
-
* `eslint-rule-factory.cjs`, `
|
|
26
|
+
* `eslint-rule-factory.cjs`, `no-raw-node-path.cjs`,
|
|
27
27
|
* `no-command-direct-factory.cjs` and `no-unix-shell-commands.cjs` all use these.
|
|
28
28
|
* Do not re-implement any of them: the sibling-factory copy is how the bug
|
|
29
29
|
* shipped four times in the first place.
|
|
@@ -36,6 +36,7 @@
|
|
|
36
36
|
|
|
37
37
|
/** Forward-slash a path and drop any leading `./` or `/` noise used for anchoring. */
|
|
38
38
|
function normalizeForMatch(value) {
|
|
39
|
+
// eslint-disable-next-line local/no-manual-path-normalize -- the rule pack is standalone CommonJS and cannot import the ESM helper; config paths and linted filenames are matched separator-agnostically on purpose.
|
|
39
40
|
return String(value).replaceAll('\\', '/').replace(/^(?:\.\/)+/, '');
|
|
40
41
|
}
|
|
41
42
|
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ESLint rule: explicit-zod-strictness
|
|
3
|
+
*
|
|
4
|
+
* Every `z.object({...})` must say, in the same method chain, what it does
|
|
5
|
+
* with a key it does not declare: `.strict()` (refuse), `.passthrough()` /
|
|
6
|
+
* `.loose()` (keep), or `.strip()` (drop — said out loud, for the write-side
|
|
7
|
+
* whitelist whose whole point is dropping). Zod's DEFAULT is that third answer
|
|
8
|
+
* left unsaid, and that is the one this repo cannot afford: CLAUDE.md retired
|
|
9
|
+
* every hand-bumped version integer on the promise that a strict schema is
|
|
10
|
+
* what refuses stale stored data, and a default-strip schema keeps none of that
|
|
11
|
+
* promise. It also hides the adopter-config trap the other way round: a key
|
|
12
|
+
* the schema strips on the way in is a key the adopter thinks is honoured.
|
|
13
|
+
*
|
|
14
|
+
* `z.strictObject()` and `z.looseObject()` are explicit by construction and
|
|
15
|
+
* pass. The marker may sit anywhere later in the chain (`.describe('…')
|
|
16
|
+
* .strict()`, `.strict().optional()`), and `.extend()` / `.partial()` /
|
|
17
|
+
* `.pick()` inherit the policy of what they were called on, so a chain that
|
|
18
|
+
* reaches a marker through them is explicit too.
|
|
19
|
+
*
|
|
20
|
+
* Option `allowDefaultStripIn: string[]` — repo-relative paths of files whose
|
|
21
|
+
* `z.object` calls may keep the default. This is a ratchet list, not a scope:
|
|
22
|
+
* name files, not directories, so a new default-strip schema cannot arrive
|
|
23
|
+
* unannounced under an allowed prefix.
|
|
24
|
+
*
|
|
25
|
+
* @example
|
|
26
|
+
* // BAD — an unknown key vanishes without a word
|
|
27
|
+
* const S = z.object({ version: z.string() });
|
|
28
|
+
*
|
|
29
|
+
* // GOOD — the policy is written where the shape is
|
|
30
|
+
* const S = z.object({ version: z.string() }).strict();
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
'use strict';
|
|
34
|
+
|
|
35
|
+
const { createExemptPathMatcher } = require('./exempt-path-matcher.cjs');
|
|
36
|
+
|
|
37
|
+
/** Chain methods that decide the unknown-key policy. */
|
|
38
|
+
const EXPLICIT_METHODS = new Set(['strict', 'passthrough', 'loose', 'strip']);
|
|
39
|
+
|
|
40
|
+
/** Whether `node` is a `z.object(...)` call. */
|
|
41
|
+
function isZodObjectCall(node) {
|
|
42
|
+
const { callee } = node;
|
|
43
|
+
return (
|
|
44
|
+
callee.type === 'MemberExpression' &&
|
|
45
|
+
!callee.computed &&
|
|
46
|
+
callee.object.type === 'Identifier' &&
|
|
47
|
+
callee.object.name === 'z' &&
|
|
48
|
+
callee.property.type === 'Identifier' &&
|
|
49
|
+
callee.property.name === 'object'
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Whether the method chain that starts at `call` reaches an explicit
|
|
55
|
+
* unknown-key method. Follows `<call>.<method>(...)` links only: the moment
|
|
56
|
+
* the value is stored, passed, or returned the chain has ended and no later
|
|
57
|
+
* `.strict()` on some other expression counts for this object.
|
|
58
|
+
*/
|
|
59
|
+
function chainDeclaresPolicy(call) {
|
|
60
|
+
let current = call;
|
|
61
|
+
for (;;) {
|
|
62
|
+
const member = current.parent;
|
|
63
|
+
if (member?.type !== 'MemberExpression' || member.object !== current || member.computed) {
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
66
|
+
const outer = member.parent;
|
|
67
|
+
if (outer?.type !== 'CallExpression' || outer.callee !== member) {
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
if (member.property.type === 'Identifier' && EXPLICIT_METHODS.has(member.property.name)) {
|
|
71
|
+
return true;
|
|
72
|
+
}
|
|
73
|
+
current = outer;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
module.exports = {
|
|
78
|
+
meta: {
|
|
79
|
+
type: 'problem',
|
|
80
|
+
docs: {
|
|
81
|
+
description:
|
|
82
|
+
'Require every z.object({...}) to declare its unknown-key policy in the same chain — ' +
|
|
83
|
+
'.strict(), .passthrough(), .loose() or an explicit .strip() — because the default silently strips keys',
|
|
84
|
+
recommended: false,
|
|
85
|
+
recommendedSeverity: 'warn',
|
|
86
|
+
},
|
|
87
|
+
schema: [
|
|
88
|
+
{
|
|
89
|
+
type: 'object',
|
|
90
|
+
properties: {
|
|
91
|
+
allowDefaultStripIn: { type: 'array', items: { type: 'string' }, uniqueItems: true },
|
|
92
|
+
},
|
|
93
|
+
additionalProperties: false,
|
|
94
|
+
},
|
|
95
|
+
],
|
|
96
|
+
messages: {
|
|
97
|
+
defaultStrip:
|
|
98
|
+
'This z.object() keeps Zod\'s default of silently DROPPING unknown keys. Say what you mean: ' +
|
|
99
|
+
'.strict() refuses them (the default for anything VAT stores or reads back), ' +
|
|
100
|
+
'.passthrough() keeps them (only for a document some other party owns — say who, beside it), ' +
|
|
101
|
+
'.strip() drops them on purpose (a write-side whitelist — say why, beside it).',
|
|
102
|
+
},
|
|
103
|
+
},
|
|
104
|
+
|
|
105
|
+
create(context) {
|
|
106
|
+
const allowed = context.options?.[0]?.allowDefaultStripIn ?? [];
|
|
107
|
+
const filename = context.filename ?? context.getFilename();
|
|
108
|
+
if (createExemptPathMatcher(allowed)(filename)) {
|
|
109
|
+
return {};
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return {
|
|
113
|
+
CallExpression(node) {
|
|
114
|
+
if (isZodObjectCall(node) && !chainDeclaresPolicy(node)) {
|
|
115
|
+
context.report({ node, messageId: 'defaultStrip' });
|
|
116
|
+
}
|
|
117
|
+
},
|
|
118
|
+
};
|
|
119
|
+
},
|
|
120
|
+
};
|
|
@@ -114,8 +114,12 @@ module.exports = {
|
|
|
114
114
|
docs: {
|
|
115
115
|
description:
|
|
116
116
|
'Disallow dynamic `import()` of a filesystem path; wrap with `pathToFileURL(p).href`.',
|
|
117
|
-
category: '
|
|
117
|
+
category: 'URLs and dynamic imports',
|
|
118
|
+
bans: '`await import(absolutePath)`',
|
|
119
|
+
useInstead: '`dynamicImportPath()` / `pathToFileURL(p).href`',
|
|
120
|
+
subpath: '/fs',
|
|
118
121
|
recommended: true,
|
|
122
|
+
recommendedSeverity: 'error',
|
|
119
123
|
},
|
|
120
124
|
messages: {
|
|
121
125
|
useFileUrl:
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ESLint rule: no-bare-executable-spawn
|
|
3
|
+
*
|
|
4
|
+
* Flags `spawn`, `spawnSync`, `execFile`, `execFileSync` or `execSync` whose
|
|
5
|
+
* first argument is the string literal `'git'` or `'node'` — bare or through
|
|
6
|
+
* a namespace (`cp.spawnSync('git', …)`).
|
|
7
|
+
*
|
|
8
|
+
* A bare name asks the OS to search `PATH` at spawn time, so a writable
|
|
9
|
+
* directory on `PATH` is a place to plant a binary (SonarCloud S4036), and
|
|
10
|
+
* `node` by name may not be the node running the caller. Resolve once and
|
|
11
|
+
* spawn the absolute path: `process.execPath` for node in source;
|
|
12
|
+
* `NODE_EXECUTABLE` / `gitExecutable()` from `@vibe-agent-toolkit/utils/testing`
|
|
13
|
+
* in tests. Sonar sees new code only and named 13 of 84 sites; this rule sees
|
|
14
|
+
* every file, so the class cannot regrow one PR at a time.
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* // BAD
|
|
18
|
+
* spawnSync('git', ['status'], { cwd });
|
|
19
|
+
*
|
|
20
|
+
* // GOOD
|
|
21
|
+
* spawnSync(gitExecutable(), ['status'], { cwd });
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
'use strict';
|
|
25
|
+
|
|
26
|
+
const { calleeName } = require('./callee-name.cjs');
|
|
27
|
+
|
|
28
|
+
const SPAWN_CALLS = new Set(['spawn', 'spawnSync', 'execFile', 'execFileSync', 'execSync']);
|
|
29
|
+
const BARE_NAMES = new Set(['git', 'node']);
|
|
30
|
+
|
|
31
|
+
/** The spawn-family callee name of a call, bare or namespaced, or null. */
|
|
32
|
+
function spawnCallName(call) {
|
|
33
|
+
const name = calleeName(call);
|
|
34
|
+
return name !== null && SPAWN_CALLS.has(name) ? name : null;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
module.exports = {
|
|
38
|
+
meta: {
|
|
39
|
+
type: 'problem',
|
|
40
|
+
docs: {
|
|
41
|
+
description:
|
|
42
|
+
"Disallow spawning 'git' or 'node' by bare name — resolve the executable once " +
|
|
43
|
+
'(process.execPath; NODE_EXECUTABLE / gitExecutable() in tests) and spawn the absolute path',
|
|
44
|
+
category: 'Filesystem and process',
|
|
45
|
+
recommended: true,
|
|
46
|
+
recommendedSeverity: 'error',
|
|
47
|
+
},
|
|
48
|
+
schema: [],
|
|
49
|
+
messages: {
|
|
50
|
+
bareName:
|
|
51
|
+
"{{call}}('{{name}}', …) searches PATH at spawn time. Spawn an absolute path: " +
|
|
52
|
+
"process.execPath for node; NODE_EXECUTABLE / gitExecutable() from '@vibe-agent-toolkit/utils/testing' in tests.",
|
|
53
|
+
},
|
|
54
|
+
},
|
|
55
|
+
|
|
56
|
+
create(context) {
|
|
57
|
+
return {
|
|
58
|
+
CallExpression(node) {
|
|
59
|
+
const call = spawnCallName(node);
|
|
60
|
+
if (call === null) {
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
const first = node.arguments[0];
|
|
64
|
+
if (first?.type === 'Literal' && typeof first.value === 'string' && BARE_NAMES.has(first.value)) {
|
|
65
|
+
context.report({ node: first, messageId: 'bareName', data: { call, name: first.value } });
|
|
66
|
+
}
|
|
67
|
+
},
|
|
68
|
+
};
|
|
69
|
+
},
|
|
70
|
+
};
|