@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,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ESLint rule: no-literal-process-exit
|
|
3
|
+
*
|
|
4
|
+
* Flags `process.exit(<number>)` and `process.exitCode = <number>`. A numeric
|
|
5
|
+
* exit code written at the call site re-decides the exit contract inline: one
|
|
6
|
+
* command exits 1 for "findings" and another exits 1 for "the config was
|
|
7
|
+
* unreadable", and nothing but a reader comparing two files can tell. The
|
|
8
|
+
* allowed shape names the meaning — `process.exit(ExitCode.FINDINGS)` — so the
|
|
9
|
+
* contract lives in one enum and every command shares it.
|
|
10
|
+
*
|
|
11
|
+
* The literal may hide inside an expression: `ok ? 0 : 1`, `status ?? 1`,
|
|
12
|
+
* `-1`. Those are flagged too, because each branch is still a number chosen at
|
|
13
|
+
* this site. Any non-literal argument — an identifier, a member, a call
|
|
14
|
+
* result — is accepted; the rule is a syntactic floor and does not chase what
|
|
15
|
+
* a variable holds.
|
|
16
|
+
*
|
|
17
|
+
* Option `allow: string[]` — repo-relative paths of the files permitted to
|
|
18
|
+
* exit with a literal (the one `bin.ts` last-resort fallback that runs when
|
|
19
|
+
* the enum module itself failed to load). Entries are matched at a path
|
|
20
|
+
* segment boundary via `exempt-path-matcher.cjs`; give the full repo-relative
|
|
21
|
+
* path, not a bare basename.
|
|
22
|
+
*
|
|
23
|
+
* @example
|
|
24
|
+
* // BAD — 1 means what, exactly?
|
|
25
|
+
* process.exit(1);
|
|
26
|
+
*
|
|
27
|
+
* // GOOD — the meaning is the code
|
|
28
|
+
* process.exit(ExitCode.FINDINGS);
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
'use strict';
|
|
32
|
+
|
|
33
|
+
const { createExemptPathMatcher } = require('./exempt-path-matcher.cjs');
|
|
34
|
+
|
|
35
|
+
/** Whether `node` is the member expression `process.<name>` (non-computed). */
|
|
36
|
+
function isProcessMember(node, name) {
|
|
37
|
+
return (
|
|
38
|
+
node.type === 'MemberExpression' &&
|
|
39
|
+
!node.computed &&
|
|
40
|
+
node.object.type === 'Identifier' &&
|
|
41
|
+
node.object.name === 'process' &&
|
|
42
|
+
node.property.type === 'Identifier' &&
|
|
43
|
+
node.property.name === name
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* The first numeric literal reachable through the expression's value
|
|
49
|
+
* branches, or null. Walks conditionals, nullish/logical fallbacks, unary
|
|
50
|
+
* minus, and TypeScript wrappers — every path a number can take to become the
|
|
51
|
+
* exit code without passing through a name.
|
|
52
|
+
*/
|
|
53
|
+
function numericLiteralLeaf(expr) {
|
|
54
|
+
switch (expr.type) {
|
|
55
|
+
case 'Literal':
|
|
56
|
+
return typeof expr.value === 'number' ? expr : null;
|
|
57
|
+
case 'UnaryExpression':
|
|
58
|
+
return numericLiteralLeaf(expr.argument);
|
|
59
|
+
case 'ConditionalExpression':
|
|
60
|
+
return numericLiteralLeaf(expr.consequent) ?? numericLiteralLeaf(expr.alternate);
|
|
61
|
+
case 'LogicalExpression':
|
|
62
|
+
return numericLiteralLeaf(expr.left) ?? numericLiteralLeaf(expr.right);
|
|
63
|
+
case 'TSAsExpression':
|
|
64
|
+
case 'TSNonNullExpression':
|
|
65
|
+
case 'TSSatisfiesExpression':
|
|
66
|
+
return numericLiteralLeaf(expr.expression);
|
|
67
|
+
default:
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
module.exports = {
|
|
73
|
+
meta: {
|
|
74
|
+
type: 'problem',
|
|
75
|
+
docs: {
|
|
76
|
+
description:
|
|
77
|
+
'Disallow process.exit(<number>) and process.exitCode = <number> — name the meaning with ' +
|
|
78
|
+
'the ExitCode enum so every command shares one exit contract',
|
|
79
|
+
recommended: false,
|
|
80
|
+
recommendedSeverity: 'warn',
|
|
81
|
+
},
|
|
82
|
+
schema: [
|
|
83
|
+
{
|
|
84
|
+
type: 'object',
|
|
85
|
+
properties: {
|
|
86
|
+
allow: { type: 'array', items: { type: 'string' }, uniqueItems: true },
|
|
87
|
+
},
|
|
88
|
+
additionalProperties: false,
|
|
89
|
+
},
|
|
90
|
+
],
|
|
91
|
+
messages: {
|
|
92
|
+
literalExit:
|
|
93
|
+
'process.exit({{literal}}) re-decides the exit contract at this call site. Use the ' +
|
|
94
|
+
'ExitCode enum from @vibe-agent-toolkit/schema — process.exit(ExitCode.X) — so the code ' +
|
|
95
|
+
'means the same thing in every command.',
|
|
96
|
+
literalExitCode:
|
|
97
|
+
'process.exitCode = {{literal}} re-decides the exit contract at this site. Assign an ' +
|
|
98
|
+
'ExitCode member from @vibe-agent-toolkit/schema instead.',
|
|
99
|
+
},
|
|
100
|
+
},
|
|
101
|
+
|
|
102
|
+
create(context) {
|
|
103
|
+
const allow = context.options?.[0]?.allow ?? [];
|
|
104
|
+
const filename = context.filename ?? context.getFilename();
|
|
105
|
+
if (createExemptPathMatcher(allow)(filename)) {
|
|
106
|
+
return {};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function reportLiteral(node, expr, messageId) {
|
|
110
|
+
const leaf = numericLiteralLeaf(expr);
|
|
111
|
+
if (leaf !== null) {
|
|
112
|
+
context.report({ node, messageId, data: { literal: String(leaf.raw ?? leaf.value) } });
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return {
|
|
117
|
+
CallExpression(node) {
|
|
118
|
+
if (isProcessMember(node.callee, 'exit') && node.arguments.length > 0) {
|
|
119
|
+
reportLiteral(node, node.arguments[0], 'literalExit');
|
|
120
|
+
}
|
|
121
|
+
},
|
|
122
|
+
AssignmentExpression(node) {
|
|
123
|
+
if (isProcessMember(node.left, 'exitCode')) {
|
|
124
|
+
reportLiteral(node, node.right, 'literalExitCode');
|
|
125
|
+
}
|
|
126
|
+
},
|
|
127
|
+
};
|
|
128
|
+
},
|
|
129
|
+
};
|
|
@@ -1,16 +1,32 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* ESLint rule to enforce
|
|
2
|
+
* ESLint rule to enforce the forward-slash converters instead of hand-rolled
|
|
3
|
+
* normalization.
|
|
3
4
|
*
|
|
4
|
-
*
|
|
5
|
+
* Two converters, because a backslash means two different things:
|
|
6
|
+
*
|
|
7
|
+
* - `toForwardSlash(p)` — a NATIVE path (from `fs`, `path.*`, git). Converts
|
|
8
|
+
* only where the host's separator is a backslash; on POSIX a backslash is a
|
|
9
|
+
* filename character and is kept. `split(path.sep).join('/')` is exactly
|
|
10
|
+
* this, so it autofixes here.
|
|
11
|
+
* - `toForwardSlashAnyPlatform(text)` — AUTHOR-WRITTEN text (an href, a glob, a
|
|
12
|
+
* config value, an archive entry name). Converts every backslash on every
|
|
13
|
+
* host. A literal-backslash `split('\\').join('/')`, `replaceAll('\\', '/')`
|
|
14
|
+
* or `replace(/\\/g, '/')` is exactly this, so each autofixes here — never to
|
|
15
|
+
* `toForwardSlash`, which would silently stop converting on POSIX.
|
|
16
|
+
*
|
|
17
|
+
* Whether a given literal-backslash site is really author text is the author's
|
|
18
|
+
* call; the fix preserves behaviour, and a native-path site should then be
|
|
19
|
+
* switched to `toForwardSlash` by hand.
|
|
5
20
|
*
|
|
6
21
|
* @example
|
|
7
22
|
* // ❌ BAD - manual normalization
|
|
8
|
-
* const
|
|
9
|
-
* const
|
|
23
|
+
* const a = relativePath.split(path.sep).join('/');
|
|
24
|
+
* const b = href.replaceAll('\\', '/');
|
|
10
25
|
*
|
|
11
|
-
* // ✅ GOOD - use utility
|
|
12
|
-
* import { toForwardSlash } from '@vibe-agent-toolkit/utils/path';
|
|
13
|
-
* const
|
|
26
|
+
* // ✅ GOOD - use the utility functions
|
|
27
|
+
* import { toForwardSlash, toForwardSlashAnyPlatform } from '@vibe-agent-toolkit/utils/path';
|
|
28
|
+
* const a = toForwardSlash(relativePath);
|
|
29
|
+
* const b = toForwardSlashAnyPlatform(href);
|
|
14
30
|
*/
|
|
15
31
|
|
|
16
32
|
const {
|
|
@@ -26,22 +42,85 @@ const {
|
|
|
26
42
|
resolveSafeModule,
|
|
27
43
|
} = require('./safe-import.cjs');
|
|
28
44
|
|
|
29
|
-
const
|
|
45
|
+
const NATIVE_FN = 'toForwardSlash';
|
|
46
|
+
const ANY_PLATFORM_FN = 'toForwardSlashAnyPlatform';
|
|
47
|
+
const MESSAGE_FOR = { [NATIVE_FN]: 'useToForwardSlash', [ANY_PLATFORM_FN]: 'useToForwardSlashAnyPlatform' };
|
|
30
48
|
const PATH_MODULES = new Set(['node:path', 'path']);
|
|
49
|
+
const BACKSLASH = '\\';
|
|
50
|
+
|
|
51
|
+
/** `.name(...)` on some receiver, with exactly `arity` arguments? */
|
|
52
|
+
function isMethodCall(node, name, arity) {
|
|
53
|
+
return (
|
|
54
|
+
node?.type === 'CallExpression' &&
|
|
55
|
+
node.callee.type === 'MemberExpression' &&
|
|
56
|
+
node.callee.property.name === name &&
|
|
57
|
+
node.arguments.length === arity
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function isStringLiteral(node, value) {
|
|
62
|
+
return node.type === 'Literal' && node.value === value;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** `/\\/g` — one literal backslash, global. */
|
|
66
|
+
function isGlobalBackslashRegex(node) {
|
|
67
|
+
return node.type === 'Literal' && node.regex?.pattern === String.raw`\\` && node.regex.flags.includes('g');
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function isPathSep(node) {
|
|
71
|
+
return node.type === 'MemberExpression' && node.object.name === 'path' && node.property.name === 'sep';
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* `<x>.split(<sep>).join('/')` → the converter it is equivalent to.
|
|
76
|
+
*
|
|
77
|
+
* Splitting on a TWO-backslash SEQUENCE (e.g. collapsing a UNC server prefix)
|
|
78
|
+
* is a different operation that neither converter matches, so it is not
|
|
79
|
+
* reported.
|
|
80
|
+
*/
|
|
81
|
+
function matchSplitJoin(node) {
|
|
82
|
+
if (!isMethodCall(node, 'join', 1) || !isStringLiteral(node.arguments[0], '/')) return undefined;
|
|
83
|
+
const split = node.callee.object;
|
|
84
|
+
if (!isMethodCall(split, 'split', 1)) return undefined;
|
|
85
|
+
const [separator] = split.arguments;
|
|
86
|
+
if (isPathSep(separator)) return { receiver: split.callee.object, fn: NATIVE_FN };
|
|
87
|
+
if (isStringLiteral(separator, BACKSLASH)) return { receiver: split.callee.object, fn: ANY_PLATFORM_FN };
|
|
88
|
+
return undefined;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** `<x>.replaceAll('\\', '/')` or `<x>.replace(/\\/g, '/')` → the any-platform converter. */
|
|
92
|
+
function matchReplace(node) {
|
|
93
|
+
const isReplace = isMethodCall(node, 'replace', 2) || isMethodCall(node, 'replaceAll', 2);
|
|
94
|
+
if (!isReplace || !isStringLiteral(node.arguments[1], '/')) return undefined;
|
|
95
|
+
const [pattern] = node.arguments;
|
|
96
|
+
const allBackslashes =
|
|
97
|
+
isGlobalBackslashRegex(pattern) ||
|
|
98
|
+
(node.callee.property.name === 'replaceAll' && isStringLiteral(pattern, BACKSLASH));
|
|
99
|
+
return allBackslashes ? { receiver: node.callee.object, fn: ANY_PLATFORM_FN } : undefined;
|
|
100
|
+
}
|
|
31
101
|
|
|
32
102
|
module.exports = {
|
|
33
103
|
meta: {
|
|
34
104
|
type: 'problem',
|
|
35
105
|
docs: {
|
|
36
|
-
description:
|
|
37
|
-
|
|
106
|
+
description:
|
|
107
|
+
'Disallow manual path normalization patterns',
|
|
108
|
+
category: 'Path handling',
|
|
109
|
+
bans: "hand-rolled `.replace(/\\\\/g, '/')` / `split(path.sep).join('/')`",
|
|
110
|
+
useInstead: '`toForwardSlash()` (native paths) / `toForwardSlashAnyPlatform()` (authored text)',
|
|
111
|
+
subpath: '/path',
|
|
38
112
|
recommended: true,
|
|
113
|
+
recommendedSeverity: 'error',
|
|
39
114
|
},
|
|
40
115
|
fixable: 'code',
|
|
41
116
|
messages: {
|
|
42
117
|
useToForwardSlash:
|
|
43
118
|
'Use toForwardSlash() from {{safeModule}} instead of manual path normalization. ' +
|
|
44
|
-
'
|
|
119
|
+
'It converts a native path only where the host separator is a backslash.',
|
|
120
|
+
useToForwardSlashAnyPlatform:
|
|
121
|
+
'Use toForwardSlashAnyPlatform() from {{safeModule}} instead of a hand-rolled backslash replace. ' +
|
|
122
|
+
'If this string is a native filesystem/git path rather than authored text, use toForwardSlash() — ' +
|
|
123
|
+
'on POSIX a backslash is a filename character.',
|
|
45
124
|
[DEAD_UNSAFE_IMPORT]: DEAD_UNSAFE_IMPORT_MESSAGE,
|
|
46
125
|
},
|
|
47
126
|
schema: [SAFE_MODULE_ONLY_SCHEMA],
|
|
@@ -50,24 +129,43 @@ module.exports = {
|
|
|
50
129
|
create(context) {
|
|
51
130
|
const sourceCode = context.getSourceCode();
|
|
52
131
|
const targetModule = resolveSafeModule(context, SAFE_PATH_MODULE);
|
|
53
|
-
// Seeded from SCOPE: a file that already
|
|
54
|
-
//
|
|
55
|
-
//
|
|
56
|
-
|
|
132
|
+
// Seeded from SCOPE: a file that already binds the name must have the call
|
|
133
|
+
// rewritten WITHOUT gaining a second binding of the same name — that is a
|
|
134
|
+
// SyntaxError. See `safe-import.cjs`.
|
|
135
|
+
const bound = {
|
|
136
|
+
[NATIVE_FN]: isNameAlreadyBound(sourceCode, NATIVE_FN),
|
|
137
|
+
[ANY_PLATFORM_FN]: isNameAlreadyBound(sourceCode, ANY_PLATFORM_FN),
|
|
138
|
+
};
|
|
57
139
|
// Never mutated — the dead-import leg must not be armed by a flag that a
|
|
58
140
|
// suppressed report's `fix()` can spend. See `dead-import.cjs`.
|
|
59
|
-
const safeBoundInSource =
|
|
141
|
+
const safeBoundInSource = bound[NATIVE_FN];
|
|
60
142
|
// The dead-import leg's OTHER gate: a `toForwardSlash(…)` call is the text
|
|
61
|
-
// this fixer writes
|
|
62
|
-
//
|
|
63
|
-
// `dead-import.cjs`. Read from the source, never from a `fix()`.
|
|
143
|
+
// this fixer writes for `path.sep`, and the only evidence available that it
|
|
144
|
+
// wrote it here. Read from the source, never from a `fix()`.
|
|
64
145
|
let safeReplacementCalled = false;
|
|
65
146
|
let utilsImportNode = null;
|
|
66
147
|
// `path.sep` is the last `path.*` reference in plenty of files, and
|
|
67
|
-
// `toForwardSlash(raw)` consumes it — leaving
|
|
68
|
-
// binding the `safePath` rules used to leave.
|
|
148
|
+
// `toForwardSlash(raw)` consumes it — leaving a dead `node:path` binding.
|
|
69
149
|
const pathImportNodes = [];
|
|
70
150
|
|
|
151
|
+
/**
|
|
152
|
+
* Add `fn` to the import, when nothing binds it yet.
|
|
153
|
+
*
|
|
154
|
+
* NOT latched: two reports insert identical text at the identical anchor, so
|
|
155
|
+
* ESLint applies one and drops the other as overlapping. Latching is not
|
|
156
|
+
* free — ESLint runs `fix()` for a SUPPRESSED problem before the
|
|
157
|
+
* `eslint-disable` filter discards it, so a latch could be spent by a report
|
|
158
|
+
* that is then thrown away.
|
|
159
|
+
*/
|
|
160
|
+
function importFix(fixer, fn) {
|
|
161
|
+
if (bound[fn]) return [];
|
|
162
|
+
if (utilsImportNode) {
|
|
163
|
+
return [fixer.insertTextAfter(utilsImportNode.specifiers.at(-1), `, ${fn}`)];
|
|
164
|
+
}
|
|
165
|
+
const newImport = `import { ${fn} } from '${targetModule}';\n`;
|
|
166
|
+
return [insertAboveWithComments(fixer, sourceCode, sourceCode.ast.body[0], newImport)];
|
|
167
|
+
}
|
|
168
|
+
|
|
71
169
|
return {
|
|
72
170
|
'Program:exit'() {
|
|
73
171
|
reportDeadUnsafeImports(
|
|
@@ -83,99 +181,30 @@ module.exports = {
|
|
|
83
181
|
if (PATH_MODULES.has(node.source.value)) {
|
|
84
182
|
pathImportNodes.push(node);
|
|
85
183
|
}
|
|
86
|
-
if (node.source.value
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
}
|
|
184
|
+
if (node.source.value !== targetModule) return;
|
|
185
|
+
utilsImportNode = node;
|
|
186
|
+
for (const spec of node.specifiers) {
|
|
187
|
+
if (spec.type === 'ImportSpecifier' && spec.imported.name in bound) {
|
|
188
|
+
bound[spec.imported.name] = true;
|
|
92
189
|
}
|
|
93
190
|
}
|
|
94
191
|
},
|
|
95
192
|
|
|
96
193
|
CallExpression(node) {
|
|
97
|
-
if (node.callee.type === 'Identifier' && node.callee.name ===
|
|
194
|
+
if (node.callee.type === 'Identifier' && node.callee.name === NATIVE_FN) {
|
|
98
195
|
safeReplacementCalled = true;
|
|
99
196
|
}
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
node
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
if (
|
|
112
|
-
splitCall.type === 'CallExpression' &&
|
|
113
|
-
splitCall.callee.type === 'MemberExpression' &&
|
|
114
|
-
splitCall.callee.property.name === 'split' &&
|
|
115
|
-
splitCall.arguments.length === 1
|
|
116
|
-
) {
|
|
117
|
-
const splitArg = splitCall.arguments[0];
|
|
118
|
-
|
|
119
|
-
// Split on path.sep, or on a single backslash character
|
|
120
|
-
// (source literal '\\'). Splitting on a two-backslash SEQUENCE
|
|
121
|
-
// (source literal '\\\\', decoded value: two backslash characters)
|
|
122
|
-
// is a different, rarer operation -- e.g. collapsing a UNC path's
|
|
123
|
-
// leading double-backslash server prefix -- and toForwardSlash()
|
|
124
|
-
// is not equivalent to it. Autofixing that case would silently
|
|
125
|
-
// change program behavior, so it is deliberately excluded here.
|
|
126
|
-
const isSplittingByPathSep =
|
|
127
|
-
(splitArg.type === 'MemberExpression' &&
|
|
128
|
-
splitArg.object.name === 'path' &&
|
|
129
|
-
splitArg.property.name === 'sep') ||
|
|
130
|
-
(splitArg.type === 'Literal' && splitArg.value === '\\');
|
|
131
|
-
|
|
132
|
-
if (isSplittingByPathSep) {
|
|
133
|
-
const variableBeingSplit = splitCall.callee.object;
|
|
134
|
-
|
|
135
|
-
context.report({
|
|
136
|
-
node,
|
|
137
|
-
messageId: 'useToForwardSlash',
|
|
138
|
-
data: { safeModule: targetModule },
|
|
139
|
-
fix(fixer) {
|
|
140
|
-
const fixes = [];
|
|
141
|
-
|
|
142
|
-
// Replace the entire .split(...).join('/') with toForwardSlash(...)
|
|
143
|
-
const originalVar = sourceCode.getText(variableBeingSplit);
|
|
144
|
-
fixes.push(fixer.replaceText(node, `${SAFE_FN}(${originalVar})`));
|
|
145
|
-
|
|
146
|
-
// Add import if needed
|
|
147
|
-
if (!hasToForwardSlashImport) {
|
|
148
|
-
if (utilsImportNode) {
|
|
149
|
-
// Add to existing utils import
|
|
150
|
-
const lastSpecifier = utilsImportNode.specifiers.at(-1);
|
|
151
|
-
fixes.push(fixer.insertTextAfter(lastSpecifier, `, ${SAFE_FN}`));
|
|
152
|
-
} else {
|
|
153
|
-
// Create new import at the top
|
|
154
|
-
const firstNode = sourceCode.ast.body[0];
|
|
155
|
-
const newImport = `import { ${SAFE_FN} } from '${targetModule}';\n`;
|
|
156
|
-
fixes.push(insertAboveWithComments(fixer, sourceCode, firstNode, newImport));
|
|
157
|
-
}
|
|
158
|
-
// NOT latched. The comment here used to claim that without a
|
|
159
|
-
// `hasToForwardSlashImport = true` a second occurrence would
|
|
160
|
-
// insert the import twice; an adversarial run could not
|
|
161
|
-
// reproduce that at any occurrence count. It cannot happen:
|
|
162
|
-
// both reports insert identical text at the identical anchor,
|
|
163
|
-
// so the ranges coincide and ESLint applies one and drops the
|
|
164
|
-
// other as overlapping.
|
|
165
|
-
//
|
|
166
|
-
// Latching it is not free, either. ESLint runs `fix()` for a
|
|
167
|
-
// SUPPRESSED problem before the `eslint-disable` filter
|
|
168
|
-
// discards it, so the first report could spend the flag and
|
|
169
|
-
// then be thrown away — leaving later occurrences rewritten
|
|
170
|
-
// to a `toForwardSlash` nothing imports.
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
return fixes;
|
|
174
|
-
},
|
|
175
|
-
});
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
}
|
|
197
|
+
const match = matchSplitJoin(node) ?? matchReplace(node);
|
|
198
|
+
if (!match) return;
|
|
199
|
+
context.report({
|
|
200
|
+
node,
|
|
201
|
+
messageId: MESSAGE_FOR[match.fn],
|
|
202
|
+
data: { safeModule: targetModule },
|
|
203
|
+
fix(fixer) {
|
|
204
|
+
const receiver = sourceCode.getText(match.receiver);
|
|
205
|
+
return [fixer.replaceText(node, `${match.fn}(${receiver})`), ...importFix(fixer, match.fn)];
|
|
206
|
+
},
|
|
207
|
+
});
|
|
179
208
|
},
|
|
180
209
|
};
|
|
181
210
|
},
|
|
@@ -21,4 +21,12 @@ module.exports = factory({
|
|
|
21
21
|
// No baked-in exemption: the file that implements normalizedTmpdir() is
|
|
22
22
|
// repo-specific. Consumers declare it as { exemptFiles: [...] }.
|
|
23
23
|
checkMemberExpression: true, // Catch os.tmpdir() pattern
|
|
24
|
+
docs: {
|
|
25
|
+
category: 'Filesystem and process',
|
|
26
|
+
bans: '`os.tmpdir()` (8.3 short names on Windows)',
|
|
27
|
+
useInstead: '`normalizedTmpdir()`',
|
|
28
|
+
subpath: '/fs',
|
|
29
|
+
recommended: true,
|
|
30
|
+
recommendedSeverity: 'error',
|
|
31
|
+
},
|
|
24
32
|
});
|
|
@@ -25,9 +25,14 @@ module.exports = {
|
|
|
25
25
|
meta: {
|
|
26
26
|
type: 'problem',
|
|
27
27
|
docs: {
|
|
28
|
-
description:
|
|
29
|
-
|
|
28
|
+
description:
|
|
29
|
+
'Disallow using path operations directly in string comparisons',
|
|
30
|
+
category: 'Path handling',
|
|
31
|
+
bans: 'raw `path.*()` results in string comparisons',
|
|
32
|
+
useInstead: 'wrap in `toForwardSlash()`',
|
|
33
|
+
subpath: '/path',
|
|
30
34
|
recommended: true,
|
|
35
|
+
recommendedSeverity: 'error',
|
|
31
36
|
},
|
|
32
37
|
messages: {
|
|
33
38
|
normalizePathOperation:
|
|
@@ -25,9 +25,14 @@ module.exports = {
|
|
|
25
25
|
meta: {
|
|
26
26
|
type: 'problem',
|
|
27
27
|
docs: {
|
|
28
|
-
description:
|
|
29
|
-
|
|
28
|
+
description:
|
|
29
|
+
'Disallow using path.sep in string operations',
|
|
30
|
+
category: 'Path handling',
|
|
31
|
+
bans: '`path.sep` embedded in a string literal',
|
|
32
|
+
useInstead: '`toForwardSlash()`',
|
|
33
|
+
subpath: '/path',
|
|
30
34
|
recommended: true,
|
|
35
|
+
recommendedSeverity: 'error',
|
|
31
36
|
},
|
|
32
37
|
messages: {
|
|
33
38
|
noPathSep:
|
|
@@ -68,9 +68,14 @@ module.exports = {
|
|
|
68
68
|
meta: {
|
|
69
69
|
type: 'problem',
|
|
70
70
|
docs: {
|
|
71
|
-
description:
|
|
72
|
-
|
|
71
|
+
description:
|
|
72
|
+
'Disallow direct path.startsWith() without normalization',
|
|
73
|
+
category: 'Path handling',
|
|
74
|
+
bans: '`path.startsWith()` on a raw path',
|
|
75
|
+
useInstead: '`toForwardSlash()` first',
|
|
76
|
+
subpath: '/path',
|
|
73
77
|
recommended: true,
|
|
78
|
+
recommendedSeverity: 'error',
|
|
74
79
|
},
|
|
75
80
|
messages: {
|
|
76
81
|
useNormalizeHelper:
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ESLint Rule: no-process-exit-in-phase
|
|
3
|
+
*
|
|
4
|
+
* Forbid `process.exit()` inside a phase entry point — a function whose name
|
|
5
|
+
* ends in `Phase`.
|
|
6
|
+
*
|
|
7
|
+
* ## Why this rule exists
|
|
8
|
+
*
|
|
9
|
+
* `vat validate`, `vat verify` and `vat build` used to run each phase as a
|
|
10
|
+
* SEPARATE CHILD PROCESS. A `process.exit()` inside a phase was then perfectly
|
|
11
|
+
* safe: it ended that child, the parent read the exit code, and the run carried
|
|
12
|
+
* on to the next phase. Nothing about the code said so — the safety came
|
|
13
|
+
* entirely from the process boundary.
|
|
14
|
+
*
|
|
15
|
+
* That boundary is gone. Phases now run in the orchestrator's own process, so a
|
|
16
|
+
* surviving `process.exit()` ends THE WHOLE RUN: every later phase is silently
|
|
17
|
+
* skipped, the parent's aggregation never happens, and the process exits 0-or-1
|
|
18
|
+
* having done half the work with nothing in the document to say so. It is the
|
|
19
|
+
* worst shape of failure this codebase has — a confident, well-formed, wrong
|
|
20
|
+
* answer — and it cannot be caught by a type, because `process.exit()`
|
|
21
|
+
* typechecks anywhere.
|
|
22
|
+
*
|
|
23
|
+
* A phase must RETURN `{ document, exitCode }` and let its caller decide whether
|
|
24
|
+
* to print and exit (a command-line run) or to fold the result into the run (an
|
|
25
|
+
* orchestrated one).
|
|
26
|
+
*
|
|
27
|
+
* ## Why the name is the marker
|
|
28
|
+
*
|
|
29
|
+
* The `…Phase` suffix is the convention every phase entry point already follows
|
|
30
|
+
* (`runResourcesValidatePhase`, `runSkillsBuildPhase`, …). Keying the rule to it
|
|
31
|
+
* means a new phase is protected the moment it is named like one, with no list
|
|
32
|
+
* to keep in sync — a list is the thing that goes stale silently, which is the
|
|
33
|
+
* defect class this rule exists to prevent in the first place.
|
|
34
|
+
*
|
|
35
|
+
* The thin Commander wrappers that CALL these functions keep their
|
|
36
|
+
* `process.exit()`: deciding how the process ends is exactly their job, and they
|
|
37
|
+
* are not named `…Phase`.
|
|
38
|
+
*/
|
|
39
|
+
|
|
40
|
+
/** Does this call expression read as `process.exit(...)`? */
|
|
41
|
+
function isProcessExitCall(node) {
|
|
42
|
+
const { callee } = node;
|
|
43
|
+
return (
|
|
44
|
+
callee.type === 'MemberExpression' &&
|
|
45
|
+
!callee.computed &&
|
|
46
|
+
callee.object.type === 'Identifier' &&
|
|
47
|
+
callee.object.name === 'process' &&
|
|
48
|
+
callee.property.type === 'Identifier' &&
|
|
49
|
+
callee.property.name === 'exit'
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* The declared name of a function-ish node, however it was declared.
|
|
55
|
+
*
|
|
56
|
+
* Covers the three spellings a phase entry point can legitimately take: a
|
|
57
|
+
* function declaration, a `const x = function () {}`, and a `const x = () => {}`.
|
|
58
|
+
* A rule that only understood declarations would be silently inert against an
|
|
59
|
+
* arrow const — the same blind spot `no-test-scoped-functions` documents.
|
|
60
|
+
*/
|
|
61
|
+
function functionName(node) {
|
|
62
|
+
if (node.id && node.id.type === 'Identifier') return node.id.name;
|
|
63
|
+
|
|
64
|
+
const { parent } = node;
|
|
65
|
+
if (parent && parent.type === 'VariableDeclarator' && parent.id.type === 'Identifier') {
|
|
66
|
+
return parent.id.name;
|
|
67
|
+
}
|
|
68
|
+
if (parent && parent.type === 'Property' && parent.key.type === 'Identifier') {
|
|
69
|
+
return parent.key.name;
|
|
70
|
+
}
|
|
71
|
+
return undefined;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const FUNCTION_TYPES = new Set([
|
|
75
|
+
'FunctionDeclaration',
|
|
76
|
+
'FunctionExpression',
|
|
77
|
+
'ArrowFunctionExpression',
|
|
78
|
+
]);
|
|
79
|
+
|
|
80
|
+
/** The nearest enclosing function named like a phase entry point, if any. */
|
|
81
|
+
function enclosingPhaseName(node) {
|
|
82
|
+
for (let current = node.parent; current; current = current.parent) {
|
|
83
|
+
if (!FUNCTION_TYPES.has(current.type)) continue;
|
|
84
|
+
const name = functionName(current);
|
|
85
|
+
if (name !== undefined && name.endsWith('Phase')) return name;
|
|
86
|
+
}
|
|
87
|
+
return undefined;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
module.exports = {
|
|
91
|
+
meta: {
|
|
92
|
+
type: 'problem',
|
|
93
|
+
docs: {
|
|
94
|
+
description:
|
|
95
|
+
'Forbid process.exit() inside a phase entry point, where it would end the whole orchestrated run',
|
|
96
|
+
category: 'Process control',
|
|
97
|
+
bans: '`process.exit()` inside a function named `…Phase`',
|
|
98
|
+
useInstead: 'return the exit code from the phase; only the command wrapper exits',
|
|
99
|
+
// Not in `recommended`: it keys on a NAMING CONVENTION that is VAT's, not a
|
|
100
|
+
// portable fact. The hazard is real and general (an in-process orchestrator
|
|
101
|
+
// whose step calls `process.exit()` silently skips every later step), but
|
|
102
|
+
// the marker is the `…Phase` suffix, and an adopter with an unrelated
|
|
103
|
+
// `computeRenderPhase()` that legitimately exits would get a finding they
|
|
104
|
+
// cannot act on. VAT enables it explicitly, repo-wide, so a NEW phase is
|
|
105
|
+
// covered the moment it is named like one.
|
|
106
|
+
recommended: false,
|
|
107
|
+
recommendedSeverity: 'error',
|
|
108
|
+
},
|
|
109
|
+
fixable: null,
|
|
110
|
+
schema: [],
|
|
111
|
+
messages: {
|
|
112
|
+
exitInPhase:
|
|
113
|
+
"'{{name}}' is a phase entry point, so process.exit() here ends the ENTIRE run — every later phase is skipped and the orchestrator never aggregates. Return { document, exitCode } instead and let the caller decide how the process ends.",
|
|
114
|
+
},
|
|
115
|
+
},
|
|
116
|
+
|
|
117
|
+
create(context) {
|
|
118
|
+
return {
|
|
119
|
+
CallExpression(node) {
|
|
120
|
+
if (!isProcessExitCall(node)) return;
|
|
121
|
+
const name = enclosingPhaseName(node);
|
|
122
|
+
if (name === undefined) return;
|
|
123
|
+
context.report({ node, messageId: 'exitInPhase', data: { name } });
|
|
124
|
+
},
|
|
125
|
+
};
|
|
126
|
+
},
|
|
127
|
+
};
|