@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,495 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.reactMemoizeLiterals = void 0;
|
|
4
|
+
const utils_1 = require("@typescript-eslint/utils");
|
|
5
|
+
const createRule_1 = require("../utils/createRule");
|
|
6
|
+
const LITERAL_DESCRIPTOR_BY_TYPE = {
|
|
7
|
+
[utils_1.AST_NODE_TYPES.ObjectExpression]: {
|
|
8
|
+
literalType: 'object literal',
|
|
9
|
+
memoHook: 'useMemo',
|
|
10
|
+
},
|
|
11
|
+
[utils_1.AST_NODE_TYPES.ArrayExpression]: {
|
|
12
|
+
literalType: 'array literal',
|
|
13
|
+
memoHook: 'useMemo',
|
|
14
|
+
},
|
|
15
|
+
[utils_1.AST_NODE_TYPES.ArrowFunctionExpression]: {
|
|
16
|
+
literalType: 'inline function',
|
|
17
|
+
memoHook: 'useCallback',
|
|
18
|
+
},
|
|
19
|
+
[utils_1.AST_NODE_TYPES.FunctionExpression]: {
|
|
20
|
+
literalType: 'inline function',
|
|
21
|
+
memoHook: 'useCallback',
|
|
22
|
+
},
|
|
23
|
+
[utils_1.AST_NODE_TYPES.FunctionDeclaration]: {
|
|
24
|
+
literalType: 'inline function',
|
|
25
|
+
memoHook: 'useCallback',
|
|
26
|
+
},
|
|
27
|
+
};
|
|
28
|
+
const SAFE_HOOK_ARGUMENTS = new Set([
|
|
29
|
+
'useMemo',
|
|
30
|
+
'useCallback',
|
|
31
|
+
'useEffect',
|
|
32
|
+
'useLayoutEffect',
|
|
33
|
+
'useInsertionEffect',
|
|
34
|
+
'useImperativeHandle',
|
|
35
|
+
'useState',
|
|
36
|
+
'useReducer',
|
|
37
|
+
'useRef',
|
|
38
|
+
'useSyncExternalStore',
|
|
39
|
+
'useDeferredValue',
|
|
40
|
+
'useTransition',
|
|
41
|
+
'useId',
|
|
42
|
+
]);
|
|
43
|
+
const MEMOIZATION_DEPS_TODO_PLACEHOLDER = '__TODO_MEMOIZATION_DEPENDENCIES__';
|
|
44
|
+
const TODO_DEPS_COMMENT = `/* ${MEMOIZATION_DEPS_TODO_PLACEHOLDER} */`;
|
|
45
|
+
const PARENTHESIZED_EXPRESSION_TYPE = utils_1.AST_NODE_TYPES.ParenthesizedExpression ??
|
|
46
|
+
'ParenthesizedExpression';
|
|
47
|
+
/**
|
|
48
|
+
* Detects React hook-style identifiers prefixed with "use".
|
|
49
|
+
* @param name Candidate identifier name.
|
|
50
|
+
* @returns True when the name follows the hook naming convention.
|
|
51
|
+
*/
|
|
52
|
+
function isHookName(name) {
|
|
53
|
+
return !!name && /^use[A-Z]/.test(name);
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Detects PascalCase identifiers commonly used for React components.
|
|
57
|
+
* @param name Candidate identifier name.
|
|
58
|
+
* @returns True when the name begins with an uppercase character.
|
|
59
|
+
*/
|
|
60
|
+
function isComponentName(name) {
|
|
61
|
+
return !!name && /^[A-Z]/.test(name);
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Type guard for parenthesized expressions to unwrap safely.
|
|
65
|
+
* @param node Node to evaluate.
|
|
66
|
+
* @returns True when the node is a parenthesized expression wrapper.
|
|
67
|
+
*/
|
|
68
|
+
function isParenthesizedExpression(node) {
|
|
69
|
+
return node.type === PARENTHESIZED_EXPRESSION_TYPE;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Extracts an identifier name from variable, assignment, or property nodes.
|
|
73
|
+
* @param node AST node that may carry an identifier.
|
|
74
|
+
* @returns Identifier name when present, otherwise null.
|
|
75
|
+
*/
|
|
76
|
+
function getNameFromNode(node) {
|
|
77
|
+
if (!node)
|
|
78
|
+
return null;
|
|
79
|
+
if (node.type === utils_1.AST_NODE_TYPES.VariableDeclarator &&
|
|
80
|
+
node.id.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
81
|
+
return node.id.name;
|
|
82
|
+
}
|
|
83
|
+
if (node.type === utils_1.AST_NODE_TYPES.AssignmentExpression &&
|
|
84
|
+
node.left.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
85
|
+
return node.left.name;
|
|
86
|
+
}
|
|
87
|
+
if (node.type === utils_1.AST_NODE_TYPES.Property &&
|
|
88
|
+
node.key.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
89
|
+
return node.key.name;
|
|
90
|
+
}
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Determines whether a node should be traversed through when resolving names.
|
|
95
|
+
* @param node Node to evaluate for transparency.
|
|
96
|
+
* @returns True when the node does not introduce a binding boundary.
|
|
97
|
+
*/
|
|
98
|
+
function isTransparentNode(node) {
|
|
99
|
+
return (node.type === utils_1.AST_NODE_TYPES.CallExpression ||
|
|
100
|
+
node.type === utils_1.AST_NODE_TYPES.MemberExpression ||
|
|
101
|
+
isExpressionWrapper(node));
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Walks ancestors through transparent nodes to find the nearest identifier name.
|
|
105
|
+
* @param startNode Node where the search begins.
|
|
106
|
+
* @returns Resolved identifier name or null if none is located.
|
|
107
|
+
*/
|
|
108
|
+
function findNameInAncestors(startNode) {
|
|
109
|
+
let current = startNode;
|
|
110
|
+
while (current) {
|
|
111
|
+
const name = getNameFromNode(current);
|
|
112
|
+
if (name) {
|
|
113
|
+
return name;
|
|
114
|
+
}
|
|
115
|
+
if (!isTransparentNode(current)) {
|
|
116
|
+
break;
|
|
117
|
+
}
|
|
118
|
+
current = current.parent;
|
|
119
|
+
}
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Resolves a function's display name from its declaration or surrounding
|
|
124
|
+
* assignment/property wrappers so HOC-wrapped and asserted components still map
|
|
125
|
+
* to their intended identifiers.
|
|
126
|
+
*/
|
|
127
|
+
function getFunctionName(fn) {
|
|
128
|
+
if (fn.type === utils_1.AST_NODE_TYPES.FunctionDeclaration) {
|
|
129
|
+
return fn.id?.name ?? null;
|
|
130
|
+
}
|
|
131
|
+
if (fn.type === utils_1.AST_NODE_TYPES.FunctionExpression && fn.id?.name) {
|
|
132
|
+
return fn.id.name;
|
|
133
|
+
}
|
|
134
|
+
const parent = fn.parent;
|
|
135
|
+
if (!parent) {
|
|
136
|
+
return null;
|
|
137
|
+
}
|
|
138
|
+
const immediateName = getNameFromNode(parent);
|
|
139
|
+
if (immediateName) {
|
|
140
|
+
return immediateName;
|
|
141
|
+
}
|
|
142
|
+
return findNameInAncestors(parent);
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Checks whether a function should be treated as a React component or hook
|
|
146
|
+
* based on naming conventions. Enables the rule to limit reports to user-facing
|
|
147
|
+
* components and hooks rather than arbitrary functions.
|
|
148
|
+
*/
|
|
149
|
+
function isComponentOrHookFunction(fn) {
|
|
150
|
+
const name = getFunctionName(fn);
|
|
151
|
+
return isComponentName(name) || isHookName(name);
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Checks whether a function node represents a hook by name.
|
|
155
|
+
* @param fn Function node to inspect.
|
|
156
|
+
* @returns True when the function follows hook naming.
|
|
157
|
+
*/
|
|
158
|
+
function isHookFunction(fn) {
|
|
159
|
+
return isHookName(getFunctionName(fn));
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Narrows nodes to the supported function-like shapes.
|
|
163
|
+
* @param node Node to test.
|
|
164
|
+
* @returns True when the node is any function declaration or expression.
|
|
165
|
+
*/
|
|
166
|
+
function isFunctionNode(node) {
|
|
167
|
+
return (node.type === utils_1.AST_NODE_TYPES.FunctionDeclaration ||
|
|
168
|
+
node.type === utils_1.AST_NODE_TYPES.FunctionExpression ||
|
|
169
|
+
node.type === utils_1.AST_NODE_TYPES.ArrowFunctionExpression);
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Extracts a hook name from simple identifier or member-expression callees.
|
|
173
|
+
* @param callee Callee expression from a call.
|
|
174
|
+
* @returns Hook name when available, otherwise null.
|
|
175
|
+
*/
|
|
176
|
+
function getHookNameFromCallee(callee) {
|
|
177
|
+
const resolvedCallee = unwrapNestedExpressions(callee);
|
|
178
|
+
if (resolvedCallee.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
179
|
+
return resolvedCallee.name;
|
|
180
|
+
}
|
|
181
|
+
if (resolvedCallee.type === utils_1.AST_NODE_TYPES.MemberExpression &&
|
|
182
|
+
!resolvedCallee.computed &&
|
|
183
|
+
resolvedCallee.property.type ===
|
|
184
|
+
utils_1.AST_NODE_TYPES.Identifier) {
|
|
185
|
+
const property = resolvedCallee.property;
|
|
186
|
+
return property.name;
|
|
187
|
+
}
|
|
188
|
+
return null;
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* Detects hook call expressions and returns their hook name.
|
|
192
|
+
* @param node Call expression to inspect.
|
|
193
|
+
* @returns Hook name when the call targets a hook, otherwise null.
|
|
194
|
+
*/
|
|
195
|
+
function isHookCall(node) {
|
|
196
|
+
const hookName = getHookNameFromCallee(node.callee);
|
|
197
|
+
return hookName && isHookName(hookName) ? hookName : null;
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* Maps literal node types to memoization metadata used for reporting.
|
|
201
|
+
* @param node Candidate literal node.
|
|
202
|
+
* @returns Descriptor with literal type and memo hook, or null.
|
|
203
|
+
*/
|
|
204
|
+
function getLiteralDescriptor(node) {
|
|
205
|
+
const descriptor = LITERAL_DESCRIPTOR_BY_TYPE[node.type] ?? null;
|
|
206
|
+
return descriptor;
|
|
207
|
+
}
|
|
208
|
+
/**
|
|
209
|
+
* Finds the nearest ancestor function considered a React component or hook.
|
|
210
|
+
* @param node Starting node for the search.
|
|
211
|
+
* @returns Owning component/hook function or null.
|
|
212
|
+
*/
|
|
213
|
+
function findEnclosingComponentOrHook(node) {
|
|
214
|
+
let current = node.parent;
|
|
215
|
+
while (current) {
|
|
216
|
+
if (isFunctionNode(current) && isComponentOrHookFunction(current)) {
|
|
217
|
+
return current;
|
|
218
|
+
}
|
|
219
|
+
current = current.parent;
|
|
220
|
+
}
|
|
221
|
+
return null;
|
|
222
|
+
}
|
|
223
|
+
/**
|
|
224
|
+
* Checks whether a node is inside an allowed hook callback that should be skipped.
|
|
225
|
+
* @param node Starting node for traversal.
|
|
226
|
+
* @returns True when enclosed by a callback to an allowed hook.
|
|
227
|
+
*/
|
|
228
|
+
function isInsideAllowedHookCallback(node) {
|
|
229
|
+
let current = node;
|
|
230
|
+
while (current) {
|
|
231
|
+
if (current.type === utils_1.AST_NODE_TYPES.FunctionExpression ||
|
|
232
|
+
current.type === utils_1.AST_NODE_TYPES.ArrowFunctionExpression) {
|
|
233
|
+
let parent = current.parent;
|
|
234
|
+
while (parent && isExpressionWrapper(parent)) {
|
|
235
|
+
parent = parent.parent;
|
|
236
|
+
}
|
|
237
|
+
if (parent?.type === utils_1.AST_NODE_TYPES.CallExpression) {
|
|
238
|
+
const hookName = getHookNameFromCallee(parent.callee);
|
|
239
|
+
const matchesCallback = parent.arguments.some((arg) => unwrapNestedExpressions(arg) === current);
|
|
240
|
+
if (hookName && SAFE_HOOK_ARGUMENTS.has(hookName) && matchesCallback) {
|
|
241
|
+
return true;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
current = current.parent;
|
|
246
|
+
}
|
|
247
|
+
return false;
|
|
248
|
+
}
|
|
249
|
+
/**
|
|
250
|
+
* Type guard for nodes that wrap an underlying expression (e.g. assertions,
|
|
251
|
+
* parentheses, optional chaining wrappers).
|
|
252
|
+
*/
|
|
253
|
+
function isExpressionWrapper(node) {
|
|
254
|
+
return (node.type === utils_1.AST_NODE_TYPES.TSAsExpression ||
|
|
255
|
+
node.type === utils_1.AST_NODE_TYPES.TSTypeAssertion ||
|
|
256
|
+
node.type === utils_1.AST_NODE_TYPES.TSSatisfiesExpression ||
|
|
257
|
+
node.type === utils_1.AST_NODE_TYPES.TSNonNullExpression ||
|
|
258
|
+
node.type === utils_1.AST_NODE_TYPES.ChainExpression ||
|
|
259
|
+
isParenthesizedExpression(node));
|
|
260
|
+
}
|
|
261
|
+
/**
|
|
262
|
+
* Unwraps TypeScript assertions, optional chaining, and parentheses to reach
|
|
263
|
+
* the underlying expression node for stable identity comparisons.
|
|
264
|
+
*/
|
|
265
|
+
function unwrapNestedExpressions(node) {
|
|
266
|
+
let current = node;
|
|
267
|
+
while (isExpressionWrapper(current)) {
|
|
268
|
+
current = current.expression;
|
|
269
|
+
}
|
|
270
|
+
return current;
|
|
271
|
+
}
|
|
272
|
+
/**
|
|
273
|
+
* Finds the nearest enclosing hook call for a literal and whether the literal
|
|
274
|
+
* is passed directly as an argument (versus nested inside another expression).
|
|
275
|
+
*/
|
|
276
|
+
function findEnclosingHookCall(node) {
|
|
277
|
+
const unwrappedTarget = unwrapNestedExpressions(node);
|
|
278
|
+
let current = node.parent;
|
|
279
|
+
while (current) {
|
|
280
|
+
if (current.type === utils_1.AST_NODE_TYPES.CallExpression) {
|
|
281
|
+
const hookName = isHookCall(current);
|
|
282
|
+
if (hookName) {
|
|
283
|
+
const isDirectArgument = current.arguments.some((arg) => {
|
|
284
|
+
const unwrappedArg = unwrapNestedExpressions(arg);
|
|
285
|
+
return unwrappedArg === unwrappedTarget;
|
|
286
|
+
});
|
|
287
|
+
return { hookName, isDirectArgument };
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
current = current.parent;
|
|
291
|
+
}
|
|
292
|
+
return null;
|
|
293
|
+
}
|
|
294
|
+
/**
|
|
295
|
+
* Finds the nearest owning function for the provided node.
|
|
296
|
+
* @param node Node to start from.
|
|
297
|
+
* @returns Enclosing function declaration/expression or null.
|
|
298
|
+
*/
|
|
299
|
+
function findOwningFunction(node) {
|
|
300
|
+
let current = node.parent;
|
|
301
|
+
while (current) {
|
|
302
|
+
if (isFunctionNode(current)) {
|
|
303
|
+
return current;
|
|
304
|
+
}
|
|
305
|
+
current = current.parent;
|
|
306
|
+
}
|
|
307
|
+
return null;
|
|
308
|
+
}
|
|
309
|
+
/**
|
|
310
|
+
* Locates the closest return statement ancestor, skipping assertion wrappers.
|
|
311
|
+
* @param node Node to start from.
|
|
312
|
+
* @returns Enclosing return statement or null.
|
|
313
|
+
*/
|
|
314
|
+
function findEnclosingReturnStatement(node) {
|
|
315
|
+
let current = node.parent;
|
|
316
|
+
while (current) {
|
|
317
|
+
if (current.type === utils_1.AST_NODE_TYPES.ReturnStatement) {
|
|
318
|
+
return current;
|
|
319
|
+
}
|
|
320
|
+
if (isExpressionWrapper(current)) {
|
|
321
|
+
current = current.parent;
|
|
322
|
+
continue;
|
|
323
|
+
}
|
|
324
|
+
break;
|
|
325
|
+
}
|
|
326
|
+
return null;
|
|
327
|
+
}
|
|
328
|
+
/**
|
|
329
|
+
* Determines whether a node is returned from the provided hook function.
|
|
330
|
+
* @param node Expression candidate for return.
|
|
331
|
+
* @param owner Hook function candidate.
|
|
332
|
+
* @returns True when the expression is the hook's return value.
|
|
333
|
+
*/
|
|
334
|
+
function isReturnValueFromHook(node, owner) {
|
|
335
|
+
if (!isHookFunction(owner))
|
|
336
|
+
return false;
|
|
337
|
+
if (owner.type === utils_1.AST_NODE_TYPES.ArrowFunctionExpression &&
|
|
338
|
+
owner.body.type !== utils_1.AST_NODE_TYPES.BlockStatement &&
|
|
339
|
+
unwrapNestedExpressions(owner.body) === unwrapNestedExpressions(node)) {
|
|
340
|
+
return true;
|
|
341
|
+
}
|
|
342
|
+
if (node.parent?.type !== utils_1.AST_NODE_TYPES.ReturnStatement ||
|
|
343
|
+
node.parent.argument !== node) {
|
|
344
|
+
const returnStatement = findEnclosingReturnStatement(node);
|
|
345
|
+
if (!returnStatement?.argument) {
|
|
346
|
+
return false;
|
|
347
|
+
}
|
|
348
|
+
if (unwrapNestedExpressions(returnStatement.argument) !==
|
|
349
|
+
unwrapNestedExpressions(node)) {
|
|
350
|
+
return false;
|
|
351
|
+
}
|
|
352
|
+
const owningFunction = findOwningFunction(returnStatement);
|
|
353
|
+
return owningFunction === owner;
|
|
354
|
+
}
|
|
355
|
+
const owningFunction = findOwningFunction(node.parent);
|
|
356
|
+
return owningFunction === owner;
|
|
357
|
+
}
|
|
358
|
+
/**
|
|
359
|
+
* Builds memoization suggestions with dependency placeholders for developers.
|
|
360
|
+
* @param node Literal node to wrap.
|
|
361
|
+
* @param descriptor Literal metadata including memo hook.
|
|
362
|
+
* @param sourceCode Source code utility for text extraction.
|
|
363
|
+
* @returns Suggestion array encouraging memoization with explicit deps TODO.
|
|
364
|
+
*/
|
|
365
|
+
function buildMemoSuggestions(node, descriptor, sourceCode) {
|
|
366
|
+
const initializerText = sourceCode.getText(node);
|
|
367
|
+
const wrappedInitializer = descriptor.literalType === 'object literal'
|
|
368
|
+
? `(${initializerText})`
|
|
369
|
+
: initializerText;
|
|
370
|
+
if (descriptor.literalType === 'inline function') {
|
|
371
|
+
return [
|
|
372
|
+
{
|
|
373
|
+
messageId: 'memoizeLiteralSuggestion',
|
|
374
|
+
data: {
|
|
375
|
+
literalType: descriptor.literalType,
|
|
376
|
+
memoHook: descriptor.memoHook,
|
|
377
|
+
},
|
|
378
|
+
fix(fixer) {
|
|
379
|
+
return fixer.replaceText(node, `${descriptor.memoHook}(${initializerText}, [${TODO_DEPS_COMMENT}])`);
|
|
380
|
+
},
|
|
381
|
+
},
|
|
382
|
+
];
|
|
383
|
+
}
|
|
384
|
+
return [
|
|
385
|
+
{
|
|
386
|
+
messageId: 'memoizeLiteralSuggestion',
|
|
387
|
+
data: {
|
|
388
|
+
literalType: descriptor.literalType,
|
|
389
|
+
memoHook: descriptor.memoHook,
|
|
390
|
+
},
|
|
391
|
+
fix(fixer) {
|
|
392
|
+
return fixer.replaceText(node, `${descriptor.memoHook}(() => ${wrappedInitializer}, [${TODO_DEPS_COMMENT}])`);
|
|
393
|
+
},
|
|
394
|
+
},
|
|
395
|
+
];
|
|
396
|
+
}
|
|
397
|
+
/**
|
|
398
|
+
* Formats a readable label for diagnostics based on the owning function.
|
|
399
|
+
* @param fn Owning component or hook function.
|
|
400
|
+
* @returns Human-friendly label for error messages.
|
|
401
|
+
*/
|
|
402
|
+
function formatContextLabel(fn) {
|
|
403
|
+
const name = getFunctionName(fn);
|
|
404
|
+
if (!name)
|
|
405
|
+
return 'this component or hook';
|
|
406
|
+
if (isHookName(name))
|
|
407
|
+
return `hook "${name}"`;
|
|
408
|
+
return `component "${name}"`;
|
|
409
|
+
}
|
|
410
|
+
exports.reactMemoizeLiterals = (0, createRule_1.createRule)({
|
|
411
|
+
name: 'react-memoize-literals',
|
|
412
|
+
meta: {
|
|
413
|
+
type: 'suggestion',
|
|
414
|
+
docs: {
|
|
415
|
+
description: 'Detect object, array, and function literals created in React components or hooks that create new references every render. Prefer memoized values (useMemo/useCallback) or module-level constants to keep referential stability.',
|
|
416
|
+
recommended: 'error',
|
|
417
|
+
},
|
|
418
|
+
hasSuggestions: true,
|
|
419
|
+
schema: [],
|
|
420
|
+
messages: {
|
|
421
|
+
componentLiteral: 'New {{literalType}} inside {{context}} is created on every render → Breaks referential stability for hooks/props and can re-run effects or re-render children → Hoist it to a module-level constant or wrap it in {{memoHook}} with the right dependencies.',
|
|
422
|
+
nestedHookLiteral: 'Nested {{literalType}} inside {{hookName}} arguments is recreated on every render → Dependency/reference comparisons change each time and defeat memoization/caching → Extract it into a memoized value (useMemo/useCallback) or hoist it to a module constant before passing it to {{hookName}}.',
|
|
423
|
+
hookReturnLiteral: '{{hookName}} returns a {{literalType}} literal on each render → Callers receive a fresh reference and may re-render or re-run effects → Memoize the returned value with useMemo/useCallback or return pre-memoized pieces so callers see stable references.',
|
|
424
|
+
memoizeLiteralSuggestion: 'This {{literalType}} is created inline → It produces a new reference each render → Wrap it in {{memoHook}} and include every closed-over value in the dependency array.',
|
|
425
|
+
},
|
|
426
|
+
},
|
|
427
|
+
defaultOptions: [],
|
|
428
|
+
create(context) {
|
|
429
|
+
const sourceCode = context.getSourceCode();
|
|
430
|
+
function reportLiteral(node) {
|
|
431
|
+
const descriptor = getLiteralDescriptor(node);
|
|
432
|
+
if (!descriptor)
|
|
433
|
+
return;
|
|
434
|
+
if (isInsideAllowedHookCallback(node)) {
|
|
435
|
+
return;
|
|
436
|
+
}
|
|
437
|
+
const owner = findEnclosingComponentOrHook(node);
|
|
438
|
+
if (!owner)
|
|
439
|
+
return;
|
|
440
|
+
const hookCall = findEnclosingHookCall(node);
|
|
441
|
+
if (hookCall) {
|
|
442
|
+
if (hookCall.isDirectArgument) {
|
|
443
|
+
// Top-level literal passed directly to a hook argument is allowed.
|
|
444
|
+
return;
|
|
445
|
+
}
|
|
446
|
+
context.report({
|
|
447
|
+
node,
|
|
448
|
+
messageId: 'nestedHookLiteral',
|
|
449
|
+
data: {
|
|
450
|
+
literalType: descriptor.literalType,
|
|
451
|
+
hookName: hookCall.hookName,
|
|
452
|
+
},
|
|
453
|
+
});
|
|
454
|
+
return;
|
|
455
|
+
}
|
|
456
|
+
if (isReturnValueFromHook(node, owner)) {
|
|
457
|
+
const hookName = getFunctionName(owner) ?? 'this hook';
|
|
458
|
+
context.report({
|
|
459
|
+
node,
|
|
460
|
+
messageId: 'hookReturnLiteral',
|
|
461
|
+
data: {
|
|
462
|
+
literalType: descriptor.literalType,
|
|
463
|
+
hookName,
|
|
464
|
+
},
|
|
465
|
+
});
|
|
466
|
+
return;
|
|
467
|
+
}
|
|
468
|
+
const contextLabel = formatContextLabel(owner);
|
|
469
|
+
// Only emit auto-fix suggestions for simple variable initializers; other
|
|
470
|
+
// contexts (returns, JSX props, nested expressions) risk unsafe rewrites.
|
|
471
|
+
const suggestions = node.parent?.type === utils_1.AST_NODE_TYPES.VariableDeclarator &&
|
|
472
|
+
node.parent.init === node
|
|
473
|
+
? buildMemoSuggestions(node, descriptor, sourceCode)
|
|
474
|
+
: undefined;
|
|
475
|
+
context.report({
|
|
476
|
+
node,
|
|
477
|
+
messageId: 'componentLiteral',
|
|
478
|
+
data: {
|
|
479
|
+
literalType: descriptor.literalType,
|
|
480
|
+
context: contextLabel,
|
|
481
|
+
memoHook: descriptor.memoHook,
|
|
482
|
+
},
|
|
483
|
+
suggest: suggestions,
|
|
484
|
+
});
|
|
485
|
+
}
|
|
486
|
+
return {
|
|
487
|
+
ObjectExpression: reportLiteral,
|
|
488
|
+
ArrayExpression: reportLiteral,
|
|
489
|
+
ArrowFunctionExpression: reportLiteral,
|
|
490
|
+
FunctionExpression: reportLiteral,
|
|
491
|
+
FunctionDeclaration: reportLiteral,
|
|
492
|
+
};
|
|
493
|
+
},
|
|
494
|
+
});
|
|
495
|
+
//# sourceMappingURL=react-memoize-literals.js.map
|
|
@@ -1 +1,2 @@
|
|
|
1
|
-
|
|
1
|
+
import { TSESLint } from '@typescript-eslint/utils';
|
|
2
|
+
export declare const reactUseMemoShouldBeComponent: TSESLint.RuleModule<"useMemoShouldBeComponent", [], TSESLint.RuleListener>;
|