@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
|
@@ -24,7 +24,7 @@ module.exports = (0, createRule_1.createRule)({
|
|
|
24
24
|
},
|
|
25
25
|
],
|
|
26
26
|
messages: {
|
|
27
|
-
useImageOptimized: 'Use ImageOptimized
|
|
27
|
+
useImageOptimized: 'Use ImageOptimized from {{ componentPath }} instead of {{ component }}. The shared wrapper handles responsive sizing, lazy loading, and blur placeholders so images stay optimized and do not hurt Core Web Vitals. Replace this usage with ImageOptimized to send the asset through the optimization pipeline.',
|
|
28
28
|
},
|
|
29
29
|
},
|
|
30
30
|
defaultOptions: [{ componentPath: 'src/components/image/ImageOptimized' }],
|
|
@@ -32,7 +32,7 @@ module.exports = (0, createRule_1.createRule)({
|
|
|
32
32
|
const options = context.options[0] || {
|
|
33
33
|
componentPath: 'src/components/image/ImageOptimized',
|
|
34
34
|
};
|
|
35
|
-
const sourceCode = context.
|
|
35
|
+
const sourceCode = context.sourceCode;
|
|
36
36
|
return {
|
|
37
37
|
// Handle JSX img elements
|
|
38
38
|
JSXElement(node) {
|
|
@@ -66,12 +66,20 @@ function checkFunction(context, node) {
|
|
|
66
66
|
isUnmemoizedExportedFunctionComponent,
|
|
67
67
|
].map((fn) => fn(parentNode, node));
|
|
68
68
|
if (results.some((result) => !!result)) {
|
|
69
|
+
const componentName = (node.type === 'FunctionDeclaration' && node.id?.name) ||
|
|
70
|
+
(parentNode.type === 'VariableDeclarator' &&
|
|
71
|
+
parentNode.id.type === 'Identifier' &&
|
|
72
|
+
parentNode.id.name) ||
|
|
73
|
+
'component';
|
|
69
74
|
context.report({
|
|
70
75
|
node,
|
|
71
76
|
messageId: 'requireMemo',
|
|
77
|
+
data: {
|
|
78
|
+
name: componentName,
|
|
79
|
+
},
|
|
72
80
|
fix: results[2] || results[1]
|
|
73
81
|
? function fix(fixer) {
|
|
74
|
-
const sourceCode = context.
|
|
82
|
+
const sourceCode = context.sourceCode;
|
|
75
83
|
let importFix = null;
|
|
76
84
|
// Search for memo import statement
|
|
77
85
|
const importDeclarations = sourceCode.ast.body.filter((node) => node.type === 'ImportDeclaration');
|
|
@@ -158,7 +166,9 @@ exports.requireMemo = {
|
|
|
158
166
|
recommended: 'error',
|
|
159
167
|
},
|
|
160
168
|
messages: {
|
|
161
|
-
requireMemo: 'Component
|
|
169
|
+
requireMemo: 'Component "{{name}}" renders JSX with props but is not wrapped in memo(). ' +
|
|
170
|
+
'Without memo the component function is recreated on every parent render, breaking referential equality and causing avoidable child re-renders. ' +
|
|
171
|
+
'Wrap the component with memo from util/memo so callers receive a stable reference; rename to "{{name}}Unmemoized" if it must stay un-memoized.',
|
|
162
172
|
},
|
|
163
173
|
schema: [],
|
|
164
174
|
fixable: 'code',
|
|
@@ -0,0 +1,485 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.requireMemoizeJsxReturners = void 0;
|
|
4
|
+
const utils_1 = require("@typescript-eslint/utils");
|
|
5
|
+
const createRule_1 = require("../utils/createRule");
|
|
6
|
+
const MEMOIZE_PREFERRED_MODULE = '@blumintinc/typescript-memoize';
|
|
7
|
+
const MEMOIZE_MODULES = new Set([
|
|
8
|
+
MEMOIZE_PREFERRED_MODULE,
|
|
9
|
+
'typescript-memoize',
|
|
10
|
+
]);
|
|
11
|
+
function isMemoizeDecorator(decorator, alias, namespaceAlias) {
|
|
12
|
+
const expression = decorator.expression;
|
|
13
|
+
const matchesAliasIdentifier = (node) => !!node && node.type === utils_1.AST_NODE_TYPES.Identifier && node.name === alias;
|
|
14
|
+
const matchesNamespaceMember = (node) => {
|
|
15
|
+
if (node.computed)
|
|
16
|
+
return false;
|
|
17
|
+
if (node.property.type !== utils_1.AST_NODE_TYPES.Identifier)
|
|
18
|
+
return false;
|
|
19
|
+
if (node.property.name !== 'Memoize')
|
|
20
|
+
return false;
|
|
21
|
+
return (!!namespaceAlias &&
|
|
22
|
+
node.object.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
23
|
+
node.object.name === namespaceAlias);
|
|
24
|
+
};
|
|
25
|
+
if (expression.type === utils_1.AST_NODE_TYPES.CallExpression) {
|
|
26
|
+
const { callee } = expression;
|
|
27
|
+
if (callee.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
28
|
+
matchesAliasIdentifier(callee)) {
|
|
29
|
+
return true;
|
|
30
|
+
}
|
|
31
|
+
if (callee.type === utils_1.AST_NODE_TYPES.MemberExpression &&
|
|
32
|
+
matchesNamespaceMember(callee)) {
|
|
33
|
+
return true;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
if (matchesAliasIdentifier(expression)) {
|
|
37
|
+
return true;
|
|
38
|
+
}
|
|
39
|
+
if (expression.type === utils_1.AST_NODE_TYPES.MemberExpression &&
|
|
40
|
+
matchesNamespaceMember(expression)) {
|
|
41
|
+
return true;
|
|
42
|
+
}
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
function getMemberName(node) {
|
|
46
|
+
const key = node.key;
|
|
47
|
+
if (key.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
48
|
+
return key.name;
|
|
49
|
+
}
|
|
50
|
+
if (key.type === utils_1.AST_NODE_TYPES.Literal && typeof key.value === 'string') {
|
|
51
|
+
return key.value;
|
|
52
|
+
}
|
|
53
|
+
if (key.type === utils_1.AST_NODE_TYPES.PrivateIdentifier) {
|
|
54
|
+
return key.name;
|
|
55
|
+
}
|
|
56
|
+
return 'member';
|
|
57
|
+
}
|
|
58
|
+
function extractFunctionsFromVariableDeclaration(declaration, functions) {
|
|
59
|
+
for (const declarator of declaration.declarations) {
|
|
60
|
+
if (declarator.id.type !== utils_1.AST_NODE_TYPES.Identifier) {
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
const init = declarator.init;
|
|
64
|
+
if (!init ||
|
|
65
|
+
(init.type !== utils_1.AST_NODE_TYPES.ArrowFunctionExpression &&
|
|
66
|
+
init.type !== utils_1.AST_NODE_TYPES.FunctionExpression)) {
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
functions.set(declarator.id.name, init);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
function traverseStatements(statements, functions) {
|
|
73
|
+
statements.forEach((statement) => visitStatement(statement, functions));
|
|
74
|
+
}
|
|
75
|
+
function visitStatementByType(statement, functions) {
|
|
76
|
+
switch (statement.type) {
|
|
77
|
+
case utils_1.AST_NODE_TYPES.BlockStatement:
|
|
78
|
+
traverseStatements(statement.body, functions);
|
|
79
|
+
break;
|
|
80
|
+
case utils_1.AST_NODE_TYPES.IfStatement:
|
|
81
|
+
visitStatement(statement.consequent, functions);
|
|
82
|
+
if (statement.alternate) {
|
|
83
|
+
visitStatement(statement.alternate, functions);
|
|
84
|
+
}
|
|
85
|
+
break;
|
|
86
|
+
case utils_1.AST_NODE_TYPES.SwitchStatement:
|
|
87
|
+
statement.cases.forEach((caseNode) => traverseStatements(caseNode.consequent, functions));
|
|
88
|
+
break;
|
|
89
|
+
case utils_1.AST_NODE_TYPES.TryStatement:
|
|
90
|
+
visitStatement(statement.block, functions);
|
|
91
|
+
if (statement.handler) {
|
|
92
|
+
visitStatement(statement.handler.body, functions);
|
|
93
|
+
}
|
|
94
|
+
if (statement.finalizer) {
|
|
95
|
+
visitStatement(statement.finalizer, functions);
|
|
96
|
+
}
|
|
97
|
+
break;
|
|
98
|
+
case utils_1.AST_NODE_TYPES.ForStatement:
|
|
99
|
+
if (statement.init &&
|
|
100
|
+
statement.init.type === utils_1.AST_NODE_TYPES.VariableDeclaration) {
|
|
101
|
+
extractFunctionsFromVariableDeclaration(statement.init, functions);
|
|
102
|
+
}
|
|
103
|
+
visitStatement(statement.body, functions);
|
|
104
|
+
break;
|
|
105
|
+
case utils_1.AST_NODE_TYPES.ForInStatement:
|
|
106
|
+
case utils_1.AST_NODE_TYPES.ForOfStatement:
|
|
107
|
+
if (statement.left.type === utils_1.AST_NODE_TYPES.VariableDeclaration) {
|
|
108
|
+
extractFunctionsFromVariableDeclaration(statement.left, functions);
|
|
109
|
+
}
|
|
110
|
+
visitStatement(statement.body, functions);
|
|
111
|
+
break;
|
|
112
|
+
case utils_1.AST_NODE_TYPES.WhileStatement:
|
|
113
|
+
case utils_1.AST_NODE_TYPES.DoWhileStatement:
|
|
114
|
+
case utils_1.AST_NODE_TYPES.LabeledStatement:
|
|
115
|
+
case utils_1.AST_NODE_TYPES.WithStatement:
|
|
116
|
+
visitStatement(statement.body, functions);
|
|
117
|
+
break;
|
|
118
|
+
default:
|
|
119
|
+
break;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
function visitStatement(statement, functions) {
|
|
123
|
+
if (statement.type === utils_1.AST_NODE_TYPES.FunctionDeclaration &&
|
|
124
|
+
statement.id?.name) {
|
|
125
|
+
functions.set(statement.id.name, statement);
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
if (statement.type === utils_1.AST_NODE_TYPES.VariableDeclaration) {
|
|
129
|
+
extractFunctionsFromVariableDeclaration(statement, functions);
|
|
130
|
+
}
|
|
131
|
+
visitStatementByType(statement, functions);
|
|
132
|
+
}
|
|
133
|
+
function collectLocalFunctions(body) {
|
|
134
|
+
const functions = new Map();
|
|
135
|
+
if (!body || body.type !== utils_1.AST_NODE_TYPES.BlockStatement) {
|
|
136
|
+
return functions;
|
|
137
|
+
}
|
|
138
|
+
traverseStatements(body.body, functions);
|
|
139
|
+
return functions;
|
|
140
|
+
}
|
|
141
|
+
/** Type guard for ParenthesizedExpression (non-standard node type in some parsers). */
|
|
142
|
+
function isParenthesizedExpression(node) {
|
|
143
|
+
return (!!node &&
|
|
144
|
+
node.type === 'ParenthesizedExpression' &&
|
|
145
|
+
'expression' in node &&
|
|
146
|
+
node.expression != null);
|
|
147
|
+
}
|
|
148
|
+
function isIdentifierReturningJsx(node, knownFunctions, cache, factoryContext) {
|
|
149
|
+
const targetFn = knownFunctions.get(node.name);
|
|
150
|
+
return (!!targetFn &&
|
|
151
|
+
functionReturnsJSX(targetFn, knownFunctions, cache, factoryContext));
|
|
152
|
+
}
|
|
153
|
+
function isWrappedReturningJsx(node, knownFunctions, cache, factoryContext) {
|
|
154
|
+
return expressionReturnsJSX(node.expression, knownFunctions, cache, factoryContext);
|
|
155
|
+
}
|
|
156
|
+
function isSequenceReturningJsx(node, knownFunctions, cache, factoryContext) {
|
|
157
|
+
return (node.expressions.length > 0 &&
|
|
158
|
+
expressionReturnsJSX(node.expressions[node.expressions.length - 1], knownFunctions, cache, factoryContext));
|
|
159
|
+
}
|
|
160
|
+
function isLogicalReturningJsx(node, knownFunctions, cache, factoryContext) {
|
|
161
|
+
return (expressionReturnsJSX(node.left, knownFunctions, cache, factoryContext) ||
|
|
162
|
+
expressionReturnsJSX(node.right, knownFunctions, cache, factoryContext));
|
|
163
|
+
}
|
|
164
|
+
function isConditionalReturningJsx(node, knownFunctions, cache, factoryContext) {
|
|
165
|
+
return (expressionReturnsJSX(node.consequent, knownFunctions, cache, factoryContext) ||
|
|
166
|
+
expressionReturnsJSX(node.alternate, knownFunctions, cache, factoryContext));
|
|
167
|
+
}
|
|
168
|
+
function callExpressionReturnsJSX(expression, knownFunctions, cache, factoryContext) {
|
|
169
|
+
const { callee } = expression;
|
|
170
|
+
const firstNonSpreadArgument = expression.arguments.find((arg) => arg.type !== utils_1.AST_NODE_TYPES.SpreadElement);
|
|
171
|
+
if (callee.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
172
|
+
if (factoryContext.reactCreateElementIdentifiers.has(callee.name)) {
|
|
173
|
+
return true;
|
|
174
|
+
}
|
|
175
|
+
if (isIdentifierReturningJsx(callee, knownFunctions, cache, factoryContext)) {
|
|
176
|
+
return true;
|
|
177
|
+
}
|
|
178
|
+
if (factoryContext.reactMemoIdentifiers.has(callee.name) &&
|
|
179
|
+
firstNonSpreadArgument &&
|
|
180
|
+
expressionReturnsJSX(firstNonSpreadArgument, knownFunctions, cache, factoryContext)) {
|
|
181
|
+
return true;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
if (callee.type === utils_1.AST_NODE_TYPES.MemberExpression &&
|
|
185
|
+
!callee.computed &&
|
|
186
|
+
callee.property.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
187
|
+
const propertyName = callee.property.name;
|
|
188
|
+
/** Treat React.createElement style calls as JSX-producing. */
|
|
189
|
+
if (propertyName === 'createElement' &&
|
|
190
|
+
callee.object.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
191
|
+
factoryContext.reactNamespaceIdentifiers.has(callee.object.name)) {
|
|
192
|
+
return true;
|
|
193
|
+
}
|
|
194
|
+
if (propertyName === 'memo' &&
|
|
195
|
+
callee.object.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
196
|
+
factoryContext.reactNamespaceIdentifiers.has(callee.object.name) &&
|
|
197
|
+
firstNonSpreadArgument &&
|
|
198
|
+
expressionReturnsJSX(firstNonSpreadArgument, knownFunctions, cache, factoryContext)) {
|
|
199
|
+
return true;
|
|
200
|
+
}
|
|
201
|
+
if ((propertyName === 'call' || propertyName === 'apply') &&
|
|
202
|
+
callee.object.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
203
|
+
if (isIdentifierReturningJsx(callee.object, knownFunctions, cache, factoryContext)) {
|
|
204
|
+
return true;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
return false;
|
|
209
|
+
}
|
|
210
|
+
function isFunctionReturningJSX(node, knownFunctions, cache, factoryContext) {
|
|
211
|
+
return functionReturnsJSX(node, knownFunctions, cache, factoryContext);
|
|
212
|
+
}
|
|
213
|
+
function dispatchExpressionReturnsJSX(expression, knownFunctions, cache, factoryContext) {
|
|
214
|
+
const type = expression.type;
|
|
215
|
+
switch (type) {
|
|
216
|
+
case utils_1.AST_NODE_TYPES.JSXElement:
|
|
217
|
+
case utils_1.AST_NODE_TYPES.JSXFragment:
|
|
218
|
+
return true;
|
|
219
|
+
case utils_1.AST_NODE_TYPES.ArrowFunctionExpression:
|
|
220
|
+
case utils_1.AST_NODE_TYPES.FunctionExpression:
|
|
221
|
+
return isFunctionReturningJSX(expression, knownFunctions, cache, factoryContext);
|
|
222
|
+
case utils_1.AST_NODE_TYPES.Identifier:
|
|
223
|
+
return isIdentifierReturningJsx(expression, knownFunctions, cache, factoryContext);
|
|
224
|
+
case utils_1.AST_NODE_TYPES.CallExpression:
|
|
225
|
+
return callExpressionReturnsJSX(expression, knownFunctions, cache, factoryContext);
|
|
226
|
+
case utils_1.AST_NODE_TYPES.ConditionalExpression:
|
|
227
|
+
return isConditionalReturningJsx(expression, knownFunctions, cache, factoryContext);
|
|
228
|
+
case utils_1.AST_NODE_TYPES.LogicalExpression:
|
|
229
|
+
return isLogicalReturningJsx(expression, knownFunctions, cache, factoryContext);
|
|
230
|
+
case utils_1.AST_NODE_TYPES.SequenceExpression:
|
|
231
|
+
return isSequenceReturningJsx(expression, knownFunctions, cache, factoryContext);
|
|
232
|
+
case utils_1.AST_NODE_TYPES.TSAsExpression:
|
|
233
|
+
case utils_1.AST_NODE_TYPES.TSTypeAssertion:
|
|
234
|
+
case utils_1.AST_NODE_TYPES.TSNonNullExpression:
|
|
235
|
+
case utils_1.AST_NODE_TYPES.TSSatisfiesExpression:
|
|
236
|
+
case utils_1.AST_NODE_TYPES.ChainExpression:
|
|
237
|
+
return isWrappedReturningJsx(expression, knownFunctions, cache, factoryContext);
|
|
238
|
+
default:
|
|
239
|
+
return false;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
function expressionReturnsJSX(expression, knownFunctions, cache, factoryContext) {
|
|
243
|
+
if (!expression)
|
|
244
|
+
return false;
|
|
245
|
+
if (isParenthesizedExpression(expression)) {
|
|
246
|
+
return expressionReturnsJSX(expression.expression, knownFunctions, cache, factoryContext);
|
|
247
|
+
}
|
|
248
|
+
return dispatchExpressionReturnsJSX(expression, knownFunctions, cache, factoryContext);
|
|
249
|
+
}
|
|
250
|
+
function statementReturnsJSX(statement, knownFunctions, cache, factoryContext) {
|
|
251
|
+
switch (statement.type) {
|
|
252
|
+
case utils_1.AST_NODE_TYPES.ReturnStatement:
|
|
253
|
+
return expressionReturnsJSX(statement.argument, knownFunctions, cache, factoryContext);
|
|
254
|
+
case utils_1.AST_NODE_TYPES.BlockStatement:
|
|
255
|
+
return statement.body.some((child) => statementReturnsJSX(child, knownFunctions, cache, factoryContext));
|
|
256
|
+
case utils_1.AST_NODE_TYPES.IfStatement:
|
|
257
|
+
return (statementReturnsJSX(statement.consequent, knownFunctions, cache, factoryContext) ||
|
|
258
|
+
(statement.alternate
|
|
259
|
+
? statementReturnsJSX(statement.alternate, knownFunctions, cache, factoryContext)
|
|
260
|
+
: false));
|
|
261
|
+
case utils_1.AST_NODE_TYPES.SwitchStatement:
|
|
262
|
+
return statement.cases.some((caseNode) => caseNode.consequent.some((consequent) => statementReturnsJSX(consequent, knownFunctions, cache, factoryContext)));
|
|
263
|
+
case utils_1.AST_NODE_TYPES.TryStatement:
|
|
264
|
+
if (statementReturnsJSX(statement.block, knownFunctions, cache, factoryContext)) {
|
|
265
|
+
return true;
|
|
266
|
+
}
|
|
267
|
+
if (statement.handler &&
|
|
268
|
+
statementReturnsJSX(statement.handler.body, knownFunctions, cache, factoryContext)) {
|
|
269
|
+
return true;
|
|
270
|
+
}
|
|
271
|
+
if (statement.finalizer &&
|
|
272
|
+
statementReturnsJSX(statement.finalizer, knownFunctions, cache, factoryContext)) {
|
|
273
|
+
return true;
|
|
274
|
+
}
|
|
275
|
+
return false;
|
|
276
|
+
case utils_1.AST_NODE_TYPES.ForStatement:
|
|
277
|
+
case utils_1.AST_NODE_TYPES.ForInStatement:
|
|
278
|
+
case utils_1.AST_NODE_TYPES.ForOfStatement:
|
|
279
|
+
case utils_1.AST_NODE_TYPES.WhileStatement:
|
|
280
|
+
case utils_1.AST_NODE_TYPES.DoWhileStatement:
|
|
281
|
+
case utils_1.AST_NODE_TYPES.LabeledStatement:
|
|
282
|
+
case utils_1.AST_NODE_TYPES.WithStatement:
|
|
283
|
+
return statementReturnsJSX(statement.body, knownFunctions, cache, factoryContext);
|
|
284
|
+
default:
|
|
285
|
+
return false;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
function functionReturnsJSX(fn, knownFunctions, cache, factoryContext) {
|
|
289
|
+
const cached = cache.get(fn);
|
|
290
|
+
if (cached === true || cached === false) {
|
|
291
|
+
return cached;
|
|
292
|
+
}
|
|
293
|
+
if (cached === 'pending') {
|
|
294
|
+
return false;
|
|
295
|
+
}
|
|
296
|
+
cache.set(fn, 'pending');
|
|
297
|
+
if (fn.type !== utils_1.AST_NODE_TYPES.FunctionDeclaration &&
|
|
298
|
+
fn.type !== utils_1.AST_NODE_TYPES.FunctionExpression &&
|
|
299
|
+
fn.type !== utils_1.AST_NODE_TYPES.ArrowFunctionExpression) {
|
|
300
|
+
cache.set(fn, false);
|
|
301
|
+
return false;
|
|
302
|
+
}
|
|
303
|
+
let extendedFunctions = knownFunctions;
|
|
304
|
+
if (fn.body && fn.body.type === utils_1.AST_NODE_TYPES.BlockStatement) {
|
|
305
|
+
const nested = collectLocalFunctions(fn.body);
|
|
306
|
+
if (nested.size > 0) {
|
|
307
|
+
extendedFunctions = new Map(knownFunctions);
|
|
308
|
+
for (const [name, nestedFn] of nested.entries()) {
|
|
309
|
+
extendedFunctions.set(name, nestedFn);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
let returnsJSX = false;
|
|
314
|
+
if (!fn.body) {
|
|
315
|
+
cache.set(fn, false);
|
|
316
|
+
return false;
|
|
317
|
+
}
|
|
318
|
+
if (fn.body.type === utils_1.AST_NODE_TYPES.BlockStatement) {
|
|
319
|
+
returnsJSX = statementReturnsJSX(fn.body, extendedFunctions, cache, factoryContext);
|
|
320
|
+
}
|
|
321
|
+
else if (expressionReturnsJSX(fn.body, extendedFunctions, cache, factoryContext)) {
|
|
322
|
+
returnsJSX = true;
|
|
323
|
+
}
|
|
324
|
+
cache.set(fn, returnsJSX);
|
|
325
|
+
return returnsJSX;
|
|
326
|
+
}
|
|
327
|
+
function getImportFixes(fixer, sourceCode, hasMemoizeImport, scheduledImportFix) {
|
|
328
|
+
const fixes = [];
|
|
329
|
+
if (hasMemoizeImport || scheduledImportFix) {
|
|
330
|
+
return { fixes, scheduledImportFix };
|
|
331
|
+
}
|
|
332
|
+
const programBody = sourceCode.ast.body;
|
|
333
|
+
/** Look for an existing import from the memoize module. */
|
|
334
|
+
const existingMemoizeImport = programBody.find((statement) => statement.type === utils_1.AST_NODE_TYPES.ImportDeclaration &&
|
|
335
|
+
MEMOIZE_MODULES.has(String(statement.source.value)));
|
|
336
|
+
if (existingMemoizeImport &&
|
|
337
|
+
existingMemoizeImport.specifiers.some((s) => s.type === utils_1.AST_NODE_TYPES.ImportSpecifier)) {
|
|
338
|
+
/** Augment existing named import. */
|
|
339
|
+
const lastSpecifier = [...existingMemoizeImport.specifiers]
|
|
340
|
+
.reverse()
|
|
341
|
+
.find((s) => s.type === utils_1.AST_NODE_TYPES.ImportSpecifier);
|
|
342
|
+
if (lastSpecifier) {
|
|
343
|
+
fixes.push(fixer.insertTextAfter(lastSpecifier, ', Memoize'));
|
|
344
|
+
return { fixes, scheduledImportFix: true };
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
const firstImport = programBody.find((statement) => statement.type === utils_1.AST_NODE_TYPES.ImportDeclaration);
|
|
348
|
+
const anchorNode = (firstImport ?? programBody[0]);
|
|
349
|
+
if (anchorNode) {
|
|
350
|
+
const text = sourceCode.text;
|
|
351
|
+
const anchorStart = anchorNode.range?.[0] ?? 0;
|
|
352
|
+
const lineStart = text.lastIndexOf('\n', anchorStart - 1) + 1;
|
|
353
|
+
const leadingWhitespace = text.slice(lineStart, anchorStart).match(/^[ \t]*/)?.[0] ?? '';
|
|
354
|
+
const importLine = `${leadingWhitespace}import { Memoize } from '${MEMOIZE_PREFERRED_MODULE}';\n`;
|
|
355
|
+
fixes.push(fixer.insertTextBeforeRange([lineStart, lineStart], importLine));
|
|
356
|
+
}
|
|
357
|
+
else {
|
|
358
|
+
fixes.push(fixer.insertTextBeforeRange([0, 0], `import { Memoize } from '${MEMOIZE_PREFERRED_MODULE}';\n`));
|
|
359
|
+
}
|
|
360
|
+
return { fixes, scheduledImportFix: true };
|
|
361
|
+
}
|
|
362
|
+
exports.requireMemoizeJsxReturners = (0, createRule_1.createRule)({
|
|
363
|
+
name: 'require-memoize-jsx-returners',
|
|
364
|
+
meta: {
|
|
365
|
+
type: 'suggestion',
|
|
366
|
+
docs: {
|
|
367
|
+
description: 'Require @Memoize() decorator on instance members that return JSX or JSX factories',
|
|
368
|
+
recommended: 'error',
|
|
369
|
+
},
|
|
370
|
+
fixable: 'code',
|
|
371
|
+
schema: [],
|
|
372
|
+
messages: {
|
|
373
|
+
requireMemoizeJsxReturner: '"{{name}}" returns JSX (or a JSX-producing factory) without @Memoize() → Each call/access creates a new component/function reference that can trigger avoidable React re-renders or remounts → Add @Memoize() to "{{name}}" and import { Memoize } from "@blumintinc/typescript-memoize".',
|
|
374
|
+
},
|
|
375
|
+
},
|
|
376
|
+
defaultOptions: [],
|
|
377
|
+
create(context) {
|
|
378
|
+
const filename = context.getFilename();
|
|
379
|
+
const isVirtualFile = filename.startsWith('<');
|
|
380
|
+
if (!isVirtualFile && !/\.tsx?$/i.test(filename)) {
|
|
381
|
+
return {};
|
|
382
|
+
}
|
|
383
|
+
let hasMemoizeImport = false;
|
|
384
|
+
let memoizeAlias = 'Memoize';
|
|
385
|
+
let memoizeNamespace = null;
|
|
386
|
+
let scheduledImportFix = false;
|
|
387
|
+
const jsxReturnCache = new WeakMap();
|
|
388
|
+
const reactMemoIdentifiers = new Set();
|
|
389
|
+
const reactNamespaceIdentifiers = new Set();
|
|
390
|
+
const reactCreateElementIdentifiers = new Set();
|
|
391
|
+
const factoryContext = {
|
|
392
|
+
reactMemoIdentifiers,
|
|
393
|
+
reactNamespaceIdentifiers,
|
|
394
|
+
reactCreateElementIdentifiers,
|
|
395
|
+
};
|
|
396
|
+
return {
|
|
397
|
+
ImportDeclaration(node) {
|
|
398
|
+
const sourceValue = String(node.source.value);
|
|
399
|
+
if (sourceValue === 'react') {
|
|
400
|
+
node.specifiers.forEach((specifier) => {
|
|
401
|
+
if (specifier.type === utils_1.AST_NODE_TYPES.ImportSpecifier &&
|
|
402
|
+
specifier.imported.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
403
|
+
specifier.imported.name === 'memo') {
|
|
404
|
+
reactMemoIdentifiers.add(specifier.local?.name ?? specifier.imported.name);
|
|
405
|
+
}
|
|
406
|
+
else if (specifier.type === utils_1.AST_NODE_TYPES.ImportSpecifier &&
|
|
407
|
+
specifier.imported.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
408
|
+
specifier.imported.name === 'createElement') {
|
|
409
|
+
reactCreateElementIdentifiers.add(specifier.local?.name ?? specifier.imported.name);
|
|
410
|
+
}
|
|
411
|
+
else if (specifier.type === utils_1.AST_NODE_TYPES.ImportDefaultSpecifier) {
|
|
412
|
+
reactNamespaceIdentifiers.add(specifier.local.name);
|
|
413
|
+
}
|
|
414
|
+
else if (specifier.type === utils_1.AST_NODE_TYPES.ImportNamespaceSpecifier) {
|
|
415
|
+
reactNamespaceIdentifiers.add(specifier.local.name);
|
|
416
|
+
}
|
|
417
|
+
});
|
|
418
|
+
}
|
|
419
|
+
if (!MEMOIZE_MODULES.has(sourceValue)) {
|
|
420
|
+
return;
|
|
421
|
+
}
|
|
422
|
+
for (const specifier of node.specifiers) {
|
|
423
|
+
if (specifier.type === utils_1.AST_NODE_TYPES.ImportSpecifier) {
|
|
424
|
+
if (specifier.imported.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
425
|
+
specifier.imported.name === 'Memoize') {
|
|
426
|
+
hasMemoizeImport = true;
|
|
427
|
+
memoizeAlias = specifier.local?.name ?? memoizeAlias;
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
else if (specifier.type === utils_1.AST_NODE_TYPES.ImportNamespaceSpecifier) {
|
|
431
|
+
hasMemoizeImport = true;
|
|
432
|
+
memoizeNamespace = specifier.local.name;
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
},
|
|
436
|
+
MethodDefinition(node) {
|
|
437
|
+
if (node.kind === 'set' || node.kind === 'constructor') {
|
|
438
|
+
return;
|
|
439
|
+
}
|
|
440
|
+
if (node.static) {
|
|
441
|
+
return;
|
|
442
|
+
}
|
|
443
|
+
if (node.value.type !== utils_1.AST_NODE_TYPES.FunctionExpression) {
|
|
444
|
+
return;
|
|
445
|
+
}
|
|
446
|
+
const hasDecorator = node.decorators?.some((decorator) => isMemoizeDecorator(decorator, memoizeAlias, memoizeNamespace));
|
|
447
|
+
if (hasDecorator) {
|
|
448
|
+
return;
|
|
449
|
+
}
|
|
450
|
+
const localFunctions = collectLocalFunctions(node.value.body);
|
|
451
|
+
if (!functionReturnsJSX(node.value, localFunctions, jsxReturnCache, factoryContext)) {
|
|
452
|
+
return;
|
|
453
|
+
}
|
|
454
|
+
const decoratorIdent = memoizeNamespace
|
|
455
|
+
? `${memoizeNamespace}.Memoize`
|
|
456
|
+
: memoizeAlias;
|
|
457
|
+
context.report({
|
|
458
|
+
node,
|
|
459
|
+
messageId: 'requireMemoizeJsxReturner',
|
|
460
|
+
data: { name: getMemberName(node) },
|
|
461
|
+
fix(fixer) {
|
|
462
|
+
const sourceCode = context.getSourceCode();
|
|
463
|
+
const { fixes, scheduledImportFix: newScheduledImportFix } = getImportFixes(fixer, sourceCode, hasMemoizeImport, scheduledImportFix);
|
|
464
|
+
scheduledImportFix = newScheduledImportFix;
|
|
465
|
+
const insertionTarget = node.decorators && node.decorators.length > 0
|
|
466
|
+
? node.decorators[0]
|
|
467
|
+
: node;
|
|
468
|
+
const insertionStart = insertionTarget.range
|
|
469
|
+
? insertionTarget.range[0]
|
|
470
|
+
: node.range
|
|
471
|
+
? node.range[0]
|
|
472
|
+
: 0;
|
|
473
|
+
const text = sourceCode.text;
|
|
474
|
+
const lineStart = text.lastIndexOf('\n', insertionStart - 1) + 1;
|
|
475
|
+
const leadingWhitespace = text.slice(lineStart, insertionStart).match(/^[ \t]*/)?.[0] ?? '';
|
|
476
|
+
fixes.push(fixer.insertTextBeforeRange([lineStart, lineStart], `${leadingWhitespace}@${decoratorIdent}()\n`));
|
|
477
|
+
return fixes;
|
|
478
|
+
},
|
|
479
|
+
});
|
|
480
|
+
},
|
|
481
|
+
};
|
|
482
|
+
},
|
|
483
|
+
});
|
|
484
|
+
exports.default = exports.requireMemoizeJsxReturners;
|
|
485
|
+
//# sourceMappingURL=require-memoize-jsx-returners.js.map
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.requireUseMemoObjectLiterals = void 0;
|
|
4
4
|
const createRule_1 = require("../utils/createRule");
|
|
5
|
+
const ASTHelpers_1 = require("../utils/ASTHelpers");
|
|
5
6
|
exports.requireUseMemoObjectLiterals = (0, createRule_1.createRule)({
|
|
6
7
|
name: 'require-usememo-object-literals',
|
|
7
8
|
meta: {
|
|
@@ -11,7 +12,7 @@ exports.requireUseMemoObjectLiterals = (0, createRule_1.createRule)({
|
|
|
11
12
|
recommended: 'error',
|
|
12
13
|
},
|
|
13
14
|
messages: {
|
|
14
|
-
requireUseMemo: 'Inline
|
|
15
|
+
requireUseMemo: 'Inline {{literalType}} literal passed to {{componentName}} prop "{{propName}}" is recreated on every render, producing a new reference that forces child components to re-render even when values stay the same. Wrap the literal in useMemo (or hoist a memoized constant) so the prop reference remains stable between renders.',
|
|
15
16
|
},
|
|
16
17
|
schema: [],
|
|
17
18
|
},
|
|
@@ -34,9 +35,7 @@ exports.requireUseMemoObjectLiterals = (0, createRule_1.createRule)({
|
|
|
34
35
|
if ((expression.type === 'ObjectExpression' ||
|
|
35
36
|
expression.type === 'ArrayExpression') &&
|
|
36
37
|
// Ensure we're in a function component context
|
|
37
|
-
context
|
|
38
|
-
.getAncestors()
|
|
39
|
-
.some((ancestor) => ancestor.type === 'FunctionDeclaration' ||
|
|
38
|
+
ASTHelpers_1.ASTHelpers.getAncestors(context, node).some((ancestor) => ancestor.type === 'FunctionDeclaration' ||
|
|
40
39
|
ancestor.type === 'ArrowFunctionExpression' ||
|
|
41
40
|
ancestor.type === 'FunctionExpression')) {
|
|
42
41
|
// Check if the parent component name starts with an uppercase letter
|
|
@@ -46,9 +45,15 @@ exports.requireUseMemoObjectLiterals = (0, createRule_1.createRule)({
|
|
|
46
45
|
? jsxElement.name.name
|
|
47
46
|
: '';
|
|
48
47
|
if (elementName && /^[A-Z]/.test(elementName)) {
|
|
48
|
+
const literalType = expression.type === 'ObjectExpression' ? 'object' : 'array';
|
|
49
49
|
context.report({
|
|
50
50
|
node: expression,
|
|
51
51
|
messageId: 'requireUseMemo',
|
|
52
|
+
data: {
|
|
53
|
+
literalType,
|
|
54
|
+
propName: typeof propName === 'string' ? propName : 'prop',
|
|
55
|
+
componentName: elementName,
|
|
56
|
+
},
|
|
52
57
|
});
|
|
53
58
|
}
|
|
54
59
|
}
|