@blumintinc/eslint-plugin-blumint 1.3.0 → 1.3.2
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
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @fileoverview Enforce generic argument for Firestore DocumentReference
|
|
2
|
+
* @fileoverview Enforce generic argument for Firestore DocumentReference, CollectionReference and CollectionGroup
|
|
3
3
|
* @author BluMint
|
|
4
4
|
*/
|
|
5
5
|
type MessageIds = 'missingGeneric' | 'invalidGeneric';
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
/**
|
|
3
|
-
* @fileoverview Enforce generic argument for Firestore DocumentReference
|
|
3
|
+
* @fileoverview Enforce generic argument for Firestore DocumentReference, CollectionReference and CollectionGroup
|
|
4
4
|
* @author BluMint
|
|
5
5
|
*/
|
|
6
6
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
@@ -18,19 +18,20 @@ exports.enforceFirestoreDocRefGeneric = (0, createRule_1.createRule)({
|
|
|
18
18
|
meta: {
|
|
19
19
|
type: 'problem',
|
|
20
20
|
docs: {
|
|
21
|
-
description: 'Enforce generic argument for Firestore DocumentReference',
|
|
21
|
+
description: 'Enforce generic argument for Firestore DocumentReference, CollectionReference and CollectionGroup',
|
|
22
22
|
recommended: 'error',
|
|
23
23
|
requiresTypeChecking: true,
|
|
24
24
|
},
|
|
25
25
|
schema: [],
|
|
26
26
|
messages: {
|
|
27
|
-
missingGeneric: '
|
|
28
|
-
invalidGeneric: '
|
|
27
|
+
missingGeneric: '{{ type }} must specify a generic type argument',
|
|
28
|
+
invalidGeneric: '{{ type }} must not use "any" or "{}" as generic type argument',
|
|
29
29
|
},
|
|
30
30
|
},
|
|
31
31
|
defaultOptions: [],
|
|
32
32
|
create(context) {
|
|
33
33
|
const typeCache = new Map();
|
|
34
|
+
const nodeCache = new WeakMap();
|
|
34
35
|
function hasInvalidType(node) {
|
|
35
36
|
if (!node)
|
|
36
37
|
return false;
|
|
@@ -106,15 +107,97 @@ exports.enforceFirestoreDocRefGeneric = (0, createRule_1.createRule)({
|
|
|
106
107
|
return false;
|
|
107
108
|
}
|
|
108
109
|
}
|
|
110
|
+
function hasTypeAnnotation(node) {
|
|
111
|
+
if (nodeCache.has(node)) {
|
|
112
|
+
return nodeCache.get(node);
|
|
113
|
+
}
|
|
114
|
+
let current = node;
|
|
115
|
+
while (current) {
|
|
116
|
+
// Variable declarations with type annotations
|
|
117
|
+
if (current.type === utils_1.AST_NODE_TYPES.VariableDeclarator && current.id.typeAnnotation) {
|
|
118
|
+
nodeCache.set(node, true);
|
|
119
|
+
return true;
|
|
120
|
+
}
|
|
121
|
+
// Class property definitions with type annotations
|
|
122
|
+
if (current.type === utils_1.AST_NODE_TYPES.PropertyDefinition && current.typeAnnotation) {
|
|
123
|
+
nodeCache.set(node, true);
|
|
124
|
+
return true;
|
|
125
|
+
}
|
|
126
|
+
// Return statements in functions with return type annotations
|
|
127
|
+
if (current.type === utils_1.AST_NODE_TYPES.ReturnStatement) {
|
|
128
|
+
const func = current.parent?.parent;
|
|
129
|
+
if (func?.type === utils_1.AST_NODE_TYPES.FunctionDeclaration && func.returnType) {
|
|
130
|
+
nodeCache.set(node, true);
|
|
131
|
+
return true;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
// Assignment expressions to class properties
|
|
135
|
+
if (current.type === utils_1.AST_NODE_TYPES.AssignmentExpression) {
|
|
136
|
+
const left = current.left;
|
|
137
|
+
if (left.type === utils_1.AST_NODE_TYPES.MemberExpression) {
|
|
138
|
+
const obj = left.object;
|
|
139
|
+
if (obj.type === utils_1.AST_NODE_TYPES.ThisExpression) {
|
|
140
|
+
const classNode = findParentClass(current);
|
|
141
|
+
if (classNode) {
|
|
142
|
+
const property = classNode.body.body.find((member) => member.type === utils_1.AST_NODE_TYPES.PropertyDefinition &&
|
|
143
|
+
member.key.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
144
|
+
member.key.name === left.property.name);
|
|
145
|
+
if (property?.typeAnnotation) {
|
|
146
|
+
nodeCache.set(node, true);
|
|
147
|
+
return true;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
current = current.parent;
|
|
154
|
+
}
|
|
155
|
+
nodeCache.set(node, false);
|
|
156
|
+
return false;
|
|
157
|
+
}
|
|
158
|
+
function findParentClass(node) {
|
|
159
|
+
let current = node;
|
|
160
|
+
while (current) {
|
|
161
|
+
if (current.type === utils_1.AST_NODE_TYPES.ClassDeclaration) {
|
|
162
|
+
return current;
|
|
163
|
+
}
|
|
164
|
+
current = current.parent;
|
|
165
|
+
}
|
|
166
|
+
return undefined;
|
|
167
|
+
}
|
|
168
|
+
function isPartOfMethodChain(node) {
|
|
169
|
+
if (node.callee.type !== utils_1.AST_NODE_TYPES.MemberExpression) {
|
|
170
|
+
return false;
|
|
171
|
+
}
|
|
172
|
+
// Check if this node is part of a method chain as the object
|
|
173
|
+
const obj = node.callee.object;
|
|
174
|
+
if (obj.type === utils_1.AST_NODE_TYPES.CallExpression) {
|
|
175
|
+
return true;
|
|
176
|
+
}
|
|
177
|
+
// Check if this node is part of a method chain as the callee
|
|
178
|
+
let current = node;
|
|
179
|
+
while (current) {
|
|
180
|
+
if (current.parent?.type === utils_1.AST_NODE_TYPES.MemberExpression &&
|
|
181
|
+
current.parent.parent?.type === utils_1.AST_NODE_TYPES.CallExpression) {
|
|
182
|
+
return true;
|
|
183
|
+
}
|
|
184
|
+
current = current.parent;
|
|
185
|
+
}
|
|
186
|
+
return false;
|
|
187
|
+
}
|
|
109
188
|
return {
|
|
110
189
|
TSTypeReference(node) {
|
|
111
190
|
if (node.typeName.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
112
|
-
node.typeName.name === 'DocumentReference'
|
|
191
|
+
(node.typeName.name === 'DocumentReference' ||
|
|
192
|
+
node.typeName.name === 'CollectionReference' ||
|
|
193
|
+
node.typeName.name === 'CollectionGroup')) {
|
|
194
|
+
const typeName = node.typeName.name;
|
|
113
195
|
// Check if generic type argument is missing
|
|
114
196
|
if (!node.typeParameters || node.typeParameters.params.length === 0) {
|
|
115
197
|
context.report({
|
|
116
198
|
node,
|
|
117
199
|
messageId: 'missingGeneric',
|
|
200
|
+
data: { type: typeName }
|
|
118
201
|
});
|
|
119
202
|
return;
|
|
120
203
|
}
|
|
@@ -124,6 +207,74 @@ exports.enforceFirestoreDocRefGeneric = (0, createRule_1.createRule)({
|
|
|
124
207
|
context.report({
|
|
125
208
|
node,
|
|
126
209
|
messageId: 'invalidGeneric',
|
|
210
|
+
data: { type: typeName }
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
},
|
|
215
|
+
CallExpression(node) {
|
|
216
|
+
// Only check method calls if there's no type annotation
|
|
217
|
+
if (hasTypeAnnotation(node)) {
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
// Check for .doc() calls
|
|
221
|
+
if (node.callee.type === utils_1.AST_NODE_TYPES.MemberExpression &&
|
|
222
|
+
node.callee.property.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
223
|
+
node.callee.property.name === 'doc') {
|
|
224
|
+
const typeAnnotation = node.typeParameters;
|
|
225
|
+
if (!typeAnnotation) {
|
|
226
|
+
context.report({
|
|
227
|
+
node,
|
|
228
|
+
messageId: 'missingGeneric',
|
|
229
|
+
data: { type: 'DocumentReference' }
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
else if (hasInvalidType(typeAnnotation.params[0])) {
|
|
233
|
+
context.report({
|
|
234
|
+
node,
|
|
235
|
+
messageId: 'invalidGeneric',
|
|
236
|
+
data: { type: 'DocumentReference' }
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
// Check for .collection() calls
|
|
241
|
+
else if (node.callee.type === utils_1.AST_NODE_TYPES.MemberExpression &&
|
|
242
|
+
node.callee.property.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
243
|
+
node.callee.property.name === 'collection' &&
|
|
244
|
+
!isPartOfMethodChain(node)) {
|
|
245
|
+
const typeAnnotation = node.typeParameters;
|
|
246
|
+
if (!typeAnnotation) {
|
|
247
|
+
context.report({
|
|
248
|
+
node,
|
|
249
|
+
messageId: 'missingGeneric',
|
|
250
|
+
data: { type: 'CollectionReference' }
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
else if (hasInvalidType(typeAnnotation.params[0])) {
|
|
254
|
+
context.report({
|
|
255
|
+
node,
|
|
256
|
+
messageId: 'invalidGeneric',
|
|
257
|
+
data: { type: 'CollectionReference' }
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
// Check for .collectionGroup() calls
|
|
262
|
+
else if (node.callee.type === utils_1.AST_NODE_TYPES.MemberExpression &&
|
|
263
|
+
node.callee.property.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
264
|
+
node.callee.property.name === 'collectionGroup') {
|
|
265
|
+
const typeAnnotation = node.typeParameters;
|
|
266
|
+
if (!typeAnnotation) {
|
|
267
|
+
context.report({
|
|
268
|
+
node,
|
|
269
|
+
messageId: 'missingGeneric',
|
|
270
|
+
data: { type: 'CollectionGroup' }
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
else if (hasInvalidType(typeAnnotation.params[0])) {
|
|
274
|
+
context.report({
|
|
275
|
+
node,
|
|
276
|
+
messageId: 'invalidGeneric',
|
|
277
|
+
data: { type: 'CollectionGroup' }
|
|
127
278
|
});
|
|
128
279
|
}
|
|
129
280
|
}
|
|
@@ -17,6 +17,8 @@ function isArrayOrPrimitive(checker, esTreeNode, nodeMap) {
|
|
|
17
17
|
// Check if it's a primitive type
|
|
18
18
|
if (type.flags &
|
|
19
19
|
(typescript_1.TypeFlags.String |
|
|
20
|
+
typescript_1.TypeFlags.StringLike |
|
|
21
|
+
typescript_1.TypeFlags.StringLiteral |
|
|
20
22
|
typescript_1.TypeFlags.Number |
|
|
21
23
|
typescript_1.TypeFlags.Boolean |
|
|
22
24
|
typescript_1.TypeFlags.Null |
|
|
@@ -36,28 +38,41 @@ function isArrayOrPrimitive(checker, esTreeNode, nodeMap) {
|
|
|
36
38
|
(typeNode && ((0, typescript_1.isArrayTypeNode)(typeNode) || (0, typescript_1.isTupleTypeNode)(typeNode)))) {
|
|
37
39
|
return true;
|
|
38
40
|
}
|
|
41
|
+
// Check if it's a string type with methods (like String object)
|
|
42
|
+
if (type.symbol?.name === 'String' || type.symbol?.escapedName === 'String') {
|
|
43
|
+
return true;
|
|
44
|
+
}
|
|
39
45
|
// If it's not a primitive or array, and has properties, it's an object
|
|
40
46
|
return false;
|
|
41
47
|
}
|
|
42
48
|
function getObjectUsagesInHook(hookBody, objectName) {
|
|
43
49
|
const usages = new Set();
|
|
44
50
|
const visited = new Set();
|
|
51
|
+
let needsEntireObject = false;
|
|
45
52
|
function buildAccessPath(node) {
|
|
46
53
|
const parts = [];
|
|
47
54
|
let current = node;
|
|
55
|
+
let hasOptionalChaining = false;
|
|
56
|
+
// Collect all parts from leaf to root
|
|
48
57
|
while (current.type === utils_1.AST_NODE_TYPES.MemberExpression) {
|
|
49
|
-
|
|
58
|
+
const memberExpr = current;
|
|
59
|
+
if (memberExpr.computed) {
|
|
50
60
|
return null; // Skip computed properties
|
|
51
61
|
}
|
|
52
|
-
if (
|
|
62
|
+
if (memberExpr.property.type !== utils_1.AST_NODE_TYPES.Identifier) {
|
|
53
63
|
return null;
|
|
54
64
|
}
|
|
55
|
-
parts.unshift(
|
|
56
|
-
|
|
65
|
+
parts.unshift(memberExpr.property.name);
|
|
66
|
+
if (memberExpr.optional) {
|
|
67
|
+
hasOptionalChaining = true;
|
|
68
|
+
}
|
|
69
|
+
current = memberExpr.object;
|
|
57
70
|
}
|
|
58
|
-
if
|
|
59
|
-
|
|
60
|
-
|
|
71
|
+
// Check if we reached the target identifier
|
|
72
|
+
if (current.type === utils_1.AST_NODE_TYPES.Identifier && current.name === objectName) {
|
|
73
|
+
// Build the path with optional chaining
|
|
74
|
+
const path = objectName + (hasOptionalChaining ? '?' : '') + parts.map(part => '.' + part).join('');
|
|
75
|
+
return path;
|
|
61
76
|
}
|
|
62
77
|
return null;
|
|
63
78
|
}
|
|
@@ -65,10 +80,27 @@ function getObjectUsagesInHook(hookBody, objectName) {
|
|
|
65
80
|
if (visited.has(node))
|
|
66
81
|
return;
|
|
67
82
|
visited.add(node);
|
|
68
|
-
if (node.type === utils_1.AST_NODE_TYPES.
|
|
83
|
+
if (node.type === utils_1.AST_NODE_TYPES.CallExpression) {
|
|
84
|
+
// Check if the object is directly passed as an argument
|
|
85
|
+
node.arguments.forEach((arg) => {
|
|
86
|
+
if (arg.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
87
|
+
arg.name === objectName) {
|
|
88
|
+
needsEntireObject = true;
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
else if (node.type === utils_1.AST_NODE_TYPES.SpreadElement) {
|
|
93
|
+
// If we find a spread operator with our target object, consider it as accessing all properties
|
|
94
|
+
if (node.argument.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
95
|
+
node.argument.name === objectName) {
|
|
96
|
+
needsEntireObject = true;
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
else if (node.type === utils_1.AST_NODE_TYPES.MemberExpression) {
|
|
69
101
|
const path = buildAccessPath(node);
|
|
70
102
|
if (path) {
|
|
71
|
-
usages.add(
|
|
103
|
+
usages.add(path);
|
|
72
104
|
}
|
|
73
105
|
}
|
|
74
106
|
// Visit all child nodes
|
|
@@ -90,6 +122,10 @@ function getObjectUsagesInHook(hookBody, objectName) {
|
|
|
90
122
|
}
|
|
91
123
|
}
|
|
92
124
|
visit(hookBody);
|
|
125
|
+
// If the entire object is needed, return an empty set to indicate valid usage
|
|
126
|
+
if (needsEntireObject) {
|
|
127
|
+
return new Set();
|
|
128
|
+
}
|
|
93
129
|
// Filter out intermediate paths
|
|
94
130
|
const paths = Array.from(usages);
|
|
95
131
|
const filteredPaths = paths.filter((path) => !paths.some((otherPath) => otherPath !== path && otherPath.startsWith(path + '.')));
|
|
@@ -100,8 +136,9 @@ exports.noEntireObjectHookDeps = (0, createRule_1.createRule)({
|
|
|
100
136
|
meta: {
|
|
101
137
|
type: 'suggestion',
|
|
102
138
|
docs: {
|
|
103
|
-
description: 'Avoid using entire objects in React hook dependency arrays when only specific fields are used',
|
|
139
|
+
description: 'Avoid using entire objects in React hook dependency arrays when only specific fields are used. Requires TypeScript and `parserOptions.project` to be configured.',
|
|
104
140
|
recommended: 'error',
|
|
141
|
+
requiresTypeChecking: true,
|
|
105
142
|
},
|
|
106
143
|
fixable: 'code',
|
|
107
144
|
schema: [],
|
|
@@ -145,6 +182,7 @@ exports.noEntireObjectHookDeps = (0, createRule_1.createRule)({
|
|
|
145
182
|
}
|
|
146
183
|
const usages = getObjectUsagesInHook(callbackArg.body, objectName);
|
|
147
184
|
// If we found specific field usages and the entire object is in deps
|
|
185
|
+
// Skip reporting if usages is empty (indicates spread operator usage)
|
|
148
186
|
if (usages.size > 0) {
|
|
149
187
|
const fields = Array.from(usages).join(', ');
|
|
150
188
|
context.report({
|
|
@@ -22,23 +22,32 @@ module.exports = (0, createRule_1.createRule)({
|
|
|
22
22
|
if (!filename.includes('functions/src')) {
|
|
23
23
|
return {};
|
|
24
24
|
}
|
|
25
|
-
let hasFirebaseAdminImport = false;
|
|
26
25
|
let httpsIdentifier = null;
|
|
26
|
+
let httpsErrorIdentifier = null;
|
|
27
27
|
return {
|
|
28
28
|
ImportDeclaration(node) {
|
|
29
29
|
if (node.source.value === 'firebase-admin' ||
|
|
30
30
|
node.source.value === 'firebase-admin/lib/https-error') {
|
|
31
|
-
|
|
32
|
-
|
|
31
|
+
// Check for direct HttpsError import
|
|
32
|
+
const httpsErrorSpecifier = node.specifiers.find((spec) => spec.type === utils_1.AST_NODE_TYPES.ImportSpecifier &&
|
|
33
|
+
spec.imported.name === 'HttpsError');
|
|
34
|
+
// Check for https import that could be used for https.HttpsError
|
|
33
35
|
const httpsSpecifier = node.specifiers.find((spec) => spec.type === utils_1.AST_NODE_TYPES.ImportSpecifier &&
|
|
34
36
|
spec.imported.name === 'https');
|
|
37
|
+
if (httpsErrorSpecifier && 'local' in httpsErrorSpecifier) {
|
|
38
|
+
httpsErrorIdentifier = httpsErrorSpecifier.local.name;
|
|
39
|
+
context.report({
|
|
40
|
+
node,
|
|
41
|
+
messageId: 'useProprietaryHttpsError',
|
|
42
|
+
});
|
|
43
|
+
}
|
|
35
44
|
if (httpsSpecifier && 'local' in httpsSpecifier) {
|
|
36
45
|
httpsIdentifier = httpsSpecifier.local.name;
|
|
46
|
+
context.report({
|
|
47
|
+
node,
|
|
48
|
+
messageId: 'useProprietaryHttpsError',
|
|
49
|
+
});
|
|
37
50
|
}
|
|
38
|
-
context.report({
|
|
39
|
-
node,
|
|
40
|
-
messageId: 'useProprietaryHttpsError',
|
|
41
|
-
});
|
|
42
51
|
}
|
|
43
52
|
},
|
|
44
53
|
ThrowStatement(node) {
|
|
@@ -60,17 +69,14 @@ module.exports = (0, createRule_1.createRule)({
|
|
|
60
69
|
return;
|
|
61
70
|
}
|
|
62
71
|
// Check for firebase-admin HttpsError usage
|
|
63
|
-
if (!hasFirebaseAdminImport) {
|
|
64
|
-
return;
|
|
65
|
-
}
|
|
66
|
-
const isHttpsError = callee.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
67
|
-
callee.name === 'HttpsError';
|
|
68
72
|
const isFirebaseHttpsError = callee.type === utils_1.AST_NODE_TYPES.MemberExpression &&
|
|
69
73
|
callee.object.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
70
74
|
callee.object.name === httpsIdentifier &&
|
|
71
75
|
callee.property.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
72
76
|
callee.property.name === 'HttpsError';
|
|
73
|
-
|
|
77
|
+
const isDirectHttpsError = callee.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
78
|
+
callee.name === httpsErrorIdentifier;
|
|
79
|
+
if (isFirebaseHttpsError || isDirectHttpsError) {
|
|
74
80
|
context.report({
|
|
75
81
|
node,
|
|
76
82
|
messageId: 'useProprietaryHttpsError',
|