@blumintinc/eslint-plugin-blumint 1.13.0 → 1.15.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 (199) hide show
  1. package/README.md +169 -139
  2. package/lib/index.js +200 -9
  3. package/lib/rules/array-methods-this-context.js +1 -1
  4. package/lib/rules/avoid-utils-directory.js +0 -4
  5. package/lib/rules/class-methods-read-top-to-bottom.js +24 -5
  6. package/lib/rules/consistent-callback-naming.js +43 -4
  7. package/lib/rules/dynamic-https-errors.d.ts +1 -1
  8. package/lib/rules/dynamic-https-errors.js +134 -44
  9. package/lib/rules/enforce-assert-safe-object-key.js +11 -11
  10. package/lib/rules/enforce-boolean-naming-prefixes.js +11 -235
  11. package/lib/rules/enforce-callback-memo.js +1 -1
  12. package/lib/rules/enforce-centralized-mock-firestore.js +1 -1
  13. package/lib/rules/enforce-console-error.js +35 -7
  14. package/lib/rules/enforce-date-ttime.d.ts +1 -0
  15. package/lib/rules/enforce-date-ttime.js +156 -0
  16. package/lib/rules/enforce-dynamic-file-naming.d.ts +2 -0
  17. package/lib/rules/enforce-dynamic-file-naming.js +53 -28
  18. package/lib/rules/enforce-dynamic-firebase-imports.d.ts +2 -1
  19. package/lib/rules/enforce-dynamic-firebase-imports.js +3 -2
  20. package/lib/rules/enforce-early-destructuring.d.ts +2 -0
  21. package/lib/rules/enforce-early-destructuring.js +980 -0
  22. package/lib/rules/enforce-empty-object-check.d.ts +11 -0
  23. package/lib/rules/enforce-empty-object-check.js +502 -0
  24. package/lib/rules/enforce-exported-function-types.js +1 -1
  25. package/lib/rules/enforce-f-extension-for-entry-points.d.ts +8 -0
  26. package/lib/rules/enforce-f-extension-for-entry-points.js +283 -0
  27. package/lib/rules/enforce-fieldpath-syntax-in-docsetter.js +100 -50
  28. package/lib/rules/enforce-firestore-doc-ref-generic.js +15 -3
  29. package/lib/rules/enforce-firestore-facade.js +4 -2
  30. package/lib/rules/enforce-firestore-set-merge.js +3 -4
  31. package/lib/rules/enforce-global-constants.js +4 -4
  32. package/lib/rules/enforce-id-capitalization.js +1 -1
  33. package/lib/rules/enforce-identifiable-firestore-type.js +108 -58
  34. package/lib/rules/enforce-memoize-async.js +71 -21
  35. package/lib/rules/enforce-memoize-getters.js +39 -15
  36. package/lib/rules/enforce-microdiff.js +1 -1
  37. package/lib/rules/enforce-object-literal-as-const.js +4 -4
  38. package/lib/rules/enforce-positive-naming.js +4 -0
  39. package/lib/rules/enforce-props-argument-name.js +44 -18
  40. package/lib/rules/enforce-safe-stringify.js +1 -1
  41. package/lib/rules/enforce-serializable-params.js +3 -3
  42. package/lib/rules/enforce-singular-type-names.js +1 -1
  43. package/lib/rules/enforce-stable-hash-spread-props.d.ts +13 -0
  44. package/lib/rules/enforce-stable-hash-spread-props.js +342 -0
  45. package/lib/rules/enforce-storage-context.d.ts +9 -0
  46. package/lib/rules/enforce-storage-context.js +646 -0
  47. package/lib/rules/enforce-timestamp-now.js +23 -7
  48. package/lib/rules/enforce-transform-memoization.d.ts +4 -0
  49. package/lib/rules/enforce-transform-memoization.js +416 -0
  50. package/lib/rules/enforce-typescript-markdown-code-blocks.js +7 -3
  51. package/lib/rules/enforce-unique-cursor-headers.d.ts +16 -0
  52. package/lib/rules/enforce-unique-cursor-headers.js +365 -0
  53. package/lib/rules/enforce-verb-noun-naming.js +3818 -4644
  54. package/lib/rules/ensure-pointer-events-none.js +28 -2
  55. package/lib/rules/export-if-in-doubt.js +27 -4
  56. package/lib/rules/extract-global-constants.js +40 -13
  57. package/lib/rules/fast-deep-equal-over-microdiff.js +29 -39
  58. package/lib/rules/firestore-transaction-reads-before-writes.js +50 -4
  59. package/lib/rules/flatten-push-calls.d.ts +2 -0
  60. package/lib/rules/flatten-push-calls.js +428 -0
  61. package/lib/rules/global-const-style.js +66 -23
  62. package/lib/rules/jsdoc-above-field.d.ts +11 -0
  63. package/lib/rules/jsdoc-above-field.js +208 -0
  64. package/lib/rules/key-only-outermost-element.js +21 -3
  65. package/lib/rules/logical-top-to-bottom-grouping.d.ts +5 -0
  66. package/lib/rules/logical-top-to-bottom-grouping.js +1258 -0
  67. package/lib/rules/memo-compare-deeply-complex-props.d.ts +3 -0
  68. package/lib/rules/memo-compare-deeply-complex-props.js +801 -0
  69. package/lib/rules/memo-nested-react-components.d.ts +8 -0
  70. package/lib/rules/memo-nested-react-components.js +415 -0
  71. package/lib/rules/no-always-true-false-conditions.d.ts +2 -1
  72. package/lib/rules/no-always-true-false-conditions.js +68 -8
  73. package/lib/rules/no-array-length-in-deps.js +9 -3
  74. package/lib/rules/no-async-foreach.js +215 -12
  75. package/lib/rules/no-circular-references.d.ts +2 -1
  76. package/lib/rules/no-circular-references.js +31 -32
  77. package/lib/rules/no-class-instance-destructuring.js +55 -12
  78. package/lib/rules/no-complex-cloud-params.js +7 -3
  79. package/lib/rules/no-compositing-layer-props.js +2 -2
  80. package/lib/rules/no-conditional-literals-in-jsx.js +37 -12
  81. package/lib/rules/no-console-error.d.ts +9 -0
  82. package/lib/rules/no-console-error.js +527 -0
  83. package/lib/rules/no-curly-brackets-around-commented-properties.d.ts +2 -0
  84. package/lib/rules/no-curly-brackets-around-commented-properties.js +258 -0
  85. package/lib/rules/no-empty-dependency-use-callbacks.d.ts +11 -0
  86. package/lib/rules/no-empty-dependency-use-callbacks.js +576 -0
  87. package/lib/rules/no-entire-object-hook-deps.js +52 -7
  88. package/lib/rules/no-excessive-parent-chain.js +4 -1
  89. package/lib/rules/no-explicit-return-type.d.ts +2 -1
  90. package/lib/rules/no-explicit-return-type.js +215 -26
  91. package/lib/rules/no-filter-without-return.js +5 -1
  92. package/lib/rules/no-firestore-jest-mock.d.ts +2 -1
  93. package/lib/rules/no-firestore-jest-mock.js +126 -8
  94. package/lib/rules/no-firestore-object-arrays.js +67 -12
  95. package/lib/rules/no-handler-suffix.d.ts +12 -0
  96. package/lib/rules/no-handler-suffix.js +305 -0
  97. package/lib/rules/no-hungarian.js +1 -1
  98. package/lib/rules/no-inline-component-prop.d.ts +10 -0
  99. package/lib/rules/no-inline-component-prop.js +465 -0
  100. package/lib/rules/no-jsx-in-hooks.js +6 -1
  101. package/lib/rules/no-jsx-whitespace-literal.js +8 -2
  102. package/lib/rules/no-margin-properties.js +6 -6
  103. package/lib/rules/no-memoize-on-static.js +9 -1
  104. package/lib/rules/no-misleading-boolean-prefixes.js +7 -3
  105. package/lib/rules/no-misused-switch-case.js +22 -1
  106. package/lib/rules/no-mixed-firestore-transactions.d.ts +3 -1
  107. package/lib/rules/no-mixed-firestore-transactions.js +296 -34
  108. package/lib/rules/no-mock-firebase-admin.js +5 -2
  109. package/lib/rules/no-object-values-on-strings.js +10 -2
  110. package/lib/rules/no-overridable-method-calls-in-constructor.js +29 -17
  111. package/lib/rules/no-passthrough-getters.d.ts +2 -2
  112. package/lib/rules/no-passthrough-getters.js +120 -2
  113. package/lib/rules/no-redundant-annotation-assertion.d.ts +2 -0
  114. package/lib/rules/no-redundant-annotation-assertion.js +402 -0
  115. package/lib/rules/no-redundant-param-types.js +16 -6
  116. package/lib/rules/no-redundant-this-params.d.ts +3 -0
  117. package/lib/rules/no-redundant-this-params.js +508 -0
  118. package/lib/rules/no-redundant-usecallback-wrapper.js +16 -3
  119. package/lib/rules/no-res-error-status-in-onrequest.d.ts +4 -0
  120. package/lib/rules/no-res-error-status-in-onrequest.js +521 -0
  121. package/lib/rules/no-restricted-properties-fix.js +11 -10
  122. package/lib/rules/no-separate-loading-state.js +7 -16
  123. package/lib/rules/no-stale-state-across-await.js +1 -1
  124. package/lib/rules/no-type-assertion-returns.js +43 -49
  125. package/lib/rules/no-undefined-null-passthrough.js +61 -31
  126. package/lib/rules/no-unmemoized-memo-without-props.d.ts +8 -0
  127. package/lib/rules/no-unmemoized-memo-without-props.js +426 -0
  128. package/lib/rules/no-unnecessary-destructuring-rename.d.ts +2 -0
  129. package/lib/rules/no-unnecessary-destructuring-rename.js +344 -0
  130. package/lib/rules/no-unnecessary-destructuring.js +14 -4
  131. package/lib/rules/no-unnecessary-verb-suffix.js +2 -1
  132. package/lib/rules/no-unpinned-dependencies.js +36 -5
  133. package/lib/rules/no-unused-props.d.ts +2 -2
  134. package/lib/rules/no-unused-props.js +295 -91
  135. package/lib/rules/no-unused-usestate.js +6 -2
  136. package/lib/rules/no-useless-fragment.js +28 -2
  137. package/lib/rules/no-useless-usememo-primitives.d.ts +9 -0
  138. package/lib/rules/no-useless-usememo-primitives.js +393 -0
  139. package/lib/rules/no-usememo-for-pass-by-value.d.ts +13 -0
  140. package/lib/rules/no-usememo-for-pass-by-value.js +757 -0
  141. package/lib/rules/no-uuidv4-base62-as-key.js +18 -13
  142. package/lib/rules/omit-index-html.d.ts +2 -1
  143. package/lib/rules/omit-index-html.js +62 -7
  144. package/lib/rules/optimize-object-boolean-conditions.js +6 -4
  145. package/lib/rules/parallelize-async-operations.js +143 -59
  146. package/lib/rules/prefer-batch-operations.d.ts +1 -3
  147. package/lib/rules/prefer-batch-operations.js +32 -5
  148. package/lib/rules/prefer-block-comments-for-declarations.js +9 -4
  149. package/lib/rules/prefer-clone-deep.js +3 -3
  150. package/lib/rules/prefer-destructuring-no-class.d.ts +2 -1
  151. package/lib/rules/prefer-destructuring-no-class.js +244 -59
  152. package/lib/rules/prefer-docsetter-setall.d.ts +2 -0
  153. package/lib/rules/prefer-docsetter-setall.js +243 -0
  154. package/lib/rules/prefer-field-paths-in-transforms.js +4 -3
  155. package/lib/rules/prefer-fragment-component.js +1 -1
  156. package/lib/rules/prefer-fragment-shorthand.js +2 -1
  157. package/lib/rules/prefer-getter-over-parameterless-method.d.ts +13 -0
  158. package/lib/rules/prefer-getter-over-parameterless-method.js +648 -0
  159. package/lib/rules/prefer-global-router-state-key.js +88 -13
  160. package/lib/rules/prefer-memoized-props.d.ts +3 -0
  161. package/lib/rules/prefer-memoized-props.js +445 -0
  162. package/lib/rules/prefer-next-dynamic.js +60 -69
  163. package/lib/rules/prefer-nullish-coalescing-boolean-props.js +118 -8
  164. package/lib/rules/prefer-nullish-coalescing-override.d.ts +2 -1
  165. package/lib/rules/prefer-nullish-coalescing-override.js +11 -5
  166. package/lib/rules/prefer-params-over-parent-id.js +221 -172
  167. package/lib/rules/prefer-settings-object.js +27 -10
  168. package/lib/rules/prefer-type-alias-over-typeof-constant.js +9 -0
  169. package/lib/rules/prefer-type-over-interface.js +7 -2
  170. package/lib/rules/prefer-url-tostring-over-tojson.js +6 -3
  171. package/lib/rules/prefer-use-deep-compare-memo.js +8 -5
  172. package/lib/rules/prefer-usecallback-over-usememo-for-functions.js +1 -1
  173. package/lib/rules/prefer-usememo-over-useeffect-usestate.js +1 -1
  174. package/lib/rules/prefer-utility-function-over-private-static.js +34 -2
  175. package/lib/rules/prevent-children-clobber.d.ts +2 -0
  176. package/lib/rules/prevent-children-clobber.js +540 -0
  177. package/lib/rules/react-memoize-literals.d.ts +4 -0
  178. package/lib/rules/react-memoize-literals.js +614 -0
  179. package/lib/rules/react-usememo-should-be-component.js +2 -2
  180. package/lib/rules/require-hooks-default-params.js +17 -16
  181. package/lib/rules/require-https-error-cause.d.ts +4 -0
  182. package/lib/rules/require-https-error-cause.js +155 -0
  183. package/lib/rules/require-https-error.js +43 -21
  184. package/lib/rules/require-image-optimized.js +1 -1
  185. package/lib/rules/require-memo.js +18 -10
  186. package/lib/rules/require-memoize-jsx-returners.d.ts +3 -0
  187. package/lib/rules/require-memoize-jsx-returners.js +485 -0
  188. package/lib/rules/require-usememo-object-literals.js +2 -3
  189. package/lib/rules/test-file-location-enforcement.js +1 -1
  190. package/lib/rules/use-latest-callback.js +6 -5
  191. package/lib/rules/vertically-group-related-functions.d.ts +16 -0
  192. package/lib/rules/vertically-group-related-functions.js +480 -0
  193. package/lib/utils/ASTHelpers.d.ts +47 -2
  194. package/lib/utils/ASTHelpers.js +370 -112
  195. package/lib/utils/getMethodName.d.ts +27 -0
  196. package/lib/utils/getMethodName.js +35 -0
  197. package/lib/utils/tsTypeClassifier.d.ts +30 -0
  198. package/lib/utils/tsTypeClassifier.js +149 -0
  199. 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