@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
@@ -43,7 +43,7 @@ const enforce_mock_firestore_1 = require("./rules/enforce-mock-firestore");
43
43
  module.exports = {
44
44
  meta: {
45
45
  name: '@blumintinc/eslint-plugin-blumint',
46
- version: '1.3.0',
46
+ version: '1.3.1',
47
47
  },
48
48
  parseOptions: {
49
49
  ecmaVersion: 2020,
@@ -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: 'DocumentReference must specify a generic type argument',
28
- invalidGeneric: 'DocumentReference must not use "any" or "{}" as generic type argument',
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
- if (current.computed) {
52
+ const memberExpr = current;
53
+ if (memberExpr.computed) {
50
54
  return null; // Skip computed properties
51
55
  }
52
- if (current.property.type !== utils_1.AST_NODE_TYPES.Identifier) {
56
+ if (memberExpr.property.type !== utils_1.AST_NODE_TYPES.Identifier) {
53
57
  return null;
54
58
  }
55
- parts.unshift(current.property.name);
56
- current = current.object;
59
+ parts.unshift(memberExpr.property.name);
60
+ if (memberExpr.optional) {
61
+ hasOptionalChaining = true;
62
+ }
63
+ current = memberExpr.object;
57
64
  }
58
- if (current.type === utils_1.AST_NODE_TYPES.Identifier &&
59
- current.name === objectName) {
60
- return parts.join('.');
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.MemberExpression) {
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(`${objectName}.${path}`);
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
- hasFirebaseAdminImport = true;
32
- // Track the local name of the https import
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
- if (isHttpsError || isFirebaseHttpsError) {
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',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blumintinc/eslint-plugin-blumint",
3
- "version": "1.3.0",
3
+ "version": "1.3.1",
4
4
  "description": "Custom eslint rules for use within BluMint",
5
5
  "author": {
6
6
  "name": "Brodie McGuire",