@blumintinc/eslint-plugin-blumint 1.3.0 → 1.3.1
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,14 +18,14 @@ 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: [],
|
|
@@ -109,12 +109,16 @@ exports.enforceFirestoreDocRefGeneric = (0, createRule_1.createRule)({
|
|
|
109
109
|
return {
|
|
110
110
|
TSTypeReference(node) {
|
|
111
111
|
if (node.typeName.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
112
|
-
node.typeName.name === 'DocumentReference'
|
|
112
|
+
(node.typeName.name === 'DocumentReference' ||
|
|
113
|
+
node.typeName.name === 'CollectionReference' ||
|
|
114
|
+
node.typeName.name === 'CollectionGroup')) {
|
|
115
|
+
const typeName = node.typeName.name;
|
|
113
116
|
// Check if generic type argument is missing
|
|
114
117
|
if (!node.typeParameters || node.typeParameters.params.length === 0) {
|
|
115
118
|
context.report({
|
|
116
119
|
node,
|
|
117
120
|
messageId: 'missingGeneric',
|
|
121
|
+
data: { type: typeName }
|
|
118
122
|
});
|
|
119
123
|
return;
|
|
120
124
|
}
|
|
@@ -124,6 +128,7 @@ exports.enforceFirestoreDocRefGeneric = (0, createRule_1.createRule)({
|
|
|
124
128
|
context.report({
|
|
125
129
|
node,
|
|
126
130
|
messageId: 'invalidGeneric',
|
|
131
|
+
data: { type: typeName }
|
|
127
132
|
});
|
|
128
133
|
}
|
|
129
134
|
}
|
|
@@ -42,22 +42,31 @@ function isArrayOrPrimitive(checker, esTreeNode, nodeMap) {
|
|
|
42
42
|
function getObjectUsagesInHook(hookBody, objectName) {
|
|
43
43
|
const usages = new Set();
|
|
44
44
|
const visited = new Set();
|
|
45
|
+
let needsEntireObject = false;
|
|
45
46
|
function buildAccessPath(node) {
|
|
46
47
|
const parts = [];
|
|
47
48
|
let current = node;
|
|
49
|
+
let hasOptionalChaining = false;
|
|
50
|
+
// Collect all parts from leaf to root
|
|
48
51
|
while (current.type === utils_1.AST_NODE_TYPES.MemberExpression) {
|
|
49
|
-
|
|
52
|
+
const memberExpr = current;
|
|
53
|
+
if (memberExpr.computed) {
|
|
50
54
|
return null; // Skip computed properties
|
|
51
55
|
}
|
|
52
|
-
if (
|
|
56
|
+
if (memberExpr.property.type !== utils_1.AST_NODE_TYPES.Identifier) {
|
|
53
57
|
return null;
|
|
54
58
|
}
|
|
55
|
-
parts.unshift(
|
|
56
|
-
|
|
59
|
+
parts.unshift(memberExpr.property.name);
|
|
60
|
+
if (memberExpr.optional) {
|
|
61
|
+
hasOptionalChaining = true;
|
|
62
|
+
}
|
|
63
|
+
current = memberExpr.object;
|
|
57
64
|
}
|
|
58
|
-
if
|
|
59
|
-
|
|
60
|
-
|
|
65
|
+
// Check if we reached the target identifier
|
|
66
|
+
if (current.type === utils_1.AST_NODE_TYPES.Identifier && current.name === objectName) {
|
|
67
|
+
// Build the path with optional chaining
|
|
68
|
+
const path = objectName + (hasOptionalChaining ? '?' : '') + parts.map(part => '.' + part).join('');
|
|
69
|
+
return path;
|
|
61
70
|
}
|
|
62
71
|
return null;
|
|
63
72
|
}
|
|
@@ -65,10 +74,27 @@ function getObjectUsagesInHook(hookBody, objectName) {
|
|
|
65
74
|
if (visited.has(node))
|
|
66
75
|
return;
|
|
67
76
|
visited.add(node);
|
|
68
|
-
if (node.type === utils_1.AST_NODE_TYPES.
|
|
77
|
+
if (node.type === utils_1.AST_NODE_TYPES.CallExpression) {
|
|
78
|
+
// Check if the object is directly passed as an argument
|
|
79
|
+
node.arguments.forEach((arg) => {
|
|
80
|
+
if (arg.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
81
|
+
arg.name === objectName) {
|
|
82
|
+
needsEntireObject = true;
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
else if (node.type === utils_1.AST_NODE_TYPES.SpreadElement) {
|
|
87
|
+
// If we find a spread operator with our target object, consider it as accessing all properties
|
|
88
|
+
if (node.argument.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
89
|
+
node.argument.name === objectName) {
|
|
90
|
+
needsEntireObject = true;
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
else if (node.type === utils_1.AST_NODE_TYPES.MemberExpression) {
|
|
69
95
|
const path = buildAccessPath(node);
|
|
70
96
|
if (path) {
|
|
71
|
-
usages.add(
|
|
97
|
+
usages.add(path);
|
|
72
98
|
}
|
|
73
99
|
}
|
|
74
100
|
// Visit all child nodes
|
|
@@ -90,6 +116,10 @@ function getObjectUsagesInHook(hookBody, objectName) {
|
|
|
90
116
|
}
|
|
91
117
|
}
|
|
92
118
|
visit(hookBody);
|
|
119
|
+
// If the entire object is needed, return an empty set to indicate valid usage
|
|
120
|
+
if (needsEntireObject) {
|
|
121
|
+
return new Set();
|
|
122
|
+
}
|
|
93
123
|
// Filter out intermediate paths
|
|
94
124
|
const paths = Array.from(usages);
|
|
95
125
|
const filteredPaths = paths.filter((path) => !paths.some((otherPath) => otherPath !== path && otherPath.startsWith(path + '.')));
|
|
@@ -145,6 +175,7 @@ exports.noEntireObjectHookDeps = (0, createRule_1.createRule)({
|
|
|
145
175
|
}
|
|
146
176
|
const usages = getObjectUsagesInHook(callbackArg.body, objectName);
|
|
147
177
|
// If we found specific field usages and the entire object is in deps
|
|
178
|
+
// Skip reporting if usages is empty (indicates spread operator usage)
|
|
148
179
|
if (usages.size > 0) {
|
|
149
180
|
const fields = Array.from(usages).join(', ');
|
|
150
181
|
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',
|