@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,365 @@
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.enforceUniqueCursorHeaders = exports.RULE_NAME = void 0;
7
+ const path_1 = __importDefault(require("path"));
8
+ const minimatch_1 = require("minimatch");
9
+ const createRule_1 = require("../utils/createRule");
10
+ const DEFAULT_OPTIONS = {
11
+ requiredPatterns: ['**/*.{ts,tsx,js,jsx}'],
12
+ excludedPatterns: [
13
+ '**/*.d.ts',
14
+ '**/dist/**',
15
+ '**/build/**',
16
+ '**/coverage/**',
17
+ ],
18
+ requiredTags: ['@fileoverview'],
19
+ allowSplitHeaders: true,
20
+ headerTemplate: null,
21
+ ignoreGeneratedFiles: true,
22
+ generatedMarkers: [
23
+ '@generated',
24
+ 'auto-generated',
25
+ 'autogenerated',
26
+ 'do not edit',
27
+ 'do not edit manually',
28
+ ],
29
+ excludedAtDirectives: [
30
+ '@ts-nocheck',
31
+ '@ts-check',
32
+ '@ts-ignore',
33
+ '@ts-expect-error',
34
+ '@jsx',
35
+ '@jsxRuntime',
36
+ '@jsxImportSource',
37
+ '@format',
38
+ '@flow',
39
+ '@jest-environment',
40
+ '@vitest-environment',
41
+ ],
42
+ };
43
+ /**
44
+ * Maximum number of characters to scan at the beginning of a file to detect generated markers.
45
+ */
46
+ const GENERATED_MARKER_SCAN_LENGTH = 500;
47
+ const normalizeArrayOption = (values, fallback) => values && values.length > 0 ? values : fallback;
48
+ const normalizeArrayOptionAllowEmpty = (values, fallback) => (values === undefined ? fallback : values);
49
+ const normalizeBooleanOption = (value, fallback) => value ?? fallback;
50
+ const normalizeNullableOption = (value, fallback) => (value === undefined ? fallback : value);
51
+ const normalizeOptions = (options) => ({
52
+ requiredPatterns: normalizeArrayOption(options?.requiredPatterns, DEFAULT_OPTIONS.requiredPatterns),
53
+ excludedPatterns: normalizeArrayOptionAllowEmpty(options?.excludedPatterns, DEFAULT_OPTIONS.excludedPatterns),
54
+ requiredTags: normalizeArrayOption(options?.requiredTags, DEFAULT_OPTIONS.requiredTags),
55
+ allowSplitHeaders: normalizeBooleanOption(options?.allowSplitHeaders, DEFAULT_OPTIONS.allowSplitHeaders),
56
+ headerTemplate: normalizeNullableOption(options?.headerTemplate, DEFAULT_OPTIONS.headerTemplate),
57
+ ignoreGeneratedFiles: normalizeBooleanOption(options?.ignoreGeneratedFiles, DEFAULT_OPTIONS.ignoreGeneratedFiles),
58
+ generatedMarkers: normalizeArrayOptionAllowEmpty(options?.generatedMarkers, DEFAULT_OPTIONS.generatedMarkers),
59
+ excludedAtDirectives: normalizeArrayOptionAllowEmpty(options?.excludedAtDirectives, DEFAULT_OPTIONS.excludedAtDirectives).map((directive) => directive.toLowerCase()),
60
+ });
61
+ const normalizeCommentValue = (comment) => comment.value
62
+ .split('\n')
63
+ .map((line) => line.replace(/^\s*\*\s?/, '').trim())
64
+ .join('\n')
65
+ .trim();
66
+ const hasRequiredTags = (text, requiredTagsLower) => {
67
+ const lowerText = text.toLowerCase();
68
+ return requiredTagsLower.some((tag) => lowerText.includes(tag));
69
+ };
70
+ const hasAllRequiredTags = (text, requiredTagsLower) => {
71
+ const lowerText = text.toLowerCase();
72
+ return requiredTagsLower.every((tag) => lowerText.includes(tag));
73
+ };
74
+ const hasHeaderMarker = (text, excludedAtDirectives) => text.split('\n').some((line) => {
75
+ const trimmed = line.trimStart();
76
+ if (!trimmed.startsWith('@')) {
77
+ return false;
78
+ }
79
+ const marker = trimmed.split(/\s+/u)[0].toLowerCase();
80
+ return !excludedAtDirectives.has(marker);
81
+ });
82
+ const isHeaderCandidate = (text, requiredTagsLower, excludedAtDirectives) => hasRequiredTags(text, requiredTagsLower) ||
83
+ hasHeaderMarker(text, excludedAtDirectives);
84
+ const isCommentAdjacent = (nextComment, lastComment) => nextComment.loc.start.line <= lastComment.loc.end.line + 1;
85
+ const shouldStopMerging = (groupedHasAllTags, nextHasAllTags) => groupedHasAllTags && nextHasAllTags;
86
+ const updateMergedState = (mergedComments, mergedText, nextComment, nextNormalized, requiredTagsLower) => {
87
+ const updatedText = `${mergedText}\n${nextNormalized}`.trim();
88
+ const updatedComments = [...mergedComments, nextComment];
89
+ const updatedHasAllTags = hasAllRequiredTags(updatedText, requiredTagsLower);
90
+ return {
91
+ mergedComments: updatedComments,
92
+ mergedText: updatedText,
93
+ groupedHasAllTags: updatedHasAllTags,
94
+ };
95
+ };
96
+ /**
97
+ * Merges adjacent header candidate comments into a single group if they are not already complete headers.
98
+ * This handles "split headers" where metadata is spread across multiple comment blocks.
99
+ */
100
+ const mergeAdjacentHeaderCandidates = (comments, startIndex, initialComment, initialText, requiredTagsLower, excludedAtDirectives) => {
101
+ let mergedComments = [initialComment];
102
+ let mergedText = initialText;
103
+ let groupedHasAllTags = hasAllRequiredTags(mergedText, requiredTagsLower);
104
+ let nextIndex = startIndex + 1;
105
+ while (nextIndex < comments.length) {
106
+ const nextComment = comments[nextIndex];
107
+ const lastComment = mergedComments[mergedComments.length - 1];
108
+ if (!isCommentAdjacent(nextComment, lastComment)) {
109
+ break;
110
+ }
111
+ const nextNormalized = normalizeCommentValue(nextComment);
112
+ if (!isHeaderCandidate(nextNormalized, requiredTagsLower, excludedAtDirectives)) {
113
+ break;
114
+ }
115
+ const nextHasAllTags = hasAllRequiredTags(nextNormalized, requiredTagsLower);
116
+ if (shouldStopMerging(groupedHasAllTags, nextHasAllTags)) {
117
+ break;
118
+ }
119
+ const updatedState = updateMergedState(mergedComments, mergedText, nextComment, nextNormalized, requiredTagsLower);
120
+ mergedComments = updatedState.mergedComments;
121
+ mergedText = updatedState.mergedText;
122
+ groupedHasAllTags = updatedState.groupedHasAllTags;
123
+ nextIndex += 1;
124
+ }
125
+ return {
126
+ mergedComments,
127
+ mergedText,
128
+ lastIndex: nextIndex - 1,
129
+ };
130
+ };
131
+ const collectHeaderGroups = (comments, options, excludedAtDirectives) => {
132
+ const requiredTagsLower = options.requiredTags.map((tag) => tag.toLowerCase());
133
+ const groups = [];
134
+ for (let index = 0; index < comments.length; index += 1) {
135
+ const comment = comments[index];
136
+ const normalized = normalizeCommentValue(comment);
137
+ if (!isHeaderCandidate(normalized, requiredTagsLower, excludedAtDirectives)) {
138
+ continue;
139
+ }
140
+ let groupedComments = [comment];
141
+ let groupedText = normalized;
142
+ if (options.allowSplitHeaders) {
143
+ const merged = mergeAdjacentHeaderCandidates(comments, index, comment, normalized, requiredTagsLower, excludedAtDirectives);
144
+ groupedComments = merged.mergedComments;
145
+ groupedText = merged.mergedText;
146
+ index = merged.lastIndex;
147
+ }
148
+ groups.push({ comments: groupedComments, text: groupedText });
149
+ }
150
+ return groups;
151
+ };
152
+ const expandRangeToFullLines = (range, sourceText) => {
153
+ let [start, end] = range;
154
+ while (start > 0 && sourceText[start - 1] !== '\n') {
155
+ start -= 1;
156
+ }
157
+ while (end < sourceText.length && sourceText[end] !== '\n') {
158
+ end += 1;
159
+ }
160
+ if (end < sourceText.length) {
161
+ end += 1;
162
+ }
163
+ return [start, end];
164
+ };
165
+ // Preserve author-provided trailing newlines while guaranteeing a blank line after the header.
166
+ const buildHeaderInsertionText = (template) => {
167
+ const normalizedWhitespace = template
168
+ .replace(/[ \t]+(\r?\n)/gu, '$1')
169
+ .replace(/[ \t]+$/u, '');
170
+ const trailingNewlines = normalizedWhitespace.match(/\n+$/u)?.[0].length ?? 0;
171
+ const requiredNewlines = trailingNewlines >= 2 ? 0 : 2 - trailingNewlines;
172
+ return `${normalizedWhitespace}${'\n'.repeat(requiredNewlines)}`;
173
+ };
174
+ const shouldSkipGeneratedFile = (sourceText, options) => {
175
+ if (!options.ignoreGeneratedFiles) {
176
+ return false;
177
+ }
178
+ const prefix = sourceText
179
+ .slice(0, GENERATED_MARKER_SCAN_LENGTH)
180
+ .toLowerCase();
181
+ return options.generatedMarkers.some((marker) => prefix.includes(marker.toLowerCase()));
182
+ };
183
+ const getTopComments = (sourceCode, node) => {
184
+ const allComments = sourceCode.getAllComments();
185
+ const firstToken = sourceCode.getFirstToken(node);
186
+ const firstTokenLine = firstToken?.loc.start.line ?? Infinity;
187
+ return allComments.filter((comment) => comment.loc.start.line < firstTokenLine);
188
+ };
189
+ const analyzeHeaderGroups = (candidateGroups, options) => {
190
+ const requiredTagsLower = options.requiredTags.map((tag) => tag.toLowerCase());
191
+ const headerGroups = candidateGroups.filter((group) => hasAllRequiredTags(group.text, requiredTagsLower));
192
+ const primaryHeader = headerGroups[0];
193
+ const duplicateGroups = headerGroups.slice(1);
194
+ const splitHeaderGroups = options.allowSplitHeaders
195
+ ? []
196
+ : candidateGroups.filter((group) => {
197
+ if (group === primaryHeader || !primaryHeader) {
198
+ return false;
199
+ }
200
+ const groupStart = group.comments[0].loc.start.line;
201
+ const groupEnd = group.comments[group.comments.length - 1].loc.end.line;
202
+ const primaryStart = primaryHeader.comments[0].loc.start.line;
203
+ const primaryEnd = primaryHeader.comments[primaryHeader.comments.length - 1].loc.end
204
+ .line;
205
+ /**
206
+ * Check if group is adjacent to primary (before or after).
207
+ * The +1 allows for touching lines (no blank line in between).
208
+ */
209
+ const isAdjacentToPrimary = groupStart <= primaryEnd + 1 && groupEnd + 1 >= primaryStart;
210
+ const hasAllTags = hasAllRequiredTags(group.text, requiredTagsLower);
211
+ return isAdjacentToPrimary && !hasAllTags;
212
+ });
213
+ return { primaryHeader, duplicateGroups, splitHeaderGroups };
214
+ };
215
+ const computeHeaderInsertion = (sourceText, headerTemplate) => {
216
+ const headerText = buildHeaderInsertionText(headerTemplate);
217
+ if (!sourceText.startsWith('#!')) {
218
+ return { index: 0, text: headerText };
219
+ }
220
+ const shebangNewlineIndex = sourceText.indexOf('\n');
221
+ if (shebangNewlineIndex === -1) {
222
+ /** Shebang exists but file has no newline - append after shebang */
223
+ return { index: sourceText.length, text: `\n${headerText}` };
224
+ }
225
+ /** Insert after shebang line */
226
+ return { index: shebangNewlineIndex + 1, text: headerText };
227
+ };
228
+ const reportMissingHeader = (context, fileName, sourceText, options) => {
229
+ const template = options.headerTemplate;
230
+ context.report({
231
+ loc: { line: 1, column: 0 },
232
+ messageId: 'missingHeader',
233
+ data: {
234
+ fileName: path_1.default.basename(fileName),
235
+ tags: options.requiredTags.join(', '),
236
+ },
237
+ fix: template !== null
238
+ ? (fixer) => {
239
+ const { index, text } = computeHeaderInsertion(sourceText, template);
240
+ return fixer.insertTextBeforeRange([index, index], text);
241
+ }
242
+ : null,
243
+ });
244
+ };
245
+ const reportDuplicates = (context, duplicateGroups, sourceText) => {
246
+ duplicateGroups.forEach((group) => {
247
+ const firstComment = group.comments[0];
248
+ const removalRange = expandRangeToFullLines([
249
+ firstComment.range[0],
250
+ group.comments[group.comments.length - 1].range[1],
251
+ ], sourceText);
252
+ context.report({
253
+ loc: firstComment.loc,
254
+ messageId: 'duplicateHeader',
255
+ data: {
256
+ line: firstComment.loc.start.line.toString(),
257
+ },
258
+ fix: (fixer) => fixer.removeRange(removalRange),
259
+ });
260
+ });
261
+ };
262
+ const reportSplitFragments = (context, splitHeaderGroups, options) => {
263
+ splitHeaderGroups.forEach((group) => {
264
+ const firstComment = group.comments[0];
265
+ context.report({
266
+ loc: firstComment.loc,
267
+ messageId: 'splitHeaderFragment',
268
+ data: {
269
+ tags: options.requiredTags.join(', '),
270
+ },
271
+ fix: null,
272
+ });
273
+ });
274
+ };
275
+ exports.RULE_NAME = 'enforce-unique-cursor-headers';
276
+ exports.enforceUniqueCursorHeaders = (0, createRule_1.createRule)({
277
+ name: exports.RULE_NAME,
278
+ meta: {
279
+ type: 'suggestion',
280
+ docs: {
281
+ description: 'Ensure files have exactly one cursor header containing required tags (e.g., @fileoverview) before any code',
282
+ recommended: 'error',
283
+ },
284
+ fixable: 'code',
285
+ schema: [
286
+ {
287
+ type: 'object',
288
+ properties: {
289
+ requiredPatterns: {
290
+ type: 'array',
291
+ items: { type: 'string' },
292
+ },
293
+ excludedPatterns: {
294
+ type: 'array',
295
+ items: { type: 'string' },
296
+ },
297
+ requiredTags: {
298
+ type: 'array',
299
+ items: { type: 'string' },
300
+ },
301
+ allowSplitHeaders: {
302
+ type: 'boolean',
303
+ },
304
+ headerTemplate: {
305
+ type: ['string', 'null'],
306
+ },
307
+ ignoreGeneratedFiles: {
308
+ type: 'boolean',
309
+ },
310
+ generatedMarkers: {
311
+ type: 'array',
312
+ items: { type: 'string' },
313
+ },
314
+ excludedAtDirectives: {
315
+ type: 'array',
316
+ items: { type: 'string' },
317
+ },
318
+ },
319
+ additionalProperties: false,
320
+ },
321
+ ],
322
+ messages: {
323
+ missingHeader: 'File "{{fileName}}" is missing a cursor header before any imports or code → Cursor headers keep ownership and metadata discoverable for automation → Add one top-of-file cursor header containing: {{tags}}.',
324
+ duplicateHeader: 'Multiple cursor headers detected before the first statement (extra header starts on line {{line}}) → Duplicate metadata blocks conflict and make ownership ambiguous → Keep exactly one cursor header at the top and remove the extra block.',
325
+ splitHeaderFragment: 'Cursor header metadata is split across adjacent comment blocks → Fragmented headers are easy to miss and let required tags drift out of sync → Merge the fragments into a single top-of-file header containing: {{tags}}.',
326
+ },
327
+ },
328
+ defaultOptions: [DEFAULT_OPTIONS],
329
+ create(context, [userOptions]) {
330
+ const options = normalizeOptions(userOptions);
331
+ const fileName = context.getFilename();
332
+ const matchPath = fileName.split(path_1.default.sep).join('/');
333
+ const excludedAtDirectives = new Set(options.excludedAtDirectives);
334
+ if (fileName === '<input>') {
335
+ return {};
336
+ }
337
+ const matchesRequired = options.requiredPatterns.some((pattern) => (0, minimatch_1.minimatch)(matchPath, pattern, { dot: true }));
338
+ if (!matchesRequired) {
339
+ return {};
340
+ }
341
+ const matchesExcluded = options.excludedPatterns.some((pattern) => (0, minimatch_1.minimatch)(matchPath, pattern, { dot: true }));
342
+ if (matchesExcluded) {
343
+ return {};
344
+ }
345
+ return {
346
+ Program(node) {
347
+ const sourceCode = context.getSourceCode();
348
+ const sourceText = sourceCode.getText();
349
+ if (shouldSkipGeneratedFile(sourceText, options)) {
350
+ return;
351
+ }
352
+ const topComments = getTopComments(sourceCode, node);
353
+ const candidateGroups = collectHeaderGroups(topComments, options, excludedAtDirectives);
354
+ const { primaryHeader, duplicateGroups, splitHeaderGroups } = analyzeHeaderGroups(candidateGroups, options);
355
+ if (!primaryHeader) {
356
+ reportMissingHeader(context, fileName, sourceText, options);
357
+ return;
358
+ }
359
+ reportDuplicates(context, duplicateGroups, sourceText);
360
+ reportSplitFragments(context, splitHeaderGroups, options);
361
+ },
362
+ };
363
+ },
364
+ });
365
+ //# sourceMappingURL=enforce-unique-cursor-headers.js.map
@@ -4537,7 +4537,7 @@ exports.enforceVerbNounNaming = (0, createRule_1.createRule)({
4537
4537
  },
4538
4538
  schema: [],
4539
4539
  messages: {
4540
- functionVerbPhrase: 'Function names should start with a verb phrase (e.g., fetchData, processRequest)',
4540
+ functionVerbPhrase: 'Function "{{name}}" should start with an action verb followed by the thing it acts on. Verb-first names tell readers this symbol performs work instead of representing data, which keeps APIs predictable and prevents accidental misuse. Rename "{{name}}" to a verb-noun phrase such as "fetchUsers" or "processRequest".',
4541
4541
  },
4542
4542
  },
4543
4543
  defaultOptions: [],
@@ -4718,9 +4718,7 @@ exports.enforceVerbNounNaming = (0, createRule_1.createRule)({
4718
4718
  const id = parent.id;
4719
4719
  if (id.type === utils_1.AST_NODE_TYPES.Identifier &&
4720
4720
  id.typeAnnotation?.type === utils_1.AST_NODE_TYPES.TSTypeAnnotation) {
4721
- const typeText = context
4722
- .getSourceCode()
4723
- .getText(id.typeAnnotation.typeAnnotation);
4721
+ const typeText = context.sourceCode.getText(id.typeAnnotation.typeAnnotation);
4724
4722
  return (typeText.includes('React.') ||
4725
4723
  typeText.includes('FC') ||
4726
4724
  typeText.includes('FunctionComponent'));
@@ -4755,6 +4753,7 @@ exports.enforceVerbNounNaming = (0, createRule_1.createRule)({
4755
4753
  context.report({
4756
4754
  node: node.id,
4757
4755
  messageId: 'functionVerbPhrase',
4756
+ data: { name: node.id.name },
4758
4757
  });
4759
4758
  }
4760
4759
  },
@@ -4771,6 +4770,7 @@ exports.enforceVerbNounNaming = (0, createRule_1.createRule)({
4771
4770
  context.report({
4772
4771
  node: node.id,
4773
4772
  messageId: 'functionVerbPhrase',
4773
+ data: { name: node.id.name },
4774
4774
  });
4775
4775
  }
4776
4776
  }
@@ -4788,6 +4788,7 @@ exports.enforceVerbNounNaming = (0, createRule_1.createRule)({
4788
4788
  context.report({
4789
4789
  node: node.key,
4790
4790
  messageId: 'functionVerbPhrase',
4791
+ data: { name: node.key.name },
4791
4792
  });
4792
4793
  }
4793
4794
  },
@@ -23,6 +23,24 @@ function isAbsoluteOrFixedPosition(propertyName, propertyValue) {
23
23
  function isPointerEventsProperty(propertyName) {
24
24
  return propertyName === 'pointerEvents' || propertyName === 'pointer-events';
25
25
  }
26
+ function formatSelector(selector) {
27
+ if (!selector)
28
+ return 'pseudo-element';
29
+ const trimmedSelector = selector.trim();
30
+ const candidates = trimmedSelector
31
+ .split(',')
32
+ .map((part) => part.trim())
33
+ .filter(Boolean);
34
+ for (const candidate of candidates) {
35
+ const match = candidate.match(/::?(before|after)\b/i);
36
+ if (match)
37
+ return `::${match[1].toLowerCase()}`;
38
+ }
39
+ if (trimmedSelector.length === 0)
40
+ return 'pseudo-element';
41
+ const snippet = trimmedSelector.slice(0, 40);
42
+ return trimmedSelector.length > 40 ? `${snippet}...` : trimmedSelector;
43
+ }
26
44
  exports.ensurePointerEventsNone = (0, createRule_1.createRule)({
27
45
  name: 'ensure-pointer-events-none',
28
46
  meta: {
@@ -34,7 +52,9 @@ exports.ensurePointerEventsNone = (0, createRule_1.createRule)({
34
52
  fixable: 'code',
35
53
  schema: [],
36
54
  messages: {
37
- missingPointerEventsNone: 'Pseudo-elements (::before, ::after) with position: absolute or fixed should have pointer-events: none to prevent blocking interactions with underlying elements',
55
+ missingPointerEventsNone: 'What\'s wrong: pseudo-element "{{selector}}" uses absolute or fixed positioning without pointer-events: none. ' +
56
+ 'Why it matters: positioned overlays capture clicks, hover, and focus, blocking the underlying control and harming accessibility. ' +
57
+ 'How to fix: add pointer-events: none so the pseudo-element stays decorative and does not intercept interactions.',
38
58
  },
39
59
  },
40
60
  defaultOptions: [],
@@ -104,9 +124,12 @@ exports.ensurePointerEventsNone = (0, createRule_1.createRule)({
104
124
  context.report({
105
125
  node,
106
126
  messageId: 'missingPointerEventsNone',
127
+ data: {
128
+ selector: formatSelector(selector),
129
+ },
107
130
  fix(fixer) {
108
131
  // Find the last property in the object
109
- const sourceCode = context.getSourceCode();
132
+ const sourceCode = context.sourceCode;
110
133
  const properties = node.properties;
111
134
  if (properties.length === 0)
112
135
  return null;
@@ -155,6 +178,9 @@ exports.ensurePointerEventsNone = (0, createRule_1.createRule)({
155
178
  context.report({
156
179
  node,
157
180
  messageId: 'missingPointerEventsNone',
181
+ data: {
182
+ selector: formatSelector(template),
183
+ },
158
184
  });
159
185
  }
160
186
  }
@@ -8,20 +8,31 @@ exports.exportIfInDoubt = (0, createRule_1.createRule)({
8
8
  meta: {
9
9
  type: 'suggestion',
10
10
  docs: {
11
- description: 'All top-level const definitions, type definitions, and functions should be exported',
11
+ description: 'All top-level variable declarations, type definitions, and functions should be exported',
12
12
  recommended: 'error',
13
13
  },
14
14
  schema: [],
15
15
  messages: {
16
- exportIfInDoubt: 'Top-level const definitions, type definitions, and functions should be exported.',
16
+ exportIfInDoubt: 'What\'s wrong: Top-level {{kind}} "{{name}}" is not exported. Why it matters: Top-level declarations define your module\'s public API; leaving this unexported makes it unusable from other files and hides reusable utilities (often resulting in dead code or duplicated implementations). How to fix: Export it (for example: {{exportExample}}) or move it into a narrower scope if it is intentionally private.',
17
17
  },
18
18
  },
19
19
  defaultOptions: [],
20
20
  create(context) {
21
- // List of top-level declarations
22
- // List of exported identifiers
23
21
  const topLevelDeclarations = [];
24
22
  const exportedIdentifiers = [];
23
+ const getDeclarationKind = (node) => {
24
+ if (node.type === 'VariableDeclarator') {
25
+ const variableParent = node.parent;
26
+ if (variableParent?.type === 'VariableDeclaration') {
27
+ return variableParent.kind;
28
+ }
29
+ return 'const';
30
+ }
31
+ if (node.type === 'FunctionDeclaration') {
32
+ return 'function';
33
+ }
34
+ return 'type';
35
+ };
25
36
  return {
26
37
  'Program > VariableDeclaration > VariableDeclarator, Program > FunctionDeclaration, Program > TSTypeAliasDeclaration'(node) {
27
38
  topLevelDeclarations.push(node);
@@ -55,9 +66,21 @@ exports.exportIfInDoubt = (0, createRule_1.createRule)({
55
66
  node.type === 'TSTypeAliasDeclaration') &&
56
67
  node.id.type === 'Identifier' &&
57
68
  !exportedIdentifiers.includes(node.id.name)) {
69
+ const name = node.id.name;
70
+ const kind = getDeclarationKind(node);
71
+ const exportExample = node.type === 'VariableDeclarator'
72
+ ? `export ${kind} ${name} = undefined;`
73
+ : node.type === 'FunctionDeclaration'
74
+ ? `export function ${name}() {}`
75
+ : `export type ${name} = unknown;`;
58
76
  context.report({
59
77
  node,
60
78
  messageId: 'exportIfInDoubt',
79
+ data: {
80
+ name,
81
+ kind,
82
+ exportExample,
83
+ },
61
84
  });
62
85
  }
63
86
  });
@@ -124,6 +124,19 @@ function isAsConstExpression(node) {
124
124
  }
125
125
  return false;
126
126
  }
127
+ function analyzeDeclarator(declaration) {
128
+ const init = declaration.init ?? null;
129
+ const isFunctionOrMutable = isFunctionDefinition(init) || isMutableValue(init);
130
+ return {
131
+ isFunctionOrMutable,
132
+ hasDependencies: !isFunctionOrMutable && init
133
+ ? ASTHelpers_1.ASTHelpers.declarationIncludesIdentifier(init)
134
+ : false,
135
+ hasAsConstAssertion: !isFunctionOrMutable && init ? isAsConstExpression(init) : false,
136
+ hasReportableIdentifier: declaration.id?.type === utils_1.AST_NODE_TYPES.Identifier &&
137
+ !isFunctionOrMutable,
138
+ };
139
+ }
127
140
  exports.extractGlobalConstants = (0, createRule_1.createRule)({
128
141
  create(context) {
129
142
  return {
@@ -131,27 +144,41 @@ exports.extractGlobalConstants = (0, createRule_1.createRule)({
131
144
  if (node.kind !== 'const') {
132
145
  return;
133
146
  }
134
- // Skip if any of the declarations are function definitions or mutable values
135
- if (node.declarations.some((d) => isFunctionDefinition(d.init) || isMutableValue(d.init))) {
147
+ const declarations = node.declarations.filter((declaration) => declaration !== null && declaration !== undefined);
148
+ if (declarations.length === 0) {
149
+ return;
150
+ }
151
+ let hasFunctionOrMutableValue = false;
152
+ let hasDependencies = false;
153
+ let hasAsConstAssertion = false;
154
+ let hasReportableIdentifierDeclaration = false;
155
+ for (const declaration of declarations) {
156
+ const analysis = analyzeDeclarator(declaration);
157
+ if (analysis.isFunctionOrMutable) {
158
+ hasFunctionOrMutableValue = true;
159
+ break;
160
+ }
161
+ hasDependencies ||= analysis.hasDependencies;
162
+ hasAsConstAssertion ||= analysis.hasAsConstAssertion;
163
+ hasReportableIdentifierDeclaration ||=
164
+ analysis.hasReportableIdentifier;
165
+ }
166
+ if (hasFunctionOrMutableValue) {
136
167
  return;
137
168
  }
138
169
  const scope = context.getScope();
139
- const hasDependencies = node.declarations.some((declaration) => declaration.init &&
140
- ASTHelpers_1.ASTHelpers.declarationIncludesIdentifier(declaration.init));
141
- // Skip constants with 'as const' type assertions used in loops
142
- const hasAsConstAssertion = node.declarations.some((declaration) => declaration.init && isAsConstExpression(declaration.init));
143
170
  // Only check function/block scoped constants without dependencies
144
171
  if (!hasDependencies &&
145
172
  !hasAsConstAssertion &&
146
173
  (scope.type === 'function' || scope.type === 'block') &&
147
174
  isInsideFunction(node) &&
148
- node.declarations.some((d) => d.id.type === 'Identifier')) {
149
- for (const d of node.declarations) {
150
- if (d.id.type !== 'Identifier')
175
+ hasReportableIdentifierDeclaration) {
176
+ for (const declaration of declarations) {
177
+ if (declaration.id?.type !== utils_1.AST_NODE_TYPES.Identifier)
151
178
  continue;
152
- const constName = d.id.name;
179
+ const constName = declaration.id.name;
153
180
  context.report({
154
- node: d,
181
+ node: declaration,
155
182
  messageId: 'extractGlobalConstants',
156
183
  data: { declarationName: constName },
157
184
  });
@@ -283,8 +310,8 @@ exports.extractGlobalConstants = (0, createRule_1.createRule)({
283
310
  },
284
311
  schema: [],
285
312
  messages: {
286
- extractGlobalConstants: 'Move this declaration {{ declarationName }} to the global scope and rename it to UPPER_SNAKE_CASE if necessary.',
287
- requireAsConst: 'Numeric literal {{ value }} in loop expression should be extracted to a constant with "as const" type assertion.',
313
+ extractGlobalConstants: 'What\'s wrong: Declaration "{{declarationName}}" does not reference values from this scope.\nWhy it matters: Keeping it nested recreates the same constant/helper on every call, which adds avoidable allocations and obscures that the value can be shared.\nHow to fix: Hoist it to module scope (use UPPER_SNAKE_CASE for immutable constants) so it is created once and can be imported.',
314
+ requireAsConst: 'What\'s wrong: Numeric literal {{value}} is used directly as a loop boundary.\nWhy it matters: Without "as const", TypeScript widens it to number, so if you later extract or reuse the value you lose the literal-type boundary and it is easier for related loops to drift out of sync.\nHow to fix: Extract it to a named constant with "as const" (or add "as const" inline) to keep the boundary explicit and reusable.',
288
315
  },
289
316
  },
290
317
  defaultOptions: [],