@vibe-agent-toolkit/utils 0.1.41 → 0.1.42
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 +146 -31
- package/dist/asset.d.ts +9 -0
- package/dist/asset.d.ts.map +1 -0
- package/dist/asset.js +9 -0
- package/dist/asset.js.map +1 -0
- package/dist/crawl.d.ts +19 -0
- package/dist/crawl.d.ts.map +1 -0
- package/dist/crawl.js +19 -0
- package/dist/crawl.js.map +1 -0
- package/dist/file-crawler.d.ts +19 -2
- package/dist/file-crawler.d.ts.map +1 -1
- package/dist/file-crawler.js +19 -1
- package/dist/file-crawler.js.map +1 -1
- package/dist/fs-utils.d.ts +60 -3
- package/dist/fs-utils.d.ts.map +1 -1
- package/dist/fs-utils.js +88 -10
- package/dist/fs-utils.js.map +1 -1
- package/dist/fs.d.ts +7 -6
- package/dist/fs.d.ts.map +1 -1
- package/dist/fs.js +7 -6
- package/dist/fs.js.map +1 -1
- package/dist/git-utils.d.ts +2 -0
- package/dist/git-utils.d.ts.map +1 -1
- package/dist/git-utils.js +16 -0
- package/dist/git-utils.js.map +1 -1
- package/dist/git.d.ts +23 -0
- package/dist/git.d.ts.map +1 -0
- package/dist/git.js +23 -0
- package/dist/git.js.map +1 -0
- package/dist/gitignore-checker.d.ts +0 -9
- package/dist/gitignore-checker.d.ts.map +1 -1
- package/dist/gitignore-checker.js +0 -12
- package/dist/gitignore-checker.js.map +1 -1
- package/dist/glob/glob-pattern.js +1 -1
- package/dist/glob/glob-pattern.js.map +1 -1
- package/dist/glob.d.ts +8 -0
- package/dist/glob.d.ts.map +1 -0
- package/dist/glob.js +8 -0
- package/dist/glob.js.map +1 -0
- package/dist/path-core.d.ts +196 -0
- package/dist/path-core.d.ts.map +1 -0
- package/dist/path-core.js +251 -0
- package/dist/path-core.js.map +1 -0
- package/dist/path-utils.d.ts +12 -183
- package/dist/path-utils.d.ts.map +1 -1
- package/dist/path-utils.js +13 -237
- package/dist/path-utils.js.map +1 -1
- package/dist/path.d.ts +12 -0
- package/dist/path.d.ts.map +1 -0
- package/dist/path.js +12 -0
- package/dist/path.js.map +1 -0
- package/dist/process.d.ts +7 -4
- package/dist/process.d.ts.map +1 -1
- package/dist/process.js +7 -4
- package/dist/process.js.map +1 -1
- package/dist/project.d.ts +31 -0
- package/dist/project.d.ts.map +1 -0
- package/dist/project.js +31 -0
- package/dist/project.js.map +1 -0
- package/dist/safe-exec.d.ts.map +1 -1
- package/dist/safe-exec.js +8 -4
- package/dist/safe-exec.js.map +1 -1
- package/dist/spawn-hardened.d.ts.map +1 -1
- package/dist/spawn-hardened.js +4 -17
- package/dist/spawn-hardened.js.map +1 -1
- package/dist/template-entry.d.ts +10 -0
- package/dist/template-entry.d.ts.map +1 -0
- package/dist/template-entry.js +10 -0
- package/dist/template-entry.js.map +1 -0
- package/dist/testing.d.ts +8 -0
- package/dist/testing.d.ts.map +1 -0
- package/dist/testing.js +8 -0
- package/dist/testing.js.map +1 -0
- package/dist/windows-shell.d.ts +140 -8
- package/dist/windows-shell.d.ts.map +1 -1
- package/dist/windows-shell.js +218 -11
- package/dist/windows-shell.js.map +1 -1
- package/dist/yaml.d.ts +9 -0
- package/dist/yaml.d.ts.map +1 -0
- package/dist/yaml.js +9 -0
- package/dist/yaml.js.map +1 -0
- package/dist/zod.d.ts +10 -0
- package/dist/zod.d.ts.map +1 -0
- package/dist/zod.js +10 -0
- package/dist/zod.js.map +1 -0
- package/eslint/README.md +217 -0
- package/eslint/index.cjs +161 -0
- package/eslint/index.d.cts +60 -0
- package/eslint/rules/dead-import.cjs +201 -0
- package/eslint/rules/eslint-rule-factory.cjs +410 -0
- package/eslint/rules/exempt-path-matcher.cjs +265 -0
- package/eslint/rules/no-bare-dynamic-import-path.cjs +142 -0
- package/eslint/rules/no-child-process-execSync.cjs +23 -0
- package/eslint/rules/no-command-direct-factory.cjs +246 -0
- package/eslint/rules/no-file-url-string-concat.cjs +77 -0
- package/eslint/rules/no-fs-mkdirSync.cjs +23 -0
- package/eslint/rules/no-fs-promises-cp.cjs +36 -0
- package/eslint/rules/no-fs-realpathSync.cjs +23 -0
- package/eslint/rules/no-hardcoded-path-split.cjs +141 -0
- package/eslint/rules/no-manual-path-normalize.cjs +161 -0
- package/eslint/rules/no-os-tmpdir.cjs +24 -0
- package/eslint/rules/no-path-join.cjs +14 -0
- package/eslint/rules/no-path-operations-in-comparisons.cjs +148 -0
- package/eslint/rules/no-path-relative.cjs +14 -0
- package/eslint/rules/no-path-resolve.cjs +14 -0
- package/eslint/rules/no-path-sep-in-strings.cjs +130 -0
- package/eslint/rules/no-path-startswith.cjs +139 -0
- package/eslint/rules/no-test-scoped-functions.cjs +134 -0
- package/eslint/rules/no-unix-shell-commands.cjs +152 -0
- package/eslint/rules/no-unsafe-root-join.cjs +85 -0
- package/eslint/rules/no-url-pathname-for-fs.cjs +107 -0
- package/eslint/rules/path-function-rule-factory.cjs +417 -0
- package/eslint/rules/prefer-startswith-over-regex.cjs +257 -0
- package/eslint/rules/require-justified-skip.cjs +348 -0
- package/eslint/rules/safe-import.cjs +164 -0
- package/package.json +65 -7
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ESLint rule to disallow splitting strings by hardcoded path separators
|
|
3
|
+
*
|
|
4
|
+
* Using .split('/') or .split('\\') on file paths breaks on Windows/Unix.
|
|
5
|
+
* Use path.basename(), path.dirname(), or normalize with toForwardSlash() first.
|
|
6
|
+
*
|
|
7
|
+
* This rule is smart enough to detect when paths are already normalized:
|
|
8
|
+
* - Inline: toForwardSlash(path).split('/') ✅
|
|
9
|
+
* - Variable: const normalized = toForwardSlash(path); normalized.split('/') ✅
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* // ❌ BAD - breaks on Windows (paths use backslashes)
|
|
13
|
+
* const filename = filePath.split('/').pop();
|
|
14
|
+
* const parts = filePath.split('\\');
|
|
15
|
+
*
|
|
16
|
+
* // ✅ GOOD - use path.basename() for filename
|
|
17
|
+
* import { basename } from 'node:path';
|
|
18
|
+
* const filename = basename(filePath);
|
|
19
|
+
*
|
|
20
|
+
* // ✅ GOOD - normalize then split (inline)
|
|
21
|
+
* import { toForwardSlash } from '@vibe-agent-toolkit/utils/path';
|
|
22
|
+
* const parts = toForwardSlash(filePath).split('/');
|
|
23
|
+
*
|
|
24
|
+
* // ✅ GOOD - normalize then split (variable)
|
|
25
|
+
* const normalizedPath = toForwardSlash(filePath);
|
|
26
|
+
* const parts = normalizedPath.split('/');
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
const {
|
|
30
|
+
SAFE_MODULE_ONLY_SCHEMA,
|
|
31
|
+
SAFE_PATH_MODULE,
|
|
32
|
+
resolveSafeModule,
|
|
33
|
+
} = require('./safe-import.cjs');
|
|
34
|
+
|
|
35
|
+
module.exports = {
|
|
36
|
+
meta: {
|
|
37
|
+
type: 'problem',
|
|
38
|
+
docs: {
|
|
39
|
+
description: 'Disallow splitting strings by hardcoded path separators',
|
|
40
|
+
category: 'Cross-platform compatibility',
|
|
41
|
+
recommended: true,
|
|
42
|
+
},
|
|
43
|
+
messages: {
|
|
44
|
+
noHardcodedSplit:
|
|
45
|
+
String.raw`Avoid .split('/') or .split('\') on file paths (breaks on Windows/Unix). ` +
|
|
46
|
+
'Use path.basename() to extract filename, or toForwardSlash() from {{safeModule}} to normalize paths first.',
|
|
47
|
+
},
|
|
48
|
+
schema: [SAFE_MODULE_ONLY_SCHEMA],
|
|
49
|
+
},
|
|
50
|
+
|
|
51
|
+
create(context) {
|
|
52
|
+
const reportData = { safeModule: resolveSafeModule(context, SAFE_PATH_MODULE) };
|
|
53
|
+
// Track variables that were assigned from toForwardSlash()
|
|
54
|
+
const normalizedVariables = new Set();
|
|
55
|
+
|
|
56
|
+
return {
|
|
57
|
+
// Track variable declarations
|
|
58
|
+
VariableDeclarator(node) {
|
|
59
|
+
// Check if this variable is assigned from toForwardSlash()
|
|
60
|
+
if (
|
|
61
|
+
node.init?.type === 'CallExpression' &&
|
|
62
|
+
node.init.callee.type === 'Identifier' &&
|
|
63
|
+
node.init.callee.name === 'toForwardSlash' &&
|
|
64
|
+
node.id.type === 'Identifier'
|
|
65
|
+
) {
|
|
66
|
+
normalizedVariables.add(node.id.name);
|
|
67
|
+
}
|
|
68
|
+
},
|
|
69
|
+
|
|
70
|
+
CallExpression(node) {
|
|
71
|
+
// Check if this is a .split() call
|
|
72
|
+
if (
|
|
73
|
+
node.callee.type !== 'MemberExpression' ||
|
|
74
|
+
node.callee.property.type !== 'Identifier' ||
|
|
75
|
+
node.callee.property.name !== 'split'
|
|
76
|
+
) {
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Check if the argument is a hardcoded path separator
|
|
81
|
+
const firstArg = node.arguments[0];
|
|
82
|
+
if (!firstArg) {
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Check for literal '/' or '\\'
|
|
87
|
+
const isPathSeparator =
|
|
88
|
+
(firstArg.type === 'Literal' &&
|
|
89
|
+
typeof firstArg.value === 'string' &&
|
|
90
|
+
(firstArg.value === '/' || firstArg.value === '\\')) ||
|
|
91
|
+
// Check for regex like /[/\\]/ or /\//
|
|
92
|
+
(firstArg.type === 'Literal' &&
|
|
93
|
+
firstArg.value instanceof RegExp &&
|
|
94
|
+
(firstArg.value.source.includes('/') ||
|
|
95
|
+
firstArg.value.source.includes('\\\\')));
|
|
96
|
+
|
|
97
|
+
if (!isPathSeparator) {
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Check if this is a safe usage (normalized path)
|
|
102
|
+
const object = node.callee.object;
|
|
103
|
+
|
|
104
|
+
// Case 1: Inline normalization - toForwardSlash(...).split('/')
|
|
105
|
+
if (
|
|
106
|
+
object.type === 'CallExpression' &&
|
|
107
|
+
object.callee.type === 'Identifier' &&
|
|
108
|
+
object.callee.name === 'toForwardSlash'
|
|
109
|
+
) {
|
|
110
|
+
return; // Safe - normalized inline
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Case 2: Variable that was normalized earlier
|
|
114
|
+
if (
|
|
115
|
+
object.type === 'Identifier' &&
|
|
116
|
+
normalizedVariables.has(object.name)
|
|
117
|
+
) {
|
|
118
|
+
return; // Safe - variable was normalized
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Case 3: Check if variable name suggests normalization
|
|
122
|
+
// Common patterns: normalizedPath, unixPath, forwardSlashPath
|
|
123
|
+
if (
|
|
124
|
+
object.type === 'Identifier' &&
|
|
125
|
+
(object.name.toLowerCase().includes('normalized') ||
|
|
126
|
+
object.name.toLowerCase().includes('unix') ||
|
|
127
|
+
object.name.toLowerCase().includes('forward'))
|
|
128
|
+
) {
|
|
129
|
+
return; // Safe - variable naming suggests normalization
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Not safe - report the issue
|
|
133
|
+
context.report({
|
|
134
|
+
node,
|
|
135
|
+
messageId: 'noHardcodedSplit',
|
|
136
|
+
data: reportData,
|
|
137
|
+
});
|
|
138
|
+
},
|
|
139
|
+
};
|
|
140
|
+
},
|
|
141
|
+
};
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ESLint rule to enforce using toForwardSlash() instead of manual normalization
|
|
3
|
+
*
|
|
4
|
+
* Detects manual path normalization patterns and suggests using the utility function.
|
|
5
|
+
*
|
|
6
|
+
* @example
|
|
7
|
+
* // ❌ BAD - manual normalization
|
|
8
|
+
* const normalized = relativePath.split(path.sep).join('/');
|
|
9
|
+
* const normalized = somePath.split('\\').join('/');
|
|
10
|
+
*
|
|
11
|
+
* // ✅ GOOD - use utility function
|
|
12
|
+
* import { toForwardSlash } from '@vibe-agent-toolkit/utils/path';
|
|
13
|
+
* const normalized = toForwardSlash(relativePath);
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
const {
|
|
17
|
+
DEAD_UNSAFE_IMPORT,
|
|
18
|
+
DEAD_UNSAFE_IMPORT_MESSAGE,
|
|
19
|
+
reportDeadUnsafeImports,
|
|
20
|
+
} = require('./dead-import.cjs');
|
|
21
|
+
const {
|
|
22
|
+
SAFE_MODULE_ONLY_SCHEMA,
|
|
23
|
+
SAFE_PATH_MODULE,
|
|
24
|
+
insertAboveWithComments,
|
|
25
|
+
isNameAlreadyBound,
|
|
26
|
+
resolveSafeModule,
|
|
27
|
+
} = require('./safe-import.cjs');
|
|
28
|
+
|
|
29
|
+
const SAFE_FN = 'toForwardSlash';
|
|
30
|
+
const PATH_MODULES = new Set(['node:path', 'path']);
|
|
31
|
+
|
|
32
|
+
module.exports = {
|
|
33
|
+
meta: {
|
|
34
|
+
type: 'problem',
|
|
35
|
+
docs: {
|
|
36
|
+
description: 'Disallow manual path normalization patterns',
|
|
37
|
+
category: 'Cross-platform compatibility',
|
|
38
|
+
recommended: true,
|
|
39
|
+
},
|
|
40
|
+
fixable: 'code',
|
|
41
|
+
messages: {
|
|
42
|
+
useToForwardSlash:
|
|
43
|
+
'Use toForwardSlash() from {{safeModule}} instead of manual path normalization. ' +
|
|
44
|
+
'Manual normalization is error-prone and less maintainable.',
|
|
45
|
+
[DEAD_UNSAFE_IMPORT]: DEAD_UNSAFE_IMPORT_MESSAGE,
|
|
46
|
+
},
|
|
47
|
+
schema: [SAFE_MODULE_ONLY_SCHEMA],
|
|
48
|
+
},
|
|
49
|
+
|
|
50
|
+
create(context) {
|
|
51
|
+
const sourceCode = context.getSourceCode();
|
|
52
|
+
const targetModule = resolveSafeModule(context, SAFE_PATH_MODULE);
|
|
53
|
+
// Seeded from SCOPE: a file that already imports `toForwardSlash` from the
|
|
54
|
+
// barrel must have the call rewritten WITHOUT gaining a second binding of
|
|
55
|
+
// the same name — that is a SyntaxError. See `safe-import.cjs`.
|
|
56
|
+
let hasToForwardSlashImport = isNameAlreadyBound(sourceCode, SAFE_FN);
|
|
57
|
+
// Never mutated — the dead-import leg must not be armed by a flag that a
|
|
58
|
+
// suppressed report's `fix()` can spend. See `dead-import.cjs`.
|
|
59
|
+
const safeBoundInSource = hasToForwardSlashImport;
|
|
60
|
+
let utilsImportNode = null;
|
|
61
|
+
// `path.sep` is the last `path.*` reference in plenty of files, and
|
|
62
|
+
// `toForwardSlash(raw)` consumes it — leaving the same dead `node:path`
|
|
63
|
+
// binding the `safePath` rules used to leave.
|
|
64
|
+
const pathImportNodes = [];
|
|
65
|
+
|
|
66
|
+
return {
|
|
67
|
+
'Program:exit'() {
|
|
68
|
+
reportDeadUnsafeImports(context, sourceCode, pathImportNodes, safeBoundInSource);
|
|
69
|
+
},
|
|
70
|
+
|
|
71
|
+
ImportDeclaration(node) {
|
|
72
|
+
if (PATH_MODULES.has(node.source.value)) {
|
|
73
|
+
pathImportNodes.push(node);
|
|
74
|
+
}
|
|
75
|
+
if (node.source.value === targetModule) {
|
|
76
|
+
utilsImportNode = node;
|
|
77
|
+
for (const spec of node.specifiers) {
|
|
78
|
+
if (spec.type === 'ImportSpecifier' && spec.imported.name === SAFE_FN) {
|
|
79
|
+
hasToForwardSlashImport = true;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
},
|
|
84
|
+
|
|
85
|
+
CallExpression(node) {
|
|
86
|
+
// Check for .split(...).join('/') pattern
|
|
87
|
+
if (
|
|
88
|
+
node.callee.type === 'MemberExpression' &&
|
|
89
|
+
node.callee.property.name === 'join' &&
|
|
90
|
+
node.arguments.length === 1 &&
|
|
91
|
+
node.arguments[0].type === 'Literal' &&
|
|
92
|
+
node.arguments[0].value === '/'
|
|
93
|
+
) {
|
|
94
|
+
// Check if the object is a .split() call
|
|
95
|
+
const splitCall = node.callee.object;
|
|
96
|
+
if (
|
|
97
|
+
splitCall.type === 'CallExpression' &&
|
|
98
|
+
splitCall.callee.type === 'MemberExpression' &&
|
|
99
|
+
splitCall.callee.property.name === 'split' &&
|
|
100
|
+
splitCall.arguments.length === 1
|
|
101
|
+
) {
|
|
102
|
+
const splitArg = splitCall.arguments[0];
|
|
103
|
+
|
|
104
|
+
// Check if splitting by path.sep, '\\', or '\\\\'
|
|
105
|
+
const isSplittingByPathSep =
|
|
106
|
+
(splitArg.type === 'MemberExpression' &&
|
|
107
|
+
splitArg.object.name === 'path' &&
|
|
108
|
+
splitArg.property.name === 'sep') ||
|
|
109
|
+
(splitArg.type === 'Literal' && (splitArg.value === '\\' || splitArg.value === '\\\\'));
|
|
110
|
+
|
|
111
|
+
if (isSplittingByPathSep) {
|
|
112
|
+
const variableBeingSplit = splitCall.callee.object;
|
|
113
|
+
|
|
114
|
+
context.report({
|
|
115
|
+
node,
|
|
116
|
+
messageId: 'useToForwardSlash',
|
|
117
|
+
data: { safeModule: targetModule },
|
|
118
|
+
fix(fixer) {
|
|
119
|
+
const fixes = [];
|
|
120
|
+
|
|
121
|
+
// Replace the entire .split(...).join('/') with toForwardSlash(...)
|
|
122
|
+
const originalVar = sourceCode.getText(variableBeingSplit);
|
|
123
|
+
fixes.push(fixer.replaceText(node, `${SAFE_FN}(${originalVar})`));
|
|
124
|
+
|
|
125
|
+
// Add import if needed
|
|
126
|
+
if (!hasToForwardSlashImport) {
|
|
127
|
+
if (utilsImportNode) {
|
|
128
|
+
// Add to existing utils import
|
|
129
|
+
const lastSpecifier = utilsImportNode.specifiers.at(-1);
|
|
130
|
+
fixes.push(fixer.insertTextAfter(lastSpecifier, `, ${SAFE_FN}`));
|
|
131
|
+
} else {
|
|
132
|
+
// Create new import at the top
|
|
133
|
+
const firstNode = sourceCode.ast.body[0];
|
|
134
|
+
const newImport = `import { ${SAFE_FN} } from '${targetModule}';\n`;
|
|
135
|
+
fixes.push(insertAboveWithComments(fixer, sourceCode, firstNode, newImport));
|
|
136
|
+
}
|
|
137
|
+
// NOT latched. The comment here used to claim that without a
|
|
138
|
+
// `hasToForwardSlashImport = true` a second occurrence would
|
|
139
|
+
// insert the import twice; an adversarial run could not
|
|
140
|
+
// reproduce that at any occurrence count. It cannot happen:
|
|
141
|
+
// both reports insert identical text at the identical anchor,
|
|
142
|
+
// so the ranges coincide and ESLint applies one and drops the
|
|
143
|
+
// other as overlapping.
|
|
144
|
+
//
|
|
145
|
+
// Latching it is not free, either. ESLint runs `fix()` for a
|
|
146
|
+
// SUPPRESSED problem before the `eslint-disable` filter
|
|
147
|
+
// discards it, so the first report could spend the flag and
|
|
148
|
+
// then be thrown away — leaving later occurrences rewritten
|
|
149
|
+
// to a `toForwardSlash` nothing imports.
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return fixes;
|
|
153
|
+
},
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
},
|
|
159
|
+
};
|
|
160
|
+
},
|
|
161
|
+
};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ESLint rule: no-os-tmpdir
|
|
3
|
+
*
|
|
4
|
+
* Prevents usage of os.tmpdir() in favor of normalizedTmpdir() from `@vibe-agent-toolkit/utils/fs`
|
|
5
|
+
*
|
|
6
|
+
* Why: os.tmpdir() returns Windows 8.3 short paths (RUNNER~1) which cause module loading
|
|
7
|
+
* errors when paths are passed to child processes or used with import statements.
|
|
8
|
+
*
|
|
9
|
+
* Auto-fix: Replaces os.tmpdir() with normalizedTmpdir() and adds required import.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const factory = require('./eslint-rule-factory.cjs');
|
|
13
|
+
const { SAFE_FS_MODULE } = require('./safe-import.cjs');
|
|
14
|
+
|
|
15
|
+
module.exports = factory({
|
|
16
|
+
unsafeFn: 'tmpdir',
|
|
17
|
+
unsafeModule: 'node:os',
|
|
18
|
+
safeFn: 'normalizedTmpdir',
|
|
19
|
+
safeModule: SAFE_FS_MODULE,
|
|
20
|
+
message: 'Use normalizedTmpdir() from {{safeModule}} instead of os.tmpdir() for Windows compatibility (prevents 8.3 short name issues like RUNNER~1)',
|
|
21
|
+
// No baked-in exemption: the file that implements normalizedTmpdir() is
|
|
22
|
+
// repo-specific. Consumers declare it as { exemptFiles: [...] }.
|
|
23
|
+
checkMemberExpression: true, // Catch os.tmpdir() pattern
|
|
24
|
+
});
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ESLint rule: no-path-join
|
|
3
|
+
*
|
|
4
|
+
* Bans path.join() from node:path. Use safePath.join() from `@vibe-agent-toolkit/utils/path`.
|
|
5
|
+
* safePath.join() wraps path.join() + toForwardSlash() to prevent Windows backslash bugs.
|
|
6
|
+
*/
|
|
7
|
+
const factory = require('./path-function-rule-factory.cjs');
|
|
8
|
+
|
|
9
|
+
module.exports = factory({
|
|
10
|
+
unsafeFn: 'join',
|
|
11
|
+
message:
|
|
12
|
+
'Use safePath.join() from {{safeModule}} instead of path.join(). ' +
|
|
13
|
+
'path.join() returns backslashes on Windows, causing Map key mismatches and path comparison bugs.',
|
|
14
|
+
});
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ESLint rule to enforce normalizing path operations before string comparisons
|
|
3
|
+
*
|
|
4
|
+
* path.relative(), path.dirname(), path.basename(), path.join() return OS-specific separators.
|
|
5
|
+
* When comparing with literal strings (especially in markdown), normalize to forward slashes.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* // ❌ BAD - path.relative() returns backslashes on Windows
|
|
9
|
+
* const relativePath = path.relative(baseDir, filePath);
|
|
10
|
+
* if (content.includes(relativePath)) { ... } // FAILS on Windows!
|
|
11
|
+
*
|
|
12
|
+
* // ✅ GOOD - normalize before comparison
|
|
13
|
+
* import { toForwardSlash } from '@vibe-agent-toolkit/utils/path';
|
|
14
|
+
* const relativePath = toForwardSlash(path.relative(baseDir, filePath));
|
|
15
|
+
* if (content.includes(relativePath)) { ... }
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
const {
|
|
19
|
+
SAFE_MODULE_ONLY_SCHEMA,
|
|
20
|
+
SAFE_PATH_MODULE,
|
|
21
|
+
resolveSafeModule,
|
|
22
|
+
} = require('./safe-import.cjs');
|
|
23
|
+
|
|
24
|
+
module.exports = {
|
|
25
|
+
meta: {
|
|
26
|
+
type: 'problem',
|
|
27
|
+
docs: {
|
|
28
|
+
description: 'Disallow using path operations directly in string comparisons',
|
|
29
|
+
category: 'Cross-platform compatibility',
|
|
30
|
+
recommended: true,
|
|
31
|
+
},
|
|
32
|
+
messages: {
|
|
33
|
+
normalizePathOperation:
|
|
34
|
+
'Wrap path.{{method}}() with toForwardSlash() from {{safeModule}} before using in string operations. ' +
|
|
35
|
+
'Path operations return OS-specific separators that fail in cross-platform comparisons.',
|
|
36
|
+
},
|
|
37
|
+
schema: [SAFE_MODULE_ONLY_SCHEMA],
|
|
38
|
+
},
|
|
39
|
+
|
|
40
|
+
create(context) {
|
|
41
|
+
const safeModule = resolveSafeModule(context, SAFE_PATH_MODULE);
|
|
42
|
+
// Path methods that return paths with OS-specific separators
|
|
43
|
+
const pathMethodsReturningPaths = new Set([
|
|
44
|
+
'relative',
|
|
45
|
+
'dirname',
|
|
46
|
+
'basename',
|
|
47
|
+
'join',
|
|
48
|
+
'resolve',
|
|
49
|
+
'normalize',
|
|
50
|
+
]);
|
|
51
|
+
|
|
52
|
+
// String methods that compare/search strings
|
|
53
|
+
const stringComparisonMethods = new Set([
|
|
54
|
+
'includes',
|
|
55
|
+
'indexOf',
|
|
56
|
+
'lastIndexOf',
|
|
57
|
+
'startsWith',
|
|
58
|
+
'endsWith',
|
|
59
|
+
'split',
|
|
60
|
+
'replace',
|
|
61
|
+
'replaceAll',
|
|
62
|
+
'match',
|
|
63
|
+
'search',
|
|
64
|
+
]);
|
|
65
|
+
|
|
66
|
+
// Track variables that hold unwrapped path operation results
|
|
67
|
+
const unwrappedPathVariables = new Set();
|
|
68
|
+
const wrappedPathVariables = new Set();
|
|
69
|
+
|
|
70
|
+
// If `node` is a `path.<method>(...)` call where method returns
|
|
71
|
+
// OS-specific separators, return that method name. Otherwise undefined.
|
|
72
|
+
const unnormalizedPathCallMethod = (node) =>
|
|
73
|
+
node?.type === 'CallExpression' &&
|
|
74
|
+
node.callee.type === 'MemberExpression' &&
|
|
75
|
+
node.callee.object.type === 'Identifier' &&
|
|
76
|
+
node.callee.object.name === 'path' &&
|
|
77
|
+
pathMethodsReturningPaths.has(node.callee.property.name)
|
|
78
|
+
? node.callee.property.name
|
|
79
|
+
: undefined;
|
|
80
|
+
|
|
81
|
+
const isStringComparisonCall = (node) =>
|
|
82
|
+
node.callee.type === 'MemberExpression' &&
|
|
83
|
+
stringComparisonMethods.has(node.callee.property.name);
|
|
84
|
+
|
|
85
|
+
const reportPathArgument = (arg) => {
|
|
86
|
+
const method = unnormalizedPathCallMethod(arg);
|
|
87
|
+
if (method !== undefined) {
|
|
88
|
+
context.report({ node: arg, messageId: 'normalizePathOperation', data: { method, safeModule } });
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
if (
|
|
92
|
+
arg.type === 'Identifier' &&
|
|
93
|
+
unwrappedPathVariables.has(arg.name) &&
|
|
94
|
+
!wrappedPathVariables.has(arg.name)
|
|
95
|
+
) {
|
|
96
|
+
context.report({
|
|
97
|
+
node: arg,
|
|
98
|
+
messageId: 'normalizePathOperation',
|
|
99
|
+
data: { method: 'operation', safeModule },
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
const reportTemplateLiteralExpressions = (templateLiteral) => {
|
|
105
|
+
for (const expr of templateLiteral.expressions) {
|
|
106
|
+
const method = unnormalizedPathCallMethod(expr);
|
|
107
|
+
if (method !== undefined) {
|
|
108
|
+
context.report({
|
|
109
|
+
node: expr,
|
|
110
|
+
messageId: 'normalizePathOperation',
|
|
111
|
+
data: { method, safeModule },
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
return {
|
|
118
|
+
VariableDeclarator(node) {
|
|
119
|
+
if (!node.init || node.id.type !== 'Identifier') return;
|
|
120
|
+
|
|
121
|
+
if (unnormalizedPathCallMethod(node.init) !== undefined) {
|
|
122
|
+
unwrappedPathVariables.add(node.id.name);
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Initializer is `toForwardSlash(<expr>)` — treat the variable as wrapped.
|
|
127
|
+
if (
|
|
128
|
+
node.init.type === 'CallExpression' &&
|
|
129
|
+
node.init.callee.type === 'Identifier' &&
|
|
130
|
+
node.init.callee.name === 'toForwardSlash' &&
|
|
131
|
+
node.init.arguments.length === 1
|
|
132
|
+
) {
|
|
133
|
+
wrappedPathVariables.add(node.id.name);
|
|
134
|
+
}
|
|
135
|
+
},
|
|
136
|
+
|
|
137
|
+
CallExpression(node) {
|
|
138
|
+
if (!isStringComparisonCall(node)) return;
|
|
139
|
+
for (const arg of node.arguments) {
|
|
140
|
+
reportPathArgument(arg);
|
|
141
|
+
if (arg.type === 'TemplateLiteral') {
|
|
142
|
+
reportTemplateLiteralExpressions(arg);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
},
|
|
146
|
+
};
|
|
147
|
+
},
|
|
148
|
+
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ESLint rule: no-path-relative
|
|
3
|
+
*
|
|
4
|
+
* Bans path.relative() from node:path. Use safePath.relative() from `@vibe-agent-toolkit/utils/path`.
|
|
5
|
+
* safePath.relative() wraps path.relative() + toForwardSlash() to prevent Windows backslash bugs.
|
|
6
|
+
*/
|
|
7
|
+
const factory = require('./path-function-rule-factory.cjs');
|
|
8
|
+
|
|
9
|
+
module.exports = factory({
|
|
10
|
+
unsafeFn: 'relative',
|
|
11
|
+
message:
|
|
12
|
+
'Use safePath.relative() from {{safeModule}} instead of path.relative(). ' +
|
|
13
|
+
'path.relative() returns backslashes on Windows, causing Map key mismatches and path comparison bugs.',
|
|
14
|
+
});
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ESLint rule: no-path-resolve
|
|
3
|
+
*
|
|
4
|
+
* Bans path.resolve() from node:path. Use safePath.resolve() from `@vibe-agent-toolkit/utils/path`.
|
|
5
|
+
* safePath.resolve() wraps path.resolve() + toForwardSlash() to prevent Windows backslash bugs.
|
|
6
|
+
*/
|
|
7
|
+
const factory = require('./path-function-rule-factory.cjs');
|
|
8
|
+
|
|
9
|
+
module.exports = factory({
|
|
10
|
+
unsafeFn: 'resolve',
|
|
11
|
+
message:
|
|
12
|
+
'Use safePath.resolve() from {{safeModule}} instead of path.resolve(). ' +
|
|
13
|
+
'path.resolve() returns backslashes on Windows, causing Map key mismatches and path comparison bugs.',
|
|
14
|
+
});
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ESLint rule to disallow using path.sep in string operations
|
|
3
|
+
*
|
|
4
|
+
* path.sep is '\\' on Windows and '/' on Unix, which causes cross-platform issues
|
|
5
|
+
* when used in string operations. Use toForwardSlash() to normalize paths instead.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* // ❌ BAD - path.sep varies by platform
|
|
9
|
+
* const parts = filePath.split(path.sep);
|
|
10
|
+
* if (filePath.includes(path.sep)) { ... }
|
|
11
|
+
*
|
|
12
|
+
* // ✅ GOOD - normalize then split
|
|
13
|
+
* import { toForwardSlash } from '@vibe-agent-toolkit/utils/path';
|
|
14
|
+
* const parts = toForwardSlash(filePath).split('/');
|
|
15
|
+
* if (toForwardSlash(filePath).includes('/')) { ... }
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
const {
|
|
19
|
+
SAFE_MODULE_ONLY_SCHEMA,
|
|
20
|
+
SAFE_PATH_MODULE,
|
|
21
|
+
resolveSafeModule,
|
|
22
|
+
} = require('./safe-import.cjs');
|
|
23
|
+
|
|
24
|
+
module.exports = {
|
|
25
|
+
meta: {
|
|
26
|
+
type: 'problem',
|
|
27
|
+
docs: {
|
|
28
|
+
description: 'Disallow using path.sep in string operations',
|
|
29
|
+
category: 'Cross-platform compatibility',
|
|
30
|
+
recommended: true,
|
|
31
|
+
},
|
|
32
|
+
messages: {
|
|
33
|
+
noPathSep:
|
|
34
|
+
'Avoid using path.sep in string operations (split, includes, indexOf, etc.). ' +
|
|
35
|
+
'Use toForwardSlash() from {{safeModule}} to normalize paths to forward slashes first.',
|
|
36
|
+
},
|
|
37
|
+
schema: [SAFE_MODULE_ONLY_SCHEMA],
|
|
38
|
+
},
|
|
39
|
+
|
|
40
|
+
create(context) {
|
|
41
|
+
const reportData = { safeModule: resolveSafeModule(context, SAFE_PATH_MODULE) };
|
|
42
|
+
// String methods that take a separator/search argument
|
|
43
|
+
const stringMethodsWithSeparator = new Set([
|
|
44
|
+
'split',
|
|
45
|
+
'includes',
|
|
46
|
+
'indexOf',
|
|
47
|
+
'lastIndexOf',
|
|
48
|
+
'startsWith',
|
|
49
|
+
'endsWith',
|
|
50
|
+
'replace',
|
|
51
|
+
'replaceAll',
|
|
52
|
+
]);
|
|
53
|
+
|
|
54
|
+
return {
|
|
55
|
+
CallExpression(node) {
|
|
56
|
+
// Check if this is a string method call
|
|
57
|
+
if (
|
|
58
|
+
node.callee.type !== 'MemberExpression' ||
|
|
59
|
+
node.callee.property.type !== 'Identifier' ||
|
|
60
|
+
!stringMethodsWithSeparator.has(node.callee.property.name)
|
|
61
|
+
) {
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Check if any argument is path.sep
|
|
66
|
+
const hasPathSepArg = node.arguments.some(
|
|
67
|
+
(arg) =>
|
|
68
|
+
arg.type === 'MemberExpression' &&
|
|
69
|
+
arg.object.type === 'Identifier' &&
|
|
70
|
+
arg.object.name === 'path' &&
|
|
71
|
+
arg.property.type === 'Identifier' &&
|
|
72
|
+
arg.property.name === 'sep',
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
if (hasPathSepArg) {
|
|
76
|
+
context.report({
|
|
77
|
+
node,
|
|
78
|
+
messageId: 'noPathSep',
|
|
79
|
+
data: reportData,
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
},
|
|
83
|
+
|
|
84
|
+
// Also catch template literals and binary expressions using path.sep
|
|
85
|
+
TemplateLiteral(node) {
|
|
86
|
+
const hasPathSep = node.expressions.some(
|
|
87
|
+
(expr) =>
|
|
88
|
+
expr.type === 'MemberExpression' &&
|
|
89
|
+
expr.object.type === 'Identifier' &&
|
|
90
|
+
expr.object.name === 'path' &&
|
|
91
|
+
expr.property.type === 'Identifier' &&
|
|
92
|
+
expr.property.name === 'sep',
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
if (hasPathSep) {
|
|
96
|
+
context.report({
|
|
97
|
+
node,
|
|
98
|
+
messageId: 'noPathSep',
|
|
99
|
+
data: reportData,
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
},
|
|
103
|
+
|
|
104
|
+
BinaryExpression(node) {
|
|
105
|
+
// Check for string concatenation with path.sep
|
|
106
|
+
if (node.operator === '+') {
|
|
107
|
+
const hasPathSep =
|
|
108
|
+
(node.left.type === 'MemberExpression' &&
|
|
109
|
+
node.left.object.type === 'Identifier' &&
|
|
110
|
+
node.left.object.name === 'path' &&
|
|
111
|
+
node.left.property.type === 'Identifier' &&
|
|
112
|
+
node.left.property.name === 'sep') ||
|
|
113
|
+
(node.right.type === 'MemberExpression' &&
|
|
114
|
+
node.right.object.type === 'Identifier' &&
|
|
115
|
+
node.right.object.name === 'path' &&
|
|
116
|
+
node.right.property.type === 'Identifier' &&
|
|
117
|
+
node.right.property.name === 'sep');
|
|
118
|
+
|
|
119
|
+
if (hasPathSep) {
|
|
120
|
+
context.report({
|
|
121
|
+
node,
|
|
122
|
+
messageId: 'noPathSep',
|
|
123
|
+
data: reportData,
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
},
|
|
128
|
+
};
|
|
129
|
+
},
|
|
130
|
+
};
|