@blumintinc/eslint-plugin-blumint 1.12.6 → 1.13.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 +140 -99
- package/lib/index.js +111 -17
- package/lib/rules/array-methods-this-context.js +20 -2
- package/lib/rules/avoid-utils-directory.js +11 -2
- package/lib/rules/dynamic-https-errors.d.ts +3 -1
- package/lib/rules/dynamic-https-errors.js +48 -10
- 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} +51 -95
- 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 +832 -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 +4 -1
- package/lib/rules/enforce-console-error.d.ts +3 -0
- package/lib/rules/enforce-console-error.js +250 -0
- package/lib/rules/enforce-css-media-queries.js +24 -46
- package/lib/rules/enforce-dynamic-firebase-imports.js +100 -19
- package/lib/rules/enforce-dynamic-imports.js +1 -1
- package/lib/rules/enforce-exported-function-types.js +3 -3
- package/lib/rules/enforce-fieldpath-syntax-in-docsetter.d.ts +2 -0
- package/lib/rules/enforce-fieldpath-syntax-in-docsetter.js +212 -0
- package/lib/rules/enforce-firestore-doc-ref-generic.js +335 -0
- package/lib/rules/enforce-firestore-facade.js +347 -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-global-constants.d.ts +4 -1
- package/lib/rules/enforce-global-constants.js +240 -12
- package/lib/rules/enforce-memoize-async.d.ts +2 -1
- package/lib/rules/enforce-memoize-async.js +96 -27
- package/lib/rules/enforce-memoize-getters.d.ts +3 -0
- package/lib/rules/enforce-memoize-getters.js +123 -0
- package/lib/rules/enforce-microdiff.js +2 -1
- package/lib/rules/enforce-positive-naming.js +5 -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-singular-type-names.js +9 -5
- package/lib/rules/enforce-typescript-markdown-code-blocks.d.ts +1 -0
- package/lib/rules/enforce-typescript-markdown-code-blocks.js +96 -0
- package/lib/rules/enforce-verb-noun-naming.js +85 -10
- package/lib/rules/extract-global-constants.js +57 -45
- package/lib/rules/fast-deep-equal-over-microdiff.d.ts +2 -1
- package/lib/rules/fast-deep-equal-over-microdiff.js +356 -78
- package/lib/rules/firestore-transaction-reads-before-writes.d.ts +1 -0
- package/lib/rules/firestore-transaction-reads-before-writes.js +231 -0
- package/lib/rules/generic-starts-with-t.js +8 -2
- 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-array-length-in-deps.d.ts +12 -0
- package/lib/rules/no-array-length-in-deps.js +308 -0
- package/lib/rules/no-async-array-filter.js +5 -2
- package/lib/rules/no-complex-cloud-params.js +15 -2
- package/lib/rules/no-compositing-layer-props.js +4 -2
- package/lib/rules/no-entire-object-hook-deps.d.ts +3 -1
- package/lib/rules/no-entire-object-hook-deps.js +317 -63
- 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-firestore-object-arrays.js +345 -30
- package/lib/rules/no-hungarian.js +13 -13
- package/lib/rules/no-margin-properties.d.ts +7 -1
- package/lib/rules/no-margin-properties.js +16 -3
- package/lib/rules/no-misleading-boolean-prefixes.d.ts +7 -0
- package/lib/rules/no-misleading-boolean-prefixes.js +327 -0
- package/lib/rules/no-overridable-method-calls-in-constructor.d.ts +1 -0
- package/lib/rules/no-overridable-method-calls-in-constructor.js +260 -0
- package/lib/rules/no-passthrough-getters.d.ts +4 -0
- package/lib/rules/no-passthrough-getters.js +170 -0
- package/lib/rules/no-redundant-usecallback-wrapper.d.ts +8 -0
- package/lib/rules/no-redundant-usecallback-wrapper.js +315 -0
- package/lib/rules/no-restricted-properties-fix.d.ts +12 -0
- package/lib/rules/no-restricted-properties-fix.js +172 -0
- package/lib/rules/no-separate-loading-state.d.ts +8 -0
- package/lib/rules/no-separate-loading-state.js +142 -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 +6 -0
- package/lib/rules/no-undefined-null-passthrough.js +166 -19
- package/lib/rules/no-unnecessary-verb-suffix.js +0 -2
- package/lib/rules/no-unsafe-firestore-spread.js +8 -5
- package/lib/rules/no-unused-props.d.ts +3 -0
- package/lib/rules/no-unused-props.js +213 -78
- package/lib/rules/optimize-object-boolean-conditions.d.ts +1 -0
- package/lib/rules/optimize-object-boolean-conditions.js +234 -0
- package/lib/rules/parallelize-async-operations.d.ts +8 -0
- package/lib/rules/parallelize-async-operations.js +375 -0
- package/lib/rules/prefer-batch-operations.js +199 -33
- package/lib/rules/prefer-block-comments-for-declarations.js +13 -3
- 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 +250 -0
- package/lib/rules/prefer-fragment-component.js +3 -3
- package/lib/rules/prefer-global-router-state-key.d.ts +7 -3
- package/lib/rules/prefer-global-router-state-key.js +230 -31
- package/lib/rules/prefer-next-dynamic.d.ts +9 -0
- package/lib/rules/prefer-next-dynamic.js +366 -0
- package/lib/rules/prefer-nullish-coalescing-boolean-props.d.ts +1 -0
- package/lib/rules/prefer-nullish-coalescing-boolean-props.js +430 -0
- package/lib/rules/prefer-nullish-coalescing-override.d.ts +6 -0
- package/lib/rules/prefer-nullish-coalescing-override.js +182 -0
- package/lib/rules/prefer-params-over-parent-id.d.ts +5 -0
- package/lib/rules/prefer-params-over-parent-id.js +228 -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-url-tostring-over-tojson.d.ts +2 -0
- package/lib/rules/prefer-url-tostring-over-tojson.js +166 -0
- package/lib/rules/prefer-use-deep-compare-memo.d.ts +4 -0
- package/lib/rules/prefer-use-deep-compare-memo.js +428 -0
- package/lib/rules/prefer-usecallback-over-usememo-for-functions.js +24 -1
- package/lib/rules/prefer-usememo-over-useeffect-usestate.js +24 -5
- 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-image-optimized.js +1 -1
- package/lib/rules/require-memo.js +11 -1
- package/lib/rules/require-usememo-object-literals.js +7 -1
- 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 +270 -0
- package/lib/utils/ASTHelpers.js +10 -0
- package/lib/utils/createRule.js +1 -1
- package/lib/utils/graph/ClassGraphSorterReadability.js +1 -1
- package/lib/utils/ruleTester.d.ts +1 -0
- package/lib/utils/ruleTester.js +4 -1
- package/package.json +21 -14
- package/lib/rules/enforce-assertSafe-object-key.d.ts +0 -2
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.firestoreTransactionReadsBeforeWrites = void 0;
|
|
4
|
+
const utils_1 = require("@typescript-eslint/utils");
|
|
5
|
+
const createRule_1 = require("../utils/createRule");
|
|
6
|
+
// Define the operations that are considered reads and writes
|
|
7
|
+
const READ_OPERATIONS = new Set(['get']);
|
|
8
|
+
const WRITE_OPERATIONS = new Set(['set', 'update', 'delete']);
|
|
9
|
+
exports.firestoreTransactionReadsBeforeWrites = (0, createRule_1.createRule)({
|
|
10
|
+
name: 'firestore-transaction-reads-before-writes',
|
|
11
|
+
meta: {
|
|
12
|
+
type: 'problem',
|
|
13
|
+
docs: {
|
|
14
|
+
description: 'Enforce that all Firestore transaction read operations are performed before any write operations',
|
|
15
|
+
recommended: 'error',
|
|
16
|
+
},
|
|
17
|
+
schema: [],
|
|
18
|
+
messages: {
|
|
19
|
+
readsAfterWrites: 'Firestore transaction read operations must be performed before any write operations. Move this read operation before any write operations.',
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
defaultOptions: [],
|
|
23
|
+
create(context) {
|
|
24
|
+
// Track transaction objects and their operations
|
|
25
|
+
const transactionScopes = new Map();
|
|
26
|
+
// Helper to check if a node is a transaction parameter
|
|
27
|
+
function isTransactionParameter(param) {
|
|
28
|
+
if (param.type !== utils_1.AST_NODE_TYPES.Identifier)
|
|
29
|
+
return false;
|
|
30
|
+
// Check for type annotation
|
|
31
|
+
const typeAnnotation = param.typeAnnotation;
|
|
32
|
+
if (!typeAnnotation ||
|
|
33
|
+
typeAnnotation.type !== utils_1.AST_NODE_TYPES.TSTypeAnnotation) {
|
|
34
|
+
// If no type annotation, check the parameter name for common patterns
|
|
35
|
+
// Include 't' as a valid transaction parameter name
|
|
36
|
+
return /^(transaction|tx|t)$/i.test(param.name);
|
|
37
|
+
}
|
|
38
|
+
const type = typeAnnotation.typeAnnotation;
|
|
39
|
+
// Check for Transaction type
|
|
40
|
+
if (type.type === utils_1.AST_NODE_TYPES.TSTypeReference) {
|
|
41
|
+
const typeName = type.typeName;
|
|
42
|
+
// Check for FirebaseFirestore.Transaction
|
|
43
|
+
if (typeName.type === utils_1.AST_NODE_TYPES.TSQualifiedName) {
|
|
44
|
+
return (typeName.left.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
45
|
+
typeName.left.name === 'FirebaseFirestore' &&
|
|
46
|
+
typeName.right.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
47
|
+
typeName.right.name === 'Transaction');
|
|
48
|
+
}
|
|
49
|
+
// Check for Transaction or FirestoreTransaction
|
|
50
|
+
if (typeName.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
51
|
+
return /Transaction/i.test(typeName.name);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
56
|
+
// Helper to find the transaction scope for a node
|
|
57
|
+
function findTransactionScope(node) {
|
|
58
|
+
let current = node;
|
|
59
|
+
while (current) {
|
|
60
|
+
// Check if this node is a transaction scope
|
|
61
|
+
if (transactionScopes.has(current)) {
|
|
62
|
+
return current;
|
|
63
|
+
}
|
|
64
|
+
// Check if this is a function with a transaction parameter
|
|
65
|
+
if (current.type === utils_1.AST_NODE_TYPES.FunctionDeclaration ||
|
|
66
|
+
current.type === utils_1.AST_NODE_TYPES.FunctionExpression ||
|
|
67
|
+
current.type === utils_1.AST_NODE_TYPES.ArrowFunctionExpression) {
|
|
68
|
+
const params = current.params;
|
|
69
|
+
if (params.some(isTransactionParameter)) {
|
|
70
|
+
// If this function has a transaction parameter, track it
|
|
71
|
+
if (!transactionScopes.has(current.body)) {
|
|
72
|
+
const transactionParam = params.find(isTransactionParameter);
|
|
73
|
+
const transactionVariables = new Set();
|
|
74
|
+
if (transactionParam &&
|
|
75
|
+
transactionParam.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
76
|
+
transactionVariables.add(transactionParam.name);
|
|
77
|
+
}
|
|
78
|
+
transactionScopes.set(current.body, {
|
|
79
|
+
hasWriteOperation: false,
|
|
80
|
+
writeNodes: [],
|
|
81
|
+
transactionVariables,
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
return current.body;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
current = current.parent;
|
|
88
|
+
}
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
// Helper to check if a node is a transaction method call
|
|
92
|
+
function isTransactionMethodCall(node, transactionScope) {
|
|
93
|
+
const callee = node.callee;
|
|
94
|
+
if (callee.type !== utils_1.AST_NODE_TYPES.MemberExpression) {
|
|
95
|
+
return { isRead: false, isWrite: false, methodName: null };
|
|
96
|
+
}
|
|
97
|
+
const object = callee.object;
|
|
98
|
+
const property = callee.property;
|
|
99
|
+
// Check if the object is a transaction
|
|
100
|
+
if (object.type !== utils_1.AST_NODE_TYPES.Identifier) {
|
|
101
|
+
return { isRead: false, isWrite: false, methodName: null };
|
|
102
|
+
}
|
|
103
|
+
// If we have a transaction scope, check if the object name is a known transaction variable
|
|
104
|
+
if (transactionScope) {
|
|
105
|
+
const scopeInfo = transactionScopes.get(transactionScope);
|
|
106
|
+
if (scopeInfo && !scopeInfo.transactionVariables.has(object.name)) {
|
|
107
|
+
return { isRead: false, isWrite: false, methodName: null };
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
else {
|
|
111
|
+
// Fallback to general pattern matching
|
|
112
|
+
if (!/^(transaction|tx|t)$/i.test(object.name)) {
|
|
113
|
+
return { isRead: false, isWrite: false, methodName: null };
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
let methodName = null;
|
|
117
|
+
// Check if the property is a read or write operation
|
|
118
|
+
if (property.type === utils_1.AST_NODE_TYPES.Identifier && !callee.computed) {
|
|
119
|
+
// Normal property access: transaction.get()
|
|
120
|
+
methodName = property.name;
|
|
121
|
+
}
|
|
122
|
+
else if (callee.computed &&
|
|
123
|
+
property.type === utils_1.AST_NODE_TYPES.Literal &&
|
|
124
|
+
typeof property.value === 'string') {
|
|
125
|
+
// Computed property access with string literal: transaction['get']
|
|
126
|
+
methodName = property.value;
|
|
127
|
+
}
|
|
128
|
+
else if (callee.computed &&
|
|
129
|
+
property.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
130
|
+
// Computed property access with variable: transaction[methodName]
|
|
131
|
+
// This is tricky to analyze statically. For now, we'll be conservative
|
|
132
|
+
// and assume it could be any method. We'll handle this in the caller.
|
|
133
|
+
return { isRead: true, isWrite: true, methodName: null }; // Could be either - let caller decide
|
|
134
|
+
}
|
|
135
|
+
if (!methodName) {
|
|
136
|
+
return { isRead: false, isWrite: false, methodName: null };
|
|
137
|
+
}
|
|
138
|
+
const isRead = READ_OPERATIONS.has(methodName);
|
|
139
|
+
const isWrite = WRITE_OPERATIONS.has(methodName);
|
|
140
|
+
return { isRead, isWrite, methodName };
|
|
141
|
+
}
|
|
142
|
+
return {
|
|
143
|
+
// Track transaction scopes from runTransaction calls
|
|
144
|
+
'CallExpression[callee.property.name="runTransaction"]'(node) {
|
|
145
|
+
const callback = node.arguments[0];
|
|
146
|
+
if (callback &&
|
|
147
|
+
(callback.type === utils_1.AST_NODE_TYPES.ArrowFunctionExpression ||
|
|
148
|
+
callback.type === utils_1.AST_NODE_TYPES.FunctionExpression)) {
|
|
149
|
+
const transactionParam = callback.params.find(isTransactionParameter);
|
|
150
|
+
const transactionVariables = new Set();
|
|
151
|
+
if (transactionParam &&
|
|
152
|
+
transactionParam.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
153
|
+
transactionVariables.add(transactionParam.name);
|
|
154
|
+
}
|
|
155
|
+
transactionScopes.set(callback.body, {
|
|
156
|
+
hasWriteOperation: false,
|
|
157
|
+
writeNodes: [],
|
|
158
|
+
transactionVariables,
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
},
|
|
162
|
+
// Clean up transaction scopes when exiting runTransaction calls
|
|
163
|
+
'CallExpression[callee.property.name="runTransaction"]:exit'(node) {
|
|
164
|
+
const callback = node.arguments[0];
|
|
165
|
+
if (callback &&
|
|
166
|
+
(callback.type === utils_1.AST_NODE_TYPES.ArrowFunctionExpression ||
|
|
167
|
+
callback.type === utils_1.AST_NODE_TYPES.FunctionExpression)) {
|
|
168
|
+
transactionScopes.delete(callback.body);
|
|
169
|
+
}
|
|
170
|
+
},
|
|
171
|
+
// Track variable assignments that reference the transaction
|
|
172
|
+
VariableDeclarator(node) {
|
|
173
|
+
if (node.id.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
174
|
+
node.init &&
|
|
175
|
+
node.init.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
176
|
+
const transactionScope = findTransactionScope(node);
|
|
177
|
+
if (transactionScope) {
|
|
178
|
+
const scopeInfo = transactionScopes.get(transactionScope);
|
|
179
|
+
if (scopeInfo &&
|
|
180
|
+
scopeInfo.transactionVariables.has(node.init.name)) {
|
|
181
|
+
scopeInfo.transactionVariables.add(node.id.name);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
},
|
|
186
|
+
// Check all call expressions for transaction operations
|
|
187
|
+
CallExpression(node) {
|
|
188
|
+
const transactionScope = findTransactionScope(node);
|
|
189
|
+
const { isRead, isWrite, methodName } = isTransactionMethodCall(node, transactionScope);
|
|
190
|
+
if (!isRead && !isWrite) {
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
if (!transactionScope) {
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
const scopeInfo = transactionScopes.get(transactionScope);
|
|
197
|
+
if (!scopeInfo) {
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
// Handle computed property access specially
|
|
201
|
+
if (isRead && isWrite) {
|
|
202
|
+
// This is a computed property access - we need to be more careful
|
|
203
|
+
// Only flag as a read if we already have writes in this scope
|
|
204
|
+
if (scopeInfo.hasWriteOperation) {
|
|
205
|
+
context.report({
|
|
206
|
+
node,
|
|
207
|
+
messageId: 'readsAfterWrites',
|
|
208
|
+
data: { method: methodName ?? 'unknown' },
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
// Don't mark as write since we don't know what method it is
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
// If it's a write operation, mark the scope as having a write
|
|
215
|
+
if (isWrite) {
|
|
216
|
+
scopeInfo.hasWriteOperation = true;
|
|
217
|
+
scopeInfo.writeNodes.push(node);
|
|
218
|
+
}
|
|
219
|
+
// If it's a read operation and the scope already has a write, report an error
|
|
220
|
+
if (isRead && scopeInfo.hasWriteOperation) {
|
|
221
|
+
context.report({
|
|
222
|
+
node,
|
|
223
|
+
messageId: 'readsAfterWrites',
|
|
224
|
+
data: { method: methodName ?? 'unknown' },
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
},
|
|
228
|
+
};
|
|
229
|
+
},
|
|
230
|
+
});
|
|
231
|
+
//# sourceMappingURL=firestore-transaction-reads-before-writes.js.map
|
|
@@ -9,9 +9,15 @@ exports.genericStartsWithT = (0, createRule_1.createRule)({
|
|
|
9
9
|
for (const param of node.params) {
|
|
10
10
|
if (typeof param.name.name === 'string' &&
|
|
11
11
|
param.name.name[0] !== 'T') {
|
|
12
|
+
const name = param.name.name;
|
|
13
|
+
const suggestedName = `T${name}`;
|
|
12
14
|
context.report({
|
|
13
15
|
node: param,
|
|
14
16
|
messageId: 'genericStartsWithT',
|
|
17
|
+
data: {
|
|
18
|
+
name,
|
|
19
|
+
suggestedName,
|
|
20
|
+
},
|
|
15
21
|
});
|
|
16
22
|
}
|
|
17
23
|
}
|
|
@@ -22,12 +28,12 @@ exports.genericStartsWithT = (0, createRule_1.createRule)({
|
|
|
22
28
|
meta: {
|
|
23
29
|
type: 'suggestion',
|
|
24
30
|
docs: {
|
|
25
|
-
description: 'Enforce TypeScript generic
|
|
31
|
+
description: 'Enforce TypeScript generic type parameters to start with T so they stand out from runtime values.',
|
|
26
32
|
recommended: 'error',
|
|
27
33
|
},
|
|
28
34
|
schema: [],
|
|
29
35
|
messages: {
|
|
30
|
-
genericStartsWithT: 'Generic type parameter should start with T.',
|
|
36
|
+
genericStartsWithT: 'Generic type parameter "{{name}}" should start with "T" (e.g., "{{suggestedName}}") so readers immediately recognize it as a generic type rather than a concrete value. T-prefixed generics make type parameters stand out in signatures and prevent confusion with runtime parameters.',
|
|
31
37
|
},
|
|
32
38
|
},
|
|
33
39
|
defaultOptions: [],
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
type Options = [
|
|
2
|
+
{
|
|
3
|
+
additionalHocNames?: string[];
|
|
4
|
+
}
|
|
5
|
+
];
|
|
6
|
+
export declare const memoizeRootLevelHocs: import("@typescript-eslint/utils/dist/ts-eslint/Rule").RuleModule<"wrapHocInUseMemo", Options, import("@typescript-eslint/utils/dist/ts-eslint/Rule").RuleListener>;
|
|
7
|
+
export {};
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.memoizeRootLevelHocs = void 0;
|
|
4
|
+
const utils_1 = require("@typescript-eslint/utils");
|
|
5
|
+
const createRule_1 = require("../utils/createRule");
|
|
6
|
+
const defaultOptions = [{ additionalHocNames: [] }];
|
|
7
|
+
const isFunctionNode = (node) => node.type === utils_1.AST_NODE_TYPES.FunctionDeclaration ||
|
|
8
|
+
node.type === utils_1.AST_NODE_TYPES.FunctionExpression ||
|
|
9
|
+
node.type === utils_1.AST_NODE_TYPES.ArrowFunctionExpression;
|
|
10
|
+
const isHookName = (name) => /^use[A-Z]/.test(name);
|
|
11
|
+
const isComponentName = (name) => /^[A-Z]/.test(name);
|
|
12
|
+
const forEachChildNode = (node, callback) => {
|
|
13
|
+
for (const key of Object.keys(node)) {
|
|
14
|
+
if (key === 'parent')
|
|
15
|
+
continue;
|
|
16
|
+
const value = node[key];
|
|
17
|
+
if (!value)
|
|
18
|
+
continue;
|
|
19
|
+
if (Array.isArray(value)) {
|
|
20
|
+
for (const child of value) {
|
|
21
|
+
if (child && typeof child.type === 'string') {
|
|
22
|
+
if (callback(child)) {
|
|
23
|
+
return true;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
else if (value && typeof value.type === 'string') {
|
|
29
|
+
if (callback(value)) {
|
|
30
|
+
return true;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return false;
|
|
35
|
+
};
|
|
36
|
+
const getFunctionName = (node) => {
|
|
37
|
+
if (node.type === utils_1.AST_NODE_TYPES.FunctionDeclaration && node.id) {
|
|
38
|
+
return node.id.name;
|
|
39
|
+
}
|
|
40
|
+
if (node.type === utils_1.AST_NODE_TYPES.FunctionExpression ||
|
|
41
|
+
node.type === utils_1.AST_NODE_TYPES.ArrowFunctionExpression) {
|
|
42
|
+
const parent = node.parent;
|
|
43
|
+
if (parent &&
|
|
44
|
+
parent.type === utils_1.AST_NODE_TYPES.VariableDeclarator &&
|
|
45
|
+
parent.id.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
46
|
+
return parent.id.name;
|
|
47
|
+
}
|
|
48
|
+
if (parent &&
|
|
49
|
+
parent.type === utils_1.AST_NODE_TYPES.Property &&
|
|
50
|
+
parent.key.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
51
|
+
return parent.key.name;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return null;
|
|
55
|
+
};
|
|
56
|
+
const containsJsx = (node, options) => {
|
|
57
|
+
if (!node)
|
|
58
|
+
return false;
|
|
59
|
+
if (node.type === utils_1.AST_NODE_TYPES.JSXElement ||
|
|
60
|
+
node.type === utils_1.AST_NODE_TYPES.JSXFragment) {
|
|
61
|
+
return true;
|
|
62
|
+
}
|
|
63
|
+
return forEachChildNode(node, (child) => {
|
|
64
|
+
if (options?.skipFunctionBodies && isFunctionNode(child)) {
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
return containsJsx(child, options);
|
|
68
|
+
});
|
|
69
|
+
};
|
|
70
|
+
const hasFunctionParent = (node) => {
|
|
71
|
+
let current = node.parent;
|
|
72
|
+
while (current) {
|
|
73
|
+
if (isFunctionNode(current)) {
|
|
74
|
+
return true;
|
|
75
|
+
}
|
|
76
|
+
current = current.parent;
|
|
77
|
+
}
|
|
78
|
+
return false;
|
|
79
|
+
};
|
|
80
|
+
const getBodyNodeForJsxCheck = (node) => {
|
|
81
|
+
if (node.type === utils_1.AST_NODE_TYPES.FunctionDeclaration ||
|
|
82
|
+
node.type === utils_1.AST_NODE_TYPES.FunctionExpression) {
|
|
83
|
+
return node.body;
|
|
84
|
+
}
|
|
85
|
+
if (node.body && node.body.type !== utils_1.AST_NODE_TYPES.BlockStatement) {
|
|
86
|
+
return node.body;
|
|
87
|
+
}
|
|
88
|
+
return node.body;
|
|
89
|
+
};
|
|
90
|
+
const isComponentOrHook = (node) => {
|
|
91
|
+
const name = getFunctionName(node);
|
|
92
|
+
const hook = name ? isHookName(name) : false;
|
|
93
|
+
const component = name ? isComponentName(name) : false;
|
|
94
|
+
const nestedFunction = hasFunctionParent(node);
|
|
95
|
+
const jsxBody = containsJsx(getBodyNodeForJsxCheck(node), {
|
|
96
|
+
skipFunctionBodies: !hook && !component,
|
|
97
|
+
});
|
|
98
|
+
if (!hook && !component && nestedFunction) {
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
if (!hook && !component && !jsxBody) {
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
if (hook) {
|
|
105
|
+
return { contextLabel: `hook${name ? ` ${name}` : ''}` };
|
|
106
|
+
}
|
|
107
|
+
return { contextLabel: `component${name ? ` ${name}` : ''}` };
|
|
108
|
+
};
|
|
109
|
+
const getCallableIdentifierName = (callee) => {
|
|
110
|
+
const maybeChain = callee;
|
|
111
|
+
if (maybeChain.type === utils_1.AST_NODE_TYPES.ChainExpression) {
|
|
112
|
+
return getCallableIdentifierName(maybeChain.expression);
|
|
113
|
+
}
|
|
114
|
+
if (callee.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
115
|
+
return callee.name;
|
|
116
|
+
}
|
|
117
|
+
if (callee.type === utils_1.AST_NODE_TYPES.MemberExpression &&
|
|
118
|
+
!callee.computed &&
|
|
119
|
+
callee.property.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
120
|
+
return callee.property.name;
|
|
121
|
+
}
|
|
122
|
+
return null;
|
|
123
|
+
};
|
|
124
|
+
const isHocIdentifier = (name, additionalHocs) => {
|
|
125
|
+
if (additionalHocs.has(name)) {
|
|
126
|
+
return true;
|
|
127
|
+
}
|
|
128
|
+
if (!name.startsWith('with')) {
|
|
129
|
+
return false;
|
|
130
|
+
}
|
|
131
|
+
const suffix = name.charAt(4);
|
|
132
|
+
return Boolean(suffix) && /^[A-Z]$/.test(suffix);
|
|
133
|
+
};
|
|
134
|
+
const findHocName = (node, additionalHocs) => {
|
|
135
|
+
const identifier = getCallableIdentifierName(node.callee);
|
|
136
|
+
if (identifier && isHocIdentifier(identifier, additionalHocs)) {
|
|
137
|
+
return identifier;
|
|
138
|
+
}
|
|
139
|
+
if (node.callee.type === utils_1.AST_NODE_TYPES.CallExpression) {
|
|
140
|
+
return findHocName(node.callee, additionalHocs);
|
|
141
|
+
}
|
|
142
|
+
return null;
|
|
143
|
+
};
|
|
144
|
+
/**
|
|
145
|
+
* Detects chained HOC calls where an inner call is immediately invoked by
|
|
146
|
+
* another call (for example, withHoc(Component)()). We only treat calls as
|
|
147
|
+
* part of the same chain when the current CallExpression is the callee of its
|
|
148
|
+
* parent and both resolve to the same HOC name, which prevents duplicate
|
|
149
|
+
* reports for patterns like withHoc(Component)(props).
|
|
150
|
+
*/
|
|
151
|
+
const getParentCallExpression = (callExpr) => callExpr.parent &&
|
|
152
|
+
callExpr.parent.type === utils_1.AST_NODE_TYPES.CallExpression &&
|
|
153
|
+
callExpr.parent.callee === callExpr
|
|
154
|
+
? callExpr.parent
|
|
155
|
+
: null;
|
|
156
|
+
const isPartOfHocChain = (hocName, parentHocName) => Boolean(parentHocName && parentHocName === hocName);
|
|
157
|
+
exports.memoizeRootLevelHocs = (0, createRule_1.createRule)({
|
|
158
|
+
name: 'memoize-root-level-hocs',
|
|
159
|
+
meta: {
|
|
160
|
+
type: 'problem',
|
|
161
|
+
docs: {
|
|
162
|
+
description: 'Prevent creating Higher-Order Components at the root level of React components/hooks without wrapping them in useMemo to keep wrapped component identities stable across renders.',
|
|
163
|
+
recommended: 'error',
|
|
164
|
+
},
|
|
165
|
+
fixable: undefined,
|
|
166
|
+
schema: [
|
|
167
|
+
{
|
|
168
|
+
type: 'object',
|
|
169
|
+
properties: {
|
|
170
|
+
additionalHocNames: {
|
|
171
|
+
type: 'array',
|
|
172
|
+
items: { type: 'string' },
|
|
173
|
+
default: [],
|
|
174
|
+
},
|
|
175
|
+
},
|
|
176
|
+
additionalProperties: false,
|
|
177
|
+
},
|
|
178
|
+
],
|
|
179
|
+
messages: {
|
|
180
|
+
wrapHocInUseMemo: 'HOC "{{hocName}}" is created inside {{contextLabel}} during render, so every render creates a brand-new wrapped component reference. React treats that as a different component, unmounting/remounting it (resetting internal state) and forcing children to re-render even when props stay the same. Wrap the HOC creation in useMemo with the correct dependencies or hoist it outside the {{contextLabel}} so the wrapped component identity stays stable.',
|
|
181
|
+
},
|
|
182
|
+
},
|
|
183
|
+
defaultOptions,
|
|
184
|
+
create(context, [options]) {
|
|
185
|
+
const additionalHocs = new Set(options?.additionalHocNames ?? []);
|
|
186
|
+
const reportUnmemoizedHoc = (node, hocName, contextInfo) => {
|
|
187
|
+
context.report({
|
|
188
|
+
node,
|
|
189
|
+
messageId: 'wrapHocInUseMemo',
|
|
190
|
+
data: {
|
|
191
|
+
hocName,
|
|
192
|
+
contextLabel: contextInfo.contextLabel,
|
|
193
|
+
},
|
|
194
|
+
});
|
|
195
|
+
};
|
|
196
|
+
const checkHocCall = (callExpr, contextInfo) => {
|
|
197
|
+
const hocName = findHocName(callExpr, additionalHocs);
|
|
198
|
+
const parentCall = getParentCallExpression(callExpr);
|
|
199
|
+
const parentHocName = parentCall && findHocName(parentCall, additionalHocs);
|
|
200
|
+
if (hocName && !isPartOfHocChain(hocName, parentHocName)) {
|
|
201
|
+
reportUnmemoizedHoc(callExpr, hocName, contextInfo);
|
|
202
|
+
}
|
|
203
|
+
};
|
|
204
|
+
const traverseFunctionBody = (node, contextInfo) => {
|
|
205
|
+
const visitNode = (current) => {
|
|
206
|
+
if (isFunctionNode(current) && current !== node) {
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
if (current.type === utils_1.AST_NODE_TYPES.CallExpression) {
|
|
210
|
+
checkHocCall(current, contextInfo);
|
|
211
|
+
}
|
|
212
|
+
forEachChildNode(current, (child) => {
|
|
213
|
+
visitNode(child);
|
|
214
|
+
return false;
|
|
215
|
+
});
|
|
216
|
+
};
|
|
217
|
+
if (!node.body) {
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
if (node.body.type === utils_1.AST_NODE_TYPES.BlockStatement) {
|
|
221
|
+
for (const statement of node.body.body) {
|
|
222
|
+
visitNode(statement);
|
|
223
|
+
}
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
visitNode(node.body);
|
|
227
|
+
};
|
|
228
|
+
const analyzeFunction = (node) => {
|
|
229
|
+
const contextInfo = isComponentOrHook(node);
|
|
230
|
+
if (!contextInfo) {
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
traverseFunctionBody(node, contextInfo);
|
|
234
|
+
};
|
|
235
|
+
return {
|
|
236
|
+
FunctionDeclaration: analyzeFunction,
|
|
237
|
+
FunctionExpression: analyzeFunction,
|
|
238
|
+
ArrowFunctionExpression: analyzeFunction,
|
|
239
|
+
};
|
|
240
|
+
},
|
|
241
|
+
});
|
|
242
|
+
//# sourceMappingURL=memoize-root-level-hocs.js.map
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { TSESLint } from '@typescript-eslint/utils';
|
|
2
|
+
export type MessageIds = 'noArrayLengthInDeps';
|
|
3
|
+
type Options = [
|
|
4
|
+
{
|
|
5
|
+
hashImport?: {
|
|
6
|
+
source?: string;
|
|
7
|
+
importName?: string;
|
|
8
|
+
};
|
|
9
|
+
}?
|
|
10
|
+
];
|
|
11
|
+
export declare const noArrayLengthInDeps: TSESLint.RuleModule<"noArrayLengthInDeps", Options, TSESLint.RuleListener>;
|
|
12
|
+
export {};
|