@blumintinc/eslint-plugin-blumint 1.13.0 → 1.14.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 (186) hide show
  1. package/README.md +170 -139
  2. package/lib/index.js +190 -1
  3. package/lib/rules/array-methods-this-context.js +1 -1
  4. package/lib/rules/class-methods-read-top-to-bottom.js +24 -5
  5. package/lib/rules/consistent-callback-naming.js +1 -1
  6. package/lib/rules/dynamic-https-errors.js +3 -4
  7. package/lib/rules/enforce-assert-safe-object-key.js +11 -11
  8. package/lib/rules/enforce-boolean-naming-prefixes.js +11 -23
  9. package/lib/rules/enforce-callback-memo.js +1 -1
  10. package/lib/rules/enforce-centralized-mock-firestore.js +1 -1
  11. package/lib/rules/enforce-console-error.js +35 -7
  12. package/lib/rules/enforce-dynamic-file-naming.d.ts +2 -0
  13. package/lib/rules/enforce-dynamic-file-naming.js +53 -28
  14. package/lib/rules/enforce-early-destructuring.d.ts +2 -0
  15. package/lib/rules/enforce-early-destructuring.js +980 -0
  16. package/lib/rules/enforce-empty-object-check.d.ts +11 -0
  17. package/lib/rules/enforce-empty-object-check.js +502 -0
  18. package/lib/rules/enforce-exported-function-types.js +1 -1
  19. package/lib/rules/enforce-fieldpath-syntax-in-docsetter.js +100 -50
  20. package/lib/rules/enforce-firestore-doc-ref-generic.js +15 -3
  21. package/lib/rules/enforce-firestore-facade.js +4 -2
  22. package/lib/rules/enforce-firestore-set-merge.js +3 -4
  23. package/lib/rules/enforce-global-constants.js +1 -1
  24. package/lib/rules/enforce-identifiable-firestore-type.js +108 -58
  25. package/lib/rules/enforce-memoize-async.js +2 -6
  26. package/lib/rules/enforce-memoize-getters.js +39 -15
  27. package/lib/rules/enforce-microdiff.js +1 -1
  28. package/lib/rules/enforce-object-literal-as-const.js +4 -4
  29. package/lib/rules/enforce-positive-naming.js +4 -0
  30. package/lib/rules/enforce-props-argument-name.js +2 -2
  31. package/lib/rules/enforce-safe-stringify.js +1 -1
  32. package/lib/rules/enforce-serializable-params.js +3 -3
  33. package/lib/rules/enforce-singular-type-names.js +1 -1
  34. package/lib/rules/enforce-stable-hash-spread-props.d.ts +13 -0
  35. package/lib/rules/enforce-stable-hash-spread-props.js +342 -0
  36. package/lib/rules/enforce-storage-context.d.ts +9 -0
  37. package/lib/rules/enforce-storage-context.js +646 -0
  38. package/lib/rules/enforce-timestamp-now.js +23 -7
  39. package/lib/rules/enforce-transform-memoization.d.ts +4 -0
  40. package/lib/rules/enforce-transform-memoization.js +416 -0
  41. package/lib/rules/enforce-typescript-markdown-code-blocks.js +7 -3
  42. package/lib/rules/enforce-unique-cursor-headers.d.ts +16 -0
  43. package/lib/rules/enforce-unique-cursor-headers.js +365 -0
  44. package/lib/rules/enforce-verb-noun-naming.js +5 -4
  45. package/lib/rules/ensure-pointer-events-none.js +28 -2
  46. package/lib/rules/export-if-in-doubt.js +27 -4
  47. package/lib/rules/extract-global-constants.js +40 -13
  48. package/lib/rules/fast-deep-equal-over-microdiff.js +29 -39
  49. package/lib/rules/firestore-transaction-reads-before-writes.js +50 -4
  50. package/lib/rules/flatten-push-calls.d.ts +2 -0
  51. package/lib/rules/flatten-push-calls.js +428 -0
  52. package/lib/rules/global-const-style.js +50 -19
  53. package/lib/rules/jsdoc-above-field.d.ts +11 -0
  54. package/lib/rules/jsdoc-above-field.js +208 -0
  55. package/lib/rules/key-only-outermost-element.js +21 -3
  56. package/lib/rules/logical-top-to-bottom-grouping.d.ts +5 -0
  57. package/lib/rules/logical-top-to-bottom-grouping.js +1258 -0
  58. package/lib/rules/memo-compare-deeply-complex-props.d.ts +3 -0
  59. package/lib/rules/memo-compare-deeply-complex-props.js +788 -0
  60. package/lib/rules/memo-nested-react-components.d.ts +8 -0
  61. package/lib/rules/memo-nested-react-components.js +410 -0
  62. package/lib/rules/no-always-true-false-conditions.d.ts +2 -1
  63. package/lib/rules/no-always-true-false-conditions.js +68 -8
  64. package/lib/rules/no-array-length-in-deps.js +9 -3
  65. package/lib/rules/no-async-foreach.js +210 -12
  66. package/lib/rules/no-circular-references.js +18 -17
  67. package/lib/rules/no-class-instance-destructuring.js +55 -12
  68. package/lib/rules/no-complex-cloud-params.js +7 -3
  69. package/lib/rules/no-compositing-layer-props.js +2 -2
  70. package/lib/rules/no-conditional-literals-in-jsx.js +37 -12
  71. package/lib/rules/no-console-error.d.ts +9 -0
  72. package/lib/rules/no-console-error.js +525 -0
  73. package/lib/rules/no-curly-brackets-around-commented-properties.d.ts +2 -0
  74. package/lib/rules/no-curly-brackets-around-commented-properties.js +258 -0
  75. package/lib/rules/no-empty-dependency-use-callbacks.d.ts +11 -0
  76. package/lib/rules/no-empty-dependency-use-callbacks.js +576 -0
  77. package/lib/rules/no-entire-object-hook-deps.js +4 -6
  78. package/lib/rules/no-excessive-parent-chain.js +1 -1
  79. package/lib/rules/no-explicit-return-type.d.ts +2 -1
  80. package/lib/rules/no-explicit-return-type.js +215 -26
  81. package/lib/rules/no-filter-without-return.js +5 -1
  82. package/lib/rules/no-firestore-jest-mock.d.ts +2 -1
  83. package/lib/rules/no-firestore-jest-mock.js +126 -8
  84. package/lib/rules/no-firestore-object-arrays.js +67 -12
  85. package/lib/rules/no-handler-suffix.d.ts +12 -0
  86. package/lib/rules/no-handler-suffix.js +305 -0
  87. package/lib/rules/no-hungarian.js +1 -1
  88. package/lib/rules/no-inline-component-prop.d.ts +10 -0
  89. package/lib/rules/no-inline-component-prop.js +456 -0
  90. package/lib/rules/no-jsx-in-hooks.js +6 -1
  91. package/lib/rules/no-jsx-whitespace-literal.js +8 -2
  92. package/lib/rules/no-margin-properties.js +6 -6
  93. package/lib/rules/no-memoize-on-static.js +9 -1
  94. package/lib/rules/no-misleading-boolean-prefixes.js +7 -3
  95. package/lib/rules/no-misused-switch-case.js +22 -1
  96. package/lib/rules/no-mixed-firestore-transactions.d.ts +3 -1
  97. package/lib/rules/no-mixed-firestore-transactions.js +296 -34
  98. package/lib/rules/no-mock-firebase-admin.js +5 -2
  99. package/lib/rules/no-object-values-on-strings.js +10 -2
  100. package/lib/rules/no-overridable-method-calls-in-constructor.js +29 -17
  101. package/lib/rules/no-passthrough-getters.js +38 -2
  102. package/lib/rules/no-redundant-annotation-assertion.d.ts +2 -0
  103. package/lib/rules/no-redundant-annotation-assertion.js +402 -0
  104. package/lib/rules/no-redundant-param-types.js +16 -6
  105. package/lib/rules/no-redundant-this-params.d.ts +3 -0
  106. package/lib/rules/no-redundant-this-params.js +459 -0
  107. package/lib/rules/no-redundant-usecallback-wrapper.js +16 -3
  108. package/lib/rules/no-res-error-status-in-onrequest.d.ts +4 -0
  109. package/lib/rules/no-res-error-status-in-onrequest.js +521 -0
  110. package/lib/rules/no-restricted-properties-fix.js +11 -10
  111. package/lib/rules/no-separate-loading-state.js +7 -16
  112. package/lib/rules/no-stale-state-across-await.js +1 -1
  113. package/lib/rules/no-type-assertion-returns.js +43 -49
  114. package/lib/rules/no-undefined-null-passthrough.js +61 -31
  115. package/lib/rules/no-unmemoized-memo-without-props.d.ts +8 -0
  116. package/lib/rules/no-unmemoized-memo-without-props.js +426 -0
  117. package/lib/rules/no-unnecessary-destructuring-rename.d.ts +2 -0
  118. package/lib/rules/no-unnecessary-destructuring-rename.js +347 -0
  119. package/lib/rules/no-unnecessary-destructuring.js +14 -4
  120. package/lib/rules/no-unnecessary-verb-suffix.js +2 -1
  121. package/lib/rules/no-unpinned-dependencies.js +36 -5
  122. package/lib/rules/no-unused-props.d.ts +2 -2
  123. package/lib/rules/no-unused-props.js +295 -91
  124. package/lib/rules/no-unused-usestate.js +6 -2
  125. package/lib/rules/no-useless-fragment.js +28 -2
  126. package/lib/rules/no-useless-usememo-primitives.d.ts +9 -0
  127. package/lib/rules/no-useless-usememo-primitives.js +393 -0
  128. package/lib/rules/no-usememo-for-pass-by-value.d.ts +13 -0
  129. package/lib/rules/no-usememo-for-pass-by-value.js +757 -0
  130. package/lib/rules/no-uuidv4-base62-as-key.js +18 -13
  131. package/lib/rules/omit-index-html.d.ts +2 -1
  132. package/lib/rules/omit-index-html.js +62 -7
  133. package/lib/rules/optimize-object-boolean-conditions.js +6 -4
  134. package/lib/rules/parallelize-async-operations.js +24 -5
  135. package/lib/rules/prefer-batch-operations.d.ts +1 -3
  136. package/lib/rules/prefer-batch-operations.js +32 -5
  137. package/lib/rules/prefer-block-comments-for-declarations.js +9 -4
  138. package/lib/rules/prefer-clone-deep.js +3 -3
  139. package/lib/rules/prefer-destructuring-no-class.d.ts +2 -1
  140. package/lib/rules/prefer-destructuring-no-class.js +244 -59
  141. package/lib/rules/prefer-docsetter-setall.d.ts +2 -0
  142. package/lib/rules/prefer-docsetter-setall.js +243 -0
  143. package/lib/rules/prefer-field-paths-in-transforms.js +4 -3
  144. package/lib/rules/prefer-fragment-component.js +1 -1
  145. package/lib/rules/prefer-fragment-shorthand.js +2 -1
  146. package/lib/rules/prefer-getter-over-parameterless-method.d.ts +13 -0
  147. package/lib/rules/prefer-getter-over-parameterless-method.js +648 -0
  148. package/lib/rules/prefer-global-router-state-key.js +88 -13
  149. package/lib/rules/prefer-memoized-props.d.ts +3 -0
  150. package/lib/rules/prefer-memoized-props.js +445 -0
  151. package/lib/rules/prefer-next-dynamic.js +60 -69
  152. package/lib/rules/prefer-nullish-coalescing-boolean-props.js +9 -4
  153. package/lib/rules/prefer-nullish-coalescing-override.d.ts +2 -1
  154. package/lib/rules/prefer-nullish-coalescing-override.js +11 -5
  155. package/lib/rules/prefer-params-over-parent-id.js +220 -171
  156. package/lib/rules/prefer-settings-object.js +2 -2
  157. package/lib/rules/prefer-type-over-interface.js +7 -2
  158. package/lib/rules/prefer-url-tostring-over-tojson.js +6 -3
  159. package/lib/rules/prefer-use-deep-compare-memo.js +8 -5
  160. package/lib/rules/prefer-usecallback-over-usememo-for-functions.js +1 -1
  161. package/lib/rules/prefer-utility-function-over-private-static.js +34 -2
  162. package/lib/rules/prevent-children-clobber.d.ts +2 -0
  163. package/lib/rules/prevent-children-clobber.js +536 -0
  164. package/lib/rules/react-memoize-literals.d.ts +4 -0
  165. package/lib/rules/react-memoize-literals.js +495 -0
  166. package/lib/rules/react-usememo-should-be-component.js +2 -2
  167. package/lib/rules/require-hooks-default-params.js +17 -16
  168. package/lib/rules/require-https-error-cause.d.ts +4 -0
  169. package/lib/rules/require-https-error-cause.js +136 -0
  170. package/lib/rules/require-https-error.js +43 -21
  171. package/lib/rules/require-image-optimized.js +1 -1
  172. package/lib/rules/require-memo.js +1 -1
  173. package/lib/rules/require-memoize-jsx-returners.d.ts +3 -0
  174. package/lib/rules/require-memoize-jsx-returners.js +485 -0
  175. package/lib/rules/require-usememo-object-literals.js +2 -3
  176. package/lib/rules/test-file-location-enforcement.js +1 -1
  177. package/lib/rules/use-latest-callback.js +6 -5
  178. package/lib/rules/vertically-group-related-functions.d.ts +16 -0
  179. package/lib/rules/vertically-group-related-functions.js +480 -0
  180. package/lib/utils/ASTHelpers.d.ts +13 -1
  181. package/lib/utils/ASTHelpers.js +24 -0
  182. package/lib/utils/getMethodName.d.ts +27 -0
  183. package/lib/utils/getMethodName.js +35 -0
  184. package/lib/utils/tsTypeClassifier.d.ts +30 -0
  185. package/lib/utils/tsTypeClassifier.js +149 -0
  186. package/package.json +3 -1
@@ -0,0 +1,480 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.verticallyGroupRelatedFunctions = void 0;
4
+ const createRule_1 = require("../utils/createRule");
5
+ const ASTHelpers_1 = require("../utils/ASTHelpers");
6
+ const DEFAULT_OPTIONS = {
7
+ exportPlacement: 'ignore',
8
+ dependencyDirection: 'callers-first',
9
+ groupOrder: ['event-handlers', 'other', 'utilities'],
10
+ eventHandlerPattern: '^(handle[A-Z]|on[A-Z])',
11
+ utilityPattern: '^(get|set|fetch|load|format|compute|transform|build|derive|prepare)',
12
+ };
13
+ const DEFAULT_OPTIONS_ARRAY = [DEFAULT_OPTIONS];
14
+ const PATTERN_LENGTH_LIMIT = 200;
15
+ const NESTED_QUANTIFIER_PATTERN = /\([^)]*[+*][^)]*\)\s*[\+\*{]/;
16
+ const BROAD_QUANTIFIER_WITH_BACKREFERENCE_PATTERN = /(?:\.\*|\.\+)[^)]*\\\d/;
17
+ const REPEATED_BROAD_QUANTIFIER_PATTERN = /(?:\.\*|\.\+).*(?:\.\*|\.\+)/;
18
+ function isPatternLikelySafe(pattern) {
19
+ if (pattern.length > PATTERN_LENGTH_LIMIT) {
20
+ return {
21
+ safe: false,
22
+ reason: `pattern longer than ${PATTERN_LENGTH_LIMIT} characters`,
23
+ };
24
+ }
25
+ if (NESTED_QUANTIFIER_PATTERN.test(pattern)) {
26
+ return {
27
+ safe: false,
28
+ reason: 'nested quantifiers can trigger catastrophic backtracking (e.g., "(.+)+")',
29
+ };
30
+ }
31
+ if (REPEATED_BROAD_QUANTIFIER_PATTERN.test(pattern)) {
32
+ return {
33
+ safe: false,
34
+ reason: 'multiple greedy wildcards in one pattern create heavy backtracking',
35
+ };
36
+ }
37
+ if (BROAD_QUANTIFIER_WITH_BACKREFERENCE_PATTERN.test(pattern)) {
38
+ return {
39
+ safe: false,
40
+ reason: 'combining greedy wildcards with backreferences is prone to ReDoS backtracking',
41
+ };
42
+ }
43
+ return { safe: true };
44
+ }
45
+ function createRegexWithFallback(pattern, fallback, onUnsafe) {
46
+ if (pattern) {
47
+ const safety = isPatternLikelySafe(pattern);
48
+ if (!safety.safe) {
49
+ onUnsafe?.(pattern, safety.reason ||
50
+ 'pattern rejected because it may cause catastrophic backtracking');
51
+ return new RegExp(fallback);
52
+ }
53
+ }
54
+ try {
55
+ return new RegExp(pattern ?? fallback);
56
+ }
57
+ catch (error) {
58
+ if (pattern) {
59
+ onUnsafe?.(pattern, error?.message || 'pattern failed to compile');
60
+ }
61
+ return new RegExp(fallback);
62
+ }
63
+ }
64
+ function normalizeGroupOrder(order) {
65
+ const base = DEFAULT_OPTIONS.groupOrder;
66
+ if (!order || order.length === 0) {
67
+ return base;
68
+ }
69
+ const seen = new Set();
70
+ const combined = [];
71
+ for (const group of [...order, ...base]) {
72
+ if (!seen.has(group)) {
73
+ seen.add(group);
74
+ combined.push(group);
75
+ }
76
+ }
77
+ return combined;
78
+ }
79
+ function classifyGroup(info) {
80
+ if (info.isEventHandler) {
81
+ return 'event-handlers';
82
+ }
83
+ if (info.isUtility) {
84
+ return 'utilities';
85
+ }
86
+ return 'other';
87
+ }
88
+ function isNamedFunctionDeclaration(node) {
89
+ return Boolean(node && node.id && node.id.type === 'Identifier');
90
+ }
91
+ function isFunctionExpressionLike(node) {
92
+ return (!!node &&
93
+ (node.type === 'ArrowFunctionExpression' ||
94
+ node.type === 'FunctionExpression' ||
95
+ node.type === 'FunctionDeclaration'));
96
+ }
97
+ function extractFunctionFromVariableDeclaration(statement) {
98
+ if (statement.declarations.length !== 1) {
99
+ return null;
100
+ }
101
+ const declarator = statement.declarations[0];
102
+ if (declarator.id.type !== 'Identifier' ||
103
+ !isFunctionExpressionLike(declarator.init)) {
104
+ return null;
105
+ }
106
+ return { name: declarator.id.name, fnNode: declarator.init };
107
+ }
108
+ function collectFunctions(programBody, eventHandlerRegex, utilityRegex) {
109
+ const functions = [];
110
+ programBody.forEach((statement, index) => {
111
+ if (statement.type === 'FunctionDeclaration') {
112
+ if (!isNamedFunctionDeclaration(statement)) {
113
+ return;
114
+ }
115
+ const name = statement.id.name;
116
+ const isExported = ASTHelpers_1.ASTHelpers.isNodeExported(statement);
117
+ functions.push({
118
+ name,
119
+ fnNode: statement,
120
+ statementNode: statement,
121
+ isExported,
122
+ isEventHandler: eventHandlerRegex.test(name),
123
+ isUtility: utilityRegex.test(name),
124
+ dependencies: [],
125
+ originalIndex: index,
126
+ });
127
+ return;
128
+ }
129
+ if (statement.type === 'ExportNamedDeclaration' && statement.declaration) {
130
+ if (statement.declaration.type === 'FunctionDeclaration') {
131
+ if (!isNamedFunctionDeclaration(statement.declaration)) {
132
+ return;
133
+ }
134
+ const name = statement.declaration.id.name;
135
+ functions.push({
136
+ name,
137
+ fnNode: statement.declaration,
138
+ statementNode: statement,
139
+ isExported: true,
140
+ isEventHandler: eventHandlerRegex.test(name),
141
+ isUtility: utilityRegex.test(name),
142
+ dependencies: [],
143
+ originalIndex: index,
144
+ });
145
+ return;
146
+ }
147
+ if (statement.declaration.type === 'VariableDeclaration') {
148
+ const extracted = extractFunctionFromVariableDeclaration(statement.declaration);
149
+ if (extracted) {
150
+ const { name, fnNode } = extracted;
151
+ functions.push({
152
+ name,
153
+ fnNode,
154
+ statementNode: statement,
155
+ isExported: true,
156
+ isEventHandler: eventHandlerRegex.test(name),
157
+ isUtility: utilityRegex.test(name),
158
+ dependencies: [],
159
+ originalIndex: index,
160
+ });
161
+ }
162
+ }
163
+ return;
164
+ }
165
+ if (statement.type === 'ExportDefaultDeclaration' &&
166
+ isFunctionExpressionLike(statement.declaration) &&
167
+ statement.declaration.id) {
168
+ const name = statement.declaration.id
169
+ .name;
170
+ functions.push({
171
+ name,
172
+ fnNode: statement.declaration,
173
+ statementNode: statement,
174
+ isExported: true,
175
+ isEventHandler: eventHandlerRegex.test(name),
176
+ isUtility: utilityRegex.test(name),
177
+ dependencies: [],
178
+ originalIndex: index,
179
+ });
180
+ return;
181
+ }
182
+ if (statement.type === 'VariableDeclaration') {
183
+ const extracted = extractFunctionFromVariableDeclaration(statement);
184
+ if (extracted) {
185
+ const { name, fnNode } = extracted;
186
+ const isExported = ASTHelpers_1.ASTHelpers.isNodeExported(statement);
187
+ functions.push({
188
+ name,
189
+ fnNode,
190
+ statementNode: statement,
191
+ isExported,
192
+ isEventHandler: eventHandlerRegex.test(name),
193
+ isUtility: utilityRegex.test(name),
194
+ dependencies: [],
195
+ originalIndex: index,
196
+ });
197
+ }
198
+ }
199
+ });
200
+ return functions;
201
+ }
202
+ function collectDependencies(fnNode, knownFunctionNames) {
203
+ const dependencies = new Set();
204
+ const visit = (node) => {
205
+ if (!node || !ASTHelpers_1.ASTHelpers.isNode(node)) {
206
+ return;
207
+ }
208
+ if ((node.type === 'CallExpression' || node.type === 'NewExpression') &&
209
+ node.callee &&
210
+ node.callee.type === 'Identifier') {
211
+ const calleeName = node.callee.name;
212
+ if (knownFunctionNames.has(calleeName)) {
213
+ dependencies.add(calleeName);
214
+ }
215
+ }
216
+ Object.values(node).forEach((value) => {
217
+ if (!value || value === node || node.parent === value) {
218
+ return;
219
+ }
220
+ if (Array.isArray(value)) {
221
+ value.forEach((child) => {
222
+ if (ASTHelpers_1.ASTHelpers.isNode(child)) {
223
+ visit(child);
224
+ }
225
+ });
226
+ }
227
+ else if (ASTHelpers_1.ASTHelpers.isNode(value)) {
228
+ visit(value);
229
+ }
230
+ });
231
+ };
232
+ visit(fnNode.body);
233
+ return [...dependencies];
234
+ }
235
+ function dependencyOrder(functions, direction) {
236
+ const dependencyMap = new Map();
237
+ const originalIndexMap = new Map(functions.map((fn) => [fn.name, fn.originalIndex]));
238
+ functions.forEach((fn) => {
239
+ dependencyMap.set(fn.name, fn.dependencies);
240
+ });
241
+ const visited = new Set();
242
+ const order = [];
243
+ const namesInOriginalOrder = functions
244
+ .slice()
245
+ .sort((a, b) => a.originalIndex - b.originalIndex)
246
+ .map((fn) => fn.name);
247
+ const incomingCount = new Map();
248
+ functions.forEach((fn) => {
249
+ if (!incomingCount.has(fn.name)) {
250
+ incomingCount.set(fn.name, 0);
251
+ }
252
+ fn.dependencies.forEach((dep) => {
253
+ incomingCount.set(dep, (incomingCount.get(dep) || 0) + 1);
254
+ });
255
+ });
256
+ const roots = namesInOriginalOrder.filter((name) => (incomingCount.get(name) || 0) === 0);
257
+ const visit = (name) => {
258
+ if (visited.has(name)) {
259
+ return;
260
+ }
261
+ visited.add(name);
262
+ const deps = dependencyMap
263
+ .get(name)
264
+ ?.slice()
265
+ .sort((a, b) => {
266
+ return ((originalIndexMap.get(a) || 0) - (originalIndexMap.get(b) || 0));
267
+ }) || [];
268
+ if (direction === 'callees-first') {
269
+ deps.forEach(visit);
270
+ order.push(name);
271
+ }
272
+ else {
273
+ order.push(name);
274
+ deps.forEach(visit);
275
+ }
276
+ };
277
+ [...roots, ...namesInOriginalOrder].forEach(visit);
278
+ return order;
279
+ }
280
+ function computeExpectedOrder(functions, options) {
281
+ const groupOrder = normalizeGroupOrder(options.groupOrder);
282
+ const dependencySequence = dependencyOrder(functions, options.dependencyDirection);
283
+ const dependencyRank = new Map(dependencySequence.map((name, idx) => [name, idx]));
284
+ const exportRank = (fn) => {
285
+ if (options.exportPlacement === 'ignore') {
286
+ return 0;
287
+ }
288
+ if (options.exportPlacement === 'top') {
289
+ return fn.isExported ? 0 : 1;
290
+ }
291
+ return fn.isExported ? 1 : 0;
292
+ };
293
+ const groupRank = (fn) => groupOrder.indexOf(classifyGroup(fn));
294
+ return functions.slice().sort((a, b) => {
295
+ return (exportRank(a) - exportRank(b) ||
296
+ groupRank(a) - groupRank(b) ||
297
+ (dependencyRank.get(a.name) ?? Number.MAX_SAFE_INTEGER) -
298
+ (dependencyRank.get(b.name) ?? Number.MAX_SAFE_INTEGER) ||
299
+ a.originalIndex - b.originalIndex);
300
+ });
301
+ }
302
+ function getStatementRangeWithComments(statement, sourceCode, consumedComments, nextStatement) {
303
+ const filterComments = (comments) => (comments || []).filter((comment) => !consumedComments || !consumedComments.has(comment));
304
+ const commentsBefore = filterComments(sourceCode.getCommentsBefore(statement) || []);
305
+ const nextLeadingComments = nextStatement
306
+ ? new Set(filterComments(sourceCode.getCommentsBefore(nextStatement) || []))
307
+ : new Set();
308
+ const trailingCandidates = filterComments(sourceCode.getCommentsAfter(statement) || []).filter((comment) => !nextLeadingComments.has(comment));
309
+ const start = commentsBefore.length > 0
310
+ ? Math.min(statement.range[0], commentsBefore[0].range[0])
311
+ : statement.range[0];
312
+ const end = trailingCandidates.length > 0
313
+ ? Math.max(statement.range[1], trailingCandidates[trailingCandidates.length - 1].range[1])
314
+ : statement.range[1];
315
+ commentsBefore.forEach((comment) => consumedComments?.add(comment));
316
+ trailingCandidates.forEach((comment) => consumedComments?.add(comment));
317
+ return [start, end];
318
+ }
319
+ exports.verticallyGroupRelatedFunctions = (0, createRule_1.createRule)({
320
+ name: 'vertically-group-related-functions',
321
+ meta: {
322
+ type: 'suggestion',
323
+ docs: {
324
+ description: 'Keep top-level functions grouped vertically so callers, exports, and helpers read top-down.',
325
+ recommended: 'error',
326
+ },
327
+ fixable: 'code',
328
+ schema: [
329
+ {
330
+ type: 'object',
331
+ properties: {
332
+ exportPlacement: {
333
+ enum: ['top', 'bottom', 'ignore'],
334
+ default: DEFAULT_OPTIONS.exportPlacement,
335
+ },
336
+ dependencyDirection: {
337
+ enum: ['callers-first', 'callees-first'],
338
+ default: DEFAULT_OPTIONS.dependencyDirection,
339
+ },
340
+ groupOrder: {
341
+ type: 'array',
342
+ items: {
343
+ enum: ['event-handlers', 'utilities', 'other'],
344
+ },
345
+ minItems: 1,
346
+ uniqueItems: true,
347
+ },
348
+ eventHandlerPattern: {
349
+ type: 'string',
350
+ },
351
+ utilityPattern: {
352
+ type: 'string',
353
+ },
354
+ },
355
+ additionalProperties: false,
356
+ },
357
+ ],
358
+ messages: {
359
+ misorderedFunction: 'Function "{{name}}" is out of order: {{reason}}. Move it {{placement}} to keep related call chains grouped so readers can scan the file top-down.',
360
+ },
361
+ },
362
+ defaultOptions: DEFAULT_OPTIONS_ARRAY,
363
+ create(context, [options]) {
364
+ const normalizedOptions = {
365
+ ...DEFAULT_OPTIONS,
366
+ ...options,
367
+ };
368
+ const warnUnsafePattern = (key, value, reason) => {
369
+ const truncatedValue = value.length > 120 ? `${value.slice(0, 117)}...` : value;
370
+ console.warn(`[vertically-group-related-functions] ${key} "${truncatedValue}" rejected: ${reason}. Falling back to safe defaults.`);
371
+ };
372
+ const eventHandlerRegex = createRegexWithFallback(options?.eventHandlerPattern, DEFAULT_OPTIONS.eventHandlerPattern, options?.eventHandlerPattern
373
+ ? (value, reason) => warnUnsafePattern('eventHandlerPattern', value, reason)
374
+ : undefined);
375
+ const utilityRegex = createRegexWithFallback(options?.utilityPattern, DEFAULT_OPTIONS.utilityPattern, options?.utilityPattern
376
+ ? (value, reason) => warnUnsafePattern('utilityPattern', value, reason)
377
+ : undefined);
378
+ return {
379
+ 'Program:exit'(node) {
380
+ const functions = collectFunctions(node.body, eventHandlerRegex, utilityRegex);
381
+ if (functions.length < 2) {
382
+ return;
383
+ }
384
+ const uniqueNames = new Set(functions.map((fn) => fn.name));
385
+ if (uniqueNames.size !== functions.length) {
386
+ return;
387
+ }
388
+ const knownNames = new Set(functions.map((fn) => fn.name));
389
+ functions.forEach((fn) => {
390
+ fn.dependencies = collectDependencies(fn.fnNode, knownNames);
391
+ });
392
+ const expectedOrderInfos = computeExpectedOrder(functions, normalizedOptions);
393
+ const expectedNames = expectedOrderInfos.map((fn) => fn.name);
394
+ const actualNames = functions.map((fn) => fn.name);
395
+ if (expectedNames.length === actualNames.length &&
396
+ expectedNames.every((name, idx) => name === actualNames[idx])) {
397
+ return;
398
+ }
399
+ const misplacedIndex = actualNames.findIndex((name, idx) => name !== expectedNames[idx]);
400
+ if (misplacedIndex === -1) {
401
+ return;
402
+ }
403
+ const misplacedInfo = functions[misplacedIndex];
404
+ const targetIndex = expectedNames.indexOf(misplacedInfo.name);
405
+ const anchorName = targetIndex > 0 ? expectedNames[targetIndex - 1] : null;
406
+ const dependencyReason = misplacedInfo.dependencies.length > 0
407
+ ? normalizedOptions.dependencyDirection === 'callees-first'
408
+ ? `it calls ${misplacedInfo.dependencies.join(', ')} and helpers should precede the callers that depend on them when dependencyDirection is "callees-first"`
409
+ : `it calls ${misplacedInfo.dependencies.join(', ')} and callers should sit above the helpers they invoke`
410
+ : 'keep related functions adjacent';
411
+ const exportReason = normalizedOptions.exportPlacement === 'ignore'
412
+ ? ''
413
+ : normalizedOptions.exportPlacement === 'top'
414
+ ? 'exports stay at the top of the file'
415
+ : 'exports stay at the bottom of the file';
416
+ const group = classifyGroup(misplacedInfo);
417
+ const groupOrder = normalizeGroupOrder(normalizedOptions.groupOrder);
418
+ const groupReason = groupOrder.indexOf(group) > 0
419
+ ? `${group.replace('-', ' ')} should follow the configured group order`
420
+ : '';
421
+ const reasons = [dependencyReason, exportReason, groupReason]
422
+ .filter(Boolean)
423
+ .join('; ');
424
+ const placement = anchorName
425
+ ? `after "${anchorName}"`
426
+ : 'at the start of the function block';
427
+ const sourceCode = context.getSourceCode();
428
+ const functionStatements = new Set(functions.map((fn) => fn.statementNode));
429
+ const sourceOrderedInfos = functions
430
+ .slice()
431
+ .sort((a, b) => a.originalIndex - b.originalIndex);
432
+ const consumedComments = new Set();
433
+ const statementRanges = new Map();
434
+ sourceOrderedInfos.forEach((info, idx) => {
435
+ const nextInfo = sourceOrderedInfos[idx + 1];
436
+ const [rangeStart, rangeEnd] = getStatementRangeWithComments(info.statementNode, sourceCode, consumedComments, nextInfo?.statementNode);
437
+ statementRanges.set(info.statementNode, [rangeStart, rangeEnd]);
438
+ });
439
+ const firstFunctionIndex = node.body.findIndex((statement) => functionStatements.has(statement));
440
+ const lastFunctionIndex = node.body.length -
441
+ 1 -
442
+ [...node.body]
443
+ .reverse()
444
+ .findIndex((statement) => functionStatements.has(statement));
445
+ context.report({
446
+ node: misplacedInfo.statementNode,
447
+ messageId: 'misorderedFunction',
448
+ data: {
449
+ name: misplacedInfo.name,
450
+ reason: reasons,
451
+ placement,
452
+ },
453
+ fix(fixer) {
454
+ if (firstFunctionIndex === -1 ||
455
+ lastFunctionIndex === -1 ||
456
+ firstFunctionIndex > lastFunctionIndex) {
457
+ return null;
458
+ }
459
+ const slice = node.body.slice(firstFunctionIndex, lastFunctionIndex + 1);
460
+ const blockContainsOnlyFunctions = slice.every((statement) => functionStatements.has(statement));
461
+ if (!blockContainsOnlyFunctions) {
462
+ return null;
463
+ }
464
+ const [start] = statementRanges.get(node.body[firstFunctionIndex]) ||
465
+ getStatementRangeWithComments(node.body[firstFunctionIndex], sourceCode);
466
+ const [, end] = statementRanges.get(node.body[lastFunctionIndex]) ||
467
+ getStatementRangeWithComments(node.body[lastFunctionIndex], sourceCode);
468
+ const textParts = expectedOrderInfos.map((fn) => {
469
+ const [fnStart, fnEnd] = statementRanges.get(fn.statementNode) ||
470
+ getStatementRangeWithComments(fn.statementNode, sourceCode);
471
+ return sourceCode.text.slice(fnStart, fnEnd);
472
+ });
473
+ return fixer.replaceTextRange([start, end], textParts.join('\n\n'));
474
+ },
475
+ });
476
+ },
477
+ };
478
+ },
479
+ });
480
+ //# sourceMappingURL=vertically-group-related-functions.js.map
@@ -1,6 +1,10 @@
1
- import { TSESTree } from '@typescript-eslint/utils';
1
+ import { TSESLint, TSESTree } from '@typescript-eslint/utils';
2
2
  import { Graph } from './graph/ClassGraphBuilder';
3
3
  export declare class ASTHelpers {
4
+ /**
5
+ * Finds a variable by name in the scope chain starting from the given scope.
6
+ */
7
+ static findVariableInScope(scope: TSESLint.Scope.Scope, name: string): TSESLint.Scope.Variable | null;
4
8
  static blockIncludesIdentifier(block: TSESTree.BlockStatement): boolean;
5
9
  static declarationIncludesIdentifier(node: TSESTree.Node | null): boolean;
6
10
  static classMethodDependenciesOf(node: TSESTree.Node | null, graph: Graph, className: string): string[];
@@ -9,4 +13,12 @@ export declare class ASTHelpers {
9
13
  static isNodeExported(node: TSESTree.Node): boolean;
10
14
  static returnsJSX(node: TSESTree.Node): boolean;
11
15
  static hasParameters(node: TSESTree.ArrowFunctionExpression | TSESTree.FunctionExpression | TSESTree.FunctionDeclaration): boolean;
16
+ /**
17
+ * Helper to get ancestors of a node in a way that is compatible with both ESLint v8 and v9.
18
+ * In ESLint v9, context.getAncestors() is deprecated and moved to context.sourceCode.getAncestors(node).
19
+ */
20
+ static getAncestors(context: {
21
+ sourceCode?: unknown;
22
+ getAncestors?: () => TSESTree.Node[];
23
+ }, node: TSESTree.Node): TSESTree.Node[];
12
24
  }
@@ -2,6 +2,21 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.ASTHelpers = void 0;
4
4
  class ASTHelpers {
5
+ /**
6
+ * Finds a variable by name in the scope chain starting from the given scope.
7
+ */
8
+ static findVariableInScope(scope, name) {
9
+ let current = scope;
10
+ while (current) {
11
+ const variable = current.set?.get(name) ??
12
+ current.variables.find((v) => v.name === name);
13
+ if (variable) {
14
+ return variable;
15
+ }
16
+ current = current.upper;
17
+ }
18
+ return null;
19
+ }
5
20
  static blockIncludesIdentifier(block) {
6
21
  for (const statement of block.body) {
7
22
  if (ASTHelpers.declarationIncludesIdentifier(statement)) {
@@ -364,6 +379,15 @@ class ASTHelpers {
364
379
  static hasParameters(node) {
365
380
  return node.params && node.params.length > 0;
366
381
  }
382
+ /**
383
+ * Helper to get ancestors of a node in a way that is compatible with both ESLint v8 and v9.
384
+ * In ESLint v9, context.getAncestors() is deprecated and moved to context.sourceCode.getAncestors(node).
385
+ */
386
+ static getAncestors(context, node) {
387
+ const sourceCode = context.sourceCode;
388
+ return (sourceCode?.getAncestors?.(node) ??
389
+ (context.getAncestors ? context.getAncestors() : []));
390
+ }
367
391
  }
368
392
  exports.ASTHelpers = ASTHelpers;
369
393
  //# sourceMappingURL=ASTHelpers.js.map
@@ -0,0 +1,27 @@
1
+ import { TSESTree } from '@typescript-eslint/utils';
2
+ /**
3
+ * Extracts a stable, human-readable name from method/property/member nodes.
4
+ * Use the options to align prefixing and computed-key handling with rule semantics.
5
+ */
6
+ export type GetMethodNameOptions = {
7
+ /**
8
+ * Prefix applied to private identifiers:
9
+ * - undefined (default): empty prefix
10
+ * - null: return the raw name without any prefix
11
+ * - string (e.g., '#'): prepend the given prefix
12
+ */
13
+ privateIdentifierPrefix?: string | null;
14
+ /**
15
+ * When true (default), computed keys fall back to source text; when false, returns empty string.
16
+ */
17
+ computedFallbackToText?: boolean;
18
+ };
19
+ export declare const getMethodName: (method: TSESTree.MethodDefinition | TSESTree.TSAbstractMethodDefinition | TSESTree.PropertyDefinition, sourceCode: Readonly<{
20
+ getText: (node: TSESTree.Node) => string;
21
+ }>, options?: GetMethodNameOptions) => string;
22
+ export declare const getPropertyName: (key: TSESTree.PropertyName | TSESTree.PrivateIdentifier, sourceCode: Readonly<{
23
+ getText: (node: TSESTree.Node) => string;
24
+ }>, options?: GetMethodNameOptions) => string;
25
+ export declare const getMemberExpressionName: (member: TSESTree.MemberExpression, sourceCode: Readonly<{
26
+ getText: (node: TSESTree.Node) => string;
27
+ }>, options?: GetMethodNameOptions) => string;
@@ -0,0 +1,35 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.getMemberExpressionName = exports.getPropertyName = exports.getMethodName = void 0;
4
+ const utils_1 = require("@typescript-eslint/utils");
5
+ const getKeyName = (key, sourceCode, { privateIdentifierPrefix = '', computedFallbackToText = true, }) => {
6
+ if (key.type === utils_1.AST_NODE_TYPES.Identifier) {
7
+ return key.name;
8
+ }
9
+ if (key.type === utils_1.AST_NODE_TYPES.PrivateIdentifier) {
10
+ if (privateIdentifierPrefix === null) {
11
+ return key.name;
12
+ }
13
+ return `${privateIdentifierPrefix}${key.name}`;
14
+ }
15
+ if (key.type === utils_1.AST_NODE_TYPES.Literal) {
16
+ if (key.value === null || key.value === undefined) {
17
+ return '';
18
+ }
19
+ return String(key.value);
20
+ }
21
+ return computedFallbackToText ? sourceCode.getText(key) : '';
22
+ };
23
+ const getMethodName = (method, sourceCode, options = {}) => {
24
+ return getKeyName(method.key, sourceCode, options);
25
+ };
26
+ exports.getMethodName = getMethodName;
27
+ const getPropertyName = (key, sourceCode, options = {}) => {
28
+ return getKeyName(key, sourceCode, options);
29
+ };
30
+ exports.getPropertyName = getPropertyName;
31
+ const getMemberExpressionName = (member, sourceCode, options = {}) => {
32
+ return getKeyName(member.property, sourceCode, options);
33
+ };
34
+ exports.getMemberExpressionName = getMemberExpressionName;
35
+ //# sourceMappingURL=getMethodName.js.map
@@ -0,0 +1,30 @@
1
+ import { TSESTree, ParserServices } from '@typescript-eslint/utils';
2
+ import * as ts from 'typescript';
3
+ /**
4
+ * Re-exporting the typescript namespace type to avoid TS4078.
5
+ */
6
+ export type TypeScriptModule = typeof ts;
7
+ export type ClassifierOptions = {
8
+ ignoreSymbol?: boolean;
9
+ };
10
+ export type ClassificationResult = {
11
+ status: 'primitive' | 'non-primitive' | 'unknown';
12
+ kind: string;
13
+ };
14
+ /**
15
+ * Describes the kind of a primitive type for error messages.
16
+ */
17
+ export declare function describeTypeKind(t: ts.Type, tsModule: TypeScriptModule, options: ClassifierOptions): string;
18
+ /**
19
+ * Classifies a TypeScript type as primitive, non-primitive, or unknown.
20
+ */
21
+ export declare function classifyType(t: ts.Type, tsModule: TypeScriptModule, options: ClassifierOptions): 'primitive' | 'non-primitive' | 'unknown';
22
+ /**
23
+ * Classifies the type of an expression using TypeScript's type checker.
24
+ */
25
+ export declare function classifyExpressionType(expr: TSESTree.Expression, { checker, tsModule, parserServices, options, }: {
26
+ checker: ts.TypeChecker;
27
+ tsModule: TypeScriptModule;
28
+ parserServices: ParserServices;
29
+ options: ClassifierOptions;
30
+ }): ClassificationResult;