@blumintinc/eslint-plugin-blumint 1.1.5 → 1.1.7
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/lib/index.js +7 -1
- package/lib/rules/class-methods-read-top-to-bottom.js +6 -4
- package/lib/rules/consistent-callback-naming.js +127 -2
- package/lib/rules/enforce-firestore-path-utils.d.ts +1 -0
- package/lib/rules/enforce-firestore-path-utils.js +78 -0
- package/lib/rules/enforce-realtimedb-path-utils.d.ts +1 -0
- package/lib/rules/enforce-realtimedb-path-utils.js +106 -0
- package/lib/rules/no-entire-object-hook-deps.d.ts +1 -0
- package/lib/rules/no-entire-object-hook-deps.js +164 -0
- package/package.json +11 -7
package/lib/index.js
CHANGED
|
@@ -34,10 +34,12 @@ const require_image_overlayed_1 = __importDefault(require("./rules/require-image
|
|
|
34
34
|
const require_usememo_object_literals_1 = require("./rules/require-usememo-object-literals");
|
|
35
35
|
const enforce_safe_stringify_1 = require("./rules/enforce-safe-stringify");
|
|
36
36
|
const avoid_utils_directory_1 = require("./rules/avoid-utils-directory");
|
|
37
|
+
const no_entire_object_hook_deps_1 = require("./rules/no-entire-object-hook-deps");
|
|
38
|
+
const enforce_firestore_path_utils_1 = require("./rules/enforce-firestore-path-utils");
|
|
37
39
|
module.exports = {
|
|
38
40
|
meta: {
|
|
39
41
|
name: '@blumintinc/eslint-plugin-blumint',
|
|
40
|
-
version: '1.1.
|
|
42
|
+
version: '1.1.7',
|
|
41
43
|
},
|
|
42
44
|
parseOptions: {
|
|
43
45
|
ecmaVersion: 2020,
|
|
@@ -47,6 +49,7 @@ module.exports = {
|
|
|
47
49
|
plugins: ['@blumintinc/blumint'],
|
|
48
50
|
rules: {
|
|
49
51
|
'@blumintinc/blumint/avoid-utils-directory': 'error',
|
|
52
|
+
'@blumintinc/blumint/enforce-firestore-path-utils': 'error',
|
|
50
53
|
'@blumintinc/blumint/no-jsx-whitespace-literal': 'error',
|
|
51
54
|
'@blumintinc/blumint/array-methods-this-context': 'warn',
|
|
52
55
|
'@blumintinc/blumint/class-methods-read-top-to-bottom': 'warn',
|
|
@@ -77,6 +80,7 @@ module.exports = {
|
|
|
77
80
|
'@blumintinc/blumint/require-image-overlayed': 'error',
|
|
78
81
|
'@blumintinc/blumint/require-usememo-object-literals': 'error',
|
|
79
82
|
'@blumintinc/blumint/enforce-safe-stringify': 'error',
|
|
83
|
+
'@blumintinc/blumint/no-entire-object-hook-deps': 'error',
|
|
80
84
|
},
|
|
81
85
|
},
|
|
82
86
|
},
|
|
@@ -112,6 +116,8 @@ module.exports = {
|
|
|
112
116
|
'require-usememo-object-literals': require_usememo_object_literals_1.requireUseMemoObjectLiterals,
|
|
113
117
|
'enforce-safe-stringify': enforce_safe_stringify_1.enforceStableStringify,
|
|
114
118
|
'avoid-utils-directory': avoid_utils_directory_1.avoidUtilsDirectory,
|
|
119
|
+
'no-entire-object-hook-deps': no_entire_object_hook_deps_1.noEntireObjectHookDeps,
|
|
120
|
+
'enforce-firestore-path-utils': enforce_firestore_path_utils_1.enforceFirestorePathUtils,
|
|
115
121
|
},
|
|
116
122
|
};
|
|
117
123
|
//# sourceMappingURL=index.js.map
|
|
@@ -49,10 +49,12 @@ exports.classMethodsReadTopToBottom = (0, createRule_1.createRule)({
|
|
|
49
49
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
50
50
|
const memberNode = node.body.find((member) => getMemberName(member) === n);
|
|
51
51
|
const comments = sourceCode.getCommentsBefore(memberNode);
|
|
52
|
-
memberNode.range =
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
52
|
+
memberNode.range = comments.length
|
|
53
|
+
? [
|
|
54
|
+
Math.min(memberNode.range[0], Math.min(...comments.map((comment) => comment.range[0]))),
|
|
55
|
+
Math.max(memberNode.range[1], Math.max(...comments.map((comment) => comment.range[1]))),
|
|
56
|
+
]
|
|
57
|
+
: memberNode.range;
|
|
56
58
|
return sourceCode.getText(memberNode);
|
|
57
59
|
})
|
|
58
60
|
.join('\n');
|
|
@@ -23,6 +23,7 @@ var __importStar = (this && this.__importStar) || function (mod) {
|
|
|
23
23
|
return result;
|
|
24
24
|
};
|
|
25
25
|
const createRule_1 = require("../utils/createRule");
|
|
26
|
+
const utils_1 = require("@typescript-eslint/utils");
|
|
26
27
|
const ts = __importStar(require("typescript"));
|
|
27
28
|
module.exports = (0, createRule_1.createRule)({
|
|
28
29
|
name: 'consistent-callback-naming',
|
|
@@ -101,6 +102,34 @@ module.exports = (0, createRule_1.createRule)({
|
|
|
101
102
|
if (propName && isPascalCase(propName)) {
|
|
102
103
|
return;
|
|
103
104
|
}
|
|
105
|
+
// Skip common non-callback props
|
|
106
|
+
const commonNonCallbackProps = new Set([
|
|
107
|
+
'theme',
|
|
108
|
+
'style',
|
|
109
|
+
'className',
|
|
110
|
+
'ref',
|
|
111
|
+
'key',
|
|
112
|
+
'component',
|
|
113
|
+
'as',
|
|
114
|
+
'sx',
|
|
115
|
+
'css', // Emotion css prop
|
|
116
|
+
]);
|
|
117
|
+
if (propName && commonNonCallbackProps.has(propName)) {
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
// Skip props on components that commonly use function props that aren't callbacks
|
|
121
|
+
const parentName = node.parent?.name;
|
|
122
|
+
const componentName = parentName?.type === 'JSXIdentifier' ? parentName.name : undefined;
|
|
123
|
+
const componentsWithFunctionProps = new Set([
|
|
124
|
+
'ThemeProvider',
|
|
125
|
+
'Transition',
|
|
126
|
+
'CSSTransition',
|
|
127
|
+
'TransitionGroup',
|
|
128
|
+
'SwitchTransition', // React Transition Group
|
|
129
|
+
]);
|
|
130
|
+
if (componentName && componentsWithFunctionProps.has(componentName)) {
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
104
133
|
// Check if the value is a function type and not a React component
|
|
105
134
|
if (isFunctionType(node.value.expression) &&
|
|
106
135
|
propName &&
|
|
@@ -122,14 +151,84 @@ module.exports = (0, createRule_1.createRule)({
|
|
|
122
151
|
'FunctionDeclaration, VariableDeclarator'(node) {
|
|
123
152
|
const functionName = node.id?.type === 'Identifier' ? node.id.name : undefined;
|
|
124
153
|
if (functionName && functionName.startsWith('handle') && node.id) {
|
|
154
|
+
// Skip autofixing for "handler" and "handlers"
|
|
155
|
+
if (functionName === 'handler' || functionName === 'handlers') {
|
|
156
|
+
context.report({
|
|
157
|
+
node,
|
|
158
|
+
messageId: 'callbackFunctionPrefix',
|
|
159
|
+
});
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
// Skip autofixing for class parameters and getters
|
|
163
|
+
const parent = node.parent;
|
|
164
|
+
if (parent?.type === utils_1.AST_NODE_TYPES.PropertyDefinition || parent?.type === utils_1.AST_NODE_TYPES.MethodDefinition) {
|
|
165
|
+
context.report({
|
|
166
|
+
node,
|
|
167
|
+
messageId: 'callbackFunctionPrefix',
|
|
168
|
+
});
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
// Get all references to this variable
|
|
172
|
+
const scope = context.getScope();
|
|
173
|
+
const variable = scope.variables.find(v => v.name === functionName);
|
|
174
|
+
const references = new Set(variable?.references ?? []);
|
|
175
|
+
// Get references from all scopes
|
|
176
|
+
const allScopes = [scope];
|
|
177
|
+
let currentScope = scope;
|
|
178
|
+
while (currentScope.upper) {
|
|
179
|
+
currentScope = currentScope.upper;
|
|
180
|
+
allScopes.push(currentScope);
|
|
181
|
+
}
|
|
182
|
+
// Get references from all scopes and their children
|
|
183
|
+
for (const s of allScopes) {
|
|
184
|
+
// Get references from current scope
|
|
185
|
+
const currentVar = s.variables.find(v => v.name === functionName);
|
|
186
|
+
if (currentVar) {
|
|
187
|
+
currentVar.references.forEach(ref => references.add(ref));
|
|
188
|
+
}
|
|
189
|
+
// Get references from child scopes
|
|
190
|
+
const childScopes = s.childScopes;
|
|
191
|
+
for (const childScope of childScopes) {
|
|
192
|
+
const childVar = childScope.variables.find(v => v.name === functionName);
|
|
193
|
+
if (childVar) {
|
|
194
|
+
childVar.references.forEach(ref => references.add(ref));
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
// Get references from sibling scopes
|
|
199
|
+
const siblingScopes = scope.upper?.childScopes ?? [];
|
|
200
|
+
for (const siblingScope of siblingScopes) {
|
|
201
|
+
if (siblingScope !== scope) {
|
|
202
|
+
const siblingVar = siblingScope.variables.find(v => v.name === functionName);
|
|
203
|
+
if (siblingVar) {
|
|
204
|
+
siblingVar.references.forEach(ref => references.add(ref));
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
// Get references from global scope
|
|
209
|
+
const sourceCode = context.getSourceCode();
|
|
210
|
+
if (sourceCode.scopeManager?.globalScope) {
|
|
211
|
+
const globalVar = sourceCode.scopeManager.globalScope.variables.find(v => v.name === functionName);
|
|
212
|
+
if (globalVar) {
|
|
213
|
+
globalVar.references.forEach(ref => references.add(ref));
|
|
214
|
+
}
|
|
215
|
+
}
|
|
125
216
|
context.report({
|
|
126
217
|
node,
|
|
127
218
|
messageId: 'callbackFunctionPrefix',
|
|
128
|
-
fix(fixer) {
|
|
219
|
+
*fix(fixer) {
|
|
129
220
|
// Remove 'handle' prefix and convert first character to lowercase
|
|
130
221
|
const newName = functionName.slice(6).charAt(0).toLowerCase() +
|
|
131
222
|
functionName.slice(7);
|
|
132
|
-
|
|
223
|
+
// Fix the declaration and all references
|
|
224
|
+
const fixes = [];
|
|
225
|
+
fixes.push(fixer.replaceText(node.id, newName));
|
|
226
|
+
for (const ref of references) {
|
|
227
|
+
if (ref.identifier !== node.id) {
|
|
228
|
+
fixes.push(fixer.replaceText(ref.identifier, newName));
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
return fixes;
|
|
133
232
|
},
|
|
134
233
|
});
|
|
135
234
|
}
|
|
@@ -140,6 +239,22 @@ module.exports = (0, createRule_1.createRule)({
|
|
|
140
239
|
node.key.name &&
|
|
141
240
|
node.key.name.startsWith('handle')) {
|
|
142
241
|
const name = node.key.name;
|
|
242
|
+
// Skip autofixing for "handler" and "handlers"
|
|
243
|
+
if (name === 'handler' || name === 'handlers') {
|
|
244
|
+
context.report({
|
|
245
|
+
node: node.key,
|
|
246
|
+
messageId: 'callbackFunctionPrefix',
|
|
247
|
+
});
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
// Skip autofixing for class parameters and getters
|
|
251
|
+
if (node.type === 'MethodDefinition' && node.kind === 'get') {
|
|
252
|
+
context.report({
|
|
253
|
+
node: node.key,
|
|
254
|
+
messageId: 'callbackFunctionPrefix',
|
|
255
|
+
});
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
143
258
|
context.report({
|
|
144
259
|
node: node.key,
|
|
145
260
|
messageId: 'callbackFunctionPrefix',
|
|
@@ -151,6 +266,16 @@ module.exports = (0, createRule_1.createRule)({
|
|
|
151
266
|
});
|
|
152
267
|
}
|
|
153
268
|
},
|
|
269
|
+
// Check constructor parameters
|
|
270
|
+
'TSParameterProperty'(node) {
|
|
271
|
+
if (node.parameter.type === 'Identifier' &&
|
|
272
|
+
node.parameter.name.startsWith('handle')) {
|
|
273
|
+
context.report({
|
|
274
|
+
node,
|
|
275
|
+
messageId: 'callbackFunctionPrefix',
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
},
|
|
154
279
|
};
|
|
155
280
|
},
|
|
156
281
|
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const enforceFirestorePathUtils: import("@typescript-eslint/utils/dist/ts-eslint/Rule").RuleModule<"requirePathUtil", [], import("@typescript-eslint/utils/dist/ts-eslint/Rule").RuleListener>;
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.enforceFirestorePathUtils = void 0;
|
|
4
|
+
const utils_1 = require("@typescript-eslint/utils");
|
|
5
|
+
const createRule_1 = require("../utils/createRule");
|
|
6
|
+
const FIRESTORE_METHODS = new Set(['doc', 'collection']);
|
|
7
|
+
exports.enforceFirestorePathUtils = (0, createRule_1.createRule)({
|
|
8
|
+
name: 'enforce-firestore-path-utils',
|
|
9
|
+
meta: {
|
|
10
|
+
type: 'suggestion',
|
|
11
|
+
docs: {
|
|
12
|
+
description: 'Enforce usage of utility functions for Firestore paths',
|
|
13
|
+
recommended: 'error',
|
|
14
|
+
},
|
|
15
|
+
schema: [],
|
|
16
|
+
messages: {
|
|
17
|
+
requirePathUtil: 'Use a utility function (e.g., toUserPath, toCollectionPath) for Firestore paths instead of string literals',
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
defaultOptions: [],
|
|
21
|
+
create(context) {
|
|
22
|
+
function isFirestoreCall(node) {
|
|
23
|
+
if (node.callee.type !== utils_1.AST_NODE_TYPES.MemberExpression) {
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
const property = node.callee.property;
|
|
27
|
+
if (property.type !== utils_1.AST_NODE_TYPES.Identifier) {
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
return FIRESTORE_METHODS.has(property.name);
|
|
31
|
+
}
|
|
32
|
+
function isStringLiteralOrTemplate(node) {
|
|
33
|
+
return (node.type === utils_1.AST_NODE_TYPES.Literal && typeof node.value === 'string' ||
|
|
34
|
+
node.type === utils_1.AST_NODE_TYPES.TemplateLiteral);
|
|
35
|
+
}
|
|
36
|
+
function isUtilityFunction(node) {
|
|
37
|
+
if (node.type !== utils_1.AST_NODE_TYPES.CallExpression) {
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
const callee = node.callee;
|
|
41
|
+
if (callee.type !== utils_1.AST_NODE_TYPES.Identifier) {
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
// Match functions starting with 'to' and ending with 'Path'
|
|
45
|
+
return /^to.*Path$/.test(callee.name);
|
|
46
|
+
}
|
|
47
|
+
return {
|
|
48
|
+
CallExpression(node) {
|
|
49
|
+
if (!isFirestoreCall(node)) {
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
// Check first argument of doc() or collection() call
|
|
53
|
+
const pathArg = node.arguments[0];
|
|
54
|
+
if (!pathArg) {
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
// Skip if it's already using a utility function
|
|
58
|
+
if (isUtilityFunction(pathArg)) {
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
// Skip if it's a variable or other non-literal expression
|
|
62
|
+
if (!isStringLiteralOrTemplate(pathArg)) {
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
// Skip test files
|
|
66
|
+
const filename = context.getFilename();
|
|
67
|
+
if (filename.includes('__tests__') || filename.includes('.test.') || filename.includes('.spec.')) {
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
context.report({
|
|
71
|
+
node: pathArg,
|
|
72
|
+
messageId: 'requirePathUtil',
|
|
73
|
+
});
|
|
74
|
+
},
|
|
75
|
+
};
|
|
76
|
+
},
|
|
77
|
+
});
|
|
78
|
+
//# sourceMappingURL=enforce-firestore-path-utils.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const enforceRealtimedbPathUtils: import("@typescript-eslint/utils/dist/ts-eslint/Rule").RuleModule<"requirePathUtil", [], import("@typescript-eslint/utils/dist/ts-eslint/Rule").RuleListener>;
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.enforceRealtimedbPathUtils = void 0;
|
|
4
|
+
const utils_1 = require("@typescript-eslint/utils");
|
|
5
|
+
const createRule_1 = require("../utils/createRule");
|
|
6
|
+
const RTDB_METHODS = new Set(['ref', 'child']);
|
|
7
|
+
exports.enforceRealtimedbPathUtils = (0, createRule_1.createRule)({
|
|
8
|
+
name: 'enforce-realtimedb-path-utils',
|
|
9
|
+
meta: {
|
|
10
|
+
type: 'suggestion',
|
|
11
|
+
docs: {
|
|
12
|
+
description: 'Enforce usage of utility functions for Realtime Database paths',
|
|
13
|
+
recommended: 'error',
|
|
14
|
+
},
|
|
15
|
+
schema: [],
|
|
16
|
+
messages: {
|
|
17
|
+
requirePathUtil: 'Use a utility function (e.g., toUserPath, toItemPath) for Realtime Database paths instead of string literals',
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
defaultOptions: [],
|
|
21
|
+
create(context) {
|
|
22
|
+
function isRTDBCall(node) {
|
|
23
|
+
if (node.callee.type !== utils_1.AST_NODE_TYPES.MemberExpression) {
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
const property = node.callee.property;
|
|
27
|
+
if (property.type !== utils_1.AST_NODE_TYPES.Identifier) {
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
// Check for both frontend and backend SDK patterns
|
|
31
|
+
if (!RTDB_METHODS.has(property.name)) {
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
// Check if it's a Firebase RTDB call by looking at the chain
|
|
35
|
+
let current = node.callee.object;
|
|
36
|
+
while (current) {
|
|
37
|
+
if (current.type === utils_1.AST_NODE_TYPES.CallExpression) {
|
|
38
|
+
if (current.callee.type === utils_1.AST_NODE_TYPES.MemberExpression) {
|
|
39
|
+
const method = current.callee.property;
|
|
40
|
+
if (method.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
41
|
+
method.name === 'database') {
|
|
42
|
+
return true;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
current = current.callee;
|
|
46
|
+
}
|
|
47
|
+
else if (current.type === utils_1.AST_NODE_TYPES.MemberExpression) {
|
|
48
|
+
// Handle chained calls like ref().child()
|
|
49
|
+
current = current.object;
|
|
50
|
+
}
|
|
51
|
+
else {
|
|
52
|
+
break;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
function isStringLiteralOrTemplate(node) {
|
|
58
|
+
return ((node.type === utils_1.AST_NODE_TYPES.Literal && typeof node.value === 'string') ||
|
|
59
|
+
node.type === utils_1.AST_NODE_TYPES.TemplateLiteral);
|
|
60
|
+
}
|
|
61
|
+
function isUtilityFunction(node) {
|
|
62
|
+
if (node.type !== utils_1.AST_NODE_TYPES.CallExpression) {
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
65
|
+
const callee = node.callee;
|
|
66
|
+
if (callee.type !== utils_1.AST_NODE_TYPES.Identifier) {
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
// Match functions starting with 'to' and ending with 'Path'
|
|
70
|
+
return /^to.*Path$/.test(callee.name);
|
|
71
|
+
}
|
|
72
|
+
return {
|
|
73
|
+
CallExpression(node) {
|
|
74
|
+
if (!isRTDBCall(node)) {
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
// Check first argument of ref() or child() call
|
|
78
|
+
const pathArg = node.arguments[0];
|
|
79
|
+
if (!pathArg) {
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
// Skip if it's already using a utility function
|
|
83
|
+
if (isUtilityFunction(pathArg)) {
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
// Skip if it's a variable or other non-literal expression
|
|
87
|
+
if (!isStringLiteralOrTemplate(pathArg)) {
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
// Skip test files
|
|
91
|
+
const filename = context.getFilename();
|
|
92
|
+
if (filename.includes('__tests__') ||
|
|
93
|
+
filename.includes('.test.') ||
|
|
94
|
+
filename.includes('.spec.') ||
|
|
95
|
+
filename.includes('mocks')) {
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
context.report({
|
|
99
|
+
node: pathArg,
|
|
100
|
+
messageId: 'requirePathUtil',
|
|
101
|
+
});
|
|
102
|
+
},
|
|
103
|
+
};
|
|
104
|
+
},
|
|
105
|
+
});
|
|
106
|
+
//# sourceMappingURL=enforce-realtimedb-path-utils.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const noEntireObjectHookDeps: import("@typescript-eslint/utils/dist/ts-eslint/Rule").RuleModule<"avoidEntireObject", [], import("@typescript-eslint/utils/dist/ts-eslint/Rule").RuleListener>;
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.noEntireObjectHookDeps = void 0;
|
|
4
|
+
const utils_1 = require("@typescript-eslint/utils");
|
|
5
|
+
const createRule_1 = require("../utils/createRule");
|
|
6
|
+
const HOOK_NAMES = new Set(['useEffect', 'useCallback', 'useMemo']);
|
|
7
|
+
function isHookCall(node) {
|
|
8
|
+
const callee = node.callee;
|
|
9
|
+
return (callee.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
10
|
+
HOOK_NAMES.has(callee.name));
|
|
11
|
+
}
|
|
12
|
+
function getObjectUsagesInHook(hookBody, objectName) {
|
|
13
|
+
const usages = new Set();
|
|
14
|
+
const visited = new Set();
|
|
15
|
+
function buildAccessPath(node) {
|
|
16
|
+
const parts = [];
|
|
17
|
+
let current = node;
|
|
18
|
+
while (current.type === utils_1.AST_NODE_TYPES.MemberExpression) {
|
|
19
|
+
if (current.computed) {
|
|
20
|
+
return null; // Skip computed properties
|
|
21
|
+
}
|
|
22
|
+
if (current.property.type !== utils_1.AST_NODE_TYPES.Identifier) {
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
parts.unshift(current.property.name);
|
|
26
|
+
current = current.object;
|
|
27
|
+
}
|
|
28
|
+
if (current.type === utils_1.AST_NODE_TYPES.Identifier && current.name === objectName) {
|
|
29
|
+
parts.unshift(objectName);
|
|
30
|
+
return parts.join('.');
|
|
31
|
+
}
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
function visit(node) {
|
|
35
|
+
if (visited.has(node)) {
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
visited.add(node);
|
|
39
|
+
if (node.type === utils_1.AST_NODE_TYPES.MemberExpression && !node.computed) {
|
|
40
|
+
const accessPath = buildAccessPath(node);
|
|
41
|
+
if (accessPath) {
|
|
42
|
+
usages.add(accessPath);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
// Visit children
|
|
46
|
+
switch (node.type) {
|
|
47
|
+
case utils_1.AST_NODE_TYPES.BlockStatement:
|
|
48
|
+
case utils_1.AST_NODE_TYPES.Program:
|
|
49
|
+
node.body.forEach(child => visit(child));
|
|
50
|
+
break;
|
|
51
|
+
case utils_1.AST_NODE_TYPES.ExpressionStatement:
|
|
52
|
+
visit(node.expression);
|
|
53
|
+
break;
|
|
54
|
+
case utils_1.AST_NODE_TYPES.CallExpression:
|
|
55
|
+
visit(node.callee);
|
|
56
|
+
node.arguments.forEach(arg => visit(arg));
|
|
57
|
+
break;
|
|
58
|
+
case utils_1.AST_NODE_TYPES.MemberExpression:
|
|
59
|
+
visit(node.object);
|
|
60
|
+
if (!node.computed) {
|
|
61
|
+
visit(node.property);
|
|
62
|
+
}
|
|
63
|
+
break;
|
|
64
|
+
case utils_1.AST_NODE_TYPES.ArrowFunctionExpression:
|
|
65
|
+
case utils_1.AST_NODE_TYPES.FunctionExpression:
|
|
66
|
+
if (node.body.type === utils_1.AST_NODE_TYPES.BlockStatement) {
|
|
67
|
+
visit(node.body);
|
|
68
|
+
}
|
|
69
|
+
else {
|
|
70
|
+
visit(node.body);
|
|
71
|
+
}
|
|
72
|
+
break;
|
|
73
|
+
case utils_1.AST_NODE_TYPES.ReturnStatement:
|
|
74
|
+
if (node.argument) {
|
|
75
|
+
visit(node.argument);
|
|
76
|
+
}
|
|
77
|
+
break;
|
|
78
|
+
case utils_1.AST_NODE_TYPES.ConditionalExpression:
|
|
79
|
+
visit(node.test);
|
|
80
|
+
visit(node.consequent);
|
|
81
|
+
visit(node.alternate);
|
|
82
|
+
break;
|
|
83
|
+
case utils_1.AST_NODE_TYPES.LogicalExpression:
|
|
84
|
+
case utils_1.AST_NODE_TYPES.BinaryExpression:
|
|
85
|
+
visit(node.left);
|
|
86
|
+
visit(node.right);
|
|
87
|
+
break;
|
|
88
|
+
case utils_1.AST_NODE_TYPES.TemplateLiteral:
|
|
89
|
+
node.expressions.forEach(expr => visit(expr));
|
|
90
|
+
break;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
visit(hookBody);
|
|
94
|
+
// Filter out intermediate paths
|
|
95
|
+
const paths = Array.from(usages);
|
|
96
|
+
const filteredPaths = paths.filter(path => !paths.some(otherPath => otherPath !== path && otherPath.startsWith(path + '.')));
|
|
97
|
+
return new Set(filteredPaths);
|
|
98
|
+
}
|
|
99
|
+
exports.noEntireObjectHookDeps = (0, createRule_1.createRule)({
|
|
100
|
+
name: 'no-entire-object-hook-deps',
|
|
101
|
+
meta: {
|
|
102
|
+
type: 'suggestion',
|
|
103
|
+
docs: {
|
|
104
|
+
description: 'Avoid using entire objects in React hook dependency arrays when only specific fields are used',
|
|
105
|
+
recommended: 'error',
|
|
106
|
+
},
|
|
107
|
+
fixable: 'code',
|
|
108
|
+
schema: [],
|
|
109
|
+
messages: {
|
|
110
|
+
avoidEntireObject: 'Avoid using entire object "{{objectName}}" in dependency array. Use specific fields: {{fields}}',
|
|
111
|
+
},
|
|
112
|
+
},
|
|
113
|
+
defaultOptions: [],
|
|
114
|
+
create(context) {
|
|
115
|
+
return {
|
|
116
|
+
CallExpression(node) {
|
|
117
|
+
if (!isHookCall(node)) {
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
// Get the dependency array argument
|
|
121
|
+
const depsArg = node.arguments[node.arguments.length - 1];
|
|
122
|
+
if (!depsArg ||
|
|
123
|
+
depsArg.type !== utils_1.AST_NODE_TYPES.ArrayExpression) {
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
// Get the hook callback function
|
|
127
|
+
const callbackArg = node.arguments[0];
|
|
128
|
+
if (!callbackArg ||
|
|
129
|
+
(callbackArg.type !== utils_1.AST_NODE_TYPES.ArrowFunctionExpression &&
|
|
130
|
+
callbackArg.type !== utils_1.AST_NODE_TYPES.FunctionExpression)) {
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
// Check each dependency in the array
|
|
134
|
+
depsArg.elements.forEach(element => {
|
|
135
|
+
if (element &&
|
|
136
|
+
element.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
137
|
+
const objectName = element.name;
|
|
138
|
+
const usages = getObjectUsagesInHook(callbackArg.body, objectName);
|
|
139
|
+
// If we found specific field usages and the entire object is in deps
|
|
140
|
+
if (usages.size > 0) {
|
|
141
|
+
const fields = Array.from(usages).join(', ');
|
|
142
|
+
context.report({
|
|
143
|
+
node: element,
|
|
144
|
+
messageId: 'avoidEntireObject',
|
|
145
|
+
data: {
|
|
146
|
+
objectName,
|
|
147
|
+
fields,
|
|
148
|
+
},
|
|
149
|
+
fix(fixer) {
|
|
150
|
+
// Only provide fix if we have specific fields to suggest
|
|
151
|
+
if (usages.size > 0) {
|
|
152
|
+
return fixer.replaceText(element, Array.from(usages).join(', '));
|
|
153
|
+
}
|
|
154
|
+
return null;
|
|
155
|
+
},
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
});
|
|
160
|
+
},
|
|
161
|
+
};
|
|
162
|
+
},
|
|
163
|
+
});
|
|
164
|
+
//# sourceMappingURL=no-entire-object-hook-deps.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@blumintinc/eslint-plugin-blumint",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.7",
|
|
4
4
|
"description": "Custom eslint rules for use within BluMint",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Brodie McGuire",
|
|
@@ -36,23 +36,28 @@
|
|
|
36
36
|
"docs": "./scripts/make-docs.sh && npm run update:eslint-docs",
|
|
37
37
|
"update:eslint-docs": "eslint-doc-generator",
|
|
38
38
|
"build": "tsc",
|
|
39
|
-
"prepare": "npm run build",
|
|
39
|
+
"prepare": "husky install && npm run build",
|
|
40
40
|
"version": "git add -A src",
|
|
41
41
|
"postversion": "git push && git push --tags",
|
|
42
42
|
"remove-hooks": "del-cli ./.husky/",
|
|
43
43
|
"release:dry-run": "semantic-release --dry-run --no-ci",
|
|
44
|
-
"test:release": "node scripts/test-release.js"
|
|
44
|
+
"test:release": "node scripts/test-release.js",
|
|
45
|
+
"commitlint": "commitlint --edit"
|
|
45
46
|
},
|
|
46
47
|
"dependencies": {
|
|
47
48
|
"requireindex": "1.2.0"
|
|
48
49
|
},
|
|
49
50
|
"devDependencies": {
|
|
51
|
+
"@commitlint/cli": "19.6.1",
|
|
52
|
+
"@commitlint/config-conventional": "19.6.0",
|
|
50
53
|
"@semantic-release/changelog": "6.0.1",
|
|
51
54
|
"@semantic-release/commit-analyzer": "9.0.2",
|
|
55
|
+
"@semantic-release/exec": "6.0.3",
|
|
52
56
|
"@semantic-release/git": "10.0.1",
|
|
53
57
|
"@semantic-release/npm": "9.0.1",
|
|
54
58
|
"@semantic-release/release-notes-generator": "10.0.3",
|
|
55
59
|
"@types/eslint": "8.37.0",
|
|
60
|
+
"@types/jest": "29.5.14",
|
|
56
61
|
"@types/node": "18.7.13",
|
|
57
62
|
"@typescript-eslint/eslint-plugin": "5.34.0",
|
|
58
63
|
"@typescript-eslint/parser": "5.48.0",
|
|
@@ -69,11 +74,12 @@
|
|
|
69
74
|
"eslint-import-resolver-typescript": "3.5.5",
|
|
70
75
|
"eslint-plugin-eslint-plugin": "5.0.0",
|
|
71
76
|
"eslint-plugin-import": "2.26.0",
|
|
72
|
-
"eslint-plugin-node": "11.1.0",
|
|
73
77
|
"eslint-plugin-jsdoc": "44.0.0",
|
|
78
|
+
"eslint-plugin-node": "11.1.0",
|
|
74
79
|
"eslint-plugin-prettier": "4.2.1",
|
|
75
80
|
"eslint-plugin-security": "1.5.0",
|
|
76
81
|
"fs": "0.0.1-security",
|
|
82
|
+
"husky": "9.1.7",
|
|
77
83
|
"jest": "29.3.1",
|
|
78
84
|
"jest-junit": "14.0.0",
|
|
79
85
|
"jsonc-eslint-parser": "2.3.0",
|
|
@@ -85,9 +91,7 @@
|
|
|
85
91
|
"semantic-release": "19.0.3",
|
|
86
92
|
"ts-jest": "29.0.5",
|
|
87
93
|
"ts-node": "10.9.1",
|
|
88
|
-
"typescript": "4.9.5"
|
|
89
|
-
"@types/jest": "^29.3.1",
|
|
90
|
-
"@semantic-release/exec": "^6.0.3"
|
|
94
|
+
"typescript": "4.9.5"
|
|
91
95
|
},
|
|
92
96
|
"peerDependencies": {
|
|
93
97
|
"eslint": ">=7"
|