@blumintinc/eslint-plugin-blumint 1.13.0 → 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 +170 -139
- package/lib/index.js +190 -1
- package/lib/rules/array-methods-this-context.js +1 -1
- 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.js +3 -4
- package/lib/rules/enforce-assert-safe-object-key.js +11 -11
- package/lib/rules/enforce-boolean-naming-prefixes.js +11 -23
- package/lib/rules/enforce-callback-memo.js +1 -1
- package/lib/rules/enforce-centralized-mock-firestore.js +1 -1
- package/lib/rules/enforce-console-error.js +35 -7
- 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-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 +1 -1
- package/lib/rules/enforce-fieldpath-syntax-in-docsetter.js +100 -50
- package/lib/rules/enforce-firestore-doc-ref-generic.js +15 -3
- package/lib/rules/enforce-firestore-facade.js +4 -2
- package/lib/rules/enforce-firestore-set-merge.js +3 -4
- package/lib/rules/enforce-global-constants.js +1 -1
- package/lib/rules/enforce-identifiable-firestore-type.js +108 -58
- package/lib/rules/enforce-memoize-async.js +2 -6
- package/lib/rules/enforce-memoize-getters.js +39 -15
- package/lib/rules/enforce-microdiff.js +1 -1
- package/lib/rules/enforce-object-literal-as-const.js +4 -4
- package/lib/rules/enforce-positive-naming.js +4 -0
- package/lib/rules/enforce-props-argument-name.js +2 -2
- 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 +1 -1
- 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.js +7 -3
- 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 +5 -4
- 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 +40 -13
- package/lib/rules/fast-deep-equal-over-microdiff.js +29 -39
- package/lib/rules/firestore-transaction-reads-before-writes.js +50 -4
- package/lib/rules/flatten-push-calls.d.ts +2 -0
- package/lib/rules/flatten-push-calls.js +428 -0
- 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/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.js +9 -3
- 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 +7 -3
- package/lib/rules/no-compositing-layer-props.js +2 -2
- 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.js +4 -6
- package/lib/rules/no-excessive-parent-chain.js +1 -1
- 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 +67 -12
- 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 +1 -1
- 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.js +6 -6
- package/lib/rules/no-memoize-on-static.js +9 -1
- package/lib/rules/no-misleading-boolean-prefixes.js +7 -3
- 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.js +29 -17
- package/lib/rules/no-passthrough-getters.js +38 -2
- 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.js +16 -3
- 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.js +11 -10
- package/lib/rules/no-separate-loading-state.js +7 -16
- package/lib/rules/no-stale-state-across-await.js +1 -1
- package/lib/rules/no-type-assertion-returns.js +43 -49
- package/lib/rules/no-undefined-null-passthrough.js +61 -31
- 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 -1
- package/lib/rules/no-unpinned-dependencies.js +36 -5
- package/lib/rules/no-unused-props.d.ts +2 -2
- package/lib/rules/no-unused-props.js +295 -91
- 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.js +6 -4
- package/lib/rules/parallelize-async-operations.js +24 -5
- package/lib/rules/prefer-batch-operations.d.ts +1 -3
- package/lib/rules/prefer-batch-operations.js +32 -5
- package/lib/rules/prefer-block-comments-for-declarations.js +9 -4
- 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-field-paths-in-transforms.js +4 -3
- package/lib/rules/prefer-fragment-component.js +1 -1
- 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.js +88 -13
- 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.js +60 -69
- package/lib/rules/prefer-nullish-coalescing-boolean-props.js +9 -4
- package/lib/rules/prefer-nullish-coalescing-override.d.ts +2 -1
- package/lib/rules/prefer-nullish-coalescing-override.js +11 -5
- package/lib/rules/prefer-params-over-parent-id.js +220 -171
- package/lib/rules/prefer-settings-object.js +2 -2
- package/lib/rules/prefer-type-over-interface.js +7 -2
- package/lib/rules/prefer-url-tostring-over-tojson.js +6 -3
- package/lib/rules/prefer-use-deep-compare-memo.js +8 -5
- package/lib/rules/prefer-usecallback-over-usememo-for-functions.js +1 -1
- 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.js +2 -2
- 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 +1 -1
- package/lib/rules/require-memo.js +1 -1
- 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 +2 -3
- package/lib/rules/test-file-location-enforcement.js +1 -1
- package/lib/rules/use-latest-callback.js +6 -5
- 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 +24 -0
- package/lib/utils/getMethodName.d.ts +27 -0
- package/lib/utils/getMethodName.js +35 -0
- package/lib/utils/tsTypeClassifier.d.ts +30 -0
- package/lib/utils/tsTypeClassifier.js +149 -0
- package/package.json +3 -1
|
@@ -25,7 +25,9 @@ exports.preferGlobalRouterStateKey = (0, createRule_1.createRule)({
|
|
|
25
25
|
},
|
|
26
26
|
defaultOptions: [],
|
|
27
27
|
create(context) {
|
|
28
|
-
const sourceCode = context.
|
|
28
|
+
const sourceCode = context.sourceCode;
|
|
29
|
+
// Prevent duplicate import insertions when multiple fixes target the same query key constant.
|
|
30
|
+
const scheduledQueryKeyNamedImports = new Set();
|
|
29
31
|
// Track imports from queryKeys.ts
|
|
30
32
|
const queryKeyImports = new Map();
|
|
31
33
|
// Track namespace imports (import * as QueryKeys from ...)
|
|
@@ -172,15 +174,42 @@ exports.preferGlobalRouterStateKey = (0, createRule_1.createRule)({
|
|
|
172
174
|
.replace(/^_|_$/g, '');
|
|
173
175
|
return `QUERY_KEY_${normalizedKey}`;
|
|
174
176
|
}
|
|
177
|
+
/**
|
|
178
|
+
* Helper to find a key in an import map based on a predicate
|
|
179
|
+
*/
|
|
180
|
+
function findImportKey(importMap, predicate) {
|
|
181
|
+
return Array.from(importMap.entries()).find(([, value]) => predicate(value))?.[0];
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* Helper to find the last directive prologue in a statement list
|
|
185
|
+
*/
|
|
186
|
+
function findLastDirective(body) {
|
|
187
|
+
let lastDirective;
|
|
188
|
+
for (const stmt of body) {
|
|
189
|
+
if (stmt.type === utils_1.AST_NODE_TYPES.ExpressionStatement &&
|
|
190
|
+
stmt.expression.type === utils_1.AST_NODE_TYPES.Literal &&
|
|
191
|
+
typeof stmt.expression.value === 'string' &&
|
|
192
|
+
typeof stmt.directive === 'string') {
|
|
193
|
+
lastDirective = stmt;
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
break;
|
|
197
|
+
}
|
|
198
|
+
return lastDirective;
|
|
199
|
+
}
|
|
175
200
|
return {
|
|
176
201
|
// Track imports from queryKeys.ts
|
|
177
202
|
ImportDeclaration(node) {
|
|
203
|
+
if (node.importKind === 'type')
|
|
204
|
+
return;
|
|
178
205
|
if (node.source.type === utils_1.AST_NODE_TYPES.Literal &&
|
|
179
206
|
typeof node.source.value === 'string') {
|
|
180
207
|
const source = node.source.value;
|
|
181
208
|
if (isQueryKeysSource(source)) {
|
|
182
209
|
node.specifiers.forEach((spec) => {
|
|
183
210
|
if (spec.type === utils_1.AST_NODE_TYPES.ImportSpecifier) {
|
|
211
|
+
if (spec.importKind === 'type')
|
|
212
|
+
return;
|
|
184
213
|
const imported = spec.imported.name;
|
|
185
214
|
const local = spec.local.name;
|
|
186
215
|
queryKeyImports.set(local, { source, imported });
|
|
@@ -235,23 +264,69 @@ exports.preferGlobalRouterStateKey = (0, createRule_1.createRule)({
|
|
|
235
264
|
const suggestedConstant = generateAutoFix(keyValue.value);
|
|
236
265
|
if (suggestedConstant) {
|
|
237
266
|
const fixes = [];
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
//
|
|
241
|
-
const
|
|
242
|
-
const alreadyImportedNamed = Array.from(queryKeyImports.values()).some((info) => isQueryKeysSource(info.source) &&
|
|
267
|
+
const namespaceAlias = findImportKey(namespaceImports, isQueryKeysSource);
|
|
268
|
+
const defaultAlias = findImportKey(defaultImports, isQueryKeysSource);
|
|
269
|
+
// Check if the constant is already imported (possibly with an alias)
|
|
270
|
+
const existingNamedImport = findImportKey(queryKeyImports, (info) => isQueryKeysSource(info.source) &&
|
|
243
271
|
info.imported === suggestedConstant);
|
|
244
|
-
const
|
|
245
|
-
|
|
246
|
-
|
|
272
|
+
const localName = existingNamedImport;
|
|
273
|
+
const importAlias = namespaceAlias ?? defaultAlias;
|
|
274
|
+
const formatConstantReference = (alias, constant) => alias ? `${alias}.${constant}` : constant;
|
|
275
|
+
const replacementText = localName
|
|
276
|
+
? localName
|
|
277
|
+
: formatConstantReference(importAlias, suggestedConstant);
|
|
278
|
+
// 1) Replace the literal with the constant (qualify if alias exists)
|
|
279
|
+
fixes.push(fixer.replaceText(keyValue, replacementText));
|
|
280
|
+
// 2) Ensure an import exists for the suggested constant
|
|
281
|
+
const hasNamespaceOrDefault = Boolean(importAlias);
|
|
282
|
+
if (!existingNamedImport &&
|
|
283
|
+
!hasNamespaceOrDefault) {
|
|
284
|
+
if (scheduledQueryKeyNamedImports.has(suggestedConstant)) {
|
|
285
|
+
return fixes;
|
|
286
|
+
}
|
|
247
287
|
const importText = `import { ${suggestedConstant} } from '@/util/routing/queryKeys';\n`;
|
|
248
|
-
const
|
|
249
|
-
|
|
250
|
-
|
|
288
|
+
const queryKeysNamedImport = sourceCode.ast.body.find((n) => n.type ===
|
|
289
|
+
utils_1.AST_NODE_TYPES.ImportDeclaration &&
|
|
290
|
+
n.importKind !== 'type' &&
|
|
291
|
+
n.source.type === utils_1.AST_NODE_TYPES.Literal &&
|
|
292
|
+
typeof n.source.value === 'string' &&
|
|
293
|
+
isQueryKeysSource(n.source.value) &&
|
|
294
|
+
n.specifiers.some((s) => s.type ===
|
|
295
|
+
utils_1.AST_NODE_TYPES.ImportSpecifier));
|
|
296
|
+
const sideEffectImport = sourceCode.ast.body.find((n) => n.type === utils_1.AST_NODE_TYPES.ImportDeclaration &&
|
|
297
|
+
n.source.type === utils_1.AST_NODE_TYPES.Literal &&
|
|
298
|
+
typeof n.source.value === 'string' &&
|
|
299
|
+
isQueryKeysSource(n.source.value) &&
|
|
300
|
+
n.specifiers.length === 0);
|
|
301
|
+
if (queryKeysNamedImport) {
|
|
302
|
+
const importSpecifiers = queryKeysNamedImport.specifiers.filter((spec) => spec.type ===
|
|
303
|
+
utils_1.AST_NODE_TYPES.ImportSpecifier);
|
|
304
|
+
const lastSpecifier = importSpecifiers[importSpecifiers.length - 1];
|
|
305
|
+
fixes.push(fixer.insertTextAfter(lastSpecifier, `, ${suggestedConstant}`));
|
|
306
|
+
}
|
|
307
|
+
else if (sideEffectImport) {
|
|
308
|
+
fixes.push(fixer.replaceText(sideEffectImport, importText.trimEnd()));
|
|
251
309
|
}
|
|
252
310
|
else {
|
|
253
|
-
|
|
311
|
+
const firstImport = sourceCode.ast.body.find((n) => n.type ===
|
|
312
|
+
utils_1.AST_NODE_TYPES.ImportDeclaration &&
|
|
313
|
+
n.importKind !== 'type') ||
|
|
314
|
+
sourceCode.ast.body.find((n) => n.type ===
|
|
315
|
+
utils_1.AST_NODE_TYPES.ImportDeclaration);
|
|
316
|
+
if (firstImport) {
|
|
317
|
+
fixes.push(fixer.insertTextBefore(firstImport, importText));
|
|
318
|
+
}
|
|
319
|
+
else {
|
|
320
|
+
const lastDirective = findLastDirective(sourceCode.ast.body);
|
|
321
|
+
if (lastDirective) {
|
|
322
|
+
fixes.push(fixer.insertTextAfter(lastDirective, `\n${importText}`));
|
|
323
|
+
}
|
|
324
|
+
else {
|
|
325
|
+
fixes.push(fixer.insertTextBeforeRange([0, 0], importText));
|
|
326
|
+
}
|
|
327
|
+
}
|
|
254
328
|
}
|
|
329
|
+
scheduledQueryKeyNamedImports.add(suggestedConstant);
|
|
255
330
|
}
|
|
256
331
|
return fixes;
|
|
257
332
|
}
|
|
@@ -0,0 +1,445 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.preferMemoizedProps = void 0;
|
|
4
|
+
const utils_1 = require("@typescript-eslint/utils");
|
|
5
|
+
const createRule_1 = require("../utils/createRule");
|
|
6
|
+
const HOOK_NAMES = new Set(['useMemo', 'useCallback']);
|
|
7
|
+
exports.preferMemoizedProps = (0, createRule_1.createRule)({
|
|
8
|
+
name: 'prefer-memoized-props',
|
|
9
|
+
meta: {
|
|
10
|
+
type: 'suggestion',
|
|
11
|
+
docs: {
|
|
12
|
+
description: 'Require memoizing reference props (objects, arrays, functions) inside React.memo components while avoiding unnecessary useMemo for pass-through values.',
|
|
13
|
+
recommended: 'error',
|
|
14
|
+
},
|
|
15
|
+
schema: [],
|
|
16
|
+
messages: {
|
|
17
|
+
memoizeReferenceProp: 'Prop "{{propName}}" in a React.memo component receives a {{kind}} that is recreated every render, so memoized children lose referential equality and re-render. Memoize this {{kind}} with useMemo/useCallback or hoist a stable constant so the prop reference stays stable.',
|
|
18
|
+
avoidPrimitiveMemo: 'useMemo around "{{value}}" only wraps a pass-through value. Primitives already compare by value, and wrapping existing references without creating new objects does not improve stability. Return the value directly instead of adding memoization noise.',
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
defaultOptions: [],
|
|
22
|
+
create(context) {
|
|
23
|
+
const sourceCode = context.getSourceCode();
|
|
24
|
+
const visitorKeys = sourceCode.visitorKeys;
|
|
25
|
+
const memoizedComponents = new WeakSet();
|
|
26
|
+
const memoScopes = [];
|
|
27
|
+
const componentStack = [];
|
|
28
|
+
const scopedFunctions = new WeakSet();
|
|
29
|
+
function pushMemoScope() {
|
|
30
|
+
memoScopes.push(new Map());
|
|
31
|
+
}
|
|
32
|
+
function popMemoScope() {
|
|
33
|
+
memoScopes.pop();
|
|
34
|
+
}
|
|
35
|
+
function currentMemoScope() {
|
|
36
|
+
return memoScopes[memoScopes.length - 1];
|
|
37
|
+
}
|
|
38
|
+
function declareFunctionBinding(name, node) {
|
|
39
|
+
const scope = currentMemoScope();
|
|
40
|
+
if (!scope)
|
|
41
|
+
return;
|
|
42
|
+
scope.set(name, node);
|
|
43
|
+
}
|
|
44
|
+
function resolveFunctionBinding(name) {
|
|
45
|
+
for (let i = memoScopes.length - 1; i >= 0; i -= 1) {
|
|
46
|
+
const binding = memoScopes[i].get(name);
|
|
47
|
+
if (binding) {
|
|
48
|
+
return binding;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return undefined;
|
|
52
|
+
}
|
|
53
|
+
function hoistFunctionDeclarations(statements) {
|
|
54
|
+
const scope = currentMemoScope();
|
|
55
|
+
if (!scope)
|
|
56
|
+
return;
|
|
57
|
+
for (const statement of statements) {
|
|
58
|
+
if (statement.type === utils_1.AST_NODE_TYPES.FunctionDeclaration &&
|
|
59
|
+
statement.id?.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
60
|
+
scope.set(statement.id.name, statement);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
function registerFunctionVariable(node) {
|
|
65
|
+
if (node.id.type !== utils_1.AST_NODE_TYPES.Identifier)
|
|
66
|
+
return;
|
|
67
|
+
const { init } = node;
|
|
68
|
+
if (init &&
|
|
69
|
+
(init.type === utils_1.AST_NODE_TYPES.FunctionExpression ||
|
|
70
|
+
init.type === utils_1.AST_NODE_TYPES.ArrowFunctionExpression)) {
|
|
71
|
+
declareFunctionBinding(node.id.name, init);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
function isReactMemoCallee(callee) {
|
|
75
|
+
if (callee.type === utils_1.AST_NODE_TYPES.Identifier && callee.name === 'memo') {
|
|
76
|
+
return true;
|
|
77
|
+
}
|
|
78
|
+
if (callee.type === utils_1.AST_NODE_TYPES.MemberExpression &&
|
|
79
|
+
!callee.computed &&
|
|
80
|
+
callee.property.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
81
|
+
callee.property.name === 'memo') {
|
|
82
|
+
return true;
|
|
83
|
+
}
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
function isHookCall(node, name) {
|
|
87
|
+
if (node.callee.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
88
|
+
node.callee.name === name) {
|
|
89
|
+
return true;
|
|
90
|
+
}
|
|
91
|
+
return (node.callee.type === utils_1.AST_NODE_TYPES.MemberExpression &&
|
|
92
|
+
!node.callee.computed &&
|
|
93
|
+
node.callee.property.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
94
|
+
node.callee.property.name === name);
|
|
95
|
+
}
|
|
96
|
+
function collectMemoizedComponents(node) {
|
|
97
|
+
pushMemoScope();
|
|
98
|
+
if (node.type === utils_1.AST_NODE_TYPES.Program) {
|
|
99
|
+
hoistFunctionDeclarations(node.body);
|
|
100
|
+
}
|
|
101
|
+
function traverse(current) {
|
|
102
|
+
const isFunctionLike = current.type === utils_1.AST_NODE_TYPES.FunctionDeclaration ||
|
|
103
|
+
current.type === utils_1.AST_NODE_TYPES.FunctionExpression ||
|
|
104
|
+
current.type === utils_1.AST_NODE_TYPES.ArrowFunctionExpression;
|
|
105
|
+
if (current.type === utils_1.AST_NODE_TYPES.FunctionDeclaration &&
|
|
106
|
+
current.id?.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
107
|
+
declareFunctionBinding(current.id.name, current);
|
|
108
|
+
}
|
|
109
|
+
let pushedScope = false;
|
|
110
|
+
if (current.type === utils_1.AST_NODE_TYPES.BlockStatement || isFunctionLike) {
|
|
111
|
+
pushMemoScope();
|
|
112
|
+
pushedScope = true;
|
|
113
|
+
if (isFunctionLike &&
|
|
114
|
+
'id' in current &&
|
|
115
|
+
current.id?.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
116
|
+
declareFunctionBinding(current.id.name, current);
|
|
117
|
+
}
|
|
118
|
+
if (current.type === utils_1.AST_NODE_TYPES.BlockStatement) {
|
|
119
|
+
hoistFunctionDeclarations(current.body);
|
|
120
|
+
}
|
|
121
|
+
else if (isFunctionLike &&
|
|
122
|
+
current.body.type === utils_1.AST_NODE_TYPES.BlockStatement) {
|
|
123
|
+
hoistFunctionDeclarations(current.body.body);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
if (current.type === utils_1.AST_NODE_TYPES.VariableDeclarator) {
|
|
127
|
+
registerFunctionVariable(current);
|
|
128
|
+
}
|
|
129
|
+
if (current.type === utils_1.AST_NODE_TYPES.CallExpression &&
|
|
130
|
+
isReactMemoCallee(current.callee)) {
|
|
131
|
+
const [firstArg] = current.arguments;
|
|
132
|
+
if (firstArg &&
|
|
133
|
+
(firstArg.type === utils_1.AST_NODE_TYPES.ArrowFunctionExpression ||
|
|
134
|
+
firstArg.type === utils_1.AST_NODE_TYPES.FunctionExpression)) {
|
|
135
|
+
memoizedComponents.add(firstArg);
|
|
136
|
+
}
|
|
137
|
+
else if (firstArg && firstArg.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
138
|
+
const binding = resolveFunctionBinding(firstArg.name);
|
|
139
|
+
if (binding) {
|
|
140
|
+
memoizedComponents.add(binding);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
const keys = visitorKeys[current.type] ?? [];
|
|
145
|
+
for (const key of keys) {
|
|
146
|
+
const value = current[key];
|
|
147
|
+
if (Array.isArray(value)) {
|
|
148
|
+
for (const child of value) {
|
|
149
|
+
if (child && typeof child === 'object') {
|
|
150
|
+
traverse(child);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
else if (value && typeof value === 'object') {
|
|
155
|
+
traverse(value);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
if (pushedScope) {
|
|
159
|
+
popMemoScope();
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
traverse(node);
|
|
163
|
+
popMemoScope();
|
|
164
|
+
}
|
|
165
|
+
collectMemoizedComponents(sourceCode.ast);
|
|
166
|
+
function isMemoizedComponent(node) {
|
|
167
|
+
return memoizedComponents.has(node);
|
|
168
|
+
}
|
|
169
|
+
function pushComponent(node) {
|
|
170
|
+
componentStack.push({ node, scopes: [new Map()] });
|
|
171
|
+
}
|
|
172
|
+
function popComponent(node) {
|
|
173
|
+
const top = componentStack[componentStack.length - 1];
|
|
174
|
+
if (top && top.node === node) {
|
|
175
|
+
componentStack.pop();
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
function currentComponent() {
|
|
179
|
+
return componentStack[componentStack.length - 1];
|
|
180
|
+
}
|
|
181
|
+
function currentScope() {
|
|
182
|
+
const component = currentComponent();
|
|
183
|
+
if (!component)
|
|
184
|
+
return undefined;
|
|
185
|
+
return component.scopes[component.scopes.length - 1];
|
|
186
|
+
}
|
|
187
|
+
function pushScopeForNestedFunction(node) {
|
|
188
|
+
const component = currentComponent();
|
|
189
|
+
if (!component)
|
|
190
|
+
return;
|
|
191
|
+
component.scopes.push(new Map());
|
|
192
|
+
scopedFunctions.add(node);
|
|
193
|
+
}
|
|
194
|
+
function popScopeForNestedFunction(node) {
|
|
195
|
+
const component = currentComponent();
|
|
196
|
+
if (!component)
|
|
197
|
+
return;
|
|
198
|
+
if (!scopedFunctions.has(node))
|
|
199
|
+
return;
|
|
200
|
+
component.scopes.pop();
|
|
201
|
+
scopedFunctions.delete(node);
|
|
202
|
+
}
|
|
203
|
+
function unwrapExpression(expression) {
|
|
204
|
+
let current = expression;
|
|
205
|
+
while (true) {
|
|
206
|
+
if (current.type === utils_1.AST_NODE_TYPES.TSAsExpression ||
|
|
207
|
+
current.type === utils_1.AST_NODE_TYPES.TSSatisfiesExpression ||
|
|
208
|
+
current.type === utils_1.AST_NODE_TYPES.TSTypeAssertion) {
|
|
209
|
+
current = current.expression;
|
|
210
|
+
continue;
|
|
211
|
+
}
|
|
212
|
+
if (current.type === utils_1.AST_NODE_TYPES.TSNonNullExpression) {
|
|
213
|
+
current = current.expression;
|
|
214
|
+
continue;
|
|
215
|
+
}
|
|
216
|
+
break;
|
|
217
|
+
}
|
|
218
|
+
return current;
|
|
219
|
+
}
|
|
220
|
+
function getBindingKind(init) {
|
|
221
|
+
if (!init) {
|
|
222
|
+
return null;
|
|
223
|
+
}
|
|
224
|
+
const unwrapped = unwrapExpression(init);
|
|
225
|
+
if (unwrapped.type === utils_1.AST_NODE_TYPES.CallExpression &&
|
|
226
|
+
((unwrapped.callee.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
227
|
+
HOOK_NAMES.has(unwrapped.callee.name)) ||
|
|
228
|
+
(unwrapped.callee.type === utils_1.AST_NODE_TYPES.MemberExpression &&
|
|
229
|
+
!unwrapped.callee.computed &&
|
|
230
|
+
unwrapped.callee.property.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
231
|
+
HOOK_NAMES.has(unwrapped.callee.property.name)))) {
|
|
232
|
+
return null;
|
|
233
|
+
}
|
|
234
|
+
if (unwrapped.type === utils_1.AST_NODE_TYPES.ObjectExpression)
|
|
235
|
+
return 'object';
|
|
236
|
+
if (unwrapped.type === utils_1.AST_NODE_TYPES.ArrayExpression)
|
|
237
|
+
return 'array';
|
|
238
|
+
if (unwrapped.type === utils_1.AST_NODE_TYPES.ArrowFunctionExpression ||
|
|
239
|
+
unwrapped.type === utils_1.AST_NODE_TYPES.FunctionExpression) {
|
|
240
|
+
return 'function';
|
|
241
|
+
}
|
|
242
|
+
return null;
|
|
243
|
+
}
|
|
244
|
+
function recordBinding(node) {
|
|
245
|
+
const scope = currentScope();
|
|
246
|
+
if (!scope)
|
|
247
|
+
return;
|
|
248
|
+
if (node.id.type !== utils_1.AST_NODE_TYPES.Identifier || !node.init) {
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
const initializer = unwrapExpression(node.init);
|
|
252
|
+
const aliasKind = initializer.type === utils_1.AST_NODE_TYPES.Identifier
|
|
253
|
+
? findBindingKind(initializer.name)
|
|
254
|
+
: null;
|
|
255
|
+
const kind = aliasKind ?? getBindingKind(initializer);
|
|
256
|
+
if (kind)
|
|
257
|
+
scope.set(node.id.name, kind);
|
|
258
|
+
}
|
|
259
|
+
function findBindingKind(name) {
|
|
260
|
+
for (let i = componentStack.length - 1; i >= 0; i -= 1) {
|
|
261
|
+
const component = componentStack[i];
|
|
262
|
+
for (let j = component.scopes.length - 1; j >= 0; j -= 1) {
|
|
263
|
+
const binding = component.scopes[j].get(name);
|
|
264
|
+
if (binding)
|
|
265
|
+
return binding;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
return null;
|
|
269
|
+
}
|
|
270
|
+
function getAttributeName(name) {
|
|
271
|
+
if (name.type === utils_1.AST_NODE_TYPES.JSXIdentifier) {
|
|
272
|
+
return name.name;
|
|
273
|
+
}
|
|
274
|
+
if (name.type === utils_1.AST_NODE_TYPES.JSXNamespacedName) {
|
|
275
|
+
return name.name.name;
|
|
276
|
+
}
|
|
277
|
+
return 'prop';
|
|
278
|
+
}
|
|
279
|
+
function reportReference(node, propName, kind) {
|
|
280
|
+
context.report({
|
|
281
|
+
node,
|
|
282
|
+
messageId: 'memoizeReferenceProp',
|
|
283
|
+
data: { propName, kind },
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
function isEnvironmentFreeLiteral(expression) {
|
|
287
|
+
const unwrapped = unwrapExpression(expression);
|
|
288
|
+
if (unwrapped.type === utils_1.AST_NODE_TYPES.Literal)
|
|
289
|
+
return true;
|
|
290
|
+
if (unwrapped.type === utils_1.AST_NODE_TYPES.TemplateLiteral &&
|
|
291
|
+
unwrapped.expressions.length === 0) {
|
|
292
|
+
return true;
|
|
293
|
+
}
|
|
294
|
+
return false;
|
|
295
|
+
}
|
|
296
|
+
function isStableReference(expression) {
|
|
297
|
+
const unwrapped = unwrapExpression(expression);
|
|
298
|
+
if (unwrapped.type === utils_1.AST_NODE_TYPES.Literal)
|
|
299
|
+
return true;
|
|
300
|
+
if (unwrapped.type === utils_1.AST_NODE_TYPES.TemplateLiteral) {
|
|
301
|
+
return unwrapped.expressions.every((expr) => isStableReference(expr));
|
|
302
|
+
}
|
|
303
|
+
if (unwrapped.type === utils_1.AST_NODE_TYPES.Identifier)
|
|
304
|
+
return true;
|
|
305
|
+
if (unwrapped.type === utils_1.AST_NODE_TYPES.MemberExpression)
|
|
306
|
+
return true;
|
|
307
|
+
if (unwrapped.type === utils_1.AST_NODE_TYPES.ChainExpression &&
|
|
308
|
+
unwrapped.expression.type === utils_1.AST_NODE_TYPES.MemberExpression) {
|
|
309
|
+
return true;
|
|
310
|
+
}
|
|
311
|
+
return false;
|
|
312
|
+
}
|
|
313
|
+
function extractReturnExpression(callback) {
|
|
314
|
+
if (callback.body.type !== utils_1.AST_NODE_TYPES.BlockStatement) {
|
|
315
|
+
return callback.body;
|
|
316
|
+
}
|
|
317
|
+
const [statement] = callback.body.body;
|
|
318
|
+
if (statement &&
|
|
319
|
+
statement.type === utils_1.AST_NODE_TYPES.ReturnStatement &&
|
|
320
|
+
statement.argument) {
|
|
321
|
+
return statement.argument;
|
|
322
|
+
}
|
|
323
|
+
return null;
|
|
324
|
+
}
|
|
325
|
+
function handleUseMemo(node) {
|
|
326
|
+
if (!currentComponent() || !isHookCall(node, 'useMemo')) {
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
329
|
+
const [callback, deps] = node.arguments;
|
|
330
|
+
if (!callback ||
|
|
331
|
+
(callback.type !== utils_1.AST_NODE_TYPES.ArrowFunctionExpression &&
|
|
332
|
+
callback.type !== utils_1.AST_NODE_TYPES.FunctionExpression)) {
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
335
|
+
const returnExpression = extractReturnExpression(callback);
|
|
336
|
+
if (!returnExpression)
|
|
337
|
+
return;
|
|
338
|
+
const dependencies = deps && deps.type === utils_1.AST_NODE_TYPES.ArrayExpression ? deps : null;
|
|
339
|
+
if (dependencies &&
|
|
340
|
+
dependencies.elements.length === 0 &&
|
|
341
|
+
!isEnvironmentFreeLiteral(returnExpression)) {
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
if (isStableReference(returnExpression)) {
|
|
345
|
+
const valueText = sourceCode.getText(returnExpression).slice(0, 80);
|
|
346
|
+
context.report({
|
|
347
|
+
node,
|
|
348
|
+
messageId: 'avoidPrimitiveMemo',
|
|
349
|
+
data: { value: valueText },
|
|
350
|
+
});
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
function handleJSXAttribute(node) {
|
|
354
|
+
if (!currentComponent())
|
|
355
|
+
return;
|
|
356
|
+
if (!node.value ||
|
|
357
|
+
node.value.type !== utils_1.AST_NODE_TYPES.JSXExpressionContainer) {
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
360
|
+
const rawExpression = node.value.expression;
|
|
361
|
+
if (rawExpression.type === utils_1.AST_NODE_TYPES.JSXEmptyExpression) {
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
364
|
+
const expression = unwrapExpression(rawExpression);
|
|
365
|
+
const propName = getAttributeName(node.name);
|
|
366
|
+
if (expression.type === utils_1.AST_NODE_TYPES.ObjectExpression) {
|
|
367
|
+
reportReference(expression, propName, 'object');
|
|
368
|
+
return;
|
|
369
|
+
}
|
|
370
|
+
if (expression.type === utils_1.AST_NODE_TYPES.ArrayExpression) {
|
|
371
|
+
reportReference(expression, propName, 'array');
|
|
372
|
+
return;
|
|
373
|
+
}
|
|
374
|
+
if (expression.type === utils_1.AST_NODE_TYPES.ArrowFunctionExpression ||
|
|
375
|
+
expression.type === utils_1.AST_NODE_TYPES.FunctionExpression) {
|
|
376
|
+
reportReference(expression, propName, 'function');
|
|
377
|
+
return;
|
|
378
|
+
}
|
|
379
|
+
if (expression.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
380
|
+
const bindingKind = findBindingKind(expression.name);
|
|
381
|
+
if (bindingKind) {
|
|
382
|
+
reportReference(expression, propName, bindingKind);
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
return {
|
|
387
|
+
FunctionDeclaration(node) {
|
|
388
|
+
if (isMemoizedComponent(node)) {
|
|
389
|
+
pushComponent(node);
|
|
390
|
+
return;
|
|
391
|
+
}
|
|
392
|
+
const scope = currentScope();
|
|
393
|
+
if (scope && node.id?.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
394
|
+
scope.set(node.id.name, 'function');
|
|
395
|
+
}
|
|
396
|
+
if (currentComponent()) {
|
|
397
|
+
pushScopeForNestedFunction(node);
|
|
398
|
+
}
|
|
399
|
+
},
|
|
400
|
+
'FunctionDeclaration:exit'(node) {
|
|
401
|
+
if (isMemoizedComponent(node)) {
|
|
402
|
+
popComponent(node);
|
|
403
|
+
return;
|
|
404
|
+
}
|
|
405
|
+
popScopeForNestedFunction(node);
|
|
406
|
+
},
|
|
407
|
+
FunctionExpression(node) {
|
|
408
|
+
if (isMemoizedComponent(node)) {
|
|
409
|
+
pushComponent(node);
|
|
410
|
+
return;
|
|
411
|
+
}
|
|
412
|
+
if (currentComponent()) {
|
|
413
|
+
pushScopeForNestedFunction(node);
|
|
414
|
+
}
|
|
415
|
+
},
|
|
416
|
+
'FunctionExpression:exit'(node) {
|
|
417
|
+
if (isMemoizedComponent(node)) {
|
|
418
|
+
popComponent(node);
|
|
419
|
+
return;
|
|
420
|
+
}
|
|
421
|
+
popScopeForNestedFunction(node);
|
|
422
|
+
},
|
|
423
|
+
ArrowFunctionExpression(node) {
|
|
424
|
+
if (isMemoizedComponent(node)) {
|
|
425
|
+
pushComponent(node);
|
|
426
|
+
return;
|
|
427
|
+
}
|
|
428
|
+
if (currentComponent()) {
|
|
429
|
+
pushScopeForNestedFunction(node);
|
|
430
|
+
}
|
|
431
|
+
},
|
|
432
|
+
'ArrowFunctionExpression:exit'(node) {
|
|
433
|
+
if (isMemoizedComponent(node)) {
|
|
434
|
+
popComponent(node);
|
|
435
|
+
return;
|
|
436
|
+
}
|
|
437
|
+
popScopeForNestedFunction(node);
|
|
438
|
+
},
|
|
439
|
+
VariableDeclarator: recordBinding,
|
|
440
|
+
CallExpression: handleUseMemo,
|
|
441
|
+
JSXAttribute: handleJSXAttribute,
|
|
442
|
+
};
|
|
443
|
+
},
|
|
444
|
+
});
|
|
445
|
+
//# sourceMappingURL=prefer-memoized-props.js.map
|