@blumintinc/eslint-plugin-blumint 1.7.2 → 1.8.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/README.md +39 -62
- package/lib/index.js +37 -1
- package/lib/rules/enforce-assert-throws.js +48 -3
- package/lib/rules/enforce-assertSafe-object-key.d.ts +2 -0
- package/lib/rules/enforce-assertSafe-object-key.js +284 -0
- package/lib/rules/enforce-centralized-mock-firestore.js +193 -15
- package/lib/rules/enforce-firestore-facade.js +8 -0
- package/lib/rules/enforce-microdiff.d.ts +3 -0
- package/lib/rules/enforce-microdiff.js +379 -0
- package/lib/rules/enforce-mock-firestore.js +2 -2
- package/lib/rules/enforce-object-literal-as-const.d.ts +4 -0
- package/lib/rules/enforce-object-literal-as-const.js +88 -0
- package/lib/rules/enforce-positive-naming.d.ts +1 -0
- package/lib/rules/enforce-positive-naming.js +363 -0
- package/lib/rules/enforce-props-argument-name.d.ts +8 -0
- package/lib/rules/enforce-props-argument-name.js +182 -0
- package/lib/rules/enforce-render-hits-memoization.js +166 -17
- package/lib/rules/enforce-timestamp-now.d.ts +1 -0
- package/lib/rules/enforce-timestamp-now.js +223 -0
- package/lib/rules/enforce-verb-noun-naming.js +1 -0
- package/lib/rules/extract-global-constants.d.ts +1 -1
- package/lib/rules/extract-global-constants.js +109 -1
- package/lib/rules/global-const-style.js +3 -2
- package/lib/rules/no-always-true-false-conditions.d.ts +3 -0
- package/lib/rules/no-always-true-false-conditions.js +1202 -0
- package/lib/rules/no-firestore-jest-mock.js +27 -4
- package/lib/rules/no-mock-firebase-admin.js +21 -8
- package/lib/rules/no-type-assertion-returns.d.ts +9 -0
- package/lib/rules/no-type-assertion-returns.js +289 -0
- package/lib/rules/no-unnecessary-verb-suffix.d.ts +1 -0
- package/lib/rules/no-unnecessary-verb-suffix.js +203 -0
- package/lib/rules/no-unused-props.js +47 -1
- package/lib/rules/prefer-clone-deep.js +294 -22
- package/lib/rules/prefer-fragment-component.js +265 -34
- package/lib/rules/prefer-global-router-state-key.d.ts +5 -0
- package/lib/rules/prefer-global-router-state-key.js +89 -0
- package/lib/rules/prefer-utility-function-over-private-static.d.ts +1 -0
- package/lib/rules/prefer-utility-function-over-private-static.js +77 -0
- package/lib/rules/react-usememo-should-be-component.d.ts +1 -0
- package/lib/rules/react-usememo-should-be-component.js +256 -0
- package/package.json +5 -5
|
@@ -21,43 +21,182 @@ exports.enforceRenderHitsMemoization = (0, createRule_1.createRule)({
|
|
|
21
21
|
},
|
|
22
22
|
defaultOptions: [],
|
|
23
23
|
create(context) {
|
|
24
|
+
const isReactComponent = (node) => {
|
|
25
|
+
if (node.type !== utils_1.AST_NODE_TYPES.Identifier)
|
|
26
|
+
return false;
|
|
27
|
+
return /^[A-Z]/.test(node.name);
|
|
28
|
+
};
|
|
24
29
|
const isMemoizedCall = (node) => {
|
|
25
30
|
if (node.type !== utils_1.AST_NODE_TYPES.CallExpression)
|
|
26
31
|
return false;
|
|
27
|
-
|
|
28
|
-
if (callee.type !== utils_1.AST_NODE_TYPES.Identifier)
|
|
32
|
+
if (!node.callee || node.callee.type !== utils_1.AST_NODE_TYPES.Identifier)
|
|
29
33
|
return false;
|
|
30
|
-
return callee.name === 'useCallback' || callee.name === 'useMemo';
|
|
34
|
+
return (node.callee.name === 'useCallback' || node.callee.name === 'useMemo');
|
|
31
35
|
};
|
|
32
|
-
const
|
|
36
|
+
const isMemoizedVariable = (node) => {
|
|
33
37
|
if (node.type !== utils_1.AST_NODE_TYPES.Identifier)
|
|
34
38
|
return false;
|
|
35
|
-
|
|
36
|
-
|
|
39
|
+
// Get the variable declaration for this identifier
|
|
40
|
+
const variable = context
|
|
41
|
+
.getScope()
|
|
42
|
+
.variables.find((v) => v.name === node.name);
|
|
43
|
+
if (!variable)
|
|
44
|
+
return false;
|
|
45
|
+
// Check if the variable is initialized with a memoized call
|
|
46
|
+
for (const def of variable.defs) {
|
|
47
|
+
if (!def || !def.node)
|
|
48
|
+
continue;
|
|
49
|
+
if (def.node.type === utils_1.AST_NODE_TYPES.VariableDeclarator &&
|
|
50
|
+
def.node.init) {
|
|
51
|
+
if (isMemoizedCall(def.node.init)) {
|
|
52
|
+
return true;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return false;
|
|
37
57
|
};
|
|
38
58
|
const isInsideMemoizedCall = (node) => {
|
|
59
|
+
// Handle the case when node is already a memoized call
|
|
60
|
+
if (isMemoizedCall(node))
|
|
61
|
+
return true;
|
|
62
|
+
// Check if the node is a reference to a memoized variable
|
|
63
|
+
if (isMemoizedVariable(node))
|
|
64
|
+
return true;
|
|
65
|
+
// Check if the node is inside a memoization hook call
|
|
39
66
|
let current = node;
|
|
40
67
|
while (current?.parent) {
|
|
41
|
-
if (
|
|
42
|
-
|
|
68
|
+
if (current.parent.type === utils_1.AST_NODE_TYPES.CallExpression) {
|
|
69
|
+
const callee = current.parent.callee;
|
|
70
|
+
if (callee.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
71
|
+
(callee.name === 'useCallback' || callee.name === 'useMemo')) {
|
|
72
|
+
return true;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
43
75
|
current = current.parent;
|
|
44
76
|
}
|
|
77
|
+
// Check if the node is a reference to a memoized value
|
|
78
|
+
const scope = context.getScope();
|
|
79
|
+
// Make sure node is an Identifier before accessing name property
|
|
80
|
+
if (node.type !== utils_1.AST_NODE_TYPES.Identifier) {
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
const variable = scope.variables.find((v) => v.name === node.name);
|
|
84
|
+
if (!variable) {
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
// Check if any definition is a memoized value
|
|
88
|
+
for (const def of variable.defs) {
|
|
89
|
+
const parent = def.node.parent;
|
|
90
|
+
if (parent?.type === utils_1.AST_NODE_TYPES.VariableDeclarator &&
|
|
91
|
+
parent.init?.type === utils_1.AST_NODE_TYPES.CallExpression) {
|
|
92
|
+
const callee = parent.init.callee;
|
|
93
|
+
if (callee.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
94
|
+
(callee.name === 'useCallback' || callee.name === 'useMemo')) {
|
|
95
|
+
return true;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
// Check if any reference is inside a memoized call
|
|
100
|
+
for (const ref of variable.references) {
|
|
101
|
+
let current = ref.identifier;
|
|
102
|
+
while (current?.parent) {
|
|
103
|
+
if (current.parent.type === utils_1.AST_NODE_TYPES.CallExpression) {
|
|
104
|
+
const callee = current.parent.callee;
|
|
105
|
+
if (callee.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
106
|
+
(callee.name === 'useCallback' || callee.name === 'useMemo')) {
|
|
107
|
+
return true;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
current = current.parent;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
// Check if the node is a property of an object that is memoized
|
|
114
|
+
const parent = node.parent;
|
|
115
|
+
if (parent?.type === utils_1.AST_NODE_TYPES.Property &&
|
|
116
|
+
parent.parent?.type === utils_1.AST_NODE_TYPES.ObjectExpression) {
|
|
117
|
+
const objectExpression = parent.parent;
|
|
118
|
+
let current = objectExpression;
|
|
119
|
+
while (current?.parent) {
|
|
120
|
+
if (current.parent.type === utils_1.AST_NODE_TYPES.CallExpression) {
|
|
121
|
+
const callee = current.parent.callee;
|
|
122
|
+
if (callee.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
123
|
+
(callee.name === 'useCallback' || callee.name === 'useMemo')) {
|
|
124
|
+
return true;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
current = current.parent;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
45
130
|
return false;
|
|
46
131
|
};
|
|
132
|
+
let useRenderHitsName = 'useRenderHits';
|
|
133
|
+
let renderHitsName = 'renderHits';
|
|
47
134
|
return {
|
|
135
|
+
ImportDeclaration(node) {
|
|
136
|
+
if (node.source.value.endsWith('useRenderHits')) {
|
|
137
|
+
for (const specifier of node.specifiers) {
|
|
138
|
+
if (specifier.type === utils_1.AST_NODE_TYPES.ImportSpecifier &&
|
|
139
|
+
specifier.imported.name === 'useRenderHits') {
|
|
140
|
+
useRenderHitsName = specifier.local.name;
|
|
141
|
+
break;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
else if (node.source.value.endsWith('renderHits')) {
|
|
146
|
+
for (const specifier of node.specifiers) {
|
|
147
|
+
if (specifier.type === utils_1.AST_NODE_TYPES.ImportSpecifier &&
|
|
148
|
+
specifier.imported.name === 'renderHits') {
|
|
149
|
+
renderHitsName = specifier.local.name;
|
|
150
|
+
break;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
},
|
|
48
155
|
CallExpression(node) {
|
|
49
|
-
if (node.callee.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
156
|
+
if (node.callee.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
157
|
+
node.callee.name === useRenderHitsName) {
|
|
50
158
|
if (node.arguments.length === 0)
|
|
51
159
|
return;
|
|
52
160
|
const options = node.arguments[0];
|
|
53
161
|
if (options.type !== utils_1.AST_NODE_TYPES.ObjectExpression)
|
|
54
162
|
return;
|
|
163
|
+
// Variable to track if we need to check properties (check both by default)
|
|
164
|
+
const checkProps = {
|
|
165
|
+
transformBefore: true,
|
|
166
|
+
render: true,
|
|
167
|
+
};
|
|
168
|
+
// First pass: Check if transformBefore or render properties exist as shorthand
|
|
55
169
|
for (const prop of options.properties) {
|
|
56
170
|
if (prop.type !== utils_1.AST_NODE_TYPES.Property)
|
|
57
171
|
continue;
|
|
58
172
|
if (prop.key.type !== utils_1.AST_NODE_TYPES.Identifier)
|
|
59
173
|
continue;
|
|
60
|
-
|
|
174
|
+
// If it's shorthand property syntax like { transformBefore } and already a memoized variable
|
|
175
|
+
if (prop.key.name === 'transformBefore' &&
|
|
176
|
+
prop.shorthand &&
|
|
177
|
+
prop.key.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
178
|
+
checkProps.transformBefore = !isMemoizedVariable(prop.key);
|
|
179
|
+
}
|
|
180
|
+
else if (prop.key.name === 'render' &&
|
|
181
|
+
prop.shorthand &&
|
|
182
|
+
prop.key.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
183
|
+
checkProps.render = !isMemoizedVariable(prop.key);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
// Second pass: Check non-shorthand properties
|
|
187
|
+
for (const prop of options.properties) {
|
|
188
|
+
if (prop.type !== utils_1.AST_NODE_TYPES.Property)
|
|
189
|
+
continue;
|
|
190
|
+
if (prop.key.type !== utils_1.AST_NODE_TYPES.Identifier)
|
|
191
|
+
continue;
|
|
192
|
+
// Skip shorthand properties that we already checked
|
|
193
|
+
if (prop.shorthand)
|
|
194
|
+
continue;
|
|
195
|
+
if (prop.key.name === 'transformBefore' &&
|
|
196
|
+
checkProps.transformBefore) {
|
|
197
|
+
// Skip if the value is already a memoized call
|
|
198
|
+
if (isMemoizedCall(prop.value))
|
|
199
|
+
continue;
|
|
61
200
|
if (!isInsideMemoizedCall(prop.value)) {
|
|
62
201
|
context.report({
|
|
63
202
|
node: prop.value,
|
|
@@ -65,7 +204,7 @@ exports.enforceRenderHitsMemoization = (0, createRule_1.createRule)({
|
|
|
65
204
|
});
|
|
66
205
|
}
|
|
67
206
|
}
|
|
68
|
-
if (prop.key.name === 'render') {
|
|
207
|
+
else if (prop.key.name === 'render' && checkProps.render) {
|
|
69
208
|
if (isReactComponent(prop.value)) {
|
|
70
209
|
context.report({
|
|
71
210
|
node: prop.value,
|
|
@@ -81,13 +220,23 @@ exports.enforceRenderHitsMemoization = (0, createRule_1.createRule)({
|
|
|
81
220
|
}
|
|
82
221
|
}
|
|
83
222
|
}
|
|
84
|
-
if (node.callee.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
223
|
+
if (node.callee.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
224
|
+
node.callee.name === renderHitsName) {
|
|
225
|
+
let current = node;
|
|
226
|
+
while (current?.parent) {
|
|
227
|
+
if (current.parent.type === utils_1.AST_NODE_TYPES.CallExpression) {
|
|
228
|
+
const callee = current.parent.callee;
|
|
229
|
+
if (callee.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
230
|
+
(callee.name === 'useCallback' || callee.name === 'useMemo')) {
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
current = current.parent;
|
|
90
235
|
}
|
|
236
|
+
context.report({
|
|
237
|
+
node,
|
|
238
|
+
messageId: 'requireMemoizedRenderHits',
|
|
239
|
+
});
|
|
91
240
|
}
|
|
92
241
|
},
|
|
93
242
|
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const enforceTimestampNow: import("@typescript-eslint/utils/dist/ts-eslint/Rule").RuleModule<"preferTimestampNow", [], import("@typescript-eslint/utils/dist/ts-eslint/Rule").RuleListener>;
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.enforceTimestampNow = void 0;
|
|
4
|
+
const utils_1 = require("@typescript-eslint/utils");
|
|
5
|
+
const createRule_1 = require("../utils/createRule");
|
|
6
|
+
exports.enforceTimestampNow = (0, createRule_1.createRule)({
|
|
7
|
+
name: 'enforce-timestamp-now',
|
|
8
|
+
meta: {
|
|
9
|
+
type: 'suggestion',
|
|
10
|
+
docs: {
|
|
11
|
+
description: 'Enforce the use of Timestamp.now() for getting the current timestamp in backend code. This rule prevents using alternatives like Timestamp.fromDate(new Date()) or other date creation patterns that could lead to inconsistency.',
|
|
12
|
+
recommended: 'error',
|
|
13
|
+
requiresTypeChecking: false,
|
|
14
|
+
extendsBaseRule: false,
|
|
15
|
+
},
|
|
16
|
+
fixable: 'code',
|
|
17
|
+
schema: [],
|
|
18
|
+
messages: {
|
|
19
|
+
preferTimestampNow: 'Use Timestamp.now() instead of creating a Date object and converting it. This is more efficient and idiomatic for Firestore.',
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
defaultOptions: [],
|
|
23
|
+
create(context) {
|
|
24
|
+
// Only apply this rule to backend code (functions/src/)
|
|
25
|
+
const filename = context.getFilename();
|
|
26
|
+
if (!filename.includes('functions/src/')) {
|
|
27
|
+
return {};
|
|
28
|
+
}
|
|
29
|
+
// Skip test files
|
|
30
|
+
if (filename.includes('.test.') || filename.includes('.spec.')) {
|
|
31
|
+
return {};
|
|
32
|
+
}
|
|
33
|
+
// Track Timestamp imports and aliases
|
|
34
|
+
const timestampAliases = new Set(['Timestamp']);
|
|
35
|
+
function isTimestampFromDateWithNewDate(node) {
|
|
36
|
+
// Check if it's a Timestamp.fromDate(new Date()) call
|
|
37
|
+
if (node.callee.type === utils_1.AST_NODE_TYPES.MemberExpression) {
|
|
38
|
+
const property = node.callee.property;
|
|
39
|
+
const object = node.callee.object;
|
|
40
|
+
if (property.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
41
|
+
property.name === 'fromDate' &&
|
|
42
|
+
object.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
43
|
+
timestampAliases.has(object.name)) {
|
|
44
|
+
// Check if the argument is new Date()
|
|
45
|
+
const arg = node.arguments[0];
|
|
46
|
+
if (arg &&
|
|
47
|
+
arg.type === utils_1.AST_NODE_TYPES.NewExpression &&
|
|
48
|
+
arg.callee.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
49
|
+
arg.callee.name === 'Date' &&
|
|
50
|
+
arg.arguments.length === 0) {
|
|
51
|
+
return true;
|
|
52
|
+
}
|
|
53
|
+
// Check if the argument is a variable reference to a Date object
|
|
54
|
+
if (arg &&
|
|
55
|
+
arg.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
56
|
+
// If it's a variable, we need to check if it's a Date that's being modified
|
|
57
|
+
// If it's modified, we shouldn't flag it
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
function isTimestampFromMillisWithDateNow(node) {
|
|
65
|
+
// Check if it's a Timestamp.fromMillis(Date.now()) call
|
|
66
|
+
if (node.callee.type === utils_1.AST_NODE_TYPES.MemberExpression) {
|
|
67
|
+
const property = node.callee.property;
|
|
68
|
+
const object = node.callee.object;
|
|
69
|
+
if (property.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
70
|
+
property.name === 'fromMillis' &&
|
|
71
|
+
object.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
72
|
+
timestampAliases.has(object.name)) {
|
|
73
|
+
// Check if the argument is Date.now()
|
|
74
|
+
const arg = node.arguments[0];
|
|
75
|
+
if (arg &&
|
|
76
|
+
arg.type === utils_1.AST_NODE_TYPES.CallExpression &&
|
|
77
|
+
arg.callee.type === utils_1.AST_NODE_TYPES.MemberExpression &&
|
|
78
|
+
arg.callee.object.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
79
|
+
arg.callee.object.name === 'Date' &&
|
|
80
|
+
arg.callee.property.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
81
|
+
arg.callee.property.name === 'now') {
|
|
82
|
+
return true;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return false;
|
|
87
|
+
}
|
|
88
|
+
function isNewDateDirectUsage(node) {
|
|
89
|
+
// Check if it's a new Date() with no arguments
|
|
90
|
+
return (node.callee.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
91
|
+
node.callee.name === 'Date' &&
|
|
92
|
+
node.arguments.length === 0);
|
|
93
|
+
}
|
|
94
|
+
// Check if a Date object is being modified (e.g., futureDate.setDate())
|
|
95
|
+
function isDateBeingModified(dateVar) {
|
|
96
|
+
// Look through the scope to find if this variable is modified
|
|
97
|
+
const scope = context.getScope();
|
|
98
|
+
const variable = scope.variables.find(v => v.name === dateVar);
|
|
99
|
+
if (!variable)
|
|
100
|
+
return false;
|
|
101
|
+
// Check if any references to this variable are followed by property access and modification
|
|
102
|
+
return variable.references.some(ref => {
|
|
103
|
+
const id = ref.identifier;
|
|
104
|
+
const parent = id.parent;
|
|
105
|
+
// Check for patterns like dateVar.setDate(), dateVar.setHours(), etc.
|
|
106
|
+
return (parent &&
|
|
107
|
+
parent.type === utils_1.AST_NODE_TYPES.MemberExpression &&
|
|
108
|
+
parent.object === id &&
|
|
109
|
+
parent.property.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
110
|
+
(parent.property.name.startsWith('set') ||
|
|
111
|
+
parent.property.name === 'toISOString' ||
|
|
112
|
+
parent.property.name === 'toLocaleString' ||
|
|
113
|
+
parent.property.name === 'toString'));
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
return {
|
|
117
|
+
ImportDeclaration(node) {
|
|
118
|
+
// Track Timestamp imports from Firebase
|
|
119
|
+
if (node.source.value === 'firebase-admin/firestore' ||
|
|
120
|
+
node.source.value === 'firebase/firestore') {
|
|
121
|
+
node.specifiers.forEach((specifier) => {
|
|
122
|
+
if (specifier.type === utils_1.AST_NODE_TYPES.ImportSpecifier &&
|
|
123
|
+
specifier.imported.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
124
|
+
specifier.imported.name === 'Timestamp') {
|
|
125
|
+
timestampAliases.add(specifier.local.name);
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
},
|
|
130
|
+
VariableDeclarator(node) {
|
|
131
|
+
// Track dynamic imports of Timestamp
|
|
132
|
+
if (node.init?.type === utils_1.AST_NODE_TYPES.AwaitExpression &&
|
|
133
|
+
node.init.argument.type === utils_1.AST_NODE_TYPES.ImportExpression) {
|
|
134
|
+
const importSource = node.init.argument.source;
|
|
135
|
+
if (importSource.type === utils_1.AST_NODE_TYPES.Literal &&
|
|
136
|
+
(importSource.value === 'firebase-admin/firestore' ||
|
|
137
|
+
importSource.value === 'firebase/firestore')) {
|
|
138
|
+
// Handle destructured imports
|
|
139
|
+
if (node.id.type === utils_1.AST_NODE_TYPES.ObjectPattern) {
|
|
140
|
+
node.id.properties.forEach((prop) => {
|
|
141
|
+
if (prop.type === utils_1.AST_NODE_TYPES.Property &&
|
|
142
|
+
prop.key.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
143
|
+
prop.key.name === 'Timestamp') {
|
|
144
|
+
if (prop.value.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
145
|
+
timestampAliases.add(prop.value.name);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
},
|
|
153
|
+
CallExpression(node) {
|
|
154
|
+
if (isTimestampFromDateWithNewDate(node)) {
|
|
155
|
+
context.report({
|
|
156
|
+
node,
|
|
157
|
+
messageId: 'preferTimestampNow',
|
|
158
|
+
fix(fixer) {
|
|
159
|
+
if (node.callee.type === utils_1.AST_NODE_TYPES.MemberExpression) {
|
|
160
|
+
const timestampObj = context
|
|
161
|
+
.getSourceCode()
|
|
162
|
+
.getText(node.callee.object);
|
|
163
|
+
return fixer.replaceText(node, `${timestampObj}.now()`);
|
|
164
|
+
}
|
|
165
|
+
return null;
|
|
166
|
+
},
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
else if (isTimestampFromMillisWithDateNow(node)) {
|
|
170
|
+
context.report({
|
|
171
|
+
node,
|
|
172
|
+
messageId: 'preferTimestampNow',
|
|
173
|
+
fix(fixer) {
|
|
174
|
+
if (node.callee.type === utils_1.AST_NODE_TYPES.MemberExpression) {
|
|
175
|
+
const timestampObj = context
|
|
176
|
+
.getSourceCode()
|
|
177
|
+
.getText(node.callee.object);
|
|
178
|
+
return fixer.replaceText(node, `${timestampObj}.now()`);
|
|
179
|
+
}
|
|
180
|
+
return null;
|
|
181
|
+
},
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
},
|
|
185
|
+
NewExpression(node) {
|
|
186
|
+
// Only flag direct new Date() usage if it's assigned to a variable named timestamp or similar
|
|
187
|
+
if (isNewDateDirectUsage(node)) {
|
|
188
|
+
const parent = node.parent;
|
|
189
|
+
if (parent &&
|
|
190
|
+
parent.type === utils_1.AST_NODE_TYPES.VariableDeclarator &&
|
|
191
|
+
parent.id.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
192
|
+
const varName = parent.id.name.toLowerCase();
|
|
193
|
+
// Check if the variable name suggests it's a timestamp
|
|
194
|
+
if (varName.includes('timestamp') ||
|
|
195
|
+
varName.includes('time') ||
|
|
196
|
+
varName.includes('now') ||
|
|
197
|
+
varName.includes('date') ||
|
|
198
|
+
varName.includes('created') ||
|
|
199
|
+
varName.includes('updated')) {
|
|
200
|
+
// Check if the Date object is being modified
|
|
201
|
+
if (isDateBeingModified(parent.id.name)) {
|
|
202
|
+
// If the Date is being modified, don't flag it
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
// Check if we have a Timestamp import before suggesting
|
|
206
|
+
if (timestampAliases.size > 0) {
|
|
207
|
+
const timestampName = Array.from(timestampAliases)[0];
|
|
208
|
+
context.report({
|
|
209
|
+
node,
|
|
210
|
+
messageId: 'preferTimestampNow',
|
|
211
|
+
fix(fixer) {
|
|
212
|
+
return fixer.replaceText(node, `${timestampName}.now()`);
|
|
213
|
+
},
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
},
|
|
220
|
+
};
|
|
221
|
+
},
|
|
222
|
+
});
|
|
223
|
+
//# sourceMappingURL=enforce-timestamp-now.js.map
|
|
@@ -10,6 +10,7 @@ const compromise_1 = __importDefault(require("compromise"));
|
|
|
10
10
|
const PREPOSITIONS = new Set(['to', 'from', 'with', 'by', 'at', 'of']);
|
|
11
11
|
// Create a Set from the verbs list for O(1) lookup
|
|
12
12
|
const VERBS_SET = new Set([
|
|
13
|
+
'clean',
|
|
13
14
|
'abbreviate',
|
|
14
15
|
'abort',
|
|
15
16
|
'abstract',
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
import { TSESLint } from '@typescript-eslint/utils';
|
|
2
|
-
export declare const extractGlobalConstants: TSESLint.RuleModule<'extractGlobalConstants', never[]>;
|
|
2
|
+
export declare const extractGlobalConstants: TSESLint.RuleModule<'extractGlobalConstants' | 'requireAsConst', never[]>;
|
|
@@ -90,6 +90,29 @@ function isMutableValue(node) {
|
|
|
90
90
|
}
|
|
91
91
|
return false;
|
|
92
92
|
}
|
|
93
|
+
function isNumericLiteral(node) {
|
|
94
|
+
if (!node)
|
|
95
|
+
return false;
|
|
96
|
+
return node.type === 'Literal' && typeof node.value === 'number';
|
|
97
|
+
}
|
|
98
|
+
function isZeroOrOne(node) {
|
|
99
|
+
if (!isNumericLiteral(node))
|
|
100
|
+
return false;
|
|
101
|
+
const value = node.value;
|
|
102
|
+
return value === 0 || value === 1;
|
|
103
|
+
}
|
|
104
|
+
function isAsConstExpression(node) {
|
|
105
|
+
if (!node)
|
|
106
|
+
return false;
|
|
107
|
+
if (node.type === 'TSAsExpression') {
|
|
108
|
+
if (node.typeAnnotation.type === 'TSTypeReference' &&
|
|
109
|
+
node.typeAnnotation.typeName.type === 'Identifier' &&
|
|
110
|
+
node.typeAnnotation.typeName.name === 'const') {
|
|
111
|
+
return true;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return false;
|
|
115
|
+
}
|
|
93
116
|
exports.extractGlobalConstants = (0, createRule_1.createRule)({
|
|
94
117
|
create(context) {
|
|
95
118
|
return {
|
|
@@ -104,8 +127,11 @@ exports.extractGlobalConstants = (0, createRule_1.createRule)({
|
|
|
104
127
|
const scope = context.getScope();
|
|
105
128
|
const hasDependencies = node.declarations.some((declaration) => declaration.init &&
|
|
106
129
|
ASTHelpers_1.ASTHelpers.declarationIncludesIdentifier(declaration.init));
|
|
130
|
+
// Skip constants with 'as const' type assertions used in loops
|
|
131
|
+
const hasAsConstAssertion = node.declarations.some((declaration) => declaration.init && isAsConstExpression(declaration.init));
|
|
107
132
|
// Only check function/block scoped constants without dependencies
|
|
108
133
|
if (!hasDependencies &&
|
|
134
|
+
!hasAsConstAssertion &&
|
|
109
135
|
(scope.type === 'function' || scope.type === 'block') &&
|
|
110
136
|
isInsideFunction(node)) {
|
|
111
137
|
const constName = node.declarations[0].id
|
|
@@ -138,18 +164,100 @@ exports.extractGlobalConstants = (0, createRule_1.createRule)({
|
|
|
138
164
|
}
|
|
139
165
|
}
|
|
140
166
|
},
|
|
167
|
+
ForStatement(node) {
|
|
168
|
+
// Check initialization
|
|
169
|
+
if (node.init && node.init.type === 'VariableDeclaration') {
|
|
170
|
+
for (const decl of node.init.declarations) {
|
|
171
|
+
if (decl.init && isNumericLiteral(decl.init) && !isZeroOrOne(decl.init) && !isAsConstExpression(decl.init)) {
|
|
172
|
+
context.report({
|
|
173
|
+
node: decl.init,
|
|
174
|
+
messageId: 'requireAsConst',
|
|
175
|
+
data: {
|
|
176
|
+
value: decl.init.value,
|
|
177
|
+
},
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
// Check test condition
|
|
183
|
+
if (node.test && node.test.type === 'BinaryExpression') {
|
|
184
|
+
if (isNumericLiteral(node.test.right) && !isZeroOrOne(node.test.right) && !isAsConstExpression(node.test.right)) {
|
|
185
|
+
context.report({
|
|
186
|
+
node: node.test.right,
|
|
187
|
+
messageId: 'requireAsConst',
|
|
188
|
+
data: {
|
|
189
|
+
value: node.test.right.value,
|
|
190
|
+
},
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
// Check update expression
|
|
195
|
+
if (node.update) {
|
|
196
|
+
if (node.update.type === 'AssignmentExpression') {
|
|
197
|
+
if (node.update.right && isNumericLiteral(node.update.right) && !isZeroOrOne(node.update.right) && !isAsConstExpression(node.update.right)) {
|
|
198
|
+
context.report({
|
|
199
|
+
node: node.update.right,
|
|
200
|
+
messageId: 'requireAsConst',
|
|
201
|
+
data: {
|
|
202
|
+
value: node.update.right.value,
|
|
203
|
+
},
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
else if (node.update.type === 'BinaryExpression') {
|
|
208
|
+
if (isNumericLiteral(node.update.right) && !isZeroOrOne(node.update.right) && !isAsConstExpression(node.update.right)) {
|
|
209
|
+
context.report({
|
|
210
|
+
node: node.update.right,
|
|
211
|
+
messageId: 'requireAsConst',
|
|
212
|
+
data: {
|
|
213
|
+
value: node.update.right.value,
|
|
214
|
+
},
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
},
|
|
220
|
+
WhileStatement(node) {
|
|
221
|
+
// Check test condition
|
|
222
|
+
if (node.test.type === 'BinaryExpression') {
|
|
223
|
+
if (isNumericLiteral(node.test.right) && !isZeroOrOne(node.test.right) && !isAsConstExpression(node.test.right)) {
|
|
224
|
+
context.report({
|
|
225
|
+
node: node.test.right,
|
|
226
|
+
messageId: 'requireAsConst',
|
|
227
|
+
data: {
|
|
228
|
+
value: node.test.right.value,
|
|
229
|
+
},
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
},
|
|
234
|
+
DoWhileStatement(node) {
|
|
235
|
+
// Check test condition
|
|
236
|
+
if (node.test.type === 'BinaryExpression') {
|
|
237
|
+
if (isNumericLiteral(node.test.right) && !isZeroOrOne(node.test.right) && !isAsConstExpression(node.test.right)) {
|
|
238
|
+
context.report({
|
|
239
|
+
node: node.test.right,
|
|
240
|
+
messageId: 'requireAsConst',
|
|
241
|
+
data: {
|
|
242
|
+
value: node.test.right.value,
|
|
243
|
+
},
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
},
|
|
141
248
|
};
|
|
142
249
|
},
|
|
143
250
|
name: 'extract-global-constants',
|
|
144
251
|
meta: {
|
|
145
252
|
type: 'suggestion',
|
|
146
253
|
docs: {
|
|
147
|
-
description: 'Extract static constants and functions to the global scope when possible',
|
|
254
|
+
description: 'Extract static constants and functions to the global scope when possible, and enforce type narrowing with as const for numeric literals in loops',
|
|
148
255
|
recommended: 'error',
|
|
149
256
|
},
|
|
150
257
|
schema: [],
|
|
151
258
|
messages: {
|
|
152
259
|
extractGlobalConstants: 'Move this declaration {{ declarationName }} to the global scope and rename it to UPPER_SNAKE_CASE if necessary.',
|
|
260
|
+
requireAsConst: 'Numeric literal {{ value }} in loop expression should be extracted to a constant with "as const" type assertion.',
|
|
153
261
|
},
|
|
154
262
|
},
|
|
155
263
|
defaultOptions: [],
|
|
@@ -29,8 +29,9 @@ exports.default = (0, createRule_1.createRule)({
|
|
|
29
29
|
if (node.kind !== 'const') {
|
|
30
30
|
return;
|
|
31
31
|
}
|
|
32
|
-
// Skip if not at program level
|
|
33
|
-
if (node.parent?.type !== utils_1.AST_NODE_TYPES.Program
|
|
32
|
+
// Skip if not at program level or not an exported declaration
|
|
33
|
+
if (node.parent?.type !== utils_1.AST_NODE_TYPES.Program &&
|
|
34
|
+
node.parent?.type !== utils_1.AST_NODE_TYPES.ExportNamedDeclaration) {
|
|
34
35
|
return;
|
|
35
36
|
}
|
|
36
37
|
// Skip if any declaration is a function component, arrow function, forwardRef, or memo
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
type MessageIds = 'alwaysTrueCondition' | 'alwaysFalseCondition';
|
|
2
|
+
export declare const noAlwaysTrueFalseConditions: import("@typescript-eslint/utils/dist/ts-eslint/Rule").RuleModule<MessageIds, [], import("@typescript-eslint/utils/dist/ts-eslint/Rule").RuleListener>;
|
|
3
|
+
export {};
|