@blumintinc/eslint-plugin-blumint 1.14.0 → 1.16.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 +36 -14
- package/lib/index.js +20 -12
- package/lib/rules/avoid-utils-directory.js +0 -4
- package/lib/rules/consistent-callback-naming.js +68 -3
- package/lib/rules/dynamic-https-errors.d.ts +1 -1
- package/lib/rules/dynamic-https-errors.js +119 -49
- package/lib/rules/enforce-boolean-naming-prefixes.d.ts +1 -0
- package/lib/rules/enforce-boolean-naming-prefixes.js +86 -331
- 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-dynamic-imports.d.ts +2 -1
- package/lib/rules/enforce-dynamic-imports.js +42 -21
- 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 +66 -15
- package/lib/rules/enforce-mui-rounded-icons.js +42 -1
- 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 +3817 -4641
- package/lib/rules/global-const-style.js +25 -4
- package/lib/rules/logical-top-to-bottom-grouping.js +80 -2
- package/lib/rules/memo-compare-deeply-complex-props.js +183 -6
- package/lib/rules/memo-nested-react-components.js +243 -103
- package/lib/rules/no-array-length-in-deps.js +74 -3
- 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 +150 -489
- package/lib/rules/no-compositing-layer-props.js +31 -0
- 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 +147 -65
- package/lib/rules/no-excessive-parent-chain.js +3 -0
- package/lib/rules/no-explicit-return-type.js +6 -0
- package/lib/rules/no-hungarian.js +119 -24
- package/lib/rules/no-inline-component-prop.js +16 -7
- package/lib/rules/no-margin-properties.js +7 -38
- 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/no-unnecessary-verb-suffix.js +79 -0
- package/lib/rules/no-unused-props.js +215 -37
- package/lib/rules/no-useless-fragment.js +10 -2
- package/lib/rules/parallelize-async-operations.js +117 -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 +75 -4
- package/lib/rules/prefer-use-deep-compare-memo.js +8 -14
- 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 +218 -13
- package/lib/rules/require-https-error-cause.js +30 -11
- package/lib/rules/require-memo.js +17 -9
- package/lib/rules/require-migration-script-metadata.d.ts +9 -0
- package/lib/rules/require-migration-script-metadata.js +206 -0
- package/lib/rules/warn-https-error-message-user-friendly.d.ts +1 -0
- package/lib/rules/warn-https-error-message-user-friendly.js +239 -0
- package/lib/utils/ASTHelpers.d.ts +49 -1
- package/lib/utils/ASTHelpers.js +394 -112
- package/package.json +7 -6
- package/release-manifest.json +166 -0
|
@@ -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,7 +40,28 @@ 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
|
]);
|
|
49
|
+
/**
|
|
50
|
+
* Hooks where any literal defined inside their arguments is safe because
|
|
51
|
+
* the hook either doesn't use the identity for comparison or provides its
|
|
52
|
+
* own stability.
|
|
53
|
+
*/
|
|
54
|
+
const HOOKS_ALLOWING_NESTED_LITERALS = new Set([
|
|
55
|
+
'useState',
|
|
56
|
+
'useReducer',
|
|
57
|
+
'useRef',
|
|
58
|
+
]);
|
|
59
|
+
/**
|
|
60
|
+
* JSX attribute names whose values are style descriptors consumed by the
|
|
61
|
+
* library (not via referential equality), so inline object literals are safe.
|
|
62
|
+
* MUI's `sx` and the standard `style` prop both fall into this category.
|
|
63
|
+
*/
|
|
64
|
+
const STYLE_JSX_ATTRIBUTE_NAMES = new Set(['sx', 'style']);
|
|
43
65
|
const MEMOIZATION_DEPS_TODO_PLACEHOLDER = '__TODO_MEMOIZATION_DEPENDENCIES__';
|
|
44
66
|
const TODO_DEPS_COMMENT = `/* ${MEMOIZATION_DEPS_TODO_PLACEHOLDER} */`;
|
|
45
67
|
const PARENTHESIZED_EXPRESSION_TYPE = utils_1.AST_NODE_TYPES.ParenthesizedExpression ??
|
|
@@ -228,17 +250,26 @@ function findEnclosingComponentOrHook(node) {
|
|
|
228
250
|
function isInsideAllowedHookCallback(node) {
|
|
229
251
|
let current = node;
|
|
230
252
|
while (current) {
|
|
231
|
-
if (current
|
|
232
|
-
current.
|
|
233
|
-
|
|
234
|
-
while (parent && isExpressionWrapper(parent)) {
|
|
235
|
-
parent = parent.parent;
|
|
253
|
+
if (isFunctionNode(current)) {
|
|
254
|
+
if (current.async && current !== node) {
|
|
255
|
+
return true;
|
|
236
256
|
}
|
|
237
|
-
if (
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
257
|
+
if (current.type === utils_1.AST_NODE_TYPES.FunctionExpression ||
|
|
258
|
+
current.type === utils_1.AST_NODE_TYPES.ArrowFunctionExpression) {
|
|
259
|
+
let parent = current.parent;
|
|
260
|
+
// Skip through TypeScript type assertions and parentheses to find
|
|
261
|
+
// the actual CallExpression that invokes the hook.
|
|
262
|
+
while (parent && isExpressionWrapper(parent)) {
|
|
263
|
+
parent = parent.parent;
|
|
264
|
+
}
|
|
265
|
+
if (parent?.type === utils_1.AST_NODE_TYPES.CallExpression) {
|
|
266
|
+
const hookName = getHookNameFromCallee(parent.callee);
|
|
267
|
+
const matchesCallback = parent.arguments.some((arg) => unwrapNestedExpressions(arg) === current);
|
|
268
|
+
if (hookName &&
|
|
269
|
+
SAFE_HOOK_ARGUMENTS.has(hookName) &&
|
|
270
|
+
matchesCallback) {
|
|
271
|
+
return true;
|
|
272
|
+
}
|
|
242
273
|
}
|
|
243
274
|
}
|
|
244
275
|
}
|
|
@@ -394,6 +425,66 @@ function buildMemoSuggestions(node, descriptor, sourceCode) {
|
|
|
394
425
|
},
|
|
395
426
|
];
|
|
396
427
|
}
|
|
428
|
+
/**
|
|
429
|
+
* Returns true when the node's value resolves to a JSX attribute that carries
|
|
430
|
+
* style data consumed by a library rather than compared by reference (MUI `sx`
|
|
431
|
+
* or the standard `style` prop).
|
|
432
|
+
*
|
|
433
|
+
* Walks up from the literal only through positions where it remains the
|
|
434
|
+
* attribute's resolved value: conditional branches (`sx={c ? {…} : {…}}`),
|
|
435
|
+
* logical fallbacks (`sx={c && {…}}`), array entries (MUI accepts `sx` arrays),
|
|
436
|
+
* and expression wrappers (`{…} as const`, parentheses). Any other parent — a
|
|
437
|
+
* function call, an object property, a spread — means the reference is observed
|
|
438
|
+
* or transformed before reaching the attribute, so the walk stops and the
|
|
439
|
+
* literal stays reported. This keeps the exemption tied to genuine style values
|
|
440
|
+
* without silencing unrelated literals that merely appear deeper in the tree.
|
|
441
|
+
*/
|
|
442
|
+
function isStyleJSXAttributeValue(node) {
|
|
443
|
+
let current = node;
|
|
444
|
+
let parent = current.parent;
|
|
445
|
+
while (parent) {
|
|
446
|
+
switch (parent.type) {
|
|
447
|
+
case utils_1.AST_NODE_TYPES.JSXExpressionContainer: {
|
|
448
|
+
if (parent.expression !== current) {
|
|
449
|
+
return false;
|
|
450
|
+
}
|
|
451
|
+
const attribute = parent.parent;
|
|
452
|
+
if (!attribute || attribute.type !== utils_1.AST_NODE_TYPES.JSXAttribute) {
|
|
453
|
+
return false;
|
|
454
|
+
}
|
|
455
|
+
const { name } = attribute;
|
|
456
|
+
return (name.type === utils_1.AST_NODE_TYPES.JSXIdentifier &&
|
|
457
|
+
STYLE_JSX_ATTRIBUTE_NAMES.has(name.name));
|
|
458
|
+
}
|
|
459
|
+
case utils_1.AST_NODE_TYPES.ConditionalExpression: {
|
|
460
|
+
if (parent.consequent !== current && parent.alternate !== current) {
|
|
461
|
+
return false;
|
|
462
|
+
}
|
|
463
|
+
break;
|
|
464
|
+
}
|
|
465
|
+
case utils_1.AST_NODE_TYPES.LogicalExpression: {
|
|
466
|
+
if (parent.left !== current && parent.right !== current) {
|
|
467
|
+
return false;
|
|
468
|
+
}
|
|
469
|
+
break;
|
|
470
|
+
}
|
|
471
|
+
case utils_1.AST_NODE_TYPES.ArrayExpression: {
|
|
472
|
+
if (!parent.elements.some((element) => element === current)) {
|
|
473
|
+
return false;
|
|
474
|
+
}
|
|
475
|
+
break;
|
|
476
|
+
}
|
|
477
|
+
default: {
|
|
478
|
+
if (!isExpressionWrapper(parent) || parent.expression !== current) {
|
|
479
|
+
return false;
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
current = parent;
|
|
484
|
+
parent = current.parent;
|
|
485
|
+
}
|
|
486
|
+
return false;
|
|
487
|
+
}
|
|
397
488
|
/**
|
|
398
489
|
* Formats a readable label for diagnostics based on the owning function.
|
|
399
490
|
* @param fn Owning component or hook function.
|
|
@@ -427,20 +518,134 @@ exports.reactMemoizeLiterals = (0, createRule_1.createRule)({
|
|
|
427
518
|
defaultOptions: [],
|
|
428
519
|
create(context) {
|
|
429
520
|
const sourceCode = context.getSourceCode();
|
|
521
|
+
/**
|
|
522
|
+
* Checks whether a VariableDeclarator's sole usage is being thrown
|
|
523
|
+
* in the same function scope.
|
|
524
|
+
*/
|
|
525
|
+
function isVariableAlwaysThrown(declarator) {
|
|
526
|
+
const variables = ASTHelpers_1.ASTHelpers.getDeclaredVariables(context, declarator);
|
|
527
|
+
if (variables.length === 0) {
|
|
528
|
+
return false;
|
|
529
|
+
}
|
|
530
|
+
const variable = variables[0];
|
|
531
|
+
const usages = variable.references.filter((ref) => !ref.init);
|
|
532
|
+
// Variables with no usages (dead code) don't bypass memoization checks
|
|
533
|
+
// because we can't prove the literal is thrown.
|
|
534
|
+
if (usages.length === 0) {
|
|
535
|
+
return false;
|
|
536
|
+
}
|
|
537
|
+
const owningFunction = findOwningFunction(declarator);
|
|
538
|
+
return usages.every((ref) => {
|
|
539
|
+
let refParent = ref.identifier
|
|
540
|
+
.parent;
|
|
541
|
+
while (refParent) {
|
|
542
|
+
if (isFunctionNode(refParent)) {
|
|
543
|
+
// Stop at function boundaries: throws inside nested functions don't
|
|
544
|
+
// abort the outer render cycle, so they don't make the literal terminal
|
|
545
|
+
// from the perspective of the declaring function.
|
|
546
|
+
return false;
|
|
547
|
+
}
|
|
548
|
+
if (refParent.type === utils_1.AST_NODE_TYPES.ThrowStatement) {
|
|
549
|
+
// Found a throw in the same lexical scope as the usage.
|
|
550
|
+
// Verify the throw is in the same function where the variable was declared
|
|
551
|
+
// to ensure the throw terminates the render before memoization matters.
|
|
552
|
+
let throwCheckParent = refParent.parent;
|
|
553
|
+
while (throwCheckParent) {
|
|
554
|
+
if (isFunctionNode(throwCheckParent)) {
|
|
555
|
+
return throwCheckParent === owningFunction;
|
|
556
|
+
}
|
|
557
|
+
throwCheckParent =
|
|
558
|
+
throwCheckParent.parent;
|
|
559
|
+
}
|
|
560
|
+
return owningFunction === null;
|
|
561
|
+
}
|
|
562
|
+
if (refParent === declarator) {
|
|
563
|
+
break;
|
|
564
|
+
}
|
|
565
|
+
refParent = refParent.parent;
|
|
566
|
+
}
|
|
567
|
+
return false;
|
|
568
|
+
});
|
|
569
|
+
}
|
|
570
|
+
/**
|
|
571
|
+
* Checks whether a literal is destined to be thrown, making referential
|
|
572
|
+
* stability irrelevant.
|
|
573
|
+
*/
|
|
574
|
+
function isTerminalUsage(node) {
|
|
575
|
+
let current = node.parent;
|
|
576
|
+
// Tracks whether the literal passed through a node that represents non-container usage.
|
|
577
|
+
// Once true, any throw we encounter won't count as "terminal" because the literal
|
|
578
|
+
// is already used in an expression that might need memoization.
|
|
579
|
+
let hasPassedForbiddenNode = false;
|
|
580
|
+
while (current) {
|
|
581
|
+
if (current.type === utils_1.AST_NODE_TYPES.ThrowStatement) {
|
|
582
|
+
return !hasPassedForbiddenNode;
|
|
583
|
+
}
|
|
584
|
+
if (current.type === utils_1.AST_NODE_TYPES.VariableDeclarator &&
|
|
585
|
+
current.id.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
586
|
+
current.init) {
|
|
587
|
+
if (!hasPassedForbiddenNode && isVariableAlwaysThrown(current)) {
|
|
588
|
+
return true;
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
// Stop at function boundaries as throws across functions are not "terminal"
|
|
592
|
+
// in the same render cycle sense for the current component/hook.
|
|
593
|
+
if (isFunctionNode(current)) {
|
|
594
|
+
break;
|
|
595
|
+
}
|
|
596
|
+
// We distinguish between "containers" (nodes that build up an error value
|
|
597
|
+
// or wrap an expression) and "usages" (nodes where the value is consumed
|
|
598
|
+
// in a way that might benefit from memoization).
|
|
599
|
+
//
|
|
600
|
+
// Containers (NewExpression for errors, wrappers, or object/array builders)
|
|
601
|
+
// are allowed because they are part of the path to a 'throw'.
|
|
602
|
+
// Other nodes (like function calls or being a prop) mark the literal as
|
|
603
|
+
// having a non-terminal usage.
|
|
604
|
+
//
|
|
605
|
+
// Note: AST_NODE_TYPES.CallExpression is intentionally excluded from allowed
|
|
606
|
+
// containers. Passing a literal into a CallExpression allows the callee to
|
|
607
|
+
// observe or store the reference, making its identity relevant even if
|
|
608
|
+
// the result is eventually thrown (e.g., const err = fn({ ... }); throw err;).
|
|
609
|
+
if (current.type !== utils_1.AST_NODE_TYPES.ObjectExpression &&
|
|
610
|
+
current.type !== utils_1.AST_NODE_TYPES.Property &&
|
|
611
|
+
current.type !== utils_1.AST_NODE_TYPES.ArrayExpression &&
|
|
612
|
+
current.type !== utils_1.AST_NODE_TYPES.NewExpression &&
|
|
613
|
+
current.type !== utils_1.AST_NODE_TYPES.ConditionalExpression &&
|
|
614
|
+
current.type !== utils_1.AST_NODE_TYPES.LogicalExpression &&
|
|
615
|
+
!isExpressionWrapper(current)) {
|
|
616
|
+
hasPassedForbiddenNode = true;
|
|
617
|
+
}
|
|
618
|
+
current = current.parent;
|
|
619
|
+
}
|
|
620
|
+
return false;
|
|
621
|
+
}
|
|
430
622
|
function reportLiteral(node) {
|
|
431
623
|
const descriptor = getLiteralDescriptor(node);
|
|
432
624
|
if (!descriptor)
|
|
433
625
|
return;
|
|
626
|
+
const owner = findEnclosingComponentOrHook(node);
|
|
627
|
+
if (!owner)
|
|
628
|
+
return;
|
|
434
629
|
if (isInsideAllowedHookCallback(node)) {
|
|
435
630
|
return;
|
|
436
631
|
}
|
|
437
|
-
|
|
438
|
-
if (!owner)
|
|
632
|
+
if (isTerminalUsage(node)) {
|
|
439
633
|
return;
|
|
634
|
+
}
|
|
635
|
+
// Inline literals that resolve to a style JSX attribute (sx, style),
|
|
636
|
+
// whether directly or via conditional/logical/array wrappers, are
|
|
637
|
+
// consumed by the library without reference equality checks, so
|
|
638
|
+
// memoization adds no stability benefit.
|
|
639
|
+
if (isStyleJSXAttributeValue(node)) {
|
|
640
|
+
return;
|
|
641
|
+
}
|
|
440
642
|
const hookCall = findEnclosingHookCall(node);
|
|
441
643
|
if (hookCall) {
|
|
442
|
-
if (hookCall.isDirectArgument
|
|
644
|
+
if (hookCall.isDirectArgument ||
|
|
645
|
+
HOOKS_ALLOWING_NESTED_LITERALS.has(hookCall.hookName)) {
|
|
443
646
|
// Top-level literal passed directly to a hook argument is allowed.
|
|
647
|
+
// Also allow nested literals for hooks that don't rely on argument
|
|
648
|
+
// reference stability for memoization (e.g. useState, useReducer).
|
|
444
649
|
return;
|
|
445
650
|
}
|
|
446
651
|
context.report({
|
|
@@ -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) {
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
type MessageIds = 'missingMetadata' | 'metadataAfterStatement' | 'missingMigrationTag' | 'invalidMigrationTag' | 'missingPhaseTag' | 'invalidPhaseTag' | 'missingDependenciesTag' | 'invalidDependenciesTag' | 'missingDescriptionTag' | 'extensionInDependencies' | 'noneCasing' | 'legacyHeaderNotAllowed' | 'multipleMetadataBlocks';
|
|
2
|
+
type Options = [
|
|
3
|
+
{
|
|
4
|
+
targetGlobs?: string[];
|
|
5
|
+
allowLegacyHeader?: boolean;
|
|
6
|
+
}
|
|
7
|
+
];
|
|
8
|
+
export declare const requireMigrationScriptMetadata: import("@typescript-eslint/utils/dist/ts-eslint/Rule").RuleModule<MessageIds, Options, import("@typescript-eslint/utils/dist/ts-eslint/Rule").RuleListener>;
|
|
9
|
+
export {};
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.requireMigrationScriptMetadata = void 0;
|
|
4
|
+
const minimatch_1 = require("minimatch");
|
|
5
|
+
const createRule_1 = require("../utils/createRule");
|
|
6
|
+
const DEFAULT_OPTIONS = {
|
|
7
|
+
targetGlobs: ['**/functions/src/callable/scripts/**/*.f.ts'],
|
|
8
|
+
allowLegacyHeader: true,
|
|
9
|
+
};
|
|
10
|
+
exports.requireMigrationScriptMetadata = (0, createRule_1.createRule)({
|
|
11
|
+
name: 'require-migration-script-metadata',
|
|
12
|
+
meta: {
|
|
13
|
+
type: 'suggestion',
|
|
14
|
+
docs: {
|
|
15
|
+
description: 'Enforce JSDoc migration metadata in callable scripts',
|
|
16
|
+
recommended: 'error',
|
|
17
|
+
},
|
|
18
|
+
schema: [
|
|
19
|
+
{
|
|
20
|
+
type: 'object',
|
|
21
|
+
properties: {
|
|
22
|
+
targetGlobs: {
|
|
23
|
+
type: 'array',
|
|
24
|
+
items: { type: 'string' },
|
|
25
|
+
},
|
|
26
|
+
allowLegacyHeader: {
|
|
27
|
+
type: 'boolean',
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
additionalProperties: false,
|
|
31
|
+
},
|
|
32
|
+
],
|
|
33
|
+
messages: {
|
|
34
|
+
missingMetadata: 'Missing migration metadata JSDoc block → Release tooling cannot determine participation or ordering, which can skip or misorder migrations → Add a top-of-file JSDoc block with `@migration` true/false; when true include `@migrationPhase`, `@migrationDependencies` (or NONE), and `@migrationDescription`.',
|
|
35
|
+
metadataAfterStatement: 'Migration metadata appears after code/imports → The release scanner only reads the top-of-file block and will miss this metadata → Move the metadata JSDoc above all imports/statements (legacy header allowed only when configured).',
|
|
36
|
+
missingMigrationTag: 'Metadata block is missing `@migration` → Without an explicit true/false, automation cannot decide whether to run this script → Add `@migration` true or `@migration` false to the metadata block.',
|
|
37
|
+
invalidMigrationTag: 'Invalid `@migration` value → Only true/false is supported so automation can branch deterministically → Replace with `@migration` true or `@migration` false.',
|
|
38
|
+
missingPhaseTag: 'Missing `@migrationPhase` while `@migration` is true → Release ordering cannot place this script → Add `@migrationPhase` before or after.',
|
|
39
|
+
invalidPhaseTag: 'Invalid `@migrationPhase` value → Release ordering only understands before/after → Use `@migrationPhase` before or `@migrationPhase` after.',
|
|
40
|
+
missingDependenciesTag: 'Missing `@migrationDependencies` while `@migration` is true → Dependency ordering cannot be computed → Add `@migrationDependencies` NONE or a comma-separated list of script names.',
|
|
41
|
+
invalidDependenciesTag: 'Invalid `@migrationDependencies` list → Empty or whitespace entries break dependency resolution → Provide a comma-separated list of non-empty names or NONE.',
|
|
42
|
+
missingDescriptionTag: 'Missing `@migrationDescription` while `@migration` is true → Release reports lose context and reviewers cannot assess impact → Add a brief `@migrationDescription`.',
|
|
43
|
+
extensionInDependencies: 'Dependency "{{name}}" includes a file extension → Dependencies are script identifiers, and extensions cause mismatches → Remove the ".f.ts" extension from "{{name}}".',
|
|
44
|
+
noneCasing: 'Invalid `@migrationDependencies` casing → Automation expects exactly "NONE" in all-caps to signal no dependencies → Change to uppercase "NONE".',
|
|
45
|
+
legacyHeaderNotAllowed: 'Legacy header/comment appears before metadata → The metadata block must be the first file-level block to keep linting deterministic → Remove leading comments or enable allowLegacyHeader.',
|
|
46
|
+
multipleMetadataBlocks: 'Multiple metadata blocks found → Conflicting `@migration` tags make automation ambiguous → Keep exactly one JSDoc block with `@migration`.',
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
defaultOptions: [DEFAULT_OPTIONS],
|
|
50
|
+
create(context, [options]) {
|
|
51
|
+
const rawFilename = context.filename ?? context.getFilename();
|
|
52
|
+
const filename = rawFilename.replace(/\\/g, '/');
|
|
53
|
+
const { targetGlobs, allowLegacyHeader } = {
|
|
54
|
+
...DEFAULT_OPTIONS,
|
|
55
|
+
...options,
|
|
56
|
+
};
|
|
57
|
+
// Only run on files matching targetGlobs
|
|
58
|
+
const isTargetFile = targetGlobs?.some((glob) => (0, minimatch_1.minimatch)(filename, glob, { dot: true, matchBase: true }));
|
|
59
|
+
if (!isTargetFile) {
|
|
60
|
+
return {};
|
|
61
|
+
}
|
|
62
|
+
const sourceCode = context.sourceCode ?? context.getSourceCode();
|
|
63
|
+
const comments = sourceCode.getAllComments();
|
|
64
|
+
const firstToken = sourceCode.getFirstToken(sourceCode.ast, {
|
|
65
|
+
includeComments: false,
|
|
66
|
+
});
|
|
67
|
+
return {
|
|
68
|
+
Program() {
|
|
69
|
+
const metadataCandidates = comments
|
|
70
|
+
.filter((comment) => comment.type === 'Block' && comment.value.startsWith('*'))
|
|
71
|
+
.map((comment) => ({ comment, tags: parseJSDocTags(comment.value) }))
|
|
72
|
+
.filter(({ tags }) => Object.keys(tags).some((tag) => tag.startsWith('migration')));
|
|
73
|
+
if (metadataCandidates.length > 1) {
|
|
74
|
+
for (const extra of metadataCandidates.slice(1)) {
|
|
75
|
+
context.report({
|
|
76
|
+
node: extra.comment,
|
|
77
|
+
messageId: 'multipleMetadataBlocks',
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
const migrationMetadata = metadataCandidates[0] ?? null;
|
|
82
|
+
if (!migrationMetadata) {
|
|
83
|
+
context.report({
|
|
84
|
+
loc: { line: 1, column: 0 },
|
|
85
|
+
messageId: 'missingMetadata',
|
|
86
|
+
});
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
const { comment, tags } = migrationMetadata;
|
|
90
|
+
if (!allowLegacyHeader) {
|
|
91
|
+
const hasLeadingComment = comments.some((c) => c.type === 'Block' && c.range[0] < comment.range[0]);
|
|
92
|
+
if (hasLeadingComment) {
|
|
93
|
+
context.report({
|
|
94
|
+
node: comment,
|
|
95
|
+
messageId: 'legacyHeaderNotAllowed',
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
// Check if metadata appears after the first statement/import
|
|
100
|
+
if (firstToken && comment.range[0] > firstToken.range[0]) {
|
|
101
|
+
context.report({
|
|
102
|
+
node: comment,
|
|
103
|
+
messageId: 'metadataAfterStatement',
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
if (!('migration' in tags)) {
|
|
107
|
+
context.report({
|
|
108
|
+
node: comment,
|
|
109
|
+
messageId: 'missingMigrationTag',
|
|
110
|
+
});
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
const migration = tags.migration;
|
|
114
|
+
if (migration !== 'true' && migration !== 'false') {
|
|
115
|
+
context.report({
|
|
116
|
+
node: comment,
|
|
117
|
+
messageId: 'invalidMigrationTag',
|
|
118
|
+
});
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
if (migration === 'true') {
|
|
122
|
+
// Check phase
|
|
123
|
+
if (!tags.migrationPhase) {
|
|
124
|
+
context.report({
|
|
125
|
+
node: comment,
|
|
126
|
+
messageId: 'missingPhaseTag',
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
else if (tags.migrationPhase !== 'before' &&
|
|
130
|
+
tags.migrationPhase !== 'after') {
|
|
131
|
+
context.report({
|
|
132
|
+
node: comment,
|
|
133
|
+
messageId: 'invalidPhaseTag',
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
// Check dependencies
|
|
137
|
+
if (!tags.migrationDependencies) {
|
|
138
|
+
context.report({
|
|
139
|
+
node: comment,
|
|
140
|
+
messageId: 'missingDependenciesTag',
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
else {
|
|
144
|
+
const deps = tags.migrationDependencies
|
|
145
|
+
.split(',')
|
|
146
|
+
.map((d) => d.trim());
|
|
147
|
+
const hasNone = deps.some((d) => d.toUpperCase() === 'NONE');
|
|
148
|
+
if (deps.some((d) => d === '')) {
|
|
149
|
+
context.report({
|
|
150
|
+
node: comment,
|
|
151
|
+
messageId: 'invalidDependenciesTag',
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
else if (hasNone && deps.length > 1) {
|
|
155
|
+
context.report({
|
|
156
|
+
node: comment,
|
|
157
|
+
messageId: 'invalidDependenciesTag',
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
else if (hasNone && deps[0] !== 'NONE') {
|
|
161
|
+
context.report({
|
|
162
|
+
node: comment,
|
|
163
|
+
messageId: 'noneCasing',
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
else if (hasNone && deps[0] === 'NONE') {
|
|
167
|
+
// Valid: exactly NONE
|
|
168
|
+
}
|
|
169
|
+
else {
|
|
170
|
+
for (const dep of deps) {
|
|
171
|
+
if (dep.endsWith('.f.ts')) {
|
|
172
|
+
context.report({
|
|
173
|
+
node: comment,
|
|
174
|
+
messageId: 'extensionInDependencies',
|
|
175
|
+
data: { name: dep },
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
// Check description
|
|
182
|
+
if (!tags.migrationDescription) {
|
|
183
|
+
context.report({
|
|
184
|
+
node: comment,
|
|
185
|
+
messageId: 'missingDescriptionTag',
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
},
|
|
190
|
+
};
|
|
191
|
+
},
|
|
192
|
+
});
|
|
193
|
+
function parseJSDocTags(commentValue) {
|
|
194
|
+
const tags = {};
|
|
195
|
+
const lines = commentValue.split('\n');
|
|
196
|
+
for (const line of lines) {
|
|
197
|
+
const trimmedLine = line.trim().replace(/^\*+\s*/, '');
|
|
198
|
+
const match = trimmedLine.match(/^@(\w+)(?:\s+(.*))?$/);
|
|
199
|
+
if (match) {
|
|
200
|
+
const [, tagName, tagValue] = match;
|
|
201
|
+
tags[tagName] = (tagValue ?? '').trim();
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
return tags;
|
|
205
|
+
}
|
|
206
|
+
//# sourceMappingURL=require-migration-script-metadata.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const warnHttpsErrorMessageUserFriendly: import("@typescript-eslint/utils/dist/ts-eslint/Rule").RuleModule<"warnHttpsErrorMessageUserFriendly", [], import("@typescript-eslint/utils/dist/ts-eslint/Rule").RuleListener>;
|