@blumintinc/eslint-plugin-blumint 1.16.2 → 1.17.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/README.md +21 -0
  2. package/lib/index.js +64 -1
  3. package/lib/rules/enforce-boolean-naming-prefixes.js +30 -14
  4. package/lib/rules/enforce-cloud-function-id-length.d.ts +5 -0
  5. package/lib/rules/enforce-cloud-function-id-length.js +104 -0
  6. package/lib/rules/enforce-is-prefix-validators.d.ts +13 -0
  7. package/lib/rules/enforce-is-prefix-validators.js +304 -0
  8. package/lib/rules/enforce-m3-sentence-case.d.ts +12 -0
  9. package/lib/rules/enforce-m3-sentence-case.js +430 -0
  10. package/lib/rules/enforce-snapshot-state-narrowing.d.ts +10 -0
  11. package/lib/rules/enforce-snapshot-state-narrowing.js +281 -0
  12. package/lib/rules/enforce-types-directory-placement.d.ts +9 -0
  13. package/lib/rules/enforce-types-directory-placement.js +276 -0
  14. package/lib/rules/no-direct-function-state.d.ts +8 -0
  15. package/lib/rules/no-direct-function-state.js +285 -0
  16. package/lib/rules/no-fill-template-mutation.d.ts +1 -0
  17. package/lib/rules/no-fill-template-mutation.js +324 -0
  18. package/lib/rules/no-hungarian.js +18 -26
  19. package/lib/rules/no-portal-inside-tooltip.d.ts +10 -0
  20. package/lib/rules/no-portal-inside-tooltip.js +219 -0
  21. package/lib/rules/no-redundant-boolean-callback-props.d.ts +11 -0
  22. package/lib/rules/no-redundant-boolean-callback-props.js +355 -0
  23. package/lib/rules/no-satisfies-in-frontend-bundle.d.ts +8 -0
  24. package/lib/rules/no-satisfies-in-frontend-bundle.js +126 -0
  25. package/lib/rules/no-single-dismiss-dialog-button.d.ts +7 -0
  26. package/lib/rules/no-single-dismiss-dialog-button.js +127 -0
  27. package/lib/rules/no-stablehash-react-nodes.d.ts +1 -0
  28. package/lib/rules/no-stablehash-react-nodes.js +325 -0
  29. package/lib/rules/parallelize-loop-awaits.d.ts +8 -0
  30. package/lib/rules/parallelize-loop-awaits.js +582 -0
  31. package/lib/rules/prefer-flat-transform-each-keys.d.ts +1 -0
  32. package/lib/rules/prefer-flat-transform-each-keys.js +228 -0
  33. package/lib/rules/prefer-getter-over-parameterless-method.d.ts +1 -0
  34. package/lib/rules/prefer-getter-over-parameterless-method.js +44 -0
  35. package/lib/rules/prefer-spread-over-reassembly.d.ts +5 -0
  36. package/lib/rules/prefer-spread-over-reassembly.js +401 -0
  37. package/lib/rules/prefer-sx-prop-over-system-props.d.ts +9 -0
  38. package/lib/rules/prefer-sx-prop-over-system-props.js +401 -0
  39. package/lib/rules/prefer-use-base62-id.d.ts +8 -0
  40. package/lib/rules/prefer-use-base62-id.js +483 -0
  41. package/lib/rules/prefer-use-theme.d.ts +1 -0
  42. package/lib/rules/prefer-use-theme.js +206 -0
  43. package/lib/rules/prefer-utility-function-own-file.d.ts +9 -0
  44. package/lib/rules/prefer-utility-function-own-file.js +505 -0
  45. package/lib/rules/react-memoize-literals.js +106 -1
  46. package/lib/rules/require-props-composition.d.ts +10 -0
  47. package/lib/rules/require-props-composition.js +433 -0
  48. package/lib/rules/require-server-timestamp-for-firestore-dates.d.ts +9 -0
  49. package/lib/rules/require-server-timestamp-for-firestore-dates.js +313 -0
  50. package/package.json +1 -1
  51. package/release-manifest.json +212 -0
@@ -0,0 +1,9 @@
1
+ type Options = [
2
+ {
3
+ minStatements?: number;
4
+ minLines?: number;
5
+ ignoreClosures?: boolean;
6
+ }
7
+ ];
8
+ export declare const preferUtilityFunctionOwnFile: import("@typescript-eslint/utils/dist/ts-eslint/Rule").RuleModule<"extractUtility", Options, import("@typescript-eslint/utils/dist/ts-eslint/Rule").RuleListener>;
9
+ export {};
@@ -0,0 +1,505 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || function (mod) {
19
+ if (mod && mod.__esModule) return mod;
20
+ var result = {};
21
+ if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
22
+ __setModuleDefault(result, mod);
23
+ return result;
24
+ };
25
+ Object.defineProperty(exports, "__esModule", { value: true });
26
+ exports.preferUtilityFunctionOwnFile = void 0;
27
+ const path = __importStar(require("path"));
28
+ const utils_1 = require("@typescript-eslint/utils");
29
+ const createRule_1 = require("../utils/createRule");
30
+ const ASTHelpers_1 = require("../utils/ASTHelpers");
31
+ const DEFAULT_MIN_STATEMENTS = 8;
32
+ const DEFAULT_MIN_LINES = 12;
33
+ const DEFAULT_IGNORE_CLOSURES = true;
34
+ /**
35
+ * Collects all identifiers referenced in a node body (for closure detection).
36
+ * Returns the set of identifier names referenced anywhere inside the node,
37
+ * not counting parameter names or locally-declared names.
38
+ */
39
+ function collectReferencedIdentifiers(node) {
40
+ const refs = new Set();
41
+ const locals = new Set();
42
+ function walk(n) {
43
+ if (!n)
44
+ return;
45
+ switch (n.type) {
46
+ case utils_1.AST_NODE_TYPES.Identifier:
47
+ refs.add(n.name);
48
+ break;
49
+ case utils_1.AST_NODE_TYPES.FunctionDeclaration:
50
+ case utils_1.AST_NODE_TYPES.FunctionExpression:
51
+ case utils_1.AST_NODE_TYPES.ArrowFunctionExpression: {
52
+ const fn = n;
53
+ // Record parameter names as locals
54
+ for (const param of fn.params) {
55
+ collectPatternNames(param, locals);
56
+ }
57
+ walk(fn.body);
58
+ break;
59
+ }
60
+ case utils_1.AST_NODE_TYPES.VariableDeclarator: {
61
+ const decl = n;
62
+ collectPatternNames(decl.id, locals);
63
+ walk(decl.init);
64
+ break;
65
+ }
66
+ default: {
67
+ // Walk all child nodes
68
+ for (const key of Object.keys(n)) {
69
+ if (key === 'parent')
70
+ continue;
71
+ const child = n[key];
72
+ if (Array.isArray(child)) {
73
+ for (const item of child) {
74
+ if (item && typeof item === 'object' && 'type' in item) {
75
+ walk(item);
76
+ }
77
+ }
78
+ }
79
+ else if (child && typeof child === 'object' && 'type' in child) {
80
+ walk(child);
81
+ }
82
+ }
83
+ }
84
+ }
85
+ }
86
+ walk(node);
87
+ // Remove purely local names from the reference set
88
+ for (const local of locals) {
89
+ refs.delete(local);
90
+ }
91
+ return refs;
92
+ }
93
+ /**
94
+ * Collects all binding names from a pattern into the given Set.
95
+ */
96
+ function collectPatternNames(node, names) {
97
+ if (!node)
98
+ return;
99
+ switch (node.type) {
100
+ case utils_1.AST_NODE_TYPES.Identifier:
101
+ names.add(node.name);
102
+ break;
103
+ case utils_1.AST_NODE_TYPES.ArrayPattern:
104
+ for (const el of node.elements) {
105
+ if (el)
106
+ collectPatternNames(el, names);
107
+ }
108
+ break;
109
+ case utils_1.AST_NODE_TYPES.ObjectPattern:
110
+ for (const prop of node.properties) {
111
+ if (prop.type === utils_1.AST_NODE_TYPES.RestElement) {
112
+ collectPatternNames(prop.argument, names);
113
+ }
114
+ else {
115
+ collectPatternNames(prop.value, names);
116
+ }
117
+ }
118
+ break;
119
+ case utils_1.AST_NODE_TYPES.AssignmentPattern:
120
+ collectPatternNames(node.left, names);
121
+ break;
122
+ case utils_1.AST_NODE_TYPES.RestElement:
123
+ collectPatternNames(node.argument, names);
124
+ break;
125
+ default:
126
+ break;
127
+ }
128
+ }
129
+ /**
130
+ * Returns the body node (BlockStatement or expression) of a function.
131
+ */
132
+ function getFunctionBody(fn) {
133
+ return fn.body ?? null;
134
+ }
135
+ /**
136
+ * Counts statements inside a BlockStatement body (top level only).
137
+ */
138
+ function countStatements(fn) {
139
+ const body = getFunctionBody(fn);
140
+ if (!body)
141
+ return 0;
142
+ if (body.type === utils_1.AST_NODE_TYPES.BlockStatement) {
143
+ return body.body.length;
144
+ }
145
+ // Arrow function with expression body — count as 1 statement
146
+ return 1;
147
+ }
148
+ /**
149
+ * Counts the number of source lines spanned by a function node.
150
+ */
151
+ function countLines(fn) {
152
+ if (!fn.loc)
153
+ return 0;
154
+ return fn.loc.end.line - fn.loc.start.line + 1;
155
+ }
156
+ /**
157
+ * Returns the function expression node if the VariableDeclarator initializer
158
+ * is (unwrapped) an arrow or function expression.
159
+ */
160
+ function extractFunctionInit(init) {
161
+ if (!init)
162
+ return null;
163
+ let node = init;
164
+ // Unwrap TS wrappers
165
+ while (node &&
166
+ (node.type === utils_1.AST_NODE_TYPES.TSAsExpression ||
167
+ node.type === utils_1.AST_NODE_TYPES.TSSatisfiesExpression ||
168
+ node.type === utils_1.AST_NODE_TYPES.TSNonNullExpression ||
169
+ node.type === utils_1.AST_NODE_TYPES.TSTypeAssertion ||
170
+ node.type === 'ParenthesizedExpression')) {
171
+ node = node.expression ?? null;
172
+ }
173
+ if (!node)
174
+ return null;
175
+ if (node.type === utils_1.AST_NODE_TYPES.ArrowFunctionExpression ||
176
+ node.type === utils_1.AST_NODE_TYPES.FunctionExpression) {
177
+ return node;
178
+ }
179
+ return null;
180
+ }
181
+ /**
182
+ * Returns true if the function returns JSX (is a React component).
183
+ * We check the body for any JSXElement or JSXFragment return.
184
+ */
185
+ function functionReturnsJSX(fn) {
186
+ return ASTHelpers_1.ASTHelpers.returnsJSX(fn);
187
+ }
188
+ /**
189
+ * Returns true if the name matches the React hook naming convention.
190
+ */
191
+ function isHookName(name) {
192
+ return /^use[A-Z]/.test(name);
193
+ }
194
+ /**
195
+ * Extracts the basename (without extension) from a filename.
196
+ * E.g. "modifyRoleMembers.f.ts" → "modifyRoleMembers"
197
+ */
198
+ function fileBasename(filename) {
199
+ const base = path.basename(filename);
200
+ // Strip all extensions (e.g. .f.ts, .test.ts)
201
+ return base.replace(/(\.\w+)+$/, '');
202
+ }
203
+ /**
204
+ * Returns true if the file should be exempt from this rule.
205
+ * Exempt: test/spec files, __mocks__ directories, type-only files under types/**
206
+ */
207
+ function isExemptFile(filename) {
208
+ if (!filename)
209
+ return false;
210
+ const normalized = filename.replace(/\\/g, '/');
211
+ // Test and spec files
212
+ if (/\.(test|spec)\.(tsx?|jsx?)$/.test(normalized))
213
+ return true;
214
+ // Files inside __mocks__ directories
215
+ if (/\/__mocks__\//.test(normalized))
216
+ return true;
217
+ // Type files (functions/src/types/**)
218
+ if (/\/types\//.test(normalized))
219
+ return true;
220
+ return false;
221
+ }
222
+ exports.preferUtilityFunctionOwnFile = (0, createRule_1.createRule)({
223
+ name: 'prefer-utility-function-own-file',
224
+ meta: {
225
+ type: 'suggestion',
226
+ docs: {
227
+ description: 'Enforce that sizable utility functions live in their own file rather than being co-located inside an entry-point or consumer file',
228
+ recommended: 'warn',
229
+ },
230
+ schema: [
231
+ {
232
+ type: 'object',
233
+ properties: {
234
+ minStatements: {
235
+ type: 'number',
236
+ minimum: 1,
237
+ default: DEFAULT_MIN_STATEMENTS,
238
+ },
239
+ minLines: {
240
+ type: 'number',
241
+ minimum: 1,
242
+ default: DEFAULT_MIN_LINES,
243
+ },
244
+ ignoreClosures: {
245
+ type: 'boolean',
246
+ default: DEFAULT_IGNORE_CLOSURES,
247
+ },
248
+ },
249
+ additionalProperties: false,
250
+ },
251
+ ],
252
+ messages: {
253
+ extractUtility: '"{{name}}" is a sizable utility function co-located in a file whose primary purpose is something else. Move it to its own file (e.g. under util/) so it is discoverable and reusable.',
254
+ },
255
+ },
256
+ defaultOptions: [{}],
257
+ create(context, [options]) {
258
+ const minStatements = options.minStatements ?? DEFAULT_MIN_STATEMENTS;
259
+ const minLines = options.minLines ?? DEFAULT_MIN_LINES;
260
+ const ignoreClosures = options.ignoreClosures ?? DEFAULT_IGNORE_CLOSURES;
261
+ const filename = context.getFilename();
262
+ // Exempt test/mock/type files entirely
263
+ if (isExemptFile(filename))
264
+ return {};
265
+ const basename = fileBasename(filename);
266
+ const topLevelFunctions = [];
267
+ let hasExportDefault = false;
268
+ // Names that appear as the handler inside `export default someWrapper(name)`
269
+ const wrappedDefaultHandlerNames = new Set();
270
+ // Names directly exported as default (export default myFunc style via ExportDefaultDeclaration referencing an identifier)
271
+ const defaultExportedIdentifiers = new Set();
272
+ // Names exported via `export { foo, bar }` specifiers
273
+ const specifierExportedNames = new Set();
274
+ return {
275
+ // Collect export default declarations
276
+ ExportDefaultDeclaration(node) {
277
+ hasExportDefault = true;
278
+ const decl = node.declaration;
279
+ // export default myFunc
280
+ if (decl.type === utils_1.AST_NODE_TYPES.Identifier) {
281
+ defaultExportedIdentifiers.add(decl.name);
282
+ }
283
+ // export default onCall(authenticatedOnly(myFunc))
284
+ if (decl.type === utils_1.AST_NODE_TYPES.CallExpression) {
285
+ // walk the call expression tree for identifier references
286
+ const findWrappedIds = (expr) => {
287
+ let e = expr;
288
+ while (e &&
289
+ (e.type === utils_1.AST_NODE_TYPES.TSAsExpression ||
290
+ e.type === utils_1.AST_NODE_TYPES.TSSatisfiesExpression ||
291
+ e.type === utils_1.AST_NODE_TYPES.TSNonNullExpression ||
292
+ e.type === utils_1.AST_NODE_TYPES.TSTypeAssertion ||
293
+ e.type === 'ParenthesizedExpression')) {
294
+ e = e.expression;
295
+ }
296
+ if (!e)
297
+ return;
298
+ if (e.type === utils_1.AST_NODE_TYPES.Identifier) {
299
+ wrappedDefaultHandlerNames.add(e.name);
300
+ }
301
+ if (e.type === utils_1.AST_NODE_TYPES.CallExpression) {
302
+ for (const arg of e.arguments) {
303
+ findWrappedIds(arg);
304
+ }
305
+ }
306
+ };
307
+ findWrappedIds(decl);
308
+ }
309
+ },
310
+ // Collect names exported via specifiers: `export { foo, bar }`
311
+ ExportNamedDeclaration(node) {
312
+ // Only handle specifier-style exports (not `export const foo = ...` which
313
+ // is handled by VariableDeclaration with ExportNamedDeclaration parent)
314
+ if (node.declaration)
315
+ return;
316
+ for (const specifier of node.specifiers) {
317
+ if (specifier.local.type === utils_1.AST_NODE_TYPES.Identifier) {
318
+ specifierExportedNames.add(specifier.local.name);
319
+ }
320
+ }
321
+ },
322
+ // Collect top-level FunctionDeclarations
323
+ FunctionDeclaration(node) {
324
+ // Only top-level: parent must be Program or ExportNamedDeclaration or ExportDefaultDeclaration
325
+ const parent = node.parent;
326
+ const isTopLevel = parent?.type === utils_1.AST_NODE_TYPES.Program ||
327
+ parent?.type === utils_1.AST_NODE_TYPES.ExportNamedDeclaration ||
328
+ parent?.type === utils_1.AST_NODE_TYPES.ExportDefaultDeclaration;
329
+ if (!isTopLevel)
330
+ return;
331
+ if (!node.id)
332
+ return;
333
+ const name = node.id.name;
334
+ const isNamedExport = parent?.type === utils_1.AST_NODE_TYPES.ExportNamedDeclaration;
335
+ const isDefaultExport = parent?.type === utils_1.AST_NODE_TYPES.ExportDefaultDeclaration;
336
+ topLevelFunctions.push({
337
+ node,
338
+ name,
339
+ fn: node,
340
+ isDefaultExport,
341
+ isNamedExport,
342
+ });
343
+ },
344
+ // Collect top-level arrow/function expression VariableDeclarators
345
+ VariableDeclaration(node) {
346
+ // Only top-level
347
+ const parent = node.parent;
348
+ const isTopLevel = parent?.type === utils_1.AST_NODE_TYPES.Program ||
349
+ parent?.type === utils_1.AST_NODE_TYPES.ExportNamedDeclaration;
350
+ if (!isTopLevel)
351
+ return;
352
+ const isNamedExport = parent?.type === utils_1.AST_NODE_TYPES.ExportNamedDeclaration;
353
+ for (const declarator of node.declarations) {
354
+ if (declarator.id.type !== utils_1.AST_NODE_TYPES.Identifier)
355
+ continue;
356
+ const name = declarator.id.name;
357
+ const fn = extractFunctionInit(declarator.init);
358
+ if (!fn)
359
+ continue;
360
+ topLevelFunctions.push({
361
+ node: declarator,
362
+ name,
363
+ fn,
364
+ isDefaultExport: false,
365
+ isNamedExport,
366
+ });
367
+ }
368
+ },
369
+ 'Program:exit'() {
370
+ // Determine the file's primary export name (basename heuristic)
371
+ // e.g. "modifyRoleMembers" in "modifyRoleMembers.f.ts"
372
+ const primaryName = basename;
373
+ // Reconcile isNamedExport for functions exported via specifiers
374
+ // (e.g. `export { foo, bar }` when foo/bar were declared without export keyword)
375
+ for (const info of topLevelFunctions) {
376
+ if (!info.isNamedExport && specifierExportedNames.has(info.name)) {
377
+ info.isNamedExport = true;
378
+ }
379
+ }
380
+ // Determine if the file has a distinct primary export (co-location gate).
381
+ // A file must have SOME other purpose for us to flag anything.
382
+ // Indicators of a distinct primary purpose:
383
+ // 1. Has `export default` (callable entry-point, page, etc.)
384
+ // 2. Has a React component (PascalCase, returning JSX)
385
+ // 3. Has another named exported util that is NOT the function being evaluated
386
+ // We'll compute this lazily per-candidate below.
387
+ // If the file has no export default AND only one top-level function, the
388
+ // file's sole purpose IS that function — it's already in its own file.
389
+ // Never flag in that case.
390
+ const totalFunctions = topLevelFunctions.length;
391
+ if (!hasExportDefault && totalFunctions <= 1)
392
+ return;
393
+ // For each candidate, determine whether to flag
394
+ for (const info of topLevelFunctions) {
395
+ const { node, name, fn, isDefaultExport } = info;
396
+ // --- Exclusion: is a hook ---
397
+ if (isHookName(name))
398
+ continue;
399
+ // --- Exclusion: returns JSX (React component) ---
400
+ if (functionReturnsJSX(fn))
401
+ continue;
402
+ // --- Exclusion: is the default export directly ---
403
+ if (isDefaultExport)
404
+ continue;
405
+ // --- Exclusion: name is referenced in export default (e.g. wrapped handler) ---
406
+ if (wrappedDefaultHandlerNames.has(name) ||
407
+ defaultExportedIdentifiers.has(name))
408
+ continue;
409
+ // --- Exclusion: name matches file basename (primary export heuristic) ---
410
+ if (name === primaryName)
411
+ continue;
412
+ // --- Exclusion: it IS a named exported util but name matches basename ---
413
+ // (already covered above, but be explicit)
414
+ // --- Size check: must be sizable ---
415
+ const stmts = countStatements(fn);
416
+ const lines = countLines(fn);
417
+ const isSizable = stmts >= minStatements || lines >= minLines;
418
+ if (!isSizable)
419
+ continue;
420
+ // --- ignoreClosures: skip if the function body references module-scoped identifiers not passed as params ---
421
+ if (ignoreClosures) {
422
+ const closesOverModuleScope = functionClosesOverModuleScope(fn, topLevelFunctions);
423
+ if (closesOverModuleScope)
424
+ continue;
425
+ }
426
+ // --- Co-location gate: file must have a distinct primary export ---
427
+ // The file has a distinct primary purpose if:
428
+ // - It has export default (and it's not THIS function)
429
+ // - Another top-level function returns JSX (a React component lives here)
430
+ // - Another named exported function exists that is not this one
431
+ const hasDistinctPrimary = determineHasDistinctPrimary(info, topLevelFunctions, hasExportDefault);
432
+ if (!hasDistinctPrimary)
433
+ continue;
434
+ context.report({
435
+ node,
436
+ messageId: 'extractUtility',
437
+ data: { name },
438
+ });
439
+ }
440
+ },
441
+ };
442
+ },
443
+ });
444
+ /**
445
+ * Returns true if the function closes over module-scoped bindings
446
+ * (identifiers defined at the top level of the module) that are NOT
447
+ * passed as parameters, making it non-trivially extractable.
448
+ *
449
+ * We identify "module-scope" identifiers as names of other top-level
450
+ * declarations in the file (functions, variables) that are NOT imported.
451
+ * Imported names and standard globals are considered "extractable" (you'd
452
+ * just re-import them).
453
+ *
454
+ * Strategy: collect referenced identifiers in the body, subtract param names,
455
+ * then check if any remain that are names of other top-level declarations.
456
+ */
457
+ function functionClosesOverModuleScope(fn, topLevelFunctions) {
458
+ const body = getFunctionBody(fn);
459
+ if (!body)
460
+ return false;
461
+ // Collect names that are parameters of this function
462
+ const paramNames = new Set();
463
+ for (const param of fn.params) {
464
+ collectPatternNames(param, paramNames);
465
+ }
466
+ // Collect all identifiers referenced inside the function body
467
+ const referencedIds = collectReferencedIdentifiers(body);
468
+ // Remove params (they're passed in, fine)
469
+ for (const param of paramNames) {
470
+ referencedIds.delete(param);
471
+ }
472
+ // Get the set of top-level sibling function names
473
+ const topLevelNames = new Set(topLevelFunctions.map((f) => f.name));
474
+ // If ANY referenced identifier is a top-level sibling function name,
475
+ // then this function closes over a module-scope binding.
476
+ for (const ref of referencedIds) {
477
+ if (topLevelNames.has(ref)) {
478
+ return true;
479
+ }
480
+ }
481
+ return false;
482
+ }
483
+ /**
484
+ * Determines whether the file has a distinct primary purpose (co-location gate).
485
+ * A file has a distinct primary purpose if (excluding the function under test):
486
+ * - It has an `export default` declaration, OR
487
+ * - Another top-level function returns JSX (a component lives here), OR
488
+ * - Another top-level function is a named export (a different util lives here)
489
+ */
490
+ function determineHasDistinctPrimary(candidate, allFunctions, hasExportDefault) {
491
+ // If there's an export default in the file, that signals a distinct primary
492
+ if (hasExportDefault)
493
+ return true;
494
+ // Check siblings for React components or other named exported utils
495
+ for (const other of allFunctions) {
496
+ if (other.name === candidate.name)
497
+ continue;
498
+ if (functionReturnsJSX(other.fn))
499
+ return true;
500
+ if (other.isNamedExport || other.isDefaultExport)
501
+ return true;
502
+ }
503
+ return false;
504
+ }
505
+ //# sourceMappingURL=prefer-utility-function-own-file.js.map
@@ -485,6 +485,98 @@ function isStyleJSXAttributeValue(node) {
485
485
  }
486
486
  return false;
487
487
  }
488
+ /**
489
+ * Returns true when the node is a JSX element or fragment. JSX nodes are
490
+ * always fresh references each render — wrapping a containing literal in
491
+ * useMemo provides no referential-stability benefit because the JSX member
492
+ * changes on every call regardless of the wrapper.
493
+ */
494
+ function isJSXNode(node) {
495
+ return (node.type === utils_1.AST_NODE_TYPES.JSXElement ||
496
+ node.type === utils_1.AST_NODE_TYPES.JSXFragment);
497
+ }
498
+ /**
499
+ * Collects the names of variables declared in the owning function's top-level
500
+ * block body whose initializers resolve to JSX elements or fragments. Used to
501
+ * detect shorthand object properties like `{ Portal }` where
502
+ * `const Portal = <div />` precedes the return statement.
503
+ *
504
+ * Only top-level declarations are scanned; inner function scopes have their
505
+ * own render lifecycles and are not candidates for this exemption.
506
+ */
507
+ function collectLocalJSXBindings(owner) {
508
+ const bindings = new Set();
509
+ const body = owner.body;
510
+ if (body.type !== utils_1.AST_NODE_TYPES.BlockStatement)
511
+ return bindings;
512
+ for (const stmt of body.body) {
513
+ if (stmt.type !== utils_1.AST_NODE_TYPES.VariableDeclaration)
514
+ continue;
515
+ for (const declarator of stmt.declarations) {
516
+ if (declarator.id.type !== utils_1.AST_NODE_TYPES.Identifier)
517
+ continue;
518
+ if (!declarator.init)
519
+ continue;
520
+ const unwrapped = unwrapNestedExpressions(declarator.init);
521
+ if (isJSXNode(unwrapped)) {
522
+ bindings.add(declarator.id.name);
523
+ }
524
+ }
525
+ }
526
+ return bindings;
527
+ }
528
+ /**
529
+ * Returns true when an ObjectExpression returned from a hook contains at
530
+ * least one property whose value is a JSX element or fragment — either
531
+ * directly inline (`{ Portal: <div /> }`) or via a shorthand identifier
532
+ * (`{ Portal }`) that resolves to a JSX initializer in the same function
533
+ * scope. Such objects cannot be stabilised by wrapping them in useMemo
534
+ * because the JSX member is a fresh reference on every render regardless of
535
+ * the wrapper. This is the same "no stability benefit" rationale applied to
536
+ * sx/style JSX attribute values.
537
+ */
538
+ function objectLiteralContainsJSXValue(node, owner) {
539
+ // Resolve shorthand bindings lazily — only when at least one shorthand
540
+ // property is present, to avoid the body scan for the common inline case.
541
+ let localJSXBindings = null;
542
+ for (const prop of node.properties) {
543
+ if (prop.type === utils_1.AST_NODE_TYPES.SpreadElement)
544
+ continue;
545
+ const value = unwrapNestedExpressions(prop.value);
546
+ // Direct inline JSX: { Portal: <div /> } or { el: <></> }
547
+ if (isJSXNode(value)) {
548
+ return true;
549
+ }
550
+ // Shorthand identifier: { Portal } — resolve to a local JSX binding.
551
+ if (prop.shorthand && value.type === utils_1.AST_NODE_TYPES.Identifier) {
552
+ if (!localJSXBindings) {
553
+ localJSXBindings = collectLocalJSXBindings(owner);
554
+ }
555
+ if (localJSXBindings.has(value.name)) {
556
+ return true;
557
+ }
558
+ }
559
+ }
560
+ return false;
561
+ }
562
+ /**
563
+ * Returns true when an ArrayExpression returned from a hook contains at least
564
+ * one element that is a JSX element or fragment (after unwrapping expression
565
+ * wrappers). The same "no stability benefit" rationale applies: a JSX element
566
+ * is a fresh reference each render, so wrapping the array in useMemo would
567
+ * recompute on every call without improving referential stability.
568
+ */
569
+ function arrayLiteralContainsJSXValue(node) {
570
+ for (const element of node.elements) {
571
+ if (!element)
572
+ continue;
573
+ const unwrapped = unwrapNestedExpressions(element);
574
+ if (isJSXNode(unwrapped)) {
575
+ return true;
576
+ }
577
+ }
578
+ return false;
579
+ }
488
580
  /**
489
581
  * Formats a readable label for diagnostics based on the owning function.
490
582
  * @param fn Owning component or hook function.
@@ -511,7 +603,7 @@ exports.reactMemoizeLiterals = (0, createRule_1.createRule)({
511
603
  messages: {
512
604
  componentLiteral: 'New {{literalType}} inside {{context}} is created on every render → Breaks referential stability for hooks/props and can re-run effects or re-render children → Hoist it to a module-level constant or wrap it in {{memoHook}} with the right dependencies.',
513
605
  nestedHookLiteral: 'Nested {{literalType}} inside {{hookName}} arguments is recreated on every render → Dependency/reference comparisons change each time and defeat memoization/caching → Extract it into a memoized value (useMemo/useCallback) or hoist it to a module constant before passing it to {{hookName}}.',
514
- hookReturnLiteral: '{{hookName}} returns a {{literalType}} literal on each render → Callers receive a fresh reference and may re-render or re-run effects → Memoize the returned value with useMemo/useCallback or return pre-memoized pieces so callers see stable references.',
606
+ hookReturnLiteral: '{{hookName}} returns an {{literalType}} on each render → Callers receive a fresh reference and may re-render or re-run effects → Memoize the returned value with useMemo/useCallback or return pre-memoized pieces so callers see stable references.',
515
607
  memoizeLiteralSuggestion: 'This {{literalType}} is created inline → It produces a new reference each render → Wrap it in {{memoHook}} and include every closed-over value in the dependency array.',
516
608
  },
517
609
  },
@@ -659,6 +751,19 @@ exports.reactMemoizeLiterals = (0, createRule_1.createRule)({
659
751
  return;
660
752
  }
661
753
  if (isReturnValueFromHook(node, owner)) {
754
+ // A returned literal whose members include a JSX element or fragment
755
+ // cannot be stabilised by useMemo: JSX nodes are inherently fresh
756
+ // references each render, so the wrapper recomputes every call
757
+ // regardless. This mirrors the "no stability benefit" carve-out for
758
+ // sx/style JSX attribute values (see isStyleJSXAttributeValue).
759
+ if (node.type === utils_1.AST_NODE_TYPES.ObjectExpression &&
760
+ objectLiteralContainsJSXValue(node, owner)) {
761
+ return;
762
+ }
763
+ if (node.type === utils_1.AST_NODE_TYPES.ArrayExpression &&
764
+ arrayLiteralContainsJSXValue(node)) {
765
+ return;
766
+ }
662
767
  const hookName = getFunctionName(owner) ?? 'this hook';
663
768
  context.report({
664
769
  node,
@@ -0,0 +1,10 @@
1
+ type Options = [
2
+ {
3
+ targetPaths?: string[];
4
+ excludeComponents?: string[];
5
+ minDependencyCount?: number;
6
+ requireAllDependencies?: boolean;
7
+ }
8
+ ];
9
+ export declare const requirePropsComposition: import("@typescript-eslint/utils/dist/ts-eslint/Rule").RuleModule<"missingPropsComposition", Options, import("@typescript-eslint/utils/dist/ts-eslint/Rule").RuleListener>;
10
+ export {};