@blumintinc/eslint-plugin-blumint 1.7.3 → 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.
Files changed (38) hide show
  1. package/README.md +39 -62
  2. package/lib/index.js +37 -1
  3. package/lib/rules/enforce-assert-throws.js +48 -3
  4. package/lib/rules/enforce-assertSafe-object-key.d.ts +2 -0
  5. package/lib/rules/enforce-assertSafe-object-key.js +284 -0
  6. package/lib/rules/enforce-centralized-mock-firestore.js +192 -14
  7. package/lib/rules/enforce-firestore-facade.js +8 -0
  8. package/lib/rules/enforce-microdiff.d.ts +3 -0
  9. package/lib/rules/enforce-microdiff.js +379 -0
  10. package/lib/rules/enforce-object-literal-as-const.d.ts +4 -0
  11. package/lib/rules/enforce-object-literal-as-const.js +88 -0
  12. package/lib/rules/enforce-positive-naming.d.ts +1 -0
  13. package/lib/rules/enforce-positive-naming.js +363 -0
  14. package/lib/rules/enforce-props-argument-name.d.ts +8 -0
  15. package/lib/rules/enforce-props-argument-name.js +182 -0
  16. package/lib/rules/enforce-render-hits-memoization.js +155 -26
  17. package/lib/rules/enforce-timestamp-now.d.ts +1 -0
  18. package/lib/rules/enforce-timestamp-now.js +223 -0
  19. package/lib/rules/extract-global-constants.d.ts +1 -1
  20. package/lib/rules/extract-global-constants.js +109 -1
  21. package/lib/rules/global-const-style.js +3 -2
  22. package/lib/rules/no-always-true-false-conditions.d.ts +3 -0
  23. package/lib/rules/no-always-true-false-conditions.js +1202 -0
  24. package/lib/rules/no-firestore-jest-mock.js +27 -4
  25. package/lib/rules/no-mock-firebase-admin.js +20 -7
  26. package/lib/rules/no-type-assertion-returns.d.ts +9 -0
  27. package/lib/rules/no-type-assertion-returns.js +289 -0
  28. package/lib/rules/no-unnecessary-verb-suffix.d.ts +1 -0
  29. package/lib/rules/no-unnecessary-verb-suffix.js +203 -0
  30. package/lib/rules/prefer-clone-deep.js +294 -22
  31. package/lib/rules/prefer-fragment-component.js +265 -34
  32. package/lib/rules/prefer-global-router-state-key.d.ts +5 -0
  33. package/lib/rules/prefer-global-router-state-key.js +89 -0
  34. package/lib/rules/prefer-utility-function-over-private-static.d.ts +1 -0
  35. package/lib/rules/prefer-utility-function-over-private-static.js +77 -0
  36. package/lib/rules/react-usememo-should-be-component.d.ts +1 -0
  37. package/lib/rules/react-usememo-should-be-component.js +256 -0
  38. package/package.json +5 -5
@@ -21,63 +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
- const callee = node.callee;
28
- if (callee.type !== utils_1.AST_NODE_TYPES.Identifier)
29
- return false;
30
- return callee.name === 'useCallback' || callee.name === 'useMemo';
31
- };
32
- const isReactComponent = (node) => {
33
- if (node.type !== utils_1.AST_NODE_TYPES.Identifier)
32
+ if (!node.callee || node.callee.type !== utils_1.AST_NODE_TYPES.Identifier)
34
33
  return false;
35
- const name = node.name;
36
- return /^[A-Z]/.test(name);
34
+ return (node.callee.name === 'useCallback' || node.callee.name === 'useMemo');
37
35
  };
38
36
  const isMemoizedVariable = (node) => {
39
37
  if (node.type !== utils_1.AST_NODE_TYPES.Identifier)
40
38
  return false;
41
39
  // Get the variable declaration for this identifier
42
- const variable = context.getScope().variables.find(v => v.name === node.name);
40
+ const variable = context
41
+ .getScope()
42
+ .variables.find((v) => v.name === node.name);
43
43
  if (!variable)
44
44
  return false;
45
45
  // Check if the variable is initialized with a memoized call
46
- const def = variable.defs[0];
47
- if (!def || !def.node)
48
- return false;
49
- if (def.node.type === utils_1.AST_NODE_TYPES.VariableDeclarator && def.node.init) {
50
- return isMemoizedCall(def.node.init);
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
+ }
51
55
  }
52
56
  return false;
53
57
  };
54
58
  const isInsideMemoizedCall = (node) => {
59
+ // Handle the case when node is already a memoized call
60
+ if (isMemoizedCall(node))
61
+ return true;
55
62
  // Check if the node is a reference to a memoized variable
56
63
  if (isMemoizedVariable(node))
57
64
  return true;
58
65
  // Check if the node is inside a memoization hook call
59
66
  let current = node;
60
67
  while (current?.parent) {
61
- if (isMemoizedCall(current.parent))
62
- return true;
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
+ }
63
75
  current = current.parent;
64
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
+ }
65
130
  return false;
66
131
  };
132
+ let useRenderHitsName = 'useRenderHits';
133
+ let renderHitsName = 'renderHits';
67
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
+ },
68
155
  CallExpression(node) {
69
- if (node.callee.type === utils_1.AST_NODE_TYPES.Identifier && node.callee.name === 'useRenderHits') {
156
+ if (node.callee.type === utils_1.AST_NODE_TYPES.Identifier &&
157
+ node.callee.name === useRenderHitsName) {
70
158
  if (node.arguments.length === 0)
71
159
  return;
72
160
  const options = node.arguments[0];
73
161
  if (options.type !== utils_1.AST_NODE_TYPES.ObjectExpression)
74
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
75
169
  for (const prop of options.properties) {
76
170
  if (prop.type !== utils_1.AST_NODE_TYPES.Property)
77
171
  continue;
78
172
  if (prop.key.type !== utils_1.AST_NODE_TYPES.Identifier)
79
173
  continue;
80
- if (prop.key.name === 'transformBefore') {
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;
81
200
  if (!isInsideMemoizedCall(prop.value)) {
82
201
  context.report({
83
202
  node: prop.value,
@@ -85,7 +204,7 @@ exports.enforceRenderHitsMemoization = (0, createRule_1.createRule)({
85
204
  });
86
205
  }
87
206
  }
88
- if (prop.key.name === 'render') {
207
+ else if (prop.key.name === 'render' && checkProps.render) {
89
208
  if (isReactComponent(prop.value)) {
90
209
  context.report({
91
210
  node: prop.value,
@@ -101,13 +220,23 @@ exports.enforceRenderHitsMemoization = (0, createRule_1.createRule)({
101
220
  }
102
221
  }
103
222
  }
104
- if (node.callee.type === utils_1.AST_NODE_TYPES.Identifier && node.callee.name === 'renderHits') {
105
- if (!isInsideMemoizedCall(node)) {
106
- context.report({
107
- node,
108
- messageId: 'requireMemoizedRenderHits',
109
- });
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;
110
235
  }
236
+ context.report({
237
+ node,
238
+ messageId: 'requireMemoizedRenderHits',
239
+ });
111
240
  }
112
241
  },
113
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
@@ -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 {};