@blumintinc/eslint-plugin-blumint 1.12.6 → 1.14.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 +171 -99
- package/lib/index.js +300 -17
- package/lib/rules/array-methods-this-context.js +20 -2
- package/lib/rules/avoid-utils-directory.js +11 -2
- package/lib/rules/class-methods-read-top-to-bottom.js +24 -5
- package/lib/rules/consistent-callback-naming.js +1 -1
- package/lib/rules/dynamic-https-errors.d.ts +3 -1
- package/lib/rules/dynamic-https-errors.js +48 -11
- package/lib/rules/enforce-assert-safe-object-key.d.ts +8 -0
- package/lib/rules/{enforce-assertSafe-object-key.js → enforce-assert-safe-object-key.js} +52 -96
- package/lib/rules/enforce-assert-throws.d.ts +3 -1
- package/lib/rules/enforce-assert-throws.js +493 -51
- package/lib/rules/enforce-boolean-naming-prefixes.d.ts +3 -1
- package/lib/rules/enforce-boolean-naming-prefixes.js +820 -39
- package/lib/rules/enforce-callable-types.js +4 -4
- package/lib/rules/enforce-callback-memo.js +143 -18
- package/lib/rules/enforce-centralized-mock-firestore.js +5 -2
- package/lib/rules/enforce-console-error.d.ts +3 -0
- package/lib/rules/enforce-console-error.js +278 -0
- package/lib/rules/enforce-css-media-queries.js +24 -46
- package/lib/rules/enforce-dynamic-file-naming.d.ts +2 -0
- package/lib/rules/enforce-dynamic-file-naming.js +53 -28
- package/lib/rules/enforce-dynamic-firebase-imports.js +100 -19
- package/lib/rules/enforce-dynamic-imports.js +1 -1
- package/lib/rules/enforce-early-destructuring.d.ts +2 -0
- package/lib/rules/enforce-early-destructuring.js +980 -0
- package/lib/rules/enforce-empty-object-check.d.ts +11 -0
- package/lib/rules/enforce-empty-object-check.js +502 -0
- package/lib/rules/enforce-exported-function-types.js +4 -4
- package/lib/rules/enforce-fieldpath-syntax-in-docsetter.d.ts +2 -0
- package/lib/rules/enforce-fieldpath-syntax-in-docsetter.js +262 -0
- package/lib/rules/enforce-firestore-doc-ref-generic.js +350 -3
- package/lib/rules/enforce-firestore-facade.js +349 -157
- package/lib/rules/enforce-firestore-rules-get-access.d.ts +3 -0
- package/lib/rules/enforce-firestore-rules-get-access.js +109 -0
- package/lib/rules/enforce-firestore-set-merge.js +3 -4
- package/lib/rules/enforce-global-constants.d.ts +4 -1
- package/lib/rules/enforce-global-constants.js +240 -12
- package/lib/rules/enforce-identifiable-firestore-type.js +108 -58
- package/lib/rules/enforce-memoize-async.d.ts +2 -1
- package/lib/rules/enforce-memoize-async.js +92 -27
- package/lib/rules/enforce-memoize-getters.d.ts +3 -0
- package/lib/rules/enforce-memoize-getters.js +147 -0
- package/lib/rules/enforce-microdiff.js +3 -2
- package/lib/rules/enforce-object-literal-as-const.js +4 -4
- package/lib/rules/enforce-positive-naming.js +9 -7
- package/lib/rules/enforce-props-argument-name.d.ts +2 -7
- package/lib/rules/enforce-props-argument-name.js +165 -151
- package/lib/rules/enforce-props-naming-consistency.d.ts +2 -0
- package/lib/rules/enforce-props-naming-consistency.js +145 -0
- package/lib/rules/enforce-querykey-ts.d.ts +9 -0
- package/lib/rules/enforce-querykey-ts.js +274 -0
- package/lib/rules/enforce-safe-stringify.js +1 -1
- package/lib/rules/enforce-serializable-params.js +3 -3
- package/lib/rules/enforce-singular-type-names.js +10 -6
- package/lib/rules/enforce-stable-hash-spread-props.d.ts +13 -0
- package/lib/rules/enforce-stable-hash-spread-props.js +342 -0
- package/lib/rules/enforce-storage-context.d.ts +9 -0
- package/lib/rules/enforce-storage-context.js +646 -0
- package/lib/rules/enforce-timestamp-now.js +23 -7
- package/lib/rules/enforce-transform-memoization.d.ts +4 -0
- package/lib/rules/enforce-transform-memoization.js +416 -0
- package/lib/rules/enforce-typescript-markdown-code-blocks.d.ts +1 -0
- package/lib/rules/enforce-typescript-markdown-code-blocks.js +100 -0
- package/lib/rules/enforce-unique-cursor-headers.d.ts +16 -0
- package/lib/rules/enforce-unique-cursor-headers.js +365 -0
- package/lib/rules/enforce-verb-noun-naming.js +90 -14
- package/lib/rules/ensure-pointer-events-none.js +28 -2
- package/lib/rules/export-if-in-doubt.js +27 -4
- package/lib/rules/extract-global-constants.js +92 -53
- package/lib/rules/fast-deep-equal-over-microdiff.d.ts +2 -1
- package/lib/rules/fast-deep-equal-over-microdiff.js +351 -83
- package/lib/rules/firestore-transaction-reads-before-writes.d.ts +1 -0
- package/lib/rules/firestore-transaction-reads-before-writes.js +277 -0
- package/lib/rules/flatten-push-calls.d.ts +2 -0
- package/lib/rules/flatten-push-calls.js +428 -0
- package/lib/rules/generic-starts-with-t.js +8 -2
- package/lib/rules/global-const-style.js +50 -19
- package/lib/rules/jsdoc-above-field.d.ts +11 -0
- package/lib/rules/jsdoc-above-field.js +208 -0
- package/lib/rules/key-only-outermost-element.js +21 -3
- package/lib/rules/logical-top-to-bottom-grouping.d.ts +5 -0
- package/lib/rules/logical-top-to-bottom-grouping.js +1258 -0
- package/lib/rules/memo-compare-deeply-complex-props.d.ts +3 -0
- package/lib/rules/memo-compare-deeply-complex-props.js +788 -0
- package/lib/rules/memo-nested-react-components.d.ts +8 -0
- package/lib/rules/memo-nested-react-components.js +410 -0
- package/lib/rules/memoize-root-level-hocs.d.ts +7 -0
- package/lib/rules/memoize-root-level-hocs.js +242 -0
- package/lib/rules/no-always-true-false-conditions.d.ts +2 -1
- package/lib/rules/no-always-true-false-conditions.js +68 -8
- package/lib/rules/no-array-length-in-deps.d.ts +12 -0
- package/lib/rules/no-array-length-in-deps.js +314 -0
- package/lib/rules/no-async-array-filter.js +5 -2
- package/lib/rules/no-async-foreach.js +210 -12
- package/lib/rules/no-circular-references.js +18 -17
- package/lib/rules/no-class-instance-destructuring.js +55 -12
- package/lib/rules/no-complex-cloud-params.js +22 -5
- package/lib/rules/no-compositing-layer-props.js +6 -4
- package/lib/rules/no-conditional-literals-in-jsx.js +37 -12
- package/lib/rules/no-console-error.d.ts +9 -0
- package/lib/rules/no-console-error.js +525 -0
- package/lib/rules/no-curly-brackets-around-commented-properties.d.ts +2 -0
- package/lib/rules/no-curly-brackets-around-commented-properties.js +258 -0
- package/lib/rules/no-empty-dependency-use-callbacks.d.ts +11 -0
- package/lib/rules/no-empty-dependency-use-callbacks.js +576 -0
- package/lib/rules/no-entire-object-hook-deps.d.ts +3 -1
- package/lib/rules/no-entire-object-hook-deps.js +320 -68
- package/lib/rules/no-excessive-parent-chain.d.ts +5 -0
- package/lib/rules/no-excessive-parent-chain.js +273 -0
- package/lib/rules/no-explicit-return-type.d.ts +2 -1
- package/lib/rules/no-explicit-return-type.js +215 -26
- package/lib/rules/no-filter-without-return.js +5 -1
- package/lib/rules/no-firestore-jest-mock.d.ts +2 -1
- package/lib/rules/no-firestore-jest-mock.js +126 -8
- package/lib/rules/no-firestore-object-arrays.js +398 -28
- package/lib/rules/no-handler-suffix.d.ts +12 -0
- package/lib/rules/no-handler-suffix.js +305 -0
- package/lib/rules/no-hungarian.js +14 -14
- package/lib/rules/no-inline-component-prop.d.ts +10 -0
- package/lib/rules/no-inline-component-prop.js +456 -0
- package/lib/rules/no-jsx-in-hooks.js +6 -1
- package/lib/rules/no-jsx-whitespace-literal.js +8 -2
- package/lib/rules/no-margin-properties.d.ts +7 -1
- package/lib/rules/no-margin-properties.js +22 -9
- package/lib/rules/no-memoize-on-static.js +9 -1
- package/lib/rules/no-misleading-boolean-prefixes.d.ts +7 -0
- package/lib/rules/no-misleading-boolean-prefixes.js +331 -0
- package/lib/rules/no-misused-switch-case.js +22 -1
- package/lib/rules/no-mixed-firestore-transactions.d.ts +3 -1
- package/lib/rules/no-mixed-firestore-transactions.js +296 -34
- package/lib/rules/no-mock-firebase-admin.js +5 -2
- package/lib/rules/no-object-values-on-strings.js +10 -2
- package/lib/rules/no-overridable-method-calls-in-constructor.d.ts +1 -0
- package/lib/rules/no-overridable-method-calls-in-constructor.js +272 -0
- package/lib/rules/no-passthrough-getters.d.ts +4 -0
- package/lib/rules/no-passthrough-getters.js +206 -0
- package/lib/rules/no-redundant-annotation-assertion.d.ts +2 -0
- package/lib/rules/no-redundant-annotation-assertion.js +402 -0
- package/lib/rules/no-redundant-param-types.js +16 -6
- package/lib/rules/no-redundant-this-params.d.ts +3 -0
- package/lib/rules/no-redundant-this-params.js +459 -0
- package/lib/rules/no-redundant-usecallback-wrapper.d.ts +8 -0
- package/lib/rules/no-redundant-usecallback-wrapper.js +328 -0
- package/lib/rules/no-res-error-status-in-onrequest.d.ts +4 -0
- package/lib/rules/no-res-error-status-in-onrequest.js +521 -0
- package/lib/rules/no-restricted-properties-fix.d.ts +12 -0
- package/lib/rules/no-restricted-properties-fix.js +173 -0
- package/lib/rules/no-separate-loading-state.d.ts +8 -0
- package/lib/rules/no-separate-loading-state.js +133 -0
- package/lib/rules/no-stale-state-across-await.d.ts +1 -0
- package/lib/rules/no-stale-state-across-await.js +185 -0
- package/lib/rules/no-static-constants-in-dynamic-files.d.ts +3 -0
- package/lib/rules/no-static-constants-in-dynamic-files.js +149 -0
- package/lib/rules/no-try-catch-already-exists-in-transaction.d.ts +1 -0
- package/lib/rules/no-try-catch-already-exists-in-transaction.js +342 -0
- package/lib/rules/no-type-assertion-returns.js +49 -49
- package/lib/rules/no-undefined-null-passthrough.js +217 -40
- package/lib/rules/no-unmemoized-memo-without-props.d.ts +8 -0
- package/lib/rules/no-unmemoized-memo-without-props.js +426 -0
- package/lib/rules/no-unnecessary-destructuring-rename.d.ts +2 -0
- package/lib/rules/no-unnecessary-destructuring-rename.js +347 -0
- package/lib/rules/no-unnecessary-destructuring.js +14 -4
- package/lib/rules/no-unnecessary-verb-suffix.js +2 -3
- package/lib/rules/no-unpinned-dependencies.js +36 -5
- package/lib/rules/no-unsafe-firestore-spread.js +8 -5
- package/lib/rules/no-unused-props.d.ts +5 -2
- package/lib/rules/no-unused-props.js +451 -112
- package/lib/rules/no-unused-usestate.js +6 -2
- package/lib/rules/no-useless-fragment.js +28 -2
- package/lib/rules/no-useless-usememo-primitives.d.ts +9 -0
- package/lib/rules/no-useless-usememo-primitives.js +393 -0
- package/lib/rules/no-usememo-for-pass-by-value.d.ts +13 -0
- package/lib/rules/no-usememo-for-pass-by-value.js +757 -0
- package/lib/rules/no-uuidv4-base62-as-key.js +18 -13
- package/lib/rules/omit-index-html.d.ts +2 -1
- package/lib/rules/omit-index-html.js +62 -7
- package/lib/rules/optimize-object-boolean-conditions.d.ts +1 -0
- package/lib/rules/optimize-object-boolean-conditions.js +236 -0
- package/lib/rules/parallelize-async-operations.d.ts +8 -0
- package/lib/rules/parallelize-async-operations.js +394 -0
- package/lib/rules/prefer-batch-operations.d.ts +1 -3
- package/lib/rules/prefer-batch-operations.js +228 -35
- package/lib/rules/prefer-block-comments-for-declarations.js +22 -7
- package/lib/rules/prefer-clone-deep.js +3 -3
- package/lib/rules/prefer-destructuring-no-class.d.ts +2 -1
- package/lib/rules/prefer-destructuring-no-class.js +244 -59
- package/lib/rules/prefer-docsetter-setall.d.ts +2 -0
- package/lib/rules/prefer-docsetter-setall.js +243 -0
- package/lib/rules/prefer-document-flattening.d.ts +4 -0
- package/lib/rules/prefer-document-flattening.js +239 -0
- package/lib/rules/prefer-field-paths-in-transforms.d.ts +7 -0
- package/lib/rules/prefer-field-paths-in-transforms.js +251 -0
- package/lib/rules/prefer-fragment-component.js +4 -4
- package/lib/rules/prefer-fragment-shorthand.js +2 -1
- package/lib/rules/prefer-getter-over-parameterless-method.d.ts +13 -0
- package/lib/rules/prefer-getter-over-parameterless-method.js +648 -0
- package/lib/rules/prefer-global-router-state-key.d.ts +7 -3
- package/lib/rules/prefer-global-router-state-key.js +305 -31
- package/lib/rules/prefer-memoized-props.d.ts +3 -0
- package/lib/rules/prefer-memoized-props.js +445 -0
- package/lib/rules/prefer-next-dynamic.d.ts +9 -0
- package/lib/rules/prefer-next-dynamic.js +357 -0
- package/lib/rules/prefer-nullish-coalescing-boolean-props.d.ts +1 -0
- package/lib/rules/prefer-nullish-coalescing-boolean-props.js +435 -0
- package/lib/rules/prefer-nullish-coalescing-override.d.ts +7 -0
- package/lib/rules/prefer-nullish-coalescing-override.js +188 -0
- package/lib/rules/prefer-params-over-parent-id.d.ts +5 -0
- package/lib/rules/prefer-params-over-parent-id.js +277 -0
- package/lib/rules/prefer-settings-object.js +23 -15
- package/lib/rules/prefer-type-alias-over-typeof-constant.d.ts +3 -0
- package/lib/rules/prefer-type-alias-over-typeof-constant.js +259 -0
- package/lib/rules/prefer-type-over-interface.js +7 -2
- package/lib/rules/prefer-url-tostring-over-tojson.d.ts +2 -0
- package/lib/rules/prefer-url-tostring-over-tojson.js +169 -0
- package/lib/rules/prefer-use-deep-compare-memo.d.ts +4 -0
- package/lib/rules/prefer-use-deep-compare-memo.js +431 -0
- package/lib/rules/prefer-usecallback-over-usememo-for-functions.js +25 -2
- package/lib/rules/prefer-usememo-over-useeffect-usestate.js +24 -5
- package/lib/rules/prefer-utility-function-over-private-static.js +34 -2
- package/lib/rules/prevent-children-clobber.d.ts +2 -0
- package/lib/rules/prevent-children-clobber.js +536 -0
- package/lib/rules/react-memoize-literals.d.ts +4 -0
- package/lib/rules/react-memoize-literals.js +495 -0
- package/lib/rules/react-usememo-should-be-component.d.ts +2 -1
- package/lib/rules/react-usememo-should-be-component.js +144 -78
- package/lib/rules/require-dynamic-firebase-imports.js +2 -1
- package/lib/rules/require-hooks-default-params.js +17 -16
- package/lib/rules/require-https-error-cause.d.ts +4 -0
- package/lib/rules/require-https-error-cause.js +136 -0
- package/lib/rules/require-https-error.js +43 -21
- package/lib/rules/require-image-optimized.js +2 -2
- package/lib/rules/require-memo.js +12 -2
- package/lib/rules/require-memoize-jsx-returners.d.ts +3 -0
- package/lib/rules/require-memoize-jsx-returners.js +485 -0
- package/lib/rules/require-usememo-object-literals.js +9 -4
- package/lib/rules/semantic-function-prefixes.js +38 -18
- package/lib/rules/sync-onwrite-name-func.js +5 -1
- package/lib/rules/test-file-location-enforcement.d.ts +1 -0
- package/lib/rules/test-file-location-enforcement.js +57 -0
- package/lib/rules/use-custom-link.js +6 -1
- package/lib/rules/use-custom-router.js +6 -1
- package/lib/rules/use-latest-callback.d.ts +3 -0
- package/lib/rules/use-latest-callback.js +271 -0
- package/lib/rules/vertically-group-related-functions.d.ts +16 -0
- package/lib/rules/vertically-group-related-functions.js +480 -0
- package/lib/utils/ASTHelpers.d.ts +13 -1
- package/lib/utils/ASTHelpers.js +34 -0
- package/lib/utils/createRule.js +1 -1
- package/lib/utils/getMethodName.d.ts +27 -0
- package/lib/utils/getMethodName.js +35 -0
- package/lib/utils/graph/ClassGraphSorterReadability.js +1 -1
- package/lib/utils/ruleTester.d.ts +1 -0
- package/lib/utils/ruleTester.js +4 -1
- package/lib/utils/tsTypeClassifier.d.ts +30 -0
- package/lib/utils/tsTypeClassifier.js +149 -0
- package/package.json +23 -14
- package/lib/rules/enforce-assertSafe-object-key.d.ts +0 -2
|
@@ -0,0 +1,410 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.memoNestedReactComponents = void 0;
|
|
4
|
+
const utils_1 = require("@typescript-eslint/utils");
|
|
5
|
+
const minimatch_1 = require("minimatch");
|
|
6
|
+
const createRule_1 = require("../utils/createRule");
|
|
7
|
+
const ASTHelpers_1 = require("../utils/ASTHelpers");
|
|
8
|
+
const CALLBACK_HOOKS = new Set(['useCallback', 'useDeepCompareCallback']);
|
|
9
|
+
const HOOK_REPLACEMENT = {
|
|
10
|
+
useCallback: 'useMemo',
|
|
11
|
+
useDeepCompareCallback: 'useDeepCompareMemo',
|
|
12
|
+
};
|
|
13
|
+
const collectReactImports = (sourceCode) => {
|
|
14
|
+
const reactImports = { namespace: null, named: {} };
|
|
15
|
+
for (const statement of sourceCode.ast.body) {
|
|
16
|
+
if (statement.type !== utils_1.AST_NODE_TYPES.ImportDeclaration)
|
|
17
|
+
continue;
|
|
18
|
+
if (statement.source.value !== 'react')
|
|
19
|
+
continue;
|
|
20
|
+
for (const specifier of statement.specifiers) {
|
|
21
|
+
if (specifier.type === utils_1.AST_NODE_TYPES.ImportSpecifier &&
|
|
22
|
+
specifier.imported.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
23
|
+
const importedName = specifier.imported.name;
|
|
24
|
+
if (importedName === 'createElement' ||
|
|
25
|
+
importedName === 'memo' ||
|
|
26
|
+
importedName === 'forwardRef') {
|
|
27
|
+
reactImports.named[importedName] = specifier.local.name;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
if (specifier.type === utils_1.AST_NODE_TYPES.ImportDefaultSpecifier ||
|
|
31
|
+
specifier.type === utils_1.AST_NODE_TYPES.ImportNamespaceSpecifier) {
|
|
32
|
+
reactImports.namespace = specifier.local.name;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return reactImports;
|
|
37
|
+
};
|
|
38
|
+
const calleeMatchesReactMember = (callee, reactImports, member) => {
|
|
39
|
+
if (callee.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
40
|
+
return reactImports.named[member] === callee.name;
|
|
41
|
+
}
|
|
42
|
+
if (callee.type === utils_1.AST_NODE_TYPES.MemberExpression &&
|
|
43
|
+
!callee.computed &&
|
|
44
|
+
callee.property.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
45
|
+
callee.property.name === member &&
|
|
46
|
+
callee.object.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
47
|
+
reactImports.namespace === callee.object.name) {
|
|
48
|
+
return true;
|
|
49
|
+
}
|
|
50
|
+
return false;
|
|
51
|
+
};
|
|
52
|
+
const isFunctionExpression = (node) => {
|
|
53
|
+
return (node.type === utils_1.AST_NODE_TYPES.ArrowFunctionExpression ||
|
|
54
|
+
node.type === utils_1.AST_NODE_TYPES.FunctionExpression);
|
|
55
|
+
};
|
|
56
|
+
const unwrapExpression = (expression) => {
|
|
57
|
+
if (expression.type === utils_1.AST_NODE_TYPES.TSAsExpression) {
|
|
58
|
+
return unwrapExpression(expression.expression);
|
|
59
|
+
}
|
|
60
|
+
if (expression.type === utils_1.AST_NODE_TYPES.TSSatisfiesExpression) {
|
|
61
|
+
return unwrapExpression(expression.expression);
|
|
62
|
+
}
|
|
63
|
+
if (expression.type === utils_1.AST_NODE_TYPES.TSNonNullExpression) {
|
|
64
|
+
return unwrapExpression(expression.expression);
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* ParenthesizedExpression appears in the parsed AST even though the type
|
|
68
|
+
* is missing from AST_NODE_TYPES. This assertion safely unwraps to keep
|
|
69
|
+
* traversal consistent.
|
|
70
|
+
*/
|
|
71
|
+
if (expression.type ===
|
|
72
|
+
'ParenthesizedExpression') {
|
|
73
|
+
return unwrapExpression(expression.expression);
|
|
74
|
+
}
|
|
75
|
+
return expression;
|
|
76
|
+
};
|
|
77
|
+
const isReactCreateElementCall = (node, reactImports) => {
|
|
78
|
+
return calleeMatchesReactMember(node.callee, reactImports, 'createElement');
|
|
79
|
+
};
|
|
80
|
+
const isHookCall = (callee) => {
|
|
81
|
+
if (callee.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
82
|
+
if (CALLBACK_HOOKS.has(callee.name)) {
|
|
83
|
+
return { name: callee.name };
|
|
84
|
+
}
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
if (callee.type === utils_1.AST_NODE_TYPES.MemberExpression &&
|
|
88
|
+
!callee.computed &&
|
|
89
|
+
callee.property.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
90
|
+
CALLBACK_HOOKS.has(callee.property.name)) {
|
|
91
|
+
return { name: callee.property.name };
|
|
92
|
+
}
|
|
93
|
+
return null;
|
|
94
|
+
};
|
|
95
|
+
const isForwardRefCall = (node, reactImports) => {
|
|
96
|
+
return calleeMatchesReactMember(node.callee, reactImports, 'forwardRef');
|
|
97
|
+
};
|
|
98
|
+
const isMemoCall = (node, reactImports) => {
|
|
99
|
+
return calleeMatchesReactMember(node.callee, reactImports, 'memo');
|
|
100
|
+
};
|
|
101
|
+
const filterPresentResults = (results) => {
|
|
102
|
+
return results.filter((result) => Boolean(result));
|
|
103
|
+
};
|
|
104
|
+
const areAllComponentsCallbacks = (components) => {
|
|
105
|
+
return components.every((result) => result.componentIsCallback);
|
|
106
|
+
};
|
|
107
|
+
const createMergedResult = (components) => {
|
|
108
|
+
return {
|
|
109
|
+
found: true,
|
|
110
|
+
// Auto-fix wraps the callback in memo() and swaps the hook. That is only valid when
|
|
111
|
+
// every branch is itself a component callback (returns JSX/createElement directly).
|
|
112
|
+
// If any branch yields a component factory, the transformation changes behavior.
|
|
113
|
+
componentIsCallback: areAllComponentsCallbacks(components),
|
|
114
|
+
};
|
|
115
|
+
};
|
|
116
|
+
const mergeComponentResults = (...results) => {
|
|
117
|
+
const present = filterPresentResults(results);
|
|
118
|
+
if (!present.length) {
|
|
119
|
+
return null;
|
|
120
|
+
}
|
|
121
|
+
return createMergedResult(present);
|
|
122
|
+
};
|
|
123
|
+
const expressionCreatesComponent = (expression, reactImports) => {
|
|
124
|
+
if (!expression) {
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
127
|
+
const unwrapped = unwrapExpression(expression);
|
|
128
|
+
if (unwrapped.type === utils_1.AST_NODE_TYPES.JSXElement ||
|
|
129
|
+
unwrapped.type === utils_1.AST_NODE_TYPES.JSXFragment) {
|
|
130
|
+
return { found: true, componentIsCallback: true };
|
|
131
|
+
}
|
|
132
|
+
if (unwrapped.type === utils_1.AST_NODE_TYPES.CallExpression &&
|
|
133
|
+
isReactCreateElementCall(unwrapped, reactImports)) {
|
|
134
|
+
return { found: true, componentIsCallback: true };
|
|
135
|
+
}
|
|
136
|
+
switch (unwrapped.type) {
|
|
137
|
+
case utils_1.AST_NODE_TYPES.ArrowFunctionExpression:
|
|
138
|
+
case utils_1.AST_NODE_TYPES.FunctionExpression: {
|
|
139
|
+
const inner = functionCreatesComponent(unwrapped, reactImports);
|
|
140
|
+
return inner ? { found: true, componentIsCallback: false } : null;
|
|
141
|
+
}
|
|
142
|
+
case utils_1.AST_NODE_TYPES.CallExpression: {
|
|
143
|
+
if (isForwardRefCall(unwrapped, reactImports) ||
|
|
144
|
+
isMemoCall(unwrapped, reactImports)) {
|
|
145
|
+
const firstArg = unwrapped.arguments[0];
|
|
146
|
+
if (firstArg && isFunctionExpression(firstArg)) {
|
|
147
|
+
const inner = functionCreatesComponent(firstArg, reactImports);
|
|
148
|
+
if (inner) {
|
|
149
|
+
return { found: true, componentIsCallback: false };
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
return null;
|
|
154
|
+
}
|
|
155
|
+
case utils_1.AST_NODE_TYPES.ConditionalExpression: {
|
|
156
|
+
const cons = expressionCreatesComponent(unwrapped.consequent, reactImports);
|
|
157
|
+
const alt = expressionCreatesComponent(unwrapped.alternate, reactImports);
|
|
158
|
+
return mergeComponentResults(cons, alt);
|
|
159
|
+
}
|
|
160
|
+
case utils_1.AST_NODE_TYPES.LogicalExpression: {
|
|
161
|
+
const left = expressionCreatesComponent(unwrapped.left, reactImports);
|
|
162
|
+
const right = expressionCreatesComponent(unwrapped.right, reactImports);
|
|
163
|
+
return mergeComponentResults(left, right);
|
|
164
|
+
}
|
|
165
|
+
case utils_1.AST_NODE_TYPES.SequenceExpression: {
|
|
166
|
+
const last = unwrapped.expressions[unwrapped.expressions.length - 1];
|
|
167
|
+
return expressionCreatesComponent(last || null, reactImports);
|
|
168
|
+
}
|
|
169
|
+
case utils_1.AST_NODE_TYPES.ArrayExpression: {
|
|
170
|
+
const matches = unwrapped.elements
|
|
171
|
+
.map((element) => element && element.type !== utils_1.AST_NODE_TYPES.SpreadElement
|
|
172
|
+
? expressionCreatesComponent(element, reactImports)
|
|
173
|
+
: null)
|
|
174
|
+
.filter(Boolean);
|
|
175
|
+
return mergeComponentResults(...matches);
|
|
176
|
+
}
|
|
177
|
+
default:
|
|
178
|
+
return null;
|
|
179
|
+
}
|
|
180
|
+
};
|
|
181
|
+
const statementCreatesComponent = (node, reactImports) => {
|
|
182
|
+
switch (node.type) {
|
|
183
|
+
case utils_1.AST_NODE_TYPES.ReturnStatement:
|
|
184
|
+
return expressionCreatesComponent(node.argument, reactImports);
|
|
185
|
+
case utils_1.AST_NODE_TYPES.IfStatement: {
|
|
186
|
+
const cons = node.consequent.type === utils_1.AST_NODE_TYPES.BlockStatement
|
|
187
|
+
? blockCreatesComponent(node.consequent, reactImports)
|
|
188
|
+
: statementCreatesComponent(node.consequent, reactImports);
|
|
189
|
+
const alt = node.alternate
|
|
190
|
+
? node.alternate.type === utils_1.AST_NODE_TYPES.BlockStatement
|
|
191
|
+
? blockCreatesComponent(node.alternate, reactImports)
|
|
192
|
+
: statementCreatesComponent(node.alternate, reactImports)
|
|
193
|
+
: null;
|
|
194
|
+
return mergeComponentResults(cons, alt);
|
|
195
|
+
}
|
|
196
|
+
case utils_1.AST_NODE_TYPES.SwitchStatement: {
|
|
197
|
+
const matches = [];
|
|
198
|
+
for (const switchCase of node.cases) {
|
|
199
|
+
for (const consequent of switchCase.consequent) {
|
|
200
|
+
matches.push(statementCreatesComponent(consequent, reactImports));
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
return mergeComponentResults(...matches);
|
|
204
|
+
}
|
|
205
|
+
case utils_1.AST_NODE_TYPES.BlockStatement:
|
|
206
|
+
return blockCreatesComponent(node, reactImports);
|
|
207
|
+
case utils_1.AST_NODE_TYPES.ForStatement:
|
|
208
|
+
case utils_1.AST_NODE_TYPES.ForInStatement:
|
|
209
|
+
case utils_1.AST_NODE_TYPES.ForOfStatement:
|
|
210
|
+
case utils_1.AST_NODE_TYPES.WhileStatement:
|
|
211
|
+
case utils_1.AST_NODE_TYPES.DoWhileStatement: {
|
|
212
|
+
if (node.body.type === utils_1.AST_NODE_TYPES.BlockStatement) {
|
|
213
|
+
return blockCreatesComponent(node.body, reactImports);
|
|
214
|
+
}
|
|
215
|
+
return statementCreatesComponent(node.body, reactImports);
|
|
216
|
+
}
|
|
217
|
+
case utils_1.AST_NODE_TYPES.TryStatement: {
|
|
218
|
+
const blockMatch = blockCreatesComponent(node.block, reactImports);
|
|
219
|
+
const handlerMatch = node.handler?.body
|
|
220
|
+
? blockCreatesComponent(node.handler.body, reactImports)
|
|
221
|
+
: null;
|
|
222
|
+
const finalizerMatch = node.finalizer
|
|
223
|
+
? blockCreatesComponent(node.finalizer, reactImports)
|
|
224
|
+
: null;
|
|
225
|
+
return mergeComponentResults(blockMatch, handlerMatch, finalizerMatch);
|
|
226
|
+
}
|
|
227
|
+
default:
|
|
228
|
+
return null;
|
|
229
|
+
}
|
|
230
|
+
};
|
|
231
|
+
const blockCreatesComponent = (block, reactImports) => {
|
|
232
|
+
const matches = [];
|
|
233
|
+
for (const statement of block.body) {
|
|
234
|
+
matches.push(statementCreatesComponent(statement, reactImports));
|
|
235
|
+
}
|
|
236
|
+
return mergeComponentResults(...matches);
|
|
237
|
+
};
|
|
238
|
+
const functionCreatesComponent = (node, reactImports) => {
|
|
239
|
+
if (node.body.type === utils_1.AST_NODE_TYPES.BlockStatement) {
|
|
240
|
+
return blockCreatesComponent(node.body, reactImports);
|
|
241
|
+
}
|
|
242
|
+
return expressionCreatesComponent(node.body, reactImports);
|
|
243
|
+
};
|
|
244
|
+
const shouldIgnoreFile = (filename, patterns) => {
|
|
245
|
+
return patterns.some((pattern) => (0, minimatch_1.minimatch)(filename, pattern));
|
|
246
|
+
};
|
|
247
|
+
const getVariableName = (node) => {
|
|
248
|
+
if (node.parent &&
|
|
249
|
+
node.parent.type === utils_1.AST_NODE_TYPES.VariableDeclarator &&
|
|
250
|
+
node.parent.id.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
251
|
+
return node.parent.id.name;
|
|
252
|
+
}
|
|
253
|
+
return null;
|
|
254
|
+
};
|
|
255
|
+
const findMemoReference = (reactImports) => {
|
|
256
|
+
if (reactImports.namespace) {
|
|
257
|
+
return `${reactImports.namespace}.memo`;
|
|
258
|
+
}
|
|
259
|
+
return reactImports.named.memo ?? null;
|
|
260
|
+
};
|
|
261
|
+
const hasIdentifierInScope = (identifierName, scope) => {
|
|
262
|
+
return !!ASTHelpers_1.ASTHelpers.findVariableInScope(scope, identifierName);
|
|
263
|
+
};
|
|
264
|
+
exports.memoNestedReactComponents = (0, createRule_1.createRule)({
|
|
265
|
+
name: 'memo-nested-react-components',
|
|
266
|
+
meta: {
|
|
267
|
+
type: 'suggestion',
|
|
268
|
+
docs: {
|
|
269
|
+
description: 'Disallow React components defined in useCallback/useDeepCompareCallback',
|
|
270
|
+
recommended: 'error',
|
|
271
|
+
},
|
|
272
|
+
fixable: 'code',
|
|
273
|
+
schema: [
|
|
274
|
+
{
|
|
275
|
+
type: 'object',
|
|
276
|
+
properties: {
|
|
277
|
+
ignorePatterns: {
|
|
278
|
+
type: 'array',
|
|
279
|
+
items: { type: 'string' },
|
|
280
|
+
default: [],
|
|
281
|
+
},
|
|
282
|
+
},
|
|
283
|
+
additionalProperties: false,
|
|
284
|
+
},
|
|
285
|
+
],
|
|
286
|
+
messages: {
|
|
287
|
+
memoizeNestedComponent: `What's wrong: React component "{{componentName}}" is created inside {{hookName}}.
|
|
288
|
+
Why it matters: Components defined inside callbacks get new identities when the callback changes, causing React to remount them and drop their state and effects.
|
|
289
|
+
How to fix: Create the component via {{replacementHook}} and wrap it in memo() so its identity stays stable across renders.`,
|
|
290
|
+
},
|
|
291
|
+
},
|
|
292
|
+
defaultOptions: [{}],
|
|
293
|
+
create(context, [options]) {
|
|
294
|
+
const ignorePatterns = options?.ignorePatterns ?? [];
|
|
295
|
+
const filename = context.filename ?? context.getFilename();
|
|
296
|
+
if (ignorePatterns.length && shouldIgnoreFile(filename, ignorePatterns)) {
|
|
297
|
+
return {};
|
|
298
|
+
}
|
|
299
|
+
const sourceCode = context.sourceCode ?? context.getSourceCode();
|
|
300
|
+
const reactImports = collectReactImports(sourceCode);
|
|
301
|
+
const memoReference = findMemoReference(reactImports);
|
|
302
|
+
return {
|
|
303
|
+
CallExpression(node) {
|
|
304
|
+
const hook = isHookCall(node.callee);
|
|
305
|
+
if (!hook)
|
|
306
|
+
return;
|
|
307
|
+
const callback = node.arguments[0];
|
|
308
|
+
if (!callback || callback.type === utils_1.AST_NODE_TYPES.SpreadElement) {
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
const componentMatch = isFunctionExpression(callback)
|
|
312
|
+
? functionCreatesComponent(callback, reactImports)
|
|
313
|
+
: expressionCreatesComponent(callback, reactImports);
|
|
314
|
+
if (!componentMatch) {
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
const componentName = getVariableName(node) ??
|
|
318
|
+
(isFunctionExpression(callback) &&
|
|
319
|
+
callback.id?.type === utils_1.AST_NODE_TYPES.Identifier
|
|
320
|
+
? callback.id.name
|
|
321
|
+
: 'component');
|
|
322
|
+
const replacementHook = HOOK_REPLACEMENT[hook.name];
|
|
323
|
+
context.report({
|
|
324
|
+
node,
|
|
325
|
+
messageId: 'memoizeNestedComponent',
|
|
326
|
+
data: {
|
|
327
|
+
componentName,
|
|
328
|
+
hookName: `${hook.name}()`,
|
|
329
|
+
replacementHook: `${replacementHook}()`,
|
|
330
|
+
},
|
|
331
|
+
fix: (fixer) => {
|
|
332
|
+
if (!memoReference) {
|
|
333
|
+
return null;
|
|
334
|
+
}
|
|
335
|
+
const memoNamespace = memoReference.endsWith('.memo')
|
|
336
|
+
? memoReference.slice(0, -'.memo'.length)
|
|
337
|
+
: null;
|
|
338
|
+
// Prefer SourceCode#getScope when available; fall back to RuleContext#getScope for ESLint compatibility.
|
|
339
|
+
const sourceCodeWithScope = sourceCode;
|
|
340
|
+
const scope = typeof sourceCodeWithScope.getScope === 'function'
|
|
341
|
+
? sourceCodeWithScope.getScope(node)
|
|
342
|
+
: context.getScope();
|
|
343
|
+
const replacementIdentifierAvailable = hook.name === 'useCallback'
|
|
344
|
+
? hasIdentifierInScope(HOOK_REPLACEMENT.useCallback, scope)
|
|
345
|
+
: hasIdentifierInScope(HOOK_REPLACEMENT.useDeepCompareCallback, scope);
|
|
346
|
+
if (!replacementIdentifierAvailable) {
|
|
347
|
+
// When the standalone replacement hook (useMemo/useDeepCompareMemo) is not imported,
|
|
348
|
+
// we can only fix member expressions that use the React namespace.
|
|
349
|
+
if (node.callee.type !== utils_1.AST_NODE_TYPES.MemberExpression ||
|
|
350
|
+
node.callee.object.type !== utils_1.AST_NODE_TYPES.Identifier) {
|
|
351
|
+
return null;
|
|
352
|
+
}
|
|
353
|
+
// Ensure memo's namespace matches the hook's namespace (e.g., React.useCallback → React.useMemo + React.memo).
|
|
354
|
+
if (memoReference &&
|
|
355
|
+
memoReference.includes('.') &&
|
|
356
|
+
memoReference !== `${node.callee.object.name}.memo`) {
|
|
357
|
+
return null;
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
// Component factories (functions returning functions that return JSX) require manual refactoring.
|
|
361
|
+
if (!componentMatch.componentIsCallback) {
|
|
362
|
+
return null;
|
|
363
|
+
}
|
|
364
|
+
// Ensure callback is a function expression before attempting to wrap it.
|
|
365
|
+
if (!isFunctionExpression(callback)) {
|
|
366
|
+
return null;
|
|
367
|
+
}
|
|
368
|
+
// React.memo() expects a synchronous component function. Wrapping async functions or
|
|
369
|
+
// generators would produce a Promise/yielding component, which is not renderable.
|
|
370
|
+
if (callback.async ||
|
|
371
|
+
(callback.type === utils_1.AST_NODE_TYPES.FunctionExpression &&
|
|
372
|
+
callback.generator)) {
|
|
373
|
+
return null;
|
|
374
|
+
}
|
|
375
|
+
// Avoid rewriting non-React namespaces (e.g., Hooks.useCallback → Hooks.useMemo),
|
|
376
|
+
// even when useMemo/memo are available as named imports.
|
|
377
|
+
if (node.callee.type === utils_1.AST_NODE_TYPES.MemberExpression) {
|
|
378
|
+
if (node.callee.object.type !== utils_1.AST_NODE_TYPES.Identifier ||
|
|
379
|
+
!reactImports.namespace ||
|
|
380
|
+
node.callee.object.name !== reactImports.namespace) {
|
|
381
|
+
return null;
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
// Prevent mixing namespaces: don't apply fix when hook and memo use different React imports.
|
|
385
|
+
if (memoNamespace &&
|
|
386
|
+
node.callee.type === utils_1.AST_NODE_TYPES.MemberExpression &&
|
|
387
|
+
node.callee.object.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
388
|
+
node.callee.object.name !== memoNamespace) {
|
|
389
|
+
return null;
|
|
390
|
+
}
|
|
391
|
+
const callbackText = sourceCode.getText(callback);
|
|
392
|
+
const callbackReplacement = `() => ${memoReference}(${callbackText})`;
|
|
393
|
+
const fixes = [
|
|
394
|
+
fixer.replaceText(callback, callbackReplacement),
|
|
395
|
+
];
|
|
396
|
+
if (node.callee.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
397
|
+
fixes.push(fixer.replaceText(node.callee, HOOK_REPLACEMENT[node.callee.name]));
|
|
398
|
+
}
|
|
399
|
+
else if (node.callee.type === utils_1.AST_NODE_TYPES.MemberExpression &&
|
|
400
|
+
node.callee.property.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
401
|
+
fixes.push(fixer.replaceText(node.callee.property, HOOK_REPLACEMENT[node.callee.property.name]));
|
|
402
|
+
}
|
|
403
|
+
return fixes;
|
|
404
|
+
},
|
|
405
|
+
});
|
|
406
|
+
},
|
|
407
|
+
};
|
|
408
|
+
},
|
|
409
|
+
});
|
|
410
|
+
//# sourceMappingURL=memo-nested-react-components.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
type Options = [
|
|
2
|
+
{
|
|
3
|
+
additionalHocNames?: string[];
|
|
4
|
+
}
|
|
5
|
+
];
|
|
6
|
+
export declare const memoizeRootLevelHocs: import("@typescript-eslint/utils/dist/ts-eslint/Rule").RuleModule<"wrapHocInUseMemo", Options, import("@typescript-eslint/utils/dist/ts-eslint/Rule").RuleListener>;
|
|
7
|
+
export {};
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.memoizeRootLevelHocs = void 0;
|
|
4
|
+
const utils_1 = require("@typescript-eslint/utils");
|
|
5
|
+
const createRule_1 = require("../utils/createRule");
|
|
6
|
+
const defaultOptions = [{ additionalHocNames: [] }];
|
|
7
|
+
const isFunctionNode = (node) => node.type === utils_1.AST_NODE_TYPES.FunctionDeclaration ||
|
|
8
|
+
node.type === utils_1.AST_NODE_TYPES.FunctionExpression ||
|
|
9
|
+
node.type === utils_1.AST_NODE_TYPES.ArrowFunctionExpression;
|
|
10
|
+
const isHookName = (name) => /^use[A-Z]/.test(name);
|
|
11
|
+
const isComponentName = (name) => /^[A-Z]/.test(name);
|
|
12
|
+
const forEachChildNode = (node, callback) => {
|
|
13
|
+
for (const key of Object.keys(node)) {
|
|
14
|
+
if (key === 'parent')
|
|
15
|
+
continue;
|
|
16
|
+
const value = node[key];
|
|
17
|
+
if (!value)
|
|
18
|
+
continue;
|
|
19
|
+
if (Array.isArray(value)) {
|
|
20
|
+
for (const child of value) {
|
|
21
|
+
if (child && typeof child.type === 'string') {
|
|
22
|
+
if (callback(child)) {
|
|
23
|
+
return true;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
else if (value && typeof value.type === 'string') {
|
|
29
|
+
if (callback(value)) {
|
|
30
|
+
return true;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return false;
|
|
35
|
+
};
|
|
36
|
+
const getFunctionName = (node) => {
|
|
37
|
+
if (node.type === utils_1.AST_NODE_TYPES.FunctionDeclaration && node.id) {
|
|
38
|
+
return node.id.name;
|
|
39
|
+
}
|
|
40
|
+
if (node.type === utils_1.AST_NODE_TYPES.FunctionExpression ||
|
|
41
|
+
node.type === utils_1.AST_NODE_TYPES.ArrowFunctionExpression) {
|
|
42
|
+
const parent = node.parent;
|
|
43
|
+
if (parent &&
|
|
44
|
+
parent.type === utils_1.AST_NODE_TYPES.VariableDeclarator &&
|
|
45
|
+
parent.id.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
46
|
+
return parent.id.name;
|
|
47
|
+
}
|
|
48
|
+
if (parent &&
|
|
49
|
+
parent.type === utils_1.AST_NODE_TYPES.Property &&
|
|
50
|
+
parent.key.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
51
|
+
return parent.key.name;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return null;
|
|
55
|
+
};
|
|
56
|
+
const containsJsx = (node, options) => {
|
|
57
|
+
if (!node)
|
|
58
|
+
return false;
|
|
59
|
+
if (node.type === utils_1.AST_NODE_TYPES.JSXElement ||
|
|
60
|
+
node.type === utils_1.AST_NODE_TYPES.JSXFragment) {
|
|
61
|
+
return true;
|
|
62
|
+
}
|
|
63
|
+
return forEachChildNode(node, (child) => {
|
|
64
|
+
if (options?.skipFunctionBodies && isFunctionNode(child)) {
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
return containsJsx(child, options);
|
|
68
|
+
});
|
|
69
|
+
};
|
|
70
|
+
const hasFunctionParent = (node) => {
|
|
71
|
+
let current = node.parent;
|
|
72
|
+
while (current) {
|
|
73
|
+
if (isFunctionNode(current)) {
|
|
74
|
+
return true;
|
|
75
|
+
}
|
|
76
|
+
current = current.parent;
|
|
77
|
+
}
|
|
78
|
+
return false;
|
|
79
|
+
};
|
|
80
|
+
const getBodyNodeForJsxCheck = (node) => {
|
|
81
|
+
if (node.type === utils_1.AST_NODE_TYPES.FunctionDeclaration ||
|
|
82
|
+
node.type === utils_1.AST_NODE_TYPES.FunctionExpression) {
|
|
83
|
+
return node.body;
|
|
84
|
+
}
|
|
85
|
+
if (node.body && node.body.type !== utils_1.AST_NODE_TYPES.BlockStatement) {
|
|
86
|
+
return node.body;
|
|
87
|
+
}
|
|
88
|
+
return node.body;
|
|
89
|
+
};
|
|
90
|
+
const isComponentOrHook = (node) => {
|
|
91
|
+
const name = getFunctionName(node);
|
|
92
|
+
const hook = name ? isHookName(name) : false;
|
|
93
|
+
const component = name ? isComponentName(name) : false;
|
|
94
|
+
const nestedFunction = hasFunctionParent(node);
|
|
95
|
+
const jsxBody = containsJsx(getBodyNodeForJsxCheck(node), {
|
|
96
|
+
skipFunctionBodies: !hook && !component,
|
|
97
|
+
});
|
|
98
|
+
if (!hook && !component && nestedFunction) {
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
if (!hook && !component && !jsxBody) {
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
if (hook) {
|
|
105
|
+
return { contextLabel: `hook${name ? ` ${name}` : ''}` };
|
|
106
|
+
}
|
|
107
|
+
return { contextLabel: `component${name ? ` ${name}` : ''}` };
|
|
108
|
+
};
|
|
109
|
+
const getCallableIdentifierName = (callee) => {
|
|
110
|
+
const maybeChain = callee;
|
|
111
|
+
if (maybeChain.type === utils_1.AST_NODE_TYPES.ChainExpression) {
|
|
112
|
+
return getCallableIdentifierName(maybeChain.expression);
|
|
113
|
+
}
|
|
114
|
+
if (callee.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
115
|
+
return callee.name;
|
|
116
|
+
}
|
|
117
|
+
if (callee.type === utils_1.AST_NODE_TYPES.MemberExpression &&
|
|
118
|
+
!callee.computed &&
|
|
119
|
+
callee.property.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
120
|
+
return callee.property.name;
|
|
121
|
+
}
|
|
122
|
+
return null;
|
|
123
|
+
};
|
|
124
|
+
const isHocIdentifier = (name, additionalHocs) => {
|
|
125
|
+
if (additionalHocs.has(name)) {
|
|
126
|
+
return true;
|
|
127
|
+
}
|
|
128
|
+
if (!name.startsWith('with')) {
|
|
129
|
+
return false;
|
|
130
|
+
}
|
|
131
|
+
const suffix = name.charAt(4);
|
|
132
|
+
return Boolean(suffix) && /^[A-Z]$/.test(suffix);
|
|
133
|
+
};
|
|
134
|
+
const findHocName = (node, additionalHocs) => {
|
|
135
|
+
const identifier = getCallableIdentifierName(node.callee);
|
|
136
|
+
if (identifier && isHocIdentifier(identifier, additionalHocs)) {
|
|
137
|
+
return identifier;
|
|
138
|
+
}
|
|
139
|
+
if (node.callee.type === utils_1.AST_NODE_TYPES.CallExpression) {
|
|
140
|
+
return findHocName(node.callee, additionalHocs);
|
|
141
|
+
}
|
|
142
|
+
return null;
|
|
143
|
+
};
|
|
144
|
+
/**
|
|
145
|
+
* Detects chained HOC calls where an inner call is immediately invoked by
|
|
146
|
+
* another call (for example, withHoc(Component)()). We only treat calls as
|
|
147
|
+
* part of the same chain when the current CallExpression is the callee of its
|
|
148
|
+
* parent and both resolve to the same HOC name, which prevents duplicate
|
|
149
|
+
* reports for patterns like withHoc(Component)(props).
|
|
150
|
+
*/
|
|
151
|
+
const getParentCallExpression = (callExpr) => callExpr.parent &&
|
|
152
|
+
callExpr.parent.type === utils_1.AST_NODE_TYPES.CallExpression &&
|
|
153
|
+
callExpr.parent.callee === callExpr
|
|
154
|
+
? callExpr.parent
|
|
155
|
+
: null;
|
|
156
|
+
const isPartOfHocChain = (hocName, parentHocName) => Boolean(parentHocName && parentHocName === hocName);
|
|
157
|
+
exports.memoizeRootLevelHocs = (0, createRule_1.createRule)({
|
|
158
|
+
name: 'memoize-root-level-hocs',
|
|
159
|
+
meta: {
|
|
160
|
+
type: 'problem',
|
|
161
|
+
docs: {
|
|
162
|
+
description: 'Prevent creating Higher-Order Components at the root level of React components/hooks without wrapping them in useMemo to keep wrapped component identities stable across renders.',
|
|
163
|
+
recommended: 'error',
|
|
164
|
+
},
|
|
165
|
+
fixable: undefined,
|
|
166
|
+
schema: [
|
|
167
|
+
{
|
|
168
|
+
type: 'object',
|
|
169
|
+
properties: {
|
|
170
|
+
additionalHocNames: {
|
|
171
|
+
type: 'array',
|
|
172
|
+
items: { type: 'string' },
|
|
173
|
+
default: [],
|
|
174
|
+
},
|
|
175
|
+
},
|
|
176
|
+
additionalProperties: false,
|
|
177
|
+
},
|
|
178
|
+
],
|
|
179
|
+
messages: {
|
|
180
|
+
wrapHocInUseMemo: 'HOC "{{hocName}}" is created inside {{contextLabel}} during render, so every render creates a brand-new wrapped component reference. React treats that as a different component, unmounting/remounting it (resetting internal state) and forcing children to re-render even when props stay the same. Wrap the HOC creation in useMemo with the correct dependencies or hoist it outside the {{contextLabel}} so the wrapped component identity stays stable.',
|
|
181
|
+
},
|
|
182
|
+
},
|
|
183
|
+
defaultOptions,
|
|
184
|
+
create(context, [options]) {
|
|
185
|
+
const additionalHocs = new Set(options?.additionalHocNames ?? []);
|
|
186
|
+
const reportUnmemoizedHoc = (node, hocName, contextInfo) => {
|
|
187
|
+
context.report({
|
|
188
|
+
node,
|
|
189
|
+
messageId: 'wrapHocInUseMemo',
|
|
190
|
+
data: {
|
|
191
|
+
hocName,
|
|
192
|
+
contextLabel: contextInfo.contextLabel,
|
|
193
|
+
},
|
|
194
|
+
});
|
|
195
|
+
};
|
|
196
|
+
const checkHocCall = (callExpr, contextInfo) => {
|
|
197
|
+
const hocName = findHocName(callExpr, additionalHocs);
|
|
198
|
+
const parentCall = getParentCallExpression(callExpr);
|
|
199
|
+
const parentHocName = parentCall && findHocName(parentCall, additionalHocs);
|
|
200
|
+
if (hocName && !isPartOfHocChain(hocName, parentHocName)) {
|
|
201
|
+
reportUnmemoizedHoc(callExpr, hocName, contextInfo);
|
|
202
|
+
}
|
|
203
|
+
};
|
|
204
|
+
const traverseFunctionBody = (node, contextInfo) => {
|
|
205
|
+
const visitNode = (current) => {
|
|
206
|
+
if (isFunctionNode(current) && current !== node) {
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
if (current.type === utils_1.AST_NODE_TYPES.CallExpression) {
|
|
210
|
+
checkHocCall(current, contextInfo);
|
|
211
|
+
}
|
|
212
|
+
forEachChildNode(current, (child) => {
|
|
213
|
+
visitNode(child);
|
|
214
|
+
return false;
|
|
215
|
+
});
|
|
216
|
+
};
|
|
217
|
+
if (!node.body) {
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
if (node.body.type === utils_1.AST_NODE_TYPES.BlockStatement) {
|
|
221
|
+
for (const statement of node.body.body) {
|
|
222
|
+
visitNode(statement);
|
|
223
|
+
}
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
visitNode(node.body);
|
|
227
|
+
};
|
|
228
|
+
const analyzeFunction = (node) => {
|
|
229
|
+
const contextInfo = isComponentOrHook(node);
|
|
230
|
+
if (!contextInfo) {
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
traverseFunctionBody(node, contextInfo);
|
|
234
|
+
};
|
|
235
|
+
return {
|
|
236
|
+
FunctionDeclaration: analyzeFunction,
|
|
237
|
+
FunctionExpression: analyzeFunction,
|
|
238
|
+
ArrowFunctionExpression: analyzeFunction,
|
|
239
|
+
};
|
|
240
|
+
},
|
|
241
|
+
});
|
|
242
|
+
//# sourceMappingURL=memoize-root-level-hocs.js.map
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { TSESLint } from '@typescript-eslint/utils';
|
|
1
2
|
type MessageIds = 'alwaysTrueCondition' | 'alwaysFalseCondition';
|
|
2
|
-
export declare const noAlwaysTrueFalseConditions:
|
|
3
|
+
export declare const noAlwaysTrueFalseConditions: TSESLint.RuleModule<MessageIds, [], TSESLint.RuleListener>;
|
|
3
4
|
export {};
|