eslint-plugin-reliability 3.1.0 → 3.1.3
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/CHANGELOG.md +104 -0
- package/LICENSE +23 -0
- package/README.md +30 -12
- package/package.json +15 -2
- package/src/index.js +1 -1
- package/src/oxlint.d.ts +20 -0
- package/src/oxlint.js +21 -0
- package/src/rules/error-handling/error-message.js +2 -0
- package/src/rules/error-handling/no-missing-error-context.js +32 -2
- package/src/rules/error-handling/no-silent-errors.js +3 -2
- package/src/rules/error-handling/no-unhandled-promise.js +116 -14
- package/src/rules/reliability/no-await-in-loop.js +19 -17
- package/src/rules/reliability/no-jsdoc-terminator-in-example.js +2 -1
- package/src/rules/reliability/no-missing-null-checks.js +157 -4
- package/src/rules/reliability/no-unsafe-type-narrowing.js +3 -2
- package/src/rules/reliability/require-network-timeout.js +10 -3
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
## [3.1.3] - 2026-05-03
|
|
2
|
+
|
|
3
|
+
### Bug Fixes
|
|
4
|
+
|
|
5
|
+
- `no-missing-null-checks`: exempt provably-non-null identifiers (built-in singletons like `Math`, `JSON`, `console`, error classes; catch-clause params; constructor results; top-level imports) from the null-check requirement. Eliminates a large class of false positives without weakening real coverage.
|
|
6
|
+
- `no-unhandled-promise`: refined detection (see source diff).
|
|
7
|
+
|
|
8
|
+
## 3.1.2 (2026-02-09)
|
|
9
|
+
|
|
10
|
+
This was a version bump only for eslint-plugin-reliability to align it with other projects, there were no code changes.
|
|
11
|
+
|
|
12
|
+
## [3.1.0] - 2026-02-08
|
|
13
|
+
|
|
14
|
+
This was a version bump only for eslint-plugin-reliability to align it with other projects, there were no code changes.
|
|
15
|
+
|
|
16
|
+
## [3.0.4] - 2026-02-08
|
|
17
|
+
|
|
18
|
+
### Bug Fixes
|
|
19
|
+
|
|
20
|
+
- align codecov component IDs with full package names ([2831b968](https://github.com/ofri-peretz/eslint/commit/2831b968))
|
|
21
|
+
|
|
22
|
+
### Documentation
|
|
23
|
+
|
|
24
|
+
- fix changelog header format across all packages ([c3a15082](https://github.com/ofri-peretz/eslint/commit/c3a15082))
|
|
25
|
+
|
|
26
|
+
### ❤️ Thank You
|
|
27
|
+
|
|
28
|
+
- Ofri Peretz
|
|
29
|
+
|
|
30
|
+
## [3.0.3] - 2026-02-06
|
|
31
|
+
|
|
32
|
+
### Bug Fixes
|
|
33
|
+
|
|
34
|
+
- align codecov component names and update docs components ([0a59a86c](https://github.com/ofri-peretz/eslint/commit/0a59a86c))
|
|
35
|
+
|
|
36
|
+
### ❤️ Thank You
|
|
37
|
+
|
|
38
|
+
- Ofri Peretz
|
|
39
|
+
|
|
40
|
+
## [3.0.2] - 2026-02-02
|
|
41
|
+
|
|
42
|
+
This was a version bump only for eslint-plugin-reliability to align it with other projects, there were no code changes.
|
|
43
|
+
|
|
44
|
+
# Changelog
|
|
45
|
+
|
|
46
|
+
All notable changes to `eslint-plugin-reliability` will be documented in this file.
|
|
47
|
+
|
|
48
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
49
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
50
|
+
|
|
51
|
+
### Documentation
|
|
52
|
+
|
|
53
|
+
- 📘 Launched new documentation site: [eslint.interlace.tools](https://eslint.interlace.tools/)
|
|
54
|
+
- 📝 Achieved 100% documentation parity (both .md and .mdx files)
|
|
55
|
+
|
|
56
|
+
## [3.0.1] - 2026-02-02
|
|
57
|
+
|
|
58
|
+
This was a version bump only for eslint-plugin-reliability to align it with other projects, there were no code changes.
|
|
59
|
+
|
|
60
|
+
## [3.0.0] - 2026-02-02
|
|
61
|
+
|
|
62
|
+
This was a version bump only for eslint-plugin-reliability to align it with other projects, there were no code changes.
|
|
63
|
+
|
|
64
|
+
## [1.0.0] - 2026-01-26
|
|
65
|
+
|
|
66
|
+
### Added
|
|
67
|
+
|
|
68
|
+
- Initial stable release with 8 reliability rules
|
|
69
|
+
- LLM-optimized error messages for AI-assisted development
|
|
70
|
+
- 100% test coverage across all rules
|
|
71
|
+
- ESLint 9 flat config support
|
|
72
|
+
- TypeScript type definitions for all rule options
|
|
73
|
+
|
|
74
|
+
### Rules
|
|
75
|
+
|
|
76
|
+
#### Error Handling (4 rules)
|
|
77
|
+
|
|
78
|
+
| Rule | Description | CWE |
|
|
79
|
+
| :------------------------- | :-------------------------------------------- | :------ |
|
|
80
|
+
| `no-unhandled-promise` | Detect unhandled promise rejections | CWE-392 |
|
|
81
|
+
| `no-silent-errors` | Detect empty catch blocks that swallow errors | CWE-390 |
|
|
82
|
+
| `no-missing-error-context` | Require error context when re-throwing | CWE-209 |
|
|
83
|
+
| `error-message` | Require meaningful error messages | CWE-209 |
|
|
84
|
+
|
|
85
|
+
#### Runtime Safety (4 rules)
|
|
86
|
+
|
|
87
|
+
| Rule | Description | CWE |
|
|
88
|
+
| :------------------------- | :-------------------------------------------- | :---------- |
|
|
89
|
+
| `no-missing-null-checks` | Detect potential null/undefined dereferences | CWE-476 |
|
|
90
|
+
| `no-unsafe-type-narrowing` | Detect unsafe type narrowing patterns | CWE-704 |
|
|
91
|
+
| `require-network-timeout` | Require timeouts on network requests | CWE-400 |
|
|
92
|
+
| `no-await-in-loop` | Detect sequential await in loops (N+1 issues) | Performance |
|
|
93
|
+
|
|
94
|
+
### Presets
|
|
95
|
+
|
|
96
|
+
- `recommended` - Balanced reliability checks (4 rules as warnings)
|
|
97
|
+
|
|
98
|
+
### Known Limitations
|
|
99
|
+
|
|
100
|
+
Documented in `docs/KNOWN-LIMITATIONS.md`:
|
|
101
|
+
|
|
102
|
+
- `no-unhandled-promises`: Promises in callbacks or async iterators may be missed
|
|
103
|
+
- `no-silent-errors`: Intentional suppression vs. actual swallowing is hard to distinguish
|
|
104
|
+
- `no-missing-null-checks`: Business-logic guarantees not in types can trigger false positives
|
package/LICENSE
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Ofri Peretz
|
|
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.
|
|
22
|
+
|
|
23
|
+
|
package/README.md
CHANGED
|
@@ -3,21 +3,20 @@
|
|
|
3
3
|
</p>
|
|
4
4
|
|
|
5
5
|
<p align="center">
|
|
6
|
-
|
|
6
|
+
Reliability rules for defensive programming, error handling, and async correctness.
|
|
7
7
|
</p>
|
|
8
8
|
|
|
9
9
|
<p align="center">
|
|
10
10
|
<a href="https://www.npmjs.com/package/eslint-plugin-reliability" target="_blank"><img src="https://img.shields.io/npm/v/eslint-plugin-reliability.svg" alt="NPM Version" /></a>
|
|
11
11
|
<a href="https://www.npmjs.com/package/eslint-plugin-reliability" target="_blank"><img src="https://img.shields.io/npm/dm/eslint-plugin-reliability.svg" alt="NPM Downloads" /></a>
|
|
12
12
|
<a href="https://opensource.org/licenses/MIT" target="_blank"><img src="https://img.shields.io/badge/License-MIT-yellow.svg" alt="Package License" /></a>
|
|
13
|
-
<a href="https://app.codecov.io/gh/ofri-peretz/eslint/components?components%5B0%5D=reliability" target="_blank"><img src="https://codecov.io/gh/ofri-peretz/eslint/graph/badge.svg?component=
|
|
13
|
+
<a href="https://app.codecov.io/gh/ofri-peretz/eslint/components?components%5B0%5D=reliability" target="_blank"><img src="https://codecov.io/gh/ofri-peretz/eslint/graph/badge.svg?component=reliability" alt="Codecov" /></a>
|
|
14
14
|
<a href="https://github.com/ofri-peretz/eslint" target="_blank"><img src="https://img.shields.io/badge/Since-Dec_2025-blue?logo=rocket&logoColor=white" alt="Since Dec 2025" /></a>
|
|
15
15
|
</p>
|
|
16
16
|
|
|
17
17
|
## Description
|
|
18
18
|
|
|
19
|
-
This plugin provides
|
|
20
|
-
By using this plugin, you can proactively identify and mitigate security risks across your entire codebase.
|
|
19
|
+
This plugin provides Reliability rules for defensive programming, error handling, and async correctness.
|
|
21
20
|
|
|
22
21
|
## Philosophy
|
|
23
22
|
|
|
@@ -126,6 +125,14 @@ const results = await Promise.all(userIds.map((id) => fetchUser(id)));
|
|
|
126
125
|
|
|
127
126
|
---
|
|
128
127
|
|
|
128
|
+
## 📦 Compatibility
|
|
129
|
+
| Package | Version |
|
|
130
|
+
| :--- | :--- |
|
|
131
|
+
| ESLint | `^8.0.0 \|\| ^9.0.0 \|\| ^10.0.0` |
|
|
132
|
+
| Node.js | `>=18.0.0` |
|
|
133
|
+
|
|
134
|
+
See the [ESLint Version Support Policy](../../docs/ESLINT_VERSION_SUPPORT.md) — current ecosystem share data, the 20% gate, and the forward-looking exception that covers v10.
|
|
135
|
+
|
|
129
136
|
## Rules
|
|
130
137
|
|
|
131
138
|
**Legend**
|
|
@@ -133,16 +140,27 @@ const results = await Promise.all(userIds.map((id) => fetchUser(id)));
|
|
|
133
140
|
| Icon | Description |
|
|
134
141
|
| :---: | :--- |
|
|
135
142
|
| 💼 | **Recommended**: Included in the recommended preset. |
|
|
136
|
-
| ⚠️ | **Warns**: Set
|
|
143
|
+
| ⚠️ | **Warns**: Set to warn in recommended preset. |
|
|
137
144
|
| 🔧 | **Auto-fixable**: Automatically fixable by the `--fix` CLI option. |
|
|
138
145
|
| 💡 | **Suggestions**: Providing code suggestions in IDE. |
|
|
139
146
|
| 🚫 | **Deprecated**: This rule is deprecated. |
|
|
140
|
-
|
|
141
|
-
|
|
|
142
|
-
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
147
|
+
| 🟢 | **Type-unaware**: AST-only, runs in oxlint JS-plugin tier. |
|
|
148
|
+
| 🟡 | **Type-aware (refining)**: pure-AST primary path; types refine precision. |
|
|
149
|
+
| 🟠 | **Type-aware (graceful)**: requires TS program; silent without it. |
|
|
150
|
+
|
|
151
|
+
<!-- AUTO-GENERATED:RULES_TABLE:START - Do not edit manually -->
|
|
152
|
+
| Rule | CWE | OWASP | CVSS | Description | 🧠 | 💼 | ⚠️ | 🔧 | 💡 | 🚫 |
|
|
153
|
+
| :--- | :---: | :---: | :---: | :--- | :---: | :---: | :---: | :---: | :---: | :---: |
|
|
154
|
+
| [error-message](https://eslint.interlace.tools/docs/quality/plugin-reliability/rules/error-message) | | | | Enforce providing a message when creating built-in Error objects for better debugging. This rule is part of… | 🟢 | | | | 💡 | |
|
|
155
|
+
| [no-await-in-loop](https://eslint.interlace.tools/docs/quality/plugin-reliability/rules/no-await-in-loop) | | | | Disallow await inside loops without considering concurrency implications | 🟢 | | | | 💡 | |
|
|
156
|
+
| [no-jsdoc-terminator-in-example](https://eslint.interlace.tools/docs/quality/plugin-reliability/rules/no-jsdoc-terminator-in-example) | | | | Detect `*/` sequences inside JSDoc `@example` blocks that prematurely close the JSDoc comment. | 🟢 | | | | 💡 | |
|
|
157
|
+
| [no-missing-error-context](https://eslint.interlace.tools/docs/quality/plugin-reliability/rules/no-missing-error-context) | | | | ESLint Rule: no-missing-error-context with LLM-optimized suggestions and auto-fix capabilities. | 🟢 | | | | 💡 | |
|
|
158
|
+
| [no-missing-null-checks](https://eslint.interlace.tools/docs/quality/plugin-reliability/rules/no-missing-null-checks) | CWE-476 | | | ESLint Rule: no-missing-null-checks with LLM-optimized suggestions and auto-fix capabilities. | 🟢 | | ⚠️ | | 💡 | |
|
|
159
|
+
| [no-silent-errors](https://eslint.interlace.tools/docs/quality/plugin-reliability/rules/no-silent-errors) | | | | ESLint Rule: no-silent-errors with LLM-optimized suggestions and auto-fix capabilities. | 🟢 | | ⚠️ | | 💡 | |
|
|
160
|
+
| [no-unhandled-promise](https://eslint.interlace.tools/docs/quality/plugin-reliability/rules/no-unhandled-promise) | CWE-1024 | | | Disallow unhandled Promise rejections with LLM-optimized suggestions for proper async error handling. This… | 🟢 | | | | 💡 | |
|
|
161
|
+
| [no-unsafe-type-narrowing](https://eslint.interlace.tools/docs/quality/plugin-reliability/rules/no-unsafe-type-narrowing) | | | | ESLint Rule: no-unsafe-type-narrowing with LLM-optimized suggestions and auto-fix capabilities. | 🟢 | | | | 💡 | |
|
|
162
|
+
| [require-network-timeout](https://eslint.interlace.tools/docs/quality/plugin-reliability/rules/require-network-timeout) | | | | Require timeout configuration for network requests. This rule is part of eslint-plugin-reliability and prov… | 🟢 | 💼 | | | 💡 | |
|
|
163
|
+
<!-- AUTO-GENERATED:RULES_TABLE:END -->
|
|
146
164
|
## 🔗 Related ESLint Plugins
|
|
147
165
|
|
|
148
166
|
Part of the **Interlace ESLint Ecosystem** — AI-native security plugins with LLM-optimized error messages:
|
|
@@ -151,7 +169,7 @@ Part of the **Interlace ESLint Ecosystem** — AI-native security plugins with L
|
|
|
151
169
|
| :--- | :---: | :--- |
|
|
152
170
|
| [`eslint-plugin-secure-coding`](https://www.npmjs.com/package/eslint-plugin-secure-coding) | [](https://www.npmjs.com/package/eslint-plugin-secure-coding) | General security rules & OWASP guidelines. |
|
|
153
171
|
| [`eslint-plugin-pg`](https://www.npmjs.com/package/eslint-plugin-pg) | [](https://www.npmjs.com/package/eslint-plugin-pg) | PostgreSQL security & best practices. |
|
|
154
|
-
| [`eslint-plugin-
|
|
172
|
+
| [`eslint-plugin-node-security`](https://www.npmjs.com/package/eslint-plugin-node-security) | [](https://www.npmjs.com/package/eslint-plugin-node-security) | Node.js core-module security (fs, child_process, vm, crypto, Buffer). |
|
|
155
173
|
| [`eslint-plugin-jwt`](https://www.npmjs.com/package/eslint-plugin-jwt) | [](https://www.npmjs.com/package/eslint-plugin-jwt) | JWT security & best practices. |
|
|
156
174
|
| [`eslint-plugin-browser-security`](https://www.npmjs.com/package/eslint-plugin-browser-security) | [](https://www.npmjs.com/package/eslint-plugin-browser-security) | Browser-specific security & XSS prevention. |
|
|
157
175
|
| [`eslint-plugin-express-security`](https://www.npmjs.com/package/eslint-plugin-express-security) | [](https://www.npmjs.com/package/eslint-plugin-express-security) | Express.js security hardening rules. |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "eslint-plugin-reliability",
|
|
3
|
-
"version": "3.1.
|
|
3
|
+
"version": "3.1.3",
|
|
4
4
|
"description": "ESLint rules for runtime stability, fault tolerance, and type safety.",
|
|
5
5
|
"type": "commonjs",
|
|
6
6
|
"main": "./src/index.js",
|
|
@@ -9,6 +9,10 @@
|
|
|
9
9
|
".": {
|
|
10
10
|
"types": "./src/index.d.ts",
|
|
11
11
|
"default": "./src/index.js"
|
|
12
|
+
},
|
|
13
|
+
"./oxlint": {
|
|
14
|
+
"types": "./src/oxlint.d.ts",
|
|
15
|
+
"default": "./src/oxlint.js"
|
|
12
16
|
}
|
|
13
17
|
},
|
|
14
18
|
"author": "Ofri Peretz <ofriperetzdev@gmail.com>",
|
|
@@ -51,5 +55,14 @@
|
|
|
51
55
|
"homepage": "https://github.com/ofri-peretz/eslint/tree/main/packages/eslint-plugin-reliability#readme",
|
|
52
56
|
"bugs": {
|
|
53
57
|
"url": "https://github.com/ofri-peretz/eslint/issues"
|
|
58
|
+
},
|
|
59
|
+
"scripts": {
|
|
60
|
+
"build": "tsx ../../scripts/build-package.ts",
|
|
61
|
+
"test": "vitest run",
|
|
62
|
+
"test:coverage": "vitest run --coverage",
|
|
63
|
+
"typecheck": "tsc -p tsconfig.lib.json --noEmit"
|
|
64
|
+
},
|
|
65
|
+
"peerDependencies": {
|
|
66
|
+
"eslint": "^8.0.0 || ^9.0.0 || ^10.0.0"
|
|
54
67
|
}
|
|
55
|
-
}
|
|
68
|
+
}
|
package/src/index.js
CHANGED
package/src/oxlint.d.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) 2025 Ofri Peretz
|
|
3
|
+
* Licensed under the MIT License. Use of this source code is governed by the
|
|
4
|
+
* MIT license that can be found in the LICENSE file.
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* oxlint sub-export.
|
|
8
|
+
*
|
|
9
|
+
* Consumers wire oxlint to this entry via:
|
|
10
|
+
* { "jsPlugins": ["eslint-plugin-reliability/oxlint"] }
|
|
11
|
+
*
|
|
12
|
+
* oxlint's JS plugin loader does `require('eslint-plugin-reliability/oxlint')` and reads
|
|
13
|
+
* `.rules` off the result. The compiled output of `export = plugin` is
|
|
14
|
+
* `module.exports = plugin`, which gives oxlint the plugin object directly
|
|
15
|
+
* (`{ meta, rules }`) — no wrapper, no `.default` indirection.
|
|
16
|
+
*
|
|
17
|
+
* Generated by scripts/generate-oxlint-shims.mjs. Do not edit by hand.
|
|
18
|
+
*/
|
|
19
|
+
import { plugin } from './index';
|
|
20
|
+
export = plugin;
|
package/src/oxlint.js
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Copyright (c) 2025 Ofri Peretz
|
|
4
|
+
* Licensed under the MIT License. Use of this source code is governed by the
|
|
5
|
+
* MIT license that can be found in the LICENSE file.
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* oxlint sub-export.
|
|
9
|
+
*
|
|
10
|
+
* Consumers wire oxlint to this entry via:
|
|
11
|
+
* { "jsPlugins": ["eslint-plugin-reliability/oxlint"] }
|
|
12
|
+
*
|
|
13
|
+
* oxlint's JS plugin loader does `require('eslint-plugin-reliability/oxlint')` and reads
|
|
14
|
+
* `.rules` off the result. The compiled output of `export = plugin` is
|
|
15
|
+
* `module.exports = plugin`, which gives oxlint the plugin object directly
|
|
16
|
+
* (`{ meta, rules }`) — no wrapper, no `.default` indirection.
|
|
17
|
+
*
|
|
18
|
+
* Generated by scripts/generate-oxlint-shims.mjs. Do not edit by hand.
|
|
19
|
+
*/
|
|
20
|
+
const index_1 = require("./index");
|
|
21
|
+
module.exports = index_1.plugin;
|
|
@@ -13,6 +13,7 @@ exports.errorMessage = (0, eslint_devkit_1.createRule)({
|
|
|
13
13
|
meta: {
|
|
14
14
|
type: 'problem',
|
|
15
15
|
docs: {
|
|
16
|
+
url: 'https://github.com/ofri-peretz/eslint/blob/main/packages/eslint-plugin-reliability/docs/rules/error-message.md',
|
|
16
17
|
description: 'Enforce providing a message when creating built-in Error objects for better debugging',
|
|
17
18
|
},
|
|
18
19
|
hasSuggestions: true,
|
|
@@ -64,6 +65,7 @@ exports.errorMessage = (0, eslint_devkit_1.createRule)({
|
|
|
64
65
|
function isErrorConstructor(name) {
|
|
65
66
|
return errorConstructors.has(name);
|
|
66
67
|
}
|
|
68
|
+
// oxlint-disable-next-line consistent-function-scoping
|
|
67
69
|
function hasMessageArgument(node) {
|
|
68
70
|
// Check if there are arguments
|
|
69
71
|
if (!node.arguments || node.arguments.length === 0) {
|
|
@@ -15,12 +15,25 @@ function hasErrorMessage(node) {
|
|
|
15
15
|
if (!node.argument) {
|
|
16
16
|
return false;
|
|
17
17
|
}
|
|
18
|
+
// `throw error;` — re-throwing a caught/named identifier. The original
|
|
19
|
+
// error already carries its own message + stack; demanding context on the
|
|
20
|
+
// re-throw is an FP. Same for `throw err`, `throw cause`, etc. Excludes
|
|
21
|
+
// the literal-like `undefined` / `NaN` / `Infinity` identifiers, which
|
|
22
|
+
// are typed as global constants but carry no diagnostic value.
|
|
23
|
+
if (node.argument.type === 'Identifier' &&
|
|
24
|
+
node.argument.name !== 'undefined' &&
|
|
25
|
+
node.argument.name !== 'NaN' &&
|
|
26
|
+
node.argument.name !== 'Infinity') {
|
|
27
|
+
return true;
|
|
28
|
+
}
|
|
18
29
|
// Check if it's a new Error() with message (includes TypeError, ReferenceError, etc.)
|
|
19
30
|
if (node.argument.type === 'NewExpression' &&
|
|
20
31
|
node.argument.callee.type === 'Identifier' &&
|
|
21
32
|
(node.argument.callee.name === 'Error' ||
|
|
22
33
|
node.argument.callee.name.endsWith('Error'))) {
|
|
23
|
-
// Check if first argument is a string (message)
|
|
34
|
+
// Check if first argument is a string (message) OR ANY expression — a
|
|
35
|
+
// custom error class like `new UserNotFoundError(userId)` builds its
|
|
36
|
+
// own message internally; the constructor argument IS the context.
|
|
24
37
|
if (node.argument.arguments.length > 0) {
|
|
25
38
|
const firstArg = node.argument.arguments[0];
|
|
26
39
|
if (firstArg.type === 'Literal' && typeof firstArg.value === 'string') {
|
|
@@ -29,6 +42,13 @@ function hasErrorMessage(node) {
|
|
|
29
42
|
if (firstArg.type === 'TemplateLiteral') {
|
|
30
43
|
return true;
|
|
31
44
|
}
|
|
45
|
+
// Non-string argument to a custom *Error class — accept as context.
|
|
46
|
+
// The rule's purpose is "throws should carry information"; passing an
|
|
47
|
+
// identifier/object to the error constructor carries information.
|
|
48
|
+
if (node.argument.callee.name !== 'Error' &&
|
|
49
|
+
node.argument.callee.name.endsWith('Error')) {
|
|
50
|
+
return true;
|
|
51
|
+
}
|
|
32
52
|
}
|
|
33
53
|
}
|
|
34
54
|
// Check if it's a string literal
|
|
@@ -49,6 +69,15 @@ function hasErrorStack(node) {
|
|
|
49
69
|
if (!node.argument) {
|
|
50
70
|
return false;
|
|
51
71
|
}
|
|
72
|
+
// `throw error;` — caught/named identifier already has a stack from the
|
|
73
|
+
// place where it was first thrown. Re-throws preserve the stack. Excludes
|
|
74
|
+
// `undefined` / `NaN` / `Infinity` global-constant identifiers.
|
|
75
|
+
if (node.argument.type === 'Identifier' &&
|
|
76
|
+
node.argument.name !== 'undefined' &&
|
|
77
|
+
node.argument.name !== 'NaN' &&
|
|
78
|
+
node.argument.name !== 'Infinity') {
|
|
79
|
+
return true;
|
|
80
|
+
}
|
|
52
81
|
// Check if it's a new Error() instance
|
|
53
82
|
if (node.argument.type === 'NewExpression' &&
|
|
54
83
|
node.argument.callee.type === 'Identifier') {
|
|
@@ -65,6 +94,7 @@ exports.noMissingErrorContext = (0, eslint_devkit_2.createRule)({
|
|
|
65
94
|
meta: {
|
|
66
95
|
type: 'suggestion',
|
|
67
96
|
docs: {
|
|
97
|
+
url: 'https://github.com/ofri-peretz/eslint/blob/main/packages/eslint-plugin-reliability/docs/rules/no-missing-error-context.md',
|
|
68
98
|
description: 'Detects thrown errors without context',
|
|
69
99
|
},
|
|
70
100
|
hasSuggestions: true,
|
|
@@ -135,7 +165,7 @@ exports.noMissingErrorContext = (0, eslint_devkit_2.createRule)({
|
|
|
135
165
|
],
|
|
136
166
|
create(context, [options = {}]) {
|
|
137
167
|
const { requireMessage = true, requireStackTrace = false, ignoreInTests = true, } = options || {};
|
|
138
|
-
const filename = context.
|
|
168
|
+
const filename = context.filename;
|
|
139
169
|
const isTestFile = ignoreInTests && /\.(test|spec)\.(ts|tsx|js|jsx)$/.test(filename);
|
|
140
170
|
if (isTestFile) {
|
|
141
171
|
return {};
|
|
@@ -71,6 +71,7 @@ exports.noSilentErrors = (0, eslint_devkit_2.createRule)({
|
|
|
71
71
|
meta: {
|
|
72
72
|
type: 'problem',
|
|
73
73
|
docs: {
|
|
74
|
+
url: 'https://github.com/ofri-peretz/eslint/blob/main/packages/eslint-plugin-reliability/docs/rules/no-silent-errors.md',
|
|
74
75
|
description: 'Detects empty catch blocks',
|
|
75
76
|
},
|
|
76
77
|
hasSuggestions: true,
|
|
@@ -135,12 +136,12 @@ exports.noSilentErrors = (0, eslint_devkit_2.createRule)({
|
|
|
135
136
|
],
|
|
136
137
|
create(context, [options = {}]) {
|
|
137
138
|
const { allowWithComment = false, ignoreInTests = true } = options || {};
|
|
138
|
-
const filename = context.
|
|
139
|
+
const filename = context.filename;
|
|
139
140
|
const isTestFile = ignoreInTests && /\.(test|spec)\.(ts|tsx|js|jsx)$/.test(filename);
|
|
140
141
|
if (isTestFile) {
|
|
141
142
|
return {};
|
|
142
143
|
}
|
|
143
|
-
const sourceCode = context.sourceCode
|
|
144
|
+
const sourceCode = context.sourceCode;
|
|
144
145
|
/**
|
|
145
146
|
* Check catch clauses
|
|
146
147
|
*/
|
|
@@ -9,20 +9,113 @@ exports.noUnhandledPromise = void 0;
|
|
|
9
9
|
const eslint_devkit_1 = require("@interlace/eslint-devkit");
|
|
10
10
|
const eslint_devkit_2 = require("@interlace/eslint-devkit");
|
|
11
11
|
/**
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
* non-promise calls that are inside handled promise chains.
|
|
12
|
+
* Built-in / library calls that are KNOWN to NOT return a promise. Firing
|
|
13
|
+
* on these produces FPs (e.g., `setTimeout(...)`, `console.log(...)`,
|
|
14
|
+
* `Math.floor(...)` are not unhandled promises).
|
|
16
15
|
*/
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
16
|
+
const NEVER_RETURNS_PROMISE_FUNCTIONS = new Set([
|
|
17
|
+
'setTimeout', 'clearTimeout', 'setInterval', 'clearInterval',
|
|
18
|
+
'setImmediate', 'clearImmediate',
|
|
19
|
+
'requestAnimationFrame', 'cancelAnimationFrame',
|
|
20
|
+
'queueMicrotask',
|
|
21
|
+
'String', 'Number', 'Boolean', 'Symbol', 'BigInt',
|
|
22
|
+
'parseInt', 'parseFloat', 'isNaN', 'isFinite',
|
|
23
|
+
'Array', 'Object',
|
|
24
|
+
]);
|
|
25
|
+
const NEVER_RETURNS_PROMISE_METHODS = new Set([
|
|
26
|
+
// console / logger
|
|
27
|
+
'log', 'error', 'warn', 'info', 'debug', 'trace', 'group', 'groupEnd',
|
|
28
|
+
'time', 'timeEnd', 'assert',
|
|
29
|
+
// Math
|
|
30
|
+
'floor', 'ceil', 'round', 'abs', 'min', 'max', 'pow', 'sqrt', 'random',
|
|
31
|
+
'sin', 'cos', 'tan', 'log2', 'log10',
|
|
32
|
+
// String / Array helpers
|
|
33
|
+
'slice', 'split', 'join', 'concat', 'includes', 'indexOf', 'lastIndexOf',
|
|
34
|
+
'startsWith', 'endsWith', 'replace', 'replaceAll', 'trim', 'toLowerCase', 'toUpperCase',
|
|
35
|
+
'repeat', 'padStart', 'padEnd', 'charAt', 'charCodeAt', 'codePointAt',
|
|
36
|
+
'push', 'pop', 'shift', 'unshift', 'splice', 'reverse', 'sort',
|
|
37
|
+
'map', 'filter', 'reduce', 'reduceRight', 'forEach', 'every', 'some', 'find', 'findIndex',
|
|
38
|
+
'flat', 'flatMap', 'fill', 'copyWithin', 'entries', 'keys', 'values',
|
|
39
|
+
// JSON
|
|
40
|
+
'parse', 'stringify',
|
|
41
|
+
// AbortController/AbortSignal
|
|
42
|
+
'abort', 'addEventListener', 'removeEventListener', 'dispatchEvent',
|
|
43
|
+
// Date / Buffer / Number / Array static helpers (sync)
|
|
44
|
+
'now', 'parse', 'UTC', 'from', 'of', 'isArray', 'isBuffer',
|
|
45
|
+
'isInteger', 'isFinite', 'isNaN', 'isSafeInteger',
|
|
46
|
+
'fromCharCode', 'fromCodePoint', 'raw',
|
|
47
|
+
// Object helpers (sync)
|
|
48
|
+
'assign', 'freeze', 'isFrozen', 'create', 'defineProperty', 'defineProperties',
|
|
49
|
+
'getOwnPropertyDescriptor', 'getOwnPropertyNames', 'getPrototypeOf', 'setPrototypeOf',
|
|
50
|
+
'preventExtensions', 'isExtensible', 'seal', 'isSealed', 'fromEntries',
|
|
51
|
+
// Promise constructors that are themselves a promise but the callee is OK
|
|
52
|
+
]);
|
|
53
|
+
/**
|
|
54
|
+
* Globals whose methods are conventionally synchronous (no method on these
|
|
55
|
+
* namespaces returns a Promise in the standard library). Used in addition
|
|
56
|
+
* to `NEVER_RETURNS_PROMISE_METHODS` because matching by method name alone
|
|
57
|
+
* is too coarse: `from` is sync on `Array`/`Buffer`/`Date` but could be
|
|
58
|
+
* async on a user-defined object.
|
|
59
|
+
*/
|
|
60
|
+
const SYNC_NAMESPACE_OBJECTS = new Set([
|
|
61
|
+
'Math', 'JSON', 'Date', 'Buffer', 'Array', 'Object', 'Number', 'String',
|
|
62
|
+
'Boolean', 'Symbol', 'BigInt', 'Reflect', 'console', 'process',
|
|
63
|
+
]);
|
|
64
|
+
/**
|
|
65
|
+
* Returns true if the call MIGHT return a Promise (default — we want to
|
|
66
|
+
* preserve detection for unknown calls). Returns false only when the
|
|
67
|
+
* callee is a known synchronous built-in (`setTimeout`, `console.log`,
|
|
68
|
+
* `Math.floor`, etc.) — those structurally never return promises and
|
|
69
|
+
* firing on them produces FPs. Keeping the default as "could be a
|
|
70
|
+
* promise" preserves recall on user-defined async functions.
|
|
71
|
+
*/
|
|
72
|
+
function isLikelyPromiseExpression(node) {
|
|
73
|
+
if (node.type !== 'CallExpression')
|
|
74
|
+
return false;
|
|
75
|
+
const callee = node.callee;
|
|
76
|
+
// Direct calls — skip known synchronous built-ins
|
|
77
|
+
if (callee.type === 'Identifier') {
|
|
78
|
+
const name = callee.name;
|
|
79
|
+
if (NEVER_RETURNS_PROMISE_FUNCTIONS.has(name))
|
|
80
|
+
return false;
|
|
20
81
|
return true;
|
|
21
82
|
}
|
|
22
|
-
//
|
|
23
|
-
|
|
24
|
-
|
|
83
|
+
// Method calls — skip known synchronous methods (Math.*, Array.*,
|
|
84
|
+
// String.*, console.*, JSON.*)
|
|
85
|
+
if (callee.type === 'MemberExpression') {
|
|
86
|
+
const prop = callee.property;
|
|
87
|
+
if (prop.type === 'Identifier') {
|
|
88
|
+
if (NEVER_RETURNS_PROMISE_METHODS.has(prop.name))
|
|
89
|
+
return false;
|
|
90
|
+
}
|
|
91
|
+
// Static helpers on known sync namespaces — `Buffer.from`, `Date.now`,
|
|
92
|
+
// `Array.isArray`, `Object.keys`, etc. The standard library never
|
|
93
|
+
// returns a Promise from any method on these globals.
|
|
94
|
+
const obj = callee.object;
|
|
95
|
+
if (obj.type === 'Identifier' && SYNC_NAMESPACE_OBJECTS.has(obj.name)) {
|
|
96
|
+
return false;
|
|
97
|
+
}
|
|
98
|
+
return true;
|
|
25
99
|
}
|
|
100
|
+
return true;
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Returns true when the call's parent indicates the promise is delegated
|
|
104
|
+
* to a caller and therefore not "unhandled" at this site. This covers:
|
|
105
|
+
* - `return fn()` — the enclosing function returns the promise; its
|
|
106
|
+
* caller takes responsibility.
|
|
107
|
+
* - `() => fn()` — concise-body arrow returns the promise.
|
|
108
|
+
* - `.then(() => fn())` — already inside a promise chain (handled by
|
|
109
|
+
* `isInsidePromiseCallback`, but the arrow-body case is the same).
|
|
110
|
+
*/
|
|
111
|
+
function isPromiseDelegatedToCaller(node) {
|
|
112
|
+
const parent = node.parent;
|
|
113
|
+
if (!parent)
|
|
114
|
+
return false;
|
|
115
|
+
if (parent.type === 'ReturnStatement')
|
|
116
|
+
return true;
|
|
117
|
+
if (parent.type === 'ArrowFunctionExpression' && parent.body === node)
|
|
118
|
+
return true;
|
|
26
119
|
return false;
|
|
27
120
|
}
|
|
28
121
|
/**
|
|
@@ -136,7 +229,10 @@ exports.noUnhandledPromise = (0, eslint_devkit_2.createRule)({
|
|
|
136
229
|
meta: {
|
|
137
230
|
type: 'problem',
|
|
138
231
|
docs: {
|
|
232
|
+
url: 'https://github.com/ofri-peretz/eslint/blob/main/packages/eslint-plugin-reliability/docs/rules/no-unhandled-promise.md',
|
|
139
233
|
description: 'Detects unhandled Promise rejections',
|
|
234
|
+
cwe: 'CWE-1024',
|
|
235
|
+
cvss: 7.5,
|
|
140
236
|
},
|
|
141
237
|
hasSuggestions: true,
|
|
142
238
|
messages: {
|
|
@@ -201,12 +297,12 @@ exports.noUnhandledPromise = (0, eslint_devkit_2.createRule)({
|
|
|
201
297
|
],
|
|
202
298
|
create(context, [options = {}]) {
|
|
203
299
|
const { ignoreInTests = true, ignoreVoidExpressions = false } = options || {};
|
|
204
|
-
const filename = context.
|
|
300
|
+
const filename = context.filename;
|
|
205
301
|
const isTestFile = ignoreInTests && /\.(test|spec)\.(ts|tsx|js|jsx)$/.test(filename);
|
|
206
302
|
if (isTestFile) {
|
|
207
303
|
return {};
|
|
208
304
|
}
|
|
209
|
-
// const sourceCode = context.sourceCode
|
|
305
|
+
// const sourceCode = context.sourceCode; // Not used
|
|
210
306
|
/**
|
|
211
307
|
* Check call expressions for unhandled promises
|
|
212
308
|
*/
|
|
@@ -215,6 +311,12 @@ exports.noUnhandledPromise = (0, eslint_devkit_2.createRule)({
|
|
|
215
311
|
if (isInsidePromiseCallback(node)) {
|
|
216
312
|
return;
|
|
217
313
|
}
|
|
314
|
+
// `return fn()` / `() => fn()` — the promise is delegated to the
|
|
315
|
+
// caller. Flagging here would force `await` everywhere a promise
|
|
316
|
+
// is forwarded, which is wrong: forwarding IS handling.
|
|
317
|
+
if (isPromiseDelegatedToCaller(node)) {
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
218
320
|
// Skip calls to promise methods (.then, .catch, .finally) as they are handled by definition
|
|
219
321
|
// But only if they have meaningful callbacks
|
|
220
322
|
if (node.callee.type === 'MemberExpression' &&
|
|
@@ -241,7 +343,7 @@ exports.noUnhandledPromise = (0, eslint_devkit_2.createRule)({
|
|
|
241
343
|
}
|
|
242
344
|
}
|
|
243
345
|
// Check if it's a promise-returning function
|
|
244
|
-
if (!
|
|
346
|
+
if (!isLikelyPromiseExpression(node)) {
|
|
245
347
|
return;
|
|
246
348
|
}
|
|
247
349
|
// Check if it's already handled
|
|
@@ -307,7 +409,7 @@ exports.noUnhandledPromise = (0, eslint_devkit_2.createRule)({
|
|
|
307
409
|
}
|
|
308
410
|
|
|
309
411
|
// Check if it's a promise-like identifier
|
|
310
|
-
if (!
|
|
412
|
+
if (!isLikelyPromiseExpression(node)) {
|
|
311
413
|
return;
|
|
312
414
|
}
|
|
313
415
|
|
|
@@ -13,6 +13,7 @@ exports.noAwaitInLoop = (0, eslint_devkit_1.createRule)({
|
|
|
13
13
|
meta: {
|
|
14
14
|
type: 'suggestion',
|
|
15
15
|
docs: {
|
|
16
|
+
url: 'https://github.com/ofri-peretz/eslint/blob/main/packages/eslint-plugin-reliability/docs/rules/no-await-in-loop.md',
|
|
16
17
|
description: 'Disallow await inside loops and suggest appropriate concurrency patterns',
|
|
17
18
|
},
|
|
18
19
|
hasSuggestions: false,
|
|
@@ -137,7 +138,7 @@ exports.noAwaitInLoop = (0, eslint_devkit_1.createRule)({
|
|
|
137
138
|
}
|
|
138
139
|
}
|
|
139
140
|
function analyzeLoopContext(node, loopType) {
|
|
140
|
-
const
|
|
141
|
+
const loopCtx = {
|
|
141
142
|
loopType,
|
|
142
143
|
operationCount: 1,
|
|
143
144
|
hasDependencies: false,
|
|
@@ -153,28 +154,28 @@ exports.noAwaitInLoop = (0, eslint_devkit_1.createRule)({
|
|
|
153
154
|
function analyzeOperations(currentNode) {
|
|
154
155
|
if (currentNode.type === 'CallExpression') {
|
|
155
156
|
if (currentNode.callee.type === 'Identifier') {
|
|
156
|
-
|
|
157
|
+
loopCtx.operations.push(currentNode.callee.name);
|
|
157
158
|
}
|
|
158
159
|
else if (currentNode.callee.type === 'MemberExpression' &&
|
|
159
160
|
currentNode.callee.property.type === 'Identifier') {
|
|
160
|
-
|
|
161
|
+
loopCtx.operations.push(currentNode.callee.property.name);
|
|
161
162
|
}
|
|
162
163
|
}
|
|
163
164
|
// Check for dependencies between iterations
|
|
164
165
|
if (currentNode.type === 'AssignmentExpression' ||
|
|
165
166
|
currentNode.type === 'UpdateExpression') {
|
|
166
|
-
|
|
167
|
+
loopCtx.hasDependencies = true;
|
|
167
168
|
}
|
|
168
169
|
// Check for side effects that might require sequential execution
|
|
169
170
|
if (currentNode.type === 'CallExpression' &&
|
|
170
|
-
(
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
171
|
+
(loopCtx.operations.includes('push') ||
|
|
172
|
+
loopCtx.operations.includes('splice') ||
|
|
173
|
+
loopCtx.operations.includes('delete'))) {
|
|
174
|
+
loopCtx.hasSideEffects = true;
|
|
174
175
|
}
|
|
175
176
|
// Count operations
|
|
176
177
|
if (currentNode.type === 'AwaitExpression') {
|
|
177
|
-
|
|
178
|
+
loopCtx.operationCount++;
|
|
178
179
|
}
|
|
179
180
|
// Recursively analyze
|
|
180
181
|
for (const key in currentNode) {
|
|
@@ -195,18 +196,19 @@ exports.noAwaitInLoop = (0, eslint_devkit_1.createRule)({
|
|
|
195
196
|
}
|
|
196
197
|
analyzeOperations(node);
|
|
197
198
|
// Determine if operations can be concurrent
|
|
198
|
-
if (
|
|
199
|
-
|
|
200
|
-
|
|
199
|
+
if (loopCtx.hasDependencies || loopCtx.hasSideEffects) {
|
|
200
|
+
loopCtx.isSequential = true;
|
|
201
|
+
loopCtx.estimatedPerformance = `sequential (${loopCtx.operationCount} operations)`;
|
|
201
202
|
}
|
|
202
203
|
else {
|
|
203
|
-
|
|
204
|
-
|
|
204
|
+
loopCtx.isSequential = false;
|
|
205
|
+
loopCtx.estimatedPerformance = `potential ${loopCtx.operationCount}x speedup with concurrency`;
|
|
205
206
|
}
|
|
206
|
-
return
|
|
207
|
+
return loopCtx;
|
|
207
208
|
}
|
|
208
|
-
|
|
209
|
-
|
|
209
|
+
// oxlint-disable-next-line consistent-function-scoping
|
|
210
|
+
function getSuggestion(loopCtx) {
|
|
211
|
+
if (loopCtx.isSequential) {
|
|
210
212
|
return 'operations may need to be sequential - consider if concurrency is safe';
|
|
211
213
|
}
|
|
212
214
|
else {
|
|
@@ -60,6 +60,7 @@ exports.noJsdocTerminatorInExample = (0, eslint_devkit_1.createRule)({
|
|
|
60
60
|
meta: {
|
|
61
61
|
type: 'problem',
|
|
62
62
|
docs: {
|
|
63
|
+
url: 'https://github.com/ofri-peretz/eslint/blob/main/packages/eslint-plugin-reliability/docs/rules/no-jsdoc-terminator-in-example.md',
|
|
63
64
|
description: 'Detects `*/` sequences inside JSDoc @example blocks that prematurely close the comment',
|
|
64
65
|
},
|
|
65
66
|
fixable: 'code',
|
|
@@ -86,7 +87,7 @@ exports.noJsdocTerminatorInExample = (0, eslint_devkit_1.createRule)({
|
|
|
86
87
|
},
|
|
87
88
|
defaultOptions: [],
|
|
88
89
|
create(context) {
|
|
89
|
-
const sourceCode = context.sourceCode
|
|
90
|
+
const sourceCode = context.sourceCode;
|
|
90
91
|
return {
|
|
91
92
|
Program() {
|
|
92
93
|
const comments = sourceCode.getAllComments();
|
|
@@ -8,6 +8,124 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
8
8
|
exports.noMissingNullChecks = void 0;
|
|
9
9
|
const eslint_devkit_1 = require("@interlace/eslint-devkit");
|
|
10
10
|
const eslint_devkit_2 = require("@interlace/eslint-devkit");
|
|
11
|
+
/**
|
|
12
|
+
* Globals and built-ins that are never null/undefined. Property access on
|
|
13
|
+
* these doesn't need a null check.
|
|
14
|
+
*/
|
|
15
|
+
// Built-in namespaces that are never null AND aren't typically mutated. We
|
|
16
|
+
// deliberately exclude `globalThis`, `window`, `self`, `top`, `parent`, and
|
|
17
|
+
// `document` — those are mutable and `globalThis.appState = ...` is a real
|
|
18
|
+
// "global state mutation" antipattern that other rules want to catch.
|
|
19
|
+
// Read-only namespaces (Math, JSON, …) and library singletons are safe to
|
|
20
|
+
// exempt because their property access is idempotent.
|
|
21
|
+
const NEVER_NULL_GLOBALS = new Set([
|
|
22
|
+
// Read-only Node/V8 magics
|
|
23
|
+
'console', 'process', 'Buffer', '__dirname', '__filename', 'module', 'exports', 'require',
|
|
24
|
+
'navigator', 'location', 'history',
|
|
25
|
+
// Built-in objects (read-only namespaces)
|
|
26
|
+
'Math', 'JSON', 'Object', 'Array', 'Number', 'String', 'Boolean', 'Date',
|
|
27
|
+
'RegExp', 'Promise', 'Symbol', 'Map', 'Set', 'WeakMap', 'WeakSet',
|
|
28
|
+
'Proxy', 'Reflect', 'Intl', 'BigInt', 'WebAssembly', 'Atomics',
|
|
29
|
+
'URL', 'URLSearchParams', 'TextEncoder', 'TextDecoder',
|
|
30
|
+
'AbortController', 'AbortSignal', 'EventTarget', 'Event', 'CustomEvent',
|
|
31
|
+
'FormData', 'Blob', 'File', 'FileReader', 'Headers', 'Request', 'Response',
|
|
32
|
+
// Error classes
|
|
33
|
+
'Error', 'TypeError', 'RangeError', 'SyntaxError', 'URIError',
|
|
34
|
+
'EvalError', 'ReferenceError', 'AggregateError',
|
|
35
|
+
// Common library / framework names
|
|
36
|
+
'fetch', 'crypto', 'performance', 'queueMicrotask',
|
|
37
|
+
'setTimeout', 'clearTimeout', 'setInterval', 'clearInterval',
|
|
38
|
+
'setImmediate', 'clearImmediate', 'requestAnimationFrame', 'cancelAnimationFrame',
|
|
39
|
+
// Common loggers (used as singletons)
|
|
40
|
+
'logger', 'log', 'winston', 'pino', 'bunyan',
|
|
41
|
+
]);
|
|
42
|
+
/**
|
|
43
|
+
* Returns true if the identifier resolves to:
|
|
44
|
+
* - A NEVER_NULL_GLOBALS entry (built-in / known singleton)
|
|
45
|
+
* - A catch-clause parameter (`catch (e) { e.message }` — never null)
|
|
46
|
+
* - A variable initialized by `new X(...)` (constructor result is never null)
|
|
47
|
+
* - A top-level import (`import x from 'y'` — never null)
|
|
48
|
+
*
|
|
49
|
+
* For these, the rule should not demand a null check.
|
|
50
|
+
*/
|
|
51
|
+
function isProvablyNonNullableIdentifier(ident, scope) {
|
|
52
|
+
if (NEVER_NULL_GLOBALS.has(ident.name))
|
|
53
|
+
return true;
|
|
54
|
+
// Walk scope chain to find the resolved variable
|
|
55
|
+
let s = scope;
|
|
56
|
+
while (s) {
|
|
57
|
+
const variable = s.variables.find((v) => v.name === ident.name);
|
|
58
|
+
if (variable) {
|
|
59
|
+
for (const def of variable.defs) {
|
|
60
|
+
// catch (e) { ... }
|
|
61
|
+
if (def.type === 'CatchClause')
|
|
62
|
+
return true;
|
|
63
|
+
// import x from 'y' / import { x } from 'y'
|
|
64
|
+
if (def.type === 'ImportBinding')
|
|
65
|
+
return true;
|
|
66
|
+
// const x = new Foo(...) / array literal / object literal / primitive
|
|
67
|
+
// / class declaration / `await fetch(...)` / template literal — these
|
|
68
|
+
// initializers cannot produce null/undefined under normal control flow.
|
|
69
|
+
// Adding them closes the bulk of the ILB-Arena-Quality FPs without
|
|
70
|
+
// sacrificing real CWE-476 detection (genuine null-deref risks come
|
|
71
|
+
// from optional/maybe lookups, not from `const x = []`).
|
|
72
|
+
if (def.type === 'Variable' && def.node?.type === 'VariableDeclarator') {
|
|
73
|
+
const init = def.node.init;
|
|
74
|
+
if (!init)
|
|
75
|
+
continue;
|
|
76
|
+
if (init.type === 'NewExpression')
|
|
77
|
+
return true;
|
|
78
|
+
if (init.type === 'ArrayExpression')
|
|
79
|
+
return true;
|
|
80
|
+
if (init.type === 'ObjectExpression')
|
|
81
|
+
return true;
|
|
82
|
+
if (init.type === 'TemplateLiteral')
|
|
83
|
+
return true;
|
|
84
|
+
if (init.type === 'ClassExpression')
|
|
85
|
+
return true;
|
|
86
|
+
// Primitive literals (string / number / boolean / regex) — never null.
|
|
87
|
+
// Skip the `null` literal itself — that IS a null and the rule
|
|
88
|
+
// should let other rules complain about it.
|
|
89
|
+
if (init.type === 'Literal' && init.value !== null)
|
|
90
|
+
return true;
|
|
91
|
+
// `await fetch(url)` / `await fetch(url, opts)` — the WHATWG fetch
|
|
92
|
+
// contract guarantees a Response on resolution; null comes only
|
|
93
|
+
// through rejection, which throws past this assignment.
|
|
94
|
+
if (init.type === 'AwaitExpression' &&
|
|
95
|
+
init.argument.type === 'CallExpression' &&
|
|
96
|
+
init.argument.callee.type === 'Identifier' &&
|
|
97
|
+
init.argument.callee.name === 'fetch')
|
|
98
|
+
return true;
|
|
99
|
+
// `JSON.parse(...)` returns a value; typically non-null. Same for
|
|
100
|
+
// common Object/Array static methods.
|
|
101
|
+
if (init.type === 'CallExpression' &&
|
|
102
|
+
init.callee.type === 'MemberExpression' &&
|
|
103
|
+
init.callee.object.type === 'Identifier' &&
|
|
104
|
+
(init.callee.object.name === 'Object' || init.callee.object.name === 'Array' ||
|
|
105
|
+
init.callee.object.name === 'JSON'))
|
|
106
|
+
return true;
|
|
107
|
+
}
|
|
108
|
+
// function f(...) — function declaration name is never null
|
|
109
|
+
if (def.type === 'FunctionName')
|
|
110
|
+
return true;
|
|
111
|
+
// class C {} — class declaration name is never null
|
|
112
|
+
if (def.type === 'ClassName')
|
|
113
|
+
return true;
|
|
114
|
+
// Function/method parameters — without type information we cannot
|
|
115
|
+
// tell `(x: T)` from `(x: T | null)`, and firing on every param-deref
|
|
116
|
+
// is the dominant FP source on real codebases (53 → 44 → most-of-rest).
|
|
117
|
+
// The contract is the caller's responsibility; treat params as
|
|
118
|
+
// non-nullable by default. Type-aware analysis (TS) is the right
|
|
119
|
+
// tool for real param-nullability detection.
|
|
120
|
+
if (def.type === 'Parameter')
|
|
121
|
+
return true;
|
|
122
|
+
}
|
|
123
|
+
return false;
|
|
124
|
+
}
|
|
125
|
+
s = s.upper;
|
|
126
|
+
}
|
|
127
|
+
return false;
|
|
128
|
+
}
|
|
11
129
|
/**
|
|
12
130
|
* Check if property access has null/undefined check
|
|
13
131
|
*/
|
|
@@ -113,7 +231,10 @@ exports.noMissingNullChecks = (0, eslint_devkit_2.createRule)({
|
|
|
113
231
|
meta: {
|
|
114
232
|
type: 'problem',
|
|
115
233
|
docs: {
|
|
234
|
+
url: 'https://github.com/ofri-peretz/eslint/blob/main/packages/eslint-plugin-reliability/docs/rules/no-missing-null-checks.md',
|
|
116
235
|
description: 'Detects potential null pointer dereferences',
|
|
236
|
+
cwe: 'CWE-476',
|
|
237
|
+
cvss: 7.5,
|
|
117
238
|
},
|
|
118
239
|
hasSuggestions: true,
|
|
119
240
|
messages: {
|
|
@@ -180,12 +301,12 @@ exports.noMissingNullChecks = (0, eslint_devkit_2.createRule)({
|
|
|
180
301
|
const { ignoreInTests = true,
|
|
181
302
|
// requireExplicitChecks = false, // Not used
|
|
182
303
|
} = options || {};
|
|
183
|
-
const filename = context.
|
|
304
|
+
const filename = context.filename;
|
|
184
305
|
const isTestFile = ignoreInTests && /\.(test|spec)\.(ts|tsx|js|jsx)$/.test(filename);
|
|
185
306
|
if (isTestFile) {
|
|
186
307
|
return {};
|
|
187
308
|
}
|
|
188
|
-
const sourceCode = context.sourceCode
|
|
309
|
+
const sourceCode = context.sourceCode;
|
|
189
310
|
// Track reported MemberExpression nodes to prevent duplicate reports
|
|
190
311
|
// Key format: "start-end" from node.range
|
|
191
312
|
const reportedMemberExpressions = new Set();
|
|
@@ -234,10 +355,30 @@ exports.noMissingNullChecks = (0, eslint_devkit_2.createRule)({
|
|
|
234
355
|
const objectNode = node.object;
|
|
235
356
|
let shouldCheck = false;
|
|
236
357
|
if (objectNode.type === 'Identifier') {
|
|
358
|
+
// Skip identifiers that resolve to globals, catch params, imports,
|
|
359
|
+
// function declarations, or `new X()` results — these are never null.
|
|
360
|
+
if (isProvablyNonNullableIdentifier(objectNode, sourceCode.getScope(node))) {
|
|
361
|
+
return;
|
|
362
|
+
}
|
|
237
363
|
shouldCheck = true;
|
|
238
364
|
}
|
|
239
365
|
else if (objectNode.type === 'MemberExpression') {
|
|
240
|
-
// Nested member expressions like value.nested.deep
|
|
366
|
+
// Nested member expressions like value.nested.deep — only fire on
|
|
367
|
+
// the deepest, where the leaf identifier matters most. Skip if the
|
|
368
|
+
// base of the chain is a known-non-null global (`console.log`,
|
|
369
|
+
// `JSON.stringify`, etc.) or is a `this`/`this.#field` chain — `this`
|
|
370
|
+
// inside a method is the class instance (never null), and private
|
|
371
|
+
// fields are always defined when accessed from inside the class.
|
|
372
|
+
let base = objectNode;
|
|
373
|
+
while (base.type === 'MemberExpression') {
|
|
374
|
+
base = base.object;
|
|
375
|
+
}
|
|
376
|
+
if (base.type === 'ThisExpression')
|
|
377
|
+
return;
|
|
378
|
+
if (base.type === 'Identifier' &&
|
|
379
|
+
isProvablyNonNullableIdentifier(base, sourceCode.getScope(node))) {
|
|
380
|
+
return;
|
|
381
|
+
}
|
|
241
382
|
shouldCheck = true;
|
|
242
383
|
}
|
|
243
384
|
if (shouldCheck && !hasNullCheck(node, sourceCode)) {
|
|
@@ -303,10 +444,22 @@ exports.noMissingNullChecks = (0, eslint_devkit_2.createRule)({
|
|
|
303
444
|
const objectNode = memberExpr.object;
|
|
304
445
|
let shouldCheck = false;
|
|
305
446
|
if (objectNode.type === 'Identifier') {
|
|
447
|
+
if (isProvablyNonNullableIdentifier(objectNode, sourceCode.getScope(memberExpr))) {
|
|
448
|
+
return;
|
|
449
|
+
}
|
|
306
450
|
shouldCheck = true;
|
|
307
451
|
}
|
|
308
452
|
else if (objectNode.type === 'MemberExpression') {
|
|
309
|
-
|
|
453
|
+
let base = objectNode;
|
|
454
|
+
while (base.type === 'MemberExpression') {
|
|
455
|
+
base = base.object;
|
|
456
|
+
}
|
|
457
|
+
if (base.type === 'ThisExpression')
|
|
458
|
+
return;
|
|
459
|
+
if (base.type === 'Identifier' &&
|
|
460
|
+
isProvablyNonNullableIdentifier(base, sourceCode.getScope(memberExpr))) {
|
|
461
|
+
return;
|
|
462
|
+
}
|
|
310
463
|
shouldCheck = true;
|
|
311
464
|
}
|
|
312
465
|
if (shouldCheck && !hasNullCheck(memberExpr, sourceCode)) {
|
|
@@ -69,6 +69,7 @@ exports.noUnsafeTypeNarrowing = (0, eslint_devkit_2.createRule)({
|
|
|
69
69
|
meta: {
|
|
70
70
|
type: 'problem',
|
|
71
71
|
docs: {
|
|
72
|
+
url: 'https://github.com/ofri-peretz/eslint/blob/main/packages/eslint-plugin-reliability/docs/rules/no-unsafe-type-narrowing.md',
|
|
72
73
|
description: 'Detects unsafe type narrowing patterns',
|
|
73
74
|
},
|
|
74
75
|
hasSuggestions: true,
|
|
@@ -133,12 +134,12 @@ exports.noUnsafeTypeNarrowing = (0, eslint_devkit_2.createRule)({
|
|
|
133
134
|
],
|
|
134
135
|
create(context, [options = {}]) {
|
|
135
136
|
const { ignoreInTests = true, allowWithComment = false } = options || {};
|
|
136
|
-
const filename = context.
|
|
137
|
+
const filename = context.filename;
|
|
137
138
|
const isTestFile = ignoreInTests && /\.(test|spec)\.(ts|tsx|js|jsx)$/.test(filename);
|
|
138
139
|
if (isTestFile) {
|
|
139
140
|
return {};
|
|
140
141
|
}
|
|
141
|
-
const sourceCode = context.sourceCode
|
|
142
|
+
const sourceCode = context.sourceCode;
|
|
142
143
|
/**
|
|
143
144
|
* Check type assertions
|
|
144
145
|
*/
|
|
@@ -17,7 +17,10 @@ exports.requireNetworkTimeout = (0, eslint_devkit_1.createRule)({
|
|
|
17
17
|
meta: {
|
|
18
18
|
type: 'problem',
|
|
19
19
|
docs: {
|
|
20
|
+
url: 'https://github.com/ofri-peretz/eslint/blob/main/packages/eslint-plugin-reliability/docs/rules/require-network-timeout.md',
|
|
20
21
|
description: 'Require timeout limits for network requests',
|
|
22
|
+
cwe: 'CWE-400',
|
|
23
|
+
cvss: 5,
|
|
21
24
|
},
|
|
22
25
|
messages: {
|
|
23
26
|
violationDetected: (0, eslint_devkit_1.formatLLMMessage)({
|
|
@@ -43,11 +46,15 @@ exports.requireNetworkTimeout = (0, eslint_devkit_1.createRule)({
|
|
|
43
46
|
callee.object.name === 'axios';
|
|
44
47
|
if (isFetch || isAxios) {
|
|
45
48
|
const optionsArg = node.arguments[1];
|
|
46
|
-
|
|
49
|
+
// Either an explicit `timeout` option, or an AbortSignal — both
|
|
50
|
+
// bound the request. `{ signal: controller.signal }` is the
|
|
51
|
+
// standard timeout-via-AbortController pattern; flagging it as
|
|
52
|
+
// "missing timeout" is a false positive.
|
|
53
|
+
const hasBound = optionsArg?.type === eslint_devkit_1.AST_NODE_TYPES.ObjectExpression &&
|
|
47
54
|
optionsArg.properties.some((p) => p.type === eslint_devkit_1.AST_NODE_TYPES.Property &&
|
|
48
55
|
p.key.type === eslint_devkit_1.AST_NODE_TYPES.Identifier &&
|
|
49
|
-
p.key.name === 'timeout');
|
|
50
|
-
if (!
|
|
56
|
+
(p.key.name === 'timeout' || p.key.name === 'signal'));
|
|
57
|
+
if (!hasBound) {
|
|
51
58
|
context.report({ node, messageId: 'violationDetected' });
|
|
52
59
|
}
|
|
53
60
|
}
|