eslint-plugin-ocavue 0.0.1 → 0.2.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 +53 -25
- package/dist/index.d.ts +11 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +76 -3
- package/dist/index.js.map +1 -1
- package/package.json +19 -13
- package/src/index.ts +19 -2
- package/src/rules/no-implicit-arrow-linebreak.test.ts +53 -0
- package/src/rules/no-implicit-arrow-linebreak.ts +65 -0
- package/src/rules/prefer-while-true.test.ts +43 -0
- package/src/rules/prefer-while-true.ts +52 -0
- package/src/index.test.ts +0 -7
package/README.md
CHANGED
|
@@ -2,43 +2,71 @@
|
|
|
2
2
|
|
|
3
3
|
[](https://www.npmjs.com/package/eslint-plugin-ocavue)
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
Custom [ESLint](https://eslint.org/) rules from [ocavue](https://github.com/ocavue).
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
## Usage
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
Requires ESLint 10 with flat config.
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
|
|
11
|
+
```sh
|
|
12
|
+
pnpm add -D eslint-plugin-ocavue
|
|
13
|
+
```
|
|
13
14
|
|
|
14
|
-
|
|
15
|
+
```js
|
|
16
|
+
// eslint.config.js
|
|
17
|
+
import ocavue from 'eslint-plugin-ocavue'
|
|
15
18
|
|
|
16
|
-
|
|
19
|
+
export default [
|
|
20
|
+
{
|
|
21
|
+
plugins: { ocavue },
|
|
22
|
+
rules: {
|
|
23
|
+
'ocavue/no-implicit-arrow-linebreak': 'error',
|
|
24
|
+
'ocavue/prefer-while-true': 'error',
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
]
|
|
28
|
+
```
|
|
17
29
|
|
|
18
|
-
|
|
19
|
-
| ---------------- | -------------------------------------------------------------------------------------------------------------------------- |
|
|
20
|
-
| `pnpm dev` | Rebuild on change (`tsdown --watch`) |
|
|
21
|
-
| `pnpm build` | Bundle the library to `dist/` |
|
|
22
|
-
| `pnpm test` | Run tests with [Vitest](https://vitest.dev/) |
|
|
23
|
-
| `pnpm lint` | Lint with [ESLint](https://eslint.org/), [oxfmt](https://oxc.rs/docs/guide/usage/formatter), and [knip](https://knip.dev/) |
|
|
24
|
-
| `pnpm fix` | Auto-fix formatting and lint issues |
|
|
25
|
-
| `pnpm typecheck` | Type-check with `tsc` |
|
|
30
|
+
## Rules
|
|
26
31
|
|
|
27
|
-
|
|
32
|
+
### `no-implicit-arrow-linebreak`
|
|
28
33
|
|
|
29
|
-
|
|
34
|
+
Disallow a linebreak between `=>` and an implicit arrow function body. Unlike [`@stylistic/implicit-arrow-linebreak`](https://eslint.style/rules/implicit-arrow-linebreak), whose fixer joins the two lines into one (which Prettier then breaks again whenever the joined line exceeds `printWidth`, so `eslint --fix` and the formatter never converge), this rule's fixer converts the body to a block body with `return`. A block body is a stable shape for Prettier and oxfmt, so one round of `eslint --fix` plus the formatter converges.
|
|
30
35
|
|
|
31
|
-
|
|
36
|
+
```js
|
|
37
|
+
// Incorrect
|
|
38
|
+
const items = Array.from({ length: count }, (_, index) =>
|
|
39
|
+
createItem(index === 0 ? 'first item label' : `item number ${index}`),
|
|
40
|
+
)
|
|
32
41
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
pnpm build
|
|
36
|
-
pnpm publish
|
|
37
|
-
```
|
|
42
|
+
// Correct
|
|
43
|
+
const short = (index) => createItem(`item number ${index}`)
|
|
38
44
|
|
|
39
|
-
|
|
45
|
+
// Correct (what the fixer produces, after formatting)
|
|
46
|
+
const items = Array.from({ length: count }, (_, index) => {
|
|
47
|
+
return createItem(index === 0 ? 'first item label' : `item number ${index}`)
|
|
48
|
+
})
|
|
49
|
+
```
|
|
40
50
|
|
|
41
|
-
|
|
51
|
+
When a comment sits between `=>` and the body, the rule reports without autofixing so the fix never drops a comment.
|
|
52
|
+
|
|
53
|
+
### `prefer-while-true`
|
|
54
|
+
|
|
55
|
+
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
|
+
|
|
57
|
+
```js
|
|
58
|
+
// Incorrect
|
|
59
|
+
for (;;) {
|
|
60
|
+
step()
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Correct (what the fixer produces)
|
|
64
|
+
while (true) {
|
|
65
|
+
step()
|
|
66
|
+
}
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
When a comment sits inside the loop head, the rule reports without autofixing so the fix never drops a comment.
|
|
42
70
|
|
|
43
71
|
## Sponsors
|
|
44
72
|
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,14 @@
|
|
|
1
1
|
//#region src/index.d.ts
|
|
2
|
-
declare const
|
|
3
|
-
|
|
2
|
+
declare const plugin: {
|
|
3
|
+
meta: {
|
|
4
|
+
name: string;
|
|
5
|
+
version: string;
|
|
6
|
+
};
|
|
7
|
+
rules: {
|
|
8
|
+
'no-implicit-arrow-linebreak': import("eslint").Rule.RuleModule;
|
|
9
|
+
'prefer-while-true': import("eslint").Rule.RuleModule;
|
|
10
|
+
};
|
|
11
|
+
};
|
|
4
12
|
//#endregion
|
|
5
|
-
export {
|
|
13
|
+
export { plugin as default };
|
|
6
14
|
//# sourceMappingURL=index.d.ts.map
|
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":";cAOM"}
|
package/dist/index.js
CHANGED
|
@@ -1,7 +1,80 @@
|
|
|
1
|
+
//#endregion
|
|
1
2
|
//#region src/index.ts
|
|
2
|
-
const
|
|
3
|
-
|
|
3
|
+
const plugin = {
|
|
4
|
+
meta: {
|
|
5
|
+
name: "eslint-plugin-ocavue",
|
|
6
|
+
version: "0.2.0"
|
|
7
|
+
},
|
|
8
|
+
rules: {
|
|
9
|
+
"no-implicit-arrow-linebreak": {
|
|
10
|
+
meta: {
|
|
11
|
+
type: "layout",
|
|
12
|
+
fixable: "code",
|
|
13
|
+
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"
|
|
16
|
+
},
|
|
17
|
+
messages: { linebreak: "Implicit arrow body must not start on a new line. Use a block body with `return` instead." },
|
|
18
|
+
schema: []
|
|
19
|
+
},
|
|
20
|
+
create(context) {
|
|
21
|
+
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);
|
|
31
|
+
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
|
|
38
|
+
});
|
|
39
|
+
} };
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
"prefer-while-true": {
|
|
43
|
+
meta: {
|
|
44
|
+
type: "suggestion",
|
|
45
|
+
fixable: "code",
|
|
46
|
+
docs: {
|
|
47
|
+
description: "enforce `while (true)` instead of `for (;;)` for infinite loops",
|
|
48
|
+
url: "https://github.com/ocavue/eslint-plugin-ocavue#prefer-while-true"
|
|
49
|
+
},
|
|
50
|
+
messages: { preferWhileTrue: "Use `while (true)` instead of `for (;;)`." },
|
|
51
|
+
schema: []
|
|
52
|
+
},
|
|
53
|
+
create(context) {
|
|
54
|
+
const sourceCode = context.sourceCode;
|
|
55
|
+
return { ForStatement(node) {
|
|
56
|
+
if (node.init || node.test || node.update) return;
|
|
57
|
+
const forToken = sourceCode.getFirstToken(node);
|
|
58
|
+
const closingParen = sourceCode.getTokenBefore(node.body);
|
|
59
|
+
if (!forToken || !closingParen) return;
|
|
60
|
+
const canFix = !sourceCode.commentsExistBetween(forToken, closingParen);
|
|
61
|
+
context.report({
|
|
62
|
+
node,
|
|
63
|
+
loc: {
|
|
64
|
+
start: forToken.loc.start,
|
|
65
|
+
end: closingParen.loc.end
|
|
66
|
+
},
|
|
67
|
+
messageId: "preferWhileTrue",
|
|
68
|
+
fix: canFix ? (fixer) => {
|
|
69
|
+
return fixer.replaceTextRange([forToken.range[0], closingParen.range[1]], "while (true)");
|
|
70
|
+
} : void 0
|
|
71
|
+
});
|
|
72
|
+
} };
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
};
|
|
4
77
|
//#endregion
|
|
5
|
-
export {
|
|
78
|
+
export { plugin as default };
|
|
6
79
|
|
|
7
80
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":[],"sources":["../src/index.ts"],"sourcesContent":["
|
|
1
|
+
{"version":3,"file":"index.js","names":["rule","noImplicitArrowLinebreak","preferWhileTrue"],"sources":["../package.json","../src/rules/no-implicit-arrow-linebreak.ts","../src/rules/prefer-while-true.ts","../src/index.ts"],"sourcesContent":["","import type { Rule } from 'eslint'\n\nconst rule: Rule.RuleModule = {\n meta: {\n type: 'layout',\n fixable: 'code',\n docs: {\n description:\n 'disallow a linebreak between `=>` and an implicit arrow function body, fixing it to a block body with `return`',\n url: 'https://github.com/ocavue/eslint-plugin-ocavue#no-implicit-arrow-linebreak',\n },\n messages: {\n linebreak:\n 'Implicit arrow body must not start on a new line. Use a block body with `return` instead.',\n },\n schema: [],\n },\n create(context) {\n const sourceCode = context.sourceCode\n return {\n ArrowFunctionExpression(node) {\n if (node.body.type === 'BlockStatement') {\n return\n }\n\n const arrowToken = sourceCode.getTokenBefore(node.body, {\n filter: (token) => token.type === 'Punctuator' && token.value === '=>',\n })\n if (!arrowToken) {\n return\n }\n const firstBodyToken = sourceCode.getTokenAfter(arrowToken)\n // The last token of the arrow function. When the body is wrapped in\n // parentheses, this is the closing `)`, so the fix keeps the parens.\n const lastToken = sourceCode.getLastToken(node)\n if (!firstBodyToken || !lastToken) {\n return\n }\n if (arrowToken.loc.end.line === firstBodyToken.loc.start.line) {\n return\n }\n\n // The fix rewrites the region between `=>` and the body, so bail out\n // of fixing (but still report) if that region contains comments.\n const canFix = !sourceCode.commentsExistBetween(arrowToken, firstBodyToken)\n\n context.report({\n node: node.body,\n messageId: 'linebreak',\n fix: canFix\n ? (fixer) => {\n const bodyText = sourceCode.text.slice(firstBodyToken.range[0], lastToken.range[1])\n return fixer.replaceTextRange(\n [arrowToken.range[1], lastToken.range[1]],\n ` { return ${bodyText} }`,\n )\n }\n : undefined,\n })\n },\n }\n },\n}\n\nexport default rule\n","import type { Rule } from 'eslint'\n\nconst rule: Rule.RuleModule = {\n meta: {\n type: 'suggestion',\n fixable: 'code',\n docs: {\n description: 'enforce `while (true)` instead of `for (;;)` for infinite loops',\n url: 'https://github.com/ocavue/eslint-plugin-ocavue#prefer-while-true',\n },\n messages: {\n preferWhileTrue: 'Use `while (true)` instead of `for (;;)`.',\n },\n schema: [],\n },\n create(context) {\n const sourceCode = context.sourceCode\n return {\n ForStatement(node) {\n if (node.init || node.test || node.update) {\n return\n }\n\n const forToken = sourceCode.getFirstToken(node)\n const closingParen = sourceCode.getTokenBefore(node.body)\n if (!forToken || !closingParen) {\n return\n }\n\n // Bail out of fixing (but still report) if the loop head contains\n // comments, so the fix never drops a comment.\n const canFix = !sourceCode.commentsExistBetween(forToken, closingParen)\n\n context.report({\n node,\n loc: { start: forToken.loc.start, end: closingParen.loc.end },\n messageId: 'preferWhileTrue',\n fix: canFix\n ? (fixer) => {\n return fixer.replaceTextRange(\n [forToken.range[0], closingParen.range[1]],\n 'while (true)',\n )\n }\n : undefined,\n })\n },\n }\n },\n}\n\nexport default rule\n","import type { ESLint } from 'eslint'\n\nimport { version } from '../package.json'\n\nimport noImplicitArrowLinebreak from './rules/no-implicit-arrow-linebreak'\nimport preferWhileTrue from './rules/prefer-while-true'\n\nconst plugin = {\n meta: {\n name: 'eslint-plugin-ocavue',\n version,\n },\n rules: {\n 'no-implicit-arrow-linebreak': noImplicitArrowLinebreak,\n 'prefer-while-true': preferWhileTrue,\n },\n} satisfies ESLint.Plugin\n\nexport default plugin\n"],"mappings":";;AGOA,MAAM,SAAS;CACb,MAAM;EACJ,MAAM;EACN;CACF;CACA,OAAO;EACL,+BAA+BC;GFVjC,MAAM;IACJ,MAAM;IACN,SAAS;IACT,MAAM;KACJ,aACE;KACF,KAAK;IACP;IACA,UAAU,EACR,WACE,4FACJ;IACA,QAAQ,CAAC;GACX;GACA,OAAO,SAAS;IACd,MAAM,aAAa,QAAQ;IAC3B,OAAO,EACL,wBAAwB,MAAM;KAC5B,IAAI,KAAK,KAAK,SAAS,kBACrB;KAGF,MAAM,aAAa,WAAW,eAAe,KAAK,MAAM,EACtD,SAAS,UAAU,MAAM,SAAS,gBAAgB,MAAM,UAAU,KACpE,CAAC;KACD,IAAI,CAAC,YACH;KAEF,MAAM,iBAAiB,WAAW,cAAc,UAAU;KAG1D,MAAM,YAAY,WAAW,aAAa,IAAI;KAC9C,IAAI,CAAC,kBAAkB,CAAC,WACtB;KAEF,IAAI,WAAW,IAAI,IAAI,SAAS,eAAe,IAAI,MAAM,MACvD;KAKF,MAAM,SAAS,CAAC,WAAW,qBAAqB,YAAY,cAAc;KAE1E,QAAQ,OAAO;MACb,MAAM,KAAK;MACX,WAAW;MACX,KAAK,UACA,UAAU;OACT,MAAM,WAAW,WAAW,KAAK,MAAM,eAAe,MAAM,IAAI,UAAU,MAAM,EAAE;OAClF,OAAO,MAAM,iBACX,CAAC,WAAW,MAAM,IAAI,UAAU,MAAM,EAAE,GACxC,aAAa,SAAS,GACxB;MACF,IACA,KAAA;KACN,CAAC;IACH,EACF;GACF;EEhDiCA;EAC/B,qBAAqBC;GDXvB,MAAM;IACJ,MAAM;IACN,SAAS;IACT,MAAM;KACJ,aAAa;KACb,KAAK;IACP;IACA,UAAU,EACR,iBAAiB,4CACnB;IACA,QAAQ,CAAC;GACX;GACA,OAAO,SAAS;IACd,MAAM,aAAa,QAAQ;IAC3B,OAAO,EACL,aAAa,MAAM;KACjB,IAAI,KAAK,QAAQ,KAAK,QAAQ,KAAK,QACjC;KAGF,MAAM,WAAW,WAAW,cAAc,IAAI;KAC9C,MAAM,eAAe,WAAW,eAAe,KAAK,IAAI;KACxD,IAAI,CAAC,YAAY,CAAC,cAChB;KAKF,MAAM,SAAS,CAAC,WAAW,qBAAqB,UAAU,YAAY;KAEtE,QAAQ,OAAO;MACb;MACA,KAAK;OAAE,OAAO,SAAS,IAAI;OAAO,KAAK,aAAa,IAAI;MAAI;MAC5D,WAAW;MACX,KAAK,UACA,UAAU;OACT,OAAO,MAAM,iBACX,CAAC,SAAS,MAAM,IAAI,aAAa,MAAM,EAAE,GACzC,cACF;MACF,IACA,KAAA;KACN,CAAC;IACH,EACF;GACF;EClCuBA;CACvB;AACF"}
|
package/package.json
CHANGED
|
@@ -1,18 +1,22 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "eslint-plugin-ocavue",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.0
|
|
5
|
-
"description": "
|
|
4
|
+
"version": "0.2.0",
|
|
5
|
+
"description": "Custom ESLint rules from ocavue",
|
|
6
6
|
"author": "ocavue <ocavue@gmail.com>",
|
|
7
7
|
"license": "MIT",
|
|
8
8
|
"funding": "https://github.com/sponsors/ocavue",
|
|
9
|
-
"homepage": "https://github.com/ocavue/
|
|
9
|
+
"homepage": "https://github.com/ocavue/eslint-plugin-ocavue#readme",
|
|
10
10
|
"repository": {
|
|
11
11
|
"type": "git",
|
|
12
|
-
"url": "https://github.com/ocavue/
|
|
12
|
+
"url": "https://github.com/ocavue/eslint-plugin-ocavue.git"
|
|
13
13
|
},
|
|
14
|
-
"bugs": "https://github.com/ocavue/
|
|
15
|
-
"keywords": [
|
|
14
|
+
"bugs": "https://github.com/ocavue/eslint-plugin-ocavue/issues",
|
|
15
|
+
"keywords": [
|
|
16
|
+
"eslint",
|
|
17
|
+
"eslintplugin",
|
|
18
|
+
"eslint-plugin"
|
|
19
|
+
],
|
|
16
20
|
"sideEffects": false,
|
|
17
21
|
"main": "./dist/index.js",
|
|
18
22
|
"module": "./dist/index.js",
|
|
@@ -27,17 +31,19 @@
|
|
|
27
31
|
"dist",
|
|
28
32
|
"src"
|
|
29
33
|
],
|
|
34
|
+
"peerDependencies": {
|
|
35
|
+
"eslint": "^10.0.0"
|
|
36
|
+
},
|
|
30
37
|
"devDependencies": {
|
|
31
|
-
"@ocavue/eslint-config": "^4.
|
|
38
|
+
"@ocavue/eslint-config": "^4.22.0",
|
|
32
39
|
"@ocavue/tsconfig": "^0.7.1",
|
|
33
40
|
"@types/node": "^24.0.0",
|
|
34
|
-
"eslint": "^10.
|
|
35
|
-
"knip": "^6.
|
|
36
|
-
"oxfmt": "^0.
|
|
37
|
-
"tsdown": "^0.
|
|
41
|
+
"eslint": "^10.14.0",
|
|
42
|
+
"knip": "^6.41.0",
|
|
43
|
+
"oxfmt": "^0.73.0",
|
|
44
|
+
"tsdown": "^0.23.1",
|
|
38
45
|
"typescript": "^6.0.3",
|
|
39
|
-
"
|
|
40
|
-
"vitest": "^4.1.11"
|
|
46
|
+
"vitest": "^5.0.5"
|
|
41
47
|
},
|
|
42
48
|
"scripts": {
|
|
43
49
|
"build": "tsdown",
|
package/src/index.ts
CHANGED
|
@@ -1,2 +1,19 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
import type { ESLint } from 'eslint'
|
|
2
|
+
|
|
3
|
+
import { version } from '../package.json'
|
|
4
|
+
|
|
5
|
+
import noImplicitArrowLinebreak from './rules/no-implicit-arrow-linebreak'
|
|
6
|
+
import preferWhileTrue from './rules/prefer-while-true'
|
|
7
|
+
|
|
8
|
+
const plugin = {
|
|
9
|
+
meta: {
|
|
10
|
+
name: 'eslint-plugin-ocavue',
|
|
11
|
+
version,
|
|
12
|
+
},
|
|
13
|
+
rules: {
|
|
14
|
+
'no-implicit-arrow-linebreak': noImplicitArrowLinebreak,
|
|
15
|
+
'prefer-while-true': preferWhileTrue,
|
|
16
|
+
},
|
|
17
|
+
} satisfies ESLint.Plugin
|
|
18
|
+
|
|
19
|
+
export default plugin
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { RuleTester } from 'eslint'
|
|
2
|
+
import { test } from 'vitest'
|
|
3
|
+
|
|
4
|
+
import rule from './no-implicit-arrow-linebreak'
|
|
5
|
+
|
|
6
|
+
test('no-implicit-arrow-linebreak', () => {
|
|
7
|
+
const tester = new RuleTester({
|
|
8
|
+
languageOptions: { ecmaVersion: 'latest', sourceType: 'module' },
|
|
9
|
+
})
|
|
10
|
+
|
|
11
|
+
tester.run('no-implicit-arrow-linebreak', rule, {
|
|
12
|
+
valid: [
|
|
13
|
+
'const f = () => fn(123)',
|
|
14
|
+
'const f = () => {\n return fn(123)\n}',
|
|
15
|
+
'const f = () => (\n fn(123)\n)',
|
|
16
|
+
'const f = async () => fn(123)',
|
|
17
|
+
'const f = (a) => (b) => a + b',
|
|
18
|
+
],
|
|
19
|
+
invalid: [
|
|
20
|
+
{
|
|
21
|
+
code: 'const f = () =>\n fn(123)',
|
|
22
|
+
output: 'const f = () => { return fn(123) }',
|
|
23
|
+
errors: [{ messageId: 'linebreak' }],
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
code: 'const f = () =>\n (fn(123))',
|
|
27
|
+
output: 'const f = () => { return (fn(123)) }',
|
|
28
|
+
errors: [{ messageId: 'linebreak' }],
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
code: 'const f = (a, b) =>\n a + b',
|
|
32
|
+
output: 'const f = (a, b) => { return a + b }',
|
|
33
|
+
errors: [{ messageId: 'linebreak' }],
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
code: 'const f = async (a) =>\n await fn(a)',
|
|
37
|
+
output: 'const f = async (a) => { return await fn(a) }',
|
|
38
|
+
errors: [{ messageId: 'linebreak' }],
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
code: 'const f = (a) => (b) =>\n a + b',
|
|
42
|
+
output: 'const f = (a) => (b) => { return a + b }',
|
|
43
|
+
errors: [{ messageId: 'linebreak' }],
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
// No autofix when a comment sits between `=>` and the body.
|
|
47
|
+
code: 'const f = () => // note\n fn(123)',
|
|
48
|
+
output: null,
|
|
49
|
+
errors: [{ messageId: 'linebreak' }],
|
|
50
|
+
},
|
|
51
|
+
],
|
|
52
|
+
})
|
|
53
|
+
})
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import type { Rule } from 'eslint'
|
|
2
|
+
|
|
3
|
+
const rule: Rule.RuleModule = {
|
|
4
|
+
meta: {
|
|
5
|
+
type: 'layout',
|
|
6
|
+
fixable: 'code',
|
|
7
|
+
docs: {
|
|
8
|
+
description:
|
|
9
|
+
'disallow a linebreak between `=>` and an implicit arrow function body, fixing it to a block body with `return`',
|
|
10
|
+
url: 'https://github.com/ocavue/eslint-plugin-ocavue#no-implicit-arrow-linebreak',
|
|
11
|
+
},
|
|
12
|
+
messages: {
|
|
13
|
+
linebreak:
|
|
14
|
+
'Implicit arrow body must not start on a new line. Use a block body with `return` instead.',
|
|
15
|
+
},
|
|
16
|
+
schema: [],
|
|
17
|
+
},
|
|
18
|
+
create(context) {
|
|
19
|
+
const sourceCode = context.sourceCode
|
|
20
|
+
return {
|
|
21
|
+
ArrowFunctionExpression(node) {
|
|
22
|
+
if (node.body.type === 'BlockStatement') {
|
|
23
|
+
return
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const arrowToken = sourceCode.getTokenBefore(node.body, {
|
|
27
|
+
filter: (token) => token.type === 'Punctuator' && token.value === '=>',
|
|
28
|
+
})
|
|
29
|
+
if (!arrowToken) {
|
|
30
|
+
return
|
|
31
|
+
}
|
|
32
|
+
const firstBodyToken = sourceCode.getTokenAfter(arrowToken)
|
|
33
|
+
// The last token of the arrow function. When the body is wrapped in
|
|
34
|
+
// parentheses, this is the closing `)`, so the fix keeps the parens.
|
|
35
|
+
const lastToken = sourceCode.getLastToken(node)
|
|
36
|
+
if (!firstBodyToken || !lastToken) {
|
|
37
|
+
return
|
|
38
|
+
}
|
|
39
|
+
if (arrowToken.loc.end.line === firstBodyToken.loc.start.line) {
|
|
40
|
+
return
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// The fix rewrites the region between `=>` and the body, so bail out
|
|
44
|
+
// of fixing (but still report) if that region contains comments.
|
|
45
|
+
const canFix = !sourceCode.commentsExistBetween(arrowToken, firstBodyToken)
|
|
46
|
+
|
|
47
|
+
context.report({
|
|
48
|
+
node: node.body,
|
|
49
|
+
messageId: 'linebreak',
|
|
50
|
+
fix: canFix
|
|
51
|
+
? (fixer) => {
|
|
52
|
+
const bodyText = sourceCode.text.slice(firstBodyToken.range[0], lastToken.range[1])
|
|
53
|
+
return fixer.replaceTextRange(
|
|
54
|
+
[arrowToken.range[1], lastToken.range[1]],
|
|
55
|
+
` { return ${bodyText} }`,
|
|
56
|
+
)
|
|
57
|
+
}
|
|
58
|
+
: undefined,
|
|
59
|
+
})
|
|
60
|
+
},
|
|
61
|
+
}
|
|
62
|
+
},
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export default rule
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { RuleTester } from 'eslint'
|
|
2
|
+
import { test } from 'vitest'
|
|
3
|
+
|
|
4
|
+
import rule from './prefer-while-true'
|
|
5
|
+
|
|
6
|
+
test('prefer-while-true', () => {
|
|
7
|
+
const tester = new RuleTester({
|
|
8
|
+
languageOptions: { ecmaVersion: 'latest', sourceType: 'module' },
|
|
9
|
+
})
|
|
10
|
+
|
|
11
|
+
tester.run('prefer-while-true', rule, {
|
|
12
|
+
valid: [
|
|
13
|
+
'while (true) {}',
|
|
14
|
+
'for (let i = 0; i < 10; i++) {}',
|
|
15
|
+
'for (; i < 10; ) {}',
|
|
16
|
+
'for (let i = 0; ; i++) {}',
|
|
17
|
+
'for (const x of xs) {}',
|
|
18
|
+
],
|
|
19
|
+
invalid: [
|
|
20
|
+
{
|
|
21
|
+
code: 'for (;;) {\n fn()\n}',
|
|
22
|
+
output: 'while (true) {\n fn()\n}',
|
|
23
|
+
errors: [{ messageId: 'preferWhileTrue' }],
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
code: 'for ( ; ; ) fn()',
|
|
27
|
+
output: 'while (true) fn()',
|
|
28
|
+
errors: [{ messageId: 'preferWhileTrue' }],
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
code: 'outer: for (;;) {\n for (;;) break outer\n}',
|
|
32
|
+
output: 'outer: while (true) {\n while (true) break outer\n}',
|
|
33
|
+
errors: [{ messageId: 'preferWhileTrue' }, { messageId: 'preferWhileTrue' }],
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
// No autofix when a comment sits inside the loop head.
|
|
37
|
+
code: 'for (; /* forever */ ;) {}',
|
|
38
|
+
output: null,
|
|
39
|
+
errors: [{ messageId: 'preferWhileTrue' }],
|
|
40
|
+
},
|
|
41
|
+
],
|
|
42
|
+
})
|
|
43
|
+
})
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import type { Rule } from 'eslint'
|
|
2
|
+
|
|
3
|
+
const rule: Rule.RuleModule = {
|
|
4
|
+
meta: {
|
|
5
|
+
type: 'suggestion',
|
|
6
|
+
fixable: 'code',
|
|
7
|
+
docs: {
|
|
8
|
+
description: 'enforce `while (true)` instead of `for (;;)` for infinite loops',
|
|
9
|
+
url: 'https://github.com/ocavue/eslint-plugin-ocavue#prefer-while-true',
|
|
10
|
+
},
|
|
11
|
+
messages: {
|
|
12
|
+
preferWhileTrue: 'Use `while (true)` instead of `for (;;)`.',
|
|
13
|
+
},
|
|
14
|
+
schema: [],
|
|
15
|
+
},
|
|
16
|
+
create(context) {
|
|
17
|
+
const sourceCode = context.sourceCode
|
|
18
|
+
return {
|
|
19
|
+
ForStatement(node) {
|
|
20
|
+
if (node.init || node.test || node.update) {
|
|
21
|
+
return
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const forToken = sourceCode.getFirstToken(node)
|
|
25
|
+
const closingParen = sourceCode.getTokenBefore(node.body)
|
|
26
|
+
if (!forToken || !closingParen) {
|
|
27
|
+
return
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Bail out of fixing (but still report) if the loop head contains
|
|
31
|
+
// comments, so the fix never drops a comment.
|
|
32
|
+
const canFix = !sourceCode.commentsExistBetween(forToken, closingParen)
|
|
33
|
+
|
|
34
|
+
context.report({
|
|
35
|
+
node,
|
|
36
|
+
loc: { start: forToken.loc.start, end: closingParen.loc.end },
|
|
37
|
+
messageId: 'preferWhileTrue',
|
|
38
|
+
fix: canFix
|
|
39
|
+
? (fixer) => {
|
|
40
|
+
return fixer.replaceTextRange(
|
|
41
|
+
[forToken.range[0], closingParen.range[1]],
|
|
42
|
+
'while (true)',
|
|
43
|
+
)
|
|
44
|
+
}
|
|
45
|
+
: undefined,
|
|
46
|
+
})
|
|
47
|
+
},
|
|
48
|
+
}
|
|
49
|
+
},
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export default rule
|