@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,169 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.preferUrlToStringOverToJson = void 0;
|
|
4
|
+
const createRule_1 = require("../utils/createRule");
|
|
5
|
+
const utils_1 = require("@typescript-eslint/utils");
|
|
6
|
+
// Helpers to identify URL constructor expressions
|
|
7
|
+
function isUrlConstructor(newExpr) {
|
|
8
|
+
const callee = newExpr.callee;
|
|
9
|
+
if (callee.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
10
|
+
return callee.name === 'URL';
|
|
11
|
+
}
|
|
12
|
+
if (callee.type === utils_1.AST_NODE_TYPES.MemberExpression) {
|
|
13
|
+
// e.g., new globalThis.URL(...), new window.URL(...)
|
|
14
|
+
return (callee.property.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
15
|
+
callee.property.name === 'URL');
|
|
16
|
+
}
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
19
|
+
function isJSONStringifyCall(node) {
|
|
20
|
+
const { callee } = node;
|
|
21
|
+
if (callee.type !== utils_1.AST_NODE_TYPES.MemberExpression)
|
|
22
|
+
return false;
|
|
23
|
+
let objectExpr = callee.object;
|
|
24
|
+
while (objectExpr.type === utils_1.AST_NODE_TYPES.MemberExpression) {
|
|
25
|
+
objectExpr = objectExpr.object;
|
|
26
|
+
}
|
|
27
|
+
const isJsonIdentifier = objectExpr.type === utils_1.AST_NODE_TYPES.Identifier && objectExpr.name === 'JSON';
|
|
28
|
+
const isStringifyProperty = callee.property.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
29
|
+
callee.property.name === 'stringify';
|
|
30
|
+
return isJsonIdentifier && isStringifyProperty;
|
|
31
|
+
}
|
|
32
|
+
function findEnclosingJSONStringify(node) {
|
|
33
|
+
let current = node;
|
|
34
|
+
while (current && current.parent) {
|
|
35
|
+
const parent = current.parent;
|
|
36
|
+
if (parent.type === utils_1.AST_NODE_TYPES.CallExpression) {
|
|
37
|
+
if (isJSONStringifyCall(parent) &&
|
|
38
|
+
parent.arguments.includes(current)) {
|
|
39
|
+
return parent;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
current = parent;
|
|
43
|
+
}
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
function isOptionalMemberExpression(expr) {
|
|
47
|
+
// @typescript-eslint defines optional?: boolean on MemberExpression for optional chaining
|
|
48
|
+
// Cast to any to safely access it across minor version diffs
|
|
49
|
+
return expr.optional === true;
|
|
50
|
+
}
|
|
51
|
+
exports.preferUrlToStringOverToJson = (0, createRule_1.createRule)({
|
|
52
|
+
create(context) {
|
|
53
|
+
const sourceCode = context.sourceCode;
|
|
54
|
+
const parserServices = sourceCode.parserServices;
|
|
55
|
+
const urlIdentifierNames = new Set();
|
|
56
|
+
let checker = null;
|
|
57
|
+
if (parserServices &&
|
|
58
|
+
parserServices.program &&
|
|
59
|
+
typeof parserServices.program.getTypeChecker === 'function') {
|
|
60
|
+
try {
|
|
61
|
+
checker = parserServices.program.getTypeChecker();
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
checker = null;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
function isUrlType(expr) {
|
|
68
|
+
// Heuristic without types
|
|
69
|
+
if (expr.type === utils_1.AST_NODE_TYPES.NewExpression &&
|
|
70
|
+
isUrlConstructor(expr)) {
|
|
71
|
+
return true;
|
|
72
|
+
}
|
|
73
|
+
if (expr.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
74
|
+
if (urlIdentifierNames.has(expr.name))
|
|
75
|
+
return true;
|
|
76
|
+
}
|
|
77
|
+
// Typed check via TS when available
|
|
78
|
+
if (checker &&
|
|
79
|
+
parserServices &&
|
|
80
|
+
parserServices.esTreeNodeToTSNodeMap) {
|
|
81
|
+
try {
|
|
82
|
+
const tsNode = parserServices.esTreeNodeToTSNodeMap.get(expr);
|
|
83
|
+
const type = checker.getTypeAtLocation(tsNode);
|
|
84
|
+
const isUrl = (t) => {
|
|
85
|
+
const sym = t.getSymbol();
|
|
86
|
+
if (sym?.getName() === 'URL')
|
|
87
|
+
return true;
|
|
88
|
+
const unionTypes = t.types;
|
|
89
|
+
return Array.isArray(unionTypes) && unionTypes.some(isUrl);
|
|
90
|
+
};
|
|
91
|
+
if (isUrl(type))
|
|
92
|
+
return true;
|
|
93
|
+
}
|
|
94
|
+
catch {
|
|
95
|
+
// ignore type errors and fall through
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return false;
|
|
99
|
+
}
|
|
100
|
+
return {
|
|
101
|
+
VariableDeclarator(node) {
|
|
102
|
+
if (node.id.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
103
|
+
node.init &&
|
|
104
|
+
node.init.type === utils_1.AST_NODE_TYPES.NewExpression &&
|
|
105
|
+
isUrlConstructor(node.init)) {
|
|
106
|
+
urlIdentifierNames.add(node.id.name);
|
|
107
|
+
}
|
|
108
|
+
},
|
|
109
|
+
AssignmentExpression(node) {
|
|
110
|
+
if (node.left.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
111
|
+
node.right.type === utils_1.AST_NODE_TYPES.NewExpression &&
|
|
112
|
+
isUrlConstructor(node.right)) {
|
|
113
|
+
urlIdentifierNames.add(node.left.name);
|
|
114
|
+
}
|
|
115
|
+
},
|
|
116
|
+
CallExpression(node) {
|
|
117
|
+
const callee = node.callee;
|
|
118
|
+
if (callee.type !== utils_1.AST_NODE_TYPES.MemberExpression)
|
|
119
|
+
return;
|
|
120
|
+
// property should be .toJSON or ['toJSON']
|
|
121
|
+
if ((callee.property.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
122
|
+
callee.property.name !== 'toJSON') ||
|
|
123
|
+
(callee.property.type === utils_1.AST_NODE_TYPES.Literal &&
|
|
124
|
+
callee.property.value !== 'toJSON')) {
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
const objectExpr = callee.object;
|
|
128
|
+
if (!isUrlType(objectExpr))
|
|
129
|
+
return;
|
|
130
|
+
const insideJSONStringify = findEnclosingJSONStringify(node);
|
|
131
|
+
const memberExpr = callee;
|
|
132
|
+
context.report({
|
|
133
|
+
node,
|
|
134
|
+
messageId: 'preferToString',
|
|
135
|
+
data: {
|
|
136
|
+
urlText: sourceCode.getText(objectExpr),
|
|
137
|
+
},
|
|
138
|
+
fix(fixer) {
|
|
139
|
+
// If inside JSON.stringify and not optional chain, replace the entire call with just the object
|
|
140
|
+
if (insideJSONStringify &&
|
|
141
|
+
!isOptionalMemberExpression(memberExpr)) {
|
|
142
|
+
const objText = sourceCode.getText(objectExpr);
|
|
143
|
+
return fixer.replaceText(node, objText);
|
|
144
|
+
}
|
|
145
|
+
// Default: change .toJSON() -> .toString()
|
|
146
|
+
const propertyRange = memberExpr.property.range;
|
|
147
|
+
return fixer.replaceTextRange(propertyRange, 'toString');
|
|
148
|
+
},
|
|
149
|
+
});
|
|
150
|
+
},
|
|
151
|
+
};
|
|
152
|
+
},
|
|
153
|
+
name: 'prefer-url-tostring-over-tojson',
|
|
154
|
+
meta: {
|
|
155
|
+
type: 'suggestion',
|
|
156
|
+
docs: {
|
|
157
|
+
description: 'Enforce the use of toString() over toJSON() on URL objects.',
|
|
158
|
+
recommended: 'error',
|
|
159
|
+
requiresTypeChecking: false,
|
|
160
|
+
},
|
|
161
|
+
fixable: 'code',
|
|
162
|
+
schema: [],
|
|
163
|
+
messages: {
|
|
164
|
+
preferToString: 'URL value {{urlText}} calls toJSON() explicitly. URL.toJSON() delegates directly to toString(), so the call is redundant. Inside JSON.stringify it is unnecessary because JSON.stringify already invokes toJSON automatically; elsewhere it hides intent because readers must remember the delegation to see it produces the same string as toString. Use toString() when you need an explicit string, or pass the URL directly to JSON.stringify to rely on its built-in serialization.',
|
|
165
|
+
},
|
|
166
|
+
},
|
|
167
|
+
defaultOptions: [],
|
|
168
|
+
});
|
|
169
|
+
//# sourceMappingURL=prefer-url-tostring-over-tojson.js.map
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { TSESLint } from '@typescript-eslint/utils';
|
|
2
|
+
export type MessageIds = 'preferUseDeepCompareMemo';
|
|
3
|
+
export declare const preferUseDeepCompareMemo: TSESLint.RuleModule<"preferUseDeepCompareMemo", [], TSESLint.RuleListener>;
|
|
4
|
+
export default preferUseDeepCompareMemo;
|
|
@@ -0,0 +1,431 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.preferUseDeepCompareMemo = void 0;
|
|
4
|
+
const utils_1 = require("@typescript-eslint/utils");
|
|
5
|
+
const createRule_1 = require("../utils/createRule");
|
|
6
|
+
// Consider these as memoizing hooks producing stable references
|
|
7
|
+
const MEMOIZING_HOOKS = new Set([
|
|
8
|
+
'useMemo',
|
|
9
|
+
'useCallback',
|
|
10
|
+
'useDeepCompareMemo',
|
|
11
|
+
'useLatestCallback',
|
|
12
|
+
]);
|
|
13
|
+
function isUseMemoCallee(callee) {
|
|
14
|
+
if (callee.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
15
|
+
return callee.name === 'useMemo';
|
|
16
|
+
}
|
|
17
|
+
if (callee.type === utils_1.AST_NODE_TYPES.MemberExpression &&
|
|
18
|
+
!callee.computed &&
|
|
19
|
+
callee.property.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
20
|
+
return callee.property.name === 'useMemo';
|
|
21
|
+
}
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
function isIdentifierMemoizedAbove(name, memoizedIds) {
|
|
25
|
+
return memoizedIds.has(name);
|
|
26
|
+
}
|
|
27
|
+
function containsJsx(node) {
|
|
28
|
+
if (!node)
|
|
29
|
+
return false;
|
|
30
|
+
const stack = [node];
|
|
31
|
+
while (stack.length) {
|
|
32
|
+
const cur = stack.pop();
|
|
33
|
+
if (cur.type === utils_1.AST_NODE_TYPES.JSXElement ||
|
|
34
|
+
cur.type === utils_1.AST_NODE_TYPES.JSXFragment) {
|
|
35
|
+
return true;
|
|
36
|
+
}
|
|
37
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
38
|
+
for (const key of Object.keys(cur)) {
|
|
39
|
+
if (key === 'parent')
|
|
40
|
+
continue;
|
|
41
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
42
|
+
const child = cur[key];
|
|
43
|
+
if (!child)
|
|
44
|
+
continue;
|
|
45
|
+
if (Array.isArray(child)) {
|
|
46
|
+
for (const c of child) {
|
|
47
|
+
if (c && typeof c === 'object' && 'type' in c) {
|
|
48
|
+
stack.push(c);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
else if (typeof child === 'object' && 'type' in child) {
|
|
53
|
+
stack.push(child);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
function isNonPrimitiveWithoutTypes(expr) {
|
|
60
|
+
switch (expr.type) {
|
|
61
|
+
case utils_1.AST_NODE_TYPES.ArrayExpression:
|
|
62
|
+
case utils_1.AST_NODE_TYPES.ObjectExpression:
|
|
63
|
+
case utils_1.AST_NODE_TYPES.FunctionExpression:
|
|
64
|
+
case utils_1.AST_NODE_TYPES.ArrowFunctionExpression:
|
|
65
|
+
case utils_1.AST_NODE_TYPES.NewExpression:
|
|
66
|
+
case utils_1.AST_NODE_TYPES.ClassExpression:
|
|
67
|
+
return true;
|
|
68
|
+
case utils_1.AST_NODE_TYPES.Identifier:
|
|
69
|
+
case utils_1.AST_NODE_TYPES.Literal:
|
|
70
|
+
case utils_1.AST_NODE_TYPES.TemplateLiteral:
|
|
71
|
+
case utils_1.AST_NODE_TYPES.UnaryExpression:
|
|
72
|
+
case utils_1.AST_NODE_TYPES.BinaryExpression:
|
|
73
|
+
case utils_1.AST_NODE_TYPES.LogicalExpression:
|
|
74
|
+
case utils_1.AST_NODE_TYPES.MemberExpression:
|
|
75
|
+
case utils_1.AST_NODE_TYPES.ChainExpression:
|
|
76
|
+
case utils_1.AST_NODE_TYPES.CallExpression:
|
|
77
|
+
default:
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
// TypeScript-aware check. Defensive and conservative.
|
|
82
|
+
function isNonPrimitiveWithTypes(context, expr) {
|
|
83
|
+
const services = context.parserServices;
|
|
84
|
+
if (!services?.program || !services?.esTreeNodeToTSNodeMap) {
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
try {
|
|
88
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
89
|
+
const ts = require('typescript');
|
|
90
|
+
const checker = services.program.getTypeChecker();
|
|
91
|
+
const tsNode = services.esTreeNodeToTSNodeMap.get(expr);
|
|
92
|
+
if (!tsNode)
|
|
93
|
+
return false;
|
|
94
|
+
const type = checker.getTypeAtLocation(tsNode);
|
|
95
|
+
// Only consider explicit functions as non-primitive here
|
|
96
|
+
if (type.getCallSignatures().length > 0)
|
|
97
|
+
return true;
|
|
98
|
+
// Avoid guessing for unknown/any or non-function types
|
|
99
|
+
if (type.flags & (ts.TypeFlags.Any | ts.TypeFlags.Unknown))
|
|
100
|
+
return false;
|
|
101
|
+
return false;
|
|
102
|
+
}
|
|
103
|
+
catch {
|
|
104
|
+
return false;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
function collectMemoizedIdentifiers(context) {
|
|
108
|
+
const memoized = new Set();
|
|
109
|
+
const sourceCode = context.sourceCode;
|
|
110
|
+
const program = sourceCode.ast;
|
|
111
|
+
function visit(node) {
|
|
112
|
+
if (node.type === utils_1.AST_NODE_TYPES.VariableDeclarator) {
|
|
113
|
+
const id = node.id;
|
|
114
|
+
const init = node.init;
|
|
115
|
+
if (!init)
|
|
116
|
+
return;
|
|
117
|
+
if (init.type === utils_1.AST_NODE_TYPES.CallExpression) {
|
|
118
|
+
let calleeName = null;
|
|
119
|
+
if (init.callee.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
120
|
+
calleeName = init.callee.name;
|
|
121
|
+
}
|
|
122
|
+
else if (init.callee.type === utils_1.AST_NODE_TYPES.MemberExpression &&
|
|
123
|
+
!init.callee.computed &&
|
|
124
|
+
init.callee.property.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
125
|
+
calleeName = init.callee.property.name;
|
|
126
|
+
}
|
|
127
|
+
if (calleeName && MEMOIZING_HOOKS.has(calleeName)) {
|
|
128
|
+
if (id.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
129
|
+
memoized.add(id.name);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
135
|
+
for (const key of Object.keys(node)) {
|
|
136
|
+
if (key === 'parent')
|
|
137
|
+
continue;
|
|
138
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
139
|
+
const child = node[key];
|
|
140
|
+
if (!child)
|
|
141
|
+
continue;
|
|
142
|
+
if (Array.isArray(child)) {
|
|
143
|
+
for (const c of child) {
|
|
144
|
+
if (c && typeof c === 'object' && 'type' in c) {
|
|
145
|
+
visit(c);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
else if (typeof child === 'object' && 'type' in child) {
|
|
150
|
+
visit(child);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
visit(program);
|
|
155
|
+
return memoized;
|
|
156
|
+
}
|
|
157
|
+
function ensureDeepCompareImportFixes(context, fixer) {
|
|
158
|
+
const fixes = [];
|
|
159
|
+
const sourceCode = context.sourceCode;
|
|
160
|
+
const program = sourceCode.ast;
|
|
161
|
+
// If already imported anywhere, skip adding
|
|
162
|
+
const hasImport = program.body.some((n) => n.type === utils_1.AST_NODE_TYPES.ImportDeclaration &&
|
|
163
|
+
n.source.value === '@blumintinc/use-deep-compare' &&
|
|
164
|
+
n.specifiers.some((s) => s.type === utils_1.AST_NODE_TYPES.ImportSpecifier &&
|
|
165
|
+
s.imported.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
166
|
+
s.imported.name === 'useDeepCompareMemo'));
|
|
167
|
+
if (hasImport)
|
|
168
|
+
return fixes;
|
|
169
|
+
// Determine insertion point and indentation
|
|
170
|
+
const importDecls = program.body.filter((n) => n.type === utils_1.AST_NODE_TYPES.ImportDeclaration);
|
|
171
|
+
const fullText = sourceCode.getText();
|
|
172
|
+
let importText = `import { useDeepCompareMemo } from '@blumintinc/use-deep-compare';\n`;
|
|
173
|
+
if (importDecls.length === 0) {
|
|
174
|
+
fixes.push(fixer.insertTextBeforeRange([0, 0], importText));
|
|
175
|
+
}
|
|
176
|
+
else {
|
|
177
|
+
const firstImport = importDecls[0];
|
|
178
|
+
const before = fullText.slice(0, firstImport.range[0]);
|
|
179
|
+
const lastNewline = before.lastIndexOf('\n');
|
|
180
|
+
const indent = lastNewline >= 0 ? before.slice(lastNewline + 1) : '';
|
|
181
|
+
importText = `${indent}${importText}`;
|
|
182
|
+
fixes.push(fixer.insertTextBefore(firstImport, importText));
|
|
183
|
+
}
|
|
184
|
+
return fixes;
|
|
185
|
+
}
|
|
186
|
+
function findVariableInScope(scope, name) {
|
|
187
|
+
if (!scope)
|
|
188
|
+
return null;
|
|
189
|
+
const found = scope.variables?.find((v) => v.name === name);
|
|
190
|
+
if (found)
|
|
191
|
+
return found;
|
|
192
|
+
if (Array.isArray(scope.childScopes)) {
|
|
193
|
+
for (const child of scope.childScopes) {
|
|
194
|
+
const v = findVariableInScope(child, name);
|
|
195
|
+
if (v)
|
|
196
|
+
return v;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
return null;
|
|
200
|
+
}
|
|
201
|
+
function isIdentifierReferenced(sourceCode, name) {
|
|
202
|
+
const scope = sourceCode.scopeManager?.globalScope;
|
|
203
|
+
if (!scope)
|
|
204
|
+
return true;
|
|
205
|
+
const variable = findVariableInScope(scope, name);
|
|
206
|
+
if (!variable)
|
|
207
|
+
return false;
|
|
208
|
+
return variable.references.some((ref) => ref.identifier && ref.identifier.name === name);
|
|
209
|
+
}
|
|
210
|
+
function removeImportSpecifierFixes(sourceCode, fixer, importDecl, specifier) {
|
|
211
|
+
const fixes = [];
|
|
212
|
+
if (importDecl.specifiers.length === 1) {
|
|
213
|
+
fixes.push(fixer.remove(importDecl));
|
|
214
|
+
return fixes;
|
|
215
|
+
}
|
|
216
|
+
const tokenAfter = sourceCode.getTokenAfter(specifier);
|
|
217
|
+
const tokenBefore = sourceCode.getTokenBefore(specifier);
|
|
218
|
+
if (tokenAfter && tokenAfter.value === ',') {
|
|
219
|
+
fixes.push(fixer.removeRange([specifier.range[0], tokenAfter.range[1]]));
|
|
220
|
+
}
|
|
221
|
+
else if (tokenBefore && tokenBefore.value === ',') {
|
|
222
|
+
fixes.push(fixer.removeRange([tokenBefore.range[0], specifier.range[1]]));
|
|
223
|
+
}
|
|
224
|
+
else {
|
|
225
|
+
fixes.push(fixer.remove(specifier));
|
|
226
|
+
}
|
|
227
|
+
return fixes;
|
|
228
|
+
}
|
|
229
|
+
function isImportedIdentifier(context, name) {
|
|
230
|
+
const sourceCode = context.sourceCode;
|
|
231
|
+
const program = sourceCode.ast;
|
|
232
|
+
for (const node of program.body) {
|
|
233
|
+
if (node.type === utils_1.AST_NODE_TYPES.ImportDeclaration) {
|
|
234
|
+
for (const spec of node.specifiers) {
|
|
235
|
+
if (spec.type === utils_1.AST_NODE_TYPES.ImportSpecifier ||
|
|
236
|
+
spec.type === utils_1.AST_NODE_TYPES.ImportDefaultSpecifier ||
|
|
237
|
+
spec.type === utils_1.AST_NODE_TYPES.ImportNamespaceSpecifier) {
|
|
238
|
+
if (spec.local.name === name)
|
|
239
|
+
return true;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
return false;
|
|
245
|
+
}
|
|
246
|
+
function identifierUsedAsObjectOrFunction(callback, name) {
|
|
247
|
+
const stack = [callback.body];
|
|
248
|
+
while (stack.length) {
|
|
249
|
+
const node = stack.pop();
|
|
250
|
+
if (node.type === utils_1.AST_NODE_TYPES.MemberExpression) {
|
|
251
|
+
let base = node.object;
|
|
252
|
+
while (base.type === utils_1.AST_NODE_TYPES.MemberExpression) {
|
|
253
|
+
base = base.object;
|
|
254
|
+
}
|
|
255
|
+
if (base.type === utils_1.AST_NODE_TYPES.Identifier && base.name === name) {
|
|
256
|
+
return true; // object/array usage via property access
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
if (node.type === utils_1.AST_NODE_TYPES.ChainExpression) {
|
|
260
|
+
const expr = node.expression;
|
|
261
|
+
if (expr.type === utils_1.AST_NODE_TYPES.MemberExpression) {
|
|
262
|
+
let base = expr.object;
|
|
263
|
+
while (base.type === utils_1.AST_NODE_TYPES.MemberExpression) {
|
|
264
|
+
base = base.object;
|
|
265
|
+
}
|
|
266
|
+
if (base.type === utils_1.AST_NODE_TYPES.Identifier && base.name === name) {
|
|
267
|
+
return true;
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
if (node.type === utils_1.AST_NODE_TYPES.CallExpression) {
|
|
272
|
+
const callee = node.callee;
|
|
273
|
+
if (callee.type === utils_1.AST_NODE_TYPES.Identifier && callee.name === name) {
|
|
274
|
+
return true; // function usage
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
if (node.type === utils_1.AST_NODE_TYPES.JSXSpreadAttribute) {
|
|
278
|
+
if (node.argument.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
279
|
+
node.argument.name === name) {
|
|
280
|
+
return true;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
if (node.type === utils_1.AST_NODE_TYPES.SpreadElement) {
|
|
284
|
+
if (node.argument.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
285
|
+
node.argument.name === name) {
|
|
286
|
+
return true;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
290
|
+
for (const key of Object.keys(node)) {
|
|
291
|
+
if (key === 'parent')
|
|
292
|
+
continue;
|
|
293
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
294
|
+
const child = node[key];
|
|
295
|
+
if (!child)
|
|
296
|
+
continue;
|
|
297
|
+
if (Array.isArray(child)) {
|
|
298
|
+
for (const c of child) {
|
|
299
|
+
if (c && typeof c === 'object' && 'type' in c) {
|
|
300
|
+
stack.push(c);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
else if (typeof child === 'object' && 'type' in child) {
|
|
305
|
+
stack.push(child);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
return false;
|
|
310
|
+
}
|
|
311
|
+
exports.preferUseDeepCompareMemo = (0, createRule_1.createRule)({
|
|
312
|
+
name: 'prefer-use-deep-compare-memo',
|
|
313
|
+
meta: {
|
|
314
|
+
type: 'suggestion',
|
|
315
|
+
docs: {
|
|
316
|
+
description: 'Enforce using useDeepCompareMemo when dependency array contains non-primitive values (objects, arrays, functions) that are not already memoized. This prevents unnecessary re-renders due to reference changes.',
|
|
317
|
+
recommended: 'error',
|
|
318
|
+
},
|
|
319
|
+
fixable: 'code',
|
|
320
|
+
schema: [],
|
|
321
|
+
messages: {
|
|
322
|
+
preferUseDeepCompareMemo: 'Dependency array for "{{hook}}" includes objects/arrays/functions that change identity each render, so React treats them as changed and reruns the memoized computation, triggering avoidable renders. Use useDeepCompareMemo (or memoize those dependencies first) so comparisons use deep equality and the memo stays stable.',
|
|
323
|
+
},
|
|
324
|
+
},
|
|
325
|
+
defaultOptions: [],
|
|
326
|
+
create(context) {
|
|
327
|
+
const memoizedIds = collectMemoizedIdentifiers(context);
|
|
328
|
+
return {
|
|
329
|
+
CallExpression(node) {
|
|
330
|
+
if (!isUseMemoCallee(node.callee))
|
|
331
|
+
return;
|
|
332
|
+
if (node.arguments.length === 0)
|
|
333
|
+
return;
|
|
334
|
+
const callback = node.arguments[0];
|
|
335
|
+
if (callback.type !== utils_1.AST_NODE_TYPES.ArrowFunctionExpression &&
|
|
336
|
+
callback.type !== utils_1.AST_NODE_TYPES.FunctionExpression) {
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
339
|
+
// Ignore if JSX is present inside the memo callback
|
|
340
|
+
if (containsJsx(callback))
|
|
341
|
+
return;
|
|
342
|
+
// Get dependency array (last argument)
|
|
343
|
+
const depsArg = node.arguments[node.arguments.length - 1];
|
|
344
|
+
if (!depsArg || depsArg.type !== utils_1.AST_NODE_TYPES.ArrayExpression)
|
|
345
|
+
return;
|
|
346
|
+
// Empty dependency arrays should be ignored
|
|
347
|
+
if (depsArg.elements.length === 0)
|
|
348
|
+
return;
|
|
349
|
+
// Determine if any dependency is a non-primitive and not already memoized
|
|
350
|
+
let hasUnmemoizedNonPrimitive = false;
|
|
351
|
+
for (const el of depsArg.elements) {
|
|
352
|
+
if (!el)
|
|
353
|
+
continue; // holes
|
|
354
|
+
if (el.type === utils_1.AST_NODE_TYPES.SpreadElement)
|
|
355
|
+
continue;
|
|
356
|
+
const expr = el;
|
|
357
|
+
// TS-aware check first
|
|
358
|
+
let isNonPrimitive = isNonPrimitiveWithTypes(context, expr);
|
|
359
|
+
// Fallback heuristic without type info for literals/arrays/objects/functions
|
|
360
|
+
if (!isNonPrimitive) {
|
|
361
|
+
isNonPrimitive = isNonPrimitiveWithoutTypes(expr);
|
|
362
|
+
}
|
|
363
|
+
// Identifier-specific heuristic: consider non-primitive only if used as object or function in callback
|
|
364
|
+
if (!isNonPrimitive && expr.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
365
|
+
// Imported identifiers are treated as stable
|
|
366
|
+
if (isImportedIdentifier(context, expr.name)) {
|
|
367
|
+
isNonPrimitive = false;
|
|
368
|
+
}
|
|
369
|
+
else if (identifierUsedAsObjectOrFunction(callback, expr.name) &&
|
|
370
|
+
!isIdentifierMemoizedAbove(expr.name, memoizedIds)) {
|
|
371
|
+
isNonPrimitive = true;
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
if (!isNonPrimitive)
|
|
375
|
+
continue;
|
|
376
|
+
// If identifier and memoized above, skip
|
|
377
|
+
if (expr.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
378
|
+
isIdentifierMemoizedAbove(expr.name, memoizedIds)) {
|
|
379
|
+
continue;
|
|
380
|
+
}
|
|
381
|
+
hasUnmemoizedNonPrimitive = true;
|
|
382
|
+
break;
|
|
383
|
+
}
|
|
384
|
+
if (!hasUnmemoizedNonPrimitive)
|
|
385
|
+
return;
|
|
386
|
+
context.report({
|
|
387
|
+
node,
|
|
388
|
+
messageId: 'preferUseDeepCompareMemo',
|
|
389
|
+
data: {
|
|
390
|
+
hook: 'useMemo',
|
|
391
|
+
},
|
|
392
|
+
fix(fixer) {
|
|
393
|
+
const fixes = [];
|
|
394
|
+
// Replace callee
|
|
395
|
+
if (node.callee.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
396
|
+
fixes.push(fixer.replaceText(node.callee, 'useDeepCompareMemo'));
|
|
397
|
+
}
|
|
398
|
+
else if (node.callee.type === utils_1.AST_NODE_TYPES.MemberExpression) {
|
|
399
|
+
fixes.push(fixer.replaceText(node.callee, 'useDeepCompareMemo'));
|
|
400
|
+
}
|
|
401
|
+
// Ensure import exists
|
|
402
|
+
fixes.push(...ensureDeepCompareImportFixes(context, fixer));
|
|
403
|
+
// Clean up now-unused React/useMemo imports if safe
|
|
404
|
+
const sourceCode = context.sourceCode;
|
|
405
|
+
const program = sourceCode.ast;
|
|
406
|
+
const reactImport = program.body.find((n) => n.type === utils_1.AST_NODE_TYPES.ImportDeclaration &&
|
|
407
|
+
n.source.value === 'react');
|
|
408
|
+
if (reactImport) {
|
|
409
|
+
// remove named useMemo if unused
|
|
410
|
+
const useMemoSpec = reactImport.specifiers.find((s) => s.type === utils_1.AST_NODE_TYPES.ImportSpecifier &&
|
|
411
|
+
s.imported.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
412
|
+
s.imported.name === 'useMemo');
|
|
413
|
+
if (useMemoSpec &&
|
|
414
|
+
!isIdentifierReferenced(sourceCode, useMemoSpec.local.name)) {
|
|
415
|
+
fixes.push(...removeImportSpecifierFixes(sourceCode, fixer, reactImport, useMemoSpec));
|
|
416
|
+
}
|
|
417
|
+
const defaultSpec = reactImport.specifiers.find((s) => s.type === utils_1.AST_NODE_TYPES.ImportDefaultSpecifier);
|
|
418
|
+
if (defaultSpec &&
|
|
419
|
+
!isIdentifierReferenced(sourceCode, defaultSpec.local.name)) {
|
|
420
|
+
fixes.push(...removeImportSpecifierFixes(sourceCode, fixer, reactImport, defaultSpec));
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
return fixes;
|
|
424
|
+
},
|
|
425
|
+
});
|
|
426
|
+
},
|
|
427
|
+
};
|
|
428
|
+
},
|
|
429
|
+
});
|
|
430
|
+
exports.default = exports.preferUseDeepCompareMemo;
|
|
431
|
+
//# sourceMappingURL=prefer-use-deep-compare-memo.js.map
|
|
@@ -29,7 +29,7 @@ exports.preferUseCallbackOverUseMemoForFunctions = (0, createRule_1.createRule)(
|
|
|
29
29
|
},
|
|
30
30
|
],
|
|
31
31
|
messages: {
|
|
32
|
-
preferUseCallback: '
|
|
32
|
+
preferUseCallback: 'useMemo returns {{callbackDescription}}, which hides that you are memoizing a callback. useCallback is built to keep function identities stable and signals intent to other hook consumers. Replace useMemo with useCallback and keep the same dependency array so props relying on stable references do not re-render unnecessarily.',
|
|
33
33
|
},
|
|
34
34
|
},
|
|
35
35
|
defaultOptions: [{ allowComplexBodies: false, allowFunctionFactories: true }],
|
|
@@ -144,8 +144,29 @@ exports.preferUseCallbackOverUseMemoForFunctions = (0, createRule_1.createRule)(
|
|
|
144
144
|
};
|
|
145
145
|
},
|
|
146
146
|
});
|
|
147
|
+
function getMemoizedFunctionDescription(node) {
|
|
148
|
+
const parent = node.parent;
|
|
149
|
+
if (parent?.type === utils_1.AST_NODE_TYPES.VariableDeclarator &&
|
|
150
|
+
parent.id.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
151
|
+
return `the callback "${parent.id.name}"`;
|
|
152
|
+
}
|
|
153
|
+
if (parent?.type === utils_1.AST_NODE_TYPES.AssignmentExpression &&
|
|
154
|
+
parent.left.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
155
|
+
return `the callback "${parent.left.name}"`;
|
|
156
|
+
}
|
|
157
|
+
if (parent?.type === utils_1.AST_NODE_TYPES.Property &&
|
|
158
|
+
parent.key.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
159
|
+
return `the "${parent.key.name}" property callback`;
|
|
160
|
+
}
|
|
161
|
+
if (parent?.type === utils_1.AST_NODE_TYPES.JSXExpressionContainer &&
|
|
162
|
+
parent.parent?.type === utils_1.AST_NODE_TYPES.JSXAttribute &&
|
|
163
|
+
parent.parent.name.type === utils_1.AST_NODE_TYPES.JSXIdentifier) {
|
|
164
|
+
return `the "${parent.parent.name.name}" prop callback`;
|
|
165
|
+
}
|
|
166
|
+
return 'this callback value';
|
|
167
|
+
}
|
|
147
168
|
function reportAndFix(node, context) {
|
|
148
|
-
const sourceCode = context.
|
|
169
|
+
const sourceCode = context.sourceCode;
|
|
149
170
|
const useMemoCallback = node.arguments[0];
|
|
150
171
|
const dependencyArray = node.arguments[1]
|
|
151
172
|
? sourceCode.getText(node.arguments[1])
|
|
@@ -168,9 +189,11 @@ function reportAndFix(node, context) {
|
|
|
168
189
|
const typeParametersText = hasTypeParameters
|
|
169
190
|
? sourceCode.getText(node.typeParameters)
|
|
170
191
|
: '';
|
|
192
|
+
const callbackDescription = getMemoizedFunctionDescription(node);
|
|
171
193
|
context.report({
|
|
172
194
|
node,
|
|
173
195
|
messageId: 'preferUseCallback',
|
|
196
|
+
data: { callbackDescription },
|
|
174
197
|
fix: (fixer) => {
|
|
175
198
|
return fixer.replaceText(node, `useCallback${typeParametersText}(${returnedFunctionText}, ${dependencyArray})`);
|
|
176
199
|
},
|