@blumintinc/eslint-plugin-blumint 1.14.0 → 1.15.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 +10 -11
- package/lib/index.js +14 -12
- package/lib/rules/avoid-utils-directory.js +0 -4
- package/lib/rules/consistent-callback-naming.js +42 -3
- package/lib/rules/dynamic-https-errors.d.ts +1 -1
- package/lib/rules/dynamic-https-errors.js +132 -41
- package/lib/rules/enforce-boolean-naming-prefixes.js +0 -212
- package/lib/rules/enforce-date-ttime.d.ts +1 -0
- package/lib/rules/enforce-date-ttime.js +156 -0
- package/lib/rules/enforce-dynamic-firebase-imports.d.ts +2 -1
- package/lib/rules/enforce-dynamic-firebase-imports.js +3 -2
- package/lib/rules/enforce-f-extension-for-entry-points.d.ts +8 -0
- package/lib/rules/enforce-f-extension-for-entry-points.js +283 -0
- package/lib/rules/enforce-global-constants.js +3 -3
- package/lib/rules/enforce-id-capitalization.js +1 -1
- package/lib/rules/enforce-memoize-async.js +69 -15
- package/lib/rules/enforce-props-argument-name.js +42 -16
- package/lib/rules/enforce-stable-hash-spread-props.js +3 -3
- package/lib/rules/enforce-transform-memoization.js +1 -1
- package/lib/rules/enforce-verb-noun-naming.js +3814 -4641
- package/lib/rules/global-const-style.js +16 -4
- package/lib/rules/memo-compare-deeply-complex-props.js +16 -3
- package/lib/rules/memo-nested-react-components.js +108 -103
- package/lib/rules/no-async-foreach.js +7 -2
- package/lib/rules/no-circular-references.d.ts +2 -1
- package/lib/rules/no-circular-references.js +13 -15
- package/lib/rules/no-console-error.js +12 -10
- package/lib/rules/no-empty-dependency-use-callbacks.js +1 -1
- package/lib/rules/no-entire-object-hook-deps.js +48 -1
- package/lib/rules/no-excessive-parent-chain.js +3 -0
- package/lib/rules/no-inline-component-prop.js +16 -7
- package/lib/rules/no-passthrough-getters.d.ts +2 -2
- package/lib/rules/no-passthrough-getters.js +83 -1
- package/lib/rules/no-redundant-this-params.js +50 -1
- package/lib/rules/no-unmemoized-memo-without-props.js +1 -1
- package/lib/rules/no-unnecessary-destructuring-rename.js +2 -5
- package/lib/rules/parallelize-async-operations.js +119 -54
- package/lib/rules/prefer-nullish-coalescing-boolean-props.js +109 -4
- package/lib/rules/prefer-params-over-parent-id.js +1 -1
- package/lib/rules/prefer-settings-object.js +27 -10
- package/lib/rules/prefer-type-alias-over-typeof-constant.js +9 -0
- package/lib/rules/prefer-usememo-over-useeffect-usestate.js +1 -1
- package/lib/rules/prevent-children-clobber.js +9 -5
- package/lib/rules/react-memoize-literals.js +131 -12
- package/lib/rules/require-https-error-cause.js +30 -11
- package/lib/rules/require-memo.js +17 -9
- package/lib/utils/ASTHelpers.d.ts +34 -1
- package/lib/utils/ASTHelpers.js +346 -112
- package/package.json +1 -1
|
@@ -47,6 +47,7 @@ exports.preferSettingsObject = (0, createRule_1.createRule)({
|
|
|
47
47
|
defaultOptions: [defaultOptions],
|
|
48
48
|
create(context, [options]) {
|
|
49
49
|
const finalOptions = { ...defaultOptions, ...options };
|
|
50
|
+
const { sourceCode } = context;
|
|
50
51
|
function getParameterType(param) {
|
|
51
52
|
if (param.type === utils_1.AST_NODE_TYPES.AssignmentPattern) {
|
|
52
53
|
return getParameterType(param.left);
|
|
@@ -56,13 +57,10 @@ exports.preferSettingsObject = (0, createRule_1.createRule)({
|
|
|
56
57
|
const typeNode = param.typeAnnotation.typeAnnotation;
|
|
57
58
|
if (typeNode.type === utils_1.AST_NODE_TYPES.TSTypeReference) {
|
|
58
59
|
// Include type parameters in the type signature to differentiate generic types
|
|
59
|
-
const typeName = typeNode.typeName
|
|
60
|
-
? typeNode.typeName.name
|
|
61
|
-
: 'unknown';
|
|
60
|
+
const typeName = sourceCode.getText(typeNode.typeName);
|
|
62
61
|
// If there are type parameters, include them in the type signature
|
|
63
62
|
if (typeNode.typeParameters &&
|
|
64
63
|
typeNode.typeParameters.params.length > 0) {
|
|
65
|
-
const sourceCode = context.sourceCode;
|
|
66
64
|
const typeParams = typeNode.typeParameters.params
|
|
67
65
|
.map((param) => sourceCode.getText(param))
|
|
68
66
|
.join(', ');
|
|
@@ -76,13 +74,10 @@ exports.preferSettingsObject = (0, createRule_1.createRule)({
|
|
|
76
74
|
const typeNode = param.typeAnnotation.typeAnnotation;
|
|
77
75
|
if (typeNode.type === utils_1.AST_NODE_TYPES.TSTypeReference) {
|
|
78
76
|
// Include type parameters in the type signature to differentiate generic types
|
|
79
|
-
const typeName = typeNode.typeName
|
|
80
|
-
? typeNode.typeName.name
|
|
81
|
-
: 'unknown';
|
|
77
|
+
const typeName = sourceCode.getText(typeNode.typeName);
|
|
82
78
|
// If there are type parameters, include them in the type signature
|
|
83
79
|
if (typeNode.typeParameters &&
|
|
84
80
|
typeNode.typeParameters.params.length > 0) {
|
|
85
|
-
const sourceCode = context.sourceCode;
|
|
86
81
|
const typeParams = typeNode.typeParameters.params
|
|
87
82
|
.map((param) => sourceCode.getText(param))
|
|
88
83
|
.join(', ');
|
|
@@ -98,12 +93,14 @@ exports.preferSettingsObject = (0, createRule_1.createRule)({
|
|
|
98
93
|
return 'boolean';
|
|
99
94
|
return typeNode.type;
|
|
100
95
|
}
|
|
101
|
-
return
|
|
96
|
+
return null;
|
|
102
97
|
}
|
|
103
98
|
function findDuplicateParameterType(params) {
|
|
104
99
|
const typeMap = new Map();
|
|
105
100
|
for (const param of params) {
|
|
106
101
|
const type = getParameterType(param);
|
|
102
|
+
if (type === null)
|
|
103
|
+
continue;
|
|
107
104
|
const count = (typeMap.get(type) || 0) + 1;
|
|
108
105
|
typeMap.set(type, count);
|
|
109
106
|
if (count > 1) {
|
|
@@ -248,6 +245,24 @@ exports.preferSettingsObject = (0, createRule_1.createRule)({
|
|
|
248
245
|
}
|
|
249
246
|
return false;
|
|
250
247
|
}
|
|
248
|
+
function hasImplicitlyTypedParams(params) {
|
|
249
|
+
return params.some((param) => {
|
|
250
|
+
let current = param;
|
|
251
|
+
if (current.type === utils_1.AST_NODE_TYPES.TSParameterProperty) {
|
|
252
|
+
current = current.parameter;
|
|
253
|
+
}
|
|
254
|
+
if (current.type === utils_1.AST_NODE_TYPES.AssignmentPattern) {
|
|
255
|
+
current = current.left;
|
|
256
|
+
}
|
|
257
|
+
if (current.type === utils_1.AST_NODE_TYPES.Identifier ||
|
|
258
|
+
current.type === utils_1.AST_NODE_TYPES.ObjectPattern ||
|
|
259
|
+
current.type === utils_1.AST_NODE_TYPES.ArrayPattern ||
|
|
260
|
+
current.type === utils_1.AST_NODE_TYPES.RestElement) {
|
|
261
|
+
return !current.typeAnnotation;
|
|
262
|
+
}
|
|
263
|
+
return true;
|
|
264
|
+
});
|
|
265
|
+
}
|
|
251
266
|
function shouldIgnoreNode(node) {
|
|
252
267
|
// Ignore built-in objects and third-party modules
|
|
253
268
|
if (isBuiltInOrThirdParty(node))
|
|
@@ -270,7 +285,7 @@ exports.preferSettingsObject = (0, createRule_1.createRule)({
|
|
|
270
285
|
parent = parent.parent;
|
|
271
286
|
}
|
|
272
287
|
}
|
|
273
|
-
// Ignore functions with A/B pattern parameters
|
|
288
|
+
// Ignore functions with A/B pattern parameters or missing type annotations
|
|
274
289
|
if ((node.type === utils_1.AST_NODE_TYPES.FunctionDeclaration ||
|
|
275
290
|
node.type === utils_1.AST_NODE_TYPES.FunctionExpression ||
|
|
276
291
|
node.type === utils_1.AST_NODE_TYPES.ArrowFunctionExpression ||
|
|
@@ -279,6 +294,8 @@ exports.preferSettingsObject = (0, createRule_1.createRule)({
|
|
|
279
294
|
Array.isArray(node.params)) {
|
|
280
295
|
if (hasABPattern(node.params))
|
|
281
296
|
return true;
|
|
297
|
+
if (hasImplicitlyTypedParams(node.params))
|
|
298
|
+
return true;
|
|
282
299
|
}
|
|
283
300
|
// Check if the function is a handler with transaction parameter
|
|
284
301
|
// This is a common pattern in Firebase/Firestore handlers
|
|
@@ -225,6 +225,15 @@ exports.preferTypeAliasOverTypeofConstant = (0, createRule_1.createRule)({
|
|
|
225
225
|
TSTypeQuery(node) {
|
|
226
226
|
if (!collected)
|
|
227
227
|
return;
|
|
228
|
+
// Skip if inside a type alias declaration (Issue #1117)
|
|
229
|
+
// This allows 'type T = typeof CONST' as the canonical way to define the alias.
|
|
230
|
+
let current = node.parent;
|
|
231
|
+
while (current) {
|
|
232
|
+
if (current.type === utils_1.AST_NODE_TYPES.TSTypeAliasDeclaration) {
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
current = current.parent;
|
|
236
|
+
}
|
|
228
237
|
// Skip `keyof typeof X`
|
|
229
238
|
if (node.parent &&
|
|
230
239
|
node.parent.type === utils_1.AST_NODE_TYPES.TSTypeOperator &&
|
|
@@ -11,7 +11,7 @@ exports.preferUseMemoOverUseEffectUseState = (0, createRule_1.createRule)({
|
|
|
11
11
|
recommended: 'error',
|
|
12
12
|
},
|
|
13
13
|
messages: {
|
|
14
|
-
preferUseMemo: '
|
|
14
|
+
preferUseMemo: 'Use useMemo to compute derived state "{{stateName}}" instead of useEffect + useState to avoid extra render cycles and stale snapshots.',
|
|
15
15
|
},
|
|
16
16
|
schema: [],
|
|
17
17
|
},
|
|
@@ -44,12 +44,12 @@ function resolveFunctionName(node) {
|
|
|
44
44
|
}
|
|
45
45
|
return null;
|
|
46
46
|
}
|
|
47
|
-
function isComponentLike(node) {
|
|
47
|
+
function isComponentLike(node, context) {
|
|
48
48
|
const name = resolveFunctionName(node);
|
|
49
49
|
if (name && /^[A-Z]/.test(name)) {
|
|
50
50
|
return true;
|
|
51
51
|
}
|
|
52
|
-
return ASTHelpers_1.ASTHelpers.returnsJSX(node.body);
|
|
52
|
+
return ASTHelpers_1.ASTHelpers.returnsJSX(node.body, context);
|
|
53
53
|
}
|
|
54
54
|
function patternHasChildrenProperty(pattern) {
|
|
55
55
|
return pattern.properties.some((prop) => {
|
|
@@ -266,7 +266,8 @@ function bindingMayContainChildren(binding, context) {
|
|
|
266
266
|
return false;
|
|
267
267
|
if (binding.typeAnnotationExcludesProperty)
|
|
268
268
|
return false;
|
|
269
|
-
const services = context.sourceCode?.parserServices ??
|
|
269
|
+
const services = context.sourceCode?.parserServices ??
|
|
270
|
+
context.parserServices;
|
|
270
271
|
if (!services?.program || !services?.esTreeNodeToTSNodeMap) {
|
|
271
272
|
return true;
|
|
272
273
|
}
|
|
@@ -410,7 +411,7 @@ exports.preventChildrenClobber = (0, createRule_1.createRule)({
|
|
|
410
411
|
return {
|
|
411
412
|
':function'(node) {
|
|
412
413
|
const ctx = {
|
|
413
|
-
isComponent: isComponentLike(node),
|
|
414
|
+
isComponent: isComponentLike(node, context),
|
|
414
415
|
bindings: new Map(),
|
|
415
416
|
propsLikeIdentifiers: new Set(),
|
|
416
417
|
childrenValueSourceIds: new Map(),
|
|
@@ -510,7 +511,10 @@ exports.preventChildrenClobber = (0, createRule_1.createRule)({
|
|
|
510
511
|
if (!bindingMayContainChildren(binding, context)) {
|
|
511
512
|
continue;
|
|
512
513
|
}
|
|
513
|
-
offendingSpreads.push({
|
|
514
|
+
offendingSpreads.push({
|
|
515
|
+
name,
|
|
516
|
+
childrenSourceId: binding.childrenSourceId,
|
|
517
|
+
});
|
|
514
518
|
}
|
|
515
519
|
if (offendingSpreads.length === 0)
|
|
516
520
|
return;
|
|
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.reactMemoizeLiterals = void 0;
|
|
4
4
|
const utils_1 = require("@typescript-eslint/utils");
|
|
5
5
|
const createRule_1 = require("../utils/createRule");
|
|
6
|
+
const ASTHelpers_1 = require("../utils/ASTHelpers");
|
|
6
7
|
const LITERAL_DESCRIPTOR_BY_TYPE = {
|
|
7
8
|
[utils_1.AST_NODE_TYPES.ObjectExpression]: {
|
|
8
9
|
literalType: 'object literal',
|
|
@@ -39,6 +40,11 @@ const SAFE_HOOK_ARGUMENTS = new Set([
|
|
|
39
40
|
'useDeferredValue',
|
|
40
41
|
'useTransition',
|
|
41
42
|
'useId',
|
|
43
|
+
'useLatestCallback',
|
|
44
|
+
'useDeepCompareMemo',
|
|
45
|
+
'useDeepCompareCallback',
|
|
46
|
+
'useDeepCompareEffect',
|
|
47
|
+
'useProgressionCallback',
|
|
42
48
|
]);
|
|
43
49
|
const MEMOIZATION_DEPS_TODO_PLACEHOLDER = '__TODO_MEMOIZATION_DEPENDENCIES__';
|
|
44
50
|
const TODO_DEPS_COMMENT = `/* ${MEMOIZATION_DEPS_TODO_PLACEHOLDER} */`;
|
|
@@ -228,17 +234,26 @@ function findEnclosingComponentOrHook(node) {
|
|
|
228
234
|
function isInsideAllowedHookCallback(node) {
|
|
229
235
|
let current = node;
|
|
230
236
|
while (current) {
|
|
231
|
-
if (current
|
|
232
|
-
current.
|
|
233
|
-
|
|
234
|
-
while (parent && isExpressionWrapper(parent)) {
|
|
235
|
-
parent = parent.parent;
|
|
237
|
+
if (isFunctionNode(current)) {
|
|
238
|
+
if (current.async && current !== node) {
|
|
239
|
+
return true;
|
|
236
240
|
}
|
|
237
|
-
if (
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
241
|
+
if (current.type === utils_1.AST_NODE_TYPES.FunctionExpression ||
|
|
242
|
+
current.type === utils_1.AST_NODE_TYPES.ArrowFunctionExpression) {
|
|
243
|
+
let parent = current.parent;
|
|
244
|
+
// Skip through TypeScript type assertions and parentheses to find
|
|
245
|
+
// the actual CallExpression that invokes the hook.
|
|
246
|
+
while (parent && isExpressionWrapper(parent)) {
|
|
247
|
+
parent = parent.parent;
|
|
248
|
+
}
|
|
249
|
+
if (parent?.type === utils_1.AST_NODE_TYPES.CallExpression) {
|
|
250
|
+
const hookName = getHookNameFromCallee(parent.callee);
|
|
251
|
+
const matchesCallback = parent.arguments.some((arg) => unwrapNestedExpressions(arg) === current);
|
|
252
|
+
if (hookName &&
|
|
253
|
+
SAFE_HOOK_ARGUMENTS.has(hookName) &&
|
|
254
|
+
matchesCallback) {
|
|
255
|
+
return true;
|
|
256
|
+
}
|
|
242
257
|
}
|
|
243
258
|
}
|
|
244
259
|
}
|
|
@@ -427,16 +442,120 @@ exports.reactMemoizeLiterals = (0, createRule_1.createRule)({
|
|
|
427
442
|
defaultOptions: [],
|
|
428
443
|
create(context) {
|
|
429
444
|
const sourceCode = context.getSourceCode();
|
|
445
|
+
/**
|
|
446
|
+
* Checks whether a VariableDeclarator's sole usage is being thrown
|
|
447
|
+
* in the same function scope.
|
|
448
|
+
*/
|
|
449
|
+
function isVariableAlwaysThrown(declarator) {
|
|
450
|
+
const variables = ASTHelpers_1.ASTHelpers.getDeclaredVariables(context, declarator);
|
|
451
|
+
if (variables.length === 0) {
|
|
452
|
+
return false;
|
|
453
|
+
}
|
|
454
|
+
const variable = variables[0];
|
|
455
|
+
const usages = variable.references.filter((ref) => !ref.init);
|
|
456
|
+
// Variables with no usages (dead code) don't bypass memoization checks
|
|
457
|
+
// because we can't prove the literal is thrown.
|
|
458
|
+
if (usages.length === 0) {
|
|
459
|
+
return false;
|
|
460
|
+
}
|
|
461
|
+
const owningFunction = findOwningFunction(declarator);
|
|
462
|
+
return usages.every((ref) => {
|
|
463
|
+
let refParent = ref.identifier
|
|
464
|
+
.parent;
|
|
465
|
+
while (refParent) {
|
|
466
|
+
if (isFunctionNode(refParent)) {
|
|
467
|
+
// Stop at function boundaries: throws inside nested functions don't
|
|
468
|
+
// abort the outer render cycle, so they don't make the literal terminal
|
|
469
|
+
// from the perspective of the declaring function.
|
|
470
|
+
return false;
|
|
471
|
+
}
|
|
472
|
+
if (refParent.type === utils_1.AST_NODE_TYPES.ThrowStatement) {
|
|
473
|
+
// Found a throw in the same lexical scope as the usage.
|
|
474
|
+
// Verify the throw is in the same function where the variable was declared
|
|
475
|
+
// to ensure the throw terminates the render before memoization matters.
|
|
476
|
+
let throwCheckParent = refParent.parent;
|
|
477
|
+
while (throwCheckParent) {
|
|
478
|
+
if (isFunctionNode(throwCheckParent)) {
|
|
479
|
+
return throwCheckParent === owningFunction;
|
|
480
|
+
}
|
|
481
|
+
throwCheckParent =
|
|
482
|
+
throwCheckParent.parent;
|
|
483
|
+
}
|
|
484
|
+
return owningFunction === null;
|
|
485
|
+
}
|
|
486
|
+
if (refParent === declarator) {
|
|
487
|
+
break;
|
|
488
|
+
}
|
|
489
|
+
refParent = refParent.parent;
|
|
490
|
+
}
|
|
491
|
+
return false;
|
|
492
|
+
});
|
|
493
|
+
}
|
|
494
|
+
/**
|
|
495
|
+
* Checks whether a literal is destined to be thrown, making referential
|
|
496
|
+
* stability irrelevant.
|
|
497
|
+
*/
|
|
498
|
+
function isTerminalUsage(node) {
|
|
499
|
+
let current = node.parent;
|
|
500
|
+
// Tracks whether the literal passed through a node that represents non-container usage.
|
|
501
|
+
// Once true, any throw we encounter won't count as "terminal" because the literal
|
|
502
|
+
// is already used in an expression that might need memoization.
|
|
503
|
+
let hasPassedForbiddenNode = false;
|
|
504
|
+
while (current) {
|
|
505
|
+
if (current.type === utils_1.AST_NODE_TYPES.ThrowStatement) {
|
|
506
|
+
return !hasPassedForbiddenNode;
|
|
507
|
+
}
|
|
508
|
+
if (current.type === utils_1.AST_NODE_TYPES.VariableDeclarator &&
|
|
509
|
+
current.id.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
510
|
+
current.init) {
|
|
511
|
+
if (!hasPassedForbiddenNode && isVariableAlwaysThrown(current)) {
|
|
512
|
+
return true;
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
// Stop at function boundaries as throws across functions are not "terminal"
|
|
516
|
+
// in the same render cycle sense for the current component/hook.
|
|
517
|
+
if (isFunctionNode(current)) {
|
|
518
|
+
break;
|
|
519
|
+
}
|
|
520
|
+
// We distinguish between "containers" (nodes that build up an error value
|
|
521
|
+
// or wrap an expression) and "usages" (nodes where the value is consumed
|
|
522
|
+
// in a way that might benefit from memoization).
|
|
523
|
+
//
|
|
524
|
+
// Containers (NewExpression for errors, wrappers, or object/array builders)
|
|
525
|
+
// are allowed because they are part of the path to a 'throw'.
|
|
526
|
+
// Other nodes (like function calls or being a prop) mark the literal as
|
|
527
|
+
// having a non-terminal usage.
|
|
528
|
+
//
|
|
529
|
+
// Note: AST_NODE_TYPES.CallExpression is intentionally excluded from allowed
|
|
530
|
+
// containers. Passing a literal into a CallExpression allows the callee to
|
|
531
|
+
// observe or store the reference, making its identity relevant even if
|
|
532
|
+
// the result is eventually thrown (e.g., const err = fn({ ... }); throw err;).
|
|
533
|
+
if (current.type !== utils_1.AST_NODE_TYPES.ObjectExpression &&
|
|
534
|
+
current.type !== utils_1.AST_NODE_TYPES.Property &&
|
|
535
|
+
current.type !== utils_1.AST_NODE_TYPES.ArrayExpression &&
|
|
536
|
+
current.type !== utils_1.AST_NODE_TYPES.NewExpression &&
|
|
537
|
+
current.type !== utils_1.AST_NODE_TYPES.ConditionalExpression &&
|
|
538
|
+
current.type !== utils_1.AST_NODE_TYPES.LogicalExpression &&
|
|
539
|
+
!isExpressionWrapper(current)) {
|
|
540
|
+
hasPassedForbiddenNode = true;
|
|
541
|
+
}
|
|
542
|
+
current = current.parent;
|
|
543
|
+
}
|
|
544
|
+
return false;
|
|
545
|
+
}
|
|
430
546
|
function reportLiteral(node) {
|
|
431
547
|
const descriptor = getLiteralDescriptor(node);
|
|
432
548
|
if (!descriptor)
|
|
433
549
|
return;
|
|
550
|
+
const owner = findEnclosingComponentOrHook(node);
|
|
551
|
+
if (!owner)
|
|
552
|
+
return;
|
|
434
553
|
if (isInsideAllowedHookCallback(node)) {
|
|
435
554
|
return;
|
|
436
555
|
}
|
|
437
|
-
|
|
438
|
-
if (!owner)
|
|
556
|
+
if (isTerminalUsage(node)) {
|
|
439
557
|
return;
|
|
558
|
+
}
|
|
440
559
|
const hookCall = findEnclosingHookCall(node);
|
|
441
560
|
if (hookCall) {
|
|
442
561
|
if (hookCall.isDirectArgument) {
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.requireHttpsErrorCause = void 0;
|
|
4
4
|
const utils_1 = require("@typescript-eslint/utils");
|
|
5
|
+
const ASTHelpers_1 = require("../utils/ASTHelpers");
|
|
5
6
|
const createRule_1 = require("../utils/createRule");
|
|
6
7
|
const isHttpsErrorCallee = (callee) => {
|
|
7
8
|
if (callee.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
@@ -42,17 +43,8 @@ exports.requireHttpsErrorCause = (0, createRule_1.createRule)({
|
|
|
42
43
|
},
|
|
43
44
|
defaultOptions: [],
|
|
44
45
|
create(context) {
|
|
45
|
-
const sourceCode = context.getSourceCode();
|
|
46
|
+
const sourceCode = context.sourceCode ?? context.getSourceCode();
|
|
46
47
|
const catchStack = [];
|
|
47
|
-
// ESLint 9 moves getScope onto sourceCode; ESLint 8 exposes context.getScope().
|
|
48
|
-
// This shim keeps the rule compatible until the codebase drops ESLint 8 support.
|
|
49
|
-
const getScopeForNode = (node) => {
|
|
50
|
-
const sourceCodeWithScope = sourceCode;
|
|
51
|
-
if (typeof sourceCodeWithScope.getScope === 'function') {
|
|
52
|
-
return sourceCodeWithScope.getScope(node);
|
|
53
|
-
}
|
|
54
|
-
return context.getScope();
|
|
55
|
-
};
|
|
56
48
|
const reportMissingCause = (node, catchName) => {
|
|
57
49
|
context.report({
|
|
58
50
|
node,
|
|
@@ -76,7 +68,7 @@ exports.requireHttpsErrorCause = (0, createRule_1.createRule)({
|
|
|
76
68
|
if (!frame.paramName || identifier.name !== frame.paramName) {
|
|
77
69
|
return false;
|
|
78
70
|
}
|
|
79
|
-
const variable = findVariableInScopeChain(
|
|
71
|
+
const variable = findVariableInScopeChain(ASTHelpers_1.ASTHelpers.getScope(context, identifier), identifier.name);
|
|
80
72
|
return (variable?.defs.some((definition) => definition.type === 'CatchClause' && definition.node === frame.node) ?? false);
|
|
81
73
|
};
|
|
82
74
|
const validateHttpsError = (node) => {
|
|
@@ -95,6 +87,33 @@ exports.requireHttpsErrorCause = (0, createRule_1.createRule)({
|
|
|
95
87
|
return;
|
|
96
88
|
}
|
|
97
89
|
const catchName = activeCatch.paramName;
|
|
90
|
+
// HttpsError accepts both positional and object-based constructors. This signature
|
|
91
|
+
// is supported for compatibility with HttpsError overloads, ensuring the rule
|
|
92
|
+
// can validate the 'cause' property within a settings object to preserve
|
|
93
|
+
// error chaining for better diagnostics.
|
|
94
|
+
if (node.arguments.length === 1 &&
|
|
95
|
+
node.arguments[0].type === utils_1.AST_NODE_TYPES.ObjectExpression) {
|
|
96
|
+
const settingsObj = node.arguments[0];
|
|
97
|
+
const causeProp = settingsObj.properties.find((prop) => prop.type === utils_1.AST_NODE_TYPES.Property &&
|
|
98
|
+
!prop.computed &&
|
|
99
|
+
((prop.key.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
100
|
+
prop.key.name === 'cause') ||
|
|
101
|
+
(prop.key.type === utils_1.AST_NODE_TYPES.Literal &&
|
|
102
|
+
prop.key.value === 'cause')));
|
|
103
|
+
if (!causeProp) {
|
|
104
|
+
reportMissingCause(node, catchName);
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
const causeValue = causeProp.value;
|
|
108
|
+
if (causeValue.type !== utils_1.AST_NODE_TYPES.Identifier) {
|
|
109
|
+
reportWrongCause(causeValue, catchName, sourceCode.getText(causeValue));
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
if (!isCatchBindingReference(causeValue, activeCatch)) {
|
|
113
|
+
reportWrongCause(causeValue, catchName, sourceCode.getText(causeValue));
|
|
114
|
+
}
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
98
117
|
if (node.arguments.length < 4) {
|
|
99
118
|
reportMissingCause(node, catchName);
|
|
100
119
|
return;
|
|
@@ -8,21 +8,25 @@ const ASTHelpers_1 = require("../utils/ASTHelpers");
|
|
|
8
8
|
const isComponentExplicitlyUnmemoized = (componentName) => componentName.toLowerCase().includes('unmemoized');
|
|
9
9
|
function isFunction(node) {
|
|
10
10
|
return (node.type === utils_1.AST_NODE_TYPES.ArrowFunctionExpression ||
|
|
11
|
-
node.type === utils_1.AST_NODE_TYPES.FunctionExpression
|
|
11
|
+
node.type === utils_1.AST_NODE_TYPES.FunctionExpression ||
|
|
12
|
+
node.type === utils_1.AST_NODE_TYPES.FunctionDeclaration);
|
|
12
13
|
}
|
|
13
|
-
function isHigherOrderFunctionReturningJSX(node) {
|
|
14
|
+
function isHigherOrderFunctionReturningJSX(node, context) {
|
|
14
15
|
if (isFunction(node)) {
|
|
15
|
-
// Check if function takes another function as an argument
|
|
16
|
-
const hasFunctionParam = 'params' in node && node.params.some(isFunction);
|
|
17
16
|
if (node.body && node.body.type === 'BlockStatement') {
|
|
18
17
|
for (const statement of node.body.body) {
|
|
19
18
|
if (statement.type === 'ReturnStatement' && statement.argument) {
|
|
20
|
-
const returnsJSX = ASTHelpers_1.ASTHelpers.returnsJSX(statement.argument);
|
|
19
|
+
const returnsJSX = ASTHelpers_1.ASTHelpers.returnsJSX(statement.argument, context);
|
|
21
20
|
const returnsFunction = isFunction(statement.argument);
|
|
22
|
-
return
|
|
21
|
+
return returnsFunction && returnsJSX;
|
|
23
22
|
}
|
|
24
23
|
}
|
|
25
24
|
}
|
|
25
|
+
else if (node.body && isFunction(node.body)) {
|
|
26
|
+
// Shorthand arrow HOC: (Comp) => (props) => <Comp {...props} />
|
|
27
|
+
// Here node.body is the inner function; check if it returns JSX.
|
|
28
|
+
return ASTHelpers_1.ASTHelpers.returnsJSX(node.body, context);
|
|
29
|
+
}
|
|
26
30
|
}
|
|
27
31
|
return false;
|
|
28
32
|
}
|
|
@@ -52,14 +56,15 @@ function checkFunction(context, node) {
|
|
|
52
56
|
if (!fileName.endsWith('.tsx')) {
|
|
53
57
|
return;
|
|
54
58
|
}
|
|
55
|
-
if (isHigherOrderFunctionReturningJSX(node)) {
|
|
59
|
+
if (isHigherOrderFunctionReturningJSX(node, context)) {
|
|
56
60
|
return;
|
|
57
61
|
}
|
|
58
62
|
const parentNode = node.parent;
|
|
59
63
|
if (node.parent.type === 'CallExpression') {
|
|
60
64
|
return;
|
|
61
65
|
}
|
|
62
|
-
if (ASTHelpers_1.ASTHelpers.returnsJSX(node.body) &&
|
|
66
|
+
if (ASTHelpers_1.ASTHelpers.returnsJSX(node.body, context) &&
|
|
67
|
+
ASTHelpers_1.ASTHelpers.hasParameters(node)) {
|
|
63
68
|
const results = [
|
|
64
69
|
isUnmemoizedArrowFunction,
|
|
65
70
|
isUnmemoizedFunctionComponent,
|
|
@@ -79,6 +84,9 @@ function checkFunction(context, node) {
|
|
|
79
84
|
},
|
|
80
85
|
fix: results[2] || results[1]
|
|
81
86
|
? function fix(fixer) {
|
|
87
|
+
if (node.async || node.generator) {
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
82
90
|
const sourceCode = context.sourceCode;
|
|
83
91
|
let importFix = null;
|
|
84
92
|
// Search for memo import statement
|
|
@@ -118,7 +126,7 @@ function checkFunction(context, node) {
|
|
|
118
126
|
const functionNameReplacement = `function ${node.id.name}Unmemoized`;
|
|
119
127
|
const fixes = [
|
|
120
128
|
fixer.replaceTextRange(functionKeywordRange, functionKeywordReplacement),
|
|
121
|
-
fixer.insertTextAfterRange([node.range[1], node.range[1]], ')'),
|
|
129
|
+
fixer.insertTextAfterRange([node.range[1], node.range[1]], ');'),
|
|
122
130
|
fixer.replaceTextRange([node.id.range[0] - 1, node.id.range[1]], functionNameReplacement),
|
|
123
131
|
];
|
|
124
132
|
if (importFix) {
|
|
@@ -1,18 +1,51 @@
|
|
|
1
1
|
import { TSESLint, TSESTree } from '@typescript-eslint/utils';
|
|
2
2
|
import { Graph } from './graph/ClassGraphBuilder';
|
|
3
3
|
export declare class ASTHelpers {
|
|
4
|
+
/**
|
|
5
|
+
* AST node shapes vary across ESLint/typescript-eslint versions, with some
|
|
6
|
+
* node types (ParenthesizedExpression, TSSatisfiesExpression) not consistently
|
|
7
|
+
* available in type definitions. Type guards and runtime checks ensure correctness
|
|
8
|
+
* despite these discrepancies, trading compile-time safety for cross-version
|
|
9
|
+
* compatibility until type definitions stabilize.
|
|
10
|
+
*
|
|
11
|
+
* Semantics Contract:
|
|
12
|
+
* - Helpers like getScope and returnsJSX must be invoked from the active
|
|
13
|
+
* visitor traversal context (ESLint 8 compatible).
|
|
14
|
+
* - returnsJSX is a heuristic and does not perform a full control-flow proof.
|
|
15
|
+
* - Reliance on runtime type guards (isParenthesizedExpression,
|
|
16
|
+
* isLoopOrLabeledStatement) is intentional.
|
|
17
|
+
*/
|
|
4
18
|
/**
|
|
5
19
|
* Finds a variable by name in the scope chain starting from the given scope.
|
|
6
20
|
*/
|
|
7
21
|
static findVariableInScope(scope: TSESLint.Scope.Scope, name: string): TSESLint.Scope.Variable | null;
|
|
22
|
+
/**
|
|
23
|
+
* Compatibility wrapper for getting the scope of a node across ESLint versions.
|
|
24
|
+
* ESLint 9 moves getScope onto sourceCode; ESLint 8 exposes context.getScope().
|
|
25
|
+
*/
|
|
26
|
+
static getScope(context: Readonly<TSESLint.RuleContext<string, readonly unknown[]>>, node: TSESTree.Node): TSESLint.Scope.Scope;
|
|
8
27
|
static blockIncludesIdentifier(block: TSESTree.BlockStatement): boolean;
|
|
9
28
|
static declarationIncludesIdentifier(node: TSESTree.Node | null): boolean;
|
|
29
|
+
/**
|
|
30
|
+
* Checks if a pattern (in a declaration or parameter) contains any dependencies.
|
|
31
|
+
* Patterns themselves define new bindings (Identifiers), but they can contain
|
|
32
|
+
* dependencies in computed keys or default values (AssignmentPattern).
|
|
33
|
+
*/
|
|
34
|
+
private static patternHasDependency;
|
|
10
35
|
static classMethodDependenciesOf(node: TSESTree.Node | null, graph: Graph, className: string): string[];
|
|
11
36
|
static isNode(value: unknown): value is TSESTree.Node;
|
|
12
37
|
static hasReturnStatement(node: TSESTree.Node): boolean;
|
|
13
38
|
static isNodeExported(node: TSESTree.Node): boolean;
|
|
14
|
-
static
|
|
39
|
+
private static isLoopOrLabeledStatement;
|
|
40
|
+
private static isParenthesizedExpression;
|
|
41
|
+
private static returnsJSXValue;
|
|
42
|
+
private static returnsJSXFromStatement;
|
|
43
|
+
static returnsJSX(node: TSESTree.Node | null | undefined, context?: Readonly<TSESLint.RuleContext<string, readonly unknown[]>>): boolean;
|
|
15
44
|
static hasParameters(node: TSESTree.ArrowFunctionExpression | TSESTree.FunctionExpression | TSESTree.FunctionDeclaration): boolean;
|
|
45
|
+
/**
|
|
46
|
+
* Compatibility wrapper for getting declared variables across ESLint versions.
|
|
47
|
+
*/
|
|
48
|
+
static getDeclaredVariables(context: Readonly<TSESLint.RuleContext<string, readonly unknown[]>>, node: TSESTree.Node): readonly TSESLint.Scope.Variable[];
|
|
16
49
|
/**
|
|
17
50
|
* Helper to get ancestors of a node in a way that is compatible with both ESLint v8 and v9.
|
|
18
51
|
* In ESLint v9, context.getAncestors() is deprecated and moved to context.sourceCode.getAncestors(node).
|