@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
|
@@ -20,7 +20,7 @@ exports.enforceMicrodiff = (0, createRule_1.createRule)({
|
|
|
20
20
|
},
|
|
21
21
|
defaultOptions: [],
|
|
22
22
|
create(context) {
|
|
23
|
-
const sourceCode = context.
|
|
23
|
+
const sourceCode = context.sourceCode;
|
|
24
24
|
const importedDiffLibraries = new Map();
|
|
25
25
|
const importedFunctions = new Map(); // Map of imported function names to their sources
|
|
26
26
|
let hasMicrodiffImport = false;
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.enforceObjectLiteralAsConst = void 0;
|
|
4
4
|
const createRule_1 = require("../utils/createRule");
|
|
5
|
+
const ASTHelpers_1 = require("../utils/ASTHelpers");
|
|
5
6
|
exports.enforceObjectLiteralAsConst = (0, createRule_1.createRule)({
|
|
6
7
|
name: 'enforce-object-literal-as-const',
|
|
7
8
|
meta: {
|
|
@@ -55,10 +56,9 @@ exports.enforceObjectLiteralAsConst = (0, createRule_1.createRule)({
|
|
|
55
56
|
return;
|
|
56
57
|
}
|
|
57
58
|
// Check if the return statement is inside a function
|
|
58
|
-
const sourceCode = context.
|
|
59
|
-
// Use
|
|
60
|
-
|
|
61
|
-
const ancestors = context.getAncestors();
|
|
59
|
+
const sourceCode = context.sourceCode;
|
|
60
|
+
// Use ASTHelpers.getAncestors for ESLint v8/v9 compatibility
|
|
61
|
+
const ancestors = ASTHelpers_1.ASTHelpers.getAncestors(context, node);
|
|
62
62
|
const isInFunction = ancestors.some((ancestor) => ancestor.type === 'FunctionDeclaration' ||
|
|
63
63
|
ancestor.type === 'ArrowFunctionExpression' ||
|
|
64
64
|
ancestor.type === 'FunctionExpression' ||
|
|
@@ -847,6 +847,10 @@ const DIS_EXCEPTIONS = [
|
|
|
847
847
|
'dismissal',
|
|
848
848
|
'disk',
|
|
849
849
|
'disks',
|
|
850
|
+
'dispute',
|
|
851
|
+
'disputed',
|
|
852
|
+
'disputes',
|
|
853
|
+
'disputing',
|
|
850
854
|
];
|
|
851
855
|
// Words that contain negative prefixes but should be treated as valid
|
|
852
856
|
const EXCEPTION_WORDS_SET = new Set([...IN_EXCEPTIONS, ...NO_EXCEPTIONS, ...UN_EXCEPTIONS, ...DIS_EXCEPTIONS].map((word) => word.toLowerCase()));
|
|
@@ -137,7 +137,7 @@ exports.enforcePropsArgumentName = (0, createRule_1.createRule)({
|
|
|
137
137
|
node.parent.type === utils_1.AST_NODE_TYPES.MethodDefinition) {
|
|
138
138
|
return;
|
|
139
139
|
}
|
|
140
|
-
const sourceCode = context.
|
|
140
|
+
const sourceCode = context.sourceCode;
|
|
141
141
|
node.params.forEach((param) => {
|
|
142
142
|
if (isDestructuredParameter(param)) {
|
|
143
143
|
return;
|
|
@@ -173,7 +173,7 @@ exports.enforcePropsArgumentName = (0, createRule_1.createRule)({
|
|
|
173
173
|
// Check class method parameters (including constructors)
|
|
174
174
|
function checkClassMethod(node) {
|
|
175
175
|
const method = node.value;
|
|
176
|
-
const sourceCode = context.
|
|
176
|
+
const sourceCode = context.sourceCode;
|
|
177
177
|
method.params.forEach((param) => {
|
|
178
178
|
if (isDestructuredParameter(param)) {
|
|
179
179
|
return;
|
|
@@ -40,7 +40,7 @@ exports.enforceStableStringify = (0, createRule_1.createRule)({
|
|
|
40
40
|
const fixes = [];
|
|
41
41
|
// Add import if not present
|
|
42
42
|
if (!hasStringifyImport) {
|
|
43
|
-
const program = context.
|
|
43
|
+
const program = context.sourceCode.ast;
|
|
44
44
|
const firstImport = program.body.find((node) => node.type === utils_1.AST_NODE_TYPES.ImportDeclaration);
|
|
45
45
|
const importStatement = "import stringify from 'safe-stable-stringify';\n";
|
|
46
46
|
if (firstImport) {
|
|
@@ -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) {
|
|
35
|
+
const name = getFunctionName(node);
|
|
36
|
+
if (name && /^[A-Z]/.test(name)) {
|
|
37
|
+
return true;
|
|
38
|
+
}
|
|
39
|
+
if (ASTHelpers_1.ASTHelpers.returnsJSX(node.body)) {
|
|
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),
|
|
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 {};
|