eslint-plugin-reliability 3.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/README.md +159 -0
- package/package.json +55 -0
- package/src/index.d.ts +180 -0
- package/src/index.js +57 -0
- package/src/lib/eslint-plugin-reliability.d.ts +1 -0
- package/src/lib/eslint-plugin-reliability.js +6 -0
- package/src/rules/error-handling/error-message.d.ts +20 -0
- package/src/rules/error-handling/error-message.js +146 -0
- package/src/rules/error-handling/no-missing-error-context.d.ts +26 -0
- package/src/rules/error-handling/no-missing-error-context.js +183 -0
- package/src/rules/error-handling/no-silent-errors.d.ts +24 -0
- package/src/rules/error-handling/no-silent-errors.js +178 -0
- package/src/rules/error-handling/no-unhandled-promise.d.ts +26 -0
- package/src/rules/error-handling/no-unhandled-promise.js +351 -0
- package/src/rules/reliability/no-await-in-loop.d.ts +24 -0
- package/src/rules/reliability/no-await-in-loop.js +234 -0
- package/src/rules/reliability/no-missing-null-checks.d.ts +26 -0
- package/src/rules/reliability/no-missing-null-checks.js +351 -0
- package/src/rules/reliability/no-unsafe-type-narrowing.d.ts +24 -0
- package/src/rules/reliability/no-unsafe-type-narrowing.js +176 -0
- package/src/rules/reliability/require-network-timeout.d.ts +12 -0
- package/src/rules/reliability/require-network-timeout.js +57 -0
|
@@ -0,0 +1,351 @@
|
|
|
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
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8
|
+
exports.noMissingNullChecks = void 0;
|
|
9
|
+
const eslint_devkit_1 = require("@interlace/eslint-devkit");
|
|
10
|
+
const eslint_devkit_2 = require("@interlace/eslint-devkit");
|
|
11
|
+
/**
|
|
12
|
+
* Check if property access has null/undefined check
|
|
13
|
+
*/
|
|
14
|
+
function hasNullCheck(node, sourceCode) {
|
|
15
|
+
// Check if node itself uses optional chaining
|
|
16
|
+
if (node.optional) {
|
|
17
|
+
return true;
|
|
18
|
+
}
|
|
19
|
+
// Check if parent is optional chaining
|
|
20
|
+
const parent = node.parent;
|
|
21
|
+
if (parent && parent.type === 'ChainExpression') {
|
|
22
|
+
return true; // Optional chaining handles null/undefined
|
|
23
|
+
}
|
|
24
|
+
// Check if used with nullish coalescing
|
|
25
|
+
if (usesNullishCoalescing(node)) {
|
|
26
|
+
return true;
|
|
27
|
+
}
|
|
28
|
+
// Check for basic explicit null checks in if statements
|
|
29
|
+
// This is a simplified check that looks for patterns like:
|
|
30
|
+
// if (obj !== null) { obj.property; }
|
|
31
|
+
if (hasExplicitNullCheck(node, sourceCode)) {
|
|
32
|
+
return true;
|
|
33
|
+
}
|
|
34
|
+
// For more sophisticated null checking, we'd need control flow analysis
|
|
35
|
+
// This is a simplified implementation that checks for basic patterns
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Check for explicit null checks in if statements
|
|
40
|
+
*/
|
|
41
|
+
function hasExplicitNullCheck(node, sourceCode) {
|
|
42
|
+
// Walk up the AST to find if statements
|
|
43
|
+
let current = node;
|
|
44
|
+
let depth = 0;
|
|
45
|
+
const maxDepth = 10;
|
|
46
|
+
while (current && depth < maxDepth) {
|
|
47
|
+
const parent = current
|
|
48
|
+
.parent;
|
|
49
|
+
if (parent && parent.type === 'IfStatement') {
|
|
50
|
+
// Check if the test condition contains a null check for our object
|
|
51
|
+
const test = parent.test;
|
|
52
|
+
if (isNullCheckForObject(test, node.object, sourceCode)) {
|
|
53
|
+
return true;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
current = parent;
|
|
57
|
+
depth++;
|
|
58
|
+
}
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Check if a test expression is a null check for a specific object
|
|
63
|
+
*/
|
|
64
|
+
function isNullCheckForObject(test, object, sourceCode) {
|
|
65
|
+
// Handle binary expressions like obj !== null
|
|
66
|
+
if (test.type === 'BinaryExpression') {
|
|
67
|
+
const { left, right, operator } = test;
|
|
68
|
+
if (operator === '!==' ||
|
|
69
|
+
operator === '!=' ||
|
|
70
|
+
operator === '===' ||
|
|
71
|
+
operator === '==') {
|
|
72
|
+
const leftText = sourceCode.getText(left);
|
|
73
|
+
const rightText = sourceCode.getText(right);
|
|
74
|
+
const objectText = sourceCode.getText(object);
|
|
75
|
+
// Check if one side matches our object and the other is null/undefined
|
|
76
|
+
if ((leftText === objectText &&
|
|
77
|
+
(rightText === 'null' || rightText === 'undefined')) ||
|
|
78
|
+
(rightText === objectText &&
|
|
79
|
+
(leftText === 'null' || leftText === 'undefined'))) {
|
|
80
|
+
return true;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
// Handle logical expressions like obj !== null && obj !== undefined
|
|
85
|
+
if (test.type === 'LogicalExpression') {
|
|
86
|
+
return (isNullCheckForObject(test.left, object, sourceCode) ||
|
|
87
|
+
isNullCheckForObject(test.right, object, sourceCode));
|
|
88
|
+
}
|
|
89
|
+
return false;
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Check if expression uses nullish coalescing
|
|
93
|
+
*/
|
|
94
|
+
function usesNullishCoalescing(node) {
|
|
95
|
+
let current = node;
|
|
96
|
+
let depth = 0;
|
|
97
|
+
const maxDepth = 5;
|
|
98
|
+
while (current && depth < maxDepth) {
|
|
99
|
+
const parent = current
|
|
100
|
+
.parent;
|
|
101
|
+
if (parent &&
|
|
102
|
+
parent.type === 'LogicalExpression' &&
|
|
103
|
+
parent.operator === '??') {
|
|
104
|
+
return true;
|
|
105
|
+
}
|
|
106
|
+
current = parent;
|
|
107
|
+
depth++;
|
|
108
|
+
}
|
|
109
|
+
return false;
|
|
110
|
+
}
|
|
111
|
+
exports.noMissingNullChecks = (0, eslint_devkit_2.createRule)({
|
|
112
|
+
name: 'no-missing-null-checks',
|
|
113
|
+
meta: {
|
|
114
|
+
type: 'problem',
|
|
115
|
+
docs: {
|
|
116
|
+
description: 'Detects potential null pointer dereferences',
|
|
117
|
+
},
|
|
118
|
+
hasSuggestions: true,
|
|
119
|
+
messages: {
|
|
120
|
+
missingNullCheck: (0, eslint_devkit_1.formatLLMMessage)({
|
|
121
|
+
icon: eslint_devkit_1.MessageIcons.WARNING,
|
|
122
|
+
issueName: 'Missing null check',
|
|
123
|
+
cwe: 'CWE-476',
|
|
124
|
+
description: 'Potential null/undefined dereference detected',
|
|
125
|
+
severity: 'HIGH',
|
|
126
|
+
fix: 'Use optional chaining (?.) or add explicit null check',
|
|
127
|
+
documentationLink: 'https://rules.sonarsource.com/javascript/RSPEC-2259/',
|
|
128
|
+
}),
|
|
129
|
+
useOptionalChaining: (0, eslint_devkit_1.formatLLMMessage)({
|
|
130
|
+
icon: eslint_devkit_1.MessageIcons.INFO,
|
|
131
|
+
issueName: 'Use Optional Chaining',
|
|
132
|
+
description: 'Use optional chaining operator',
|
|
133
|
+
severity: 'LOW',
|
|
134
|
+
fix: 'obj?.property?.method()',
|
|
135
|
+
documentationLink: 'https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Optional_chaining',
|
|
136
|
+
}),
|
|
137
|
+
useNullishCoalescing: (0, eslint_devkit_1.formatLLMMessage)({
|
|
138
|
+
icon: eslint_devkit_1.MessageIcons.INFO,
|
|
139
|
+
issueName: 'Use Nullish Coalescing',
|
|
140
|
+
description: 'Use nullish coalescing operator',
|
|
141
|
+
severity: 'LOW',
|
|
142
|
+
fix: 'value ?? defaultValue',
|
|
143
|
+
documentationLink: 'https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Nullish_coalescing',
|
|
144
|
+
}),
|
|
145
|
+
addExplicitCheck: (0, eslint_devkit_1.formatLLMMessage)({
|
|
146
|
+
icon: eslint_devkit_1.MessageIcons.INFO,
|
|
147
|
+
issueName: 'Add Explicit Check',
|
|
148
|
+
description: 'Add explicit null check',
|
|
149
|
+
severity: 'LOW',
|
|
150
|
+
fix: 'if (obj !== null) { obj.property }',
|
|
151
|
+
documentationLink: 'https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/null',
|
|
152
|
+
}),
|
|
153
|
+
},
|
|
154
|
+
schema: [
|
|
155
|
+
{
|
|
156
|
+
type: 'object',
|
|
157
|
+
properties: {
|
|
158
|
+
ignoreInTests: {
|
|
159
|
+
type: 'boolean',
|
|
160
|
+
default: true,
|
|
161
|
+
description: 'Ignore in test files',
|
|
162
|
+
},
|
|
163
|
+
requireExplicitChecks: {
|
|
164
|
+
type: 'boolean',
|
|
165
|
+
default: false,
|
|
166
|
+
description: 'Require explicit null checks',
|
|
167
|
+
},
|
|
168
|
+
},
|
|
169
|
+
additionalProperties: false,
|
|
170
|
+
},
|
|
171
|
+
],
|
|
172
|
+
},
|
|
173
|
+
defaultOptions: [
|
|
174
|
+
{
|
|
175
|
+
ignoreInTests: true,
|
|
176
|
+
requireExplicitChecks: false,
|
|
177
|
+
},
|
|
178
|
+
],
|
|
179
|
+
create(context, [options = {}]) {
|
|
180
|
+
const { ignoreInTests = true,
|
|
181
|
+
// requireExplicitChecks = false, // Not used
|
|
182
|
+
} = options || {};
|
|
183
|
+
const filename = context.getFilename();
|
|
184
|
+
const isTestFile = ignoreInTests && /\.(test|spec)\.(ts|tsx|js|jsx)$/.test(filename);
|
|
185
|
+
if (isTestFile) {
|
|
186
|
+
return {};
|
|
187
|
+
}
|
|
188
|
+
const sourceCode = context.sourceCode || context.sourceCode;
|
|
189
|
+
// Track reported MemberExpression nodes to prevent duplicate reports
|
|
190
|
+
// Key format: "start-end" from node.range
|
|
191
|
+
const reportedMemberExpressions = new Set();
|
|
192
|
+
/**
|
|
193
|
+
* Get a unique key for a MemberExpression node to track if it's been reported
|
|
194
|
+
*/
|
|
195
|
+
function getMemberExpressionKey(node) {
|
|
196
|
+
// Use the node's range for a unique identifier
|
|
197
|
+
// Range is [start, end] character positions in the source
|
|
198
|
+
if (node.range && Array.isArray(node.range) && node.range.length >= 2) {
|
|
199
|
+
return `me-${node.range[0]}-${node.range[1]}`;
|
|
200
|
+
}
|
|
201
|
+
// Fallback: use location if range is not available
|
|
202
|
+
const loc = node
|
|
203
|
+
.loc;
|
|
204
|
+
if (loc && loc.start) {
|
|
205
|
+
return `me-${loc.start.line}-${loc.start.column}-${loc.end?.line || loc.start.line}-${loc.end?.column || loc.start.column}`;
|
|
206
|
+
}
|
|
207
|
+
// Last resort: use a hash of the node structure
|
|
208
|
+
return `me-${JSON.stringify(node).slice(0, 50)}`;
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* Check member expressions for null safety
|
|
212
|
+
*/
|
|
213
|
+
function checkMemberExpression(node) {
|
|
214
|
+
// Skip if already using optional chaining
|
|
215
|
+
if (node.optional) {
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
// Skip if parent is optional chaining
|
|
219
|
+
const parent = node
|
|
220
|
+
.parent;
|
|
221
|
+
if (parent && parent.type === 'ChainExpression') {
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
// Only report on the "deepest" member expression in a chain
|
|
225
|
+
// If this member expression is the object of another member expression,
|
|
226
|
+
// don't report it yet - let the deepest one be reported
|
|
227
|
+
if (parent &&
|
|
228
|
+
parent.type === 'MemberExpression' &&
|
|
229
|
+
parent.object === node) {
|
|
230
|
+
return; // This is an intermediate member expression
|
|
231
|
+
}
|
|
232
|
+
// Check if object might be null/undefined
|
|
233
|
+
// Check for Identifier or nested MemberExpression
|
|
234
|
+
const objectNode = node.object;
|
|
235
|
+
let shouldCheck = false;
|
|
236
|
+
if (objectNode.type === 'Identifier') {
|
|
237
|
+
shouldCheck = true;
|
|
238
|
+
}
|
|
239
|
+
else if (objectNode.type === 'MemberExpression') {
|
|
240
|
+
// Nested member expressions like value.nested.deep
|
|
241
|
+
shouldCheck = true;
|
|
242
|
+
}
|
|
243
|
+
if (shouldCheck && !hasNullCheck(node, sourceCode)) {
|
|
244
|
+
const nodeKey = getMemberExpressionKey(node);
|
|
245
|
+
if (reportedMemberExpressions.has(nodeKey)) {
|
|
246
|
+
return; // Already reported
|
|
247
|
+
}
|
|
248
|
+
try {
|
|
249
|
+
reportedMemberExpressions.add(nodeKey);
|
|
250
|
+
context.report({
|
|
251
|
+
node,
|
|
252
|
+
messageId: 'missingNullCheck',
|
|
253
|
+
suggest: [
|
|
254
|
+
{
|
|
255
|
+
messageId: 'useOptionalChaining',
|
|
256
|
+
fix: () => null,
|
|
257
|
+
},
|
|
258
|
+
{
|
|
259
|
+
messageId: 'useNullishCoalescing',
|
|
260
|
+
fix: () => null,
|
|
261
|
+
},
|
|
262
|
+
{
|
|
263
|
+
messageId: 'addExplicitCheck',
|
|
264
|
+
fix: () => null,
|
|
265
|
+
},
|
|
266
|
+
],
|
|
267
|
+
});
|
|
268
|
+
/* c8 ignore next 4 -- defensive error handling, hard to trigger in tests */
|
|
269
|
+
}
|
|
270
|
+
catch {
|
|
271
|
+
// Silently skip if there's an error
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
/**
|
|
277
|
+
* Check call expressions for null safety (e.g., obj.method())
|
|
278
|
+
* Only check if it's an actual method call, not just a property access
|
|
279
|
+
*/
|
|
280
|
+
function checkCallExpression(node) {
|
|
281
|
+
/* c8 ignore next 4 -- defensive type check, always true when called by ESLint visitor */
|
|
282
|
+
// Ensure this is actually a CallExpression (not just a MemberExpression)
|
|
283
|
+
if (node.type !== 'CallExpression') {
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
// Only check if callee is a member expression (e.g., obj.method())
|
|
287
|
+
// This ensures we only check method calls, not property accesses
|
|
288
|
+
if (node.callee.type === 'MemberExpression') {
|
|
289
|
+
const memberExpr = node.callee;
|
|
290
|
+
// Skip if already using optional chaining
|
|
291
|
+
if (memberExpr.optional) {
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
// Skip if parent is optional chaining
|
|
295
|
+
const parent = memberExpr.parent;
|
|
296
|
+
if (parent && parent.type === 'ChainExpression') {
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
// Skip if this MemberExpression was already reported by checkMemberExpression
|
|
300
|
+
// We can't easily check this, so we'll rely on the fact that CallExpression
|
|
301
|
+
// is only triggered for actual method calls, not property accesses
|
|
302
|
+
// Check if object might be null/undefined
|
|
303
|
+
const objectNode = memberExpr.object;
|
|
304
|
+
let shouldCheck = false;
|
|
305
|
+
if (objectNode.type === 'Identifier') {
|
|
306
|
+
shouldCheck = true;
|
|
307
|
+
}
|
|
308
|
+
else if (objectNode.type === 'MemberExpression') {
|
|
309
|
+
// Nested member expressions
|
|
310
|
+
shouldCheck = true;
|
|
311
|
+
}
|
|
312
|
+
if (shouldCheck && !hasNullCheck(memberExpr, sourceCode)) {
|
|
313
|
+
const nodeKey = getMemberExpressionKey(memberExpr);
|
|
314
|
+
if (reportedMemberExpressions.has(nodeKey)) {
|
|
315
|
+
return; // Already reported by checkMemberExpression
|
|
316
|
+
}
|
|
317
|
+
try {
|
|
318
|
+
reportedMemberExpressions.add(nodeKey);
|
|
319
|
+
context.report({
|
|
320
|
+
node: memberExpr,
|
|
321
|
+
messageId: 'missingNullCheck',
|
|
322
|
+
suggest: [
|
|
323
|
+
{
|
|
324
|
+
messageId: 'useOptionalChaining',
|
|
325
|
+
fix: () => null,
|
|
326
|
+
},
|
|
327
|
+
{
|
|
328
|
+
messageId: 'useNullishCoalescing',
|
|
329
|
+
fix: () => null,
|
|
330
|
+
},
|
|
331
|
+
{
|
|
332
|
+
messageId: 'addExplicitCheck',
|
|
333
|
+
fix: () => null,
|
|
334
|
+
},
|
|
335
|
+
],
|
|
336
|
+
});
|
|
337
|
+
/* c8 ignore next 4 -- defensive error handling, hard to trigger in tests */
|
|
338
|
+
}
|
|
339
|
+
catch {
|
|
340
|
+
// Silently skip if there's an error
|
|
341
|
+
return;
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
return {
|
|
347
|
+
MemberExpression: checkMemberExpression,
|
|
348
|
+
CallExpression: checkCallExpression,
|
|
349
|
+
};
|
|
350
|
+
},
|
|
351
|
+
});
|
|
@@ -0,0 +1,24 @@
|
|
|
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
|
+
* ESLint Rule: no-unsafe-type-narrowing
|
|
8
|
+
* Detects unsafe type narrowing patterns
|
|
9
|
+
*
|
|
10
|
+
* @see https://rules.sonarsource.com/javascript/RSPEC-4326/
|
|
11
|
+
*/
|
|
12
|
+
import type { TSESLint } from '@interlace/eslint-devkit';
|
|
13
|
+
type MessageIds = 'unsafeTypeNarrowing' | 'useTypeGuard' | 'useProperNarrowing' | 'validateBeforeAssert';
|
|
14
|
+
export interface Options {
|
|
15
|
+
/** Ignore in test files. Default: true */
|
|
16
|
+
ignoreInTests?: boolean;
|
|
17
|
+
/** Allow type assertions with comments. Default: false */
|
|
18
|
+
allowWithComment?: boolean;
|
|
19
|
+
}
|
|
20
|
+
type RuleOptions = [Options?];
|
|
21
|
+
export declare const noUnsafeTypeNarrowing: TSESLint.RuleModule<MessageIds, RuleOptions, unknown, TSESLint.RuleListener> & {
|
|
22
|
+
name: string;
|
|
23
|
+
};
|
|
24
|
+
export {};
|
|
@@ -0,0 +1,176 @@
|
|
|
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
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8
|
+
exports.noUnsafeTypeNarrowing = void 0;
|
|
9
|
+
const eslint_devkit_1 = require("@interlace/eslint-devkit");
|
|
10
|
+
const eslint_devkit_2 = require("@interlace/eslint-devkit");
|
|
11
|
+
/**
|
|
12
|
+
* Check if type assertion is unsafe (as unknown as T)
|
|
13
|
+
*/
|
|
14
|
+
function isUnsafeTypeAssertion(node) {
|
|
15
|
+
// Check for double assertion pattern: as unknown as T or as any as T
|
|
16
|
+
if (node.expression.type === 'TSAsExpression') {
|
|
17
|
+
const innerAssertion = node.expression;
|
|
18
|
+
// Check if inner assertion is to 'unknown'
|
|
19
|
+
if (innerAssertion.typeAnnotation.type === 'TSUnknownKeyword') {
|
|
20
|
+
return true; // as unknown as T pattern
|
|
21
|
+
}
|
|
22
|
+
// Also check for 'any' type
|
|
23
|
+
if (innerAssertion.typeAnnotation.type === 'TSAnyKeyword') {
|
|
24
|
+
return true; // as any as T pattern
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
// Don't flag direct assertions to unknown/any - those are handled by TSC
|
|
28
|
+
// Only flag the double assertion pattern which bypasses type safety
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Check if type assertion has explanatory comment
|
|
33
|
+
*/
|
|
34
|
+
function hasExplanatoryComment(node, sourceCode) {
|
|
35
|
+
const comments = sourceCode.getAllComments();
|
|
36
|
+
const nodeStart = node.loc?.start;
|
|
37
|
+
if (!nodeStart || !comments.length) {
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
// Look for explanatory comments near the type assertion
|
|
41
|
+
const explanatoryPatterns = [
|
|
42
|
+
/type.?guard/i,
|
|
43
|
+
/validated/i,
|
|
44
|
+
/checked/i,
|
|
45
|
+
/safe/i,
|
|
46
|
+
/known/i,
|
|
47
|
+
/intentional/i,
|
|
48
|
+
/necessary/i,
|
|
49
|
+
/framework/i,
|
|
50
|
+
/library/i,
|
|
51
|
+
/third.?party/i,
|
|
52
|
+
/legacy/i,
|
|
53
|
+
/todo/i,
|
|
54
|
+
/fixme/i,
|
|
55
|
+
];
|
|
56
|
+
// Check comments before the assertion (within 1 line)
|
|
57
|
+
for (const comment of comments) {
|
|
58
|
+
if (comment.loc && nodeStart.line - comment.loc.end.line <= 1) {
|
|
59
|
+
const commentText = comment.value.toLowerCase();
|
|
60
|
+
if (explanatoryPatterns.some((pattern) => pattern.test(commentText))) {
|
|
61
|
+
return true;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
exports.noUnsafeTypeNarrowing = (0, eslint_devkit_2.createRule)({
|
|
68
|
+
name: 'no-unsafe-type-narrowing',
|
|
69
|
+
meta: {
|
|
70
|
+
type: 'problem',
|
|
71
|
+
docs: {
|
|
72
|
+
description: 'Detects unsafe type narrowing patterns',
|
|
73
|
+
},
|
|
74
|
+
hasSuggestions: true,
|
|
75
|
+
messages: {
|
|
76
|
+
unsafeTypeNarrowing: (0, eslint_devkit_1.formatLLMMessage)({
|
|
77
|
+
icon: eslint_devkit_1.MessageIcons.WARNING,
|
|
78
|
+
issueName: 'Unsafe type narrowing',
|
|
79
|
+
description: 'Unsafe type assertion or narrowing detected',
|
|
80
|
+
severity: 'MEDIUM',
|
|
81
|
+
fix: 'Use type guards or proper validation before type assertion',
|
|
82
|
+
documentationLink: 'https://rules.sonarsource.com/javascript/RSPEC-4326/',
|
|
83
|
+
}),
|
|
84
|
+
useTypeGuard: (0, eslint_devkit_1.formatLLMMessage)({
|
|
85
|
+
icon: eslint_devkit_1.MessageIcons.INFO,
|
|
86
|
+
issueName: 'Use Type Guard',
|
|
87
|
+
description: 'Use type guard function',
|
|
88
|
+
severity: 'LOW',
|
|
89
|
+
fix: 'function isType(value: unknown): value is Type { return ... }',
|
|
90
|
+
documentationLink: 'https://www.typescriptlang.org/docs/handbook/2/narrowing.html#using-type-predicates',
|
|
91
|
+
}),
|
|
92
|
+
useProperNarrowing: (0, eslint_devkit_1.formatLLMMessage)({
|
|
93
|
+
icon: eslint_devkit_1.MessageIcons.INFO,
|
|
94
|
+
issueName: 'Use Type Narrowing',
|
|
95
|
+
description: 'Use proper type narrowing',
|
|
96
|
+
severity: 'LOW',
|
|
97
|
+
fix: 'if (typeof value === "string") { ... }',
|
|
98
|
+
documentationLink: 'https://www.typescriptlang.org/docs/handbook/2/narrowing.html',
|
|
99
|
+
}),
|
|
100
|
+
validateBeforeAssert: (0, eslint_devkit_1.formatLLMMessage)({
|
|
101
|
+
icon: eslint_devkit_1.MessageIcons.INFO,
|
|
102
|
+
issueName: 'Validate First',
|
|
103
|
+
description: 'Validate before type assertion',
|
|
104
|
+
severity: 'LOW',
|
|
105
|
+
fix: 'if (isValid(value)) { const typed = value as Type; }',
|
|
106
|
+
documentationLink: 'https://www.typescriptlang.org/docs/handbook/2/narrowing.html',
|
|
107
|
+
}),
|
|
108
|
+
},
|
|
109
|
+
schema: [
|
|
110
|
+
{
|
|
111
|
+
type: 'object',
|
|
112
|
+
properties: {
|
|
113
|
+
ignoreInTests: {
|
|
114
|
+
type: 'boolean',
|
|
115
|
+
default: true,
|
|
116
|
+
description: 'Ignore in test files',
|
|
117
|
+
},
|
|
118
|
+
allowWithComment: {
|
|
119
|
+
type: 'boolean',
|
|
120
|
+
default: false,
|
|
121
|
+
description: 'Allow type assertions with comments',
|
|
122
|
+
},
|
|
123
|
+
},
|
|
124
|
+
additionalProperties: false,
|
|
125
|
+
},
|
|
126
|
+
],
|
|
127
|
+
},
|
|
128
|
+
defaultOptions: [
|
|
129
|
+
{
|
|
130
|
+
ignoreInTests: true,
|
|
131
|
+
allowWithComment: false,
|
|
132
|
+
},
|
|
133
|
+
],
|
|
134
|
+
create(context, [options = {}]) {
|
|
135
|
+
const { ignoreInTests = true, allowWithComment = false } = options || {};
|
|
136
|
+
const filename = context.getFilename();
|
|
137
|
+
const isTestFile = ignoreInTests && /\.(test|spec)\.(ts|tsx|js|jsx)$/.test(filename);
|
|
138
|
+
if (isTestFile) {
|
|
139
|
+
return {};
|
|
140
|
+
}
|
|
141
|
+
const sourceCode = context.sourceCode || context.sourceCode;
|
|
142
|
+
/**
|
|
143
|
+
* Check type assertions
|
|
144
|
+
*/
|
|
145
|
+
function checkTypeAssertion(node) {
|
|
146
|
+
if (!isUnsafeTypeAssertion(node)) {
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
// Check if comment explains the unsafe assertion
|
|
150
|
+
if (allowWithComment && hasExplanatoryComment(node, sourceCode)) {
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
context.report({
|
|
154
|
+
node,
|
|
155
|
+
messageId: 'unsafeTypeNarrowing',
|
|
156
|
+
suggest: [
|
|
157
|
+
{
|
|
158
|
+
messageId: 'useTypeGuard',
|
|
159
|
+
fix: () => null, // Cannot auto-fix without context
|
|
160
|
+
},
|
|
161
|
+
{
|
|
162
|
+
messageId: 'useProperNarrowing',
|
|
163
|
+
fix: () => null,
|
|
164
|
+
},
|
|
165
|
+
{
|
|
166
|
+
messageId: 'validateBeforeAssert',
|
|
167
|
+
fix: () => null,
|
|
168
|
+
},
|
|
169
|
+
],
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
return {
|
|
173
|
+
TSAsExpression: checkTypeAssertion,
|
|
174
|
+
};
|
|
175
|
+
},
|
|
176
|
+
});
|
|
@@ -0,0 +1,12 @@
|
|
|
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
|
+
export interface Options {
|
|
7
|
+
}
|
|
8
|
+
type RuleOptions = [Options?];
|
|
9
|
+
export declare const requireNetworkTimeout: import("@typescript-eslint/utils/ts-eslint").RuleModule<"violationDetected", RuleOptions, unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
|
|
10
|
+
name: string;
|
|
11
|
+
};
|
|
12
|
+
export {};
|
|
@@ -0,0 +1,57 @@
|
|
|
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
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8
|
+
exports.requireNetworkTimeout = void 0;
|
|
9
|
+
/**
|
|
10
|
+
* @fileoverview Require timeout limits for network requests
|
|
11
|
+
* @see https://owasp.org/www-project-mobile-top-10/
|
|
12
|
+
* @see https://cwe.mitre.org/data/definitions/770.html
|
|
13
|
+
*/
|
|
14
|
+
const eslint_devkit_1 = require("@interlace/eslint-devkit");
|
|
15
|
+
exports.requireNetworkTimeout = (0, eslint_devkit_1.createRule)({
|
|
16
|
+
name: 'require-network-timeout',
|
|
17
|
+
meta: {
|
|
18
|
+
type: 'problem',
|
|
19
|
+
docs: {
|
|
20
|
+
description: 'Require timeout limits for network requests',
|
|
21
|
+
},
|
|
22
|
+
messages: {
|
|
23
|
+
violationDetected: (0, eslint_devkit_1.formatLLMMessage)({
|
|
24
|
+
icon: eslint_devkit_1.MessageIcons.SECURITY,
|
|
25
|
+
issueName: 'violation Detected',
|
|
26
|
+
cwe: 'CWE-400',
|
|
27
|
+
description: 'Require timeout limits for network requests detected - fetch/axios without timeout option',
|
|
28
|
+
severity: 'MEDIUM',
|
|
29
|
+
fix: 'Review and apply secure practices',
|
|
30
|
+
documentationLink: 'https://cwe.mitre.org/data/definitions/400.html',
|
|
31
|
+
}),
|
|
32
|
+
},
|
|
33
|
+
schema: [],
|
|
34
|
+
},
|
|
35
|
+
defaultOptions: [],
|
|
36
|
+
create(context) {
|
|
37
|
+
return {
|
|
38
|
+
CallExpression(node) {
|
|
39
|
+
const callee = node.callee;
|
|
40
|
+
const isFetch = callee.type === eslint_devkit_1.AST_NODE_TYPES.Identifier && callee.name === 'fetch';
|
|
41
|
+
const isAxios = callee.type === eslint_devkit_1.AST_NODE_TYPES.MemberExpression &&
|
|
42
|
+
callee.object.type === eslint_devkit_1.AST_NODE_TYPES.Identifier &&
|
|
43
|
+
callee.object.name === 'axios';
|
|
44
|
+
if (isFetch || isAxios) {
|
|
45
|
+
const optionsArg = node.arguments[1];
|
|
46
|
+
const hasTimeout = optionsArg?.type === eslint_devkit_1.AST_NODE_TYPES.ObjectExpression &&
|
|
47
|
+
optionsArg.properties.some((p) => p.type === eslint_devkit_1.AST_NODE_TYPES.Property &&
|
|
48
|
+
p.key.type === eslint_devkit_1.AST_NODE_TYPES.Identifier &&
|
|
49
|
+
p.key.name === 'timeout');
|
|
50
|
+
if (!hasTimeout) {
|
|
51
|
+
context.report({ node, messageId: 'violationDetected' });
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
},
|
|
55
|
+
};
|
|
56
|
+
},
|
|
57
|
+
});
|