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 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
- # Temporary Holding Version
1
+ # eslint-plugin-boolean-trap
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ [![npm](https://img.shields.io/npm/v/eslint-plugin-boolean-trap.svg)](https://www.npmjs.com/package/eslint-plugin-boolean-trap)
4
+ [![CI](https://github.com/acus9345/eslint-plugin-boolean-trap/actions/workflows/ci.yml/badge.svg)](https://github.com/acus9345/eslint-plugin-boolean-trap/actions/workflows/ci.yml)
5
+ [![license](https://img.shields.io/npm/l/eslint-plugin-boolean-trap.svg)](./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": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
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
+ }
@@ -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;