@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,213 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ESLint rule: no-bare-symlink-in-tests
|
|
3
|
+
*
|
|
4
|
+
* Bans unguarded `fs.symlinkSync()` / `fs.promises.symlink()`, with a different
|
|
5
|
+
* remedy on each side of the test boundary:
|
|
6
|
+
*
|
|
7
|
+
* - **test files** → route through `createSymlink()` / `createSymlinkAsync()`
|
|
8
|
+
* from `@vibe-agent-toolkit/utils`, which require a probed capability token.
|
|
9
|
+
* - **shipped code** → there is no wrapper to route through, and there must not
|
|
10
|
+
* be: `createSymlink()` lives on the `utils/testing` subpath, so pointing
|
|
11
|
+
* production code at it would be worse advice than the bare call. Prefer a
|
|
12
|
+
* junction for a directory link on win32, or catch the failure and name the
|
|
13
|
+
* missing privilege. An `eslint-disable` justification is the sanctioned way
|
|
14
|
+
* to say "this platform is deliberately out of scope" — see
|
|
15
|
+
* `cli/src/commands/agent/install.ts`, where `--dev` is knowingly unavailable
|
|
16
|
+
* on an unprivileged Windows and fails saying exactly that.
|
|
17
|
+
*
|
|
18
|
+
* ⚠️ **The name is now narrower than the rule.** It covers shipped code too;
|
|
19
|
+
* renaming it is a public-API change to `@vibe-agent-toolkit/utils/eslint` and
|
|
20
|
+
* has not been done.
|
|
21
|
+
*
|
|
22
|
+
* Why: creating a symlink on Windows needs Developer Mode or
|
|
23
|
+
* `SeCreateSymbolicLinkPrivilege`, which most dev boxes and CI agents lack. A
|
|
24
|
+
* bare `symlinkSync()` call throws `EPERM` there with no visible skip — the
|
|
25
|
+
* test just fails, or (worse) the failure is masked by a `try`/`catch` that
|
|
26
|
+
* swallows it silently. `createSymlink()`/`createSymlinkAsync()` require a
|
|
27
|
+
* `SymlinkCapability` token as their first argument, and the only way to mint
|
|
28
|
+
* one is `symlinkCapability()`, which performs the real probe. That token
|
|
29
|
+
* requirement is what this rule enforces structurally: a test cannot reach the
|
|
30
|
+
* real syscall without first proving — or explicitly declining via vitest's
|
|
31
|
+
* `skip()` — that the host supports it.
|
|
32
|
+
*
|
|
33
|
+
* No auto-fix: unlike a straight rename, the replacement needs a capability
|
|
34
|
+
* token threaded from a probe call, which is a judgment call about where that
|
|
35
|
+
* probe belongs in the surrounding test (per-test via `{ skip }`, or hoisted
|
|
36
|
+
* to a shared `beforeAll`) that a mechanical fixer cannot make safely.
|
|
37
|
+
*/
|
|
38
|
+
|
|
39
|
+
const {
|
|
40
|
+
EXEMPT_FILES_SCHEMA,
|
|
41
|
+
UNANCHORED_EXEMPT_FILE,
|
|
42
|
+
UNANCHORED_EXEMPT_MESSAGE,
|
|
43
|
+
createConfigurableExemptPathMatcher,
|
|
44
|
+
isTestFile,
|
|
45
|
+
reportUnanchoredExemptEntries,
|
|
46
|
+
} = require('./exempt-path-matcher.cjs');
|
|
47
|
+
|
|
48
|
+
/** `node:fs` names carrying the sync primitive. */
|
|
49
|
+
const SYNC_MODULES = new Set(['node:fs', 'fs']);
|
|
50
|
+
/** `node:fs/promises` names carrying the async primitive. */
|
|
51
|
+
const ASYNC_MODULES = new Set(['node:fs/promises', 'fs/promises']);
|
|
52
|
+
|
|
53
|
+
module.exports = {
|
|
54
|
+
meta: {
|
|
55
|
+
type: 'problem',
|
|
56
|
+
docs: {
|
|
57
|
+
description:
|
|
58
|
+
'Ban unguarded fs.symlinkSync()/fs.symlink() — route tests through createSymlink()/createSymlinkAsync(), and guard shipped code against the Windows privilege requirement',
|
|
59
|
+
category: 'Filesystem and process',
|
|
60
|
+
bans: 'unguarded `fs.symlinkSync()` / `fs.promises.symlink()`',
|
|
61
|
+
useInstead: 'in tests: `createSymlink(cap, …)` / `createSymlinkAsync(cap, …)`; in shipped code: a win32 junction, or a `catch` naming the privilege',
|
|
62
|
+
subpath: '/testing',
|
|
63
|
+
// Not in `recommended`: half its advice is unreachable without a helper the
|
|
64
|
+
// consumer may not have. In a test file it points at `createSymlink()` /
|
|
65
|
+
// `createSymlinkAsync()` on THIS package's `./testing` subpath, routed through
|
|
66
|
+
// a probed capability token and the vitest-specific `skip()` idiom; an adopter
|
|
67
|
+
// on a different runner should not silently inherit that. Its shipped-code
|
|
68
|
+
// half (`unguardedSymlink`) is portable advice on its own — the two share one
|
|
69
|
+
// rule id and cannot be enabled separately, and the test half is what keeps
|
|
70
|
+
// the pair out. VAT enables the whole rule explicitly.
|
|
71
|
+
recommended: false,
|
|
72
|
+
recommendedSeverity: 'error',
|
|
73
|
+
},
|
|
74
|
+
fixable: null,
|
|
75
|
+
schema: [EXEMPT_FILES_SCHEMA],
|
|
76
|
+
messages: {
|
|
77
|
+
noBareSymlink:
|
|
78
|
+
'Bare {{fn}}() in a test file can throw EPERM on Windows without Developer Mode. ' +
|
|
79
|
+
'Probe with symlinkCapability() and call {{safeFn}}(cap, ...) from @vibe-agent-toolkit/utils, ' +
|
|
80
|
+
"routing a missing capability through vitest's skip() rather than a silent return.",
|
|
81
|
+
// Deliberately a different remedy, not a reworded version of the same one.
|
|
82
|
+
// Production code cannot `skip()`, and `createSymlink()` lives on the
|
|
83
|
+
// `@vibe-agent-toolkit/utils/testing` subpath — telling shipped code to
|
|
84
|
+
// import a test helper would be worse advice than the bare call.
|
|
85
|
+
unguardedSymlink:
|
|
86
|
+
'Unguarded {{fn}}() will throw EPERM on Windows unless the process holds ' +
|
|
87
|
+
'SeCreateSymbolicLinkPrivilege (Developer Mode or an elevated shell) — most user machines ' +
|
|
88
|
+
'and CI agents do not. For a DIRECTORY link prefer a junction on win32 ' +
|
|
89
|
+
"(`process.platform === 'win32' ? 'junction' : 'dir'`, absolute target), which needs no " +
|
|
90
|
+
'elevation; otherwise catch the failure and say what privilege is missing, or degrade to a copy. ' +
|
|
91
|
+
'If this platform is deliberately out of scope, say so in an eslint-disable justification.',
|
|
92
|
+
[UNANCHORED_EXEMPT_FILE]: UNANCHORED_EXEMPT_MESSAGE,
|
|
93
|
+
},
|
|
94
|
+
},
|
|
95
|
+
|
|
96
|
+
create(context) {
|
|
97
|
+
const filename = context.getFilename();
|
|
98
|
+
|
|
99
|
+
const exemptMatcherFor = createConfigurableExemptPathMatcher([]);
|
|
100
|
+
if (exemptMatcherFor(context)(filename)) {
|
|
101
|
+
// Still surface a malformed exemption list: the file we are standing in
|
|
102
|
+
// may be exempt only BECAUSE the entry is unanchored.
|
|
103
|
+
return {
|
|
104
|
+
Program(node) {
|
|
105
|
+
reportUnanchoredExemptEntries(context, node);
|
|
106
|
+
},
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Test files and shipped code are both covered, with different remedies.
|
|
111
|
+
//
|
|
112
|
+
// The rule was test-only for one revision, and that left two blind spots
|
|
113
|
+
// that mattered: VAT's own `src/` fixture builders (`trap-corpus.ts` had to
|
|
114
|
+
// be migrated by hand precisely because no rule could see it), and — since
|
|
115
|
+
// this rule ships publicly on `@vibe-agent-toolkit/utils/eslint` — every
|
|
116
|
+
// adopter's production code, which faces the identical Windows hazard with
|
|
117
|
+
// none of the test lane's ability to skip.
|
|
118
|
+
//
|
|
119
|
+
// ⚠️ Extending it is what MAKES `exemptFiles` load-bearing:
|
|
120
|
+
// `packages/utils/src/test-helpers.ts` holds the one sanctioned
|
|
121
|
+
// `symlinkSync` call and is not a test file, so it was previously excluded
|
|
122
|
+
// for free. It now needs a real exemption entry in eslint.config.js.
|
|
123
|
+
const messageId = isTestFile(filename) ? 'noBareSymlink' : 'unguardedSymlink';
|
|
124
|
+
|
|
125
|
+
// Local names bound to each module's default/namespace import, e.g.
|
|
126
|
+
// `import fs from 'node:fs/promises'` binds `fs` to ASYNC_MODULES.
|
|
127
|
+
const syncNamespaceNames = new Set();
|
|
128
|
+
const asyncNamespaceNames = new Set();
|
|
129
|
+
// Whether `symlinkSync` / `symlink` were pulled in as bare named imports.
|
|
130
|
+
let syncNamedImported = false;
|
|
131
|
+
let asyncNamedImported = false;
|
|
132
|
+
|
|
133
|
+
function namespaceLocalName(importNode) {
|
|
134
|
+
const spec = importNode.specifiers.find(
|
|
135
|
+
(candidate) => candidate.type === 'ImportDefaultSpecifier' || candidate.type === 'ImportNamespaceSpecifier',
|
|
136
|
+
);
|
|
137
|
+
return spec ? spec.local.name : null;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function importsNamed(importNode, name) {
|
|
141
|
+
return importNode.specifiers.some(
|
|
142
|
+
(spec) => spec.type === 'ImportSpecifier' && spec.imported.name === name,
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return {
|
|
147
|
+
Program(node) {
|
|
148
|
+
reportUnanchoredExemptEntries(context, node);
|
|
149
|
+
},
|
|
150
|
+
|
|
151
|
+
ImportDeclaration(node) {
|
|
152
|
+
const source = node.source.value;
|
|
153
|
+
if (SYNC_MODULES.has(source)) {
|
|
154
|
+
const local = namespaceLocalName(node);
|
|
155
|
+
if (local) syncNamespaceNames.add(local);
|
|
156
|
+
syncNamedImported = syncNamedImported || importsNamed(node, 'symlinkSync');
|
|
157
|
+
}
|
|
158
|
+
if (ASYNC_MODULES.has(source)) {
|
|
159
|
+
const local = namespaceLocalName(node);
|
|
160
|
+
if (local) asyncNamespaceNames.add(local);
|
|
161
|
+
asyncNamedImported = asyncNamedImported || importsNamed(node, 'symlink');
|
|
162
|
+
}
|
|
163
|
+
},
|
|
164
|
+
|
|
165
|
+
CallExpression(node) {
|
|
166
|
+
const { callee } = node;
|
|
167
|
+
|
|
168
|
+
// Bare `symlinkSync(...)` from a named import.
|
|
169
|
+
if (syncNamedImported && callee.type === 'Identifier' && callee.name === 'symlinkSync') {
|
|
170
|
+
context.report({ node, messageId, data: { fn: 'symlinkSync', safeFn: 'createSymlink' } });
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Bare `symlink(...)` from a named `node:fs/promises` import.
|
|
175
|
+
if (asyncNamedImported && callee.type === 'Identifier' && callee.name === 'symlink') {
|
|
176
|
+
context.report({ node, messageId, data: { fn: 'symlink', safeFn: 'createSymlinkAsync' } });
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// `fs.promises.symlink(...)` — the two-hop shape reached via a tracked
|
|
181
|
+
// sync namespace import (`import fs from 'node:fs'`).
|
|
182
|
+
if (
|
|
183
|
+
callee.type === 'MemberExpression' &&
|
|
184
|
+
callee.property.name === 'symlink' &&
|
|
185
|
+
callee.object.type === 'MemberExpression' &&
|
|
186
|
+
callee.object.property.name === 'promises' &&
|
|
187
|
+
callee.object.object.type === 'Identifier' &&
|
|
188
|
+
syncNamespaceNames.has(callee.object.object.name)
|
|
189
|
+
) {
|
|
190
|
+
context.report({ node, messageId, data: { fn: 'symlink', safeFn: 'createSymlinkAsync' } });
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
if (callee.type !== 'MemberExpression' || callee.object.type !== 'Identifier') {
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
const receiver = callee.object.name;
|
|
198
|
+
|
|
199
|
+
// `fs.symlinkSync(...)` / `nodeFs.symlinkSync(...)` on a tracked sync namespace.
|
|
200
|
+
if (syncNamespaceNames.has(receiver) && callee.property.name === 'symlinkSync') {
|
|
201
|
+
context.report({ node, messageId, data: { fn: 'symlinkSync', safeFn: 'createSymlink' } });
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// `fs.symlink(...)` on a tracked async namespace (the common
|
|
206
|
+
// `import fs from 'node:fs/promises'` shape).
|
|
207
|
+
if (asyncNamespaceNames.has(receiver) && callee.property.name === 'symlink') {
|
|
208
|
+
context.report({ node, messageId, data: { fn: 'symlink', safeFn: 'createSymlinkAsync' } });
|
|
209
|
+
}
|
|
210
|
+
},
|
|
211
|
+
};
|
|
212
|
+
},
|
|
213
|
+
};
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ESLint rule: no-blind-catch
|
|
3
|
+
*
|
|
4
|
+
* Flags a `catch` clause that never LOOKS at its error and never THROWS. Such
|
|
5
|
+
* a clause answers every failure the same way — a sentinel `return null`, a
|
|
6
|
+
* `continue`, an empty body, a fallback assignment — so the case it was
|
|
7
|
+
* written for (a file that is not there) becomes indistinguishable from the
|
|
8
|
+
* cases it was not (a permission refusal, a corrupt artifact, a `TypeError`
|
|
9
|
+
* from a bug two frames down). The result is a tool that is quietest exactly
|
|
10
|
+
* when it is most wrong: a directory the OS refused to list reports as empty,
|
|
11
|
+
* and a build that examined nothing exits 0.
|
|
12
|
+
*
|
|
13
|
+
* This is the class behind the worst finding in seven consecutive adversarial
|
|
14
|
+
* review rounds on one branch. A crawler learned to REFUSE an unreadable
|
|
15
|
+
* directory instead of skipping it; one caller's `catch { return null }`
|
|
16
|
+
* absorbed the refusal, and `vat audit` silently ran with no population. The
|
|
17
|
+
* fix was type-forcing the seam so `tsc` enumerated the callers — but a blind
|
|
18
|
+
* `catch` is the one seam `tsc` cannot see, because it compiles for every
|
|
19
|
+
* callee that ever learns to throw.
|
|
20
|
+
*
|
|
21
|
+
* ## What counts as handling
|
|
22
|
+
*
|
|
23
|
+
* The rule is a FLOOR, deliberately syntactic: the clause is fine if either
|
|
24
|
+
*
|
|
25
|
+
* 1. the error binding is REFERENCED anywhere in the body — narrowing on it
|
|
26
|
+
* (`isFilesystemAccessError(e)`, `e instanceof X`, `e.code === 'ENOENT'`),
|
|
27
|
+
* carrying it into a report (`errors.push(String(e))`, `{ ok: false, error }`),
|
|
28
|
+
* or logging it — or
|
|
29
|
+
* 2. the body THROWS (a rethrow, or a translation into a louder error), at the
|
|
30
|
+
* clause's own level — a `throw` inside a nested function is a promise to
|
|
31
|
+
* fail later, not a rethrow from this catch.
|
|
32
|
+
*
|
|
33
|
+
* A reference is a reference: the rule cannot tell `narrow(e)` from `log(e)`,
|
|
34
|
+
* and does not try. What it guarantees is the weaker, enforceable property —
|
|
35
|
+
* *the error was looked at before being discarded* — which is the property
|
|
36
|
+
* every one of the shipped defects lacked.
|
|
37
|
+
*
|
|
38
|
+
* ## What this rule does NOT catch
|
|
39
|
+
*
|
|
40
|
+
* - `catch (e) { void e; return null; }` — a reference written to dodge the
|
|
41
|
+
* rule. Code review's job.
|
|
42
|
+
* - A narrowing that is wrong: `if (e instanceof Error) return null` looks at
|
|
43
|
+
* the error and still absorbs everything. The rule sees a reference.
|
|
44
|
+
* - A catch whose body references the binding only to build a message it
|
|
45
|
+
* then throws away.
|
|
46
|
+
*
|
|
47
|
+
* There is deliberately NO annotation escape hatch. Every site this rule
|
|
48
|
+
* flags has a legitimate rewrite — narrow and rethrow, or carry the error into
|
|
49
|
+
* the result — and an `eslint-disable` with a reason is already the escape
|
|
50
|
+
* hatch ESLint provides, visible in the diff and countable with
|
|
51
|
+
* `rg 'eslint-disable.*no-blind-catch'`.
|
|
52
|
+
*
|
|
53
|
+
* @example
|
|
54
|
+
* // BAD — "not there" and "refused" and "bug" all become null
|
|
55
|
+
* try { return statSync(p); } catch { return null; }
|
|
56
|
+
*
|
|
57
|
+
* // GOOD — narrowed to the case the sentinel means, everything else stays loud
|
|
58
|
+
* try { return statSync(p); } catch (e) {
|
|
59
|
+
* if (isPathAbsentError(e)) return null;
|
|
60
|
+
* throw e;
|
|
61
|
+
* }
|
|
62
|
+
*
|
|
63
|
+
* // GOOD — the error reaches the report instead of vanishing
|
|
64
|
+
* try { return parse(text); } catch (e) {
|
|
65
|
+
* return { ok: false, reason: String(e) };
|
|
66
|
+
* }
|
|
67
|
+
*/
|
|
68
|
+
|
|
69
|
+
'use strict';
|
|
70
|
+
|
|
71
|
+
/** Function-boundary node types: a `throw` inside one is not this clause's throw. */
|
|
72
|
+
const FUNCTION_TYPES = new Set([
|
|
73
|
+
'FunctionDeclaration',
|
|
74
|
+
'FunctionExpression',
|
|
75
|
+
'ArrowFunctionExpression',
|
|
76
|
+
]);
|
|
77
|
+
|
|
78
|
+
/** Keys on an ESTree node that point somewhere other than a child. */
|
|
79
|
+
const NON_CHILD_KEYS = new Set(['parent', 'loc', 'range']);
|
|
80
|
+
|
|
81
|
+
/** Whether `value` is an ESTree node (as opposed to a token, a literal, or null). */
|
|
82
|
+
function isNode(value) {
|
|
83
|
+
return value !== null && typeof value === 'object' && typeof value.type === 'string';
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** Every child node of `node`, in no particular order. */
|
|
87
|
+
function childNodes(node) {
|
|
88
|
+
const children = [];
|
|
89
|
+
for (const key of Object.keys(node)) {
|
|
90
|
+
if (NON_CHILD_KEYS.has(key)) {
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
const value = node[key];
|
|
94
|
+
const values = Array.isArray(value) ? value : [value];
|
|
95
|
+
children.push(...values.filter((child) => isNode(child)));
|
|
96
|
+
}
|
|
97
|
+
return children;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Whether `body` contains a `ThrowStatement` reachable without crossing a
|
|
102
|
+
* function boundary. Iterative so a deeply nested body cannot blow the stack.
|
|
103
|
+
*/
|
|
104
|
+
function throwsAtOwnLevel(body) {
|
|
105
|
+
const stack = [body];
|
|
106
|
+
while (stack.length > 0) {
|
|
107
|
+
const node = stack.pop();
|
|
108
|
+
if (node.type === 'ThrowStatement') {
|
|
109
|
+
return true;
|
|
110
|
+
}
|
|
111
|
+
if (!FUNCTION_TYPES.has(node.type)) {
|
|
112
|
+
stack.push(...childNodes(node));
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
return false;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Whether any binding the clause declares (`catch (e)`, `catch ({ code })`) is
|
|
120
|
+
* read in the body. A clause with no param declares nothing and reads nothing.
|
|
121
|
+
*/
|
|
122
|
+
function readsErrorBinding(sourceCode, node) {
|
|
123
|
+
if (node.param === null) {
|
|
124
|
+
return false;
|
|
125
|
+
}
|
|
126
|
+
const declared = sourceCode.getDeclaredVariables(node);
|
|
127
|
+
return declared.some((variable) => variable.references.length > 0);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
module.exports = {
|
|
131
|
+
meta: {
|
|
132
|
+
type: 'problem',
|
|
133
|
+
docs: {
|
|
134
|
+
description:
|
|
135
|
+
'Disallow a catch clause that neither reads its error nor throws — it absorbs a bug, a ' +
|
|
136
|
+
'permission refusal, or a corrupt artifact into the same answer as the case it was written for',
|
|
137
|
+
category: 'Error handling',
|
|
138
|
+
bans: 'a `catch` that neither reads its error nor throws',
|
|
139
|
+
useInstead: 'narrow on the error and rethrow the rest, or carry it into the result',
|
|
140
|
+
// `warn`, not `error`: the same migration-volume criterion as
|
|
141
|
+
// `no-raw-node-path`, without the autofix. A blind `catch` has no
|
|
142
|
+
// mechanical rewrite — each site is a decision about WHICH failure the
|
|
143
|
+
// sentinel stands for — and the first run on an existing tree is a long
|
|
144
|
+
// list (202 sites on this repo's own ~200k lines). `warn` makes that a
|
|
145
|
+
// backlog to burn down rather than a wall; the hazard is real at every one.
|
|
146
|
+
recommended: true,
|
|
147
|
+
recommendedSeverity: 'warn',
|
|
148
|
+
},
|
|
149
|
+
schema: [],
|
|
150
|
+
messages: {
|
|
151
|
+
blindCatch:
|
|
152
|
+
'This catch discards the error without looking at it, so a bug, a permission refusal, or a ' +
|
|
153
|
+
'corrupt artifact is absorbed into the same result as the case it was written for. Narrow it ' +
|
|
154
|
+
"to that case and rethrow the rest — `if (isPathAbsentError(e)) return null; throw e;` — or " +
|
|
155
|
+
'carry the error into the result so the report can show it.',
|
|
156
|
+
},
|
|
157
|
+
},
|
|
158
|
+
|
|
159
|
+
create(context) {
|
|
160
|
+
const sourceCode = context.sourceCode ?? context.getSourceCode();
|
|
161
|
+
|
|
162
|
+
return {
|
|
163
|
+
CatchClause(node) {
|
|
164
|
+
if (readsErrorBinding(sourceCode, node) || throwsAtOwnLevel(node.body)) {
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
context.report({ node, messageId: 'blindCatch' });
|
|
168
|
+
},
|
|
169
|
+
};
|
|
170
|
+
},
|
|
171
|
+
};
|
|
@@ -20,4 +20,12 @@ module.exports = factory({
|
|
|
20
20
|
message: 'Use safeExecSync() from {{safeModule}} instead of child_process.execSync() to prevent command injection (security + cross-platform)',
|
|
21
21
|
// No baked-in exemption: the file that implements safeExecSync() is
|
|
22
22
|
// repo-specific. Consumers declare it as { exemptFiles: [...] }.
|
|
23
|
+
docs: {
|
|
24
|
+
category: 'Filesystem and process',
|
|
25
|
+
bans: '`child_process.execSync()`',
|
|
26
|
+
useInstead: '`safeExecSync()`',
|
|
27
|
+
subpath: '/process',
|
|
28
|
+
recommended: true,
|
|
29
|
+
recommendedSeverity: 'error',
|
|
30
|
+
},
|
|
23
31
|
});
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ESLint rule: no-decaying-referent
|
|
3
|
+
*
|
|
4
|
+
* Flags, in comments under `src/`, three kinds of referent that rot in place:
|
|
5
|
+
*
|
|
6
|
+
* - an issue or PR number (`#145`) — the issue closes, the PR merges, and the
|
|
7
|
+
* comment keeps pointing at a discussion nobody reopens;
|
|
8
|
+
* - an ISO date (`2026-08-22`, `2026-08`) — "re-derived on" is a claim about
|
|
9
|
+
* freshness that is false the day after it is written;
|
|
10
|
+
* - a named person (`Jeff`, configurable) — a ruling attributed to someone is a
|
|
11
|
+
* ruling nobody else feels entitled to revisit.
|
|
12
|
+
*
|
|
13
|
+
* None of these is the RULE the comment exists to state. The rule stays; the
|
|
14
|
+
* history goes where history is kept — the commit message, the CHANGELOG, or
|
|
15
|
+
* `docs/contributing/` — and `git log -L` holds the rest without decaying.
|
|
16
|
+
*
|
|
17
|
+
* Scope: files with a `/src/` segment that are not test files. Tests and
|
|
18
|
+
* scripts are not linted here; Markdown never reaches ESLint. One report per
|
|
19
|
+
* comment block, on the FIRST referent found, whichever kind it is — the fix is
|
|
20
|
+
* to rewrite the comment, not to hunt tokens one at a time.
|
|
21
|
+
*
|
|
22
|
+
* Options:
|
|
23
|
+
* - `names: string[]` — whole-word, case-sensitive names to treat as referents
|
|
24
|
+
* (default `['Jeff']`). Pass `[]` to disable the class.
|
|
25
|
+
* - `allowDates: boolean` — leave dates alone (default `false`).
|
|
26
|
+
*
|
|
27
|
+
* One line shape is exempt without an option: `@vendor-claim reviewed=<date>
|
|
28
|
+
* verify=<how>`. That date is READ by a freshness gate that fails the build when
|
|
29
|
+
* it ages past its budget — a date with a mechanism behind it is the opposite of
|
|
30
|
+
* a decaying one. The exemption covers only the line carrying the tag.
|
|
31
|
+
*
|
|
32
|
+
* @example
|
|
33
|
+
* // BAD — the reader must open #145 to learn what the code does
|
|
34
|
+
* // Fixed in #145 (Jeff, 2026-08-22): skip the second crawl.
|
|
35
|
+
*
|
|
36
|
+
* // GOOD — the rule, stated; the history is in `git log -L`
|
|
37
|
+
* // The second crawl is skipped: the first already answered from git.
|
|
38
|
+
*/
|
|
39
|
+
|
|
40
|
+
'use strict';
|
|
41
|
+
|
|
42
|
+
const { isTestFile } = require('./exempt-path-matcher.cjs');
|
|
43
|
+
|
|
44
|
+
/** `#12` … `#1234`, not preceded by a word/URL/entity character and not part of a longer number. */
|
|
45
|
+
const ISSUE_REF = /(?<![\w/&#])#\d{2,4}(?!\d)/u;
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* `20YY-MM` or `20YY-MM-DD`, standing alone. Glued to a word, a hyphen or a
|
|
49
|
+
* path separator it names a thing (`sweep-2026-09-12/`, a JSON Schema dialect's
|
|
50
|
+
* `draft/2020-12/schema`), not a day something was true.
|
|
51
|
+
*/
|
|
52
|
+
const ISO_DATE = /(?<![\w/-])20\d\d-\d\d(?:-\d\d)?(?![\w-])/u;
|
|
53
|
+
|
|
54
|
+
/** A character that continues a word, for the whole-word check on names. */
|
|
55
|
+
const WORD_CHAR = /\w/u;
|
|
56
|
+
|
|
57
|
+
/** The one annotation whose date is machine-checked rather than left to rot. */
|
|
58
|
+
const VENDOR_CLAIM_TAG = '@vendor-claim';
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* `text` with every line carrying {@link VENDOR_CLAIM_TAG} blanked to spaces
|
|
62
|
+
* of the same length, so match indices still map onto the original comment.
|
|
63
|
+
*/
|
|
64
|
+
function blankVendorClaimLines(text) {
|
|
65
|
+
if (!text.includes(VENDOR_CLAIM_TAG)) {
|
|
66
|
+
return text;
|
|
67
|
+
}
|
|
68
|
+
return text
|
|
69
|
+
.split('\n')
|
|
70
|
+
.map((line) => (line.includes(VENDOR_CLAIM_TAG) ? ' '.repeat(line.length) : line))
|
|
71
|
+
.join('\n');
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* The first whole-word, case-sensitive occurrence of any of `names` in `text`,
|
|
76
|
+
* as a `{ index, match }` pair, or null. A hand search rather than a built
|
|
77
|
+
* RegExp so a configured name needs no escaping and the rule stays free of a
|
|
78
|
+
* non-literal RegExp constructor.
|
|
79
|
+
*/
|
|
80
|
+
function findWholeWordName(text, names) {
|
|
81
|
+
let best = null;
|
|
82
|
+
for (const name of names) {
|
|
83
|
+
let from = 0;
|
|
84
|
+
for (;;) {
|
|
85
|
+
const index = text.indexOf(name, from);
|
|
86
|
+
if (index === -1 || (best !== null && index >= best.index)) {
|
|
87
|
+
break;
|
|
88
|
+
}
|
|
89
|
+
const boundedBefore = index === 0 || !WORD_CHAR.test(text[index - 1]);
|
|
90
|
+
const boundedAfter = !WORD_CHAR.test(text[index + name.length] ?? '');
|
|
91
|
+
if (boundedBefore && boundedAfter) {
|
|
92
|
+
best = { index, match: name };
|
|
93
|
+
break;
|
|
94
|
+
}
|
|
95
|
+
from = index + 1;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return best;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** Whether a linted filename is a non-test file under a `src/` directory. */
|
|
102
|
+
function isSourceFile(filename) {
|
|
103
|
+
if (!filename) {
|
|
104
|
+
return false;
|
|
105
|
+
}
|
|
106
|
+
// 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.
|
|
107
|
+
const normalized = String(filename).replaceAll('\\', '/');
|
|
108
|
+
return normalized.includes('/src/') && !isTestFile(normalized);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* The earliest referent in `text` across the enabled classes, or null.
|
|
113
|
+
* Each class contributes at most its first match; the earliest of those wins.
|
|
114
|
+
*/
|
|
115
|
+
function firstReferent(text, matchers) {
|
|
116
|
+
let best = null;
|
|
117
|
+
for (const { kind, find } of matchers) {
|
|
118
|
+
const found = find(text);
|
|
119
|
+
if (found !== null && (best === null || found.index < best.index)) {
|
|
120
|
+
best = { kind, referent: found.match, index: found.index };
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
return best;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** A matcher backed by a RegExp literal. */
|
|
127
|
+
function regexMatcher(kind, pattern) {
|
|
128
|
+
return {
|
|
129
|
+
kind,
|
|
130
|
+
find(text) {
|
|
131
|
+
const match = pattern.exec(text);
|
|
132
|
+
return match === null ? null : { index: match.index, match: match[0] };
|
|
133
|
+
},
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* The source location of `index` within a comment's value, accounting for the
|
|
139
|
+
* two-character opener (`//`, `/*`, `#!`) and any newlines before the match.
|
|
140
|
+
*/
|
|
141
|
+
function locationInComment(comment, index) {
|
|
142
|
+
const before = comment.value.slice(0, index);
|
|
143
|
+
const lastNewline = before.lastIndexOf('\n');
|
|
144
|
+
const newlines = before.match(/\n/gu)?.length ?? 0;
|
|
145
|
+
const line = comment.loc.start.line + newlines;
|
|
146
|
+
const column = lastNewline === -1 ? comment.loc.start.column + 2 + index : index - lastNewline - 1;
|
|
147
|
+
return { line, column };
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** Build the enabled matcher table from the rule options. */
|
|
151
|
+
function buildMatchers(options) {
|
|
152
|
+
const matchers = [regexMatcher('an issue or PR number', ISSUE_REF)];
|
|
153
|
+
if (options.allowDates !== true) {
|
|
154
|
+
matchers.push(regexMatcher('a date', ISO_DATE));
|
|
155
|
+
}
|
|
156
|
+
const names = (options.names ?? ['Jeff']).filter((name) => name.length > 0);
|
|
157
|
+
if (names.length > 0) {
|
|
158
|
+
matchers.push({ kind: 'a person', find: (text) => findWholeWordName(text, names) });
|
|
159
|
+
}
|
|
160
|
+
return matchers;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
module.exports = {
|
|
164
|
+
meta: {
|
|
165
|
+
type: 'suggestion',
|
|
166
|
+
docs: {
|
|
167
|
+
description:
|
|
168
|
+
'Disallow issue/PR numbers, ISO dates and named people in src comments — they decay in ' +
|
|
169
|
+
'place; the rule belongs in the comment and the history in the commit, CHANGELOG or docs',
|
|
170
|
+
recommended: false,
|
|
171
|
+
recommendedSeverity: 'warn',
|
|
172
|
+
},
|
|
173
|
+
schema: [
|
|
174
|
+
{
|
|
175
|
+
type: 'object',
|
|
176
|
+
properties: {
|
|
177
|
+
names: { type: 'array', items: { type: 'string' }, uniqueItems: true },
|
|
178
|
+
allowDates: { type: 'boolean' },
|
|
179
|
+
},
|
|
180
|
+
additionalProperties: false,
|
|
181
|
+
},
|
|
182
|
+
],
|
|
183
|
+
messages: {
|
|
184
|
+
decayingReferent:
|
|
185
|
+
'This comment cites {{kind}} ({{referent}}), which decays in place: nobody rewrites the ' +
|
|
186
|
+
'comment when the issue closes, the date passes, or the person moves on. Keep the rule the ' +
|
|
187
|
+
'comment states and move the history to the commit message, the CHANGELOG, or ' +
|
|
188
|
+
'docs/contributing/ — `git log -L` keeps the rest.',
|
|
189
|
+
},
|
|
190
|
+
},
|
|
191
|
+
|
|
192
|
+
create(context) {
|
|
193
|
+
const filename = context.filename ?? context.getFilename();
|
|
194
|
+
if (!isSourceFile(filename)) {
|
|
195
|
+
return {};
|
|
196
|
+
}
|
|
197
|
+
const sourceCode = context.sourceCode ?? context.getSourceCode();
|
|
198
|
+
const matchers = buildMatchers(context.options?.[0] ?? {});
|
|
199
|
+
|
|
200
|
+
return {
|
|
201
|
+
Program() {
|
|
202
|
+
for (const comment of sourceCode.getAllComments()) {
|
|
203
|
+
const found = firstReferent(blankVendorClaimLines(comment.value), matchers);
|
|
204
|
+
if (found === null) {
|
|
205
|
+
continue;
|
|
206
|
+
}
|
|
207
|
+
const start = locationInComment(comment, found.index);
|
|
208
|
+
context.report({
|
|
209
|
+
loc: { start, end: { line: start.line, column: start.column + found.referent.length } },
|
|
210
|
+
messageId: 'decayingReferent',
|
|
211
|
+
data: { kind: found.kind, referent: found.referent },
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
},
|
|
215
|
+
};
|
|
216
|
+
},
|
|
217
|
+
};
|