@blumintinc/eslint-plugin-blumint 1.5.0 → 1.5.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
@@ -52,10 +52,14 @@ const enforce_memoize_async_1 = require("./rules/enforce-memoize-async");
52
52
  const enforce_exported_function_types_1 = require("./rules/enforce-exported-function-types");
53
53
  const no_redundant_param_types_1 = require("./rules/no-redundant-param-types");
54
54
  const no_class_instance_destructuring_1 = require("./rules/no-class-instance-destructuring");
55
+ const no_firestore_object_arrays_1 = require("./rules/no-firestore-object-arrays");
56
+ const no_memoize_on_static_1 = require("./rules/no-memoize-on-static");
57
+ const no_unsafe_firestore_spread_1 = require("./rules/no-unsafe-firestore-spread");
58
+ const no_jsx_in_hooks_1 = require("./rules/no-jsx-in-hooks");
55
59
  module.exports = {
56
60
  meta: {
57
61
  name: '@blumintinc/eslint-plugin-blumint',
58
- version: '1.5.0',
62
+ version: '1.5.1',
59
63
  },
60
64
  parseOptions: {
61
65
  ecmaVersion: 2020,
@@ -113,6 +117,10 @@ module.exports = {
113
117
  '@blumintinc/blumint/enforce-exported-function-types': 'error',
114
118
  '@blumintinc/blumint/no-redundant-param-types': 'error',
115
119
  '@blumintinc/blumint/no-class-instance-destructuring': 'error',
120
+ '@blumintinc/blumint/no-firestore-object-arrays': 'warn',
121
+ '@blumintinc/blumint/no-memoize-on-static': 'error',
122
+ '@blumintinc/blumint/no-unsafe-firestore-spread': 'error',
123
+ '@blumintinc/blumint/no-jsx-in-hooks': 'error',
116
124
  },
117
125
  },
118
126
  },
@@ -166,6 +174,10 @@ module.exports = {
166
174
  'enforce-exported-function-types': enforce_exported_function_types_1.enforceExportedFunctionTypes,
167
175
  'no-redundant-param-types': no_redundant_param_types_1.noRedundantParamTypes,
168
176
  'no-class-instance-destructuring': no_class_instance_destructuring_1.noClassInstanceDestructuring,
177
+ 'no-firestore-object-arrays': no_firestore_object_arrays_1.noFirestoreObjectArrays,
178
+ 'no-memoize-on-static': no_memoize_on_static_1.noMemoizeOnStatic,
179
+ 'no-unsafe-firestore-spread': no_unsafe_firestore_spread_1.noUnsafeFirestoreSpread,
180
+ 'no-jsx-in-hooks': no_jsx_in_hooks_1.noJsxInHooks,
169
181
  },
170
182
  };
171
183
  //# sourceMappingURL=index.js.map
@@ -40,17 +40,44 @@ exports.enforceExportedFunctionTypes = (0, createRule_1.createRule)({
40
40
  }
41
41
  return false;
42
42
  }
43
- function getTypeNames(node) {
43
+ function findTypeParameters(node) {
44
+ // Find all type parameters in scope
45
+ const typeParams = new Set();
46
+ let current = node;
47
+ while (current) {
48
+ // Handle type parameters in function declarations
49
+ if (current.type === utils_1.AST_NODE_TYPES.ArrowFunctionExpression ||
50
+ current.type === utils_1.AST_NODE_TYPES.FunctionDeclaration) {
51
+ if ('typeParameters' in current && current.typeParameters) {
52
+ current.typeParameters.params.forEach((param) => {
53
+ if (param.type === utils_1.AST_NODE_TYPES.TSTypeParameter) {
54
+ typeParams.add(param.name.name);
55
+ }
56
+ });
57
+ }
58
+ }
59
+ current = current.parent;
60
+ }
61
+ return typeParams;
62
+ }
63
+ function getTypeNames(node, typeParams) {
44
64
  if (!node)
45
65
  return [];
66
+ // Initialize type parameters if not provided
67
+ if (!typeParams) {
68
+ typeParams = findTypeParameters(node);
69
+ }
46
70
  switch (node.type) {
47
71
  case utils_1.AST_NODE_TYPES.TSTypeReference:
48
72
  if (node.typeName.type === utils_1.AST_NODE_TYPES.Identifier) {
73
+ // Skip checking generic type parameters (e.g., T in <T extends DocumentData>)
74
+ if (typeParams.has(node.typeName.name))
75
+ return [];
49
76
  const names = [node.typeName.name];
50
- // For generic types like AuthenticatedRequest<Params>, check both the base type and type parameters
77
+ // For generic types like AuthenticatedRequest<Params>, check type arguments
51
78
  if ('typeParameters' in node && node.typeParameters) {
52
79
  node.typeParameters.params.forEach((param) => {
53
- names.push(...getTypeNames(param));
80
+ names.push(...getTypeNames(param, typeParams));
54
81
  });
55
82
  }
56
83
  return names;
@@ -35,8 +35,10 @@ exports.enforceMemoizeAsync = (0, createRule_1.createRule)({
35
35
  }
36
36
  },
37
37
  MethodDefinition(node) {
38
- // Only process async methods
39
- if (node.value.type !== utils_1.AST_NODE_TYPES.FunctionExpression || !node.value.async) {
38
+ // Only process async instance methods (skip static methods)
39
+ if (node.value.type !== utils_1.AST_NODE_TYPES.FunctionExpression ||
40
+ !node.value.async ||
41
+ node.static) {
40
42
  return;
41
43
  }
42
44
  // Skip methods with more than one parameter
@@ -7,23 +7,10 @@ exports.enforceVerbNounNaming = void 0;
7
7
  const utils_1 = require("@typescript-eslint/utils");
8
8
  const createRule_1 = require("../utils/createRule");
9
9
  const compromise_1 = __importDefault(require("compromise"));
10
- const PREPOSITIONS = ['to', 'from', 'with', 'by', 'at', 'of'];
11
- // Common short verbs that should be allowed
12
- const COMMON_VERBS = new Set([
13
- 'sync',
14
- 'fix',
15
- 'set',
16
- 'log',
17
- 'get',
18
- 'put',
19
- 'add',
20
- 'map',
21
- 'run',
22
- 'use',
23
- 'has',
24
- 'is',
25
- 'do',
26
- ]);
10
+ const verbs_json_1 = __importDefault(require("../utils/verbs.json"));
11
+ const PREPOSITIONS = new Set(['to', 'from', 'with', 'by', 'at', 'of']);
12
+ // Create a Set from the verbs list for O(1) lookup
13
+ const VERBS_SET = new Set(verbs_json_1.default);
27
14
  exports.enforceVerbNounNaming = (0, createRule_1.createRule)({
28
15
  name: 'enforce-verb-noun-naming',
29
16
  meta: {
@@ -47,33 +34,36 @@ exports.enforceVerbNounNaming = (0, createRule_1.createRule)({
47
34
  const words = rest.split(/(?=[A-Z])/);
48
35
  return firstChar + words[0];
49
36
  }
50
- function toSentence(name) {
51
- // Add "I" prefix to create a proper sentence for better verb detection
52
- return 'I ' + name.split(/(?=[A-Z])/).join(' ');
37
+ function toPhrase(name) {
38
+ return name.split(/(?=[A-Z])/).join(' ');
53
39
  }
54
- function getPossibleTags(sentence) {
40
+ function getPossibleTags(sentence, index = 0) {
55
41
  const doc = (0, compromise_1.default)(sentence);
56
42
  const terms = doc.terms().json();
57
- if (terms.length === 0 || !terms[0].terms || !terms[0].terms[0].tags)
43
+ if (terms.length < index ||
44
+ !terms[index]?.terms?.length ||
45
+ !terms[index]?.terms[0]?.tags)
58
46
  return [];
59
- const tags = terms[0].terms[0].tags;
47
+ const tags = terms[index].terms[0].tags;
60
48
  return tags;
61
49
  }
62
50
  function isVerbPhrase(name) {
63
51
  const firstWord = extractFirstWord(name);
64
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
65
52
  const firstWordLower = firstWord.toLowerCase();
66
- // Check for prepositions and common verbs first
67
- if (PREPOSITIONS.includes(firstWordLower) || COMMON_VERBS.has(firstWordLower)) {
53
+ // Check for prepositions first
54
+ if (PREPOSITIONS.has(firstWordLower)) {
68
55
  return true;
69
56
  }
70
- // Try both with and without "I" prefix to catch more verb forms
71
- const withPrefixTags = getPossibleTags(toSentence(name));
72
- const withoutPrefixTags = getPossibleTags(firstWord);
57
+ // Check against our comprehensive verbs list first
58
+ if (VERBS_SET.has(firstWordLower)) {
59
+ return true;
60
+ }
61
+ // Fall back to NLP approach if not found in verbs list
62
+ const withoutPrefixTags = getPossibleTags(toPhrase(name));
73
63
  // Check if either form is recognized as a verb
74
- const isVerb = withPrefixTags.includes('Verb') || withoutPrefixTags.includes('Verb');
75
- const isPreposition = withPrefixTags.includes('Preposition');
76
- const isConjunction = withPrefixTags.includes('Conjunction');
64
+ const isVerb = withoutPrefixTags.includes('Verb');
65
+ const isPreposition = withoutPrefixTags.includes('Preposition');
66
+ const isConjunction = withoutPrefixTags.includes('Conjunction');
77
67
  // For non-prepositions/conjunctions, require verb form
78
68
  if (isPreposition || isConjunction) {
79
69
  return true;
@@ -0,0 +1 @@
1
+ export declare const noFirestoreObjectArrays: import("@typescript-eslint/utils/dist/ts-eslint/Rule").RuleModule<"noObjectArrays", [], import("@typescript-eslint/utils/dist/ts-eslint/Rule").RuleListener>;
@@ -0,0 +1,86 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.noFirestoreObjectArrays = void 0;
4
+ const utils_1 = require("@typescript-eslint/utils");
5
+ const createRule_1 = require("../utils/createRule");
6
+ const PRIMITIVE_TYPES = new Set([
7
+ 'string',
8
+ 'number',
9
+ 'boolean',
10
+ 'Date',
11
+ 'Timestamp',
12
+ 'null',
13
+ 'undefined',
14
+ 'GeoPoint',
15
+ ]);
16
+ const isInFirestoreTypesDirectory = (filename) => {
17
+ return filename.includes('functions/src/types/firestore');
18
+ };
19
+ const isObjectType = (node) => {
20
+ switch (node.type) {
21
+ case utils_1.AST_NODE_TYPES.TSTypeLiteral:
22
+ return true;
23
+ case utils_1.AST_NODE_TYPES.TSTypeReference:
24
+ if (node.typeName.type === utils_1.AST_NODE_TYPES.Identifier) {
25
+ const typeName = node.typeName.name;
26
+ return !PRIMITIVE_TYPES.has(typeName);
27
+ }
28
+ else if (node.typeName.type === utils_1.AST_NODE_TYPES.TSQualifiedName) {
29
+ // Handle namespace.Type cases
30
+ return true;
31
+ }
32
+ return true;
33
+ case utils_1.AST_NODE_TYPES.TSIntersectionType:
34
+ case utils_1.AST_NODE_TYPES.TSUnionType:
35
+ return node.types.some(isObjectType);
36
+ case utils_1.AST_NODE_TYPES.TSMappedType:
37
+ case utils_1.AST_NODE_TYPES.TSIndexedAccessType:
38
+ return true;
39
+ default:
40
+ return false;
41
+ }
42
+ };
43
+ exports.noFirestoreObjectArrays = (0, createRule_1.createRule)({
44
+ name: 'no-firestore-object-arrays',
45
+ meta: {
46
+ type: 'problem',
47
+ docs: {
48
+ description: 'Disallow arrays of objects in Firestore type definitions to optimize performance and avoid unnecessary fetches',
49
+ recommended: 'warn',
50
+ },
51
+ schema: [],
52
+ messages: {
53
+ noObjectArrays: 'Arrays of objects are not recommended in Firestore. Use subcollections, arrays of IDs, or structured maps (Record<string, T>) instead.',
54
+ },
55
+ },
56
+ defaultOptions: [],
57
+ create(context) {
58
+ if (!isInFirestoreTypesDirectory(context.getFilename())) {
59
+ return {};
60
+ }
61
+ return {
62
+ TSArrayType(node) {
63
+ if (isObjectType(node.elementType)) {
64
+ context.report({
65
+ node,
66
+ messageId: 'noObjectArrays',
67
+ });
68
+ }
69
+ },
70
+ TSTypeReference(node) {
71
+ // Handle Array<T> and ReadonlyArray<T> syntax
72
+ const typeName = node.typeName.name;
73
+ if ((typeName === 'Array' || typeName === 'ReadonlyArray') && node.typeParameters) {
74
+ const elementType = node.typeParameters.params[0];
75
+ if (isObjectType(elementType)) {
76
+ context.report({
77
+ node,
78
+ messageId: 'noObjectArrays',
79
+ });
80
+ }
81
+ }
82
+ },
83
+ };
84
+ },
85
+ });
86
+ //# sourceMappingURL=no-firestore-object-arrays.js.map
@@ -0,0 +1 @@
1
+ export declare const noJsxInHooks: import("@typescript-eslint/utils/dist/ts-eslint/Rule").RuleModule<"noJsxInHooks", [], import("@typescript-eslint/utils/dist/ts-eslint/Rule").RuleListener>;
@@ -0,0 +1,200 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.noJsxInHooks = void 0;
4
+ const utils_1 = require("@typescript-eslint/utils");
5
+ const createRule_1 = require("../utils/createRule");
6
+ const isJsxElement = (node) => {
7
+ if (!node)
8
+ return false;
9
+ if (node.type === utils_1.AST_NODE_TYPES.ConditionalExpression) {
10
+ return isJsxElement(node.consequent) || isJsxElement(node.alternate);
11
+ }
12
+ return (node.type === utils_1.AST_NODE_TYPES.JSXElement ||
13
+ node.type === utils_1.AST_NODE_TYPES.JSXFragment ||
14
+ node.type === utils_1.AST_NODE_TYPES.JSXExpressionContainer);
15
+ };
16
+ const isJsxReturnType = (node) => {
17
+ if (node.typeAnnotation.type === utils_1.AST_NODE_TYPES.TSTypeReference) {
18
+ const typeName = node.typeAnnotation.typeName;
19
+ if (typeName.type === utils_1.AST_NODE_TYPES.Identifier) {
20
+ return ['JSX', 'ReactNode', 'ReactElement'].includes(typeName.name);
21
+ }
22
+ if (typeName.type === utils_1.AST_NODE_TYPES.TSQualifiedName) {
23
+ return typeName.left.type === utils_1.AST_NODE_TYPES.Identifier &&
24
+ typeName.left.name === 'JSX' &&
25
+ typeName.right.type === utils_1.AST_NODE_TYPES.Identifier &&
26
+ typeName.right.name === 'Element';
27
+ }
28
+ }
29
+ return false;
30
+ };
31
+ const containsJsxInBlockStatement = (node) => {
32
+ const variablesWithJsx = new Set();
33
+ for (const statement of node.body) {
34
+ // Check variable declarations for JSX assignments
35
+ if (statement.type === utils_1.AST_NODE_TYPES.VariableDeclaration) {
36
+ for (const declarator of statement.declarations) {
37
+ if (declarator.init) {
38
+ if (declarator.init.type === utils_1.AST_NODE_TYPES.CallExpression &&
39
+ containsJsxInUseMemo(declarator.init)) {
40
+ if (declarator.id.type === utils_1.AST_NODE_TYPES.Identifier) {
41
+ variablesWithJsx.add(declarator.id.name);
42
+ }
43
+ }
44
+ }
45
+ }
46
+ }
47
+ // Check return statements
48
+ if (statement.type === utils_1.AST_NODE_TYPES.ReturnStatement && statement.argument) {
49
+ if (isJsxElement(statement.argument)) {
50
+ return true;
51
+ }
52
+ if (statement.argument.type === utils_1.AST_NODE_TYPES.CallExpression) {
53
+ if (containsJsxInUseMemo(statement.argument)) {
54
+ return true;
55
+ }
56
+ }
57
+ if (statement.argument.type === utils_1.AST_NODE_TYPES.Identifier &&
58
+ variablesWithJsx.has(statement.argument.name)) {
59
+ return true;
60
+ }
61
+ }
62
+ // Check if statements
63
+ if (statement.type === utils_1.AST_NODE_TYPES.IfStatement) {
64
+ if (statement.consequent.type === utils_1.AST_NODE_TYPES.ReturnStatement &&
65
+ statement.consequent.argument) {
66
+ if (isJsxElement(statement.consequent.argument)) {
67
+ return true;
68
+ }
69
+ if (statement.consequent.argument.type === utils_1.AST_NODE_TYPES.Identifier &&
70
+ variablesWithJsx.has(statement.consequent.argument.name)) {
71
+ return true;
72
+ }
73
+ }
74
+ if (statement.consequent.type === utils_1.AST_NODE_TYPES.BlockStatement &&
75
+ containsJsxInBlockStatement(statement.consequent)) {
76
+ return true;
77
+ }
78
+ if (statement.alternate) {
79
+ if (statement.alternate.type === utils_1.AST_NODE_TYPES.ReturnStatement &&
80
+ statement.alternate.argument) {
81
+ if (isJsxElement(statement.alternate.argument)) {
82
+ return true;
83
+ }
84
+ if (statement.alternate.argument.type === utils_1.AST_NODE_TYPES.Identifier &&
85
+ variablesWithJsx.has(statement.alternate.argument.name)) {
86
+ return true;
87
+ }
88
+ }
89
+ if (statement.alternate.type === utils_1.AST_NODE_TYPES.BlockStatement &&
90
+ containsJsxInBlockStatement(statement.alternate)) {
91
+ return true;
92
+ }
93
+ }
94
+ }
95
+ }
96
+ return false;
97
+ };
98
+ const containsJsxInUseMemo = (node) => {
99
+ if (node.callee.type === utils_1.AST_NODE_TYPES.Identifier &&
100
+ node.callee.name === 'useMemo' &&
101
+ node.arguments.length > 0) {
102
+ const callback = node.arguments[0];
103
+ if (callback.type === utils_1.AST_NODE_TYPES.ArrowFunctionExpression ||
104
+ callback.type === utils_1.AST_NODE_TYPES.FunctionExpression) {
105
+ const body = callback.body;
106
+ if (isJsxElement(body)) {
107
+ return true;
108
+ }
109
+ if (body.type === utils_1.AST_NODE_TYPES.BlockStatement) {
110
+ return containsJsxInBlockStatement(body);
111
+ }
112
+ if (body.type === utils_1.AST_NODE_TYPES.CallExpression &&
113
+ body.callee.type === utils_1.AST_NODE_TYPES.MemberExpression &&
114
+ body.callee.property.type === utils_1.AST_NODE_TYPES.Identifier &&
115
+ body.callee.property.name === 'map') {
116
+ const mapCallback = body.arguments[0];
117
+ if ((mapCallback.type === utils_1.AST_NODE_TYPES.ArrowFunctionExpression ||
118
+ mapCallback.type === utils_1.AST_NODE_TYPES.FunctionExpression) &&
119
+ isJsxElement(mapCallback.body)) {
120
+ return true;
121
+ }
122
+ }
123
+ }
124
+ }
125
+ return false;
126
+ };
127
+ exports.noJsxInHooks = (0, createRule_1.createRule)({
128
+ name: 'no-jsx-in-hooks',
129
+ meta: {
130
+ type: 'problem',
131
+ docs: {
132
+ description: 'Prevent hooks from returning JSX',
133
+ recommended: 'error',
134
+ },
135
+ schema: [],
136
+ messages: {
137
+ noJsxInHooks: 'Hooks should not return JSX. Convert this hook into a component or extract the JSX into a separate component.',
138
+ },
139
+ },
140
+ defaultOptions: [],
141
+ create(context) {
142
+ return {
143
+ FunctionDeclaration(node) {
144
+ if (node.id && node.id.name.startsWith('use')) {
145
+ // Check return type annotation
146
+ if (node.returnType && isJsxReturnType(node.returnType)) {
147
+ context.report({
148
+ node: node.id,
149
+ messageId: 'noJsxInHooks',
150
+ });
151
+ return;
152
+ }
153
+ // Check return statements
154
+ if (node.body.type === utils_1.AST_NODE_TYPES.BlockStatement) {
155
+ if (containsJsxInBlockStatement(node.body)) {
156
+ context.report({
157
+ node: node.id,
158
+ messageId: 'noJsxInHooks',
159
+ });
160
+ }
161
+ }
162
+ }
163
+ },
164
+ ArrowFunctionExpression(node) {
165
+ const parent = node.parent;
166
+ if (parent &&
167
+ parent.type === utils_1.AST_NODE_TYPES.VariableDeclarator &&
168
+ parent.id.type === utils_1.AST_NODE_TYPES.Identifier &&
169
+ parent.id.name.startsWith('use')) {
170
+ // Check return type annotation
171
+ if (node.returnType && isJsxReturnType(node.returnType)) {
172
+ context.report({
173
+ node: parent.id,
174
+ messageId: 'noJsxInHooks',
175
+ });
176
+ return;
177
+ }
178
+ // Check direct JSX return
179
+ if (isJsxElement(node.body)) {
180
+ context.report({
181
+ node: parent.id,
182
+ messageId: 'noJsxInHooks',
183
+ });
184
+ return;
185
+ }
186
+ // Check block body returns
187
+ if (node.body.type === utils_1.AST_NODE_TYPES.BlockStatement) {
188
+ if (containsJsxInBlockStatement(node.body)) {
189
+ context.report({
190
+ node: parent.id,
191
+ messageId: 'noJsxInHooks',
192
+ });
193
+ }
194
+ }
195
+ }
196
+ },
197
+ };
198
+ },
199
+ });
200
+ //# sourceMappingURL=no-jsx-in-hooks.js.map
@@ -0,0 +1 @@
1
+ export declare const noMemoizeOnStatic: import("@typescript-eslint/utils/dist/ts-eslint/Rule").RuleModule<"noMemoizeOnStatic", [], import("@typescript-eslint/utils/dist/ts-eslint/Rule").RuleListener>;
@@ -0,0 +1,54 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.noMemoizeOnStatic = void 0;
4
+ const utils_1 = require("@typescript-eslint/utils");
5
+ const createRule_1 = require("../utils/createRule");
6
+ exports.noMemoizeOnStatic = (0, createRule_1.createRule)({
7
+ name: 'no-memoize-on-static',
8
+ meta: {
9
+ type: 'problem',
10
+ docs: {
11
+ description: 'Prevent using @Memoize() decorator on static methods',
12
+ recommended: 'error',
13
+ },
14
+ schema: [],
15
+ messages: {
16
+ noMemoizeOnStatic: '@Memoize() decorator should not be used on static methods',
17
+ },
18
+ },
19
+ defaultOptions: [],
20
+ create(context) {
21
+ // Track renamed imports of Memoize
22
+ const memoizeAliases = new Set(['Memoize']);
23
+ return {
24
+ ImportSpecifier(node) {
25
+ if (node.imported.type === utils_1.AST_NODE_TYPES.Identifier &&
26
+ node.imported.name === 'Memoize' &&
27
+ node.local.type === utils_1.AST_NODE_TYPES.Identifier) {
28
+ memoizeAliases.add(node.local.name);
29
+ }
30
+ },
31
+ 'MethodDefinition[static=true]'(node) {
32
+ if (node.decorators) {
33
+ for (const decorator of node.decorators) {
34
+ const expr = decorator.expression;
35
+ if (
36
+ // Handle @Memoize()
37
+ (expr.type === utils_1.AST_NODE_TYPES.CallExpression &&
38
+ expr.callee.type === utils_1.AST_NODE_TYPES.Identifier &&
39
+ memoizeAliases.has(expr.callee.name)) ||
40
+ // Handle @Memoize (without parentheses)
41
+ (expr.type === utils_1.AST_NODE_TYPES.Identifier &&
42
+ memoizeAliases.has(expr.name))) {
43
+ context.report({
44
+ node: decorator,
45
+ messageId: 'noMemoizeOnStatic',
46
+ });
47
+ }
48
+ }
49
+ }
50
+ },
51
+ };
52
+ },
53
+ });
54
+ //# sourceMappingURL=no-memoize-on-static.js.map
@@ -0,0 +1,3 @@
1
+ type MessageIds = 'unsafeObjectSpread' | 'unsafeArraySpread';
2
+ export declare const noUnsafeFirestoreSpread: import("@typescript-eslint/utils/dist/ts-eslint/Rule").RuleModule<MessageIds, [], import("@typescript-eslint/utils/dist/ts-eslint/Rule").RuleListener>;
3
+ export {};
@@ -0,0 +1,125 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.noUnsafeFirestoreSpread = void 0;
4
+ const utils_1 = require("@typescript-eslint/utils");
5
+ const createRule_1 = require("../utils/createRule");
6
+ exports.noUnsafeFirestoreSpread = (0, createRule_1.createRule)({
7
+ name: 'no-unsafe-firestore-spread',
8
+ meta: {
9
+ type: 'problem',
10
+ docs: {
11
+ description: 'Prevent unsafe object/array spreads in Firestore updates',
12
+ recommended: 'error',
13
+ },
14
+ fixable: 'code',
15
+ schema: [],
16
+ messages: {
17
+ unsafeObjectSpread: 'Avoid using object spread in Firestore updates. Use dot notation with FieldPath instead.',
18
+ unsafeArraySpread: 'Avoid using array spread in Firestore updates. Use FieldValue.arrayUnion() instead.',
19
+ },
20
+ },
21
+ defaultOptions: [],
22
+ create(context) {
23
+ function isFirestoreSetMergeCall(node) {
24
+ // Check for merge: true in the last argument
25
+ const lastArg = node.arguments[node.arguments.length - 1];
26
+ if (lastArg?.type === utils_1.AST_NODE_TYPES.ObjectExpression) {
27
+ const hasMergeTrue = lastArg.properties.some((prop) => prop.type === utils_1.AST_NODE_TYPES.Property &&
28
+ prop.key.type === utils_1.AST_NODE_TYPES.Identifier &&
29
+ prop.key.name === 'merge' &&
30
+ prop.value.type === utils_1.AST_NODE_TYPES.Literal &&
31
+ prop.value.value === true);
32
+ if (!hasMergeTrue)
33
+ return false;
34
+ // Check if it's a set() method call or setDoc() function call
35
+ if (node.callee.type === utils_1.AST_NODE_TYPES.MemberExpression) {
36
+ const property = node.callee.property;
37
+ return (property.type === utils_1.AST_NODE_TYPES.Identifier &&
38
+ property.name === 'set');
39
+ }
40
+ else if (node.callee.type === utils_1.AST_NODE_TYPES.Identifier) {
41
+ return node.callee.name === 'setDoc';
42
+ }
43
+ }
44
+ return false;
45
+ }
46
+ function checkObjectForSpreads(node, parentPath = '') {
47
+ for (const property of node.properties) {
48
+ if (property.type === utils_1.AST_NODE_TYPES.SpreadElement) {
49
+ context.report({
50
+ node: property,
51
+ messageId: 'unsafeObjectSpread',
52
+ fix: null,
53
+ });
54
+ }
55
+ else if (property.type === utils_1.AST_NODE_TYPES.Property) {
56
+ const key = property.key.type === utils_1.AST_NODE_TYPES.Identifier
57
+ ? property.key.name
58
+ : '';
59
+ const newPath = parentPath ? `${parentPath}.${key}` : key;
60
+ if (property.value.type === utils_1.AST_NODE_TYPES.ObjectExpression) {
61
+ checkObjectForSpreads(property.value, newPath);
62
+ }
63
+ else if (property.value.type === utils_1.AST_NODE_TYPES.ArrayExpression) {
64
+ checkArrayForSpreads(property.value);
65
+ }
66
+ else if (property.value.type === utils_1.AST_NODE_TYPES.CallExpression) {
67
+ // Handle chained array methods like [...array].filter()
68
+ let current = property.value;
69
+ while (current) {
70
+ if (current.type === utils_1.AST_NODE_TYPES.ArrayExpression) {
71
+ checkArrayForSpreads(current);
72
+ break;
73
+ }
74
+ else if (current.type === utils_1.AST_NODE_TYPES.ObjectExpression) {
75
+ checkObjectForSpreads(current, newPath);
76
+ break;
77
+ }
78
+ // Move up to check the caller if it's a method chain
79
+ if (current.type === utils_1.AST_NODE_TYPES.CallExpression &&
80
+ current.callee.type === utils_1.AST_NODE_TYPES.MemberExpression) {
81
+ current = current.callee.object;
82
+ }
83
+ else {
84
+ break;
85
+ }
86
+ }
87
+ }
88
+ }
89
+ }
90
+ }
91
+ function checkArrayForSpreads(node) {
92
+ // Check for spreads in the array expression itself
93
+ for (const element of node.elements) {
94
+ if (element?.type === utils_1.AST_NODE_TYPES.SpreadElement) {
95
+ context.report({
96
+ node: element,
97
+ messageId: 'unsafeArraySpread',
98
+ fix: null,
99
+ });
100
+ }
101
+ }
102
+ }
103
+ return {
104
+ CallExpression(node) {
105
+ if (!isFirestoreSetMergeCall(node))
106
+ return;
107
+ // For set() calls, the update object can be either the first or second argument
108
+ // If it's a docRef.set(data) call, it's the first argument
109
+ // If it's a docRef.set(docRef, data) call (like in transactions/batches), it's the second argument
110
+ let updateArg;
111
+ if (node.arguments.length === 2) {
112
+ updateArg = node.arguments[0];
113
+ }
114
+ else if (node.arguments.length === 3) {
115
+ // In a transaction or batch operation, the data is the second argument
116
+ updateArg = node.arguments[1];
117
+ }
118
+ if (!updateArg || updateArg.type !== utils_1.AST_NODE_TYPES.ObjectExpression)
119
+ return;
120
+ checkObjectForSpreads(updateArg);
121
+ },
122
+ };
123
+ },
124
+ });
125
+ //# sourceMappingURL=no-unsafe-firestore-spread.js.map
@@ -0,0 +1 @@
1
+ export declare const requireHooksDefaultParams: import("@typescript-eslint/utils/dist/ts-eslint/Rule").RuleModule<"requireDefaultParams", [], import("@typescript-eslint/utils/dist/ts-eslint/Rule").RuleListener>;
@@ -0,0 +1,195 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.requireHooksDefaultParams = void 0;
4
+ const utils_1 = require("@typescript-eslint/utils");
5
+ const createRule_1 = require("../utils/createRule");
6
+ exports.requireHooksDefaultParams = (0, createRule_1.createRule)({
7
+ name: 'require-hooks-default-params',
8
+ meta: {
9
+ type: 'suggestion',
10
+ docs: {
11
+ description: 'Enforce React hooks with optional parameters to default to an empty object',
12
+ recommended: 'error',
13
+ },
14
+ fixable: 'code',
15
+ schema: [],
16
+ messages: {
17
+ requireDefaultParams: 'React hooks with all optional parameters should default to an empty object',
18
+ },
19
+ },
20
+ defaultOptions: [],
21
+ create(context) {
22
+ function isHookName(name) {
23
+ return name.startsWith('use') && name[3]?.toUpperCase() === name[3];
24
+ }
25
+ function hasAllOptionalProperties(typeNode) {
26
+ // Handle type literals directly
27
+ if (typeNode.type === utils_1.AST_NODE_TYPES.TSTypeLiteral) {
28
+ return typeNode.members.every(member => {
29
+ if (member.type !== utils_1.AST_NODE_TYPES.TSPropertySignature) {
30
+ return false;
31
+ }
32
+ return member.optional === true;
33
+ });
34
+ }
35
+ // Handle type references
36
+ if (typeNode.type === utils_1.AST_NODE_TYPES.TSTypeReference) {
37
+ const typeName = typeNode.typeName;
38
+ if (typeName.type !== utils_1.AST_NODE_TYPES.Identifier) {
39
+ return false;
40
+ }
41
+ const scope = context.getScope();
42
+ const variable = scope.variables.find(v => v.name === typeName.name);
43
+ if (!variable || !variable.defs[0]?.node) {
44
+ // If we can't find the type definition, assume it's a type with all optional properties
45
+ // This handles cases where the type is imported from another module
46
+ return true;
47
+ }
48
+ const def = variable.defs[0].node;
49
+ if (def.type === utils_1.AST_NODE_TYPES.TSTypeAliasDeclaration) {
50
+ return hasAllOptionalProperties(def.typeAnnotation);
51
+ }
52
+ else if (def.type === utils_1.AST_NODE_TYPES.TSInterfaceDeclaration) {
53
+ return def.body.body.every(member => {
54
+ if (member.type !== utils_1.AST_NODE_TYPES.TSPropertySignature) {
55
+ return false;
56
+ }
57
+ return member.optional === true;
58
+ });
59
+ }
60
+ // If we found the type definition but it's not a type alias or interface declaration,
61
+ // assume it's a type with all optional properties
62
+ // This handles cases where the type is imported from another module
63
+ return true;
64
+ }
65
+ // Handle type alias declarations
66
+ if (typeNode.type === utils_1.AST_NODE_TYPES.TSTypeAliasDeclaration) {
67
+ return hasAllOptionalProperties(typeNode.typeAnnotation);
68
+ }
69
+ // Handle interface declarations
70
+ if (typeNode.type === utils_1.AST_NODE_TYPES.TSInterfaceDeclaration) {
71
+ return typeNode.body.body.every(member => {
72
+ if (member.type !== utils_1.AST_NODE_TYPES.TSPropertySignature) {
73
+ return false;
74
+ }
75
+ return member.optional === true;
76
+ });
77
+ }
78
+ return false;
79
+ }
80
+ function checkHookParam(param) {
81
+ // If it's already an assignment pattern, check if the left side is an object pattern
82
+ if (param.type === utils_1.AST_NODE_TYPES.AssignmentPattern) {
83
+ if (param.left.type === utils_1.AST_NODE_TYPES.ObjectPattern && param.left.typeAnnotation) {
84
+ if (hasAllOptionalProperties(param.left.typeAnnotation.typeAnnotation)) {
85
+ return; // Already has a default value and is correctly typed
86
+ }
87
+ }
88
+ return;
89
+ }
90
+ // If it's an object pattern, check if it needs a default value
91
+ if (param.type === utils_1.AST_NODE_TYPES.ObjectPattern && param.typeAnnotation) {
92
+ const typeAnnotation = param.typeAnnotation.typeAnnotation;
93
+ if (typeAnnotation.type === utils_1.AST_NODE_TYPES.TSTypeReference) {
94
+ const typeName = typeAnnotation.typeName;
95
+ if (typeName.type === utils_1.AST_NODE_TYPES.Identifier) {
96
+ const scope = context.getScope();
97
+ const variable = scope.variables.find(v => v.name === typeName.name);
98
+ if (!variable || !variable.defs[0]?.node) {
99
+ // If we can't find the type definition, assume it's a type with all optional properties
100
+ // This handles cases where the type is imported from another module
101
+ context.report({
102
+ node: param,
103
+ messageId: 'requireDefaultParams',
104
+ fix(fixer) {
105
+ const paramText = context.getSourceCode().getText(param);
106
+ return fixer.replaceText(param, `${paramText} = {}`);
107
+ },
108
+ });
109
+ return;
110
+ }
111
+ const def = variable.defs[0].node;
112
+ if (def.type === utils_1.AST_NODE_TYPES.TSTypeAliasDeclaration) {
113
+ if (hasAllOptionalProperties(def.typeAnnotation)) {
114
+ context.report({
115
+ node: param,
116
+ messageId: 'requireDefaultParams',
117
+ fix(fixer) {
118
+ const paramText = context.getSourceCode().getText(param);
119
+ return fixer.replaceText(param, `${paramText} = {}`);
120
+ },
121
+ });
122
+ }
123
+ }
124
+ else if (def.type === utils_1.AST_NODE_TYPES.TSInterfaceDeclaration) {
125
+ if (def.body.body.every(member => {
126
+ if (member.type !== utils_1.AST_NODE_TYPES.TSPropertySignature) {
127
+ return false;
128
+ }
129
+ return member.optional === true;
130
+ })) {
131
+ context.report({
132
+ node: param,
133
+ messageId: 'requireDefaultParams',
134
+ fix(fixer) {
135
+ const paramText = context.getSourceCode().getText(param);
136
+ return fixer.replaceText(param, `${paramText} = {}`);
137
+ },
138
+ });
139
+ }
140
+ }
141
+ else {
142
+ // If we found the type definition but it's not a type alias or interface declaration,
143
+ // assume it's a type with all optional properties
144
+ // This handles cases where the type is imported from another module
145
+ context.report({
146
+ node: param,
147
+ messageId: 'requireDefaultParams',
148
+ fix(fixer) {
149
+ const paramText = context.getSourceCode().getText(param);
150
+ return fixer.replaceText(param, `${paramText} = {}`);
151
+ },
152
+ });
153
+ }
154
+ }
155
+ }
156
+ else if (hasAllOptionalProperties(typeAnnotation)) {
157
+ context.report({
158
+ node: param,
159
+ messageId: 'requireDefaultParams',
160
+ fix(fixer) {
161
+ const paramText = context.getSourceCode().getText(param);
162
+ return fixer.replaceText(param, `${paramText} = {}`);
163
+ },
164
+ });
165
+ }
166
+ }
167
+ }
168
+ return {
169
+ FunctionDeclaration(node) {
170
+ if (!node.id || !isHookName(node.id.name)) {
171
+ return;
172
+ }
173
+ if (node.params.length !== 1) {
174
+ return;
175
+ }
176
+ checkHookParam(node.params[0]);
177
+ },
178
+ ArrowFunctionExpression(node) {
179
+ const parent = node.parent;
180
+ if (!parent ||
181
+ parent.type !== utils_1.AST_NODE_TYPES.VariableDeclarator ||
182
+ !parent.id ||
183
+ parent.id.type !== utils_1.AST_NODE_TYPES.Identifier ||
184
+ !isHookName(parent.id.name)) {
185
+ return;
186
+ }
187
+ if (node.params.length !== 1) {
188
+ return;
189
+ }
190
+ checkHookParam(node.params[0]);
191
+ },
192
+ };
193
+ },
194
+ });
195
+ //# sourceMappingURL=require-hooks-default-params.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blumintinc/eslint-plugin-blumint",
3
- "version": "1.5.0",
3
+ "version": "1.5.1",
4
4
  "description": "Custom eslint rules for use within BluMint",
5
5
  "author": {
6
6
  "name": "Brodie McGuire",