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 CHANGED
@@ -21,6 +21,9 @@ export default [
21
21
  plugins: { ocavue },
22
22
  rules: {
23
23
  'ocavue/no-implicit-arrow-linebreak': 'error',
24
+ 'ocavue/prefer-constant-function': 'error',
25
+ 'ocavue/prefer-noop': 'error',
26
+ 'ocavue/prefer-sleep': 'error',
24
27
  'ocavue/prefer-while-true': 'error',
25
28
  },
26
29
  },
@@ -35,9 +38,9 @@ Disallow a linebreak between `=>` and an implicit arrow function body. Unlike [`
35
38
 
36
39
  ```js
37
40
  // Incorrect
38
- const items = Array.from({ length: count }, (_, index) =>
39
- createItem(index === 0 ? 'first item label' : `item number ${index}`),
40
- )
41
+ const items = Array.from({ length: count }, (_, index) => {
42
+ return createItem(index === 0 ? 'first item label' : `item number ${index}`)
43
+ })
41
44
 
42
45
  // Correct
43
46
  const short = (index) => createItem(`item number ${index}`)
@@ -50,13 +53,85 @@ const items = Array.from({ length: count }, (_, index) => {
50
53
 
51
54
  When a comment sits between `=>` and the body, the rule reports without autofixing so the fix never drops a comment.
52
55
 
56
+ ### `prefer-constant-function`
57
+
58
+ Enforce `returnTrue`, `returnFalse` and `returnNull` from [`@ocavue/utils`](https://github.com/ocavue/utils) instead of inline functions such as `() => true`, `() => false` and `() => null`.
59
+
60
+ ```js
61
+ // Incorrect
62
+ const all = items.filter(() => true)
63
+
64
+ // Correct (what the suggestion produces)
65
+ import { returnTrue } from '@ocavue/utils'
66
+
67
+ const all = items.filter(returnTrue)
68
+ ```
69
+
70
+ The rule offers an editor suggestion, not an autofix. `returnFalse` returns the literal type `false`, while TypeScript infers `() => false` as `() => boolean`, so the swap can narrow an inferred type:
71
+
72
+ ```ts
73
+ const [open, setOpen] = useState(() => false) // open: boolean
74
+ const [open, setOpen] = useState(returnFalse) // open: false, so setOpen(true) is a type error
75
+ ```
76
+
77
+ The rule uses the same `checkDependency` option, skip conditions and import handling as [`prefer-noop`](#prefer-noop).
78
+
79
+ ### `prefer-noop`
80
+
81
+ Enforce `noop` from [`@ocavue/utils`](https://github.com/ocavue/utils) instead of inline empty functions such as `() => {}`, `() => undefined` and `function () {}`. A shared `noop` reads as intent ("do nothing on purpose") and avoids creating a new function on every render or call.
82
+
83
+ ```js
84
+ // Incorrect
85
+ ;<button onClick={() => {}} />
86
+
87
+ // Correct (what the fixer produces)
88
+ import { noop } from '@ocavue/utils'
89
+
90
+ ;<button onClick={noop} />
91
+ ```
92
+
93
+ The rule only runs when the nearest `package.json` of the linted file lists `@ocavue/utils` in `dependencies`, `devDependencies` or `peerDependencies`, or when the file already imports `@ocavue/utils`. Set `checkDependency: false` to run it in every file:
94
+
95
+ ```js
96
+ export default [
97
+ {
98
+ rules: {
99
+ 'ocavue/prefer-noop': ['error', { checkDependency: false }],
100
+ },
101
+ },
102
+ ]
103
+ ```
104
+
105
+ The rule skips functions where the swap could change runtime behavior: `async` functions, generators, named function expressions, methods, and functions with default values or destructuring in their parameters.
106
+
107
+ The fixer adds `noop` to an existing `import { ... } from '@ocavue/utils'`, or adds a new import. It reports without autofixing when the function holds a comment, when another binding named `noop` is in scope, or when the file is not an ES module.
108
+
109
+ ### `prefer-sleep`
110
+
111
+ Enforce `sleep` from [`@ocavue/utils`](https://github.com/ocavue/utils) instead of a `new Promise` that only resolves after `setTimeout`.
112
+
113
+ ```js
114
+ // Incorrect
115
+ await new Promise((resolve) => setTimeout(resolve, 100))
116
+ await new Promise((resolve) => setTimeout(() => resolve(), 100))
117
+
118
+ // Correct (what the fixer produces)
119
+ import { sleep } from '@ocavue/utils'
120
+
121
+ await sleep(100)
122
+ ```
123
+
124
+ The rule also matches `globalThis.setTimeout` and `window.setTimeout`, block bodies, and `new Promise<void>` in TypeScript. It skips promises that resolve with a value, pass extra arguments to `setTimeout`, or compute the delay from the executor's parameters.
125
+
126
+ The rule uses the same `checkDependency` option and import handling as [`prefer-noop`](#prefer-noop).
127
+
53
128
  ### `prefer-while-true`
54
129
 
55
130
  Enforce `while (true)` instead of `for (;;)` for infinite loops. Rules such as [`no-constant-condition`](https://eslint.org/docs/latest/rules/no-constant-condition) (with `checkLoops: "all"`) and [`@typescript-eslint/no-unnecessary-condition`](https://typescript-eslint.io/rules/no-unnecessary-condition) report `while (true)` but not `for (;;)`, so people often rewrite the loop as `for (;;)` to silence them. That only hides the infinite loop from the linter: the code still loops forever. This rule removes that workaround. `while (true)` also reads more clearly than `for (;;)`.
56
131
 
57
132
  ```js
58
133
  // Incorrect
59
- for (;;) {
134
+ while (true) {
60
135
  step()
61
136
  }
62
137
 
package/dist/index.d.ts CHANGED
@@ -6,6 +6,9 @@ declare const plugin: {
6
6
  };
7
7
  rules: {
8
8
  'no-implicit-arrow-linebreak': import("eslint").Rule.RuleModule;
9
+ 'prefer-constant-function': import("eslint").Rule.RuleModule;
10
+ 'prefer-noop': import("eslint").Rule.RuleModule;
11
+ 'prefer-sleep': import("eslint").Rule.RuleModule;
9
12
  'prefer-while-true': import("eslint").Rule.RuleModule;
10
13
  };
11
14
  };
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","names":[],"sources":["../src/index.ts"],"mappings":";cAOM"}
1
+ {"version":3,"file":"index.d.ts","names":[],"sources":["../src/index.ts"],"mappings":";cAUM"}
package/dist/index.js CHANGED
@@ -1,40 +1,438 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ //#region package.json
4
+ var version = "0.3.0";
5
+ //#endregion
6
+ //#region src/rules/no-implicit-arrow-linebreak.ts
7
+ const rule$4 = {
8
+ meta: {
9
+ type: "layout",
10
+ fixable: "code",
11
+ docs: {
12
+ description: "disallow a linebreak between `=>` and an implicit arrow function body, fixing it to a block body with `return`",
13
+ url: "https://github.com/ocavue/eslint-plugin-ocavue#no-implicit-arrow-linebreak"
14
+ },
15
+ messages: { linebreak: "Implicit arrow body must not start on a new line. Use a block body with `return` instead." },
16
+ schema: []
17
+ },
18
+ create(context) {
19
+ const sourceCode = context.sourceCode;
20
+ return { ArrowFunctionExpression(node) {
21
+ if (node.body.type === "BlockStatement") return;
22
+ const arrowToken = sourceCode.getTokenBefore(node.body, { filter: (token) => token.type === "Punctuator" && token.value === "=>" });
23
+ if (!arrowToken) return;
24
+ const firstBodyToken = sourceCode.getTokenAfter(arrowToken);
25
+ const lastToken = sourceCode.getLastToken(node);
26
+ if (!firstBodyToken || !lastToken) return;
27
+ if (arrowToken.loc.end.line === firstBodyToken.loc.start.line) return;
28
+ const canFix = !sourceCode.commentsExistBetween(arrowToken, firstBodyToken);
29
+ context.report({
30
+ node: node.body,
31
+ messageId: "linebreak",
32
+ fix: canFix ? (fixer) => {
33
+ const bodyText = sourceCode.text.slice(firstBodyToken.range[0], lastToken.range[1]);
34
+ return fixer.replaceTextRange([arrowToken.range[1], lastToken.range[1]], ` { return ${bodyText} }`);
35
+ } : void 0
36
+ });
37
+ } };
38
+ }
39
+ };
40
+ //#endregion
41
+ //#region src/utils/constant-function.ts
42
+ function getConstantValue(node) {
43
+ if (node.type === "Literal") {
44
+ switch (node.raw) {
45
+ case "true":
46
+ case "false":
47
+ case "null": return node.raw;
48
+ }
49
+ return;
50
+ }
51
+ if (node.type === "Identifier" && node.name === "undefined") return "undefined";
52
+ if (node.type === "UnaryExpression" && node.operator === "void" && node.argument.type === "Literal") return "undefined";
53
+ }
54
+ function getReturnedValue(node) {
55
+ const body = node.body;
56
+ if (body.type !== "BlockStatement") return getConstantValue(body);
57
+ if (body.body.length === 0) return "undefined";
58
+ if (body.body.length === 1) {
59
+ const statement = body.body[0];
60
+ if (statement.type === "ReturnStatement") return statement.argument ? getConstantValue(statement.argument) : "undefined";
61
+ }
62
+ }
63
+ /**
64
+ * Returns true when the function can be swapped with a function reference
65
+ * without changing runtime behavior.
66
+ */
67
+ function isReplaceable(node) {
68
+ if (node.async || node.generator) return false;
69
+ if (node.typeParameters) return false;
70
+ if (!node.params.every((param) => {
71
+ return param.type === "Identifier" || param.type === "RestElement" && param.argument.type === "Identifier";
72
+ })) return false;
73
+ if (node.type === "FunctionExpression") {
74
+ if (node.id) return false;
75
+ const parent = node.parent;
76
+ if (parent.type === "Property" && (parent.method || parent.kind !== "init")) return false;
77
+ if (parent.type === "MethodDefinition") return false;
78
+ if (parent.type === "NewExpression" && parent.callee === node) return false;
79
+ }
80
+ return true;
81
+ }
82
+ /**
83
+ * Returns the value that an inline function always returns, such as
84
+ * `'true'` for `() => true` or `'undefined'` for `() => {}`. Returns
85
+ * `undefined` when the function does more than that, or when replacing it
86
+ * could change runtime behavior.
87
+ */
88
+ function getConstantFunctionValue(node) {
89
+ return isReplaceable(node) ? getReturnedValue(node) : void 0;
90
+ }
91
+ //#endregion
92
+ //#region src/utils/has-dependency.ts
93
+ const cache = /* @__PURE__ */ new Map();
94
+ function readPackageJson(dir) {
95
+ let text;
96
+ try {
97
+ text = fs.readFileSync(path.join(dir, "package.json"), "utf8");
98
+ } catch {
99
+ return;
100
+ }
101
+ try {
102
+ const json = JSON.parse(text);
103
+ return json && typeof json === "object" ? json : {};
104
+ } catch {
105
+ return {};
106
+ }
107
+ }
108
+ function findNearestPackageJson(startDir) {
109
+ const visited = [];
110
+ let dir = startDir;
111
+ let result = null;
112
+ while (true) {
113
+ const cached = cache.get(dir);
114
+ if (cached !== void 0) {
115
+ result = cached;
116
+ break;
117
+ }
118
+ visited.push(dir);
119
+ const pkg = readPackageJson(dir);
120
+ if (pkg) {
121
+ result = pkg;
122
+ break;
123
+ }
124
+ const parent = path.dirname(dir);
125
+ if (parent === dir) break;
126
+ dir = parent;
127
+ }
128
+ for (const visitedDir of visited) cache.set(visitedDir, result);
129
+ return result;
130
+ }
131
+ /**
132
+ * Returns true when the nearest `package.json` of `filename` lists
133
+ * `packageName` in `dependencies`, `devDependencies` or `peerDependencies`.
134
+ *
135
+ * A package never depends on itself, so this returns false inside the
136
+ * package named `packageName`.
137
+ */
138
+ function hasDependency(filename, packageName) {
139
+ if (!path.isAbsolute(filename)) return false;
140
+ const pkg = findNearestPackageJson(path.dirname(filename));
141
+ if (!pkg || pkg.name === packageName) return false;
142
+ return [
143
+ pkg.dependencies,
144
+ pkg.devDependencies,
145
+ pkg.peerDependencies
146
+ ].some((deps) => deps != null && typeof deps === "object" && Object.hasOwn(deps, packageName));
147
+ }
148
+ //#endregion
149
+ //#region src/utils/package-import.ts
150
+ const UTILS_PACKAGE = "@ocavue/utils";
151
+ const packageRuleSchema = [{
152
+ type: "object",
153
+ properties: { checkDependency: { type: "boolean" } },
154
+ additionalProperties: false
155
+ }];
156
+ function getImportKind(node) {
157
+ return node.importKind;
158
+ }
159
+ function getImportedName(specifier) {
160
+ const imported = specifier.imported;
161
+ return imported.type === "Identifier" ? imported.name : String(imported.value);
162
+ }
163
+ /**
164
+ * Returns the value `import` declarations of `packageName` in the program.
165
+ */
166
+ function getPackageImports(program, packageName) {
167
+ return program.body.filter((statement) => {
168
+ return statement.type === "ImportDeclaration" && statement.source.value === packageName && getImportKind(statement) !== "type";
169
+ });
170
+ }
171
+ /**
172
+ * Returns true when the linted file can import from `packageName`: the
173
+ * nearest `package.json` depends on it, or the file already imports it. The
174
+ * `checkDependency: false` option skips the check.
175
+ */
176
+ function isPackageAvailable(context, packageName) {
177
+ if ((context.options[0] ?? {}).checkDependency === false) return true;
178
+ return hasDependency(context.physicalFilename, packageName) || getPackageImports(context.sourceCode.ast, packageName).length > 0;
179
+ }
180
+ function findVariable(scope, name) {
181
+ for (let current = scope; current; current = current.upper) {
182
+ const variable = current.set.get(name);
183
+ if (variable) return variable;
184
+ }
185
+ }
186
+ /**
187
+ * Creates a fix that replaces `node` and makes sure `name` is imported from
188
+ * `packageName`. Returns `undefined` when the fix is not safe: the file is
189
+ * not an ES module, the node holds comments, or another binding already uses
190
+ * the name.
191
+ */
192
+ function createImportFix({ context, node, name, packageName, replace }) {
193
+ const sourceCode = context.sourceCode;
194
+ const program = sourceCode.ast;
195
+ if (context.languageOptions.sourceType !== "module") return;
196
+ if (sourceCode.getCommentsInside(node).length > 0) return;
197
+ const scope = sourceCode.getScope(node);
198
+ const packageImports = getPackageImports(program, packageName);
199
+ for (const declaration of packageImports) for (const specifier of declaration.specifiers) {
200
+ if (specifier.type !== "ImportSpecifier" || getImportKind(specifier) === "type" || getImportedName(specifier) !== name) continue;
201
+ const local = specifier.local.name;
202
+ if (findVariable(scope, local)?.defs[0]?.node === specifier) return (fixer) => fixer.replaceText(node, replace(local));
203
+ }
204
+ if (findVariable(scope, name)) return;
205
+ if (sourceCode.scopeManager.globalScope?.through.some((reference) => reference.identifier.name === name)) return;
206
+ const replacement = replace(name);
207
+ const namedImport = packageImports.find((declaration) => {
208
+ return declaration.specifiers.some((specifier) => specifier.type === "ImportSpecifier");
209
+ });
210
+ if (namedImport) {
211
+ const lastSpecifier = namedImport.specifiers.at(-1);
212
+ return (fixer) => [fixer.replaceText(node, replacement), fixer.insertTextAfter(lastSpecifier, `, ${name}`)];
213
+ }
214
+ const importText = `import { ${name} } from '${packageName}'`;
215
+ const lastImport = program.body.findLast((statement) => statement.type === "ImportDeclaration");
216
+ if (lastImport) {
217
+ const semicolon = sourceCode.getText(lastImport).endsWith(";") ? ";" : "";
218
+ return (fixer) => [fixer.replaceText(node, replacement), fixer.insertTextAfter(lastImport, `\n${importText}${semicolon}`)];
219
+ }
220
+ const firstStatement = program.body.find((statement) => {
221
+ return !(statement.type === "ExpressionStatement" && typeof statement.directive === "string");
222
+ });
223
+ if (!firstStatement) return;
224
+ return (fixer) => [fixer.replaceText(node, replacement), fixer.insertTextBefore(firstStatement, `${importText}\n`)];
225
+ }
226
+ //#endregion
227
+ //#region src/rules/prefer-constant-function.ts
228
+ const FUNCTION_NAMES = {
229
+ true: "returnTrue",
230
+ false: "returnFalse",
231
+ null: "returnNull"
232
+ };
233
+ const rule$3 = {
234
+ meta: {
235
+ type: "suggestion",
236
+ hasSuggestions: true,
237
+ docs: {
238
+ description: "enforce `returnTrue`, `returnFalse` and `returnNull` from `@ocavue/utils` instead of inline functions",
239
+ url: "https://github.com/ocavue/eslint-plugin-ocavue#prefer-constant-function"
240
+ },
241
+ messages: {
242
+ preferConstantFunction: "Use `{{name}}` from `@ocavue/utils` instead of an inline function.",
243
+ replaceWithConstantFunction: "Replace with `{{name}}` from `@ocavue/utils`."
244
+ },
245
+ schema: packageRuleSchema
246
+ },
247
+ create(context) {
248
+ if (!isPackageAvailable(context, "@ocavue/utils")) return {};
249
+ function check(node) {
250
+ const value = getConstantFunctionValue(node);
251
+ const name = value && FUNCTION_NAMES[value];
252
+ if (!name) return;
253
+ const fix = createImportFix({
254
+ context,
255
+ node,
256
+ name,
257
+ packageName: UTILS_PACKAGE,
258
+ replace: (local) => local
259
+ });
260
+ context.report({
261
+ node,
262
+ messageId: "preferConstantFunction",
263
+ data: { name },
264
+ suggest: fix ? [{
265
+ messageId: "replaceWithConstantFunction",
266
+ data: { name },
267
+ fix
268
+ }] : []
269
+ });
270
+ }
271
+ return {
272
+ ArrowFunctionExpression: check,
273
+ FunctionExpression: check
274
+ };
275
+ }
276
+ };
277
+ //#endregion
278
+ //#region src/rules/prefer-noop.ts
279
+ const rule$2 = {
280
+ meta: {
281
+ type: "suggestion",
282
+ fixable: "code",
283
+ docs: {
284
+ description: "enforce `noop` from `@ocavue/utils` instead of inline empty functions",
285
+ url: "https://github.com/ocavue/eslint-plugin-ocavue#prefer-noop"
286
+ },
287
+ messages: { preferNoop: "Use `noop` from `@ocavue/utils` instead of an inline empty function." },
288
+ schema: packageRuleSchema
289
+ },
290
+ create(context) {
291
+ if (!isPackageAvailable(context, "@ocavue/utils")) return {};
292
+ function check(node) {
293
+ if (getConstantFunctionValue(node) !== "undefined") return;
294
+ context.report({
295
+ node,
296
+ messageId: "preferNoop",
297
+ fix: createImportFix({
298
+ context,
299
+ node,
300
+ name: "noop",
301
+ packageName: UTILS_PACKAGE,
302
+ replace: (local) => local
303
+ })
304
+ });
305
+ }
306
+ return {
307
+ ArrowFunctionExpression: check,
308
+ FunctionExpression: check
309
+ };
310
+ }
311
+ };
312
+ //#endregion
313
+ //#region src/rules/prefer-sleep.ts
314
+ function isFunction(node) {
315
+ return node.type === "ArrowFunctionExpression" || node.type === "FunctionExpression";
316
+ }
317
+ /**
318
+ * Returns true when `name` refers to a global variable, not a local binding.
319
+ */
320
+ function isGlobal(scope, name) {
321
+ const variable = findVariable(scope, name);
322
+ return !variable || variable.defs.length === 0;
323
+ }
324
+ /**
325
+ * Returns the only expression that a function body evaluates, for bodies
326
+ * such as `x`, `{ x }` and `{ return x }`.
327
+ */
328
+ function getOnlyExpression(node) {
329
+ const body = node.body;
330
+ if (body.type !== "BlockStatement") return body;
331
+ if (body.body.length !== 1) return;
332
+ const statement = body.body[0];
333
+ if (statement.type === "ExpressionStatement") return statement.expression;
334
+ if (statement.type === "ReturnStatement" && statement.argument) return statement.argument;
335
+ }
336
+ function isIdentifier(node, name) {
337
+ return node.type === "Identifier" && node.name === name;
338
+ }
339
+ /**
340
+ * Returns true for `setTimeout`, `globalThis.setTimeout` and
341
+ * `window.setTimeout`.
342
+ */
343
+ function isSetTimeout(node, scope) {
344
+ if (node.type === "Identifier") return node.name === "setTimeout" && isGlobal(scope, "setTimeout");
345
+ return node.type === "MemberExpression" && !node.computed && isIdentifier(node.property, "setTimeout") && node.object.type === "Identifier" && (node.object.name === "globalThis" || node.object.name === "window") && isGlobal(scope, node.object.name);
346
+ }
347
+ /**
348
+ * Returns true for `resolve` and `() => resolve()`.
349
+ */
350
+ function isResolveCallback(node, resolve) {
351
+ if (isIdentifier(node, resolve)) return true;
352
+ if (!isFunction(node) || node.async || node.generator || node.params.length > 0) return false;
353
+ const expression = getOnlyExpression(node);
354
+ return expression?.type === "CallExpression" && isIdentifier(expression.callee, resolve) && expression.arguments.length === 0;
355
+ }
356
+ /**
357
+ * Returns true for no type argument, `<void>` and `<undefined>`.
358
+ */
359
+ function hasVoidTypeArgument(node) {
360
+ const typeArguments = node.typeArguments;
361
+ if (!typeArguments) return true;
362
+ return typeArguments.params.length === 1 && (typeArguments.params[0].type === "TSVoidKeyword" || typeArguments.params[0].type === "TSUndefinedKeyword");
363
+ }
1
364
  //#endregion
2
365
  //#region src/index.ts
3
366
  const plugin = {
4
367
  meta: {
5
368
  name: "eslint-plugin-ocavue",
6
- version: "0.2.0"
369
+ version
7
370
  },
8
371
  rules: {
9
- "no-implicit-arrow-linebreak": {
372
+ "no-implicit-arrow-linebreak": rule$4,
373
+ "prefer-constant-function": rule$3,
374
+ "prefer-noop": rule$2,
375
+ "prefer-sleep": {
10
376
  meta: {
11
- type: "layout",
377
+ type: "suggestion",
12
378
  fixable: "code",
13
379
  docs: {
14
- description: "disallow a linebreak between `=>` and an implicit arrow function body, fixing it to a block body with `return`",
15
- url: "https://github.com/ocavue/eslint-plugin-ocavue#no-implicit-arrow-linebreak"
380
+ description: "enforce `sleep` from `@ocavue/utils` instead of `new Promise` with `setTimeout`",
381
+ url: "https://github.com/ocavue/eslint-plugin-ocavue#prefer-sleep"
16
382
  },
17
- messages: { linebreak: "Implicit arrow body must not start on a new line. Use a block body with `return` instead." },
18
- schema: []
383
+ messages: { preferSleep: "Use `sleep` from `@ocavue/utils` instead of `new Promise` with `setTimeout`." },
384
+ schema: packageRuleSchema
19
385
  },
20
386
  create(context) {
387
+ if (!isPackageAvailable(context, "@ocavue/utils")) return {};
21
388
  const sourceCode = context.sourceCode;
22
- return { ArrowFunctionExpression(node) {
23
- if (node.body.type === "BlockStatement") return;
24
- const arrowToken = sourceCode.getTokenBefore(node.body, { filter: (token) => token.type === "Punctuator" && token.value === "=>" });
25
- if (!arrowToken) return;
26
- const firstBodyToken = sourceCode.getTokenAfter(arrowToken);
27
- const lastToken = sourceCode.getLastToken(node);
28
- if (!firstBodyToken || !lastToken) return;
29
- if (arrowToken.loc.end.line === firstBodyToken.loc.start.line) return;
30
- const canFix = !sourceCode.commentsExistBetween(arrowToken, firstBodyToken);
389
+ /**
390
+ * Returns the delay expression of `new Promise((resolve) =>
391
+ * setTimeout(resolve, delay))`, or `undefined` when the node does not
392
+ * match.
393
+ */
394
+ function getDelay(node) {
395
+ if (node.type !== "NewExpression" || !isIdentifier(node.callee, "Promise") || node.arguments.length !== 1 || !hasVoidTypeArgument(node)) return;
396
+ if (!isGlobal(sourceCode.getScope(node), "Promise")) return;
397
+ const executor = node.arguments[0];
398
+ if (!isFunction(executor) || executor.async || executor.generator) return;
399
+ if (!executor.params.every((param) => param.type === "Identifier")) return;
400
+ const resolveParam = executor.params[0];
401
+ if (resolveParam?.type !== "Identifier") return;
402
+ const call = getOnlyExpression(executor);
403
+ if (call?.type !== "CallExpression" || call.optional) return;
404
+ const executorScope = sourceCode.getScope(executor);
405
+ if (!isSetTimeout(call.callee, executorScope)) return;
406
+ const [callback, delay] = call.arguments;
407
+ if (call.arguments.length !== 2 || !delay || delay.type === "SpreadElement") return;
408
+ if (!isResolveCallback(callback, resolveParam.name)) return;
409
+ if (executorScope.variables.some((variable) => {
410
+ return variable.references.some((reference) => {
411
+ const range = reference.identifier.range;
412
+ return range[0] >= delay.range[0] && range[1] <= delay.range[1];
413
+ });
414
+ })) return;
415
+ if (executor.type === "FunctionExpression" && sourceCode.getTokens(delay).some((token) => {
416
+ return token.value === "this" || token.value === "arguments";
417
+ })) return;
418
+ return delay;
419
+ }
420
+ return { NewExpression(node) {
421
+ const delay = getDelay(node);
422
+ if (!delay) return;
423
+ const delayText = sourceCode.getText(delay);
31
424
  context.report({
32
- node: node.body,
33
- messageId: "linebreak",
34
- fix: canFix ? (fixer) => {
35
- const bodyText = sourceCode.text.slice(firstBodyToken.range[0], lastToken.range[1]);
36
- return fixer.replaceTextRange([arrowToken.range[1], lastToken.range[1]], ` { return ${bodyText} }`);
37
- } : void 0
425
+ node,
426
+ messageId: "preferSleep",
427
+ fix: createImportFix({
428
+ context,
429
+ node,
430
+ name: "sleep",
431
+ packageName: UTILS_PACKAGE,
432
+ replace: (local) => {
433
+ return delay.type === "SequenceExpression" ? `${local}((${delayText}))` : `${local}(${delayText})`;
434
+ }
435
+ })
38
436
  });
39
437
  } };
40
438
  }