@vibe-agent-toolkit/utils 0.1.42 → 0.2.0-rc.2
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 +19 -3
- package/dist/file-crawler.d.ts.map +1 -1
- package/dist/file-crawler.js +88 -2
- package/dist/file-crawler.js.map +1 -1
- package/dist/fs-utils.d.ts +389 -30
- package/dist/fs-utils.d.ts.map +1 -1
- package/dist/fs-utils.js +425 -56
- package/dist/fs-utils.js.map +1 -1
- package/dist/fs.d.ts +2 -1
- package/dist/fs.d.ts.map +1 -1
- package/dist/fs.js +7 -1
- package/dist/fs.js.map +1 -1
- package/dist/git-root-cache.d.ts +44 -0
- package/dist/git-root-cache.d.ts.map +1 -0
- package/dist/git-root-cache.js +68 -0
- package/dist/git-root-cache.js.map +1 -0
- package/dist/git-utils.d.ts +11 -0
- package/dist/git-utils.d.ts.map +1 -1
- package/dist/git-utils.js +28 -8
- package/dist/git-utils.js.map +1 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +37 -2
- package/dist/index.js.map +1 -1
- package/dist/numeric-args.d.ts +24 -0
- package/dist/numeric-args.d.ts.map +1 -0
- package/dist/numeric-args.js +37 -0
- package/dist/numeric-args.js.map +1 -0
- package/dist/path-core.d.ts +30 -0
- package/dist/path-core.d.ts.map +1 -1
- package/dist/path-core.js +32 -0
- package/dist/path-core.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/project-utils.d.ts +7 -1
- package/dist/project-utils.d.ts.map +1 -1
- package/dist/project-utils.js +9 -1
- package/dist/project-utils.js.map +1 -1
- package/dist/test-helpers.d.ts +16 -0
- package/dist/test-helpers.d.ts.map +1 -1
- package/dist/test-helpers.js +28 -1
- package/dist/test-helpers.js.map +1 -1
- package/eslint/README.md +1 -1
- package/eslint/rules/dead-import.cjs +61 -11
- package/eslint/rules/eslint-rule-factory.cjs +16 -1
- package/eslint/rules/no-manual-path-normalize.cjs +24 -3
- package/eslint/rules/path-function-rule-factory.cjs +99 -20
- package/eslint/rules/prefer-startswith-over-regex.cjs +24 -1
- package/package.json +2 -2
package/dist/test-helpers.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"test-helpers.js","sourceRoot":"","sources":["../src/test-helpers.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAC1C,OAAO,EAAE,WAAW,EAAE,MAAM,EAAE,MAAM,SAAS,CAAC;
|
|
1
|
+
{"version":3,"file":"test-helpers.js","sourceRoot":"","sources":["../src/test-helpers.ts"],"names":[],"mappings":"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"}
|
package/eslint/README.md
CHANGED
|
@@ -197,7 +197,7 @@ So the rules now report it themselves, as a separate `deadUnsafeImport` finding
|
|
|
197
197
|
Deliberately narrow:
|
|
198
198
|
|
|
199
199
|
- **A closed list of modules** — `node:path`, `node:os`, `node:fs`, `node:fs/promises`, `node:child_process` and their bare spellings. All Node builtins, all side-effect-free with certainty decided when the rule was written. This is *not* a general unused-import rule and will not become one; for blanket cleanup, `eslint-plugin-unused-imports` already exists and already autofixes.
|
|
200
|
-
- **Only in a file
|
|
200
|
+
- **Only in a file this rule migrated** — the safe symbol must be bound AND the rule's own replacement must actually be called in the file (`safePath.join(…)`, `normalizedTmpdir()`, `toForwardSlash(…)`). "The safe symbol is in scope" alone is not evidence: `safePath` arrives for reasons that have nothing to do with a call this pack consumed, and an import that was dead already — dead before the pack ever ran — was then deleted as though this fixer had orphaned it. A dead import in a file this pack never touched is somebody else's business.
|
|
201
201
|
- **Only whole declarations, only with zero references left** — evaluated against the source as it stands on that pass, so the removal always lands *after* the rewrite that consumed the last reference, never speculatively beside it. A partially-dead declaration (`import path, { sep }` with `sep` still live) is left alone, as are bare `import 'node:path'` side-effect imports and anything carrying a `type` specifier.
|
|
202
202
|
|
|
203
203
|
This cannot be delegated: `@typescript-eslint/no-unused-vars` declares `meta.fixable: 'code'` but emits only a **suggestion** for an unused import, and `--fix` never applies suggestions; `sonarjs/unused-import` declares no fixer at all. Verified with both enabled alongside these rules in one `verifyAndFix` — the import survived. Those rules abstain for a good reason, since removing an import can change behaviour; a rule that *created* the orphan knows it just consumed the last reference and knows the module, so it can act where a generic rule cannot.
|
|
@@ -63,9 +63,16 @@ const REMOVABLE_MODULES = new Set([
|
|
|
63
63
|
]);
|
|
64
64
|
|
|
65
65
|
const DEAD_UNSAFE_IMPORT = 'deadUnsafeImport';
|
|
66
|
+
// States what the rule can actually observe, and no more. The earlier wording —
|
|
67
|
+
// "this rule's autofix rewrote the last call that referenced it" — asserted a
|
|
68
|
+
// CAUSE, and the leg cannot see causes: by the time a binding reads as dead the
|
|
69
|
+
// rewrite that killed it is a pass in the past and left no trace naming which
|
|
70
|
+
// import it drew on. On a file with two imports of the same module, one of them
|
|
71
|
+
// dead since before the pack ever ran, that sentence was simply false about one
|
|
72
|
+
// of the two. See `reportDeadUnsafeImports` for what the gate does prove.
|
|
66
73
|
const DEAD_UNSAFE_IMPORT_MESSAGE =
|
|
67
|
-
"'{{local}}'
|
|
68
|
-
"
|
|
74
|
+
"'{{local}}' has no remaining references, and this file already calls the safe " +
|
|
75
|
+
"replacement this rule's autofix writes. Remove the '{{module}}' import.";
|
|
69
76
|
|
|
70
77
|
/**
|
|
71
78
|
* Does this declaration bring in anything the type checker alone can see?
|
|
@@ -128,12 +135,46 @@ function isDeadRemovableImport(sourceCode, node) {
|
|
|
128
135
|
* reported. The removal therefore lands on a later pass, after the rewrite that
|
|
129
136
|
* consumed the last reference — never speculatively alongside it.
|
|
130
137
|
*
|
|
131
|
-
*
|
|
132
|
-
*
|
|
133
|
-
*
|
|
134
|
-
*
|
|
135
|
-
*
|
|
136
|
-
*
|
|
138
|
+
* ## The two-part gate, and the evidence it cannot get
|
|
139
|
+
*
|
|
140
|
+
* Both gates keep this a REPAIR leg rather than a general unused-import rule,
|
|
141
|
+
* and both must be read from the SOURCE, never from a flag a `fix()` can flip:
|
|
142
|
+
* ESLint runs `fix()` for a suppressed problem before the `eslint-disable`
|
|
143
|
+
* filter discards it, so any mutable "did I add the import?" flag is already
|
|
144
|
+
* spent and lying by the time this runs.
|
|
145
|
+
*
|
|
146
|
+
* - `safeBoundInSource` — the safe symbol is in scope at all.
|
|
147
|
+
* - `replacementCalled` — THIS rule's own replacement is actually CALLED in this
|
|
148
|
+
* file (`safePath.join(…)`, `normalizedTmpdir()`, `toForwardSlash(…)`), which
|
|
149
|
+
* is the text its fixer writes and nothing else does.
|
|
150
|
+
*
|
|
151
|
+
* The first alone was the whole gate once, and it let an unrelated coincidence
|
|
152
|
+
* arm the leg: `safePath` reaches scope for reasons that have nothing to do with
|
|
153
|
+
* a call this pack consumed, and any `node:path` import that happened to be dead
|
|
154
|
+
* already — dead before the pack ever ran, for somebody else's reason — was then
|
|
155
|
+
* deleted under a message claiming this fixer had orphaned it. That is the
|
|
156
|
+
* general unused-import rule the module docstring above declines to be, reached
|
|
157
|
+
* by accident. The second gate closes it: no call to this rule's replacement
|
|
158
|
+
* means this rule rewrote nothing here, whatever else is in scope.
|
|
159
|
+
*
|
|
160
|
+
* It does NOT get down to the individual declaration, and it cannot. Per-import
|
|
161
|
+
* evidence has to name WHICH import a consumed reference belonged to, and the
|
|
162
|
+
* only pass that knows is the one doing the rewrite — where every reference is
|
|
163
|
+
* still live and nothing is dead to report. Removing speculatively in that pass
|
|
164
|
+
* instead is the trap the rest of this pack is built around: a suppressed call
|
|
165
|
+
* report still runs its `fix()`, so the removal lands while the call it was
|
|
166
|
+
* paired with survives, and the file is left with a dangling reference (146 of
|
|
167
|
+
* them, measured, in the defect this file exists to prevent). Latching state
|
|
168
|
+
* across passes to carry the answer forward is the rejected `WeakMap` approach
|
|
169
|
+
* for the same reason, one layer worse. So a file holding two imports of one
|
|
170
|
+
* module, one of them dead all along, still gets both removed and one true
|
|
171
|
+
* message plus one that merely describes the file rather than the history. The
|
|
172
|
+
* message is worded to stay true of both; the residual is a redundant deletion,
|
|
173
|
+
* never a broken one.
|
|
174
|
+
*
|
|
175
|
+
* Being over-strict here is cheap by construction: a gate that declines leaves
|
|
176
|
+
* an unused import, which is a lint finding a human reads. Only firing wrongly
|
|
177
|
+
* costs anything, and only the CALL rewrite can break a file.
|
|
137
178
|
*
|
|
138
179
|
* Its own report, with its own `fix`, deliberately — so the deletion appears in
|
|
139
180
|
* lint output and can be suppressed at the import line, rather than a rewrite
|
|
@@ -161,10 +202,19 @@ function isDeadRemovableImport(sourceCode, node) {
|
|
|
161
202
|
* @param {object} context - ESLint rule context.
|
|
162
203
|
* @param {object} sourceCode - ESLint `SourceCode` for the file being linted.
|
|
163
204
|
* @param {object[]} importNodes - Unsafe-module `ImportDeclaration`s seen this pass.
|
|
164
|
-
* @param {boolean}
|
|
205
|
+
* @param {boolean} safeBoundInSource - Was the safe symbol already bound in the SOURCE?
|
|
206
|
+
* @param {boolean} replacementCalled - Does the SOURCE call this rule's own safe
|
|
207
|
+
* replacement? Both are required, and both are the caller's to compute from
|
|
208
|
+
* source — see the gate discussion above for why neither may be defaulted.
|
|
165
209
|
*/
|
|
166
|
-
function reportDeadUnsafeImports(
|
|
167
|
-
|
|
210
|
+
function reportDeadUnsafeImports(
|
|
211
|
+
context,
|
|
212
|
+
sourceCode,
|
|
213
|
+
importNodes,
|
|
214
|
+
safeBoundInSource,
|
|
215
|
+
replacementCalled,
|
|
216
|
+
) {
|
|
217
|
+
if (!safeBoundInSource || !replacementCalled) {
|
|
168
218
|
return;
|
|
169
219
|
}
|
|
170
220
|
for (const node of importNodes) {
|
|
@@ -207,6 +207,11 @@ module.exports = function createNoUnsafeRule(config) {
|
|
|
207
207
|
// not be armed by a flag a suppressed report can spend — ESLint runs
|
|
208
208
|
// `fix()` before the `eslint-disable` filter discards the problem.
|
|
209
209
|
const safeBoundInSource = hasSafeImport;
|
|
210
|
+
// The dead-import leg's OTHER gate: does this file actually call `safeFn`,
|
|
211
|
+
// the free function this fixer writes? "The safe symbol is in scope" alone
|
|
212
|
+
// let an unrelated coincidence arm the leg — see `dead-import.cjs`. Read
|
|
213
|
+
// from the source during traversal, never from a `fix()`.
|
|
214
|
+
let safeReplacementCalled = false;
|
|
210
215
|
let unsafeImportNode = null;
|
|
211
216
|
const unsafeImportNodes = [];
|
|
212
217
|
let safeImportNode = null;
|
|
@@ -223,7 +228,13 @@ module.exports = function createNoUnsafeRule(config) {
|
|
|
223
228
|
},
|
|
224
229
|
|
|
225
230
|
'Program:exit'() {
|
|
226
|
-
reportDeadUnsafeImports(
|
|
231
|
+
reportDeadUnsafeImports(
|
|
232
|
+
context,
|
|
233
|
+
sourceCode,
|
|
234
|
+
unsafeImportNodes,
|
|
235
|
+
safeBoundInSource,
|
|
236
|
+
safeReplacementCalled,
|
|
237
|
+
);
|
|
227
238
|
},
|
|
228
239
|
|
|
229
240
|
ImportDeclaration(node) {
|
|
@@ -281,6 +292,10 @@ module.exports = function createNoUnsafeRule(config) {
|
|
|
281
292
|
},
|
|
282
293
|
|
|
283
294
|
CallExpression(node) {
|
|
295
|
+
if (node.callee.type === 'Identifier' && node.callee.name === safeFn) {
|
|
296
|
+
safeReplacementCalled = true;
|
|
297
|
+
}
|
|
298
|
+
|
|
284
299
|
let isUnsafeCall = false;
|
|
285
300
|
|
|
286
301
|
// Check for direct function call: unsafeFn()
|
|
@@ -57,6 +57,11 @@ module.exports = {
|
|
|
57
57
|
// Never mutated — the dead-import leg must not be armed by a flag that a
|
|
58
58
|
// suppressed report's `fix()` can spend. See `dead-import.cjs`.
|
|
59
59
|
const safeBoundInSource = hasToForwardSlashImport;
|
|
60
|
+
// The dead-import leg's OTHER gate: a `toForwardSlash(…)` call is the text
|
|
61
|
+
// this fixer writes, and the only evidence available that it wrote it here.
|
|
62
|
+
// Without it, any file with `toForwardSlash` in scope armed the leg — see
|
|
63
|
+
// `dead-import.cjs`. Read from the source, never from a `fix()`.
|
|
64
|
+
let safeReplacementCalled = false;
|
|
60
65
|
let utilsImportNode = null;
|
|
61
66
|
// `path.sep` is the last `path.*` reference in plenty of files, and
|
|
62
67
|
// `toForwardSlash(raw)` consumes it — leaving the same dead `node:path`
|
|
@@ -65,7 +70,13 @@ module.exports = {
|
|
|
65
70
|
|
|
66
71
|
return {
|
|
67
72
|
'Program:exit'() {
|
|
68
|
-
reportDeadUnsafeImports(
|
|
73
|
+
reportDeadUnsafeImports(
|
|
74
|
+
context,
|
|
75
|
+
sourceCode,
|
|
76
|
+
pathImportNodes,
|
|
77
|
+
safeBoundInSource,
|
|
78
|
+
safeReplacementCalled,
|
|
79
|
+
);
|
|
69
80
|
},
|
|
70
81
|
|
|
71
82
|
ImportDeclaration(node) {
|
|
@@ -83,6 +94,10 @@ module.exports = {
|
|
|
83
94
|
},
|
|
84
95
|
|
|
85
96
|
CallExpression(node) {
|
|
97
|
+
if (node.callee.type === 'Identifier' && node.callee.name === SAFE_FN) {
|
|
98
|
+
safeReplacementCalled = true;
|
|
99
|
+
}
|
|
100
|
+
|
|
86
101
|
// Check for .split(...).join('/') pattern
|
|
87
102
|
if (
|
|
88
103
|
node.callee.type === 'MemberExpression' &&
|
|
@@ -101,12 +116,18 @@ module.exports = {
|
|
|
101
116
|
) {
|
|
102
117
|
const splitArg = splitCall.arguments[0];
|
|
103
118
|
|
|
104
|
-
//
|
|
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.
|
|
105
126
|
const isSplittingByPathSep =
|
|
106
127
|
(splitArg.type === 'MemberExpression' &&
|
|
107
128
|
splitArg.object.name === 'path' &&
|
|
108
129
|
splitArg.property.name === 'sep') ||
|
|
109
|
-
(splitArg.type === 'Literal' &&
|
|
130
|
+
(splitArg.type === 'Literal' && splitArg.value === '\\');
|
|
110
131
|
|
|
111
132
|
if (isSplittingByPathSep) {
|
|
112
133
|
const variableBeingSplit = splitCall.callee.object;
|
|
@@ -147,11 +147,38 @@ function isIdentifierBound(sourceCode, node, name) {
|
|
|
147
147
|
return false;
|
|
148
148
|
}
|
|
149
149
|
|
|
150
|
+
/**
|
|
151
|
+
* Is this the call THIS rule's fixer writes — `safePath.<unsafeFn>(…)`?
|
|
152
|
+
*
|
|
153
|
+
* The evidence that this rule migrated something in this file, and the reason it
|
|
154
|
+
* is not enough to ask whether `safePath` is merely in scope. Read by BOTH legs
|
|
155
|
+
* that would otherwise mistake a file-wide fact for a claim about `unsafeFn`:
|
|
156
|
+
* the dead-import leg, and the bare-call repair leg in `classifyCall`.
|
|
157
|
+
* Matched whether or not `safePath` is bound: an ORPHANED `safePath.join(…)` —
|
|
158
|
+
* the repair leg's own subject — is still this fixer's handiwork.
|
|
159
|
+
*
|
|
160
|
+
* @param {object} node - A `CallExpression`.
|
|
161
|
+
* @param {string} unsafeFn - The member this rule migrates.
|
|
162
|
+
* @returns {boolean} True if the callee is `safePath.<unsafeFn>`.
|
|
163
|
+
*/
|
|
164
|
+
function isSafeReplacementCall(node, unsafeFn) {
|
|
165
|
+
return (
|
|
166
|
+
node.callee.type === 'MemberExpression' &&
|
|
167
|
+
node.callee.object.type === 'Identifier' &&
|
|
168
|
+
node.callee.object.name === SAFE_OBJECT &&
|
|
169
|
+
node.callee.property.type === 'Identifier' &&
|
|
170
|
+
node.callee.property.name === unsafeFn
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
|
|
150
174
|
/**
|
|
151
175
|
* Check if a call expression is an unsafe path function call.
|
|
152
176
|
*
|
|
153
177
|
* Returns `{ isNamed }` — or `{ importOnly: true }` for a call that is already
|
|
154
|
-
* correct and merely missing its import — or
|
|
178
|
+
* correct and merely missing its import — or `{ isRepair: true }` for a bare call
|
|
179
|
+
* that is ours only if the file also shows THIS function mid-migration, which
|
|
180
|
+
* `Program:exit` decides once the whole file has been seen — or null if not a
|
|
181
|
+
* match.
|
|
155
182
|
*/
|
|
156
183
|
function classifyCall(node, unsafeFn, state, sourceCode) {
|
|
157
184
|
const isMember = node.callee.type === 'MemberExpression' && node.callee.property.type === 'Identifier';
|
|
@@ -166,15 +193,33 @@ function classifyCall(node, unsafeFn, state, sourceCode) {
|
|
|
166
193
|
// `--fix` reached a stable fixpoint over source that no longer compiles and
|
|
167
194
|
// exited clean.
|
|
168
195
|
//
|
|
169
|
-
//
|
|
170
|
-
//
|
|
171
|
-
//
|
|
172
|
-
//
|
|
173
|
-
//
|
|
174
|
-
//
|
|
175
|
-
//
|
|
196
|
+
// An unbound `join` is NOT reliably our `join`: ESLint scope analysis does
|
|
197
|
+
// not bind `declare global { function join() }`, and it cannot see an ambient
|
|
198
|
+
// global from a `globals.d.ts`, an `@types` package, or a bundler — `resolve`
|
|
199
|
+
// and `relative` are entirely plausible as those. So the leg needs a second
|
|
200
|
+
// condition, and `safePathBoundInSource` alone was the wrong one: it is a
|
|
201
|
+
// fact about the FILE, not about `unsafeFn`, and a SIBLING instance of this
|
|
202
|
+
// factory hands it over for free. `no-path-resolve` rewrites a
|
|
203
|
+
// `path.resolve(...)` and imports `safePath`; on the next `--fix` pass the
|
|
204
|
+
// `join` instance sees a file with `safePath` bound and an ambient-global
|
|
205
|
+
// `join(...)` that predates any of this pack's involvement, and rewrites it
|
|
206
|
+
// to a different function. Measured with both rules enabled over one file.
|
|
207
|
+
//
|
|
208
|
+
// So this is only a CANDIDATE. `Program:exit` admits it once the file has
|
|
209
|
+
// been seen to contain a `safePath.<unsafeFn>(...)` call — the same positive
|
|
210
|
+
// evidence the dead-import leg reads, and the only in-file signal that THIS
|
|
211
|
+
// function is mid-migration HERE. Every genuine strand carries it:
|
|
212
|
+
// `removeSpecifier` only ever ships inside the same report's fix as the
|
|
213
|
+
// callee rewrite that consumed the specifier, and ESLint merges a report's
|
|
214
|
+
// fixes into one all-or-nothing range — so a file cannot lose the `node:path`
|
|
215
|
+
// specifier without gaining a `safePath.<unsafeFn>(` call.
|
|
216
|
+
//
|
|
217
|
+
// What this gives up: a file a HUMAN half-migrated by hand — import deleted,
|
|
218
|
+
// not one call rewritten — is no longer finished by `--fix`. That file is a
|
|
219
|
+
// loud `no-undef`/`tsc` error rather than a silent one, and it was never this
|
|
220
|
+
// pack's doing. Silently redirecting a live call to another function is.
|
|
176
221
|
if (state.safePathBoundInSource && !isIdentifierBound(sourceCode, node.callee, unsafeFn)) {
|
|
177
|
-
return { isNamed: false };
|
|
222
|
+
return { isNamed: false, isRepair: true };
|
|
178
223
|
}
|
|
179
224
|
return null;
|
|
180
225
|
}
|
|
@@ -363,23 +408,58 @@ module.exports = function createPathFunctionRule(config) {
|
|
|
363
408
|
// in any file that also has a `path.join()` to fix.
|
|
364
409
|
safePathBoundInSource: isNameAlreadyBound(sourceCode, SAFE_OBJECT),
|
|
365
410
|
safeImportNode: null,
|
|
411
|
+
// Set from the SOURCE as traversal walks it, and read only at
|
|
412
|
+
// `Program:exit` — so it is a fact about the text being linted, not
|
|
413
|
+
// about what a `fix()` intends to write. See `dead-import.cjs`, and the
|
|
414
|
+
// repair leg in `classifyCall`, which gates on the same flag.
|
|
415
|
+
safeReplacementCalled: false,
|
|
416
|
+
// Bare unbound calls awaiting that evidence. Held rather than reported,
|
|
417
|
+
// because a candidate can precede the migrated call that vouches for it.
|
|
418
|
+
deferredRepairs: [],
|
|
366
419
|
// EVERY path-module declaration, not just the one carrying `unsafeFn`.
|
|
367
420
|
// A file's dead binding is `import path from 'node:path'`, which
|
|
368
421
|
// `trackPathImport` only ever recorded as a NAME. See `dead-import.cjs`.
|
|
369
422
|
pathImportNodes: [],
|
|
370
423
|
};
|
|
371
424
|
|
|
425
|
+
const report = (node, classification) => {
|
|
426
|
+
context.report({
|
|
427
|
+
node,
|
|
428
|
+
messageId: 'noUnsafePathFn',
|
|
429
|
+
// The module name reaches the message through `{{safeModule}}` rather
|
|
430
|
+
// than being spelled out in each rule's string, so the advice cannot
|
|
431
|
+
// drift from where the fixer actually writes the import.
|
|
432
|
+
data: { safeModule: state.safeModule },
|
|
433
|
+
fix(fixer) {
|
|
434
|
+
return buildFix(fixer, node, unsafeFn, classification, sourceCode, state);
|
|
435
|
+
},
|
|
436
|
+
});
|
|
437
|
+
};
|
|
438
|
+
|
|
372
439
|
return {
|
|
373
440
|
Program(node) {
|
|
374
441
|
reportUnanchoredExemptEntries(context, node);
|
|
375
442
|
},
|
|
376
443
|
|
|
377
444
|
'Program:exit'() {
|
|
445
|
+
// Holding these back cannot change any FIX, only when it is computed.
|
|
446
|
+
// A repair candidate is classified only when `safePath` is bound in the
|
|
447
|
+
// source, so `hasSafePathImport` starts true and `namedImportSpec` is
|
|
448
|
+
// null: the report carries no import insert and no specifier removal,
|
|
449
|
+
// and therefore nothing shared that an earlier report could have spent
|
|
450
|
+
// first. (`importOnly` is the mirror case — it requires `safePath`
|
|
451
|
+
// UNBOUND — so the two can never arise in the same file.)
|
|
452
|
+
if (state.safeReplacementCalled) {
|
|
453
|
+
for (const candidate of state.deferredRepairs) {
|
|
454
|
+
report(candidate, { isNamed: false });
|
|
455
|
+
}
|
|
456
|
+
}
|
|
378
457
|
reportDeadUnsafeImports(
|
|
379
458
|
context,
|
|
380
459
|
sourceCode,
|
|
381
460
|
state.pathImportNodes,
|
|
382
461
|
state.safePathBoundInSource,
|
|
462
|
+
state.safeReplacementCalled,
|
|
383
463
|
);
|
|
384
464
|
},
|
|
385
465
|
|
|
@@ -394,22 +474,21 @@ module.exports = function createPathFunctionRule(config) {
|
|
|
394
474
|
},
|
|
395
475
|
|
|
396
476
|
CallExpression(node) {
|
|
477
|
+
if (isSafeReplacementCall(node, unsafeFn)) {
|
|
478
|
+
state.safeReplacementCalled = true;
|
|
479
|
+
}
|
|
480
|
+
|
|
397
481
|
const classification = classifyCall(node, unsafeFn, state, sourceCode);
|
|
398
482
|
if (!classification) {
|
|
399
483
|
return;
|
|
400
484
|
}
|
|
401
485
|
|
|
402
|
-
|
|
403
|
-
node
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
data: { safeModule: state.safeModule },
|
|
409
|
-
fix(fixer) {
|
|
410
|
-
return buildFix(fixer, node, unsafeFn, classification, sourceCode, state);
|
|
411
|
-
},
|
|
412
|
-
});
|
|
486
|
+
if (classification.isRepair) {
|
|
487
|
+
state.deferredRepairs.push(node);
|
|
488
|
+
return;
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
report(node, classification);
|
|
413
492
|
},
|
|
414
493
|
};
|
|
415
494
|
},
|
|
@@ -92,6 +92,29 @@ function literalEquivalent(patternBody) {
|
|
|
92
92
|
return literal;
|
|
93
93
|
}
|
|
94
94
|
|
|
95
|
+
/**
|
|
96
|
+
* Count the run of consecutive `\\` characters immediately preceding `index`
|
|
97
|
+
* in `text`.
|
|
98
|
+
*
|
|
99
|
+
* Used to decide whether a trailing `$` is an anchor or an escaped literal
|
|
100
|
+
* dollar sign: an EVEN run (including zero) means the `$` itself is
|
|
101
|
+
* unescaped — a genuine end-of-string anchor. An ODD run means the last of
|
|
102
|
+
* those backslashes escapes the `$`, making it a literal character. Looking
|
|
103
|
+
* only at the last two characters of the pattern (`\\$`) gets this wrong for
|
|
104
|
+
* two or more consecutive backslashes: `/\\\\$/` (an escaped backslash `\\\\`
|
|
105
|
+
* followed by an unescaped `$` anchor) also ends in the two characters `\\$`,
|
|
106
|
+
* but the `$` there IS an anchor.
|
|
107
|
+
*/
|
|
108
|
+
function countTrailingBackslashes(text, index) {
|
|
109
|
+
let count = 0;
|
|
110
|
+
let position = index - 1;
|
|
111
|
+
while (position >= 0 && text[position] === '\\') {
|
|
112
|
+
count += 1;
|
|
113
|
+
position -= 1;
|
|
114
|
+
}
|
|
115
|
+
return count;
|
|
116
|
+
}
|
|
117
|
+
|
|
95
118
|
/**
|
|
96
119
|
* Find the variable `identifier` resolves to, searching outward from its scope.
|
|
97
120
|
*/
|
|
@@ -245,7 +268,7 @@ module.exports = {
|
|
|
245
268
|
}
|
|
246
269
|
}
|
|
247
270
|
|
|
248
|
-
if (pattern.endsWith('$') &&
|
|
271
|
+
if (pattern.endsWith('$') && countTrailingBackslashes(pattern, pattern.length - 1) % 2 === 0) {
|
|
249
272
|
const literal = literalEquivalent(pattern.slice(0, -1));
|
|
250
273
|
if (literal !== null && literal !== '') {
|
|
251
274
|
report('preferEndsWith', literal);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vibe-agent-toolkit/utils",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0-rc.2",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Core utility functions shared across the vibe-agent-toolkit packages",
|
|
6
6
|
"sideEffects": false,
|
|
@@ -84,7 +84,7 @@
|
|
|
84
84
|
"README.md"
|
|
85
85
|
],
|
|
86
86
|
"scripts": {
|
|
87
|
-
"build": "tsc && tsx ../dev-tools/src/copy-yaml-assets.ts",
|
|
87
|
+
"build": "rimraf --glob dist \"*.tsbuildinfo\" && tsc && tsx ../dev-tools/src/copy-yaml-assets.ts",
|
|
88
88
|
"test:unit": "vitest run",
|
|
89
89
|
"test:integration": "vitest run --config vitest.integration.config.ts",
|
|
90
90
|
"test:system": "vitest run --config vitest.system.config.ts",
|