eslint-plugin-security 1.7.0 → 1.7.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/CHANGELOG.md +7 -0
- package/package.json +1 -1
- package/rules/detect-child-process.js +8 -1
- package/rules/detect-non-literal-fs-filename.js +18 -24
- package/rules/detect-non-literal-regexp.js +10 -1
- package/rules/detect-non-literal-require.js +8 -2
- package/test/detect-child-process.js +8 -0
- package/test/detect-non-literal-fs-filename.js +56 -1
- package/test/detect-non-literal-regexp.js +8 -1
- package/test/detect-non-literal-require.js +15 -1
- package/test/utils/is-static-expression.js +252 -0
- package/utils/find-variable.js +18 -0
- package/utils/import-utils.js +2 -12
- package/utils/is-static-expression.js +219 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
### [1.7.1](https://www.github.com/eslint-community/eslint-plugin-security/compare/v1.7.0...v1.7.1) (2023-02-02)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Bug Fixes
|
|
7
|
+
|
|
8
|
+
* false positives for static expressions in detect-non-literal-fs-filename, detect-child-process, detect-non-literal-regexp, and detect-non-literal-require ([#109](https://www.github.com/eslint-community/eslint-plugin-security/issues/109)) ([56102b5](https://www.github.com/eslint-community/eslint-plugin-security/commit/56102b50aed4bd632dd668770eb37de58788110b))
|
|
9
|
+
|
|
3
10
|
## [1.7.0](https://www.github.com/eslint-community/eslint-plugin-security/compare/v1.6.0...v1.7.0) (2023-01-26)
|
|
4
11
|
|
|
5
12
|
|
package/package.json
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
'use strict';
|
|
7
7
|
|
|
8
8
|
const { getImportAccessPath } = require('../utils/import-utils');
|
|
9
|
+
const { isStaticExpression } = require('../utils/is-static-expression');
|
|
9
10
|
const childProcessPackageNames = ['child_process', 'node:child_process'];
|
|
10
11
|
|
|
11
12
|
//------------------------------------------------------------------------------
|
|
@@ -41,7 +42,13 @@ module.exports = {
|
|
|
41
42
|
}
|
|
42
43
|
|
|
43
44
|
// Reports non-literal `exec()` calls.
|
|
44
|
-
if (
|
|
45
|
+
if (
|
|
46
|
+
!node.arguments.length ||
|
|
47
|
+
isStaticExpression({
|
|
48
|
+
node: node.arguments[0],
|
|
49
|
+
scope: context.getScope(),
|
|
50
|
+
})
|
|
51
|
+
) {
|
|
45
52
|
return;
|
|
46
53
|
}
|
|
47
54
|
const pathInfo = getImportAccessPath({
|
|
@@ -10,21 +10,7 @@ const funcNames = Object.keys(fsMetaData);
|
|
|
10
10
|
const fsPackageNames = ['fs', 'node:fs', 'fs/promises', 'node:fs/promises', 'fs-extra'];
|
|
11
11
|
|
|
12
12
|
const { getImportAccessPath } = require('../utils/import-utils');
|
|
13
|
-
|
|
14
|
-
//------------------------------------------------------------------------------
|
|
15
|
-
// Utils
|
|
16
|
-
//------------------------------------------------------------------------------
|
|
17
|
-
|
|
18
|
-
function getIndices(node, argMeta) {
|
|
19
|
-
return (argMeta || []).filter((argIndex) => node.arguments[argIndex].type !== 'Literal');
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
function generateReport({ context, node, packageName, methodName, indices }) {
|
|
23
|
-
if (!indices || indices.length === 0) {
|
|
24
|
-
return;
|
|
25
|
-
}
|
|
26
|
-
context.report({ node, message: `Found ${methodName} from package "${packageName}" with non literal argument at index ${indices.join(',')}` });
|
|
27
|
-
}
|
|
13
|
+
const { isStaticExpression } = require('../utils/is-static-expression');
|
|
28
14
|
|
|
29
15
|
//------------------------------------------------------------------------------
|
|
30
16
|
// Rule Definition
|
|
@@ -87,15 +73,23 @@ module.exports = {
|
|
|
87
73
|
}
|
|
88
74
|
const packageName = pathInfo.packageName;
|
|
89
75
|
|
|
90
|
-
const indices =
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
76
|
+
const indices = [];
|
|
77
|
+
for (const index of fsMetaData[fnName] || []) {
|
|
78
|
+
if (index >= node.arguments.length) {
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
const argument = node.arguments[index];
|
|
82
|
+
if (isStaticExpression({ node: argument, scope: context.getScope() })) {
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
indices.push(index);
|
|
86
|
+
}
|
|
87
|
+
if (indices.length) {
|
|
88
|
+
context.report({
|
|
89
|
+
node,
|
|
90
|
+
message: `Found ${fnName} from package "${packageName}" with non literal argument at index ${indices.join(',')}`,
|
|
91
|
+
});
|
|
92
|
+
}
|
|
99
93
|
},
|
|
100
94
|
};
|
|
101
95
|
},
|
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
|
|
6
6
|
'use strict';
|
|
7
7
|
|
|
8
|
+
const { isStaticExpression } = require('../utils/is-static-expression');
|
|
9
|
+
|
|
8
10
|
//------------------------------------------------------------------------------
|
|
9
11
|
// Rule Definition
|
|
10
12
|
//------------------------------------------------------------------------------
|
|
@@ -24,7 +26,14 @@ module.exports = {
|
|
|
24
26
|
NewExpression: function (node) {
|
|
25
27
|
if (node.callee.name === 'RegExp') {
|
|
26
28
|
const args = node.arguments;
|
|
27
|
-
if (
|
|
29
|
+
if (
|
|
30
|
+
args &&
|
|
31
|
+
args.length > 0 &&
|
|
32
|
+
!isStaticExpression({
|
|
33
|
+
node: args[0],
|
|
34
|
+
scope: context.getScope(),
|
|
35
|
+
})
|
|
36
|
+
) {
|
|
28
37
|
return context.report({ node: node, message: 'Found non-literal argument to RegExp Constructor' });
|
|
29
38
|
}
|
|
30
39
|
}
|
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
|
|
6
6
|
'use strict';
|
|
7
7
|
|
|
8
|
+
const { isStaticExpression } = require('../utils/is-static-expression');
|
|
9
|
+
|
|
8
10
|
//------------------------------------------------------------------------------
|
|
9
11
|
// Rule Definition
|
|
10
12
|
//------------------------------------------------------------------------------
|
|
@@ -25,8 +27,12 @@ module.exports = {
|
|
|
25
27
|
if (node.callee.name === 'require') {
|
|
26
28
|
const args = node.arguments;
|
|
27
29
|
if (
|
|
28
|
-
|
|
29
|
-
|
|
30
|
+
args &&
|
|
31
|
+
args.length > 0 &&
|
|
32
|
+
!isStaticExpression({
|
|
33
|
+
node: args[0],
|
|
34
|
+
scope: context.getScope(),
|
|
35
|
+
})
|
|
30
36
|
) {
|
|
31
37
|
return context.report({ node: node, message: 'Found non-literal argument in require' });
|
|
32
38
|
}
|
|
@@ -50,6 +50,14 @@ tester.run(ruleName, rule, {
|
|
|
50
50
|
function fn () {
|
|
51
51
|
require('child_process').spawn(str)
|
|
52
52
|
}`,
|
|
53
|
+
`
|
|
54
|
+
var child_process = require('child_process');
|
|
55
|
+
var FOO = 'ls';
|
|
56
|
+
child_process.exec(FOO);`,
|
|
57
|
+
`
|
|
58
|
+
import child_process from 'child_process';
|
|
59
|
+
const FOO = 'ls';
|
|
60
|
+
child_process.exec(FOO);`,
|
|
53
61
|
],
|
|
54
62
|
invalid: [
|
|
55
63
|
{
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
const RuleTester = require('eslint').RuleTester;
|
|
4
4
|
const tester = new RuleTester({
|
|
5
5
|
parserOptions: {
|
|
6
|
-
ecmaVersion:
|
|
6
|
+
ecmaVersion: 13,
|
|
7
7
|
sourceType: 'module',
|
|
8
8
|
},
|
|
9
9
|
});
|
|
@@ -24,6 +24,51 @@ tester.run(ruleName, require(`../rules/${ruleName}`), {
|
|
|
24
24
|
code: `var something = require('fs').readFile, readFile = require('foo').readFile;
|
|
25
25
|
readFile(c);`,
|
|
26
26
|
},
|
|
27
|
+
{
|
|
28
|
+
code: `
|
|
29
|
+
import { promises as fsp } from 'fs';
|
|
30
|
+
import fs from 'fs';
|
|
31
|
+
import path from 'path';
|
|
32
|
+
|
|
33
|
+
const index = await fsp.readFile(path.resolve(__dirname, './index.html'), 'utf-8');
|
|
34
|
+
const key = fs.readFileSync(path.join(__dirname, './ssl.key'));
|
|
35
|
+
await fsp.writeFile(path.resolve(__dirname, './sitemap.xml'), sitemap);`,
|
|
36
|
+
globals: {
|
|
37
|
+
__dirname: 'readonly',
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
code: `
|
|
42
|
+
import fs from 'fs';
|
|
43
|
+
import path from 'path';
|
|
44
|
+
const dirname = path.dirname(__filename)
|
|
45
|
+
const key = fs.readFileSync(path.resolve(dirname, './index.html'));`,
|
|
46
|
+
globals: {
|
|
47
|
+
__filename: 'readonly',
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
code: `
|
|
52
|
+
import fs from 'fs';
|
|
53
|
+
const key = fs.readFileSync(\`\${process.cwd()}/path/to/foo.json\`);`,
|
|
54
|
+
globals: {
|
|
55
|
+
process: 'readonly',
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
`
|
|
59
|
+
import fs from 'fs';
|
|
60
|
+
import path from 'path';
|
|
61
|
+
import url from 'url';
|
|
62
|
+
const dirname = path.dirname(url.fileURLToPath(import.meta.url));
|
|
63
|
+
const html = fs.readFileSync(path.resolve(dirname, './index.html'), 'utf-8');`,
|
|
64
|
+
{
|
|
65
|
+
code: `
|
|
66
|
+
import fs from 'fs';
|
|
67
|
+
const pkg = fs.readFileSync(require.resolve('eslint/package.json'), 'utf-8');`,
|
|
68
|
+
globals: {
|
|
69
|
+
require: 'readonly',
|
|
70
|
+
},
|
|
71
|
+
},
|
|
27
72
|
],
|
|
28
73
|
invalid: [
|
|
29
74
|
/// requires
|
|
@@ -141,5 +186,15 @@ tester.run(ruleName, require(`../rules/${ruleName}`), {
|
|
|
141
186
|
code: "var fs = require('fs');\nfunction foo () {\nvar { readFile: something } = fs.promises;\nsomething(filename);\n}",
|
|
142
187
|
errors: [{ message: 'Found readFile from package "fs" with non literal argument at index 0' }],
|
|
143
188
|
},
|
|
189
|
+
{
|
|
190
|
+
code: `
|
|
191
|
+
import fs from 'fs';
|
|
192
|
+
import path from 'path';
|
|
193
|
+
const key = fs.readFileSync(path.resolve(__dirname, foo));`,
|
|
194
|
+
globals: {
|
|
195
|
+
__filename: 'readonly',
|
|
196
|
+
},
|
|
197
|
+
errors: [{ message: 'Found readFileSync from package "fs" with non literal argument at index 0' }],
|
|
198
|
+
},
|
|
144
199
|
],
|
|
145
200
|
});
|
|
@@ -7,7 +7,14 @@ const ruleName = 'detect-non-literal-regexp';
|
|
|
7
7
|
const invalid = "var a = new RegExp(c, 'i')";
|
|
8
8
|
|
|
9
9
|
tester.run(ruleName, require(`../rules/${ruleName}`), {
|
|
10
|
-
valid: [
|
|
10
|
+
valid: [
|
|
11
|
+
{ code: "var a = new RegExp('ab+c', 'i')" },
|
|
12
|
+
{
|
|
13
|
+
code: `
|
|
14
|
+
var source = 'ab+c'
|
|
15
|
+
var a = new RegExp(source, 'i')`,
|
|
16
|
+
},
|
|
17
|
+
],
|
|
11
18
|
invalid: [
|
|
12
19
|
{
|
|
13
20
|
code: invalid,
|
|
@@ -7,7 +7,21 @@ const tester = new RuleTester({ parserOptions: { ecmaVersion: 6 } });
|
|
|
7
7
|
const ruleName = 'detect-non-literal-require';
|
|
8
8
|
|
|
9
9
|
tester.run(ruleName, require(`../rules/${ruleName}`), {
|
|
10
|
-
valid: [
|
|
10
|
+
valid: [
|
|
11
|
+
{ code: "var a = require('b')" },
|
|
12
|
+
{ code: 'var a = require(`b`)' },
|
|
13
|
+
{
|
|
14
|
+
code: `
|
|
15
|
+
const d = 'debounce'
|
|
16
|
+
var a = require(\`lodash/\${d}\`)`,
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
code: "const utils = require(__dirname + '/utils');",
|
|
20
|
+
globals: {
|
|
21
|
+
__dirname: 'readonly',
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
],
|
|
11
25
|
invalid: [
|
|
12
26
|
{
|
|
13
27
|
code: 'var a = require(c)',
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { isStaticExpression } = require('../../utils/is-static-expression');
|
|
4
|
+
const { deepStrictEqual } = require('assert');
|
|
5
|
+
|
|
6
|
+
const Linter = require('eslint').Linter;
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Get the return value using `isStaticExpression()`.
|
|
10
|
+
* Give `isStaticExpression()` the argument given to `target()` in the code as an expression.
|
|
11
|
+
*/
|
|
12
|
+
function getIsStaticExpressionResult(code) {
|
|
13
|
+
const linter = new Linter();
|
|
14
|
+
const result = [];
|
|
15
|
+
linter.defineRule('test-rule', {
|
|
16
|
+
create(context) {
|
|
17
|
+
return {
|
|
18
|
+
'CallExpression[callee.name = target]'(node) {
|
|
19
|
+
result.push(
|
|
20
|
+
...node.arguments.map((expr) =>
|
|
21
|
+
isStaticExpression({
|
|
22
|
+
node: expr,
|
|
23
|
+
scope: context.getScope(),
|
|
24
|
+
})
|
|
25
|
+
)
|
|
26
|
+
);
|
|
27
|
+
},
|
|
28
|
+
};
|
|
29
|
+
},
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
const linterResult = linter.verify(code, {
|
|
33
|
+
parserOptions: {
|
|
34
|
+
ecmaVersion: 11,
|
|
35
|
+
sourceType: 'module',
|
|
36
|
+
},
|
|
37
|
+
globals: {
|
|
38
|
+
__dirname: 'readonly',
|
|
39
|
+
__filename: 'readonly',
|
|
40
|
+
require: 'readonly',
|
|
41
|
+
},
|
|
42
|
+
rules: {
|
|
43
|
+
'test-rule': 'error',
|
|
44
|
+
},
|
|
45
|
+
});
|
|
46
|
+
deepStrictEqual(linterResult, []);
|
|
47
|
+
|
|
48
|
+
return result;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
describe('isStaticExpression', () => {
|
|
52
|
+
describe('The result of isStaticExpression should be as expected.', () => {
|
|
53
|
+
for (const { code, result } of [
|
|
54
|
+
{
|
|
55
|
+
code: `target('foo');`,
|
|
56
|
+
result: [true],
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
code: `target(a);`,
|
|
60
|
+
result: [false],
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
code: `
|
|
64
|
+
const a = 'i'
|
|
65
|
+
target(a);`,
|
|
66
|
+
result: [true],
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
code: `
|
|
70
|
+
const a = b
|
|
71
|
+
target(a);`,
|
|
72
|
+
result: [false],
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
code: `
|
|
76
|
+
const a = a
|
|
77
|
+
target(a);`,
|
|
78
|
+
result: [false],
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
code: `
|
|
82
|
+
var a = 'foo'
|
|
83
|
+
var a = 'bar'
|
|
84
|
+
target(a);`,
|
|
85
|
+
result: [false],
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
code: `
|
|
89
|
+
var a = 'foo'
|
|
90
|
+
a = 'bar'
|
|
91
|
+
var b = 'bar'
|
|
92
|
+
target(a);
|
|
93
|
+
target(b);`,
|
|
94
|
+
result: [false, true],
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
code: `target(\`foo\`);`,
|
|
98
|
+
result: [true],
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
code: `
|
|
102
|
+
target(\`foo\${a}\`);`,
|
|
103
|
+
result: [false],
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
code: `
|
|
107
|
+
const a = 'i'
|
|
108
|
+
target(\`foo\${a}\`);`,
|
|
109
|
+
result: [true],
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
code: `
|
|
113
|
+
const a = 'i'
|
|
114
|
+
target('foo' + 'bar');
|
|
115
|
+
target(a + 'foo');
|
|
116
|
+
target('foo' + a + 'bar');
|
|
117
|
+
`,
|
|
118
|
+
result: [true, true, true],
|
|
119
|
+
},
|
|
120
|
+
{
|
|
121
|
+
code: `
|
|
122
|
+
const a = 'i'
|
|
123
|
+
target(b + 'bar');
|
|
124
|
+
target('foo' + a + b);
|
|
125
|
+
`,
|
|
126
|
+
result: [false, false],
|
|
127
|
+
},
|
|
128
|
+
{
|
|
129
|
+
code: `
|
|
130
|
+
target(__dirname, __filename);
|
|
131
|
+
`,
|
|
132
|
+
result: [true, true],
|
|
133
|
+
},
|
|
134
|
+
{
|
|
135
|
+
code: `
|
|
136
|
+
function fn(__dirname) {
|
|
137
|
+
target(__dirname, __filename);
|
|
138
|
+
}
|
|
139
|
+
`,
|
|
140
|
+
result: [false, true],
|
|
141
|
+
},
|
|
142
|
+
{
|
|
143
|
+
code: `
|
|
144
|
+
const __filename = a
|
|
145
|
+
target(__dirname, __filename);
|
|
146
|
+
`,
|
|
147
|
+
result: [true, false],
|
|
148
|
+
},
|
|
149
|
+
{
|
|
150
|
+
code: `
|
|
151
|
+
import path from 'path';
|
|
152
|
+
target(path.resolve(__dirname, './index.html'));
|
|
153
|
+
target(path.join(__dirname, './ssl.key'));
|
|
154
|
+
target(path.resolve(__dirname, './sitemap.xml'));
|
|
155
|
+
`,
|
|
156
|
+
result: [true, true, true],
|
|
157
|
+
},
|
|
158
|
+
{
|
|
159
|
+
code: `
|
|
160
|
+
import { posix as path } from 'path';
|
|
161
|
+
target(path.resolve(__dirname, './index.html'));
|
|
162
|
+
`,
|
|
163
|
+
result: [true],
|
|
164
|
+
},
|
|
165
|
+
{
|
|
166
|
+
code: `
|
|
167
|
+
const path = require('path');
|
|
168
|
+
target(path.resolve(__dirname, './index.html'));
|
|
169
|
+
`,
|
|
170
|
+
result: [true],
|
|
171
|
+
},
|
|
172
|
+
{
|
|
173
|
+
code: `
|
|
174
|
+
import path from 'unknown';
|
|
175
|
+
target(path.resolve(__dirname, './index.html'));
|
|
176
|
+
`,
|
|
177
|
+
result: [false],
|
|
178
|
+
},
|
|
179
|
+
{
|
|
180
|
+
code: `
|
|
181
|
+
import path from 'path';
|
|
182
|
+
target(path.unknown(__dirname, './index.html'));
|
|
183
|
+
`,
|
|
184
|
+
result: [false],
|
|
185
|
+
},
|
|
186
|
+
{
|
|
187
|
+
code: `
|
|
188
|
+
import path from 'path';
|
|
189
|
+
target(path.resolve.unknown(__dirname, './index.html'));
|
|
190
|
+
`,
|
|
191
|
+
result: [false],
|
|
192
|
+
},
|
|
193
|
+
{
|
|
194
|
+
code: `
|
|
195
|
+
import path from 'path';
|
|
196
|
+
const FOO = 'static'
|
|
197
|
+
target(path.resolve(__dirname, foo));
|
|
198
|
+
target(path.resolve(__dirname, FOO));
|
|
199
|
+
`,
|
|
200
|
+
result: [false, true],
|
|
201
|
+
},
|
|
202
|
+
{
|
|
203
|
+
code: `
|
|
204
|
+
import path from 'path';
|
|
205
|
+
const FOO = 'static'
|
|
206
|
+
target(__dirname + path.sep + foo);
|
|
207
|
+
target(__dirname + path.sep + FOO);
|
|
208
|
+
`,
|
|
209
|
+
result: [false, true],
|
|
210
|
+
},
|
|
211
|
+
{
|
|
212
|
+
code: `
|
|
213
|
+
target(require.resolve('static'));
|
|
214
|
+
target(require.resolve(foo));
|
|
215
|
+
`,
|
|
216
|
+
result: [true, false],
|
|
217
|
+
},
|
|
218
|
+
{
|
|
219
|
+
code: `
|
|
220
|
+
target(require);
|
|
221
|
+
target(require('static'));
|
|
222
|
+
`,
|
|
223
|
+
result: [false, false],
|
|
224
|
+
},
|
|
225
|
+
{
|
|
226
|
+
code: `
|
|
227
|
+
import url from "node:url";
|
|
228
|
+
import path from "node:path";
|
|
229
|
+
|
|
230
|
+
const filename = url.fileURLToPath(import.meta.url);
|
|
231
|
+
const dirname = path.dirname(url.fileURLToPath(import.meta.url));
|
|
232
|
+
|
|
233
|
+
target(filename);
|
|
234
|
+
target(dirname);
|
|
235
|
+
`,
|
|
236
|
+
result: [true, true],
|
|
237
|
+
},
|
|
238
|
+
{
|
|
239
|
+
code: `
|
|
240
|
+
import url from "node:url";
|
|
241
|
+
target(import.meta.url);
|
|
242
|
+
target(url.unknown(import.meta.url));
|
|
243
|
+
`,
|
|
244
|
+
result: [true, false],
|
|
245
|
+
},
|
|
246
|
+
]) {
|
|
247
|
+
it(code, () => {
|
|
248
|
+
deepStrictEqual(getIsStaticExpressionResult(code), result);
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
});
|
|
252
|
+
});
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
module.exports.findVariable = findVariable;
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Find the variable of a given name.
|
|
5
|
+
* @param {import("eslint").Scope.Scope} scope the scope to start finding
|
|
6
|
+
* @param {string} name the variable name to find.
|
|
7
|
+
* @returns {import("eslint").Scope.Variable | null}
|
|
8
|
+
*/
|
|
9
|
+
function findVariable(scope, name) {
|
|
10
|
+
while (scope != null) {
|
|
11
|
+
const variable = scope.set.get(name);
|
|
12
|
+
if (variable != null) {
|
|
13
|
+
return variable;
|
|
14
|
+
}
|
|
15
|
+
scope = scope.upper;
|
|
16
|
+
}
|
|
17
|
+
return null;
|
|
18
|
+
}
|
package/utils/import-utils.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
const { findVariable } = require('./find-variable');
|
|
2
|
+
|
|
1
3
|
module.exports.getImportAccessPath = getImportAccessPath;
|
|
2
4
|
|
|
3
5
|
/**
|
|
@@ -182,15 +184,3 @@ function getImportAccessPath({ node, scope, packageNames }) {
|
|
|
182
184
|
return node && node.type === 'ImportDeclaration' && packageNames.includes(node.source.value);
|
|
183
185
|
}
|
|
184
186
|
}
|
|
185
|
-
|
|
186
|
-
/** @returns {import("eslint").Scope.Variable | null} */
|
|
187
|
-
function findVariable(scope, name) {
|
|
188
|
-
while (scope != null) {
|
|
189
|
-
const variable = scope.set.get(name);
|
|
190
|
-
if (variable != null) {
|
|
191
|
-
return variable;
|
|
192
|
-
}
|
|
193
|
-
scope = scope.upper;
|
|
194
|
-
}
|
|
195
|
-
return null;
|
|
196
|
-
}
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
const { findVariable } = require('./find-variable');
|
|
2
|
+
const { getImportAccessPath } = require('./import-utils');
|
|
3
|
+
|
|
4
|
+
module.exports.isStaticExpression = isStaticExpression;
|
|
5
|
+
|
|
6
|
+
const PATH_PACKAGE_NAMES = ['path', 'node:path', 'path/posix', 'node:path/posix'];
|
|
7
|
+
const URL_PACKAGE_NAMES = ['url', 'node:url'];
|
|
8
|
+
const PATH_CONSTRUCTION_METHOD_NAMES = new Set(['basename', 'dirname', 'extname', 'join', 'normalize', 'relative', 'resolve', 'toNamespacedPath']);
|
|
9
|
+
const PATH_STATIC_MEMBER_NAMES = new Set(['delimiter', 'sep']);
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* @type {WeakMap<import("estree").Expression, boolean>}
|
|
13
|
+
*/
|
|
14
|
+
const cache = new WeakMap();
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Checks whether the given expression node is a static or not.
|
|
18
|
+
*
|
|
19
|
+
* @param {Object} params
|
|
20
|
+
* @param {import("estree").Expression} params.node The node to check.
|
|
21
|
+
* @param {import("eslint").Scope.Scope} params.scope The scope of the given node.
|
|
22
|
+
* @returns {boolean} if true, the given expression node is a static.
|
|
23
|
+
*/
|
|
24
|
+
function isStaticExpression({ node, scope }) {
|
|
25
|
+
const tracked = new Set();
|
|
26
|
+
return isStatic(node);
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* @param {import("estree").Expression} node
|
|
30
|
+
* @returns {boolean}
|
|
31
|
+
*/
|
|
32
|
+
function isStatic(node) {
|
|
33
|
+
let result = cache.get(node);
|
|
34
|
+
if (result == null) {
|
|
35
|
+
result = isStaticWithoutCache(node);
|
|
36
|
+
cache.set(node, result);
|
|
37
|
+
}
|
|
38
|
+
return result;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* @param {import("estree").Expression} node
|
|
42
|
+
* @returns {boolean}
|
|
43
|
+
*/
|
|
44
|
+
function isStaticWithoutCache(node) {
|
|
45
|
+
if (tracked.has(node)) {
|
|
46
|
+
// Guard infinite loops.
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
tracked.add(node);
|
|
50
|
+
if (node.type === 'Literal') {
|
|
51
|
+
return true;
|
|
52
|
+
}
|
|
53
|
+
if (node.type === 'TemplateLiteral') {
|
|
54
|
+
// A node is static if all interpolations are static.
|
|
55
|
+
return node.expressions.every(isStatic);
|
|
56
|
+
}
|
|
57
|
+
if (node.type === 'BinaryExpression') {
|
|
58
|
+
// An expression is static if both operands are static.
|
|
59
|
+
return isStatic(node.left) && isStatic(node.right);
|
|
60
|
+
}
|
|
61
|
+
if (node.type === 'Identifier') {
|
|
62
|
+
const variable = findVariable(scope, node.name);
|
|
63
|
+
if (variable) {
|
|
64
|
+
if (variable.defs.length === 0) {
|
|
65
|
+
if (node.name === '__dirname' || node.name === '__filename') {
|
|
66
|
+
// It is a global variable that can be used in CJS of Node.js.
|
|
67
|
+
return true;
|
|
68
|
+
}
|
|
69
|
+
} else if (variable.defs.length === 1) {
|
|
70
|
+
const def = variable.defs[0];
|
|
71
|
+
if (
|
|
72
|
+
def.type === 'Variable' &&
|
|
73
|
+
// It has an initial value.
|
|
74
|
+
def.node.init &&
|
|
75
|
+
// It does not write new values.
|
|
76
|
+
variable.references.every((ref) => ref.isReadOnly() || ref.identifier === def.name)
|
|
77
|
+
) {
|
|
78
|
+
// A variable is static if its initial value is static.
|
|
79
|
+
return isStatic(def.node.init);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
} else {
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return isStaticPath(node) || isStaticFileURLToPath(node) || isStaticImportMetaUrl(node) || isStaticRequireResolve(node) || isStaticCwd(node);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Checks whether the given expression is a static path construction.
|
|
91
|
+
*
|
|
92
|
+
* @param {import("estree").Expression} node The node to check.
|
|
93
|
+
* @returns {boolean} if true, the given expression is a static path construction.
|
|
94
|
+
*/
|
|
95
|
+
function isStaticPath(node) {
|
|
96
|
+
const pathInfo = getImportAccessPath({
|
|
97
|
+
node: node.type === 'CallExpression' ? node.callee : node,
|
|
98
|
+
scope,
|
|
99
|
+
packageNames: PATH_PACKAGE_NAMES,
|
|
100
|
+
});
|
|
101
|
+
if (!pathInfo) {
|
|
102
|
+
return false;
|
|
103
|
+
}
|
|
104
|
+
/** @type {string | undefined} */
|
|
105
|
+
let name;
|
|
106
|
+
if (pathInfo.path.length === 1) {
|
|
107
|
+
// e.g. import path from 'path'
|
|
108
|
+
name = pathInfo.path[0];
|
|
109
|
+
} else if (pathInfo.path.length === 2 && pathInfo.path[0] === 'posix') {
|
|
110
|
+
// e.g. import { posix as path } from 'path'
|
|
111
|
+
name = pathInfo.path[1];
|
|
112
|
+
}
|
|
113
|
+
if (name == null) {
|
|
114
|
+
return false;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (node.type === 'CallExpression') {
|
|
118
|
+
if (!PATH_CONSTRUCTION_METHOD_NAMES.has(name)) {
|
|
119
|
+
return false;
|
|
120
|
+
}
|
|
121
|
+
return Boolean(node.arguments.length) && node.arguments.every(isStatic);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return PATH_STATIC_MEMBER_NAMES.has(name);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Checks whether the given expression is a static `url.fileURLToPath()`.
|
|
129
|
+
*
|
|
130
|
+
* @param {import("estree").Expression} node The node to check.
|
|
131
|
+
* @returns {boolean} if true, the given expression is a static `url.fileURLToPath()`.
|
|
132
|
+
*/
|
|
133
|
+
function isStaticFileURLToPath(node) {
|
|
134
|
+
if (node.type !== 'CallExpression') {
|
|
135
|
+
return false;
|
|
136
|
+
}
|
|
137
|
+
const pathInfo = getImportAccessPath({
|
|
138
|
+
node: node.callee,
|
|
139
|
+
scope,
|
|
140
|
+
packageNames: URL_PACKAGE_NAMES,
|
|
141
|
+
});
|
|
142
|
+
if (!pathInfo || pathInfo.path.length !== 1) {
|
|
143
|
+
return false;
|
|
144
|
+
}
|
|
145
|
+
let name = pathInfo.path[0];
|
|
146
|
+
if (name !== 'fileURLToPath') {
|
|
147
|
+
return false;
|
|
148
|
+
}
|
|
149
|
+
return Boolean(node.arguments.length) && node.arguments.every(isStatic);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Checks whether the given expression is an `import.meta.url`.
|
|
154
|
+
*
|
|
155
|
+
* @param {import("estree").Expression} node The node to check.
|
|
156
|
+
* @returns {boolean} if true, the given expression is an `import.meta.url`.
|
|
157
|
+
*/
|
|
158
|
+
function isStaticImportMetaUrl(node) {
|
|
159
|
+
return (
|
|
160
|
+
node.type === 'MemberExpression' &&
|
|
161
|
+
!node.computed &&
|
|
162
|
+
node.property.type === 'Identifier' &&
|
|
163
|
+
node.property.name === 'url' &&
|
|
164
|
+
node.object.type === 'MetaProperty' &&
|
|
165
|
+
node.object.meta.name === 'import' &&
|
|
166
|
+
node.object.property.name === 'meta'
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Checks whether the given expression is a static `require.resolve()`.
|
|
172
|
+
*
|
|
173
|
+
* @param {import("estree").Expression} node The node to check.
|
|
174
|
+
* @returns {boolean} if true, the given expression is a static `require.resolve()`.
|
|
175
|
+
*/
|
|
176
|
+
function isStaticRequireResolve(node) {
|
|
177
|
+
if (
|
|
178
|
+
node.type !== 'CallExpression' ||
|
|
179
|
+
node.callee.type !== 'MemberExpression' ||
|
|
180
|
+
node.callee.computed ||
|
|
181
|
+
node.callee.property.type !== 'Identifier' ||
|
|
182
|
+
node.callee.property.name !== 'resolve' ||
|
|
183
|
+
node.callee.object.type !== 'Identifier' ||
|
|
184
|
+
node.callee.object.name !== 'require'
|
|
185
|
+
) {
|
|
186
|
+
return false;
|
|
187
|
+
}
|
|
188
|
+
const variable = findVariable(scope, node.callee.object.name);
|
|
189
|
+
if (!variable || variable.defs.length !== 0) {
|
|
190
|
+
return false;
|
|
191
|
+
}
|
|
192
|
+
return Boolean(node.arguments.length) && node.arguments.every(isStatic);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Checks whether the given expression is a static `process.cwd()`.
|
|
197
|
+
*
|
|
198
|
+
* @param {import("estree").Expression} node The node to check.
|
|
199
|
+
* @returns {boolean} if true, the given expression is a static `process.cwd()`.
|
|
200
|
+
*/
|
|
201
|
+
function isStaticCwd(node) {
|
|
202
|
+
if (
|
|
203
|
+
node.type !== 'CallExpression' ||
|
|
204
|
+
node.callee.type !== 'MemberExpression' ||
|
|
205
|
+
node.callee.computed ||
|
|
206
|
+
node.callee.property.type !== 'Identifier' ||
|
|
207
|
+
node.callee.property.name !== 'cwd' ||
|
|
208
|
+
node.callee.object.type !== 'Identifier' ||
|
|
209
|
+
node.callee.object.name !== 'process'
|
|
210
|
+
) {
|
|
211
|
+
return false;
|
|
212
|
+
}
|
|
213
|
+
const variable = findVariable(scope, node.callee.object.name);
|
|
214
|
+
if (!variable || variable.defs.length !== 0) {
|
|
215
|
+
return false;
|
|
216
|
+
}
|
|
217
|
+
return true;
|
|
218
|
+
}
|
|
219
|
+
}
|