@blumintinc/eslint-plugin-blumint 1.13.0 → 1.15.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 +169 -139
- package/lib/index.js +200 -9
- package/lib/rules/array-methods-this-context.js +1 -1
- package/lib/rules/avoid-utils-directory.js +0 -4
- package/lib/rules/class-methods-read-top-to-bottom.js +24 -5
- package/lib/rules/consistent-callback-naming.js +43 -4
- package/lib/rules/dynamic-https-errors.d.ts +1 -1
- package/lib/rules/dynamic-https-errors.js +134 -44
- package/lib/rules/enforce-assert-safe-object-key.js +11 -11
- package/lib/rules/enforce-boolean-naming-prefixes.js +11 -235
- 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-date-ttime.d.ts +1 -0
- package/lib/rules/enforce-date-ttime.js +156 -0
- 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.d.ts +2 -1
- package/lib/rules/enforce-dynamic-firebase-imports.js +3 -2
- 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-f-extension-for-entry-points.d.ts +8 -0
- package/lib/rules/enforce-f-extension-for-entry-points.js +283 -0
- 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 +4 -4
- package/lib/rules/enforce-id-capitalization.js +1 -1
- package/lib/rules/enforce-identifiable-firestore-type.js +108 -58
- package/lib/rules/enforce-memoize-async.js +71 -21
- 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 +44 -18
- 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 +3818 -4644
- 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 +66 -23
- 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 +801 -0
- package/lib/rules/memo-nested-react-components.d.ts +8 -0
- package/lib/rules/memo-nested-react-components.js +415 -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 +215 -12
- package/lib/rules/no-circular-references.d.ts +2 -1
- package/lib/rules/no-circular-references.js +31 -32
- 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 +527 -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 +52 -7
- package/lib/rules/no-excessive-parent-chain.js +4 -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 +465 -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.d.ts +2 -2
- package/lib/rules/no-passthrough-getters.js +120 -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 +508 -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 +344 -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 +143 -59
- 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 +118 -8
- 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 +221 -172
- package/lib/rules/prefer-settings-object.js +27 -10
- package/lib/rules/prefer-type-alias-over-typeof-constant.js +9 -0
- 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-usememo-over-useeffect-usestate.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 +540 -0
- package/lib/rules/react-memoize-literals.d.ts +4 -0
- package/lib/rules/react-memoize-literals.js +614 -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 +155 -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 +18 -10
- 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 +47 -2
- package/lib/utils/ASTHelpers.js +370 -112
- 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
|
@@ -17,7 +17,7 @@ exports.default = (0, createRule_1.createRule)({
|
|
|
17
17
|
meta: {
|
|
18
18
|
type: 'problem',
|
|
19
19
|
docs: {
|
|
20
|
-
description: 'Enforce serializable parameters
|
|
20
|
+
description: 'Enforce serializable parameters for Firebase callable/HTTPS functions.',
|
|
21
21
|
recommended: 'error',
|
|
22
22
|
},
|
|
23
23
|
schema: [
|
|
@@ -38,8 +38,8 @@ exports.default = (0, createRule_1.createRule)({
|
|
|
38
38
|
},
|
|
39
39
|
],
|
|
40
40
|
messages: {
|
|
41
|
-
nonSerializableParam: '
|
|
42
|
-
nonSerializableProperty: '
|
|
41
|
+
nonSerializableParam: 'What\'s wrong: parameter type "{{ type }}" is not JSON-safe for Firebase Callable/HTTPS functions → Why it matters: Firebase transports request payloads as JSON, so {{ type }} values may be coerced, dropped, or lose semantic type in transit → How to fix: convert {{ type }} to a JSON-safe primitive, array, or plain object (e.g., ISO date string, document path string, Map/Set as arrays/objects) before adding it to the request type.',
|
|
42
|
+
nonSerializableProperty: 'What\'s wrong: property "{{ prop }}" uses a non-JSON-safe type "{{ type }}" → Why it matters: Firebase may coerce, drop, or strip semantic type when serializing callable/HTTPS request payloads → How to fix: accept only JSON-safe primitives, arrays, or plain objects, and convert {{ type }} to a safe representation (e.g., Date/Timestamp -> ISO string, DocumentReference -> document path string, Map/Set -> an array or object).',
|
|
43
43
|
},
|
|
44
44
|
},
|
|
45
45
|
defaultOptions: [
|
|
@@ -111,7 +111,7 @@ exports.enforceSingularTypeNames = (0, createRule_1.createRule)({
|
|
|
111
111
|
},
|
|
112
112
|
schema: [],
|
|
113
113
|
messages: {
|
|
114
|
-
typeShouldBeSingular: "Type name '{{name}}'
|
|
114
|
+
typeShouldBeSingular: "Type name '{{name}}' is plural, which signals a collection and hides whether this alias, interface, or enum represents one value or many. Plural type identifiers push callers to misuse the symbol for arrays or maps. Rename it to a singular noun such as '{{suggestedName}}' so the declaration clearly models a single instance and leaves plural names for container types.",
|
|
115
115
|
},
|
|
116
116
|
},
|
|
117
117
|
defaultOptions: [],
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { TSESLint } from '@typescript-eslint/utils';
|
|
2
|
+
type Options = [
|
|
3
|
+
{
|
|
4
|
+
hashImport?: {
|
|
5
|
+
source?: string;
|
|
6
|
+
importName?: string;
|
|
7
|
+
};
|
|
8
|
+
allowedHashFunctions?: string[];
|
|
9
|
+
hookNames?: string[];
|
|
10
|
+
}?
|
|
11
|
+
];
|
|
12
|
+
export declare const enforceStableHashSpreadProps: TSESLint.RuleModule<"wrapSpreadPropsWithStableHash", Options, TSESLint.RuleListener>;
|
|
13
|
+
export {};
|
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.enforceStableHashSpreadProps = void 0;
|
|
4
|
+
const utils_1 = require("@typescript-eslint/utils");
|
|
5
|
+
const ASTHelpers_1 = require("../utils/ASTHelpers");
|
|
6
|
+
const createRule_1 = require("../utils/createRule");
|
|
7
|
+
const DEFAULT_HASH_IMPORT = {
|
|
8
|
+
source: 'functions/src/util/hash/stableHash',
|
|
9
|
+
importName: 'stableHash',
|
|
10
|
+
};
|
|
11
|
+
const DEFAULT_HOOKS = new Set([
|
|
12
|
+
'useEffect',
|
|
13
|
+
'useLayoutEffect',
|
|
14
|
+
'useCallback',
|
|
15
|
+
'useInsertionEffect',
|
|
16
|
+
]);
|
|
17
|
+
const IGNORED_MEMO_HOOKS = new Set(['useMemo', 'useDeepCompareMemo']);
|
|
18
|
+
function getFunctionName(node) {
|
|
19
|
+
if ('id' in node && node.id?.name) {
|
|
20
|
+
return node.id.name;
|
|
21
|
+
}
|
|
22
|
+
if (node.type === utils_1.AST_NODE_TYPES.FunctionExpression &&
|
|
23
|
+
node.parent?.type === utils_1.AST_NODE_TYPES.VariableDeclarator &&
|
|
24
|
+
node.parent.id.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
25
|
+
return node.parent.id.name;
|
|
26
|
+
}
|
|
27
|
+
if (node.type === utils_1.AST_NODE_TYPES.ArrowFunctionExpression &&
|
|
28
|
+
node.parent?.type === utils_1.AST_NODE_TYPES.VariableDeclarator &&
|
|
29
|
+
node.parent.id.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
30
|
+
return node.parent.id.name;
|
|
31
|
+
}
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
function isProbablyComponent(node, context) {
|
|
35
|
+
const name = getFunctionName(node);
|
|
36
|
+
if (name && /^[A-Z]/.test(name)) {
|
|
37
|
+
return true;
|
|
38
|
+
}
|
|
39
|
+
if (ASTHelpers_1.ASTHelpers.returnsJSX(node.body, context)) {
|
|
40
|
+
return true;
|
|
41
|
+
}
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
function collectRestNamesFromPattern(pattern, restNames) {
|
|
45
|
+
if (pattern.type === utils_1.AST_NODE_TYPES.ObjectPattern) {
|
|
46
|
+
for (const prop of pattern.properties) {
|
|
47
|
+
if (prop.type === utils_1.AST_NODE_TYPES.RestElement &&
|
|
48
|
+
prop.argument.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
49
|
+
restNames.add(prop.argument.name);
|
|
50
|
+
}
|
|
51
|
+
else if (prop.type === utils_1.AST_NODE_TYPES.Property &&
|
|
52
|
+
prop.value.type === utils_1.AST_NODE_TYPES.ObjectPattern) {
|
|
53
|
+
collectRestNamesFromPattern(prop.value, restNames);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
else if (pattern.type === utils_1.AST_NODE_TYPES.RestElement) {
|
|
58
|
+
if (pattern.argument.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
59
|
+
restNames.add(pattern.argument.name);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
else if (pattern.type === utils_1.AST_NODE_TYPES.AssignmentPattern) {
|
|
63
|
+
collectRestNamesFromPattern(pattern.left, restNames);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
function collectPropsIdentifiersFromParam(param, propsIdentifiers) {
|
|
67
|
+
if (param.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
68
|
+
propsIdentifiers.add(param.name);
|
|
69
|
+
}
|
|
70
|
+
else if (param.type === utils_1.AST_NODE_TYPES.AssignmentPattern &&
|
|
71
|
+
param.left.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
72
|
+
propsIdentifiers.add(param.left.name);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
function getHookName(node) {
|
|
76
|
+
const callee = node.callee;
|
|
77
|
+
if (callee.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
78
|
+
return callee.name;
|
|
79
|
+
}
|
|
80
|
+
if (callee.type === utils_1.AST_NODE_TYPES.MemberExpression &&
|
|
81
|
+
!callee.computed &&
|
|
82
|
+
callee.property.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
83
|
+
return callee.property.name;
|
|
84
|
+
}
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
function stripTypeWrappers(expression) {
|
|
88
|
+
if (expression.type === utils_1.AST_NODE_TYPES.TSNonNullExpression) {
|
|
89
|
+
return stripTypeWrappers(expression.expression);
|
|
90
|
+
}
|
|
91
|
+
if (expression.type === utils_1.AST_NODE_TYPES.TSAsExpression) {
|
|
92
|
+
return stripTypeWrappers(expression.expression);
|
|
93
|
+
}
|
|
94
|
+
if (expression.type === utils_1.AST_NODE_TYPES.TSTypeAssertion) {
|
|
95
|
+
return stripTypeWrappers(expression.expression);
|
|
96
|
+
}
|
|
97
|
+
if (expression.type === utils_1.AST_NODE_TYPES.ChainExpression) {
|
|
98
|
+
return stripTypeWrappers(expression.expression);
|
|
99
|
+
}
|
|
100
|
+
if (expression.type === 'ParenthesizedExpression') {
|
|
101
|
+
return stripTypeWrappers(expression.expression);
|
|
102
|
+
}
|
|
103
|
+
return expression;
|
|
104
|
+
}
|
|
105
|
+
function isWrappedWithAllowedHash(expression, allowedHashes) {
|
|
106
|
+
const unwrapped = stripTypeWrappers(expression);
|
|
107
|
+
if (unwrapped.type === utils_1.AST_NODE_TYPES.CallExpression) {
|
|
108
|
+
const callee = unwrapped.callee;
|
|
109
|
+
if (callee.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
110
|
+
allowedHashes.has(callee.name)) {
|
|
111
|
+
return true;
|
|
112
|
+
}
|
|
113
|
+
if (callee.type === utils_1.AST_NODE_TYPES.MemberExpression &&
|
|
114
|
+
!callee.computed &&
|
|
115
|
+
callee.property.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
116
|
+
allowedHashes.has(callee.property.name)) {
|
|
117
|
+
return true;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return false;
|
|
121
|
+
}
|
|
122
|
+
function getIdentifierFromExpression(expression) {
|
|
123
|
+
const unwrapped = stripTypeWrappers(expression);
|
|
124
|
+
if (unwrapped.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
125
|
+
return unwrapped;
|
|
126
|
+
}
|
|
127
|
+
return null;
|
|
128
|
+
}
|
|
129
|
+
function getStableHashLocalNames(sourceCode, hashImport) {
|
|
130
|
+
const localNames = [];
|
|
131
|
+
const program = sourceCode.ast;
|
|
132
|
+
for (const node of program.body) {
|
|
133
|
+
if (node.type === utils_1.AST_NODE_TYPES.ImportDeclaration &&
|
|
134
|
+
node.source.value === hashImport.source) {
|
|
135
|
+
for (const spec of node.specifiers) {
|
|
136
|
+
if (spec.type === utils_1.AST_NODE_TYPES.ImportSpecifier &&
|
|
137
|
+
spec.imported.name === hashImport.importName) {
|
|
138
|
+
localNames.push(spec.local.name);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
return localNames;
|
|
144
|
+
}
|
|
145
|
+
function isStableHashImported(sourceCode, hashImport) {
|
|
146
|
+
return getStableHashLocalNames(sourceCode, hashImport).length > 0;
|
|
147
|
+
}
|
|
148
|
+
function getIndentBeforeNode(sourceCode, node) {
|
|
149
|
+
const lineText = sourceCode.lines[node.loc.start.line - 1] ?? '';
|
|
150
|
+
const match = lineText.match(/^[ \t]*/);
|
|
151
|
+
return match ? match[0] : '';
|
|
152
|
+
}
|
|
153
|
+
function hasExhaustiveDepsDisable(sourceCode, callNode, depsNode) {
|
|
154
|
+
const [start, end] = [callNode.range[0], depsNode.range[1]];
|
|
155
|
+
const callStartLine = callNode.loc.start.line;
|
|
156
|
+
const depsStartLine = depsNode.loc.start.line;
|
|
157
|
+
return sourceCode
|
|
158
|
+
.getAllComments()
|
|
159
|
+
.some((comment) => comment.value.includes('react-hooks/exhaustive-deps') &&
|
|
160
|
+
((comment.range[0] >= start && comment.range[1] <= end) ||
|
|
161
|
+
comment.loc.end.line === callStartLine - 1 ||
|
|
162
|
+
comment.loc.end.line === depsStartLine - 1));
|
|
163
|
+
}
|
|
164
|
+
exports.enforceStableHashSpreadProps = (0, createRule_1.createRule)({
|
|
165
|
+
name: 'enforce-stable-hash-spread-props',
|
|
166
|
+
meta: {
|
|
167
|
+
type: 'suggestion',
|
|
168
|
+
docs: {
|
|
169
|
+
description: 'Require stableHash wrapping when spread props rest objects are used in React hook dependency arrays to avoid re-renders triggered by new object references on every render.',
|
|
170
|
+
recommended: 'error',
|
|
171
|
+
},
|
|
172
|
+
fixable: 'code',
|
|
173
|
+
schema: [
|
|
174
|
+
{
|
|
175
|
+
type: 'object',
|
|
176
|
+
properties: {
|
|
177
|
+
hashImport: {
|
|
178
|
+
type: 'object',
|
|
179
|
+
properties: {
|
|
180
|
+
source: { type: 'string' },
|
|
181
|
+
importName: { type: 'string' },
|
|
182
|
+
},
|
|
183
|
+
additionalProperties: false,
|
|
184
|
+
},
|
|
185
|
+
allowedHashFunctions: {
|
|
186
|
+
type: 'array',
|
|
187
|
+
items: { type: 'string' },
|
|
188
|
+
},
|
|
189
|
+
hookNames: {
|
|
190
|
+
type: 'array',
|
|
191
|
+
items: { type: 'string' },
|
|
192
|
+
},
|
|
193
|
+
},
|
|
194
|
+
additionalProperties: false,
|
|
195
|
+
},
|
|
196
|
+
],
|
|
197
|
+
messages: {
|
|
198
|
+
wrapSpreadPropsWithStableHash: 'Rest props object(s) "{{names}}" are recreated on every render, so using them directly in a dependency array makes React rerun the hook on every render. Wrap each in stableHash() (or a memoized hash) and depend on that stable value instead to avoid noisy re-renders.',
|
|
199
|
+
},
|
|
200
|
+
},
|
|
201
|
+
defaultOptions: [{}],
|
|
202
|
+
create(context) {
|
|
203
|
+
const sourceCode = context.sourceCode ??
|
|
204
|
+
context.getSourceCode();
|
|
205
|
+
const [options = {}] = context.options;
|
|
206
|
+
const hashImport = {
|
|
207
|
+
source: options.hashImport?.source ?? DEFAULT_HASH_IMPORT.source,
|
|
208
|
+
importName: options.hashImport?.importName ?? DEFAULT_HASH_IMPORT.importName,
|
|
209
|
+
};
|
|
210
|
+
const existingHashLocalNames = getStableHashLocalNames(sourceCode, hashImport);
|
|
211
|
+
const userHookNames = new Set(options.hookNames ?? []);
|
|
212
|
+
const allowedHashes = new Set([
|
|
213
|
+
...existingHashLocalNames,
|
|
214
|
+
hashImport.importName,
|
|
215
|
+
...(options.allowedHashFunctions ?? []),
|
|
216
|
+
]);
|
|
217
|
+
const hookNames = new Set([...DEFAULT_HOOKS, ...userHookNames]);
|
|
218
|
+
let importPlanned = false;
|
|
219
|
+
const hashIdentifier = existingHashLocalNames[0] ?? hashImport.importName;
|
|
220
|
+
const functionStack = [];
|
|
221
|
+
function getCurrentComponentContext() {
|
|
222
|
+
for (let i = functionStack.length - 1; i >= 0; i -= 1) {
|
|
223
|
+
if (functionStack[i].isComponent) {
|
|
224
|
+
return functionStack[i];
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
return undefined;
|
|
228
|
+
}
|
|
229
|
+
return {
|
|
230
|
+
':function'(node) {
|
|
231
|
+
const restNames = new Set();
|
|
232
|
+
const propsIdentifiers = new Set();
|
|
233
|
+
for (const param of node.params) {
|
|
234
|
+
collectRestNamesFromPattern(param, restNames);
|
|
235
|
+
collectPropsIdentifiersFromParam(param, propsIdentifiers);
|
|
236
|
+
}
|
|
237
|
+
functionStack.push({
|
|
238
|
+
node,
|
|
239
|
+
isComponent: isProbablyComponent(node, context),
|
|
240
|
+
restNames,
|
|
241
|
+
propsIdentifiers,
|
|
242
|
+
});
|
|
243
|
+
},
|
|
244
|
+
'FunctionDeclaration:exit'() {
|
|
245
|
+
functionStack.pop();
|
|
246
|
+
},
|
|
247
|
+
'FunctionExpression:exit'() {
|
|
248
|
+
functionStack.pop();
|
|
249
|
+
},
|
|
250
|
+
'ArrowFunctionExpression:exit'() {
|
|
251
|
+
functionStack.pop();
|
|
252
|
+
},
|
|
253
|
+
VariableDeclarator(node) {
|
|
254
|
+
const current = getCurrentComponentContext();
|
|
255
|
+
if (!current || !current.isComponent)
|
|
256
|
+
return;
|
|
257
|
+
if (node.id.type === utils_1.AST_NODE_TYPES.ObjectPattern &&
|
|
258
|
+
node.init &&
|
|
259
|
+
node.init.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
260
|
+
current.propsIdentifiers.has(node.init.name)) {
|
|
261
|
+
collectRestNamesFromPattern(node.id, current.restNames);
|
|
262
|
+
}
|
|
263
|
+
},
|
|
264
|
+
CallExpression(node) {
|
|
265
|
+
const current = getCurrentComponentContext();
|
|
266
|
+
if (!current || !current.isComponent)
|
|
267
|
+
return;
|
|
268
|
+
const hookName = getHookName(node);
|
|
269
|
+
if (!hookName || !hookNames.has(hookName))
|
|
270
|
+
return;
|
|
271
|
+
if (IGNORED_MEMO_HOOKS.has(hookName) && !userHookNames.has(hookName))
|
|
272
|
+
return;
|
|
273
|
+
if (node.arguments.length < 2)
|
|
274
|
+
return;
|
|
275
|
+
const depsArg = node.arguments[node.arguments.length - 1];
|
|
276
|
+
if (depsArg.type !== utils_1.AST_NODE_TYPES.ArrayExpression)
|
|
277
|
+
return;
|
|
278
|
+
const offendingElements = [];
|
|
279
|
+
for (const element of depsArg.elements) {
|
|
280
|
+
if (!element || element.type === utils_1.AST_NODE_TYPES.SpreadElement) {
|
|
281
|
+
continue;
|
|
282
|
+
}
|
|
283
|
+
if (isWrappedWithAllowedHash(element, allowedHashes)) {
|
|
284
|
+
continue;
|
|
285
|
+
}
|
|
286
|
+
const identifier = getIdentifierFromExpression(element);
|
|
287
|
+
if (!identifier) {
|
|
288
|
+
continue;
|
|
289
|
+
}
|
|
290
|
+
if (current.restNames.has(identifier.name)) {
|
|
291
|
+
offendingElements.push({ node: element, name: identifier.name });
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
if (offendingElements.length === 0)
|
|
295
|
+
return;
|
|
296
|
+
const offendingNames = Array.from(new Set(offendingElements.map(({ name }) => name)));
|
|
297
|
+
context.report({
|
|
298
|
+
node: depsArg,
|
|
299
|
+
messageId: 'wrapSpreadPropsWithStableHash',
|
|
300
|
+
data: { names: offendingNames.join(', ') },
|
|
301
|
+
fix(fixer) {
|
|
302
|
+
const fixes = [];
|
|
303
|
+
const seen = new Set();
|
|
304
|
+
for (const { node: targetNode } of offendingElements) {
|
|
305
|
+
if (seen.has(targetNode.range[0]))
|
|
306
|
+
continue;
|
|
307
|
+
seen.add(targetNode.range[0]);
|
|
308
|
+
const original = sourceCode.getText(targetNode);
|
|
309
|
+
fixes.push(fixer.replaceText(targetNode, `${hashIdentifier}(${original})`));
|
|
310
|
+
}
|
|
311
|
+
if (!isStableHashImported(sourceCode, hashImport) &&
|
|
312
|
+
!importPlanned) {
|
|
313
|
+
const importText = `import { ${hashImport.importName} } from '${hashImport.source}';\n`;
|
|
314
|
+
const program = sourceCode.ast;
|
|
315
|
+
const firstImport = program.body.find((n) => n.type === utils_1.AST_NODE_TYPES.ImportDeclaration);
|
|
316
|
+
if (firstImport) {
|
|
317
|
+
fixes.push(fixer.insertTextBefore(firstImport, importText));
|
|
318
|
+
}
|
|
319
|
+
else {
|
|
320
|
+
fixes.push(fixer.insertTextBeforeRange([0, 0], importText));
|
|
321
|
+
}
|
|
322
|
+
importPlanned = true;
|
|
323
|
+
}
|
|
324
|
+
if (!hasExhaustiveDepsDisable(sourceCode, node, depsArg)) {
|
|
325
|
+
const indent = getIndentBeforeNode(sourceCode, depsArg);
|
|
326
|
+
const tokenBefore = sourceCode.getTokenBefore(depsArg, {
|
|
327
|
+
includeComments: true,
|
|
328
|
+
});
|
|
329
|
+
const needsLeadingNewline = tokenBefore?.loc.end.line === depsArg.loc.start.line;
|
|
330
|
+
const commentText = needsLeadingNewline
|
|
331
|
+
? `\n${indent}// eslint-disable-next-line react-hooks/exhaustive-deps\n${indent}`
|
|
332
|
+
: `// eslint-disable-next-line react-hooks/exhaustive-deps\n${indent}`;
|
|
333
|
+
fixes.push(fixer.insertTextBefore(depsArg, commentText));
|
|
334
|
+
}
|
|
335
|
+
return fixes;
|
|
336
|
+
},
|
|
337
|
+
});
|
|
338
|
+
},
|
|
339
|
+
};
|
|
340
|
+
},
|
|
341
|
+
});
|
|
342
|
+
//# sourceMappingURL=enforce-stable-hash-spread-props.js.map
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { TSESLint } from '@typescript-eslint/utils';
|
|
2
|
+
type RuleOptions = [
|
|
3
|
+
{
|
|
4
|
+
allowInTests?: boolean;
|
|
5
|
+
allow?: string[];
|
|
6
|
+
}?
|
|
7
|
+
];
|
|
8
|
+
export declare const enforceStorageContext: TSESLint.RuleModule<"useStorageContext", RuleOptions, TSESLint.RuleListener>;
|
|
9
|
+
export {};
|