@blumintinc/eslint-plugin-blumint 1.4.0 → 1.5.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 +7 -1
- package/lib/rules/array-methods-this-context.js +2 -2
- package/lib/rules/enforce-callback-memo.js +3 -3
- package/lib/rules/enforce-dynamic-firebase-imports.js +2 -2
- package/lib/rules/enforce-exported-function-types.js +88 -85
- package/lib/rules/enforce-firestore-doc-ref-generic.js +2 -2
- package/lib/rules/enforce-firestore-path-utils.js +2 -2
- package/lib/rules/enforce-firestore-set-merge.js +35 -4
- package/lib/rules/enforce-identifiable-firestore-type.js +2 -2
- package/lib/rules/enforce-memoize-async.js +2 -2
- package/lib/rules/enforce-mock-firestore.js +3 -3
- package/lib/rules/enforce-realtimedb-path-utils.js +1 -1
- package/lib/rules/enforce-safe-stringify.js +2 -2
- package/lib/rules/enforce-serializable-params.js +3 -3
- package/lib/rules/enforce-verb-noun-naming.js +39 -7
- package/lib/rules/no-async-array-filter.js +2 -2
- package/lib/rules/no-async-foreach.js +1 -1
- package/lib/rules/no-class-instance-destructuring.d.ts +1 -0
- package/lib/rules/no-class-instance-destructuring.js +95 -0
- package/lib/rules/no-compositing-layer-props.js +1 -1
- package/lib/rules/no-conditional-literals-in-jsx.js +1 -1
- package/lib/rules/no-entire-object-hook-deps.js +1 -1
- package/lib/rules/no-explicit-return-type.d.ts +1 -0
- package/lib/rules/no-explicit-return-type.js +5 -2
- package/lib/rules/no-filter-without-return.js +1 -1
- package/lib/rules/no-misused-switch-case.js +2 -2
- package/lib/rules/no-redundant-param-types.d.ts +2 -0
- package/lib/rules/no-redundant-param-types.js +129 -0
- package/lib/rules/no-unused-props.js +1 -1
- package/lib/rules/no-useless-fragment.js +1 -1
- package/lib/rules/prefer-destructuring-no-class.d.ts +8 -0
- package/lib/rules/prefer-destructuring-no-class.js +200 -0
- package/lib/rules/require-usememo-object-literals.js +1 -1
- package/lib/rules/semantic-function-prefixes.js +43 -4
- package/package.json +1 -1
package/lib/index.js
CHANGED
|
@@ -50,10 +50,12 @@ const enforce_serializable_params_1 = __importDefault(require("./rules/enforce-s
|
|
|
50
50
|
const enforce_realtimedb_path_utils_1 = require("./rules/enforce-realtimedb-path-utils");
|
|
51
51
|
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
|
+
const no_redundant_param_types_1 = require("./rules/no-redundant-param-types");
|
|
54
|
+
const no_class_instance_destructuring_1 = require("./rules/no-class-instance-destructuring");
|
|
53
55
|
module.exports = {
|
|
54
56
|
meta: {
|
|
55
57
|
name: '@blumintinc/eslint-plugin-blumint',
|
|
56
|
-
version: '1.
|
|
58
|
+
version: '1.5.0',
|
|
57
59
|
},
|
|
58
60
|
parseOptions: {
|
|
59
61
|
ecmaVersion: 2020,
|
|
@@ -109,6 +111,8 @@ module.exports = {
|
|
|
109
111
|
'@blumintinc/blumint/enforce-realtimedb-path-utils': 'error',
|
|
110
112
|
'@blumintinc/blumint/enforce-memoize-async': 'error',
|
|
111
113
|
'@blumintinc/blumint/enforce-exported-function-types': 'error',
|
|
114
|
+
'@blumintinc/blumint/no-redundant-param-types': 'error',
|
|
115
|
+
'@blumintinc/blumint/no-class-instance-destructuring': 'error',
|
|
112
116
|
},
|
|
113
117
|
},
|
|
114
118
|
},
|
|
@@ -160,6 +164,8 @@ module.exports = {
|
|
|
160
164
|
'enforce-realtimedb-path-utils': enforce_realtimedb_path_utils_1.enforceRealtimedbPathUtils,
|
|
161
165
|
'enforce-memoize-async': enforce_memoize_async_1.enforceMemoizeAsync,
|
|
162
166
|
'enforce-exported-function-types': enforce_exported_function_types_1.enforceExportedFunctionTypes,
|
|
167
|
+
'no-redundant-param-types': no_redundant_param_types_1.noRedundantParamTypes,
|
|
168
|
+
'no-class-instance-destructuring': no_class_instance_destructuring_1.noClassInstanceDestructuring,
|
|
163
169
|
},
|
|
164
170
|
};
|
|
165
171
|
//# sourceMappingURL=index.js.map
|
|
@@ -55,8 +55,8 @@ exports.arrayMethodsThisContext = (0, createRule_1.createRule)({
|
|
|
55
55
|
},
|
|
56
56
|
schema: [],
|
|
57
57
|
messages: {
|
|
58
|
-
unexpected: 'Use an arrow function to preserve "this" context.',
|
|
59
|
-
preferArrow: 'Use an arrow function instead of binding this.',
|
|
58
|
+
unexpected: 'Use an arrow function to preserve "this" context in array methods. Instead of `array.map(this.method)`, use `array.map((x) => this.method(x))`.',
|
|
59
|
+
preferArrow: 'Use an arrow function instead of binding "this". Instead of `array.map(function(x) {}.bind(this))`, use `array.map((x) => {...})`.',
|
|
60
60
|
},
|
|
61
61
|
},
|
|
62
62
|
defaultOptions: [],
|
|
@@ -7,12 +7,12 @@ exports.default = (0, createRule_1.createRule)({
|
|
|
7
7
|
meta: {
|
|
8
8
|
type: 'suggestion',
|
|
9
9
|
docs: {
|
|
10
|
-
description: 'Enforce useCallback
|
|
10
|
+
description: 'Enforce useCallback for inline functions and useMemo for objects/arrays containing functions in JSX props to prevent unnecessary re-renders. This improves React component performance by ensuring stable function references across renders and memoizing complex objects.',
|
|
11
11
|
recommended: 'error',
|
|
12
12
|
},
|
|
13
13
|
messages: {
|
|
14
|
-
enforceCallback: 'Inline functions in JSX props should be wrapped with useCallback',
|
|
15
|
-
enforceMemo: 'Objects/arrays containing functions in JSX props should be wrapped with useMemo',
|
|
14
|
+
enforceCallback: 'Inline functions in JSX props should be wrapped with useCallback to prevent unnecessary re-renders. Instead of `<Button onClick={() => handleClick(id)} />`, use `<Button onClick={useCallback(() => handleClick(id), [id])} />`.',
|
|
15
|
+
enforceMemo: 'Objects/arrays containing functions in JSX props should be wrapped with useMemo to prevent unnecessary re-renders. Instead of `<Component config={{ onSubmit: () => {...} }} />`, use `<Component config={useMemo(() => ({ onSubmit: () => {...} }), [])} />`.',
|
|
16
16
|
},
|
|
17
17
|
schema: [],
|
|
18
18
|
},
|
|
@@ -7,12 +7,12 @@ exports.enforceFirebaseImports = (0, createRule_1.createRule)({
|
|
|
7
7
|
meta: {
|
|
8
8
|
type: 'problem',
|
|
9
9
|
docs: {
|
|
10
|
-
description: 'Enforce dynamic importing for modules within the firebaseCloud directory',
|
|
10
|
+
description: 'Enforce dynamic importing for modules within the firebaseCloud directory to optimize initial bundle size. This ensures Firebase-related code is only loaded when needed, improving application startup time and reducing the main bundle size.',
|
|
11
11
|
recommended: 'error',
|
|
12
12
|
},
|
|
13
13
|
schema: [],
|
|
14
14
|
messages: {
|
|
15
|
-
noDynamicImport: 'Static imports from firebaseCloud directory are not allowed.
|
|
15
|
+
noDynamicImport: 'Static imports from firebaseCloud directory are not allowed to reduce initial bundle size. Instead of `import { func } from "./firebaseCloud/module"`, use dynamic import: `const { func } = await import("./firebaseCloud/module")`.',
|
|
16
16
|
},
|
|
17
17
|
},
|
|
18
18
|
defaultOptions: [],
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.enforceExportedFunctionTypes = void 0;
|
|
4
|
+
/* eslint-disable @typescript-eslint/no-empty-function */
|
|
4
5
|
const utils_1 = require("@typescript-eslint/utils");
|
|
5
6
|
const createRule_1 = require("../utils/createRule");
|
|
6
7
|
exports.enforceExportedFunctionTypes = (0, createRule_1.createRule)({
|
|
@@ -13,9 +14,9 @@ exports.enforceExportedFunctionTypes = (0, createRule_1.createRule)({
|
|
|
13
14
|
},
|
|
14
15
|
schema: [],
|
|
15
16
|
messages: {
|
|
16
|
-
missingExportedType: 'Type {{typeName}} should be exported since it is used in an exported function',
|
|
17
|
-
missingExportedReturnType: 'Return type {{typeName}} should be exported since it is used in an exported function',
|
|
18
|
-
missingExportedPropsType: 'Props type {{typeName}} should be exported since it is used in an exported React component',
|
|
17
|
+
missingExportedType: 'Type {{typeName}} should be exported since it is used in an exported function. Add `export` before the type definition: `export type {{typeName}} = ...`',
|
|
18
|
+
missingExportedReturnType: 'Return type {{typeName}} should be exported since it is used in an exported function. Add `export` before the type definition: `export type {{typeName}} = ...`',
|
|
19
|
+
missingExportedPropsType: 'Props type {{typeName}} should be exported since it is used in an exported React component. Add `export` before the type definition: `export type {{typeName}} = ...`',
|
|
19
20
|
},
|
|
20
21
|
},
|
|
21
22
|
defaultOptions: [],
|
|
@@ -33,30 +34,95 @@ exports.enforceExportedFunctionTypes = (0, createRule_1.createRule)({
|
|
|
33
34
|
return false;
|
|
34
35
|
if (parent.type === utils_1.AST_NODE_TYPES.ExportNamedDeclaration ||
|
|
35
36
|
parent.type === utils_1.AST_NODE_TYPES.ExportDefaultDeclaration ||
|
|
36
|
-
parent.type === utils_1.AST_NODE_TYPES.VariableDeclarator &&
|
|
37
|
+
(parent.type === utils_1.AST_NODE_TYPES.VariableDeclarator &&
|
|
38
|
+
isExported(parent.parent))) {
|
|
37
39
|
return true;
|
|
38
40
|
}
|
|
39
41
|
return false;
|
|
40
42
|
}
|
|
41
|
-
function
|
|
43
|
+
function getTypeNames(node) {
|
|
42
44
|
if (!node)
|
|
43
|
-
return
|
|
45
|
+
return [];
|
|
44
46
|
switch (node.type) {
|
|
45
47
|
case utils_1.AST_NODE_TYPES.TSTypeReference:
|
|
46
48
|
if (node.typeName.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
47
|
-
|
|
49
|
+
const names = [node.typeName.name];
|
|
50
|
+
// For generic types like AuthenticatedRequest<Params>, check both the base type and type parameters
|
|
51
|
+
if ('typeParameters' in node && node.typeParameters) {
|
|
52
|
+
node.typeParameters.params.forEach((param) => {
|
|
53
|
+
names.push(...getTypeNames(param));
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
return names;
|
|
48
57
|
}
|
|
49
58
|
break;
|
|
50
59
|
case utils_1.AST_NODE_TYPES.TSTypeLiteral:
|
|
51
|
-
return 'AnonymousType';
|
|
60
|
+
return ['AnonymousType'];
|
|
61
|
+
}
|
|
62
|
+
return [];
|
|
63
|
+
}
|
|
64
|
+
function isBuiltInType(typeName) {
|
|
65
|
+
const builtInTypes = new Set([
|
|
66
|
+
'string',
|
|
67
|
+
'number',
|
|
68
|
+
'boolean',
|
|
69
|
+
'null',
|
|
70
|
+
'undefined',
|
|
71
|
+
'void',
|
|
72
|
+
'any',
|
|
73
|
+
'never',
|
|
74
|
+
'unknown',
|
|
75
|
+
'object',
|
|
76
|
+
'Date',
|
|
77
|
+
'RegExp',
|
|
78
|
+
'Error',
|
|
79
|
+
'Promise',
|
|
80
|
+
'Array',
|
|
81
|
+
'Function',
|
|
82
|
+
'Symbol',
|
|
83
|
+
'BigInt',
|
|
84
|
+
'Map',
|
|
85
|
+
'Set',
|
|
86
|
+
'WeakMap',
|
|
87
|
+
'WeakSet',
|
|
88
|
+
]);
|
|
89
|
+
return builtInTypes.has(typeName);
|
|
90
|
+
}
|
|
91
|
+
function checkAndReportType(node, parentNode, messageId) {
|
|
92
|
+
const typeNames = getTypeNames(node);
|
|
93
|
+
for (const typeName of typeNames) {
|
|
94
|
+
if (typeName !== 'AnonymousType' &&
|
|
95
|
+
!isBuiltInType(typeName) &&
|
|
96
|
+
!isTypeExported(typeName)) {
|
|
97
|
+
// Check if we've already reported this type
|
|
98
|
+
const key = `${typeName}-${parentNode.loc?.start.line}-${parentNode.loc?.start.column}`;
|
|
99
|
+
if (!reportedTypes.has(key)) {
|
|
100
|
+
reportedTypes.add(key);
|
|
101
|
+
context.report({
|
|
102
|
+
node: parentNode,
|
|
103
|
+
messageId,
|
|
104
|
+
data: { typeName },
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
}
|
|
52
108
|
}
|
|
53
|
-
return undefined;
|
|
54
109
|
}
|
|
55
110
|
function isTypeExported(typeName) {
|
|
56
111
|
const sourceCode = context.getSourceCode();
|
|
57
112
|
const program = sourceCode.ast;
|
|
113
|
+
// Check for imported types
|
|
114
|
+
const importedTypes = program.body.filter((node) => {
|
|
115
|
+
if (node.type === utils_1.AST_NODE_TYPES.ImportDeclaration) {
|
|
116
|
+
return node.specifiers.some((specifier) => specifier.type === utils_1.AST_NODE_TYPES.ImportSpecifier &&
|
|
117
|
+
specifier.local.name === typeName);
|
|
118
|
+
}
|
|
119
|
+
return false;
|
|
120
|
+
});
|
|
121
|
+
if (importedTypes.length > 0) {
|
|
122
|
+
return true;
|
|
123
|
+
}
|
|
58
124
|
// Check for exported type declarations
|
|
59
|
-
const exportedTypes = program.body.filter(node => {
|
|
125
|
+
const exportedTypes = program.body.filter((node) => {
|
|
60
126
|
if (node.type === utils_1.AST_NODE_TYPES.ExportNamedDeclaration) {
|
|
61
127
|
if (node.declaration?.type === utils_1.AST_NODE_TYPES.TSTypeAliasDeclaration) {
|
|
62
128
|
return node.declaration.id.name === typeName;
|
|
@@ -72,7 +138,7 @@ exports.enforceExportedFunctionTypes = (0, createRule_1.createRule)({
|
|
|
72
138
|
}
|
|
73
139
|
// Check for type aliases in the current scope
|
|
74
140
|
const scope = context.getScope();
|
|
75
|
-
const variable = scope.variables.find(v => v.name === typeName);
|
|
141
|
+
const variable = scope.variables.find((v) => v.name === typeName);
|
|
76
142
|
if (!variable)
|
|
77
143
|
return false;
|
|
78
144
|
const def = variable.defs[0];
|
|
@@ -112,36 +178,13 @@ exports.enforceExportedFunctionTypes = (0, createRule_1.createRule)({
|
|
|
112
178
|
return;
|
|
113
179
|
// Check return type
|
|
114
180
|
if (node.returnType?.typeAnnotation) {
|
|
115
|
-
|
|
116
|
-
if (typeName && !isTypeExported(typeName)) {
|
|
117
|
-
// Check if we've already reported this type
|
|
118
|
-
const key = `${typeName}-${node.loc?.start.line}-${node.loc?.start.column}`;
|
|
119
|
-
if (!reportedTypes.has(key)) {
|
|
120
|
-
reportedTypes.add(key);
|
|
121
|
-
context.report({
|
|
122
|
-
node: node.returnType,
|
|
123
|
-
messageId: 'missingExportedReturnType',
|
|
124
|
-
data: { typeName },
|
|
125
|
-
});
|
|
126
|
-
}
|
|
127
|
-
}
|
|
181
|
+
checkAndReportType(node.returnType.typeAnnotation, node.returnType, 'missingExportedReturnType');
|
|
128
182
|
}
|
|
129
183
|
// Check parameter types
|
|
130
|
-
node.params.forEach(param => {
|
|
131
|
-
if (param.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
// Check if we've already reported this type
|
|
135
|
-
const key = `${typeName}-${param.loc?.start.line}-${param.loc?.start.column}`;
|
|
136
|
-
if (!reportedTypes.has(key)) {
|
|
137
|
-
reportedTypes.add(key);
|
|
138
|
-
context.report({
|
|
139
|
-
node: param.typeAnnotation,
|
|
140
|
-
messageId: 'missingExportedType',
|
|
141
|
-
data: { typeName },
|
|
142
|
-
});
|
|
143
|
-
}
|
|
144
|
-
}
|
|
184
|
+
node.params.forEach((param) => {
|
|
185
|
+
if (param.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
186
|
+
param.typeAnnotation) {
|
|
187
|
+
checkAndReportType(param.typeAnnotation.typeAnnotation, param.typeAnnotation, 'missingExportedType');
|
|
145
188
|
}
|
|
146
189
|
});
|
|
147
190
|
},
|
|
@@ -155,36 +198,13 @@ exports.enforceExportedFunctionTypes = (0, createRule_1.createRule)({
|
|
|
155
198
|
return;
|
|
156
199
|
// Check return type
|
|
157
200
|
if (node.returnType?.typeAnnotation) {
|
|
158
|
-
|
|
159
|
-
if (typeName && !isTypeExported(typeName)) {
|
|
160
|
-
// Check if we've already reported this type
|
|
161
|
-
const key = `${typeName}-${node.loc?.start.line}-${node.loc?.start.column}`;
|
|
162
|
-
if (!reportedTypes.has(key)) {
|
|
163
|
-
reportedTypes.add(key);
|
|
164
|
-
context.report({
|
|
165
|
-
node: node.returnType,
|
|
166
|
-
messageId: 'missingExportedReturnType',
|
|
167
|
-
data: { typeName },
|
|
168
|
-
});
|
|
169
|
-
}
|
|
170
|
-
}
|
|
201
|
+
checkAndReportType(node.returnType.typeAnnotation, node.returnType, 'missingExportedReturnType');
|
|
171
202
|
}
|
|
172
203
|
// Check parameter types
|
|
173
|
-
node.params.forEach(param => {
|
|
174
|
-
if (param.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
// Check if we've already reported this type
|
|
178
|
-
const key = `${typeName}-${param.loc?.start.line}-${param.loc?.start.column}`;
|
|
179
|
-
if (!reportedTypes.has(key)) {
|
|
180
|
-
reportedTypes.add(key);
|
|
181
|
-
context.report({
|
|
182
|
-
node: param.typeAnnotation,
|
|
183
|
-
messageId: 'missingExportedType',
|
|
184
|
-
data: { typeName },
|
|
185
|
-
});
|
|
186
|
-
}
|
|
187
|
-
}
|
|
204
|
+
node.params.forEach((param) => {
|
|
205
|
+
if (param.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
206
|
+
param.typeAnnotation) {
|
|
207
|
+
checkAndReportType(param.typeAnnotation.typeAnnotation, param.typeAnnotation, 'missingExportedType');
|
|
188
208
|
}
|
|
189
209
|
});
|
|
190
210
|
},
|
|
@@ -194,20 +214,7 @@ exports.enforceExportedFunctionTypes = (0, createRule_1.createRule)({
|
|
|
194
214
|
return;
|
|
195
215
|
// Check props parameter
|
|
196
216
|
if (node.typeAnnotation) {
|
|
197
|
-
|
|
198
|
-
if (typeName && !isTypeExported(typeName)) {
|
|
199
|
-
// Check if we've already reported this type
|
|
200
|
-
const key = `${typeName}-${node.loc?.start.line}-${node.loc?.start.column}`;
|
|
201
|
-
if (!reportedTypes.has(key)) {
|
|
202
|
-
reportedTypes.add(key);
|
|
203
|
-
context.report({
|
|
204
|
-
node: node.typeAnnotation,
|
|
205
|
-
messageId: 'missingExportedType',
|
|
206
|
-
data: { typeName },
|
|
207
|
-
});
|
|
208
|
-
return;
|
|
209
|
-
}
|
|
210
|
-
}
|
|
217
|
+
checkAndReportType(node.typeAnnotation.typeAnnotation, node.typeAnnotation, 'missingExportedPropsType');
|
|
211
218
|
}
|
|
212
219
|
},
|
|
213
220
|
// Skip type checking for React components since we handle them separately
|
|
@@ -239,10 +246,6 @@ exports.enforceExportedFunctionTypes = (0, createRule_1.createRule)({
|
|
|
239
246
|
'VariableDeclarator[id.name=/^[A-Z]/] > ArrowFunctionExpression > Identifier[typeAnnotation] > TSTypeAnnotation > TSTypeReference > TSQualifiedName > TSQualifiedName > TSQualifiedName > TSQualifiedName > Identifier'() { },
|
|
240
247
|
'FunctionDeclaration[id.name=/^[A-Z]/] > Identifier[typeAnnotation] > TSTypeAnnotation > TSTypeReference > TSQualifiedName > TSQualifiedName > TSQualifiedName > TSQualifiedName > TSQualifiedName'() { },
|
|
241
248
|
'VariableDeclarator[id.name=/^[A-Z]/] > ArrowFunctionExpression > Identifier[typeAnnotation] > TSTypeAnnotation > TSTypeReference > TSQualifiedName > TSQualifiedName > TSQualifiedName > TSQualifiedName > TSQualifiedName'() { },
|
|
242
|
-
'FunctionDeclaration[id.name=/^[A-Z]/] > Identifier[typeAnnotation] > TSTypeAnnotation > TSTypeReference > TSQualifiedName > TSQualifiedName > TSQualifiedName > TSQualifiedName > TSQualifiedName > Identifier'() { },
|
|
243
|
-
'VariableDeclarator[id.name=/^[A-Z]/] > ArrowFunctionExpression > Identifier[typeAnnotation] > TSTypeAnnotation > TSTypeReference > TSQualifiedName > TSQualifiedName > TSQualifiedName > TSQualifiedName > TSQualifiedName > Identifier'() { },
|
|
244
|
-
'FunctionDeclaration[id.name=/^[A-Z]/] > Identifier[typeAnnotation] > TSTypeAnnotation > TSTypeReference > TSQualifiedName > TSQualifiedName > TSQualifiedName > TSQualifiedName > TSQualifiedName > TSQualifiedName'() { },
|
|
245
|
-
'VariableDeclarator[id.name=/^[A-Z]/] > ArrowFunctionExpression > Identifier[typeAnnotation] > TSTypeAnnotation > TSTypeReference > TSQualifiedName > TSQualifiedName > TSQualifiedName > TSQualifiedName > TSQualifiedName > TSQualifiedName'() { },
|
|
246
249
|
};
|
|
247
250
|
},
|
|
248
251
|
});
|
|
@@ -24,8 +24,8 @@ exports.enforceFirestoreDocRefGeneric = (0, createRule_1.createRule)({
|
|
|
24
24
|
},
|
|
25
25
|
schema: [],
|
|
26
26
|
messages: {
|
|
27
|
-
missingGeneric: '{{ type }} must specify a generic type argument',
|
|
28
|
-
invalidGeneric: '{{ type }} must not use "any" or "{}" as generic type argument',
|
|
27
|
+
missingGeneric: '{{ type }} must specify a generic type argument for type safety. Instead of `const docRef = doc(collection)`, use `const docRef = doc<YourType>(collection)`.',
|
|
28
|
+
invalidGeneric: '{{ type }} must not use "any" or "{}" as generic type argument. Define a proper interface/type for your document: `interface UserDoc { name: string; age: number; }` and use it: `const docRef = doc<UserDoc>(collection)`.',
|
|
29
29
|
},
|
|
30
30
|
},
|
|
31
31
|
defaultOptions: [],
|
|
@@ -9,12 +9,12 @@ exports.enforceFirestorePathUtils = (0, createRule_1.createRule)({
|
|
|
9
9
|
meta: {
|
|
10
10
|
type: 'suggestion',
|
|
11
11
|
docs: {
|
|
12
|
-
description: 'Enforce usage of utility functions for Firestore paths',
|
|
12
|
+
description: 'Enforce usage of utility functions for Firestore paths to ensure type safety, maintainability, and consistent path construction. This prevents errors from manual string concatenation and makes path changes easier to manage.',
|
|
13
13
|
recommended: 'error',
|
|
14
14
|
},
|
|
15
15
|
schema: [],
|
|
16
16
|
messages: {
|
|
17
|
-
requirePathUtil: 'Use a utility function
|
|
17
|
+
requirePathUtil: 'Use a utility function for Firestore paths to ensure type safety and maintainability. Instead of `doc("users/" + userId)`, create and use a utility function: `const toUserPath = (id: string) => `users/${id}`; doc(toUserPath(userId))`.',
|
|
18
18
|
},
|
|
19
19
|
},
|
|
20
20
|
defaultOptions: [],
|
|
@@ -8,7 +8,7 @@ exports.enforceFirestoreSetMerge = (0, createRule_1.createRule)({
|
|
|
8
8
|
meta: {
|
|
9
9
|
type: 'suggestion',
|
|
10
10
|
docs: {
|
|
11
|
-
description: 'Enforce using set() with { merge: true } instead of update() for Firestore operations',
|
|
11
|
+
description: 'Enforce using set() with { merge: true } instead of update() for Firestore operations to ensure consistent behavior. The update() method fails if the document does not exist, while set() with { merge: true } creates the document if needed and safely merges fields, making it more reliable and predictable.',
|
|
12
12
|
recommended: 'error',
|
|
13
13
|
requiresTypeChecking: false,
|
|
14
14
|
extendsBaseRule: false,
|
|
@@ -16,19 +16,50 @@ exports.enforceFirestoreSetMerge = (0, createRule_1.createRule)({
|
|
|
16
16
|
fixable: 'code',
|
|
17
17
|
schema: [],
|
|
18
18
|
messages: {
|
|
19
|
-
preferSetMerge: 'Use set() with { merge: true } instead of update() for more predictable Firestore operations',
|
|
19
|
+
preferSetMerge: 'Use set() with { merge: true } instead of update() for more predictable Firestore operations. Instead of `docRef.update({ field: value })`, use `docRef.set({ field: value }, { merge: true })`. This ensures consistent behavior when the document does not exist.',
|
|
20
20
|
},
|
|
21
21
|
},
|
|
22
22
|
defaultOptions: [],
|
|
23
23
|
create(context) {
|
|
24
24
|
const updateAliases = new Set();
|
|
25
25
|
function isFirestoreUpdateCall(node) {
|
|
26
|
+
// Check if it's a set() call with merge: true
|
|
26
27
|
if (node.callee.type === utils_1.AST_NODE_TYPES.MemberExpression) {
|
|
27
28
|
const property = node.callee.property;
|
|
28
|
-
|
|
29
|
-
|
|
29
|
+
if (property.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
30
|
+
// If it's a set() call, check if it has merge: true
|
|
31
|
+
if (property.name === 'set') {
|
|
32
|
+
const lastArg = node.arguments[node.arguments.length - 1];
|
|
33
|
+
if (lastArg?.type === utils_1.AST_NODE_TYPES.ObjectExpression) {
|
|
34
|
+
const hasMergeTrue = lastArg.properties.some((prop) => prop.type === utils_1.AST_NODE_TYPES.Property &&
|
|
35
|
+
prop.key.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
36
|
+
prop.key.name === 'merge' &&
|
|
37
|
+
prop.value.type === utils_1.AST_NODE_TYPES.Literal &&
|
|
38
|
+
prop.value.value === true);
|
|
39
|
+
if (hasMergeTrue) {
|
|
40
|
+
return false; // Already using set with merge: true
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
// Only flag update() calls
|
|
45
|
+
return property.name === 'update';
|
|
46
|
+
}
|
|
30
47
|
}
|
|
31
48
|
if (node.callee.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
49
|
+
// Check if it's a setDoc() call with merge: true
|
|
50
|
+
if (node.callee.name === 'setDoc') {
|
|
51
|
+
const lastArg = node.arguments[node.arguments.length - 1];
|
|
52
|
+
if (lastArg?.type === utils_1.AST_NODE_TYPES.ObjectExpression) {
|
|
53
|
+
const hasMergeTrue = lastArg.properties.some((prop) => prop.type === utils_1.AST_NODE_TYPES.Property &&
|
|
54
|
+
prop.key.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
55
|
+
prop.key.name === 'merge' &&
|
|
56
|
+
prop.value.type === utils_1.AST_NODE_TYPES.Literal &&
|
|
57
|
+
prop.value.value === true);
|
|
58
|
+
if (hasMergeTrue) {
|
|
59
|
+
return false; // Already using setDoc with merge: true
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
32
63
|
return updateAliases.has(node.callee.name);
|
|
33
64
|
}
|
|
34
65
|
return false;
|
|
@@ -17,8 +17,8 @@ exports.enforceIdentifiableFirestoreType = (0, createRule_1.createRule)({
|
|
|
17
17
|
},
|
|
18
18
|
schema: [],
|
|
19
19
|
messages: {
|
|
20
|
-
missingType: 'Expected exported type "{{ typeName }}" in index.ts under folder "{{ folderName }}"',
|
|
21
|
-
notExtendingIdentifiable: 'Type "{{ typeName }}" must extend "Identifiable"
|
|
20
|
+
missingType: 'Expected exported type "{{ typeName }}" in index.ts under folder "{{ folderName }}". Create a type that matches the folder name: `export type {{ typeName }} = { /* fields */ }`.',
|
|
21
|
+
notExtendingIdentifiable: 'Type "{{ typeName }}" must extend "Identifiable" to ensure all Firestore documents have an ID field. Add `extends Identifiable` or include `id: string`: `export type {{ typeName }} = { id: string; /* other fields */ }`.',
|
|
22
22
|
},
|
|
23
23
|
},
|
|
24
24
|
defaultOptions: [],
|
|
@@ -8,13 +8,13 @@ exports.enforceMemoizeAsync = (0, createRule_1.createRule)({
|
|
|
8
8
|
meta: {
|
|
9
9
|
type: 'suggestion',
|
|
10
10
|
docs: {
|
|
11
|
-
description: 'Enforce @Memoize() decorator on async methods with 0-1 parameters',
|
|
11
|
+
description: 'Enforce @Memoize() decorator on async methods with 0-1 parameters to cache results and prevent redundant API calls or expensive computations. This improves performance by reusing previous results when the same parameters are provided, particularly useful for data fetching methods.',
|
|
12
12
|
recommended: 'error',
|
|
13
13
|
},
|
|
14
14
|
fixable: 'code',
|
|
15
15
|
schema: [],
|
|
16
16
|
messages: {
|
|
17
|
-
requireMemoize: 'Async methods with 0-1 parameters should be decorated with @Memoize()',
|
|
17
|
+
requireMemoize: 'Async methods with 0-1 parameters should be decorated with @Memoize() to cache results and improve performance. Instead of `async getData(id?: string)`, use `@Memoize()\nasync getData(id?: string)`. Import Memoize from "typescript-memoize".',
|
|
18
18
|
},
|
|
19
19
|
},
|
|
20
20
|
defaultOptions: [],
|
|
@@ -13,13 +13,13 @@ exports.enforceFirestoreMock = (0, createRule_1.createRule)({
|
|
|
13
13
|
meta: {
|
|
14
14
|
type: 'problem',
|
|
15
15
|
docs: {
|
|
16
|
-
description: 'Enforce using mockFirestore
|
|
16
|
+
description: 'Enforce using the standardized mockFirestore utility instead of manual Firestore mocking or third-party mocks. This ensures consistent test behavior across the codebase, reduces boilerplate, and provides type-safe mocking of Firestore operations.',
|
|
17
17
|
recommended: 'error',
|
|
18
18
|
},
|
|
19
19
|
schema: [],
|
|
20
20
|
messages: {
|
|
21
|
-
noManualFirestoreMock: 'Use mockFirestore from __mocks__/functions/src/config/mockFirestore instead of manually mocking Firestore',
|
|
22
|
-
noMockFirebase: 'Use mockFirestore from __mocks__/functions/src/config/mockFirestore instead of mockFirebase',
|
|
21
|
+
noManualFirestoreMock: 'Use mockFirestore from __mocks__/functions/src/config/mockFirestore instead of manually mocking Firestore. Replace `jest.mock("firebase-admin", () => ({ firestore: () => ({ /* mock */ }) }))` with `import { mockFirestore } from "__mocks__/functions/src/config/mockFirestore"; jest.mock("firebase-admin", () => mockFirestore)`.',
|
|
22
|
+
noMockFirebase: 'Use mockFirestore from __mocks__/functions/src/config/mockFirestore instead of mockFirebase. Replace `import { mockFirebase } from "firestore-jest-mock"` with `import { mockFirestore } from "__mocks__/functions/src/config/mockFirestore"`.',
|
|
23
23
|
},
|
|
24
24
|
},
|
|
25
25
|
defaultOptions: [],
|
|
@@ -14,7 +14,7 @@ exports.enforceRealtimedbPathUtils = (0, createRule_1.createRule)({
|
|
|
14
14
|
},
|
|
15
15
|
schema: [],
|
|
16
16
|
messages: {
|
|
17
|
-
requirePathUtil: 'Use a utility function
|
|
17
|
+
requirePathUtil: 'Use a utility function for Realtime Database paths to ensure type safety and maintainability. Instead of `ref("users/" + userId)`, create and use a utility function: `const toUserPath = (id: string) => `users/${id}`; ref(toUserPath(userId))`.',
|
|
18
18
|
},
|
|
19
19
|
},
|
|
20
20
|
defaultOptions: [],
|
|
@@ -8,13 +8,13 @@ exports.enforceStableStringify = (0, createRule_1.createRule)({
|
|
|
8
8
|
meta: {
|
|
9
9
|
type: 'problem',
|
|
10
10
|
docs: {
|
|
11
|
-
description: 'Enforce using safe-stable-stringify instead of JSON.stringify',
|
|
11
|
+
description: 'Enforce using safe-stable-stringify instead of JSON.stringify to handle circular references and ensure deterministic output. JSON.stringify can throw errors on circular references and produce inconsistent output for objects with the same properties in different orders. safe-stable-stringify handles these cases safely.',
|
|
12
12
|
recommended: 'error',
|
|
13
13
|
},
|
|
14
14
|
fixable: 'code',
|
|
15
15
|
schema: [],
|
|
16
16
|
messages: {
|
|
17
|
-
useStableStringify: 'Use safe-stable-stringify instead of JSON.stringify for safer serialization',
|
|
17
|
+
useStableStringify: 'Use safe-stable-stringify instead of JSON.stringify for safer serialization. Replace `JSON.stringify(obj)` with `stringify(obj)`. First import it: `import stringify from "safe-stable-stringify"`. This handles circular references and provides deterministic output.',
|
|
18
18
|
},
|
|
19
19
|
},
|
|
20
20
|
defaultOptions: [],
|
|
@@ -17,7 +17,7 @@ exports.default = (0, createRule_1.createRule)({
|
|
|
17
17
|
meta: {
|
|
18
18
|
type: 'problem',
|
|
19
19
|
docs: {
|
|
20
|
-
description: 'Enforce serializable parameters in Firebase Callable/HTTPS Cloud Functions',
|
|
20
|
+
description: 'Enforce serializable parameters in Firebase Callable/HTTPS Cloud Functions to prevent runtime errors. Firebase Functions can only pass JSON-serializable data, so using non-serializable types like Date, DocumentReference, or Map will cause failures. Use primitive types, plain objects, and arrays instead, converting complex types to their serializable representations (e.g., Date to ISO string).',
|
|
21
21
|
recommended: 'error',
|
|
22
22
|
},
|
|
23
23
|
schema: [
|
|
@@ -38,8 +38,8 @@ exports.default = (0, createRule_1.createRule)({
|
|
|
38
38
|
},
|
|
39
39
|
],
|
|
40
40
|
messages: {
|
|
41
|
-
nonSerializableParam: 'Parameter type "{{ type }}" is not serializable',
|
|
42
|
-
nonSerializableProperty: 'Property "{{ prop }}" has non-serializable type "{{ type }}"',
|
|
41
|
+
nonSerializableParam: 'Parameter type "{{ type }}" is not serializable in Firebase Cloud Functions. Use JSON-serializable types like string, number, boolean, arrays, or plain objects. Instead of `Date`, use ISO strings: `new Date().toISOString()`.',
|
|
42
|
+
nonSerializableProperty: 'Property "{{ prop }}" has non-serializable type "{{ type }}". Use JSON-serializable types. For example, instead of `{ timestamp: Date }`, use `{ timestamp: string }` with ISO format.',
|
|
43
43
|
},
|
|
44
44
|
},
|
|
45
45
|
defaultOptions: [
|
|
@@ -8,6 +8,22 @@ const utils_1 = require("@typescript-eslint/utils");
|
|
|
8
8
|
const createRule_1 = require("../utils/createRule");
|
|
9
9
|
const compromise_1 = __importDefault(require("compromise"));
|
|
10
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
|
+
]);
|
|
11
27
|
exports.enforceVerbNounNaming = (0, createRule_1.createRule)({
|
|
12
28
|
name: 'enforce-verb-noun-naming',
|
|
13
29
|
meta: {
|
|
@@ -32,7 +48,8 @@ exports.enforceVerbNounNaming = (0, createRule_1.createRule)({
|
|
|
32
48
|
return firstChar + words[0];
|
|
33
49
|
}
|
|
34
50
|
function toSentence(name) {
|
|
35
|
-
|
|
51
|
+
// Add "I" prefix to create a proper sentence for better verb detection
|
|
52
|
+
return 'I ' + name.split(/(?=[A-Z])/).join(' ');
|
|
36
53
|
}
|
|
37
54
|
function getPossibleTags(sentence) {
|
|
38
55
|
const doc = (0, compromise_1.default)(sentence);
|
|
@@ -45,14 +62,23 @@ exports.enforceVerbNounNaming = (0, createRule_1.createRule)({
|
|
|
45
62
|
function isVerbPhrase(name) {
|
|
46
63
|
const firstWord = extractFirstWord(name);
|
|
47
64
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
48
|
-
|
|
65
|
+
const firstWordLower = firstWord.toLowerCase();
|
|
66
|
+
// Check for prepositions and common verbs first
|
|
67
|
+
if (PREPOSITIONS.includes(firstWordLower) || COMMON_VERBS.has(firstWordLower)) {
|
|
49
68
|
return true;
|
|
50
69
|
}
|
|
51
|
-
|
|
52
|
-
const
|
|
53
|
-
const
|
|
54
|
-
|
|
55
|
-
|
|
70
|
+
// Try both with and without "I" prefix to catch more verb forms
|
|
71
|
+
const withPrefixTags = getPossibleTags(toSentence(name));
|
|
72
|
+
const withoutPrefixTags = getPossibleTags(firstWord);
|
|
73
|
+
// 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');
|
|
77
|
+
// For non-prepositions/conjunctions, require verb form
|
|
78
|
+
if (isPreposition || isConjunction) {
|
|
79
|
+
return true;
|
|
80
|
+
}
|
|
81
|
+
return isVerb;
|
|
56
82
|
}
|
|
57
83
|
function isJsxReturnFunction(node) {
|
|
58
84
|
if (node.type !== utils_1.AST_NODE_TYPES.FunctionDeclaration &&
|
|
@@ -99,6 +125,12 @@ exports.enforceVerbNounNaming = (0, createRule_1.createRule)({
|
|
|
99
125
|
MethodDefinition(node) {
|
|
100
126
|
if (node.key.type !== utils_1.AST_NODE_TYPES.Identifier)
|
|
101
127
|
return;
|
|
128
|
+
// Skip getters since they represent properties and should use noun phrases
|
|
129
|
+
if (node.kind === 'get')
|
|
130
|
+
return;
|
|
131
|
+
// Skip constructors since they are special class methods
|
|
132
|
+
if (node.kind === 'constructor')
|
|
133
|
+
return;
|
|
102
134
|
if (!isVerbPhrase(node.key.name)) {
|
|
103
135
|
context.report({
|
|
104
136
|
node: node.key,
|
|
@@ -27,12 +27,12 @@ exports.noAsyncArrayFilter = (0, createRule_1.createRule)({
|
|
|
27
27
|
meta: {
|
|
28
28
|
type: 'problem',
|
|
29
29
|
docs: {
|
|
30
|
-
description: 'Disallow async callbacks
|
|
30
|
+
description: 'Disallow async callbacks in Array.filter() as they lead to incorrect filtering. Since async functions return Promises which are always truthy, the filter will keep all elements regardless of the async check\'s result. Use Promise.all() with map() first, then filter based on the resolved results.',
|
|
31
31
|
recommended: 'error',
|
|
32
32
|
},
|
|
33
33
|
schema: [],
|
|
34
34
|
messages: {
|
|
35
|
-
unexpected: 'Async array filter is dangerous as a Promise object will always be truthy.
|
|
35
|
+
unexpected: 'Async array filter is dangerous as a Promise object will always be truthy. Instead of `array.filter(async x => await someCheck(x))`, first resolve the promises with `Promise.all()` or move the async logic elsewhere: `const results = await Promise.all(array.map(x => someCheck(x))); array.filter((_, i) => results[i])`.',
|
|
36
36
|
},
|
|
37
37
|
},
|
|
38
38
|
defaultOptions: [],
|
|
@@ -24,7 +24,7 @@ exports.noAsyncForEach = {
|
|
|
24
24
|
meta: {
|
|
25
25
|
type: 'problem',
|
|
26
26
|
docs: {
|
|
27
|
-
description: 'Disallow Array.forEach with an async callback function',
|
|
27
|
+
description: 'Disallow Array.forEach with an async callback function as it does not wait for promises to resolve. This can lead to race conditions and unexpected behavior. Use a standard for...of loop for sequential execution or Promise.all with map for concurrent execution.',
|
|
28
28
|
recommended: 'error',
|
|
29
29
|
},
|
|
30
30
|
messages: {
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const noClassInstanceDestructuring: import("@typescript-eslint/utils/dist/ts-eslint/Rule").RuleModule<"noClassInstanceDestructuring", [], import("@typescript-eslint/utils/dist/ts-eslint/Rule").RuleListener>;
|