@blumintinc/eslint-plugin-blumint 1.12.6 → 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 +171 -99
- package/lib/index.js +300 -17
- package/lib/rules/array-methods-this-context.js +20 -2
- package/lib/rules/avoid-utils-directory.js +11 -2
- 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.d.ts +3 -1
- package/lib/rules/dynamic-https-errors.js +48 -11
- package/lib/rules/enforce-assert-safe-object-key.d.ts +8 -0
- package/lib/rules/{enforce-assertSafe-object-key.js → enforce-assert-safe-object-key.js} +52 -96
- package/lib/rules/enforce-assert-throws.d.ts +3 -1
- package/lib/rules/enforce-assert-throws.js +493 -51
- package/lib/rules/enforce-boolean-naming-prefixes.d.ts +3 -1
- package/lib/rules/enforce-boolean-naming-prefixes.js +820 -39
- package/lib/rules/enforce-callable-types.js +4 -4
- package/lib/rules/enforce-callback-memo.js +143 -18
- package/lib/rules/enforce-centralized-mock-firestore.js +5 -2
- package/lib/rules/enforce-console-error.d.ts +3 -0
- package/lib/rules/enforce-console-error.js +278 -0
- package/lib/rules/enforce-css-media-queries.js +24 -46
- 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-dynamic-firebase-imports.js +100 -19
- package/lib/rules/enforce-dynamic-imports.js +1 -1
- 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 +4 -4
- package/lib/rules/enforce-fieldpath-syntax-in-docsetter.d.ts +2 -0
- package/lib/rules/enforce-fieldpath-syntax-in-docsetter.js +262 -0
- package/lib/rules/enforce-firestore-doc-ref-generic.js +350 -3
- package/lib/rules/enforce-firestore-facade.js +349 -157
- package/lib/rules/enforce-firestore-rules-get-access.d.ts +3 -0
- package/lib/rules/enforce-firestore-rules-get-access.js +109 -0
- package/lib/rules/enforce-firestore-set-merge.js +3 -4
- package/lib/rules/enforce-global-constants.d.ts +4 -1
- package/lib/rules/enforce-global-constants.js +240 -12
- package/lib/rules/enforce-identifiable-firestore-type.js +108 -58
- package/lib/rules/enforce-memoize-async.d.ts +2 -1
- package/lib/rules/enforce-memoize-async.js +92 -27
- package/lib/rules/enforce-memoize-getters.d.ts +3 -0
- package/lib/rules/enforce-memoize-getters.js +147 -0
- package/lib/rules/enforce-microdiff.js +3 -2
- package/lib/rules/enforce-object-literal-as-const.js +4 -4
- package/lib/rules/enforce-positive-naming.js +9 -7
- package/lib/rules/enforce-props-argument-name.d.ts +2 -7
- package/lib/rules/enforce-props-argument-name.js +165 -151
- package/lib/rules/enforce-props-naming-consistency.d.ts +2 -0
- package/lib/rules/enforce-props-naming-consistency.js +145 -0
- package/lib/rules/enforce-querykey-ts.d.ts +9 -0
- package/lib/rules/enforce-querykey-ts.js +274 -0
- 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 +10 -6
- 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.d.ts +1 -0
- package/lib/rules/enforce-typescript-markdown-code-blocks.js +100 -0
- 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 +90 -14
- 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 +92 -53
- package/lib/rules/fast-deep-equal-over-microdiff.d.ts +2 -1
- package/lib/rules/fast-deep-equal-over-microdiff.js +351 -83
- package/lib/rules/firestore-transaction-reads-before-writes.d.ts +1 -0
- package/lib/rules/firestore-transaction-reads-before-writes.js +277 -0
- package/lib/rules/flatten-push-calls.d.ts +2 -0
- package/lib/rules/flatten-push-calls.js +428 -0
- package/lib/rules/generic-starts-with-t.js +8 -2
- 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/memoize-root-level-hocs.d.ts +7 -0
- package/lib/rules/memoize-root-level-hocs.js +242 -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.d.ts +12 -0
- package/lib/rules/no-array-length-in-deps.js +314 -0
- package/lib/rules/no-async-array-filter.js +5 -2
- 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 +22 -5
- package/lib/rules/no-compositing-layer-props.js +6 -4
- 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.d.ts +3 -1
- package/lib/rules/no-entire-object-hook-deps.js +320 -68
- package/lib/rules/no-excessive-parent-chain.d.ts +5 -0
- package/lib/rules/no-excessive-parent-chain.js +273 -0
- 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 +398 -28
- 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 +14 -14
- 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.d.ts +7 -1
- package/lib/rules/no-margin-properties.js +22 -9
- package/lib/rules/no-memoize-on-static.js +9 -1
- package/lib/rules/no-misleading-boolean-prefixes.d.ts +7 -0
- package/lib/rules/no-misleading-boolean-prefixes.js +331 -0
- 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.d.ts +1 -0
- package/lib/rules/no-overridable-method-calls-in-constructor.js +272 -0
- package/lib/rules/no-passthrough-getters.d.ts +4 -0
- package/lib/rules/no-passthrough-getters.js +206 -0
- 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.d.ts +8 -0
- package/lib/rules/no-redundant-usecallback-wrapper.js +328 -0
- 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.d.ts +12 -0
- package/lib/rules/no-restricted-properties-fix.js +173 -0
- package/lib/rules/no-separate-loading-state.d.ts +8 -0
- package/lib/rules/no-separate-loading-state.js +133 -0
- package/lib/rules/no-stale-state-across-await.d.ts +1 -0
- package/lib/rules/no-stale-state-across-await.js +185 -0
- package/lib/rules/no-static-constants-in-dynamic-files.d.ts +3 -0
- package/lib/rules/no-static-constants-in-dynamic-files.js +149 -0
- package/lib/rules/no-try-catch-already-exists-in-transaction.d.ts +1 -0
- package/lib/rules/no-try-catch-already-exists-in-transaction.js +342 -0
- package/lib/rules/no-type-assertion-returns.js +49 -49
- package/lib/rules/no-undefined-null-passthrough.js +217 -40
- 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 -3
- package/lib/rules/no-unpinned-dependencies.js +36 -5
- package/lib/rules/no-unsafe-firestore-spread.js +8 -5
- package/lib/rules/no-unused-props.d.ts +5 -2
- package/lib/rules/no-unused-props.js +451 -112
- 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.d.ts +1 -0
- package/lib/rules/optimize-object-boolean-conditions.js +236 -0
- package/lib/rules/parallelize-async-operations.d.ts +8 -0
- package/lib/rules/parallelize-async-operations.js +394 -0
- package/lib/rules/prefer-batch-operations.d.ts +1 -3
- package/lib/rules/prefer-batch-operations.js +228 -35
- package/lib/rules/prefer-block-comments-for-declarations.js +22 -7
- 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-document-flattening.d.ts +4 -0
- package/lib/rules/prefer-document-flattening.js +239 -0
- package/lib/rules/prefer-field-paths-in-transforms.d.ts +7 -0
- package/lib/rules/prefer-field-paths-in-transforms.js +251 -0
- package/lib/rules/prefer-fragment-component.js +4 -4
- 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.d.ts +7 -3
- package/lib/rules/prefer-global-router-state-key.js +305 -31
- 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.d.ts +9 -0
- package/lib/rules/prefer-next-dynamic.js +357 -0
- package/lib/rules/prefer-nullish-coalescing-boolean-props.d.ts +1 -0
- package/lib/rules/prefer-nullish-coalescing-boolean-props.js +435 -0
- package/lib/rules/prefer-nullish-coalescing-override.d.ts +7 -0
- package/lib/rules/prefer-nullish-coalescing-override.js +188 -0
- package/lib/rules/prefer-params-over-parent-id.d.ts +5 -0
- package/lib/rules/prefer-params-over-parent-id.js +277 -0
- package/lib/rules/prefer-settings-object.js +23 -15
- package/lib/rules/prefer-type-alias-over-typeof-constant.d.ts +3 -0
- package/lib/rules/prefer-type-alias-over-typeof-constant.js +259 -0
- package/lib/rules/prefer-type-over-interface.js +7 -2
- package/lib/rules/prefer-url-tostring-over-tojson.d.ts +2 -0
- package/lib/rules/prefer-url-tostring-over-tojson.js +169 -0
- package/lib/rules/prefer-use-deep-compare-memo.d.ts +4 -0
- package/lib/rules/prefer-use-deep-compare-memo.js +431 -0
- package/lib/rules/prefer-usecallback-over-usememo-for-functions.js +25 -2
- package/lib/rules/prefer-usememo-over-useeffect-usestate.js +24 -5
- 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.d.ts +2 -1
- package/lib/rules/react-usememo-should-be-component.js +144 -78
- package/lib/rules/require-dynamic-firebase-imports.js +2 -1
- 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 +2 -2
- package/lib/rules/require-memo.js +12 -2
- 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 +9 -4
- package/lib/rules/semantic-function-prefixes.js +38 -18
- package/lib/rules/sync-onwrite-name-func.js +5 -1
- package/lib/rules/test-file-location-enforcement.d.ts +1 -0
- package/lib/rules/test-file-location-enforcement.js +57 -0
- package/lib/rules/use-custom-link.js +6 -1
- package/lib/rules/use-custom-router.js +6 -1
- package/lib/rules/use-latest-callback.d.ts +3 -0
- package/lib/rules/use-latest-callback.js +271 -0
- 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 +34 -0
- package/lib/utils/createRule.js +1 -1
- package/lib/utils/getMethodName.d.ts +27 -0
- package/lib/utils/getMethodName.js +35 -0
- package/lib/utils/graph/ClassGraphSorterReadability.js +1 -1
- package/lib/utils/ruleTester.d.ts +1 -0
- package/lib/utils/ruleTester.js +4 -1
- package/lib/utils/tsTypeClassifier.d.ts +30 -0
- package/lib/utils/tsTypeClassifier.js +149 -0
- package/package.json +23 -14
- package/lib/rules/enforce-assertSafe-object-key.d.ts +0 -2
|
@@ -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: [],
|
|
@@ -4600,18 +4600,86 @@ exports.enforceVerbNounNaming = (0, createRule_1.createRule)({
|
|
|
4600
4600
|
}
|
|
4601
4601
|
return false;
|
|
4602
4602
|
}
|
|
4603
|
+
function bodyContainsJsx(node) {
|
|
4604
|
+
const stack = [node];
|
|
4605
|
+
while (stack.length) {
|
|
4606
|
+
const current = stack.pop();
|
|
4607
|
+
if (current.type === utils_1.AST_NODE_TYPES.JSXElement ||
|
|
4608
|
+
current.type === utils_1.AST_NODE_TYPES.JSXFragment) {
|
|
4609
|
+
return true;
|
|
4610
|
+
}
|
|
4611
|
+
for (const key of Object.keys(current)) {
|
|
4612
|
+
if (key === 'parent')
|
|
4613
|
+
continue;
|
|
4614
|
+
const value = current[key];
|
|
4615
|
+
if (Array.isArray(value)) {
|
|
4616
|
+
for (const child of value) {
|
|
4617
|
+
if (child && typeof child.type === 'string') {
|
|
4618
|
+
stack.push(child);
|
|
4619
|
+
}
|
|
4620
|
+
}
|
|
4621
|
+
}
|
|
4622
|
+
else if (value && typeof value.type === 'string') {
|
|
4623
|
+
stack.push(value);
|
|
4624
|
+
}
|
|
4625
|
+
}
|
|
4626
|
+
}
|
|
4627
|
+
return false;
|
|
4628
|
+
}
|
|
4603
4629
|
function hasJsxReturn(node) {
|
|
4604
|
-
|
|
4605
|
-
|
|
4606
|
-
|
|
4607
|
-
|
|
4608
|
-
|
|
4630
|
+
/**
|
|
4631
|
+
* Recursively checks if an expression looks like JSX
|
|
4632
|
+
*/
|
|
4633
|
+
function isJsxExpression(expr) {
|
|
4634
|
+
if (!expr)
|
|
4635
|
+
return false;
|
|
4636
|
+
// Handle parenthesized expressions
|
|
4637
|
+
if (expr.type === 'ParenthesizedExpression') {
|
|
4638
|
+
return isJsxExpression(expr.expression);
|
|
4639
|
+
}
|
|
4640
|
+
switch (expr.type) {
|
|
4641
|
+
case utils_1.AST_NODE_TYPES.JSXElement:
|
|
4642
|
+
case utils_1.AST_NODE_TYPES.JSXFragment:
|
|
4643
|
+
return true;
|
|
4644
|
+
case utils_1.AST_NODE_TYPES.ConditionalExpression:
|
|
4645
|
+
return (isJsxExpression(expr.consequent) ||
|
|
4646
|
+
isJsxExpression(expr.alternate));
|
|
4647
|
+
case utils_1.AST_NODE_TYPES.LogicalExpression:
|
|
4648
|
+
// e.g. condition && <div />
|
|
4649
|
+
return isJsxExpression(expr.right);
|
|
4650
|
+
default:
|
|
4651
|
+
return false;
|
|
4652
|
+
}
|
|
4609
4653
|
}
|
|
4610
|
-
|
|
4611
|
-
|
|
4612
|
-
|
|
4613
|
-
|
|
4614
|
-
|
|
4654
|
+
/**
|
|
4655
|
+
* Recursively checks if a statement/body returns JSX
|
|
4656
|
+
*/
|
|
4657
|
+
function checkBodyForJsx(body) {
|
|
4658
|
+
if (body.type === utils_1.AST_NODE_TYPES.BlockStatement) {
|
|
4659
|
+
for (const stmt of body.body) {
|
|
4660
|
+
if (stmt.type === utils_1.AST_NODE_TYPES.ReturnStatement) {
|
|
4661
|
+
if (isJsxExpression(stmt.argument))
|
|
4662
|
+
return true;
|
|
4663
|
+
}
|
|
4664
|
+
else if (stmt.type === utils_1.AST_NODE_TYPES.IfStatement) {
|
|
4665
|
+
if (checkBodyForJsx(stmt.consequent))
|
|
4666
|
+
return true;
|
|
4667
|
+
if (stmt.alternate && checkBodyForJsx(stmt.alternate))
|
|
4668
|
+
return true;
|
|
4669
|
+
}
|
|
4670
|
+
// We could traverse more (try/catch, switch), but this covers most idiomatic React
|
|
4671
|
+
}
|
|
4672
|
+
}
|
|
4673
|
+
else {
|
|
4674
|
+
// Implicit return (arrow function expression body)
|
|
4675
|
+
return isJsxExpression(body);
|
|
4676
|
+
}
|
|
4677
|
+
return false;
|
|
4678
|
+
}
|
|
4679
|
+
if (node.type === utils_1.AST_NODE_TYPES.FunctionDeclaration ||
|
|
4680
|
+
node.type === utils_1.AST_NODE_TYPES.FunctionExpression ||
|
|
4681
|
+
node.type === utils_1.AST_NODE_TYPES.ArrowFunctionExpression) {
|
|
4682
|
+
return checkBodyForJsx(node.body);
|
|
4615
4683
|
}
|
|
4616
4684
|
return false;
|
|
4617
4685
|
}
|
|
@@ -4650,9 +4718,7 @@ exports.enforceVerbNounNaming = (0, createRule_1.createRule)({
|
|
|
4650
4718
|
const id = parent.id;
|
|
4651
4719
|
if (id.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
4652
4720
|
id.typeAnnotation?.type === utils_1.AST_NODE_TYPES.TSTypeAnnotation) {
|
|
4653
|
-
const typeText = context
|
|
4654
|
-
.getSourceCode()
|
|
4655
|
-
.getText(id.typeAnnotation.typeAnnotation);
|
|
4721
|
+
const typeText = context.sourceCode.getText(id.typeAnnotation.typeAnnotation);
|
|
4656
4722
|
return (typeText.includes('React.') ||
|
|
4657
4723
|
typeText.includes('FC') ||
|
|
4658
4724
|
typeText.includes('FunctionComponent'));
|
|
@@ -4664,6 +4730,13 @@ exports.enforceVerbNounNaming = (0, createRule_1.createRule)({
|
|
|
4664
4730
|
// 5. Component name ends with "Unmemoized" (common React pattern)
|
|
4665
4731
|
functionName && functionName.endsWith('Unmemoized'),
|
|
4666
4732
|
];
|
|
4733
|
+
// If the function has a PascalCase name AND returns JSX, it's definitely a React component
|
|
4734
|
+
if (functionName && isPascalCase(functionName) && hasJsxReturn(node)) {
|
|
4735
|
+
return true;
|
|
4736
|
+
}
|
|
4737
|
+
if (functionName && isPascalCase(functionName) && bodyContainsJsx(node)) {
|
|
4738
|
+
return true;
|
|
4739
|
+
}
|
|
4667
4740
|
// Consider it a React component if it matches at least 2 indicators
|
|
4668
4741
|
// OR if it has the Unmemoized suffix (strong indicator of a React component)
|
|
4669
4742
|
return (indicators.filter(Boolean).length >= 2 ||
|
|
@@ -4680,6 +4753,7 @@ exports.enforceVerbNounNaming = (0, createRule_1.createRule)({
|
|
|
4680
4753
|
context.report({
|
|
4681
4754
|
node: node.id,
|
|
4682
4755
|
messageId: 'functionVerbPhrase',
|
|
4756
|
+
data: { name: node.id.name },
|
|
4683
4757
|
});
|
|
4684
4758
|
}
|
|
4685
4759
|
},
|
|
@@ -4696,6 +4770,7 @@ exports.enforceVerbNounNaming = (0, createRule_1.createRule)({
|
|
|
4696
4770
|
context.report({
|
|
4697
4771
|
node: node.id,
|
|
4698
4772
|
messageId: 'functionVerbPhrase',
|
|
4773
|
+
data: { name: node.id.name },
|
|
4699
4774
|
});
|
|
4700
4775
|
}
|
|
4701
4776
|
}
|
|
@@ -4713,6 +4788,7 @@ exports.enforceVerbNounNaming = (0, createRule_1.createRule)({
|
|
|
4713
4788
|
context.report({
|
|
4714
4789
|
node: node.key,
|
|
4715
4790
|
messageId: 'functionVerbPhrase',
|
|
4791
|
+
data: { name: node.key.name },
|
|
4716
4792
|
});
|
|
4717
4793
|
}
|
|
4718
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
|
});
|