eslint-plugin-boolean-trap 0.0.0-stage → 1.0.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/LICENSE +21 -0
- package/README.md +92 -2
- package/docs/rules/no-boolean-literal-args.md +85 -0
- package/docs/rules/no-boolean-params.md +75 -0
- package/package.json +56 -4
- package/src/index.js +35 -0
- package/src/rules/no-boolean-literal-args.js +88 -0
- package/src/rules/no-boolean-params.js +116 -0
- package/src/utils.js +112 -0
- package/types/index.d.ts +15 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 acus9345
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,93 @@
|
|
|
1
|
-
#
|
|
1
|
+
# eslint-plugin-boolean-trap
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[](https://www.npmjs.com/package/eslint-plugin-boolean-trap)
|
|
4
|
+
[](https://github.com/acus9345/eslint-plugin-boolean-trap/actions/workflows/ci.yml)
|
|
5
|
+
[](./LICENSE)
|
|
6
|
+
|
|
7
|
+
ESLint rules that catch the **boolean trap**: bare `true` / `false` arguments that make call sites unreadable.
|
|
8
|
+
|
|
9
|
+
```js
|
|
10
|
+
el.addEventListener('click', onClick, true); // what does true mean here?
|
|
11
|
+
render(data, false, true); // and which one is which?
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
```js
|
|
15
|
+
el.addEventListener('click', onClick, { capture: true }); // ✅ obvious
|
|
16
|
+
render(data, { animate: false, force: true }); // ✅ obvious
|
|
17
|
+
render(data, /* animate= */ false, /* force= */ true); // ✅ labeled
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Works with ESLint 9 and 10 (flat config), JavaScript and TypeScript.
|
|
21
|
+
|
|
22
|
+
## Install
|
|
23
|
+
|
|
24
|
+
```sh
|
|
25
|
+
npm install --save-dev eslint-plugin-boolean-trap
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Usage
|
|
29
|
+
|
|
30
|
+
```js
|
|
31
|
+
// eslint.config.js
|
|
32
|
+
import booleanTrap from 'eslint-plugin-boolean-trap';
|
|
33
|
+
|
|
34
|
+
export default [
|
|
35
|
+
booleanTrap.configs.recommended,
|
|
36
|
+
];
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Or pick rules yourself:
|
|
40
|
+
|
|
41
|
+
```js
|
|
42
|
+
import booleanTrap from 'eslint-plugin-boolean-trap';
|
|
43
|
+
|
|
44
|
+
export default [
|
|
45
|
+
{
|
|
46
|
+
plugins: { 'boolean-trap': booleanTrap },
|
|
47
|
+
rules: {
|
|
48
|
+
'boolean-trap/no-boolean-literal-args': ['error', { ignoreFunctions: ['classList.toggle'] }],
|
|
49
|
+
'boolean-trap/no-boolean-params': 'warn',
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
];
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
For TypeScript files, use [`typescript-eslint`](https://typescript-eslint.io/)'s parser as usual. `no-boolean-params` then also reads `boolean` type annotations.
|
|
56
|
+
|
|
57
|
+
## Rules
|
|
58
|
+
|
|
59
|
+
| Rule | Description | recommended | strict |
|
|
60
|
+
| --- | --- | :-: | :-: |
|
|
61
|
+
| [`no-boolean-literal-args`](./docs/rules/no-boolean-literal-args.md) | Disallow passing boolean literals as positional arguments | ✅ | ✅ |
|
|
62
|
+
| [`no-boolean-params`](./docs/rules/no-boolean-params.md) | Disallow positional boolean parameters in function signatures | ✅ | ✅ |
|
|
63
|
+
|
|
64
|
+
## Configs
|
|
65
|
+
|
|
66
|
+
| Config | What it does |
|
|
67
|
+
| --- | --- |
|
|
68
|
+
| `recommended` | Both rules as errors. Single-argument calls like `setVisible(true)` and single-parameter functions are allowed. |
|
|
69
|
+
| `strict` | Both rules as errors, and single-argument calls and single-parameter functions are reported too. |
|
|
70
|
+
|
|
71
|
+
## Tests and assertions
|
|
72
|
+
|
|
73
|
+
Assertion libraries often take booleans positionally (`assert.equal(actual, true)`). Ignore them by name, or relax the rule for test files:
|
|
74
|
+
|
|
75
|
+
```js
|
|
76
|
+
export default [
|
|
77
|
+
booleanTrap.configs.recommended,
|
|
78
|
+
{
|
|
79
|
+
files: ['**/*.test.*'],
|
|
80
|
+
rules: {
|
|
81
|
+
'boolean-trap/no-boolean-literal-args': ['error', { ignoreFunctions: ['assert.*', 't.*'] }],
|
|
82
|
+
},
|
|
83
|
+
},
|
|
84
|
+
];
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
## Why
|
|
88
|
+
|
|
89
|
+
A positional boolean hides its meaning at every call site. To understand `fetchUser(id, true, false)` you have to open the function definition. Options objects, named constants or separate functions (`insert` / `upsert` instead of `save(item, overwrite)`) make the intent readable where the call happens.
|
|
90
|
+
|
|
91
|
+
## License
|
|
92
|
+
|
|
93
|
+
[MIT](./LICENSE) © acus9345
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# boolean-trap/no-boolean-literal-args
|
|
2
|
+
|
|
3
|
+
Disallow passing boolean literals as positional arguments.
|
|
4
|
+
|
|
5
|
+
✅ Enabled in the `recommended` and `strict` configs.
|
|
6
|
+
|
|
7
|
+
## Rule details
|
|
8
|
+
|
|
9
|
+
A bare `true` or `false` in a call tells the reader nothing:
|
|
10
|
+
|
|
11
|
+
```js
|
|
12
|
+
el.addEventListener('click', onClick, true); // true = useCapture? once? passive?
|
|
13
|
+
render(data, false, true); // which one is which?
|
|
14
|
+
new Widget(parent, true);
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
This rule reports each boolean literal passed positionally to a function call or `new` expression.
|
|
18
|
+
|
|
19
|
+
Examples of **incorrect** code:
|
|
20
|
+
|
|
21
|
+
```js
|
|
22
|
+
/* eslint boolean-trap/no-boolean-literal-args: "error" */
|
|
23
|
+
|
|
24
|
+
el.addEventListener('click', onClick, true);
|
|
25
|
+
render(data, false, true);
|
|
26
|
+
new Widget(parent, true);
|
|
27
|
+
this.store.save(item, false);
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Examples of **correct** code:
|
|
31
|
+
|
|
32
|
+
```js
|
|
33
|
+
/* eslint boolean-trap/no-boolean-literal-args: "error" */
|
|
34
|
+
|
|
35
|
+
el.addEventListener('click', onClick, { capture: true });
|
|
36
|
+
render(data, { animate: false, force: true });
|
|
37
|
+
|
|
38
|
+
const OVERWRITE = true;
|
|
39
|
+
save(item, OVERWRITE);
|
|
40
|
+
|
|
41
|
+
// Labeled arguments (Chromium / Java-style comments) are fine
|
|
42
|
+
render(data, /* animate= */ false, /* force= */ true);
|
|
43
|
+
save(
|
|
44
|
+
item,
|
|
45
|
+
true, // overwrite
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
// A single argument is usually clear from the function name
|
|
49
|
+
setVisible(true);
|
|
50
|
+
expect(value).toBe(false);
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## Options
|
|
54
|
+
|
|
55
|
+
```js
|
|
56
|
+
'boolean-trap/no-boolean-literal-args': ['error', {
|
|
57
|
+
allowSingleArgument: true,
|
|
58
|
+
allowLabeledArguments: true,
|
|
59
|
+
ignoreFunctions: [],
|
|
60
|
+
}]
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### `allowSingleArgument`
|
|
64
|
+
|
|
65
|
+
Default `true`. Calls with exactly one argument, such as `setVisible(true)` or `toBe(false)`, are allowed.
|
|
66
|
+
Set to `false` (as the `strict` config does) to report them too: `repaint(false)` is still a trap.
|
|
67
|
+
|
|
68
|
+
### `allowLabeledArguments`
|
|
69
|
+
|
|
70
|
+
Default `true`. A comment directly before or after the literal, or a `//` comment after its trailing comma on the same line, counts as a label and silences the report.
|
|
71
|
+
|
|
72
|
+
### `ignoreFunctions`
|
|
73
|
+
|
|
74
|
+
Default `[]`. Names of functions whose boolean arguments are allowed. `*` matches any characters except `.`.
|
|
75
|
+
A pattern matches the full call name or any trailing part of it, so `toggle` and `classList.toggle` both cover `el.classList.toggle(...)`, and `assert.*` covers `assert.equal(...)`.
|
|
76
|
+
|
|
77
|
+
```js
|
|
78
|
+
'boolean-trap/no-boolean-literal-args': ['error', {
|
|
79
|
+
ignoreFunctions: ['assert.*', 'classList.toggle', 'set*'],
|
|
80
|
+
}]
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
## When not to use it
|
|
84
|
+
|
|
85
|
+
If your code base relies heavily on a third-party API with positional boolean flags that you cannot change, add those functions to `ignoreFunctions` instead of turning the rule off.
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# boolean-trap/no-boolean-params
|
|
2
|
+
|
|
3
|
+
Disallow positional boolean parameters in function signatures.
|
|
4
|
+
|
|
5
|
+
✅ Enabled in the `recommended` and `strict` configs.
|
|
6
|
+
|
|
7
|
+
## Rule details
|
|
8
|
+
|
|
9
|
+
[`no-boolean-literal-args`](./no-boolean-literal-args.md) catches the boolean trap at the call site. This rule catches it where it is created: a function that takes a positional boolean forces every caller to write `true` or `false` with no hint of meaning.
|
|
10
|
+
|
|
11
|
+
A parameter counts as a positional boolean when it:
|
|
12
|
+
|
|
13
|
+
- has a boolean default value: `function load(url, cache = true)`
|
|
14
|
+
- has a TypeScript `boolean` annotation, including `boolean | undefined` and `true | false | null`: `function load(url: string, cache?: boolean)`
|
|
15
|
+
- is a TypeScript parameter property typed `boolean`: `constructor(private strict: boolean)`
|
|
16
|
+
|
|
17
|
+
It checks function declarations, expressions, arrow functions, class and object methods, `declare function` signatures, abstract methods and interface method signatures.
|
|
18
|
+
Destructured parameters are never reported, since an options object is the recommended fix. Setters, the TypeScript `this` parameter and function type aliases are ignored.
|
|
19
|
+
|
|
20
|
+
Examples of **incorrect** code:
|
|
21
|
+
|
|
22
|
+
```ts
|
|
23
|
+
/* eslint boolean-trap/no-boolean-params: "error" */
|
|
24
|
+
|
|
25
|
+
function load(url, cache = true) {}
|
|
26
|
+
|
|
27
|
+
class Store {
|
|
28
|
+
save(item: Item, overwrite: boolean) {}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
interface Api {
|
|
32
|
+
fetch(url: string, force?: boolean): Promise<Response>;
|
|
33
|
+
}
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Examples of **correct** code:
|
|
37
|
+
|
|
38
|
+
```ts
|
|
39
|
+
/* eslint boolean-trap/no-boolean-params: "error" */
|
|
40
|
+
|
|
41
|
+
function load(url, { cache = true } = {}) {}
|
|
42
|
+
|
|
43
|
+
class Store {
|
|
44
|
+
save(item: Item, options: { overwrite: boolean }) {}
|
|
45
|
+
// or two well-named methods
|
|
46
|
+
insert(item: Item) {}
|
|
47
|
+
upsert(item: Item) {}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// One boolean parameter is allowed by default
|
|
51
|
+
function setVisible(visible: boolean) {}
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Options
|
|
55
|
+
|
|
56
|
+
```js
|
|
57
|
+
'boolean-trap/no-boolean-params': ['error', {
|
|
58
|
+
allowSingleParameter: true,
|
|
59
|
+
ignoreFunctions: [],
|
|
60
|
+
}]
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### `allowSingleParameter`
|
|
64
|
+
|
|
65
|
+
Default `true`. Functions whose only parameter is a boolean, such as `setVisible(visible)`, are allowed.
|
|
66
|
+
Set to `false` (as the `strict` config does) to report them too.
|
|
67
|
+
|
|
68
|
+
### `ignoreFunctions`
|
|
69
|
+
|
|
70
|
+
Default `[]`. Function or method names to skip. `*` matches any characters except `.`, so `legacy*` skips `legacyLoad` and `legacySave`.
|
|
71
|
+
A pattern matches the full name or any trailing part of it, so `run` and `exports.run` both cover `module.exports.run = function (...) {}`.
|
|
72
|
+
|
|
73
|
+
## When not to use it
|
|
74
|
+
|
|
75
|
+
When you implement an interface defined elsewhere (a framework callback, a DOM API shim) you may have to accept positional booleans. Use `ignoreFunctions` or an inline disable comment for those.
|
package/package.json
CHANGED
|
@@ -1,6 +1,58 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "eslint-plugin-boolean-trap",
|
|
3
|
-
"version": "
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "ESLint rules that catch the boolean trap: positional true/false arguments and boolean parameters that make call sites unreadable.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"eslint",
|
|
7
|
+
"eslintplugin",
|
|
8
|
+
"eslint-plugin",
|
|
9
|
+
"boolean-trap",
|
|
10
|
+
"boolean",
|
|
11
|
+
"readability",
|
|
12
|
+
"code-quality",
|
|
13
|
+
"typescript"
|
|
14
|
+
],
|
|
15
|
+
"homepage": "https://github.com/acus9345/eslint-plugin-boolean-trap#readme",
|
|
16
|
+
"bugs": {
|
|
17
|
+
"url": "https://github.com/acus9345/eslint-plugin-boolean-trap/issues"
|
|
18
|
+
},
|
|
19
|
+
"repository": {
|
|
20
|
+
"type": "git",
|
|
21
|
+
"url": "git+https://github.com/acus9345/eslint-plugin-boolean-trap.git"
|
|
22
|
+
},
|
|
23
|
+
"license": "MIT",
|
|
24
|
+
"author": "acus9345",
|
|
25
|
+
"type": "module",
|
|
26
|
+
"main": "./src/index.js",
|
|
27
|
+
"types": "./types/index.d.ts",
|
|
28
|
+
"exports": {
|
|
29
|
+
".": {
|
|
30
|
+
"types": "./types/index.d.ts",
|
|
31
|
+
"default": "./src/index.js"
|
|
32
|
+
},
|
|
33
|
+
"./package.json": "./package.json"
|
|
34
|
+
},
|
|
35
|
+
"files": [
|
|
36
|
+
"src",
|
|
37
|
+
"types",
|
|
38
|
+
"docs"
|
|
39
|
+
],
|
|
40
|
+
"scripts": {
|
|
41
|
+
"test": "node --test",
|
|
42
|
+
"lint": "eslint .",
|
|
43
|
+
"check": "npm run lint && npm test"
|
|
44
|
+
},
|
|
45
|
+
"engines": {
|
|
46
|
+
"node": "^18.18.0 || >=20.9.0"
|
|
47
|
+
},
|
|
48
|
+
"peerDependencies": {
|
|
49
|
+
"eslint": "^9.0.0 || ^10.0.0"
|
|
50
|
+
},
|
|
51
|
+
"devDependencies": {
|
|
52
|
+
"@eslint/js": "^10.0.1",
|
|
53
|
+
"@typescript-eslint/parser": "^8.71.2",
|
|
54
|
+
"eslint": "^10.13.0",
|
|
55
|
+
"globals": "^17.13.0",
|
|
56
|
+
"typescript": "~6.0.3"
|
|
57
|
+
}
|
|
58
|
+
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { createRequire } from 'node:module';
|
|
2
|
+
import noBooleanLiteralArgs from './rules/no-boolean-literal-args.js';
|
|
3
|
+
import noBooleanParams from './rules/no-boolean-params.js';
|
|
4
|
+
|
|
5
|
+
const { name, version } = createRequire(import.meta.url)('../package.json');
|
|
6
|
+
|
|
7
|
+
const plugin = {
|
|
8
|
+
meta: { name, version, namespace: 'boolean-trap' },
|
|
9
|
+
rules: {
|
|
10
|
+
'no-boolean-literal-args': noBooleanLiteralArgs,
|
|
11
|
+
'no-boolean-params': noBooleanParams,
|
|
12
|
+
},
|
|
13
|
+
configs: /** @type {Record<string, import('eslint').Linter.Config>} */ ({}),
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
Object.assign(plugin.configs, {
|
|
17
|
+
recommended: {
|
|
18
|
+
name: 'boolean-trap/recommended',
|
|
19
|
+
plugins: { 'boolean-trap': plugin },
|
|
20
|
+
rules: {
|
|
21
|
+
'boolean-trap/no-boolean-literal-args': 'error',
|
|
22
|
+
'boolean-trap/no-boolean-params': 'error',
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
strict: {
|
|
26
|
+
name: 'boolean-trap/strict',
|
|
27
|
+
plugins: { 'boolean-trap': plugin },
|
|
28
|
+
rules: {
|
|
29
|
+
'boolean-trap/no-boolean-literal-args': ['error', { allowSingleArgument: false }],
|
|
30
|
+
'boolean-trap/no-boolean-params': ['error', { allowSingleParameter: false }],
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
export default plugin;
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { createNameMatcher, docsUrl, getStaticName, isBooleanLiteral, ordinal } from '../utils.js';
|
|
2
|
+
|
|
3
|
+
const DEFAULTS = {
|
|
4
|
+
allowSingleArgument: true,
|
|
5
|
+
allowLabeledArguments: true,
|
|
6
|
+
ignoreFunctions: /** @type {string[]} */ ([]),
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
/** @type {import('eslint').Rule.RuleModule} */
|
|
10
|
+
export default {
|
|
11
|
+
meta: {
|
|
12
|
+
type: 'suggestion',
|
|
13
|
+
docs: {
|
|
14
|
+
description: 'Disallow passing boolean literals as positional arguments',
|
|
15
|
+
recommended: true,
|
|
16
|
+
url: docsUrl('no-boolean-literal-args'),
|
|
17
|
+
},
|
|
18
|
+
schema: [
|
|
19
|
+
{
|
|
20
|
+
type: 'object',
|
|
21
|
+
properties: {
|
|
22
|
+
allowSingleArgument: { type: 'boolean' },
|
|
23
|
+
allowLabeledArguments: { type: 'boolean' },
|
|
24
|
+
ignoreFunctions: { type: 'array', items: { type: 'string' }, uniqueItems: true },
|
|
25
|
+
},
|
|
26
|
+
additionalProperties: false,
|
|
27
|
+
},
|
|
28
|
+
],
|
|
29
|
+
defaultOptions: [DEFAULTS],
|
|
30
|
+
messages: {
|
|
31
|
+
positionalBoolean:
|
|
32
|
+
'`{{value}}` passed as the {{position}} argument of {{callee}} says nothing about what it does. Use an options object, a named constant, or label it: `/* name= */ {{value}}`.',
|
|
33
|
+
positionalBooleanNoLabel:
|
|
34
|
+
'`{{value}}` passed as the {{position}} argument of {{callee}} says nothing about what it does. Use an options object or a named constant.',
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
|
|
38
|
+
create(context) {
|
|
39
|
+
const options = { ...DEFAULTS, ...context.options[0] };
|
|
40
|
+
const { sourceCode } = context;
|
|
41
|
+
const isIgnored = createNameMatcher(options.ignoreFunctions);
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* `fn(/* force *\/ true)`, `fn(true /* force *\/)` and a `// force` line comment
|
|
45
|
+
* right after the trailing comma all count as a label.
|
|
46
|
+
* @param {import('estree').Node} arg
|
|
47
|
+
*/
|
|
48
|
+
function isLabeled(arg) {
|
|
49
|
+
if (sourceCode.getCommentsBefore(arg).length > 0) return true;
|
|
50
|
+
if (sourceCode.getCommentsAfter(arg).length > 0) return true;
|
|
51
|
+
const next = sourceCode.getTokenAfter(arg);
|
|
52
|
+
if (next?.value !== ',') return false;
|
|
53
|
+
return sourceCode
|
|
54
|
+
.getCommentsAfter(next)
|
|
55
|
+
.some((comment) => comment.type === 'Line' && comment.loc?.start.line === arg.loc?.end.line);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** @param {import('estree').CallExpression | import('estree').NewExpression} node */
|
|
59
|
+
function check(node) {
|
|
60
|
+
const args = node.arguments;
|
|
61
|
+
if (options.allowSingleArgument && args.length === 1) return;
|
|
62
|
+
if (!args.some(isBooleanLiteral)) return;
|
|
63
|
+
|
|
64
|
+
const name = getStaticName(node.callee);
|
|
65
|
+
if (isIgnored(name)) return;
|
|
66
|
+
const callee = name ? `\`${name}\`` : 'this call';
|
|
67
|
+
|
|
68
|
+
args.forEach((arg, index) => {
|
|
69
|
+
if (!isBooleanLiteral(arg)) return;
|
|
70
|
+
if (options.allowLabeledArguments && isLabeled(arg)) return;
|
|
71
|
+
context.report({
|
|
72
|
+
node: arg,
|
|
73
|
+
messageId: options.allowLabeledArguments ? 'positionalBoolean' : 'positionalBooleanNoLabel',
|
|
74
|
+
data: {
|
|
75
|
+
value: String(/** @type {import('estree').Literal} */ (arg).value),
|
|
76
|
+
position: ordinal(index + 1),
|
|
77
|
+
callee,
|
|
78
|
+
},
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return {
|
|
84
|
+
CallExpression: check,
|
|
85
|
+
NewExpression: check,
|
|
86
|
+
};
|
|
87
|
+
},
|
|
88
|
+
};
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { createNameMatcher, docsUrl, getFunctionName, isBooleanLiteral } from '../utils.js';
|
|
2
|
+
|
|
3
|
+
const DEFAULTS = {
|
|
4
|
+
allowSingleParameter: true,
|
|
5
|
+
ignoreFunctions: /** @type {string[]} */ ([]),
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
const NULLISH_TYPES = new Set(['TSUndefinedKeyword', 'TSNullKeyword']);
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* `boolean`, or a union of `boolean`/`true`/`false` with `null`/`undefined`.
|
|
12
|
+
* @param {any} type
|
|
13
|
+
*/
|
|
14
|
+
function isBooleanType(type) {
|
|
15
|
+
if (type.type === 'TSBooleanKeyword') return true;
|
|
16
|
+
if (type.type !== 'TSUnionType') return false;
|
|
17
|
+
let hasBoolean = false;
|
|
18
|
+
for (const member of type.types) {
|
|
19
|
+
if (member.type === 'TSBooleanKeyword' || (member.type === 'TSLiteralType' && isBooleanLiteral(member.literal))) {
|
|
20
|
+
hasBoolean = true;
|
|
21
|
+
} else if (!NULLISH_TYPES.has(member.type)) {
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return hasBoolean;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** @param {any} identifier */
|
|
29
|
+
const hasBooleanAnnotation = (identifier) =>
|
|
30
|
+
identifier.typeAnnotation?.typeAnnotation != null && isBooleanType(identifier.typeAnnotation.typeAnnotation);
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* The identifier of a positional boolean parameter, or null.
|
|
34
|
+
* Destructured parameters are fine: that is the recommended fix.
|
|
35
|
+
* @param {any} param
|
|
36
|
+
*/
|
|
37
|
+
function getBooleanParam(param) {
|
|
38
|
+
const target = param.type === 'TSParameterProperty' ? param.parameter : param;
|
|
39
|
+
if (target.type === 'AssignmentPattern') {
|
|
40
|
+
if (target.left.type !== 'Identifier') return null;
|
|
41
|
+
return isBooleanLiteral(target.right) || hasBooleanAnnotation(target.left) ? target.left : null;
|
|
42
|
+
}
|
|
43
|
+
if (target.type === 'Identifier' && hasBooleanAnnotation(target)) return target;
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** @param {any} node */
|
|
48
|
+
function isSetter(node) {
|
|
49
|
+
return (node.parent?.type === 'MethodDefinition' || node.parent?.type === 'Property') && node.parent.kind === 'set';
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** @type {import('eslint').Rule.RuleModule} */
|
|
53
|
+
export default {
|
|
54
|
+
meta: {
|
|
55
|
+
type: 'suggestion',
|
|
56
|
+
docs: {
|
|
57
|
+
description: 'Disallow positional boolean parameters in function signatures',
|
|
58
|
+
recommended: true,
|
|
59
|
+
url: docsUrl('no-boolean-params'),
|
|
60
|
+
},
|
|
61
|
+
schema: [
|
|
62
|
+
{
|
|
63
|
+
type: 'object',
|
|
64
|
+
properties: {
|
|
65
|
+
allowSingleParameter: { type: 'boolean' },
|
|
66
|
+
ignoreFunctions: { type: 'array', items: { type: 'string' }, uniqueItems: true },
|
|
67
|
+
},
|
|
68
|
+
additionalProperties: false,
|
|
69
|
+
},
|
|
70
|
+
],
|
|
71
|
+
defaultOptions: [DEFAULTS],
|
|
72
|
+
messages: {
|
|
73
|
+
booleanParam:
|
|
74
|
+
'Positional boolean parameter `{{name}}` in {{fn}} forces callers to write bare `true`/`false`. Accept an options object (`{ {{name}} }`) or split it into well-named functions.',
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
|
|
78
|
+
create(context) {
|
|
79
|
+
const options = { ...DEFAULTS, ...context.options[0] };
|
|
80
|
+
const isIgnored = createNameMatcher(options.ignoreFunctions);
|
|
81
|
+
|
|
82
|
+
/** @param {any} node */
|
|
83
|
+
function check(node) {
|
|
84
|
+
if (isSetter(node)) return;
|
|
85
|
+
// A TypeScript `this` parameter is not passed by callers.
|
|
86
|
+
const params = node.params.filter(
|
|
87
|
+
(/** @type {any} */ param) => !(param.type === 'Identifier' && param.name === 'this'),
|
|
88
|
+
);
|
|
89
|
+
if (options.allowSingleParameter && params.length === 1) return;
|
|
90
|
+
|
|
91
|
+
const booleanParams = params.map(getBooleanParam).filter(Boolean);
|
|
92
|
+
if (booleanParams.length === 0) return;
|
|
93
|
+
|
|
94
|
+
const name = getFunctionName(node);
|
|
95
|
+
if (isIgnored(name)) return;
|
|
96
|
+
const fn = name ? `\`${name}\`` : 'an anonymous function';
|
|
97
|
+
|
|
98
|
+
for (const identifier of booleanParams) {
|
|
99
|
+
context.report({
|
|
100
|
+
node: identifier,
|
|
101
|
+
messageId: 'booleanParam',
|
|
102
|
+
data: { name: identifier.name, fn },
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return {
|
|
108
|
+
FunctionDeclaration: check,
|
|
109
|
+
FunctionExpression: check,
|
|
110
|
+
ArrowFunctionExpression: check,
|
|
111
|
+
TSDeclareFunction: check,
|
|
112
|
+
TSEmptyBodyFunctionExpression: check,
|
|
113
|
+
TSMethodSignature: check,
|
|
114
|
+
};
|
|
115
|
+
},
|
|
116
|
+
};
|
package/src/utils.js
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
const DOCS_BASE = 'https://github.com/acus9345/eslint-plugin-boolean-trap/blob/main/docs/rules/';
|
|
2
|
+
|
|
3
|
+
/** @param {string} ruleName */
|
|
4
|
+
export const docsUrl = (ruleName) => `${DOCS_BASE}${ruleName}.md`;
|
|
5
|
+
|
|
6
|
+
/** @param {any} node */
|
|
7
|
+
export function isBooleanLiteral(node) {
|
|
8
|
+
return node?.type === 'Literal' && typeof node.value === 'boolean';
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Name of a non-computed (or literal-computed) member/property key.
|
|
13
|
+
* @param {any} key
|
|
14
|
+
* @param {boolean} computed
|
|
15
|
+
* @returns {string | null}
|
|
16
|
+
*/
|
|
17
|
+
function getKeyName(key, computed) {
|
|
18
|
+
if (!computed && key.type === 'Identifier') return key.name;
|
|
19
|
+
if (key.type === 'PrivateIdentifier') return `#${key.name}`;
|
|
20
|
+
if (key.type === 'Literal' && (typeof key.value === 'string' || typeof key.value === 'number')) {
|
|
21
|
+
return String(key.value);
|
|
22
|
+
}
|
|
23
|
+
if (key.type === 'TemplateLiteral' && key.expressions.length === 0) {
|
|
24
|
+
return key.quasis[0].value.cooked;
|
|
25
|
+
}
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Dotted name of an expression such as `foo`, `this.save`, `a.b.c` or `#run`.
|
|
31
|
+
* When only the last segment is known (`getThing().toggle`) that segment is returned.
|
|
32
|
+
* @param {any} node
|
|
33
|
+
* @returns {string | null}
|
|
34
|
+
*/
|
|
35
|
+
export function getStaticName(node) {
|
|
36
|
+
switch (node.type) {
|
|
37
|
+
case 'Identifier':
|
|
38
|
+
return node.name;
|
|
39
|
+
case 'PrivateIdentifier':
|
|
40
|
+
return `#${node.name}`;
|
|
41
|
+
case 'ThisExpression':
|
|
42
|
+
return 'this';
|
|
43
|
+
case 'Super':
|
|
44
|
+
return 'super';
|
|
45
|
+
case 'ChainExpression':
|
|
46
|
+
case 'TSNonNullExpression':
|
|
47
|
+
case 'TSAsExpression':
|
|
48
|
+
return getStaticName(node.expression);
|
|
49
|
+
case 'MemberExpression': {
|
|
50
|
+
const property = getKeyName(node.property, node.computed);
|
|
51
|
+
if (!property) return null;
|
|
52
|
+
const object = getStaticName(node.object);
|
|
53
|
+
return object ? `${object}.${property}` : property;
|
|
54
|
+
}
|
|
55
|
+
default:
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Best-effort name for a function, method or method signature.
|
|
62
|
+
* @param {any} node
|
|
63
|
+
* @returns {string | null}
|
|
64
|
+
*/
|
|
65
|
+
export function getFunctionName(node) {
|
|
66
|
+
if (node.id?.type === 'Identifier') return node.id.name;
|
|
67
|
+
if (node.type === 'TSMethodSignature') return getKeyName(node.key, node.computed);
|
|
68
|
+
|
|
69
|
+
const { parent } = node;
|
|
70
|
+
switch (parent?.type) {
|
|
71
|
+
case 'VariableDeclarator':
|
|
72
|
+
return parent.id.type === 'Identifier' ? parent.id.name : null;
|
|
73
|
+
case 'MethodDefinition':
|
|
74
|
+
case 'TSAbstractMethodDefinition':
|
|
75
|
+
case 'Property':
|
|
76
|
+
case 'PropertyDefinition':
|
|
77
|
+
return getKeyName(parent.key, parent.computed);
|
|
78
|
+
case 'AssignmentExpression':
|
|
79
|
+
return getStaticName(parent.left);
|
|
80
|
+
default:
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** @param {string} text */
|
|
86
|
+
const escapeRegExp = (text) => text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Builds a predicate for `ignoreFunctions` patterns. `*` matches anything except `.`.
|
|
90
|
+
* A pattern matches the whole dotted name or any trailing segments of it,
|
|
91
|
+
* so `toggle` and `classList.toggle` both ignore `el.classList.toggle(...)`.
|
|
92
|
+
* @param {string[]} patterns
|
|
93
|
+
* @returns {(name: string | null) => boolean}
|
|
94
|
+
*/
|
|
95
|
+
export function createNameMatcher(patterns) {
|
|
96
|
+
const regexes = patterns.map(
|
|
97
|
+
(pattern) => new RegExp(`^${pattern.split('*').map(escapeRegExp).join('[^.]*')}$`),
|
|
98
|
+
);
|
|
99
|
+
return (name) => {
|
|
100
|
+
if (!name || regexes.length === 0) return false;
|
|
101
|
+
const segments = name.split('.');
|
|
102
|
+
const suffixes = segments.map((_, index) => segments.slice(index).join('.'));
|
|
103
|
+
return regexes.some((regex) => suffixes.some((suffix) => regex.test(suffix)));
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** @param {number} n */
|
|
108
|
+
export function ordinal(n) {
|
|
109
|
+
const mod100 = n % 100;
|
|
110
|
+
if (mod100 >= 11 && mod100 <= 13) return `${n}th`;
|
|
111
|
+
return `${n}${{ 1: 'st', 2: 'nd', 3: 'rd' }[n % 10] ?? 'th'}`;
|
|
112
|
+
}
|
package/types/index.d.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { ESLint, Linter, Rule } from 'eslint';
|
|
2
|
+
|
|
3
|
+
declare const plugin: ESLint.Plugin & {
|
|
4
|
+
meta: { name: string; version: string; namespace: 'boolean-trap' };
|
|
5
|
+
rules: {
|
|
6
|
+
'no-boolean-literal-args': Rule.RuleModule;
|
|
7
|
+
'no-boolean-params': Rule.RuleModule;
|
|
8
|
+
};
|
|
9
|
+
configs: {
|
|
10
|
+
recommended: Linter.Config;
|
|
11
|
+
strict: Linter.Config;
|
|
12
|
+
};
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export default plugin;
|