eslint-plugin-ocavue 0.1.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 +97 -3
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +464 -31
- package/dist/index.js.map +1 -1
- package/package.json +9 -7
- package/src/index.ts +8 -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/rules/prefer-while-true.test.ts +43 -0
- package/src/rules/prefer-while-true.ts +52 -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
package/README.md
CHANGED
|
@@ -21,6 +21,10 @@ 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',
|
|
27
|
+
'ocavue/prefer-while-true': 'error',
|
|
24
28
|
},
|
|
25
29
|
},
|
|
26
30
|
]
|
|
@@ -34,9 +38,9 @@ Disallow a linebreak between `=>` and an implicit arrow function body. Unlike [`
|
|
|
34
38
|
|
|
35
39
|
```js
|
|
36
40
|
// Incorrect
|
|
37
|
-
const items = Array.from({ length: count }, (_, index) =>
|
|
38
|
-
createItem(index === 0 ? 'first item label' : `item number ${index}`)
|
|
39
|
-
)
|
|
41
|
+
const items = Array.from({ length: count }, (_, index) => {
|
|
42
|
+
return createItem(index === 0 ? 'first item label' : `item number ${index}`)
|
|
43
|
+
})
|
|
40
44
|
|
|
41
45
|
// Correct
|
|
42
46
|
const short = (index) => createItem(`item number ${index}`)
|
|
@@ -49,6 +53,96 @@ const items = Array.from({ length: count }, (_, index) => {
|
|
|
49
53
|
|
|
50
54
|
When a comment sits between `=>` and the body, the rule reports without autofixing so the fix never drops a comment.
|
|
51
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
|
+
|
|
128
|
+
### `prefer-while-true`
|
|
129
|
+
|
|
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 (;;)`.
|
|
131
|
+
|
|
132
|
+
```js
|
|
133
|
+
// Incorrect
|
|
134
|
+
while (true) {
|
|
135
|
+
step()
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Correct (what the fixer produces)
|
|
139
|
+
while (true) {
|
|
140
|
+
step()
|
|
141
|
+
}
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
When a comment sits inside the loop head, the rule reports without autofixing so the fix never drops a comment.
|
|
145
|
+
|
|
52
146
|
## Sponsors
|
|
53
147
|
|
|
54
148
|
<p align="center">
|
package/dist/index.d.ts
CHANGED
|
@@ -6,6 +6,10 @@ 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;
|
|
12
|
+
'prefer-while-true': import("eslint").Rule.RuleModule;
|
|
9
13
|
};
|
|
10
14
|
};
|
|
11
15
|
//#endregion
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","names":[],"sources":["../src/index.ts"],"mappings":";
|
|
1
|
+
{"version":3,"file":"index.d.ts","names":[],"sources":["../src/index.ts"],"mappings":";cAUM"}
|
package/dist/index.js
CHANGED
|
@@ -1,43 +1,476 @@
|
|
|
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
|
|
369
|
+
version
|
|
7
370
|
},
|
|
8
|
-
rules: {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
371
|
+
rules: {
|
|
372
|
+
"no-implicit-arrow-linebreak": rule$4,
|
|
373
|
+
"prefer-constant-function": rule$3,
|
|
374
|
+
"prefer-noop": rule$2,
|
|
375
|
+
"prefer-sleep": {
|
|
376
|
+
meta: {
|
|
377
|
+
type: "suggestion",
|
|
378
|
+
fixable: "code",
|
|
379
|
+
docs: {
|
|
380
|
+
description: "enforce `sleep` from `@ocavue/utils` instead of `new Promise` with `setTimeout`",
|
|
381
|
+
url: "https://github.com/ocavue/eslint-plugin-ocavue#prefer-sleep"
|
|
382
|
+
},
|
|
383
|
+
messages: { preferSleep: "Use `sleep` from `@ocavue/utils` instead of `new Promise` with `setTimeout`." },
|
|
384
|
+
schema: packageRuleSchema
|
|
15
385
|
},
|
|
16
|
-
|
|
17
|
-
|
|
386
|
+
create(context) {
|
|
387
|
+
if (!isPackageAvailable(context, "@ocavue/utils")) return {};
|
|
388
|
+
const sourceCode = context.sourceCode;
|
|
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);
|
|
424
|
+
context.report({
|
|
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
|
+
})
|
|
436
|
+
});
|
|
437
|
+
} };
|
|
438
|
+
}
|
|
18
439
|
},
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
440
|
+
"prefer-while-true": {
|
|
441
|
+
meta: {
|
|
442
|
+
type: "suggestion",
|
|
443
|
+
fixable: "code",
|
|
444
|
+
docs: {
|
|
445
|
+
description: "enforce `while (true)` instead of `for (;;)` for infinite loops",
|
|
446
|
+
url: "https://github.com/ocavue/eslint-plugin-ocavue#prefer-while-true"
|
|
447
|
+
},
|
|
448
|
+
messages: { preferWhileTrue: "Use `while (true)` instead of `for (;;)`." },
|
|
449
|
+
schema: []
|
|
450
|
+
},
|
|
451
|
+
create(context) {
|
|
452
|
+
const sourceCode = context.sourceCode;
|
|
453
|
+
return { ForStatement(node) {
|
|
454
|
+
if (node.init || node.test || node.update) return;
|
|
455
|
+
const forToken = sourceCode.getFirstToken(node);
|
|
456
|
+
const closingParen = sourceCode.getTokenBefore(node.body);
|
|
457
|
+
if (!forToken || !closingParen) return;
|
|
458
|
+
const canFix = !sourceCode.commentsExistBetween(forToken, closingParen);
|
|
459
|
+
context.report({
|
|
460
|
+
node,
|
|
461
|
+
loc: {
|
|
462
|
+
start: forToken.loc.start,
|
|
463
|
+
end: closingParen.loc.end
|
|
464
|
+
},
|
|
465
|
+
messageId: "preferWhileTrue",
|
|
466
|
+
fix: canFix ? (fixer) => {
|
|
467
|
+
return fixer.replaceTextRange([forToken.range[0], closingParen.range[1]], "while (true)");
|
|
468
|
+
} : void 0
|
|
469
|
+
});
|
|
470
|
+
} };
|
|
471
|
+
}
|
|
39
472
|
}
|
|
40
|
-
}
|
|
473
|
+
}
|
|
41
474
|
};
|
|
42
475
|
//#endregion
|
|
43
476
|
export { plugin as default };
|