@vibe-agent-toolkit/utils 0.1.41 → 0.1.42-rc.1
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 +191 -0
- package/eslint/index.cjs +161 -0
- package/eslint/index.d.cts +60 -0
- package/eslint/rules/eslint-rule-factory.cjs +241 -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 +130 -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 +216 -0
- package/eslint/rules/prefer-startswith-over-regex.cjs +111 -0
- package/eslint/rules/require-justified-skip.cjs +348 -0
- package/eslint/rules/safe-import.cjs +141 -0
- package/package.json +65 -7
|
@@ -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
|
+
};
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ESLint rule to enforce using toForwardSlash() before path.startsWith()
|
|
3
|
+
*
|
|
4
|
+
* Cross-platform path comparisons fail on Windows when mixing backslashes and forward slashes.
|
|
5
|
+
* This rule enforces using toForwardSlash() from `@vibe-agent-toolkit/utils/path` for consistent behavior.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* // ❌ BAD - fails on Windows
|
|
9
|
+
* if (pluginDir.startsWith(marketplaceDir)) { ... }
|
|
10
|
+
*
|
|
11
|
+
* // ✅ GOOD - works cross-platform
|
|
12
|
+
* import { toForwardSlash } from '@vibe-agent-toolkit/utils/path';
|
|
13
|
+
* const normalizedPlugin = toForwardSlash(pluginDir);
|
|
14
|
+
* const normalizedMarketplace = toForwardSlash(marketplaceDir);
|
|
15
|
+
* if (normalizedPlugin.startsWith(normalizedMarketplace)) { ... }
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Check if the comparison argument is a URL scheme or absolute path indicator
|
|
20
|
+
* These don't need normalization: 'http://', 'https://', 'file://', '/', '#'
|
|
21
|
+
*/
|
|
22
|
+
function isUrlSchemeOrAbsoluteCheck(node) {
|
|
23
|
+
if (node.arguments.length === 0 || node.arguments[0]?.type !== 'Literal') {
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const compareValue = node.arguments[0].value;
|
|
28
|
+
if (typeof compareValue !== 'string') {
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return (
|
|
33
|
+
compareValue === '/' ||
|
|
34
|
+
compareValue === '#' ||
|
|
35
|
+
compareValue.startsWith('http://') ||
|
|
36
|
+
compareValue.startsWith('https://') ||
|
|
37
|
+
compareValue.startsWith('file://')
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Check if a variable name contains path-related keywords
|
|
43
|
+
*/
|
|
44
|
+
function hasPathKeyword(name) {
|
|
45
|
+
const lowerName = name.toLowerCase();
|
|
46
|
+
return (
|
|
47
|
+
lowerName.includes('path') ||
|
|
48
|
+
lowerName.includes('dir') ||
|
|
49
|
+
lowerName.includes('file') ||
|
|
50
|
+
lowerName.includes('location')
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Check if a variable name indicates it's already normalized
|
|
56
|
+
*/
|
|
57
|
+
function isNormalizedVariable(name) {
|
|
58
|
+
return name.toLowerCase().startsWith('normalized');
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const {
|
|
62
|
+
SAFE_MODULE_ONLY_SCHEMA,
|
|
63
|
+
SAFE_PATH_MODULE,
|
|
64
|
+
resolveSafeModule,
|
|
65
|
+
} = require('./safe-import.cjs');
|
|
66
|
+
|
|
67
|
+
module.exports = {
|
|
68
|
+
meta: {
|
|
69
|
+
type: 'problem',
|
|
70
|
+
docs: {
|
|
71
|
+
description: 'Disallow direct path.startsWith() without normalization',
|
|
72
|
+
category: 'Cross-platform compatibility',
|
|
73
|
+
recommended: true,
|
|
74
|
+
},
|
|
75
|
+
messages: {
|
|
76
|
+
useNormalizeHelper:
|
|
77
|
+
'Use toForwardSlash() from {{safeModule}} before path.startsWith() for cross-platform compatibility. ' +
|
|
78
|
+
'Direct string comparison fails on Windows with mixed separators.',
|
|
79
|
+
},
|
|
80
|
+
schema: [SAFE_MODULE_ONLY_SCHEMA],
|
|
81
|
+
},
|
|
82
|
+
|
|
83
|
+
create(context) {
|
|
84
|
+
const reportData = { safeModule: resolveSafeModule(context, SAFE_PATH_MODULE) };
|
|
85
|
+
return {
|
|
86
|
+
CallExpression(node) {
|
|
87
|
+
// Check if this is a .startsWith() call
|
|
88
|
+
if (
|
|
89
|
+
node.callee.type !== 'MemberExpression' ||
|
|
90
|
+
node.callee.property.type !== 'Identifier' ||
|
|
91
|
+
node.callee.property.name !== 'startsWith'
|
|
92
|
+
) {
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Skip URL scheme and absolute path checks
|
|
97
|
+
if (isUrlSchemeOrAbsoluteCheck(node)) {
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const objectNode = node.callee.object;
|
|
102
|
+
|
|
103
|
+
// Check identifier variables (e.g., pluginPath.startsWith(...))
|
|
104
|
+
if (objectNode.type === 'Identifier') {
|
|
105
|
+
const varName = objectNode.name;
|
|
106
|
+
|
|
107
|
+
if (isNormalizedVariable(varName)) {
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (hasPathKeyword(varName)) {
|
|
112
|
+
context.report({
|
|
113
|
+
node,
|
|
114
|
+
messageId: 'useNormalizeHelper',
|
|
115
|
+
data: reportData,
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Check member expressions (e.g., resource.filePath.startsWith(...))
|
|
121
|
+
if (objectNode.type === 'MemberExpression' && objectNode.property.type === 'Identifier') {
|
|
122
|
+
const propName = objectNode.property.name;
|
|
123
|
+
|
|
124
|
+
if (isNormalizedVariable(propName)) {
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (hasPathKeyword(propName)) {
|
|
129
|
+
context.report({
|
|
130
|
+
node,
|
|
131
|
+
messageId: 'useNormalizeHelper',
|
|
132
|
+
data: reportData,
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
},
|
|
137
|
+
};
|
|
138
|
+
},
|
|
139
|
+
};
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ESLint rule to disallow function declarations inside test blocks
|
|
3
|
+
*
|
|
4
|
+
* Catches the SonarQube code smell: "Move function to the outer scope" (S1515)
|
|
5
|
+
* Functions defined inside describe/it/test blocks should be moved to module scope
|
|
6
|
+
* for better reusability and to prevent the code smell.
|
|
7
|
+
*
|
|
8
|
+
* Why: Helper functions inside test blocks are harder to:
|
|
9
|
+
* - Reuse across test files
|
|
10
|
+
* - Test independently
|
|
11
|
+
* - Understand (hidden inside blocks)
|
|
12
|
+
* - Maintain (scattered throughout test suites)
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* // BAD - Function inside describe block
|
|
16
|
+
* describe('validate command', () => {
|
|
17
|
+
* function setupWorkingDirectoryMocks(configDir, treeHash) {
|
|
18
|
+
* // ... setup code
|
|
19
|
+
* }
|
|
20
|
+
*
|
|
21
|
+
* it('should work', () => {
|
|
22
|
+
* setupWorkingDirectoryMocks('/path', 'abc123');
|
|
23
|
+
* });
|
|
24
|
+
* });
|
|
25
|
+
*
|
|
26
|
+
* // GOOD - Function at module scope
|
|
27
|
+
* function setupWorkingDirectoryMocks(configDir, treeHash) {
|
|
28
|
+
* // ... setup code
|
|
29
|
+
* }
|
|
30
|
+
*
|
|
31
|
+
* describe('validate command', () => {
|
|
32
|
+
* it('should work', () => {
|
|
33
|
+
* setupWorkingDirectoryMocks('/path', 'abc123');
|
|
34
|
+
* });
|
|
35
|
+
* });
|
|
36
|
+
*/
|
|
37
|
+
|
|
38
|
+
const TEST_BLOCK_PATTERN = /^(describe|it|test|before|after|beforeEach|afterEach|beforeAll|afterAll)$/i;
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Check if a node is a test-related call expression
|
|
42
|
+
* (describe, it, test, beforeEach, afterEach, beforeAll, afterAll, etc.)
|
|
43
|
+
*/
|
|
44
|
+
function isTestBlock(node) {
|
|
45
|
+
if (node.type !== 'CallExpression') {
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const callee = node.callee;
|
|
50
|
+
|
|
51
|
+
// Direct calls: describe(), it(), test(), etc.
|
|
52
|
+
if (callee.type === 'Identifier') {
|
|
53
|
+
return TEST_BLOCK_PATTERN.test(callee.name);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Member calls: test.describe(), test.it(), etc. (Playwright)
|
|
57
|
+
if (
|
|
58
|
+
callee.type === 'MemberExpression' &&
|
|
59
|
+
callee.property.type === 'Identifier'
|
|
60
|
+
) {
|
|
61
|
+
return TEST_BLOCK_PATTERN.test(callee.property.name);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
module.exports = {
|
|
68
|
+
meta: {
|
|
69
|
+
type: 'suggestion',
|
|
70
|
+
docs: {
|
|
71
|
+
description: 'Disallow function declarations inside test blocks (SonarQube S1515)',
|
|
72
|
+
category: 'Best Practices',
|
|
73
|
+
recommended: true,
|
|
74
|
+
},
|
|
75
|
+
messages: {
|
|
76
|
+
moveToModuleScope:
|
|
77
|
+
'Move function \'{{name}}\' to module scope (outside describe/it/test blocks). ' +
|
|
78
|
+
'Helper functions inside test blocks are harder to reuse and maintain. ' +
|
|
79
|
+
'This matches SonarQube rule S1515 (Intentionality/Maintainability).',
|
|
80
|
+
},
|
|
81
|
+
schema: [
|
|
82
|
+
{
|
|
83
|
+
type: 'object',
|
|
84
|
+
properties: {
|
|
85
|
+
allowedFunctionNames: {
|
|
86
|
+
type: 'array',
|
|
87
|
+
items: { type: 'string' },
|
|
88
|
+
description: 'Function names that are allowed inside test blocks',
|
|
89
|
+
},
|
|
90
|
+
},
|
|
91
|
+
additionalProperties: false,
|
|
92
|
+
},
|
|
93
|
+
],
|
|
94
|
+
},
|
|
95
|
+
|
|
96
|
+
create(context) {
|
|
97
|
+
let testBlockDepth = 0;
|
|
98
|
+
const allowedNames = new Set(
|
|
99
|
+
context.options[0]?.allowedFunctionNames || []
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
return {
|
|
103
|
+
CallExpression(node) {
|
|
104
|
+
if (isTestBlock(node)) {
|
|
105
|
+
testBlockDepth++;
|
|
106
|
+
}
|
|
107
|
+
},
|
|
108
|
+
|
|
109
|
+
'CallExpression:exit'(node) {
|
|
110
|
+
if (isTestBlock(node)) {
|
|
111
|
+
testBlockDepth--;
|
|
112
|
+
}
|
|
113
|
+
},
|
|
114
|
+
|
|
115
|
+
FunctionDeclaration(node) {
|
|
116
|
+
if (testBlockDepth > 0) {
|
|
117
|
+
const functionName = node.id?.name || '<anonymous>';
|
|
118
|
+
|
|
119
|
+
if (allowedNames.has(functionName)) {
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
context.report({
|
|
124
|
+
node,
|
|
125
|
+
messageId: 'moveToModuleScope',
|
|
126
|
+
data: {
|
|
127
|
+
name: functionName,
|
|
128
|
+
},
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
},
|
|
132
|
+
};
|
|
133
|
+
},
|
|
134
|
+
};
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ESLint rule: no-unix-shell-commands
|
|
3
|
+
*
|
|
4
|
+
* Prevents usage of Unix-specific commands in exec/spawn calls that break cross-platform compatibility.
|
|
5
|
+
*
|
|
6
|
+
* Why: Commands like 'tar', 'ls', 'touch', 'grep' are Unix-specific and fail on Windows.
|
|
7
|
+
* Use cross-platform alternatives: Node.js fs APIs, or cross-platform npm packages.
|
|
8
|
+
*
|
|
9
|
+
* Detects Unix commands in:
|
|
10
|
+
* - safeExecSync('tar', ...)
|
|
11
|
+
* - safeExecResult('ls', ...)
|
|
12
|
+
* - spawn('grep', ...)
|
|
13
|
+
* - spawnSync('find', ...)
|
|
14
|
+
* - execSync('tar xzf file.tar.gz')
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
const { isTestFile } = require('./exempt-path-matcher.cjs');
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Unix-specific commands that should not be used
|
|
21
|
+
* Categorized for better documentation
|
|
22
|
+
*/
|
|
23
|
+
const UNIX_COMMANDS = {
|
|
24
|
+
// File operations
|
|
25
|
+
fileOps: ['ls', 'touch', 'rm', 'mv', 'cp', 'ln', 'chmod', 'chown', 'chgrp'],
|
|
26
|
+
|
|
27
|
+
// Archiving/compression
|
|
28
|
+
archive: ['tar', 'gzip', 'gunzip', 'zip', 'unzip', 'bzip2'],
|
|
29
|
+
|
|
30
|
+
// Text processing
|
|
31
|
+
// Note on `echo`: on Windows `echo` is a shell builtin (cmd.exe / PowerShell),
|
|
32
|
+
// not an .EXE on PATH — except inside Git Bash's MSYS, where `/usr/bin/echo.exe`
|
|
33
|
+
// ships from coreutils. So `safeExecSync('echo', …)` works only when the
|
|
34
|
+
// launching shell happens to have `C:\Program Files\Git\usr\bin` on PATH
|
|
35
|
+
// (e.g., Git Bash, or GitHub-hosted windows-latest runners that add it).
|
|
36
|
+
// Use Node (`safeExecSync('node', [echoFixturePath, …])`) instead, pointing at
|
|
37
|
+
// a committed one-line script fixture that prints its argv.
|
|
38
|
+
text: ['grep', 'sed', 'awk', 'cat', 'head', 'tail', 'wc', 'cut', 'sort', 'uniq', 'tr', 'echo'],
|
|
39
|
+
|
|
40
|
+
// File searching
|
|
41
|
+
search: ['find', 'locate'],
|
|
42
|
+
|
|
43
|
+
// System info
|
|
44
|
+
system: ['ps', 'top', 'kill', 'df', 'du', 'who', 'uname'],
|
|
45
|
+
|
|
46
|
+
// Networking
|
|
47
|
+
network: ['curl', 'wget', 'ping', 'netstat', 'ifconfig'],
|
|
48
|
+
|
|
49
|
+
// Shell utilities
|
|
50
|
+
shell: ['sh', 'bash', 'zsh', 'source', 'export', 'env'],
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
// Flatten to single array for easier checking
|
|
54
|
+
const ALL_UNIX_COMMANDS = Object.values(UNIX_COMMANDS).flat();
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Get cross-platform alternatives for common Unix commands
|
|
58
|
+
*/
|
|
59
|
+
function getAlternatives(command) {
|
|
60
|
+
const alternatives = {
|
|
61
|
+
ls: 'fs.readdirSync() or Glob tool',
|
|
62
|
+
touch: 'fs.writeFileSync(path, "") or fs.utimesSync()',
|
|
63
|
+
rm: 'fs.unlinkSync() or fs.rmSync()',
|
|
64
|
+
mv: 'fs.renameSync()',
|
|
65
|
+
cp: 'fs.copyFileSync()',
|
|
66
|
+
cat: 'fs.readFileSync() or Read tool',
|
|
67
|
+
grep: 'Grep tool or string.includes()',
|
|
68
|
+
find: 'Glob tool or fs.readdirSync() with recursion',
|
|
69
|
+
tar: 'tar-fs or tar-stream npm package',
|
|
70
|
+
gzip: 'zlib module',
|
|
71
|
+
chmod: 'fs.chmodSync()',
|
|
72
|
+
chown: 'fs.chownSync()',
|
|
73
|
+
head: 'Read tool with limit parameter',
|
|
74
|
+
tail: 'Read tool with offset parameter',
|
|
75
|
+
sed: 'Edit tool or string.replace()',
|
|
76
|
+
awk: 'string.split() and array methods',
|
|
77
|
+
echo: 'safeExecSync(\'node\', [echoFixturePath, ...args]) using a committed script fixture that prints its argv — `echo` is only present on Windows when Git\'s coreutils are on PATH',
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
return alternatives[command] || 'Node.js fs module or cross-platform npm package';
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
module.exports = {
|
|
85
|
+
meta: {
|
|
86
|
+
type: 'problem',
|
|
87
|
+
docs: {
|
|
88
|
+
description: 'Prevent Unix-specific commands that break Windows compatibility',
|
|
89
|
+
category: 'Cross-Platform',
|
|
90
|
+
recommended: true,
|
|
91
|
+
},
|
|
92
|
+
fixable: null, // No auto-fix - requires manual refactoring
|
|
93
|
+
schema: [],
|
|
94
|
+
messages: {
|
|
95
|
+
noUnixCommand:
|
|
96
|
+
'Unix-specific command "{{command}}" breaks Windows compatibility. Use: {{alternative}}',
|
|
97
|
+
},
|
|
98
|
+
},
|
|
99
|
+
|
|
100
|
+
create(context) {
|
|
101
|
+
// Exempt test files that specifically test Unix command detection.
|
|
102
|
+
// Anchored on the basename by `exempt-path-matcher.cjs` — a substring check
|
|
103
|
+
// also exempted `example.test.ts.bak`, a `.test.ts-helpers/` directory, and
|
|
104
|
+
// `tsconfig.test.json` (via the `.test.js` spelling).
|
|
105
|
+
if (isTestFile(context.getFilename())) {
|
|
106
|
+
return {};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const factory = require('./no-command-direct-factory.cjs');
|
|
110
|
+
const { createCommandChecker } = factory;
|
|
111
|
+
|
|
112
|
+
return {
|
|
113
|
+
CallExpression: createCommandChecker({
|
|
114
|
+
context,
|
|
115
|
+
shouldReport: (node, firstArg, matchesExecSync) => {
|
|
116
|
+
// For shell-free execution (safeExecSync, spawn, etc.)
|
|
117
|
+
if (!matchesExecSync) {
|
|
118
|
+
if (typeof firstArg.value === 'string' && ALL_UNIX_COMMANDS.includes(firstArg.value)) {
|
|
119
|
+
return {
|
|
120
|
+
command: firstArg.value,
|
|
121
|
+
alternative: getAlternatives(firstArg.value),
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
return undefined;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// For execSync (shell-based) - check if any Unix command is in the string
|
|
128
|
+
for (const unixCmd of ALL_UNIX_COMMANDS) {
|
|
129
|
+
if (matchesExecSync(firstArg, unixCmd)) {
|
|
130
|
+
// Extract command name for better error message
|
|
131
|
+
let command = unixCmd;
|
|
132
|
+
if (firstArg.type === 'Literal') {
|
|
133
|
+
command = firstArg.value.trim().split(/\s+/)[0];
|
|
134
|
+
} else if (firstArg.type === 'TemplateLiteral') {
|
|
135
|
+
const firstQuasi = firstArg.quasis[0].value.cooked || firstArg.quasis[0].value.raw;
|
|
136
|
+
command = firstQuasi.trim().split(/\s+/)[0];
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return {
|
|
140
|
+
command,
|
|
141
|
+
alternative: getAlternatives(command),
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return undefined;
|
|
147
|
+
},
|
|
148
|
+
messageId: 'noUnixCommand',
|
|
149
|
+
}),
|
|
150
|
+
};
|
|
151
|
+
},
|
|
152
|
+
};
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ESLint rule: no-unsafe-root-join
|
|
3
|
+
*
|
|
4
|
+
* Flags `safePath.join(<root>, …)` and `safePath.resolve(<root>, …)` where
|
|
5
|
+
* the first argument is an identifier whose name ends in "root" (case-insensitive,
|
|
6
|
+
* e.g. harnessRoot, stagedRoot, pluginRoot). These are security-root path joins
|
|
7
|
+
* that should use `safePath.joinUnderRoot()` to prevent caller-controlled segments
|
|
8
|
+
* from escaping the root on Windows via drive-letter or absolute path injection.
|
|
9
|
+
*
|
|
10
|
+
* Rule is intentionally narrow: only *Root-named first arguments are flagged to
|
|
11
|
+
* avoid false positives on ordinary joins.
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* // ❌ BAD — silent escape on Windows if item.name is 'C:\evil'
|
|
15
|
+
* const dest = safePath.join(harnessRoot, stagedDirName(item.name));
|
|
16
|
+
*
|
|
17
|
+
* // ✅ GOOD — throws on Windows drive-letter or absolute segment
|
|
18
|
+
* const dest = safePath.joinUnderRoot(harnessRoot, stagedDirName(item.name));
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
'use strict';
|
|
22
|
+
|
|
23
|
+
/** Return true when the identifier name ends with 'root' (case-insensitive). */
|
|
24
|
+
function isRootIdentifier(name) {
|
|
25
|
+
return name.toLowerCase().endsWith('root');
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Return true when the first argument of a CallExpression is an Identifier
|
|
30
|
+
* whose name ends in 'root' (case-insensitive).
|
|
31
|
+
*/
|
|
32
|
+
function firstArgIsRootIdentifier(node) {
|
|
33
|
+
if (node.arguments.length === 0) return false;
|
|
34
|
+
const first = node.arguments[0];
|
|
35
|
+
return first.type === 'Identifier' && isRootIdentifier(first.name);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
module.exports = {
|
|
39
|
+
meta: {
|
|
40
|
+
type: 'problem',
|
|
41
|
+
docs: {
|
|
42
|
+
description:
|
|
43
|
+
'Enforce safePath.joinUnderRoot() for joins whose first arg is a security root variable (name ends in "Root").',
|
|
44
|
+
category: 'Security',
|
|
45
|
+
recommended: true,
|
|
46
|
+
},
|
|
47
|
+
messages: {
|
|
48
|
+
useJoinUnderRoot:
|
|
49
|
+
'Use safePath.joinUnderRoot({{root}}, …) instead of safePath.{{method}}({{root}}, …) ' +
|
|
50
|
+
'when the first argument is a security root. ' +
|
|
51
|
+
'safePath.{{method}}() does not prevent caller-controlled segments from escaping the root on Windows.',
|
|
52
|
+
},
|
|
53
|
+
schema: [],
|
|
54
|
+
},
|
|
55
|
+
|
|
56
|
+
create(context) {
|
|
57
|
+
return {
|
|
58
|
+
CallExpression(node) {
|
|
59
|
+
// Must be a MemberExpression: safePath.<method>(...)
|
|
60
|
+
if (node.callee.type !== 'MemberExpression') return;
|
|
61
|
+
|
|
62
|
+
const obj = node.callee.object;
|
|
63
|
+
const prop = node.callee.property;
|
|
64
|
+
|
|
65
|
+
// Object must be the identifier 'safePath'
|
|
66
|
+
if (obj.type !== 'Identifier' || obj.name !== 'safePath') return;
|
|
67
|
+
|
|
68
|
+
// Method must be 'join' or 'resolve'
|
|
69
|
+
if (prop.type !== 'Identifier') return;
|
|
70
|
+
const method = prop.name;
|
|
71
|
+
if (method !== 'join' && method !== 'resolve') return;
|
|
72
|
+
|
|
73
|
+
// First argument must be an identifier ending in 'root'
|
|
74
|
+
if (!firstArgIsRootIdentifier(node)) return;
|
|
75
|
+
|
|
76
|
+
const rootName = node.arguments[0].name;
|
|
77
|
+
context.report({
|
|
78
|
+
node,
|
|
79
|
+
messageId: 'useJoinUnderRoot',
|
|
80
|
+
data: { root: rootName, method },
|
|
81
|
+
});
|
|
82
|
+
},
|
|
83
|
+
};
|
|
84
|
+
},
|
|
85
|
+
};
|