@blumintinc/eslint-plugin-blumint 1.5.5 → 1.6.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/lib/index.js CHANGED
@@ -56,10 +56,16 @@ const no_firestore_object_arrays_1 = require("./rules/no-firestore-object-arrays
56
56
  const no_memoize_on_static_1 = require("./rules/no-memoize-on-static");
57
57
  const no_unsafe_firestore_spread_1 = require("./rules/no-unsafe-firestore-spread");
58
58
  const no_jsx_in_hooks_1 = require("./rules/no-jsx-in-hooks");
59
+ const enforce_assert_throws_1 = require("./rules/enforce-assert-throws");
60
+ const prefer_batch_operations_1 = require("./rules/prefer-batch-operations");
61
+ const no_complex_cloud_params_1 = require("./rules/no-complex-cloud-params");
62
+ const no_mixed_firestore_transactions_1 = require("./rules/no-mixed-firestore-transactions");
63
+ const enforce_firestore_facade_1 = require("./rules/enforce-firestore-facade");
64
+ const sync_onwrite_name_func_1 = require("./rules/sync-onwrite-name-func");
59
65
  module.exports = {
60
66
  meta: {
61
67
  name: '@blumintinc/eslint-plugin-blumint',
62
- version: '1.5.5',
68
+ version: '1.6.0',
63
69
  },
64
70
  parseOptions: {
65
71
  ecmaVersion: 2020,
@@ -121,6 +127,12 @@ module.exports = {
121
127
  '@blumintinc/blumint/no-memoize-on-static': 'error',
122
128
  '@blumintinc/blumint/no-unsafe-firestore-spread': 'error',
123
129
  '@blumintinc/blumint/no-jsx-in-hooks': 'error',
130
+ '@blumintinc/blumint/enforce-assert-throws': 'error',
131
+ '@blumintinc/blumint/prefer-batch-operations': 'error',
132
+ '@blumintinc/blumint/no-complex-cloud-params': 'error',
133
+ '@blumintinc/blumint/no-mixed-firestore-transactions': 'error',
134
+ '@blumintinc/blumint/enforce-firestore-facade': 'error',
135
+ '@blumintinc/blumint/sync-onwrite-name-func': 'error',
124
136
  },
125
137
  },
126
138
  },
@@ -178,6 +190,12 @@ module.exports = {
178
190
  'no-memoize-on-static': no_memoize_on_static_1.noMemoizeOnStatic,
179
191
  'no-unsafe-firestore-spread': no_unsafe_firestore_spread_1.noUnsafeFirestoreSpread,
180
192
  'no-jsx-in-hooks': no_jsx_in_hooks_1.noJsxInHooks,
193
+ 'enforce-assert-throws': enforce_assert_throws_1.enforceAssertThrows,
194
+ 'prefer-batch-operations': prefer_batch_operations_1.preferBatchOperations,
195
+ 'no-complex-cloud-params': no_complex_cloud_params_1.noComplexCloudParams,
196
+ 'no-mixed-firestore-transactions': no_mixed_firestore_transactions_1.noMixedFirestoreTransactions,
197
+ 'enforce-firestore-facade': enforce_firestore_facade_1.enforceFirestoreFacade,
198
+ 'sync-onwrite-name-func': sync_onwrite_name_func_1.syncOnwriteNameFunc,
181
199
  },
182
200
  };
183
201
  //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ export declare const enforceAssertThrows: import("@typescript-eslint/utils/dist/ts-eslint/Rule").RuleModule<"assertShouldThrow", [], import("@typescript-eslint/utils/dist/ts-eslint/Rule").RuleListener>;
@@ -0,0 +1,109 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.enforceAssertThrows = void 0;
4
+ const utils_1 = require("@typescript-eslint/utils");
5
+ const createRule_1 = require("../utils/createRule");
6
+ exports.enforceAssertThrows = (0, createRule_1.createRule)({
7
+ name: 'enforce-assert-throws',
8
+ meta: {
9
+ type: 'problem',
10
+ docs: {
11
+ description: 'Enforce that functions with assert- prefix must throw an error',
12
+ recommended: 'error',
13
+ },
14
+ schema: [],
15
+ messages: {
16
+ assertShouldThrow: 'Functions with assert- prefix must throw an error. Either rename the function or add a throw statement.',
17
+ },
18
+ },
19
+ defaultOptions: [],
20
+ create(context) {
21
+ function hasThrowStatement(node) {
22
+ let hasThrow = false;
23
+ function walk(node) {
24
+ if (node.type === utils_1.AST_NODE_TYPES.ThrowStatement) {
25
+ hasThrow = true;
26
+ return;
27
+ }
28
+ // Don't check throw statements in nested functions
29
+ if (node.type === utils_1.AST_NODE_TYPES.FunctionDeclaration ||
30
+ node.type === utils_1.AST_NODE_TYPES.FunctionExpression ||
31
+ node.type === utils_1.AST_NODE_TYPES.ArrowFunctionExpression) {
32
+ if (node !== currentFunction) {
33
+ return;
34
+ }
35
+ }
36
+ // Don't count throws in catch blocks that are just re-throwing
37
+ if (node.type === utils_1.AST_NODE_TYPES.CatchClause) {
38
+ return;
39
+ }
40
+ // Handle BlockStatement specially
41
+ if (node.type === utils_1.AST_NODE_TYPES.BlockStatement) {
42
+ node.body.forEach(stmt => walk(stmt));
43
+ return;
44
+ }
45
+ // Handle IfStatement specially
46
+ if (node.type === utils_1.AST_NODE_TYPES.IfStatement) {
47
+ walk(node.consequent);
48
+ if (node.alternate) {
49
+ walk(node.alternate);
50
+ }
51
+ return;
52
+ }
53
+ // Handle other node types
54
+ for (const key of Object.keys(node)) {
55
+ const value = node[key];
56
+ if (Array.isArray(value)) {
57
+ value.forEach(item => {
58
+ if (item && typeof item === 'object' && !('parent' in item)) {
59
+ walk(item);
60
+ }
61
+ });
62
+ }
63
+ else if (value && typeof value === 'object' && !('parent' in value)) {
64
+ walk(value);
65
+ }
66
+ }
67
+ }
68
+ walk(node);
69
+ return hasThrow;
70
+ }
71
+ let currentFunction = null;
72
+ function checkFunction(node) {
73
+ let functionName = '';
74
+ if (node.type === utils_1.AST_NODE_TYPES.MethodDefinition) {
75
+ functionName = node.key.type === utils_1.AST_NODE_TYPES.Identifier ? node.key.name : '';
76
+ }
77
+ else if (node.type === utils_1.AST_NODE_TYPES.FunctionDeclaration && node.id) {
78
+ functionName = node.id.name;
79
+ }
80
+ else if (node.type === utils_1.AST_NODE_TYPES.FunctionExpression ||
81
+ node.type === utils_1.AST_NODE_TYPES.ArrowFunctionExpression) {
82
+ const parent = node.parent;
83
+ if (parent &&
84
+ parent.type === utils_1.AST_NODE_TYPES.VariableDeclarator &&
85
+ parent.id.type === utils_1.AST_NODE_TYPES.Identifier) {
86
+ functionName = parent.id.name;
87
+ }
88
+ }
89
+ if (functionName.toLowerCase().startsWith('assert')) {
90
+ currentFunction = node;
91
+ const functionBody = node.type === utils_1.AST_NODE_TYPES.MethodDefinition ? node.value.body : node.body;
92
+ if (functionBody && !hasThrowStatement(functionBody)) {
93
+ context.report({
94
+ node,
95
+ messageId: 'assertShouldThrow',
96
+ });
97
+ }
98
+ currentFunction = null;
99
+ }
100
+ }
101
+ return {
102
+ FunctionDeclaration: checkFunction,
103
+ FunctionExpression: checkFunction,
104
+ ArrowFunctionExpression: checkFunction,
105
+ MethodDefinition: checkFunction,
106
+ };
107
+ },
108
+ });
109
+ //# sourceMappingURL=enforce-assert-throws.js.map
@@ -45,9 +45,10 @@ exports.enforceExportedFunctionTypes = (0, createRule_1.createRule)({
45
45
  const typeParams = new Set();
46
46
  let current = node;
47
47
  while (current) {
48
- // Handle type parameters in function declarations
48
+ // Handle type parameters in function declarations, arrow functions, and variable declarations
49
49
  if (current.type === utils_1.AST_NODE_TYPES.ArrowFunctionExpression ||
50
- current.type === utils_1.AST_NODE_TYPES.FunctionDeclaration) {
50
+ current.type === utils_1.AST_NODE_TYPES.FunctionDeclaration ||
51
+ current.type === utils_1.AST_NODE_TYPES.VariableDeclarator) {
51
52
  if ('typeParameters' in current && current.typeParameters) {
52
53
  current.typeParameters.params.forEach((param) => {
53
54
  if (param.type === utils_1.AST_NODE_TYPES.TSTypeParameter) {
@@ -0,0 +1,3 @@
1
+ type MessageIds = 'noDirectGet' | 'noDirectSet' | 'noDirectUpdate' | 'noDirectDelete';
2
+ export declare const enforceFirestoreFacade: import("@typescript-eslint/utils/dist/ts-eslint/Rule").RuleModule<MessageIds, [], import("@typescript-eslint/utils/dist/ts-eslint/Rule").RuleListener>;
3
+ export {};
@@ -0,0 +1,126 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.enforceFirestoreFacade = void 0;
4
+ const utils_1 = require("@typescript-eslint/utils");
5
+ const createRule_1 = require("../utils/createRule");
6
+ const FIRESTORE_METHODS = new Set(['get', 'set', 'update', 'delete']);
7
+ const isMemberExpression = (node) => {
8
+ return node.type === utils_1.AST_NODE_TYPES.MemberExpression;
9
+ };
10
+ const isFirestoreMethodCall = (node) => {
11
+ if (!isMemberExpression(node.callee))
12
+ return false;
13
+ const property = node.callee.property;
14
+ if (!isIdentifier(property) || !FIRESTORE_METHODS.has(property.name)) {
15
+ return false;
16
+ }
17
+ // Check if the method is called on a facade instance
18
+ const object = node.callee.object;
19
+ if (isIdentifier(object)) {
20
+ const name = object.name;
21
+ // Skip if it's a facade instance
22
+ if (name.includes('Fetcher') || name.includes('Setter') || name.includes('Tx')) {
23
+ return false;
24
+ }
25
+ // Check for batch or transaction
26
+ if (/batch|transaction/i.test(name)) {
27
+ return true;
28
+ }
29
+ }
30
+ // Check if it's a Firestore reference
31
+ let current = object;
32
+ let foundDocOrCollection = false;
33
+ while (current) {
34
+ if (isCallExpression(current)) {
35
+ const callee = current.callee;
36
+ if (isMemberExpression(callee)) {
37
+ const property = callee.property;
38
+ if (isIdentifier(property) && (property.name === 'doc' || property.name === 'collection')) {
39
+ foundDocOrCollection = true;
40
+ break;
41
+ }
42
+ }
43
+ }
44
+ if (isMemberExpression(current)) {
45
+ current = current.object;
46
+ }
47
+ else {
48
+ break;
49
+ }
50
+ }
51
+ // If we haven't found a doc/collection call yet, check if the object is a variable
52
+ if (!foundDocOrCollection && isIdentifier(object)) {
53
+ const name = object.name;
54
+ // If the variable name contains 'doc' or 'ref', it's likely a Firestore reference
55
+ if (name.toLowerCase().includes('doc') || name.toLowerCase().includes('ref')) {
56
+ return true;
57
+ }
58
+ }
59
+ return foundDocOrCollection;
60
+ };
61
+ const isCallExpression = (node) => {
62
+ return node.type === utils_1.AST_NODE_TYPES.CallExpression;
63
+ };
64
+ const isIdentifier = (node) => {
65
+ return node.type === utils_1.AST_NODE_TYPES.Identifier;
66
+ };
67
+ exports.enforceFirestoreFacade = (0, createRule_1.createRule)({
68
+ name: 'enforce-firestore-facade',
69
+ meta: {
70
+ type: 'problem',
71
+ docs: {
72
+ description: 'Enforce usage of Firestore facades instead of direct Firestore methods',
73
+ recommended: 'error',
74
+ },
75
+ schema: [],
76
+ messages: {
77
+ noDirectGet: 'Use FirestoreFetcher or FirestoreDocFetcher instead of direct .get() calls',
78
+ noDirectSet: 'Use DocSetter or DocSetterTransaction instead of direct .set() calls',
79
+ noDirectUpdate: 'Use DocSetter or DocSetterTransaction instead of direct .update() calls',
80
+ noDirectDelete: 'Use DocSetter or DocSetterTransaction instead of direct .delete() calls',
81
+ },
82
+ },
83
+ defaultOptions: [],
84
+ create(context) {
85
+ return {
86
+ CallExpression(node) {
87
+ if (!isFirestoreMethodCall(node))
88
+ return;
89
+ const callee = node.callee;
90
+ if (!isMemberExpression(callee))
91
+ return;
92
+ const property = callee.property;
93
+ if (!isIdentifier(property))
94
+ return;
95
+ // Report appropriate error based on method
96
+ switch (property.name) {
97
+ case 'get':
98
+ context.report({
99
+ node,
100
+ messageId: 'noDirectGet',
101
+ });
102
+ break;
103
+ case 'set':
104
+ context.report({
105
+ node,
106
+ messageId: 'noDirectSet',
107
+ });
108
+ break;
109
+ case 'update':
110
+ context.report({
111
+ node,
112
+ messageId: 'noDirectUpdate',
113
+ });
114
+ break;
115
+ case 'delete':
116
+ context.report({
117
+ node,
118
+ messageId: 'noDirectDelete',
119
+ });
120
+ break;
121
+ }
122
+ },
123
+ };
124
+ },
125
+ });
126
+ //# sourceMappingURL=enforce-firestore-facade.js.map
@@ -41,8 +41,80 @@ exports.enforceFirestoreSetMerge = (0, createRule_1.createRule)({
41
41
  }
42
42
  }
43
43
  }
44
- // Only flag update() calls
45
- return property.name === 'update';
44
+ // Only flag update() calls that are Firestore operations
45
+ if (property.name === 'update') {
46
+ const object = node.callee.object;
47
+ // Check for BatchManager update calls
48
+ if (object.type === utils_1.AST_NODE_TYPES.MemberExpression &&
49
+ object.property.type === utils_1.AST_NODE_TYPES.Identifier &&
50
+ object.property.name === 'batchManager') {
51
+ return true;
52
+ }
53
+ if (object.type === utils_1.AST_NODE_TYPES.CallExpression) {
54
+ // Check if it's a createHash().update() call
55
+ if (object.callee.type === utils_1.AST_NODE_TYPES.Identifier &&
56
+ object.callee.name === 'createHash') {
57
+ return false;
58
+ }
59
+ }
60
+ // Check if it's a Firestore document reference or transaction
61
+ let current = node;
62
+ while (current?.parent) {
63
+ current = current.parent;
64
+ if (current.type === utils_1.AST_NODE_TYPES.MemberExpression) {
65
+ const obj = current.object;
66
+ if (obj.type === utils_1.AST_NODE_TYPES.Identifier) {
67
+ // Check for common Firestore variable names
68
+ if (obj.name === 'db' ||
69
+ obj.name === 'firestore' ||
70
+ obj.name === 'transaction' ||
71
+ obj.name === 'docRef' ||
72
+ obj.name === 'userRef' ||
73
+ obj.name.endsWith('Ref')) {
74
+ return true;
75
+ }
76
+ }
77
+ }
78
+ }
79
+ // Check if it's a Firestore document reference method chain
80
+ let currentObj = object;
81
+ while (currentObj.type === utils_1.AST_NODE_TYPES.MemberExpression) {
82
+ if (currentObj.property.type === utils_1.AST_NODE_TYPES.Identifier) {
83
+ const methodName = currentObj.property.name;
84
+ if (methodName === 'collection' || methodName === 'doc') {
85
+ return true;
86
+ }
87
+ }
88
+ currentObj = currentObj.object;
89
+ }
90
+ // Check if it's a transaction.update() call
91
+ if (object.type === utils_1.AST_NODE_TYPES.Identifier &&
92
+ object.name === 'transaction') {
93
+ return true;
94
+ }
95
+ // Check if it's a Firestore document reference by looking at imports
96
+ const program = context
97
+ .getAncestors()
98
+ .find((node) => node.type === utils_1.AST_NODE_TYPES.Program);
99
+ if (program) {
100
+ for (const node of program.body) {
101
+ if (node.type === utils_1.AST_NODE_TYPES.VariableDeclaration) {
102
+ for (const decl of node.declarations) {
103
+ if (decl.init?.type === utils_1.AST_NODE_TYPES.CallExpression &&
104
+ decl.init.callee.type ===
105
+ utils_1.AST_NODE_TYPES.MemberExpression &&
106
+ decl.init.callee.property.type ===
107
+ utils_1.AST_NODE_TYPES.Identifier &&
108
+ decl.init.callee.property.name === 'firestore') {
109
+ return true;
110
+ }
111
+ }
112
+ }
113
+ }
114
+ }
115
+ return false;
116
+ }
117
+ return false;
46
118
  }
47
119
  }
48
120
  if (node.callee.type === utils_1.AST_NODE_TYPES.Identifier) {
@@ -75,6 +147,15 @@ exports.enforceFirestoreSetMerge = (0, createRule_1.createRule)({
75
147
  const data = sourceCode.getText(args[1]);
76
148
  return `${object}.set(${docRef}, ${data}, { merge: true })`;
77
149
  }
150
+ if (object.includes('batchManager')) {
151
+ const docRef = sourceCode.getText(args[0]);
152
+ const data = sourceCode.getText(args[1]);
153
+ return `${object}.set({
154
+ ref: ${docRef},
155
+ data: ${data},
156
+ merge: true,
157
+ })`;
158
+ }
78
159
  const data = sourceCode.getText(args[0]);
79
160
  return `${object}.set(${data}, { merge: true })`;
80
161
  }
@@ -432,6 +432,7 @@ const VERBS_SET = new Set([
432
432
  'include',
433
433
  'includes',
434
434
  'increase',
435
+ 'increment',
435
436
  'index',
436
437
  'influence',
437
438
  'inform',
@@ -0,0 +1 @@
1
+ export declare const noComplexCloudParams: import("@typescript-eslint/utils/dist/ts-eslint/Rule").RuleModule<"noComplexObjects", [], import("@typescript-eslint/utils/dist/ts-eslint/Rule").RuleListener>;