eslint-plugin-ocavue 0.2.0 → 0.3.0
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 +79 -4
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +420 -22
- package/dist/index.js.map +1 -1
- package/package.json +5 -3
- package/src/index.ts +6 -0
- package/src/rules/prefer-constant-function.test.ts +134 -0
- package/src/rules/prefer-constant-function.ts +73 -0
- package/src/rules/prefer-noop.test.ts +263 -0
- package/src/rules/prefer-noop.ts +53 -0
- package/src/rules/prefer-sleep.test.ts +194 -0
- package/src/rules/prefer-sleep.ts +228 -0
- package/src/utils/constant-function.ts +102 -0
- package/src/utils/has-dependency.ts +81 -0
- package/src/utils/package-import.ts +174 -0
- package/src/utils/test-projects.ts +38 -0
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
import { RuleTester } from 'eslint'
|
|
2
|
+
import tseslint from 'typescript-eslint'
|
|
3
|
+
import { test } from 'vitest'
|
|
4
|
+
|
|
5
|
+
import { useUtilsProjects } from '../utils/test-projects'
|
|
6
|
+
|
|
7
|
+
import rule from './prefer-sleep'
|
|
8
|
+
|
|
9
|
+
const js = useUtilsProjects('js')
|
|
10
|
+
const ts = useUtilsProjects('ts')
|
|
11
|
+
|
|
12
|
+
const IMPORT = "import { sleep } from '@ocavue/utils'\n"
|
|
13
|
+
|
|
14
|
+
test('prefer-sleep', () => {
|
|
15
|
+
const tester = new RuleTester({
|
|
16
|
+
languageOptions: { ecmaVersion: 'latest', sourceType: 'module' },
|
|
17
|
+
})
|
|
18
|
+
const filename = js.withUtils
|
|
19
|
+
|
|
20
|
+
tester.run('prefer-sleep', rule, {
|
|
21
|
+
valid: [
|
|
22
|
+
// The project does not depend on `@ocavue/utils`.
|
|
23
|
+
{ code: 'await new Promise((r) => setTimeout(r, 100))', filename: js.withoutUtils },
|
|
24
|
+
// `@ocavue/utils` itself.
|
|
25
|
+
{ code: 'await new Promise((r) => setTimeout(r, 100))', filename: js.utilsRepo },
|
|
26
|
+
|
|
27
|
+
// Not a plain delay.
|
|
28
|
+
{ code: 'new Promise((r) => setTimeout(r))', filename },
|
|
29
|
+
{ code: 'new Promise((r) => setTimeout(r, 100, value))', filename },
|
|
30
|
+
{ code: 'new Promise((r) => setTimeout(() => r(value), 100))', filename },
|
|
31
|
+
{ code: 'new Promise((r) => setTimeout(() => { g(); r() }, 100))', filename },
|
|
32
|
+
{ code: 'new Promise((r) => setTimeout(other, 100))', filename },
|
|
33
|
+
{ code: 'new Promise((r) => { g(); setTimeout(r, 100) })', filename },
|
|
34
|
+
{ code: 'new Promise((r, reject) => setTimeout(reject, 100))', filename },
|
|
35
|
+
{ code: 'new Promise(async (r) => setTimeout(r, 100))', filename },
|
|
36
|
+
{ code: 'new Promise(({ r }) => setTimeout(r, 100))', filename },
|
|
37
|
+
{ code: 'new Promise(() => setTimeout(g, 100))', filename },
|
|
38
|
+
{ code: 'new Promise((r) => setTimeout(...args))', filename },
|
|
39
|
+
{ code: 'new Promise((r) => setInterval(r, 100))', filename },
|
|
40
|
+
{ code: 'new Promise((r) => foo.setTimeout(r, 100))', filename },
|
|
41
|
+
|
|
42
|
+
// The delay uses the executor's parameters, `this` or `arguments`.
|
|
43
|
+
{ code: 'new Promise((r) => setTimeout(r, r.length))', filename },
|
|
44
|
+
{ code: 'new Promise((r, reject) => setTimeout(r, reject.length))', filename },
|
|
45
|
+
{ code: 'new Promise(function (r) { setTimeout(r, this.ms) })', filename },
|
|
46
|
+
{ code: 'new Promise(function (r) { setTimeout(r, arguments.length) })', filename },
|
|
47
|
+
|
|
48
|
+
// Local bindings that shadow globals.
|
|
49
|
+
{ code: 'const Promise = P\nnew Promise((r) => setTimeout(r, 100))', filename },
|
|
50
|
+
{ code: 'const setTimeout = t\nnew Promise((r) => setTimeout(r, 100))', filename },
|
|
51
|
+
|
|
52
|
+
// Already uses `sleep`.
|
|
53
|
+
{ code: `${IMPORT}await sleep(100)`, filename },
|
|
54
|
+
],
|
|
55
|
+
invalid: [
|
|
56
|
+
{
|
|
57
|
+
code: 'await new Promise((resolve) => setTimeout(resolve, 100))',
|
|
58
|
+
output: `${IMPORT}await sleep(100)`,
|
|
59
|
+
filename,
|
|
60
|
+
errors: [{ messageId: 'preferSleep' }],
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
code: 'await new Promise(r => setTimeout(r, ms))',
|
|
64
|
+
output: `${IMPORT}await sleep(ms)`,
|
|
65
|
+
filename,
|
|
66
|
+
errors: [{ messageId: 'preferSleep' }],
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
code: 'await new Promise((resolve) => { setTimeout(resolve, 100) })',
|
|
70
|
+
output: `${IMPORT}await sleep(100)`,
|
|
71
|
+
filename,
|
|
72
|
+
errors: [{ messageId: 'preferSleep' }],
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
code: 'await new Promise((resolve) => { return setTimeout(resolve, 100) })',
|
|
76
|
+
output: `${IMPORT}await sleep(100)`,
|
|
77
|
+
filename,
|
|
78
|
+
errors: [{ messageId: 'preferSleep' }],
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
code: 'await new Promise((resolve) => setTimeout(() => resolve(), 100))',
|
|
82
|
+
output: `${IMPORT}await sleep(100)`,
|
|
83
|
+
filename,
|
|
84
|
+
errors: [{ messageId: 'preferSleep' }],
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
code: 'await new Promise((resolve) => setTimeout(() => { resolve() }, 100))',
|
|
88
|
+
output: `${IMPORT}await sleep(100)`,
|
|
89
|
+
filename,
|
|
90
|
+
errors: [{ messageId: 'preferSleep' }],
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
code: 'await new Promise(function (resolve) { setTimeout(resolve, 100) })',
|
|
94
|
+
output: `${IMPORT}await sleep(100)`,
|
|
95
|
+
filename,
|
|
96
|
+
errors: [{ messageId: 'preferSleep' }],
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
code: 'await new Promise((resolve, _reject) => setTimeout(resolve, 100))',
|
|
100
|
+
output: `${IMPORT}await sleep(100)`,
|
|
101
|
+
filename,
|
|
102
|
+
errors: [{ messageId: 'preferSleep' }],
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
code: 'await new Promise((r) => globalThis.setTimeout(r, 100))',
|
|
106
|
+
output: `${IMPORT}await sleep(100)`,
|
|
107
|
+
filename,
|
|
108
|
+
errors: [{ messageId: 'preferSleep' }],
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
code: 'await new Promise((r) => window.setTimeout(r, 100))',
|
|
112
|
+
output: `${IMPORT}await sleep(100)`,
|
|
113
|
+
filename,
|
|
114
|
+
errors: [{ messageId: 'preferSleep' }],
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
code: 'await new Promise((r) => setTimeout(r, this.delay * 2))',
|
|
118
|
+
output: `${IMPORT}await sleep(this.delay * 2)`,
|
|
119
|
+
filename,
|
|
120
|
+
errors: [{ messageId: 'preferSleep' }],
|
|
121
|
+
},
|
|
122
|
+
{
|
|
123
|
+
code: 'await new Promise((r) => setTimeout(r, (a, b)))',
|
|
124
|
+
output: `${IMPORT}await sleep((a, b))`,
|
|
125
|
+
filename,
|
|
126
|
+
errors: [{ messageId: 'preferSleep' }],
|
|
127
|
+
},
|
|
128
|
+
{
|
|
129
|
+
code: 'const wait = () => new Promise((r) => setTimeout(r, 0))',
|
|
130
|
+
output: `${IMPORT}const wait = () => sleep(0)`,
|
|
131
|
+
filename,
|
|
132
|
+
errors: [{ messageId: 'preferSleep' }],
|
|
133
|
+
},
|
|
134
|
+
{
|
|
135
|
+
code: "import { noop } from '@ocavue/utils'\nawait new Promise((r) => setTimeout(r, 100))",
|
|
136
|
+
output: "import { noop, sleep } from '@ocavue/utils'\nawait sleep(100)",
|
|
137
|
+
filename,
|
|
138
|
+
errors: [{ messageId: 'preferSleep' }],
|
|
139
|
+
},
|
|
140
|
+
{
|
|
141
|
+
// Reported without a fix.
|
|
142
|
+
code: 'const sleep = 1\nawait new Promise((r) => setTimeout(r, 100))',
|
|
143
|
+
output: null,
|
|
144
|
+
filename,
|
|
145
|
+
errors: [{ messageId: 'preferSleep' }],
|
|
146
|
+
},
|
|
147
|
+
{
|
|
148
|
+
code: 'await new Promise((r) => setTimeout(r, 100 /* ms */))',
|
|
149
|
+
output: null,
|
|
150
|
+
filename,
|
|
151
|
+
errors: [{ messageId: 'preferSleep' }],
|
|
152
|
+
},
|
|
153
|
+
{
|
|
154
|
+
code: 'await new Promise((r) => setTimeout(r, 100))',
|
|
155
|
+
output: `${IMPORT}await sleep(100)`,
|
|
156
|
+
filename: js.withoutUtils,
|
|
157
|
+
options: [{ checkDependency: false }],
|
|
158
|
+
errors: [{ messageId: 'preferSleep' }],
|
|
159
|
+
},
|
|
160
|
+
],
|
|
161
|
+
})
|
|
162
|
+
})
|
|
163
|
+
|
|
164
|
+
test('prefer-sleep with TypeScript', () => {
|
|
165
|
+
const tester = new RuleTester({
|
|
166
|
+
languageOptions: {
|
|
167
|
+
parser: tseslint.parser,
|
|
168
|
+
ecmaVersion: 'latest',
|
|
169
|
+
sourceType: 'module',
|
|
170
|
+
},
|
|
171
|
+
})
|
|
172
|
+
const filename = ts.withUtils
|
|
173
|
+
|
|
174
|
+
tester.run('prefer-sleep', rule, {
|
|
175
|
+
valid: [
|
|
176
|
+
// `sleep` returns `Promise<void>`.
|
|
177
|
+
{ code: 'await new Promise<number>((r) => setTimeout(r, 100))', filename },
|
|
178
|
+
],
|
|
179
|
+
invalid: [
|
|
180
|
+
{
|
|
181
|
+
code: 'await new Promise<void>((resolve) => setTimeout(resolve, 100))',
|
|
182
|
+
output: `${IMPORT}await sleep(100)`,
|
|
183
|
+
filename,
|
|
184
|
+
errors: [{ messageId: 'preferSleep' }],
|
|
185
|
+
},
|
|
186
|
+
{
|
|
187
|
+
code: 'await new Promise<undefined>((r) => setTimeout(r, ms as number))',
|
|
188
|
+
output: `${IMPORT}await sleep(ms as number)`,
|
|
189
|
+
filename,
|
|
190
|
+
errors: [{ messageId: 'preferSleep' }],
|
|
191
|
+
},
|
|
192
|
+
],
|
|
193
|
+
})
|
|
194
|
+
})
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
import type { Rule, Scope } from 'eslint'
|
|
2
|
+
import type {
|
|
3
|
+
ArrowFunctionExpression,
|
|
4
|
+
CallExpression,
|
|
5
|
+
Expression,
|
|
6
|
+
FunctionExpression,
|
|
7
|
+
Node,
|
|
8
|
+
SpreadElement,
|
|
9
|
+
} from 'estree'
|
|
10
|
+
|
|
11
|
+
import {
|
|
12
|
+
createImportFix,
|
|
13
|
+
findVariable,
|
|
14
|
+
isPackageAvailable,
|
|
15
|
+
packageRuleSchema,
|
|
16
|
+
UTILS_PACKAGE,
|
|
17
|
+
} from '../utils/package-import'
|
|
18
|
+
|
|
19
|
+
type FunctionNode = ArrowFunctionExpression | FunctionExpression
|
|
20
|
+
|
|
21
|
+
function isFunction(node: Node): node is FunctionNode {
|
|
22
|
+
return node.type === 'ArrowFunctionExpression' || node.type === 'FunctionExpression'
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Returns true when `name` refers to a global variable, not a local binding.
|
|
27
|
+
*/
|
|
28
|
+
function isGlobal(scope: Scope.Scope, name: string): boolean {
|
|
29
|
+
const variable = findVariable(scope, name)
|
|
30
|
+
return !variable || variable.defs.length === 0
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Returns the only expression that a function body evaluates, for bodies
|
|
35
|
+
* such as `x`, `{ x }` and `{ return x }`.
|
|
36
|
+
*/
|
|
37
|
+
function getOnlyExpression(node: FunctionNode): Expression | undefined {
|
|
38
|
+
const body = node.body
|
|
39
|
+
if (body.type !== 'BlockStatement') {
|
|
40
|
+
return body
|
|
41
|
+
}
|
|
42
|
+
if (body.body.length !== 1) {
|
|
43
|
+
return undefined
|
|
44
|
+
}
|
|
45
|
+
const statement = body.body[0]
|
|
46
|
+
if (statement.type === 'ExpressionStatement') {
|
|
47
|
+
return statement.expression
|
|
48
|
+
}
|
|
49
|
+
if (statement.type === 'ReturnStatement' && statement.argument) {
|
|
50
|
+
return statement.argument
|
|
51
|
+
}
|
|
52
|
+
return undefined
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function isIdentifier(node: Node | SpreadElement, name: string): boolean {
|
|
56
|
+
return node.type === 'Identifier' && node.name === name
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Returns true for `setTimeout`, `globalThis.setTimeout` and
|
|
61
|
+
* `window.setTimeout`.
|
|
62
|
+
*/
|
|
63
|
+
function isSetTimeout(node: Expression, scope: Scope.Scope): boolean {
|
|
64
|
+
if (node.type === 'Identifier') {
|
|
65
|
+
return node.name === 'setTimeout' && isGlobal(scope, 'setTimeout')
|
|
66
|
+
}
|
|
67
|
+
return (
|
|
68
|
+
node.type === 'MemberExpression' &&
|
|
69
|
+
!node.computed &&
|
|
70
|
+
isIdentifier(node.property, 'setTimeout') &&
|
|
71
|
+
node.object.type === 'Identifier' &&
|
|
72
|
+
(node.object.name === 'globalThis' || node.object.name === 'window') &&
|
|
73
|
+
isGlobal(scope, node.object.name)
|
|
74
|
+
)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Returns true for `resolve` and `() => resolve()`.
|
|
79
|
+
*/
|
|
80
|
+
function isResolveCallback(node: Expression | SpreadElement, resolve: string): boolean {
|
|
81
|
+
if (isIdentifier(node, resolve)) {
|
|
82
|
+
return true
|
|
83
|
+
}
|
|
84
|
+
if (!isFunction(node) || node.async || node.generator || node.params.length > 0) {
|
|
85
|
+
return false
|
|
86
|
+
}
|
|
87
|
+
const expression = getOnlyExpression(node)
|
|
88
|
+
return (
|
|
89
|
+
expression?.type === 'CallExpression' &&
|
|
90
|
+
isIdentifier(expression.callee, resolve) &&
|
|
91
|
+
expression.arguments.length === 0
|
|
92
|
+
)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Returns true for no type argument, `<void>` and `<undefined>`.
|
|
97
|
+
*/
|
|
98
|
+
function hasVoidTypeArgument(node: Node): boolean {
|
|
99
|
+
const typeArguments = (node as { typeArguments?: { params: { type: string }[] } }).typeArguments
|
|
100
|
+
if (!typeArguments) {
|
|
101
|
+
return true
|
|
102
|
+
}
|
|
103
|
+
return (
|
|
104
|
+
typeArguments.params.length === 1 &&
|
|
105
|
+
(typeArguments.params[0].type === 'TSVoidKeyword' ||
|
|
106
|
+
typeArguments.params[0].type === 'TSUndefinedKeyword')
|
|
107
|
+
)
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const rule: Rule.RuleModule = {
|
|
111
|
+
meta: {
|
|
112
|
+
type: 'suggestion',
|
|
113
|
+
fixable: 'code',
|
|
114
|
+
docs: {
|
|
115
|
+
description:
|
|
116
|
+
'enforce `sleep` from `@ocavue/utils` instead of `new Promise` with `setTimeout`',
|
|
117
|
+
url: 'https://github.com/ocavue/eslint-plugin-ocavue#prefer-sleep',
|
|
118
|
+
},
|
|
119
|
+
messages: {
|
|
120
|
+
preferSleep: 'Use `sleep` from `@ocavue/utils` instead of `new Promise` with `setTimeout`.',
|
|
121
|
+
},
|
|
122
|
+
schema: packageRuleSchema,
|
|
123
|
+
},
|
|
124
|
+
create(context) {
|
|
125
|
+
if (!isPackageAvailable(context, UTILS_PACKAGE)) {
|
|
126
|
+
return {}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const sourceCode = context.sourceCode
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Returns the delay expression of `new Promise((resolve) =>
|
|
133
|
+
* setTimeout(resolve, delay))`, or `undefined` when the node does not
|
|
134
|
+
* match.
|
|
135
|
+
*/
|
|
136
|
+
function getDelay(node: Node): Expression | undefined {
|
|
137
|
+
if (
|
|
138
|
+
node.type !== 'NewExpression' ||
|
|
139
|
+
!isIdentifier(node.callee, 'Promise') ||
|
|
140
|
+
node.arguments.length !== 1 ||
|
|
141
|
+
!hasVoidTypeArgument(node)
|
|
142
|
+
) {
|
|
143
|
+
return undefined
|
|
144
|
+
}
|
|
145
|
+
if (!isGlobal(sourceCode.getScope(node), 'Promise')) {
|
|
146
|
+
return undefined
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const executor = node.arguments[0]
|
|
150
|
+
if (!isFunction(executor) || executor.async || executor.generator) {
|
|
151
|
+
return undefined
|
|
152
|
+
}
|
|
153
|
+
if (!executor.params.every((param) => param.type === 'Identifier')) {
|
|
154
|
+
return undefined
|
|
155
|
+
}
|
|
156
|
+
const resolveParam = executor.params[0]
|
|
157
|
+
if (resolveParam?.type !== 'Identifier') {
|
|
158
|
+
return undefined
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const call = getOnlyExpression(executor)
|
|
162
|
+
if (call?.type !== 'CallExpression' || call.optional) {
|
|
163
|
+
return undefined
|
|
164
|
+
}
|
|
165
|
+
const executorScope = sourceCode.getScope(executor)
|
|
166
|
+
if (!isSetTimeout(call.callee as Expression, executorScope)) {
|
|
167
|
+
return undefined
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const [callback, delay] = (call as CallExpression).arguments
|
|
171
|
+
if (call.arguments.length !== 2 || !delay || delay.type === 'SpreadElement') {
|
|
172
|
+
return undefined
|
|
173
|
+
}
|
|
174
|
+
if (!isResolveCallback(callback, resolveParam.name)) {
|
|
175
|
+
return undefined
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// The delay moves out of the executor, so it must not use the
|
|
179
|
+
// executor's parameters, `this` or `arguments`.
|
|
180
|
+
const usesExecutorParams = executorScope.variables.some((variable) => {
|
|
181
|
+
return variable.references.some((reference) => {
|
|
182
|
+
const range = reference.identifier.range!
|
|
183
|
+
return range[0] >= delay.range![0] && range[1] <= delay.range![1]
|
|
184
|
+
})
|
|
185
|
+
})
|
|
186
|
+
if (usesExecutorParams) {
|
|
187
|
+
return undefined
|
|
188
|
+
}
|
|
189
|
+
if (
|
|
190
|
+
executor.type === 'FunctionExpression' &&
|
|
191
|
+
sourceCode.getTokens(delay).some((token) => {
|
|
192
|
+
return token.value === 'this' || token.value === 'arguments'
|
|
193
|
+
})
|
|
194
|
+
) {
|
|
195
|
+
return undefined
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
return delay
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
return {
|
|
202
|
+
NewExpression(node) {
|
|
203
|
+
const delay = getDelay(node)
|
|
204
|
+
if (!delay) {
|
|
205
|
+
return
|
|
206
|
+
}
|
|
207
|
+
const delayText = sourceCode.getText(delay)
|
|
208
|
+
context.report({
|
|
209
|
+
node,
|
|
210
|
+
messageId: 'preferSleep',
|
|
211
|
+
fix: createImportFix({
|
|
212
|
+
context,
|
|
213
|
+
node,
|
|
214
|
+
name: 'sleep',
|
|
215
|
+
packageName: UTILS_PACKAGE,
|
|
216
|
+
replace: (local) => {
|
|
217
|
+
return delay.type === 'SequenceExpression'
|
|
218
|
+
? `${local}((${delayText}))`
|
|
219
|
+
: `${local}(${delayText})`
|
|
220
|
+
},
|
|
221
|
+
}),
|
|
222
|
+
})
|
|
223
|
+
},
|
|
224
|
+
}
|
|
225
|
+
},
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
export default rule
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import type { Rule } from 'eslint'
|
|
2
|
+
import type { ArrowFunctionExpression, Expression, FunctionExpression, Node } from 'estree'
|
|
3
|
+
|
|
4
|
+
export type FunctionNode = (ArrowFunctionExpression | FunctionExpression) & Rule.NodeParentExtension
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The value that a constant function returns.
|
|
8
|
+
*/
|
|
9
|
+
export type ConstantValue = 'undefined' | 'true' | 'false' | 'null'
|
|
10
|
+
|
|
11
|
+
function getConstantValue(node: Expression): ConstantValue | undefined {
|
|
12
|
+
if (node.type === 'Literal') {
|
|
13
|
+
switch (node.raw) {
|
|
14
|
+
case 'true':
|
|
15
|
+
case 'false':
|
|
16
|
+
case 'null':
|
|
17
|
+
return node.raw
|
|
18
|
+
}
|
|
19
|
+
return undefined
|
|
20
|
+
}
|
|
21
|
+
if (node.type === 'Identifier' && node.name === 'undefined') {
|
|
22
|
+
return 'undefined'
|
|
23
|
+
}
|
|
24
|
+
if (
|
|
25
|
+
node.type === 'UnaryExpression' &&
|
|
26
|
+
node.operator === 'void' &&
|
|
27
|
+
node.argument.type === 'Literal'
|
|
28
|
+
) {
|
|
29
|
+
return 'undefined'
|
|
30
|
+
}
|
|
31
|
+
return undefined
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function getReturnedValue(node: FunctionNode): ConstantValue | undefined {
|
|
35
|
+
const body = node.body
|
|
36
|
+
if (body.type !== 'BlockStatement') {
|
|
37
|
+
return getConstantValue(body)
|
|
38
|
+
}
|
|
39
|
+
if (body.body.length === 0) {
|
|
40
|
+
return 'undefined'
|
|
41
|
+
}
|
|
42
|
+
if (body.body.length === 1) {
|
|
43
|
+
const statement = body.body[0]
|
|
44
|
+
if (statement.type === 'ReturnStatement') {
|
|
45
|
+
return statement.argument ? getConstantValue(statement.argument) : 'undefined'
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return undefined
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Returns true when the function can be swapped with a function reference
|
|
53
|
+
* without changing runtime behavior.
|
|
54
|
+
*/
|
|
55
|
+
function isReplaceable(node: FunctionNode): boolean {
|
|
56
|
+
if (node.async || node.generator) {
|
|
57
|
+
return false
|
|
58
|
+
}
|
|
59
|
+
if ((node as { typeParameters?: unknown }).typeParameters) {
|
|
60
|
+
return false
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Every parameter must be a plain identifier. Default values may have side
|
|
64
|
+
// effects, and destructuring may throw.
|
|
65
|
+
const hasPlainParams = node.params.every((param) => {
|
|
66
|
+
return (
|
|
67
|
+
param.type === 'Identifier' ||
|
|
68
|
+
(param.type === 'RestElement' && param.argument.type === 'Identifier')
|
|
69
|
+
)
|
|
70
|
+
})
|
|
71
|
+
if (!hasPlainParams) {
|
|
72
|
+
return false
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (node.type === 'FunctionExpression') {
|
|
76
|
+
if (node.id) {
|
|
77
|
+
return false
|
|
78
|
+
}
|
|
79
|
+
const parent: Node = node.parent
|
|
80
|
+
if (parent.type === 'Property' && (parent.method || parent.kind !== 'init')) {
|
|
81
|
+
return false
|
|
82
|
+
}
|
|
83
|
+
if (parent.type === 'MethodDefinition') {
|
|
84
|
+
return false
|
|
85
|
+
}
|
|
86
|
+
if (parent.type === 'NewExpression' && parent.callee === node) {
|
|
87
|
+
return false
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return true
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Returns the value that an inline function always returns, such as
|
|
96
|
+
* `'true'` for `() => true` or `'undefined'` for `() => {}`. Returns
|
|
97
|
+
* `undefined` when the function does more than that, or when replacing it
|
|
98
|
+
* could change runtime behavior.
|
|
99
|
+
*/
|
|
100
|
+
export function getConstantFunctionValue(node: FunctionNode): ConstantValue | undefined {
|
|
101
|
+
return isReplaceable(node) ? getReturnedValue(node) : undefined
|
|
102
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import fs from 'node:fs'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
|
|
4
|
+
interface PackageJson {
|
|
5
|
+
name?: string
|
|
6
|
+
dependencies?: Record<string, string>
|
|
7
|
+
devDependencies?: Record<string, string>
|
|
8
|
+
peerDependencies?: Record<string, string>
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
// Maps a directory to its nearest `package.json`, or `null` when there is none.
|
|
12
|
+
const cache = new Map<string, PackageJson | null>()
|
|
13
|
+
|
|
14
|
+
function readPackageJson(dir: string): PackageJson | undefined {
|
|
15
|
+
let text: string
|
|
16
|
+
try {
|
|
17
|
+
text = fs.readFileSync(path.join(dir, 'package.json'), 'utf8')
|
|
18
|
+
} catch {
|
|
19
|
+
return undefined
|
|
20
|
+
}
|
|
21
|
+
try {
|
|
22
|
+
const json: unknown = JSON.parse(text)
|
|
23
|
+
return json && typeof json === 'object' ? json : {}
|
|
24
|
+
} catch {
|
|
25
|
+
return {}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function findNearestPackageJson(startDir: string): PackageJson | null {
|
|
30
|
+
const visited: string[] = []
|
|
31
|
+
let dir = startDir
|
|
32
|
+
let result: PackageJson | null = null
|
|
33
|
+
|
|
34
|
+
while (true) {
|
|
35
|
+
const cached = cache.get(dir)
|
|
36
|
+
if (cached !== undefined) {
|
|
37
|
+
result = cached
|
|
38
|
+
break
|
|
39
|
+
}
|
|
40
|
+
visited.push(dir)
|
|
41
|
+
|
|
42
|
+
const pkg = readPackageJson(dir)
|
|
43
|
+
if (pkg) {
|
|
44
|
+
result = pkg
|
|
45
|
+
break
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const parent = path.dirname(dir)
|
|
49
|
+
if (parent === dir) {
|
|
50
|
+
break
|
|
51
|
+
}
|
|
52
|
+
dir = parent
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
for (const visitedDir of visited) {
|
|
56
|
+
cache.set(visitedDir, result)
|
|
57
|
+
}
|
|
58
|
+
return result
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Returns true when the nearest `package.json` of `filename` lists
|
|
63
|
+
* `packageName` in `dependencies`, `devDependencies` or `peerDependencies`.
|
|
64
|
+
*
|
|
65
|
+
* A package never depends on itself, so this returns false inside the
|
|
66
|
+
* package named `packageName`.
|
|
67
|
+
*/
|
|
68
|
+
export function hasDependency(filename: string, packageName: string): boolean {
|
|
69
|
+
if (!path.isAbsolute(filename)) {
|
|
70
|
+
return false
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const pkg = findNearestPackageJson(path.dirname(filename))
|
|
74
|
+
if (!pkg || pkg.name === packageName) {
|
|
75
|
+
return false
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return [pkg.dependencies, pkg.devDependencies, pkg.peerDependencies].some(
|
|
79
|
+
(deps) => deps != null && typeof deps === 'object' && Object.hasOwn(deps, packageName),
|
|
80
|
+
)
|
|
81
|
+
}
|