@blumintinc/eslint-plugin-blumint 1.12.6 → 1.13.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 (141) hide show
  1. package/README.md +140 -99
  2. package/lib/index.js +111 -17
  3. package/lib/rules/array-methods-this-context.js +20 -2
  4. package/lib/rules/avoid-utils-directory.js +11 -2
  5. package/lib/rules/dynamic-https-errors.d.ts +3 -1
  6. package/lib/rules/dynamic-https-errors.js +48 -10
  7. package/lib/rules/enforce-assert-safe-object-key.d.ts +8 -0
  8. package/lib/rules/{enforce-assertSafe-object-key.js → enforce-assert-safe-object-key.js} +51 -95
  9. package/lib/rules/enforce-assert-throws.d.ts +3 -1
  10. package/lib/rules/enforce-assert-throws.js +493 -51
  11. package/lib/rules/enforce-boolean-naming-prefixes.d.ts +3 -1
  12. package/lib/rules/enforce-boolean-naming-prefixes.js +832 -39
  13. package/lib/rules/enforce-callable-types.js +4 -4
  14. package/lib/rules/enforce-callback-memo.js +143 -18
  15. package/lib/rules/enforce-centralized-mock-firestore.js +4 -1
  16. package/lib/rules/enforce-console-error.d.ts +3 -0
  17. package/lib/rules/enforce-console-error.js +250 -0
  18. package/lib/rules/enforce-css-media-queries.js +24 -46
  19. package/lib/rules/enforce-dynamic-firebase-imports.js +100 -19
  20. package/lib/rules/enforce-dynamic-imports.js +1 -1
  21. package/lib/rules/enforce-exported-function-types.js +3 -3
  22. package/lib/rules/enforce-fieldpath-syntax-in-docsetter.d.ts +2 -0
  23. package/lib/rules/enforce-fieldpath-syntax-in-docsetter.js +212 -0
  24. package/lib/rules/enforce-firestore-doc-ref-generic.js +335 -0
  25. package/lib/rules/enforce-firestore-facade.js +347 -157
  26. package/lib/rules/enforce-firestore-rules-get-access.d.ts +3 -0
  27. package/lib/rules/enforce-firestore-rules-get-access.js +109 -0
  28. package/lib/rules/enforce-global-constants.d.ts +4 -1
  29. package/lib/rules/enforce-global-constants.js +240 -12
  30. package/lib/rules/enforce-memoize-async.d.ts +2 -1
  31. package/lib/rules/enforce-memoize-async.js +96 -27
  32. package/lib/rules/enforce-memoize-getters.d.ts +3 -0
  33. package/lib/rules/enforce-memoize-getters.js +123 -0
  34. package/lib/rules/enforce-microdiff.js +2 -1
  35. package/lib/rules/enforce-positive-naming.js +5 -7
  36. package/lib/rules/enforce-props-argument-name.d.ts +2 -7
  37. package/lib/rules/enforce-props-argument-name.js +165 -151
  38. package/lib/rules/enforce-props-naming-consistency.d.ts +2 -0
  39. package/lib/rules/enforce-props-naming-consistency.js +145 -0
  40. package/lib/rules/enforce-querykey-ts.d.ts +9 -0
  41. package/lib/rules/enforce-querykey-ts.js +274 -0
  42. package/lib/rules/enforce-singular-type-names.js +9 -5
  43. package/lib/rules/enforce-typescript-markdown-code-blocks.d.ts +1 -0
  44. package/lib/rules/enforce-typescript-markdown-code-blocks.js +96 -0
  45. package/lib/rules/enforce-verb-noun-naming.js +85 -10
  46. package/lib/rules/extract-global-constants.js +57 -45
  47. package/lib/rules/fast-deep-equal-over-microdiff.d.ts +2 -1
  48. package/lib/rules/fast-deep-equal-over-microdiff.js +356 -78
  49. package/lib/rules/firestore-transaction-reads-before-writes.d.ts +1 -0
  50. package/lib/rules/firestore-transaction-reads-before-writes.js +231 -0
  51. package/lib/rules/generic-starts-with-t.js +8 -2
  52. package/lib/rules/memoize-root-level-hocs.d.ts +7 -0
  53. package/lib/rules/memoize-root-level-hocs.js +242 -0
  54. package/lib/rules/no-array-length-in-deps.d.ts +12 -0
  55. package/lib/rules/no-array-length-in-deps.js +308 -0
  56. package/lib/rules/no-async-array-filter.js +5 -2
  57. package/lib/rules/no-complex-cloud-params.js +15 -2
  58. package/lib/rules/no-compositing-layer-props.js +4 -2
  59. package/lib/rules/no-entire-object-hook-deps.d.ts +3 -1
  60. package/lib/rules/no-entire-object-hook-deps.js +317 -63
  61. package/lib/rules/no-excessive-parent-chain.d.ts +5 -0
  62. package/lib/rules/no-excessive-parent-chain.js +273 -0
  63. package/lib/rules/no-firestore-object-arrays.js +345 -30
  64. package/lib/rules/no-hungarian.js +13 -13
  65. package/lib/rules/no-margin-properties.d.ts +7 -1
  66. package/lib/rules/no-margin-properties.js +16 -3
  67. package/lib/rules/no-misleading-boolean-prefixes.d.ts +7 -0
  68. package/lib/rules/no-misleading-boolean-prefixes.js +327 -0
  69. package/lib/rules/no-overridable-method-calls-in-constructor.d.ts +1 -0
  70. package/lib/rules/no-overridable-method-calls-in-constructor.js +260 -0
  71. package/lib/rules/no-passthrough-getters.d.ts +4 -0
  72. package/lib/rules/no-passthrough-getters.js +170 -0
  73. package/lib/rules/no-redundant-usecallback-wrapper.d.ts +8 -0
  74. package/lib/rules/no-redundant-usecallback-wrapper.js +315 -0
  75. package/lib/rules/no-restricted-properties-fix.d.ts +12 -0
  76. package/lib/rules/no-restricted-properties-fix.js +172 -0
  77. package/lib/rules/no-separate-loading-state.d.ts +8 -0
  78. package/lib/rules/no-separate-loading-state.js +142 -0
  79. package/lib/rules/no-stale-state-across-await.d.ts +1 -0
  80. package/lib/rules/no-stale-state-across-await.js +185 -0
  81. package/lib/rules/no-static-constants-in-dynamic-files.d.ts +3 -0
  82. package/lib/rules/no-static-constants-in-dynamic-files.js +149 -0
  83. package/lib/rules/no-try-catch-already-exists-in-transaction.d.ts +1 -0
  84. package/lib/rules/no-try-catch-already-exists-in-transaction.js +342 -0
  85. package/lib/rules/no-type-assertion-returns.js +6 -0
  86. package/lib/rules/no-undefined-null-passthrough.js +166 -19
  87. package/lib/rules/no-unnecessary-verb-suffix.js +0 -2
  88. package/lib/rules/no-unsafe-firestore-spread.js +8 -5
  89. package/lib/rules/no-unused-props.d.ts +3 -0
  90. package/lib/rules/no-unused-props.js +213 -78
  91. package/lib/rules/optimize-object-boolean-conditions.d.ts +1 -0
  92. package/lib/rules/optimize-object-boolean-conditions.js +234 -0
  93. package/lib/rules/parallelize-async-operations.d.ts +8 -0
  94. package/lib/rules/parallelize-async-operations.js +375 -0
  95. package/lib/rules/prefer-batch-operations.js +199 -33
  96. package/lib/rules/prefer-block-comments-for-declarations.js +13 -3
  97. package/lib/rules/prefer-document-flattening.d.ts +4 -0
  98. package/lib/rules/prefer-document-flattening.js +239 -0
  99. package/lib/rules/prefer-field-paths-in-transforms.d.ts +7 -0
  100. package/lib/rules/prefer-field-paths-in-transforms.js +250 -0
  101. package/lib/rules/prefer-fragment-component.js +3 -3
  102. package/lib/rules/prefer-global-router-state-key.d.ts +7 -3
  103. package/lib/rules/prefer-global-router-state-key.js +230 -31
  104. package/lib/rules/prefer-next-dynamic.d.ts +9 -0
  105. package/lib/rules/prefer-next-dynamic.js +366 -0
  106. package/lib/rules/prefer-nullish-coalescing-boolean-props.d.ts +1 -0
  107. package/lib/rules/prefer-nullish-coalescing-boolean-props.js +430 -0
  108. package/lib/rules/prefer-nullish-coalescing-override.d.ts +6 -0
  109. package/lib/rules/prefer-nullish-coalescing-override.js +182 -0
  110. package/lib/rules/prefer-params-over-parent-id.d.ts +5 -0
  111. package/lib/rules/prefer-params-over-parent-id.js +228 -0
  112. package/lib/rules/prefer-settings-object.js +23 -15
  113. package/lib/rules/prefer-type-alias-over-typeof-constant.d.ts +3 -0
  114. package/lib/rules/prefer-type-alias-over-typeof-constant.js +259 -0
  115. package/lib/rules/prefer-url-tostring-over-tojson.d.ts +2 -0
  116. package/lib/rules/prefer-url-tostring-over-tojson.js +166 -0
  117. package/lib/rules/prefer-use-deep-compare-memo.d.ts +4 -0
  118. package/lib/rules/prefer-use-deep-compare-memo.js +428 -0
  119. package/lib/rules/prefer-usecallback-over-usememo-for-functions.js +24 -1
  120. package/lib/rules/prefer-usememo-over-useeffect-usestate.js +24 -5
  121. package/lib/rules/react-usememo-should-be-component.d.ts +2 -1
  122. package/lib/rules/react-usememo-should-be-component.js +144 -78
  123. package/lib/rules/require-dynamic-firebase-imports.js +2 -1
  124. package/lib/rules/require-image-optimized.js +1 -1
  125. package/lib/rules/require-memo.js +11 -1
  126. package/lib/rules/require-usememo-object-literals.js +7 -1
  127. package/lib/rules/semantic-function-prefixes.js +38 -18
  128. package/lib/rules/sync-onwrite-name-func.js +5 -1
  129. package/lib/rules/test-file-location-enforcement.d.ts +1 -0
  130. package/lib/rules/test-file-location-enforcement.js +57 -0
  131. package/lib/rules/use-custom-link.js +6 -1
  132. package/lib/rules/use-custom-router.js +6 -1
  133. package/lib/rules/use-latest-callback.d.ts +3 -0
  134. package/lib/rules/use-latest-callback.js +270 -0
  135. package/lib/utils/ASTHelpers.js +10 -0
  136. package/lib/utils/createRule.js +1 -1
  137. package/lib/utils/graph/ClassGraphSorterReadability.js +1 -1
  138. package/lib/utils/ruleTester.d.ts +1 -0
  139. package/lib/utils/ruleTester.js +4 -1
  140. package/package.json +21 -14
  141. package/lib/rules/enforce-assertSafe-object-key.d.ts +0 -2
@@ -7,11 +7,11 @@ exports.preferUseMemoOverUseEffectUseState = (0, createRule_1.createRule)({
7
7
  meta: {
8
8
  type: 'suggestion',
9
9
  docs: {
10
- description: 'Prefer useMemo over useEffect + useState for pure computations. Using useEffect to update state with a pure computation causes unnecessary re-renders.',
10
+ description: 'Prefer useMemo over useEffect + useState for pure computations to avoid extra render cycles and stale derived state.',
11
11
  recommended: 'error',
12
12
  },
13
13
  messages: {
14
- preferUseMemo: 'Prefer useMemo over useEffect + useState for pure computations to avoid unnecessary re-renders',
14
+ preferUseMemo: 'Derived state "{{stateName}}" is computed inside useEffect and copied into React state even though the value comes from a pure calculation. That extra render cycle and state indirection make components re-render more and risk stale snapshots when dependencies change. Compute the value with useMemo (or inline in render) and read it directly instead of mirroring it into state.',
15
15
  },
16
16
  schema: [],
17
17
  },
@@ -84,7 +84,8 @@ exports.preferUseMemoOverUseEffectUseState = (0, createRule_1.createRule)({
84
84
  if (initialValue &&
85
85
  isIdentifierReference(initialValue) &&
86
86
  isIdentifierReference(setterArgument) &&
87
- initialValue.name === setterArgument.name) {
87
+ initialValue.name ===
88
+ setterArgument.name) {
88
89
  return true;
89
90
  }
90
91
  // If the initial value is a function that references a prop and the setter argument
@@ -111,7 +112,9 @@ exports.preferUseMemoOverUseEffectUseState = (0, createRule_1.createRule)({
111
112
  const stateName = node.id.elements[0].name;
112
113
  const setterName = node.id.elements[1].name;
113
114
  const initialValue = node.init.arguments[0] || null;
114
- stateSetters.set(setterName, { stateName, initialValue });
115
+ // keep a reference to the defining identifier node
116
+ const defId = node.id.elements[1];
117
+ stateSetters.set(setterName, { stateName, initialValue, defId });
115
118
  }
116
119
  }
117
120
  },
@@ -133,7 +136,20 @@ exports.preferUseMemoOverUseEffectUseState = (0, createRule_1.createRule)({
133
136
  statement.expression.callee.type === 'Identifier') {
134
137
  const setterName = statement.expression.callee.name;
135
138
  const stateInfo = stateSetters.get(setterName);
136
- if (stateInfo && statement.expression.arguments.length === 1) {
139
+ // Verify the setter identifier resolves to the tracked binding
140
+ const calleeId = statement.expression
141
+ .callee;
142
+ const scope = context.getScope();
143
+ const variable = scope.set.get(calleeId.name) ||
144
+ scope.upper?.set.get(calleeId.name);
145
+ const defNode = variable?.defs?.[0]?.name;
146
+ if (!stateInfo ||
147
+ !defNode ||
148
+ defNode.type !== 'Identifier' ||
149
+ defNode.name !== stateInfo.defId.name) {
150
+ return;
151
+ }
152
+ if (statement.expression.arguments.length === 1) {
137
153
  const computation = statement.expression.arguments[0];
138
154
  // Skip if this is a state synchronization pattern
139
155
  if (isStateSynchronization(stateInfo.initialValue, computation)) {
@@ -145,6 +161,9 @@ exports.preferUseMemoOverUseEffectUseState = (0, createRule_1.createRule)({
145
161
  context.report({
146
162
  node,
147
163
  messageId: 'preferUseMemo',
164
+ data: {
165
+ stateName: stateInfo.stateName,
166
+ },
148
167
  });
149
168
  }
150
169
  }
@@ -1 +1,2 @@
1
- export declare const reactUseMemoShouldBeComponent: import("@typescript-eslint/utils/dist/ts-eslint/Rule").RuleModule<"useMemoShouldBeComponent", [], import("@typescript-eslint/utils/dist/ts-eslint/Rule").RuleListener>;
1
+ import { TSESLint } from '@typescript-eslint/utils';
2
+ export declare const reactUseMemoShouldBeComponent: TSESLint.RuleModule<"useMemoShouldBeComponent", [], TSESLint.RuleListener>;
@@ -3,6 +3,13 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.reactUseMemoShouldBeComponent = void 0;
4
4
  const utils_1 = require("@typescript-eslint/utils");
5
5
  const createRule_1 = require("../utils/createRule");
6
+ const ARRAY_RETURNING_METHODS = new Set([
7
+ 'map',
8
+ 'flatMap',
9
+ 'filter',
10
+ 'concat',
11
+ 'slice',
12
+ ]);
6
13
  /**
7
14
  * Checks if a node is a JSX element or fragment
8
15
  */
@@ -24,6 +31,99 @@ const isJsxElement = (node) => {
24
31
  return (node.type === utils_1.AST_NODE_TYPES.JSXElement ||
25
32
  node.type === utils_1.AST_NODE_TYPES.JSXFragment);
26
33
  };
34
+ /**
35
+ * True if the resolved variable is referenced inside a JSX prop (attribute or spread).
36
+ */
37
+ const isUsedAsComponentProp = (context, variableName, node) => {
38
+ // Find containing function for proper scope acquisition
39
+ let current = node;
40
+ let fn;
41
+ while (current?.parent) {
42
+ current = current.parent;
43
+ if (current.type === utils_1.AST_NODE_TYPES.ArrowFunctionExpression ||
44
+ current.type === utils_1.AST_NODE_TYPES.FunctionDeclaration ||
45
+ current.type === utils_1.AST_NODE_TYPES.FunctionExpression) {
46
+ fn = current;
47
+ break;
48
+ }
49
+ }
50
+ const sourceCode = context.getSourceCode();
51
+ const scopeManager = sourceCode.scopeManager;
52
+ const scope = scopeManager &&
53
+ ((fn && scopeManager.acquire(fn)) || scopeManager.acquire(node));
54
+ if (!scopeManager || !scope) {
55
+ return true;
56
+ }
57
+ const variable = scope.variables.find((v) => v.name === variableName) ??
58
+ scope.upper?.variables.find((v) => v.name === variableName) ??
59
+ null;
60
+ if (!variable) {
61
+ return true;
62
+ }
63
+ for (const ref of variable.references) {
64
+ let p = ref.identifier.parent ?? null;
65
+ while (p) {
66
+ if (p.type === utils_1.AST_NODE_TYPES.JSXAttribute ||
67
+ p.type === utils_1.AST_NODE_TYPES.JSXSpreadAttribute) {
68
+ return true;
69
+ }
70
+ p = p.parent;
71
+ }
72
+ }
73
+ return false;
74
+ };
75
+ const isUsedAsJsxChild = (context, variableName, node) => {
76
+ // Traverse the nearest function (or program) to see if the identifier appears
77
+ // as a JSX child expression.
78
+ const sourceCode = context.getSourceCode();
79
+ let container = sourceCode.ast;
80
+ let current = node;
81
+ while (current?.parent) {
82
+ current = current.parent;
83
+ if (current.type === utils_1.AST_NODE_TYPES.FunctionDeclaration ||
84
+ current.type === utils_1.AST_NODE_TYPES.FunctionExpression ||
85
+ current.type === utils_1.AST_NODE_TYPES.ArrowFunctionExpression) {
86
+ container = current;
87
+ break;
88
+ }
89
+ }
90
+ let found = false;
91
+ const visited = new Set();
92
+ const visit = (n) => {
93
+ if (found || visited.has(n))
94
+ return;
95
+ visited.add(n);
96
+ if (n.type === utils_1.AST_NODE_TYPES.JSXExpressionContainer &&
97
+ n.expression.type === utils_1.AST_NODE_TYPES.Identifier &&
98
+ n.expression.name === variableName &&
99
+ n.parent &&
100
+ (n.parent.type === utils_1.AST_NODE_TYPES.JSXElement ||
101
+ n.parent.type === utils_1.AST_NODE_TYPES.JSXFragment) &&
102
+ n.parent.children.includes(n)) {
103
+ found = true;
104
+ return;
105
+ }
106
+ for (const key of Object.keys(n)) {
107
+ if (key === 'parent')
108
+ continue;
109
+ const value = n[key];
110
+ if (!value)
111
+ continue;
112
+ if (Array.isArray(value)) {
113
+ for (const child of value) {
114
+ if (child && typeof child.type === 'string') {
115
+ visit(child);
116
+ }
117
+ }
118
+ }
119
+ else if (value && typeof value.type === 'string') {
120
+ visit(value);
121
+ }
122
+ }
123
+ };
124
+ visit(container);
125
+ return found;
126
+ };
27
127
  /**
28
128
  * Checks if a variable is used multiple times in a component
29
129
  */
@@ -149,28 +249,13 @@ const containsJsxInFunction = (node) => {
149
249
  if (body.type === utils_1.AST_NODE_TYPES.BlockStatement) {
150
250
  return containsJsxInBlockStatement(body);
151
251
  }
152
- // Check for array methods returning JSX
252
+ // Check for array methods returning arrays (do not flag arrays of JSX)
153
253
  if (body.type === utils_1.AST_NODE_TYPES.CallExpression) {
154
254
  if (body.callee.type === utils_1.AST_NODE_TYPES.MemberExpression &&
155
255
  body.callee.property.type === utils_1.AST_NODE_TYPES.Identifier) {
156
- // Check array methods like map, filter, find, etc.
157
- const arrayMethods = [
158
- 'map',
159
- 'filter',
160
- 'find',
161
- 'findIndex',
162
- 'some',
163
- 'every',
164
- 'reduce',
165
- ];
166
- if (arrayMethods.includes(body.callee.property.name) &&
167
- body.arguments.length > 0) {
168
- const callback = body.arguments[0];
169
- if ((callback.type === utils_1.AST_NODE_TYPES.ArrowFunctionExpression ||
170
- callback.type === utils_1.AST_NODE_TYPES.FunctionExpression) &&
171
- containsJsxInFunction(callback)) {
172
- return true;
173
- }
256
+ if (ARRAY_RETURNING_METHODS.has(body.callee.property.name)) {
257
+ // Returning an array (even if its elements are JSX) should not be flagged
258
+ return false;
174
259
  }
175
260
  }
176
261
  // Check for IIFE
@@ -239,70 +324,27 @@ const containsJsxInExpression = (node) => {
239
324
  }
240
325
  return containsJsxInObject(node);
241
326
  case utils_1.AST_NODE_TYPES.CallExpression:
242
- // Special case for array methods that return data structures with JSX
327
+ // Do not flag arrays of JSX: common array-producing patterns
243
328
  if (node.callee.type === utils_1.AST_NODE_TYPES.MemberExpression &&
244
- node.callee.property.type === utils_1.AST_NODE_TYPES.Identifier &&
245
- node.callee.property.name === 'map' &&
246
- node.arguments.length > 0) {
247
- const callback = node.arguments[0];
248
- if (callback.type === utils_1.AST_NODE_TYPES.ArrowFunctionExpression ||
249
- callback.type === utils_1.AST_NODE_TYPES.FunctionExpression) {
250
- // Check if the callback returns an object with both JSX and non-JSX properties
251
- if (callback.body.type !== utils_1.AST_NODE_TYPES.BlockStatement &&
252
- callback.body.type === utils_1.AST_NODE_TYPES.ObjectExpression) {
253
- let hasNonJsxProperties = false;
254
- let hasJsxProperties = false;
255
- for (const property of callback.body.properties) {
256
- if (property.type === utils_1.AST_NODE_TYPES.Property && property.value) {
257
- if (isJsxElement(property.value)) {
258
- hasJsxProperties = true;
259
- }
260
- else if (property.value.type !== utils_1.AST_NODE_TYPES.ObjectExpression) {
261
- hasNonJsxProperties = true;
262
- }
263
- }
264
- }
265
- // If the object has both JSX and non-JSX properties, it's likely a data object
266
- if (hasNonJsxProperties && hasJsxProperties) {
267
- return false;
268
- }
269
- }
270
- // For other cases, check if the function returns JSX directly
271
- return containsJsxInFunction(callback);
329
+ node.callee.property.type === utils_1.AST_NODE_TYPES.Identifier) {
330
+ if (ARRAY_RETURNING_METHODS.has(node.callee.property.name)) {
331
+ return false;
272
332
  }
273
333
  }
334
+ // Array.from(...) returns an array as well
335
+ if (node.callee.type === utils_1.AST_NODE_TYPES.MemberExpression &&
336
+ node.callee.object.type === utils_1.AST_NODE_TYPES.Identifier &&
337
+ node.callee.object.name === 'Array' &&
338
+ node.callee.property.type === utils_1.AST_NODE_TYPES.Identifier &&
339
+ node.callee.property.name === 'from') {
340
+ return false;
341
+ }
274
342
  // Check if it's an IIFE
275
343
  if (node.callee.type === utils_1.AST_NODE_TYPES.ArrowFunctionExpression ||
276
344
  node.callee.type === utils_1.AST_NODE_TYPES.FunctionExpression) {
277
345
  return containsJsxInFunction(node.callee);
278
346
  }
279
- // Check array methods
280
- if (node.callee.type === utils_1.AST_NODE_TYPES.MemberExpression &&
281
- node.callee.property.type === utils_1.AST_NODE_TYPES.Identifier) {
282
- const arrayMethods = [
283
- 'filter',
284
- 'find',
285
- 'findIndex',
286
- 'some',
287
- 'every',
288
- 'reduce',
289
- ];
290
- if (arrayMethods.includes(node.callee.property.name) &&
291
- node.arguments.length > 0) {
292
- const callback = node.arguments[0];
293
- if (callback.type === utils_1.AST_NODE_TYPES.ArrowFunctionExpression ||
294
- callback.type === utils_1.AST_NODE_TYPES.FunctionExpression) {
295
- return containsJsxInFunction(callback);
296
- }
297
- }
298
- }
299
- // Check arguments for JSX
300
- for (const arg of node.arguments) {
301
- if (arg.type !== utils_1.AST_NODE_TYPES.SpreadElement &&
302
- containsJsxInExpression(arg)) {
303
- return true;
304
- }
305
- }
347
+ // Do not flag based on JSX in arguments of non-IIFE calls
306
348
  return false;
307
349
  case utils_1.AST_NODE_TYPES.ArrowFunctionExpression:
308
350
  case utils_1.AST_NODE_TYPES.FunctionExpression:
@@ -371,8 +413,15 @@ const containsJsxInBlockStatement = (node) => {
371
413
  // Check variable declarations for JSX
372
414
  if (statement.type === utils_1.AST_NODE_TYPES.VariableDeclaration) {
373
415
  for (const declarator of statement.declarations) {
374
- if (declarator.init && containsJsxInExpression(declarator.init)) {
375
- return true;
416
+ if (declarator.init) {
417
+ // Ignore nested function expressions (helpers/callbacks) even if they return JSX
418
+ if (declarator.init.type === utils_1.AST_NODE_TYPES.ArrowFunctionExpression ||
419
+ declarator.init.type === utils_1.AST_NODE_TYPES.FunctionExpression) {
420
+ continue;
421
+ }
422
+ if (containsJsxInExpression(declarator.init)) {
423
+ return true;
424
+ }
376
425
  }
377
426
  }
378
427
  }
@@ -441,7 +490,7 @@ exports.reactUseMemoShouldBeComponent = (0, createRule_1.createRule)({
441
490
  },
442
491
  schema: [],
443
492
  messages: {
444
- useMemoShouldBeComponent: 'useMemo returning JSX should be extracted into a separate component. Use React.memo() for component memoization instead.',
493
+ useMemoShouldBeComponent: 'useMemo result "{{memoName}}" returns JSX, which hides a component inside a memoized value. JSX needs component identity so React can manage props, state, and dev tooling; wrapping it in useMemo bypasses that boundary and makes reuse/debugging harder. Extract this JSX into its own component and memoize it with React.memo if stability is required.',
445
494
  },
446
495
  },
447
496
  defaultOptions: [],
@@ -454,15 +503,32 @@ exports.reactUseMemoShouldBeComponent = (0, createRule_1.createRule)({
454
503
  node.parent.type === utils_1.AST_NODE_TYPES.VariableDeclarator &&
455
504
  node.parent.id.type === utils_1.AST_NODE_TYPES.Identifier) {
456
505
  const variableName = node.parent.id.name;
506
+ // Only report when the memoized value is used as a JSX child
507
+ if (!isUsedAsJsxChild(context, variableName, node)) {
508
+ return;
509
+ }
457
510
  // Check if the variable is used multiple times in the component
458
511
  if (isUsedMultipleTimes(variableName, node)) {
459
512
  // If the variable is used multiple times, allow it
460
513
  return;
461
514
  }
515
+ // Check if the variable is used as a prop value in a JSX element
516
+ if (isUsedAsComponentProp(context, variableName, node)) {
517
+ // If the variable is used as a prop value, allow it
518
+ return;
519
+ }
462
520
  }
521
+ const memoName = node.parent &&
522
+ node.parent.type === utils_1.AST_NODE_TYPES.VariableDeclarator &&
523
+ node.parent.id.type === utils_1.AST_NODE_TYPES.Identifier
524
+ ? node.parent.id.name
525
+ : 'useMemo return value';
463
526
  context.report({
464
527
  node,
465
528
  messageId: 'useMemoShouldBeComponent',
529
+ data: {
530
+ memoName,
531
+ },
466
532
  });
467
533
  }
468
534
  },
@@ -14,7 +14,7 @@ exports.default = (0, createRule_1.createRule)({
14
14
  fixable: 'code',
15
15
  schema: [],
16
16
  messages: {
17
- requireDynamicImport: 'Firebase dependencies must be imported dynamically to reduce bundle size',
17
+ requireDynamicImport: 'Static Firebase import from "{{importSource}}" keeps the Firebase SDK in the initial bundle and blocks route-level code splitting. Use an `await import(\'{{importSource}}\')` dynamic import so Firebase loads only on the code path that needs it; type-only imports remain allowed.',
18
18
  },
19
19
  },
20
20
  defaultOptions: [],
@@ -72,6 +72,7 @@ exports.default = (0, createRule_1.createRule)({
72
72
  context.report({
73
73
  node,
74
74
  messageId: 'requireDynamicImport',
75
+ data: { importSource },
75
76
  fix(fixer) {
76
77
  const dynamicImport = createDynamicImport(node);
77
78
  return fixer.replaceText(node, dynamicImport);
@@ -24,7 +24,7 @@ module.exports = (0, createRule_1.createRule)({
24
24
  },
25
25
  ],
26
26
  messages: {
27
- useImageOptimized: 'Use ImageOptimized component from {{ componentPath }} instead of {{ component }}',
27
+ useImageOptimized: 'Use ImageOptimized from {{ componentPath }} instead of {{ component }}. The shared wrapper handles responsive sizing, lazy loading, and blur placeholders so images stay optimized and do not hurt Core Web Vitals. Replace this usage with ImageOptimized to send the asset through the optimization pipeline.',
28
28
  },
29
29
  },
30
30
  defaultOptions: [{ componentPath: 'src/components/image/ImageOptimized' }],
@@ -66,9 +66,17 @@ function checkFunction(context, node) {
66
66
  isUnmemoizedExportedFunctionComponent,
67
67
  ].map((fn) => fn(parentNode, node));
68
68
  if (results.some((result) => !!result)) {
69
+ const componentName = (node.type === 'FunctionDeclaration' && node.id?.name) ||
70
+ (parentNode.type === 'VariableDeclarator' &&
71
+ parentNode.id.type === 'Identifier' &&
72
+ parentNode.id.name) ||
73
+ 'component';
69
74
  context.report({
70
75
  node,
71
76
  messageId: 'requireMemo',
77
+ data: {
78
+ name: componentName,
79
+ },
72
80
  fix: results[2] || results[1]
73
81
  ? function fix(fixer) {
74
82
  const sourceCode = context.getSourceCode();
@@ -158,7 +166,9 @@ exports.requireMemo = {
158
166
  recommended: 'error',
159
167
  },
160
168
  messages: {
161
- requireMemo: 'Component definition not wrapped in memo()',
169
+ requireMemo: 'Component "{{name}}" renders JSX with props but is not wrapped in memo(). ' +
170
+ 'Without memo the component function is recreated on every parent render, breaking referential equality and causing avoidable child re-renders. ' +
171
+ 'Wrap the component with memo from util/memo so callers receive a stable reference; rename to "{{name}}Unmemoized" if it must stay un-memoized.',
162
172
  },
163
173
  schema: [],
164
174
  fixable: 'code',
@@ -11,7 +11,7 @@ exports.requireUseMemoObjectLiterals = (0, createRule_1.createRule)({
11
11
  recommended: 'error',
12
12
  },
13
13
  messages: {
14
- requireUseMemo: 'Inline object/array literals in JSX props should be wrapped in useMemo to prevent unnecessary re-renders',
14
+ requireUseMemo: 'Inline {{literalType}} literal passed to {{componentName}} prop "{{propName}}" is recreated on every render, producing a new reference that forces child components to re-render even when values stay the same. Wrap the literal in useMemo (or hoist a memoized constant) so the prop reference remains stable between renders.',
15
15
  },
16
16
  schema: [],
17
17
  },
@@ -46,9 +46,15 @@ exports.requireUseMemoObjectLiterals = (0, createRule_1.createRule)({
46
46
  ? jsxElement.name.name
47
47
  : '';
48
48
  if (elementName && /^[A-Z]/.test(elementName)) {
49
+ const literalType = expression.type === 'ObjectExpression' ? 'object' : 'array';
49
50
  context.report({
50
51
  node: expression,
51
52
  messageId: 'requireUseMemo',
53
+ data: {
54
+ literalType,
55
+ propName: typeof propName === 'string' ? propName : 'prop',
56
+ componentName: elementName,
57
+ },
52
58
  });
53
59
  }
54
60
  }
@@ -24,17 +24,43 @@ const SUGGESTED_ALTERNATIVES = {
24
24
  process: ['transform', 'sanitize', 'compute'],
25
25
  do: ['execute', 'perform', 'apply'],
26
26
  };
27
+ function extractFirstWord(name) {
28
+ let firstWord = name;
29
+ for (let i = 1; i < name.length; i++) {
30
+ const currentChar = name[i];
31
+ const prevChar = name[i - 1];
32
+ // lowercase -> Uppercase (camel/Pascal boundary)
33
+ if (prevChar >= 'a' &&
34
+ prevChar <= 'z' &&
35
+ currentChar >= 'A' &&
36
+ currentChar <= 'Z') {
37
+ firstWord = name.substring(0, i);
38
+ break;
39
+ }
40
+ // UPPERCASE acronym -> lowercase word start (e.g., XML|Http)
41
+ if (prevChar >= 'A' &&
42
+ prevChar <= 'Z' &&
43
+ currentChar >= 'a' &&
44
+ currentChar <= 'z') {
45
+ if (i > 1) {
46
+ firstWord = name.substring(0, i - 1);
47
+ break;
48
+ }
49
+ }
50
+ }
51
+ return firstWord;
52
+ }
27
53
  exports.semanticFunctionPrefixes = (0, createRule_1.createRule)({
28
54
  name: 'semantic-function-prefixes',
29
55
  meta: {
30
56
  type: 'suggestion',
31
57
  docs: {
32
- description: 'Enforce semantic function prefixes over generic ones like "get" and "update"',
58
+ description: 'Require semantic function prefixes instead of generic verbs so callers know whether a function fetches data, transforms input, or mutates state',
33
59
  recommended: 'error',
34
60
  },
35
61
  schema: [],
36
62
  messages: {
37
- avoidGenericPrefix: 'Avoid using generic prefix "{{prefix}}". Consider using one of these alternatives: {{alternatives}}',
63
+ avoidGenericPrefix: 'Function "{{functionName}}" starts with the generic prefix "{{prefix}}", which hides whether it fetches remote data, transforms input, or mutates state. Use a semantic verb such as {{alternatives}} to describe the operation and set caller expectations.',
38
64
  },
39
65
  },
40
66
  defaultOptions: [],
@@ -54,20 +80,17 @@ exports.semanticFunctionPrefixes = (0, createRule_1.createRule)({
54
80
  if (NEXTJS_DATA_FUNCTIONS.has(methodName))
55
81
  return;
56
82
  // Extract first word from PascalCase/camelCase
57
- let firstWord = methodName;
58
- for (let i = 1; i < methodName.length; i++) {
59
- if (methodName[i] >= 'A' && methodName[i] <= 'Z') {
60
- firstWord = methodName.substring(0, i);
61
- break;
62
- }
63
- }
83
+ const firstWord = extractFirstWord(methodName);
64
84
  // Check for disallowed prefixes
85
+ // Only flag if the disallowed word is used as a prefix (not the entire name)
65
86
  for (const prefix of DISALLOWED_PREFIXES) {
66
- if (firstWord.toLowerCase() === prefix.toLowerCase()) {
87
+ if (firstWord.toLowerCase() === prefix.toLowerCase() &&
88
+ firstWord.length < methodName.length) {
67
89
  context.report({
68
90
  node: node.key,
69
91
  messageId: 'avoidGenericPrefix',
70
92
  data: {
93
+ functionName: methodName,
71
94
  prefix,
72
95
  alternatives: SUGGESTED_ALTERNATIVES[prefix].join(', '),
73
96
  },
@@ -99,20 +122,17 @@ exports.semanticFunctionPrefixes = (0, createRule_1.createRule)({
99
122
  if (NEXTJS_DATA_FUNCTIONS.has(functionName))
100
123
  return;
101
124
  // Extract first word from PascalCase/camelCase
102
- let firstWord = functionName;
103
- for (let i = 1; i < functionName.length; i++) {
104
- if (functionName[i] >= 'A' && functionName[i] <= 'Z') {
105
- firstWord = functionName.substring(0, i);
106
- break;
107
- }
108
- }
125
+ const firstWord = extractFirstWord(functionName);
109
126
  // Check for disallowed prefixes
127
+ // Only flag if the disallowed word is used as a prefix (not the entire name)
110
128
  for (const prefix of DISALLOWED_PREFIXES) {
111
- if (firstWord.toLowerCase() === prefix.toLowerCase()) {
129
+ if (firstWord.toLowerCase() === prefix.toLowerCase() &&
130
+ firstWord.length < functionName.length) {
112
131
  context.report({
113
132
  node: node.id || node,
114
133
  messageId: 'avoidGenericPrefix',
115
134
  data: {
135
+ functionName,
116
136
  prefix,
117
137
  alternatives: SUGGESTED_ALTERNATIVES[prefix].join(', '),
118
138
  },
@@ -14,7 +14,7 @@ exports.syncOnwriteNameFunc = (0, createRule_1.createRule)({
14
14
  fixable: 'code',
15
15
  schema: [],
16
16
  messages: {
17
- mismatchedName: 'The name field should match the func field in onWrite handlers',
17
+ mismatchedName: 'OnWrite handler name "{{nameValue}}" does not match func reference "{{funcName}}". The name field is what gets registered for deploys, logs, and alerts, so a mismatch hides which function is actually running. Rename the "name" value to "{{funcName}}" or point "func" to a function named "{{nameValue}}" so the trigger label and implementation stay in sync.',
18
18
  },
19
19
  },
20
20
  defaultOptions: [],
@@ -68,6 +68,10 @@ exports.syncOnwriteNameFunc = (0, createRule_1.createRule)({
68
68
  context.report({
69
69
  node: nameProperty,
70
70
  messageId: 'mismatchedName',
71
+ data: {
72
+ nameValue,
73
+ funcName,
74
+ },
71
75
  fix(fixer) {
72
76
  return fixer.replaceText(value, `'${funcName}'`);
73
77
  },
@@ -0,0 +1 @@
1
+ export declare const testFileLocationEnforcement: import("@typescript-eslint/utils/dist/ts-eslint/Rule").RuleModule<"misplacedTestFile", [], import("@typescript-eslint/utils/dist/ts-eslint/Rule").RuleListener>;
@@ -0,0 +1,57 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.testFileLocationEnforcement = void 0;
7
+ const fs_1 = __importDefault(require("fs"));
8
+ const path_1 = __importDefault(require("path"));
9
+ const createRule_1 = require("../utils/createRule");
10
+ const TEST_FILE_PATTERN = /\.test\.tsx?$/i;
11
+ const SUPPORTED_EXTENSIONS = ['.ts', '.tsx', '.js', '.jsx'];
12
+ exports.testFileLocationEnforcement = (0, createRule_1.createRule)({
13
+ name: 'test-file-location-enforcement',
14
+ meta: {
15
+ type: 'suggestion',
16
+ docs: {
17
+ description: 'Enforce colocating *.test.ts(x) files with the code they cover.',
18
+ recommended: 'error',
19
+ },
20
+ schema: [],
21
+ messages: {
22
+ misplacedTestFile: 'Test file "{{testFile}}" is not colocated with its subject. Keep tests in the same directory as {{expectedNames}} so refactors move code and coverage together and engineers can find the implementation without searching separate test folders.',
23
+ },
24
+ },
25
+ defaultOptions: [],
26
+ create(context) {
27
+ return {
28
+ Program(node) {
29
+ const filename = context.getFilename();
30
+ if (filename === '<input>' ||
31
+ filename === '<text>' ||
32
+ filename.includes('node_modules') ||
33
+ !TEST_FILE_PATTERN.test(filename)) {
34
+ return;
35
+ }
36
+ const directory = path_1.default.dirname(filename);
37
+ const testFileName = path_1.default.basename(filename);
38
+ const baseName = testFileName.replace(TEST_FILE_PATTERN, '');
39
+ const candidates = SUPPORTED_EXTENSIONS.map((extension) => path_1.default.join(directory, `${baseName}${extension}`));
40
+ const hasSibling = candidates.some((candidate) => fs_1.default.existsSync(candidate));
41
+ if (hasSibling) {
42
+ return;
43
+ }
44
+ const relativePath = path_1.default.isAbsolute(filename)
45
+ ? path_1.default.relative(process.cwd(), filename) || filename
46
+ : filename;
47
+ const expectedNames = SUPPORTED_EXTENSIONS.map((extension) => `"${baseName}${extension}"`).join(' or ');
48
+ context.report({
49
+ node,
50
+ messageId: 'misplacedTestFile',
51
+ data: { testFile: relativePath, expectedNames },
52
+ });
53
+ },
54
+ };
55
+ },
56
+ });
57
+ //# sourceMappingURL=test-file-location-enforcement.js.map
@@ -13,7 +13,7 @@ exports.useCustomLink = (0, createRule_1.createRule)({
13
13
  fixable: 'code',
14
14
  schema: [],
15
15
  messages: {
16
- useCustomLink: 'Import Link from src/components/Link instead of next/link',
16
+ useCustomLink: 'Import "{{localName}}" from src/components/Link instead of next/link. The custom Link wraps Next.js navigation with design system defaults and analytics hooks; importing next/link bypasses those wrappers and leads to inconsistent styling and missing instrumentation. Replace the import source with src/components/Link so routing uses the shared wrapper.',
17
17
  },
18
18
  },
19
19
  defaultOptions: [],
@@ -30,6 +30,11 @@ exports.useCustomLink = (0, createRule_1.createRule)({
30
30
  context.report({
31
31
  node,
32
32
  messageId: 'useCustomLink',
33
+ data: {
34
+ localName: defaultSpecifier?.local?.name ||
35
+ defaultAsSpecifier?.local?.name ||
36
+ 'Link',
37
+ },
33
38
  fix(fixer) {
34
39
  // Get the local name of the imported Link component
35
40
  const localName = defaultSpecifier?.local?.name ||