@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.
- package/README.md +170 -139
- package/lib/index.js +190 -1
- package/lib/rules/array-methods-this-context.js +1 -1
- package/lib/rules/class-methods-read-top-to-bottom.js +24 -5
- package/lib/rules/consistent-callback-naming.js +1 -1
- package/lib/rules/dynamic-https-errors.js +3 -4
- package/lib/rules/enforce-assert-safe-object-key.js +11 -11
- package/lib/rules/enforce-boolean-naming-prefixes.js +11 -23
- package/lib/rules/enforce-callback-memo.js +1 -1
- package/lib/rules/enforce-centralized-mock-firestore.js +1 -1
- package/lib/rules/enforce-console-error.js +35 -7
- package/lib/rules/enforce-dynamic-file-naming.d.ts +2 -0
- package/lib/rules/enforce-dynamic-file-naming.js +53 -28
- package/lib/rules/enforce-early-destructuring.d.ts +2 -0
- package/lib/rules/enforce-early-destructuring.js +980 -0
- package/lib/rules/enforce-empty-object-check.d.ts +11 -0
- package/lib/rules/enforce-empty-object-check.js +502 -0
- package/lib/rules/enforce-exported-function-types.js +1 -1
- package/lib/rules/enforce-fieldpath-syntax-in-docsetter.js +100 -50
- package/lib/rules/enforce-firestore-doc-ref-generic.js +15 -3
- package/lib/rules/enforce-firestore-facade.js +4 -2
- package/lib/rules/enforce-firestore-set-merge.js +3 -4
- package/lib/rules/enforce-global-constants.js +1 -1
- package/lib/rules/enforce-identifiable-firestore-type.js +108 -58
- package/lib/rules/enforce-memoize-async.js +2 -6
- package/lib/rules/enforce-memoize-getters.js +39 -15
- package/lib/rules/enforce-microdiff.js +1 -1
- package/lib/rules/enforce-object-literal-as-const.js +4 -4
- package/lib/rules/enforce-positive-naming.js +4 -0
- package/lib/rules/enforce-props-argument-name.js +2 -2
- package/lib/rules/enforce-safe-stringify.js +1 -1
- package/lib/rules/enforce-serializable-params.js +3 -3
- package/lib/rules/enforce-singular-type-names.js +1 -1
- package/lib/rules/enforce-stable-hash-spread-props.d.ts +13 -0
- package/lib/rules/enforce-stable-hash-spread-props.js +342 -0
- package/lib/rules/enforce-storage-context.d.ts +9 -0
- package/lib/rules/enforce-storage-context.js +646 -0
- package/lib/rules/enforce-timestamp-now.js +23 -7
- package/lib/rules/enforce-transform-memoization.d.ts +4 -0
- package/lib/rules/enforce-transform-memoization.js +416 -0
- package/lib/rules/enforce-typescript-markdown-code-blocks.js +7 -3
- package/lib/rules/enforce-unique-cursor-headers.d.ts +16 -0
- package/lib/rules/enforce-unique-cursor-headers.js +365 -0
- package/lib/rules/enforce-verb-noun-naming.js +5 -4
- package/lib/rules/ensure-pointer-events-none.js +28 -2
- package/lib/rules/export-if-in-doubt.js +27 -4
- package/lib/rules/extract-global-constants.js +40 -13
- package/lib/rules/fast-deep-equal-over-microdiff.js +29 -39
- package/lib/rules/firestore-transaction-reads-before-writes.js +50 -4
- package/lib/rules/flatten-push-calls.d.ts +2 -0
- package/lib/rules/flatten-push-calls.js +428 -0
- package/lib/rules/global-const-style.js +50 -19
- package/lib/rules/jsdoc-above-field.d.ts +11 -0
- package/lib/rules/jsdoc-above-field.js +208 -0
- package/lib/rules/key-only-outermost-element.js +21 -3
- package/lib/rules/logical-top-to-bottom-grouping.d.ts +5 -0
- package/lib/rules/logical-top-to-bottom-grouping.js +1258 -0
- package/lib/rules/memo-compare-deeply-complex-props.d.ts +3 -0
- package/lib/rules/memo-compare-deeply-complex-props.js +788 -0
- package/lib/rules/memo-nested-react-components.d.ts +8 -0
- package/lib/rules/memo-nested-react-components.js +410 -0
- package/lib/rules/no-always-true-false-conditions.d.ts +2 -1
- package/lib/rules/no-always-true-false-conditions.js +68 -8
- package/lib/rules/no-array-length-in-deps.js +9 -3
- package/lib/rules/no-async-foreach.js +210 -12
- package/lib/rules/no-circular-references.js +18 -17
- package/lib/rules/no-class-instance-destructuring.js +55 -12
- package/lib/rules/no-complex-cloud-params.js +7 -3
- package/lib/rules/no-compositing-layer-props.js +2 -2
- package/lib/rules/no-conditional-literals-in-jsx.js +37 -12
- package/lib/rules/no-console-error.d.ts +9 -0
- package/lib/rules/no-console-error.js +525 -0
- package/lib/rules/no-curly-brackets-around-commented-properties.d.ts +2 -0
- package/lib/rules/no-curly-brackets-around-commented-properties.js +258 -0
- package/lib/rules/no-empty-dependency-use-callbacks.d.ts +11 -0
- package/lib/rules/no-empty-dependency-use-callbacks.js +576 -0
- package/lib/rules/no-entire-object-hook-deps.js +4 -6
- package/lib/rules/no-excessive-parent-chain.js +1 -1
- package/lib/rules/no-explicit-return-type.d.ts +2 -1
- package/lib/rules/no-explicit-return-type.js +215 -26
- package/lib/rules/no-filter-without-return.js +5 -1
- package/lib/rules/no-firestore-jest-mock.d.ts +2 -1
- package/lib/rules/no-firestore-jest-mock.js +126 -8
- package/lib/rules/no-firestore-object-arrays.js +67 -12
- package/lib/rules/no-handler-suffix.d.ts +12 -0
- package/lib/rules/no-handler-suffix.js +305 -0
- package/lib/rules/no-hungarian.js +1 -1
- package/lib/rules/no-inline-component-prop.d.ts +10 -0
- package/lib/rules/no-inline-component-prop.js +456 -0
- package/lib/rules/no-jsx-in-hooks.js +6 -1
- package/lib/rules/no-jsx-whitespace-literal.js +8 -2
- package/lib/rules/no-margin-properties.js +6 -6
- package/lib/rules/no-memoize-on-static.js +9 -1
- package/lib/rules/no-misleading-boolean-prefixes.js +7 -3
- package/lib/rules/no-misused-switch-case.js +22 -1
- package/lib/rules/no-mixed-firestore-transactions.d.ts +3 -1
- package/lib/rules/no-mixed-firestore-transactions.js +296 -34
- package/lib/rules/no-mock-firebase-admin.js +5 -2
- package/lib/rules/no-object-values-on-strings.js +10 -2
- package/lib/rules/no-overridable-method-calls-in-constructor.js +29 -17
- package/lib/rules/no-passthrough-getters.js +38 -2
- package/lib/rules/no-redundant-annotation-assertion.d.ts +2 -0
- package/lib/rules/no-redundant-annotation-assertion.js +402 -0
- package/lib/rules/no-redundant-param-types.js +16 -6
- package/lib/rules/no-redundant-this-params.d.ts +3 -0
- package/lib/rules/no-redundant-this-params.js +459 -0
- package/lib/rules/no-redundant-usecallback-wrapper.js +16 -3
- package/lib/rules/no-res-error-status-in-onrequest.d.ts +4 -0
- package/lib/rules/no-res-error-status-in-onrequest.js +521 -0
- package/lib/rules/no-restricted-properties-fix.js +11 -10
- package/lib/rules/no-separate-loading-state.js +7 -16
- package/lib/rules/no-stale-state-across-await.js +1 -1
- package/lib/rules/no-type-assertion-returns.js +43 -49
- package/lib/rules/no-undefined-null-passthrough.js +61 -31
- package/lib/rules/no-unmemoized-memo-without-props.d.ts +8 -0
- package/lib/rules/no-unmemoized-memo-without-props.js +426 -0
- package/lib/rules/no-unnecessary-destructuring-rename.d.ts +2 -0
- package/lib/rules/no-unnecessary-destructuring-rename.js +347 -0
- package/lib/rules/no-unnecessary-destructuring.js +14 -4
- package/lib/rules/no-unnecessary-verb-suffix.js +2 -1
- package/lib/rules/no-unpinned-dependencies.js +36 -5
- package/lib/rules/no-unused-props.d.ts +2 -2
- package/lib/rules/no-unused-props.js +295 -91
- package/lib/rules/no-unused-usestate.js +6 -2
- package/lib/rules/no-useless-fragment.js +28 -2
- package/lib/rules/no-useless-usememo-primitives.d.ts +9 -0
- package/lib/rules/no-useless-usememo-primitives.js +393 -0
- package/lib/rules/no-usememo-for-pass-by-value.d.ts +13 -0
- package/lib/rules/no-usememo-for-pass-by-value.js +757 -0
- package/lib/rules/no-uuidv4-base62-as-key.js +18 -13
- package/lib/rules/omit-index-html.d.ts +2 -1
- package/lib/rules/omit-index-html.js +62 -7
- package/lib/rules/optimize-object-boolean-conditions.js +6 -4
- package/lib/rules/parallelize-async-operations.js +24 -5
- package/lib/rules/prefer-batch-operations.d.ts +1 -3
- package/lib/rules/prefer-batch-operations.js +32 -5
- package/lib/rules/prefer-block-comments-for-declarations.js +9 -4
- package/lib/rules/prefer-clone-deep.js +3 -3
- package/lib/rules/prefer-destructuring-no-class.d.ts +2 -1
- package/lib/rules/prefer-destructuring-no-class.js +244 -59
- package/lib/rules/prefer-docsetter-setall.d.ts +2 -0
- package/lib/rules/prefer-docsetter-setall.js +243 -0
- package/lib/rules/prefer-field-paths-in-transforms.js +4 -3
- package/lib/rules/prefer-fragment-component.js +1 -1
- package/lib/rules/prefer-fragment-shorthand.js +2 -1
- package/lib/rules/prefer-getter-over-parameterless-method.d.ts +13 -0
- package/lib/rules/prefer-getter-over-parameterless-method.js +648 -0
- package/lib/rules/prefer-global-router-state-key.js +88 -13
- package/lib/rules/prefer-memoized-props.d.ts +3 -0
- package/lib/rules/prefer-memoized-props.js +445 -0
- package/lib/rules/prefer-next-dynamic.js +60 -69
- package/lib/rules/prefer-nullish-coalescing-boolean-props.js +9 -4
- package/lib/rules/prefer-nullish-coalescing-override.d.ts +2 -1
- package/lib/rules/prefer-nullish-coalescing-override.js +11 -5
- package/lib/rules/prefer-params-over-parent-id.js +220 -171
- package/lib/rules/prefer-settings-object.js +2 -2
- package/lib/rules/prefer-type-over-interface.js +7 -2
- package/lib/rules/prefer-url-tostring-over-tojson.js +6 -3
- package/lib/rules/prefer-use-deep-compare-memo.js +8 -5
- package/lib/rules/prefer-usecallback-over-usememo-for-functions.js +1 -1
- package/lib/rules/prefer-utility-function-over-private-static.js +34 -2
- package/lib/rules/prevent-children-clobber.d.ts +2 -0
- package/lib/rules/prevent-children-clobber.js +536 -0
- package/lib/rules/react-memoize-literals.d.ts +4 -0
- package/lib/rules/react-memoize-literals.js +495 -0
- package/lib/rules/react-usememo-should-be-component.js +2 -2
- package/lib/rules/require-hooks-default-params.js +17 -16
- package/lib/rules/require-https-error-cause.d.ts +4 -0
- package/lib/rules/require-https-error-cause.js +136 -0
- package/lib/rules/require-https-error.js +43 -21
- package/lib/rules/require-image-optimized.js +1 -1
- package/lib/rules/require-memo.js +1 -1
- package/lib/rules/require-memoize-jsx-returners.d.ts +3 -0
- package/lib/rules/require-memoize-jsx-returners.js +485 -0
- package/lib/rules/require-usememo-object-literals.js +2 -3
- package/lib/rules/test-file-location-enforcement.js +1 -1
- package/lib/rules/use-latest-callback.js +6 -5
- package/lib/rules/vertically-group-related-functions.d.ts +16 -0
- package/lib/rules/vertically-group-related-functions.js +480 -0
- package/lib/utils/ASTHelpers.d.ts +13 -1
- package/lib/utils/ASTHelpers.js +24 -0
- package/lib/utils/getMethodName.d.ts +27 -0
- package/lib/utils/getMethodName.js +35 -0
- package/lib/utils/tsTypeClassifier.d.ts +30 -0
- package/lib/utils/tsTypeClassifier.js +149 -0
- 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
|
|
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: '
|
|
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.
|
|
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
|
|
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
|
|
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
|
-
|
|
135
|
-
if (
|
|
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
|
-
|
|
149
|
-
for (const
|
|
150
|
-
if (
|
|
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 =
|
|
179
|
+
const constName = declaration.id.name;
|
|
153
180
|
context.report({
|
|
154
|
-
node:
|
|
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: '
|
|
287
|
-
requireAsConst: 'Numeric literal {{
|
|
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: [],
|