@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,480 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.verticallyGroupRelatedFunctions = void 0;
|
|
4
|
+
const createRule_1 = require("../utils/createRule");
|
|
5
|
+
const ASTHelpers_1 = require("../utils/ASTHelpers");
|
|
6
|
+
const DEFAULT_OPTIONS = {
|
|
7
|
+
exportPlacement: 'ignore',
|
|
8
|
+
dependencyDirection: 'callers-first',
|
|
9
|
+
groupOrder: ['event-handlers', 'other', 'utilities'],
|
|
10
|
+
eventHandlerPattern: '^(handle[A-Z]|on[A-Z])',
|
|
11
|
+
utilityPattern: '^(get|set|fetch|load|format|compute|transform|build|derive|prepare)',
|
|
12
|
+
};
|
|
13
|
+
const DEFAULT_OPTIONS_ARRAY = [DEFAULT_OPTIONS];
|
|
14
|
+
const PATTERN_LENGTH_LIMIT = 200;
|
|
15
|
+
const NESTED_QUANTIFIER_PATTERN = /\([^)]*[+*][^)]*\)\s*[\+\*{]/;
|
|
16
|
+
const BROAD_QUANTIFIER_WITH_BACKREFERENCE_PATTERN = /(?:\.\*|\.\+)[^)]*\\\d/;
|
|
17
|
+
const REPEATED_BROAD_QUANTIFIER_PATTERN = /(?:\.\*|\.\+).*(?:\.\*|\.\+)/;
|
|
18
|
+
function isPatternLikelySafe(pattern) {
|
|
19
|
+
if (pattern.length > PATTERN_LENGTH_LIMIT) {
|
|
20
|
+
return {
|
|
21
|
+
safe: false,
|
|
22
|
+
reason: `pattern longer than ${PATTERN_LENGTH_LIMIT} characters`,
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
if (NESTED_QUANTIFIER_PATTERN.test(pattern)) {
|
|
26
|
+
return {
|
|
27
|
+
safe: false,
|
|
28
|
+
reason: 'nested quantifiers can trigger catastrophic backtracking (e.g., "(.+)+")',
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
if (REPEATED_BROAD_QUANTIFIER_PATTERN.test(pattern)) {
|
|
32
|
+
return {
|
|
33
|
+
safe: false,
|
|
34
|
+
reason: 'multiple greedy wildcards in one pattern create heavy backtracking',
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
if (BROAD_QUANTIFIER_WITH_BACKREFERENCE_PATTERN.test(pattern)) {
|
|
38
|
+
return {
|
|
39
|
+
safe: false,
|
|
40
|
+
reason: 'combining greedy wildcards with backreferences is prone to ReDoS backtracking',
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
return { safe: true };
|
|
44
|
+
}
|
|
45
|
+
function createRegexWithFallback(pattern, fallback, onUnsafe) {
|
|
46
|
+
if (pattern) {
|
|
47
|
+
const safety = isPatternLikelySafe(pattern);
|
|
48
|
+
if (!safety.safe) {
|
|
49
|
+
onUnsafe?.(pattern, safety.reason ||
|
|
50
|
+
'pattern rejected because it may cause catastrophic backtracking');
|
|
51
|
+
return new RegExp(fallback);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
try {
|
|
55
|
+
return new RegExp(pattern ?? fallback);
|
|
56
|
+
}
|
|
57
|
+
catch (error) {
|
|
58
|
+
if (pattern) {
|
|
59
|
+
onUnsafe?.(pattern, error?.message || 'pattern failed to compile');
|
|
60
|
+
}
|
|
61
|
+
return new RegExp(fallback);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
function normalizeGroupOrder(order) {
|
|
65
|
+
const base = DEFAULT_OPTIONS.groupOrder;
|
|
66
|
+
if (!order || order.length === 0) {
|
|
67
|
+
return base;
|
|
68
|
+
}
|
|
69
|
+
const seen = new Set();
|
|
70
|
+
const combined = [];
|
|
71
|
+
for (const group of [...order, ...base]) {
|
|
72
|
+
if (!seen.has(group)) {
|
|
73
|
+
seen.add(group);
|
|
74
|
+
combined.push(group);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return combined;
|
|
78
|
+
}
|
|
79
|
+
function classifyGroup(info) {
|
|
80
|
+
if (info.isEventHandler) {
|
|
81
|
+
return 'event-handlers';
|
|
82
|
+
}
|
|
83
|
+
if (info.isUtility) {
|
|
84
|
+
return 'utilities';
|
|
85
|
+
}
|
|
86
|
+
return 'other';
|
|
87
|
+
}
|
|
88
|
+
function isNamedFunctionDeclaration(node) {
|
|
89
|
+
return Boolean(node && node.id && node.id.type === 'Identifier');
|
|
90
|
+
}
|
|
91
|
+
function isFunctionExpressionLike(node) {
|
|
92
|
+
return (!!node &&
|
|
93
|
+
(node.type === 'ArrowFunctionExpression' ||
|
|
94
|
+
node.type === 'FunctionExpression' ||
|
|
95
|
+
node.type === 'FunctionDeclaration'));
|
|
96
|
+
}
|
|
97
|
+
function extractFunctionFromVariableDeclaration(statement) {
|
|
98
|
+
if (statement.declarations.length !== 1) {
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
const declarator = statement.declarations[0];
|
|
102
|
+
if (declarator.id.type !== 'Identifier' ||
|
|
103
|
+
!isFunctionExpressionLike(declarator.init)) {
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
return { name: declarator.id.name, fnNode: declarator.init };
|
|
107
|
+
}
|
|
108
|
+
function collectFunctions(programBody, eventHandlerRegex, utilityRegex) {
|
|
109
|
+
const functions = [];
|
|
110
|
+
programBody.forEach((statement, index) => {
|
|
111
|
+
if (statement.type === 'FunctionDeclaration') {
|
|
112
|
+
if (!isNamedFunctionDeclaration(statement)) {
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
const name = statement.id.name;
|
|
116
|
+
const isExported = ASTHelpers_1.ASTHelpers.isNodeExported(statement);
|
|
117
|
+
functions.push({
|
|
118
|
+
name,
|
|
119
|
+
fnNode: statement,
|
|
120
|
+
statementNode: statement,
|
|
121
|
+
isExported,
|
|
122
|
+
isEventHandler: eventHandlerRegex.test(name),
|
|
123
|
+
isUtility: utilityRegex.test(name),
|
|
124
|
+
dependencies: [],
|
|
125
|
+
originalIndex: index,
|
|
126
|
+
});
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
if (statement.type === 'ExportNamedDeclaration' && statement.declaration) {
|
|
130
|
+
if (statement.declaration.type === 'FunctionDeclaration') {
|
|
131
|
+
if (!isNamedFunctionDeclaration(statement.declaration)) {
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
const name = statement.declaration.id.name;
|
|
135
|
+
functions.push({
|
|
136
|
+
name,
|
|
137
|
+
fnNode: statement.declaration,
|
|
138
|
+
statementNode: statement,
|
|
139
|
+
isExported: true,
|
|
140
|
+
isEventHandler: eventHandlerRegex.test(name),
|
|
141
|
+
isUtility: utilityRegex.test(name),
|
|
142
|
+
dependencies: [],
|
|
143
|
+
originalIndex: index,
|
|
144
|
+
});
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
if (statement.declaration.type === 'VariableDeclaration') {
|
|
148
|
+
const extracted = extractFunctionFromVariableDeclaration(statement.declaration);
|
|
149
|
+
if (extracted) {
|
|
150
|
+
const { name, fnNode } = extracted;
|
|
151
|
+
functions.push({
|
|
152
|
+
name,
|
|
153
|
+
fnNode,
|
|
154
|
+
statementNode: statement,
|
|
155
|
+
isExported: true,
|
|
156
|
+
isEventHandler: eventHandlerRegex.test(name),
|
|
157
|
+
isUtility: utilityRegex.test(name),
|
|
158
|
+
dependencies: [],
|
|
159
|
+
originalIndex: index,
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
if (statement.type === 'ExportDefaultDeclaration' &&
|
|
166
|
+
isFunctionExpressionLike(statement.declaration) &&
|
|
167
|
+
statement.declaration.id) {
|
|
168
|
+
const name = statement.declaration.id
|
|
169
|
+
.name;
|
|
170
|
+
functions.push({
|
|
171
|
+
name,
|
|
172
|
+
fnNode: statement.declaration,
|
|
173
|
+
statementNode: statement,
|
|
174
|
+
isExported: true,
|
|
175
|
+
isEventHandler: eventHandlerRegex.test(name),
|
|
176
|
+
isUtility: utilityRegex.test(name),
|
|
177
|
+
dependencies: [],
|
|
178
|
+
originalIndex: index,
|
|
179
|
+
});
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
if (statement.type === 'VariableDeclaration') {
|
|
183
|
+
const extracted = extractFunctionFromVariableDeclaration(statement);
|
|
184
|
+
if (extracted) {
|
|
185
|
+
const { name, fnNode } = extracted;
|
|
186
|
+
const isExported = ASTHelpers_1.ASTHelpers.isNodeExported(statement);
|
|
187
|
+
functions.push({
|
|
188
|
+
name,
|
|
189
|
+
fnNode,
|
|
190
|
+
statementNode: statement,
|
|
191
|
+
isExported,
|
|
192
|
+
isEventHandler: eventHandlerRegex.test(name),
|
|
193
|
+
isUtility: utilityRegex.test(name),
|
|
194
|
+
dependencies: [],
|
|
195
|
+
originalIndex: index,
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
});
|
|
200
|
+
return functions;
|
|
201
|
+
}
|
|
202
|
+
function collectDependencies(fnNode, knownFunctionNames) {
|
|
203
|
+
const dependencies = new Set();
|
|
204
|
+
const visit = (node) => {
|
|
205
|
+
if (!node || !ASTHelpers_1.ASTHelpers.isNode(node)) {
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
if ((node.type === 'CallExpression' || node.type === 'NewExpression') &&
|
|
209
|
+
node.callee &&
|
|
210
|
+
node.callee.type === 'Identifier') {
|
|
211
|
+
const calleeName = node.callee.name;
|
|
212
|
+
if (knownFunctionNames.has(calleeName)) {
|
|
213
|
+
dependencies.add(calleeName);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
Object.values(node).forEach((value) => {
|
|
217
|
+
if (!value || value === node || node.parent === value) {
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
if (Array.isArray(value)) {
|
|
221
|
+
value.forEach((child) => {
|
|
222
|
+
if (ASTHelpers_1.ASTHelpers.isNode(child)) {
|
|
223
|
+
visit(child);
|
|
224
|
+
}
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
else if (ASTHelpers_1.ASTHelpers.isNode(value)) {
|
|
228
|
+
visit(value);
|
|
229
|
+
}
|
|
230
|
+
});
|
|
231
|
+
};
|
|
232
|
+
visit(fnNode.body);
|
|
233
|
+
return [...dependencies];
|
|
234
|
+
}
|
|
235
|
+
function dependencyOrder(functions, direction) {
|
|
236
|
+
const dependencyMap = new Map();
|
|
237
|
+
const originalIndexMap = new Map(functions.map((fn) => [fn.name, fn.originalIndex]));
|
|
238
|
+
functions.forEach((fn) => {
|
|
239
|
+
dependencyMap.set(fn.name, fn.dependencies);
|
|
240
|
+
});
|
|
241
|
+
const visited = new Set();
|
|
242
|
+
const order = [];
|
|
243
|
+
const namesInOriginalOrder = functions
|
|
244
|
+
.slice()
|
|
245
|
+
.sort((a, b) => a.originalIndex - b.originalIndex)
|
|
246
|
+
.map((fn) => fn.name);
|
|
247
|
+
const incomingCount = new Map();
|
|
248
|
+
functions.forEach((fn) => {
|
|
249
|
+
if (!incomingCount.has(fn.name)) {
|
|
250
|
+
incomingCount.set(fn.name, 0);
|
|
251
|
+
}
|
|
252
|
+
fn.dependencies.forEach((dep) => {
|
|
253
|
+
incomingCount.set(dep, (incomingCount.get(dep) || 0) + 1);
|
|
254
|
+
});
|
|
255
|
+
});
|
|
256
|
+
const roots = namesInOriginalOrder.filter((name) => (incomingCount.get(name) || 0) === 0);
|
|
257
|
+
const visit = (name) => {
|
|
258
|
+
if (visited.has(name)) {
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
visited.add(name);
|
|
262
|
+
const deps = dependencyMap
|
|
263
|
+
.get(name)
|
|
264
|
+
?.slice()
|
|
265
|
+
.sort((a, b) => {
|
|
266
|
+
return ((originalIndexMap.get(a) || 0) - (originalIndexMap.get(b) || 0));
|
|
267
|
+
}) || [];
|
|
268
|
+
if (direction === 'callees-first') {
|
|
269
|
+
deps.forEach(visit);
|
|
270
|
+
order.push(name);
|
|
271
|
+
}
|
|
272
|
+
else {
|
|
273
|
+
order.push(name);
|
|
274
|
+
deps.forEach(visit);
|
|
275
|
+
}
|
|
276
|
+
};
|
|
277
|
+
[...roots, ...namesInOriginalOrder].forEach(visit);
|
|
278
|
+
return order;
|
|
279
|
+
}
|
|
280
|
+
function computeExpectedOrder(functions, options) {
|
|
281
|
+
const groupOrder = normalizeGroupOrder(options.groupOrder);
|
|
282
|
+
const dependencySequence = dependencyOrder(functions, options.dependencyDirection);
|
|
283
|
+
const dependencyRank = new Map(dependencySequence.map((name, idx) => [name, idx]));
|
|
284
|
+
const exportRank = (fn) => {
|
|
285
|
+
if (options.exportPlacement === 'ignore') {
|
|
286
|
+
return 0;
|
|
287
|
+
}
|
|
288
|
+
if (options.exportPlacement === 'top') {
|
|
289
|
+
return fn.isExported ? 0 : 1;
|
|
290
|
+
}
|
|
291
|
+
return fn.isExported ? 1 : 0;
|
|
292
|
+
};
|
|
293
|
+
const groupRank = (fn) => groupOrder.indexOf(classifyGroup(fn));
|
|
294
|
+
return functions.slice().sort((a, b) => {
|
|
295
|
+
return (exportRank(a) - exportRank(b) ||
|
|
296
|
+
groupRank(a) - groupRank(b) ||
|
|
297
|
+
(dependencyRank.get(a.name) ?? Number.MAX_SAFE_INTEGER) -
|
|
298
|
+
(dependencyRank.get(b.name) ?? Number.MAX_SAFE_INTEGER) ||
|
|
299
|
+
a.originalIndex - b.originalIndex);
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
function getStatementRangeWithComments(statement, sourceCode, consumedComments, nextStatement) {
|
|
303
|
+
const filterComments = (comments) => (comments || []).filter((comment) => !consumedComments || !consumedComments.has(comment));
|
|
304
|
+
const commentsBefore = filterComments(sourceCode.getCommentsBefore(statement) || []);
|
|
305
|
+
const nextLeadingComments = nextStatement
|
|
306
|
+
? new Set(filterComments(sourceCode.getCommentsBefore(nextStatement) || []))
|
|
307
|
+
: new Set();
|
|
308
|
+
const trailingCandidates = filterComments(sourceCode.getCommentsAfter(statement) || []).filter((comment) => !nextLeadingComments.has(comment));
|
|
309
|
+
const start = commentsBefore.length > 0
|
|
310
|
+
? Math.min(statement.range[0], commentsBefore[0].range[0])
|
|
311
|
+
: statement.range[0];
|
|
312
|
+
const end = trailingCandidates.length > 0
|
|
313
|
+
? Math.max(statement.range[1], trailingCandidates[trailingCandidates.length - 1].range[1])
|
|
314
|
+
: statement.range[1];
|
|
315
|
+
commentsBefore.forEach((comment) => consumedComments?.add(comment));
|
|
316
|
+
trailingCandidates.forEach((comment) => consumedComments?.add(comment));
|
|
317
|
+
return [start, end];
|
|
318
|
+
}
|
|
319
|
+
exports.verticallyGroupRelatedFunctions = (0, createRule_1.createRule)({
|
|
320
|
+
name: 'vertically-group-related-functions',
|
|
321
|
+
meta: {
|
|
322
|
+
type: 'suggestion',
|
|
323
|
+
docs: {
|
|
324
|
+
description: 'Keep top-level functions grouped vertically so callers, exports, and helpers read top-down.',
|
|
325
|
+
recommended: 'error',
|
|
326
|
+
},
|
|
327
|
+
fixable: 'code',
|
|
328
|
+
schema: [
|
|
329
|
+
{
|
|
330
|
+
type: 'object',
|
|
331
|
+
properties: {
|
|
332
|
+
exportPlacement: {
|
|
333
|
+
enum: ['top', 'bottom', 'ignore'],
|
|
334
|
+
default: DEFAULT_OPTIONS.exportPlacement,
|
|
335
|
+
},
|
|
336
|
+
dependencyDirection: {
|
|
337
|
+
enum: ['callers-first', 'callees-first'],
|
|
338
|
+
default: DEFAULT_OPTIONS.dependencyDirection,
|
|
339
|
+
},
|
|
340
|
+
groupOrder: {
|
|
341
|
+
type: 'array',
|
|
342
|
+
items: {
|
|
343
|
+
enum: ['event-handlers', 'utilities', 'other'],
|
|
344
|
+
},
|
|
345
|
+
minItems: 1,
|
|
346
|
+
uniqueItems: true,
|
|
347
|
+
},
|
|
348
|
+
eventHandlerPattern: {
|
|
349
|
+
type: 'string',
|
|
350
|
+
},
|
|
351
|
+
utilityPattern: {
|
|
352
|
+
type: 'string',
|
|
353
|
+
},
|
|
354
|
+
},
|
|
355
|
+
additionalProperties: false,
|
|
356
|
+
},
|
|
357
|
+
],
|
|
358
|
+
messages: {
|
|
359
|
+
misorderedFunction: 'Function "{{name}}" is out of order: {{reason}}. Move it {{placement}} to keep related call chains grouped so readers can scan the file top-down.',
|
|
360
|
+
},
|
|
361
|
+
},
|
|
362
|
+
defaultOptions: DEFAULT_OPTIONS_ARRAY,
|
|
363
|
+
create(context, [options]) {
|
|
364
|
+
const normalizedOptions = {
|
|
365
|
+
...DEFAULT_OPTIONS,
|
|
366
|
+
...options,
|
|
367
|
+
};
|
|
368
|
+
const warnUnsafePattern = (key, value, reason) => {
|
|
369
|
+
const truncatedValue = value.length > 120 ? `${value.slice(0, 117)}...` : value;
|
|
370
|
+
console.warn(`[vertically-group-related-functions] ${key} "${truncatedValue}" rejected: ${reason}. Falling back to safe defaults.`);
|
|
371
|
+
};
|
|
372
|
+
const eventHandlerRegex = createRegexWithFallback(options?.eventHandlerPattern, DEFAULT_OPTIONS.eventHandlerPattern, options?.eventHandlerPattern
|
|
373
|
+
? (value, reason) => warnUnsafePattern('eventHandlerPattern', value, reason)
|
|
374
|
+
: undefined);
|
|
375
|
+
const utilityRegex = createRegexWithFallback(options?.utilityPattern, DEFAULT_OPTIONS.utilityPattern, options?.utilityPattern
|
|
376
|
+
? (value, reason) => warnUnsafePattern('utilityPattern', value, reason)
|
|
377
|
+
: undefined);
|
|
378
|
+
return {
|
|
379
|
+
'Program:exit'(node) {
|
|
380
|
+
const functions = collectFunctions(node.body, eventHandlerRegex, utilityRegex);
|
|
381
|
+
if (functions.length < 2) {
|
|
382
|
+
return;
|
|
383
|
+
}
|
|
384
|
+
const uniqueNames = new Set(functions.map((fn) => fn.name));
|
|
385
|
+
if (uniqueNames.size !== functions.length) {
|
|
386
|
+
return;
|
|
387
|
+
}
|
|
388
|
+
const knownNames = new Set(functions.map((fn) => fn.name));
|
|
389
|
+
functions.forEach((fn) => {
|
|
390
|
+
fn.dependencies = collectDependencies(fn.fnNode, knownNames);
|
|
391
|
+
});
|
|
392
|
+
const expectedOrderInfos = computeExpectedOrder(functions, normalizedOptions);
|
|
393
|
+
const expectedNames = expectedOrderInfos.map((fn) => fn.name);
|
|
394
|
+
const actualNames = functions.map((fn) => fn.name);
|
|
395
|
+
if (expectedNames.length === actualNames.length &&
|
|
396
|
+
expectedNames.every((name, idx) => name === actualNames[idx])) {
|
|
397
|
+
return;
|
|
398
|
+
}
|
|
399
|
+
const misplacedIndex = actualNames.findIndex((name, idx) => name !== expectedNames[idx]);
|
|
400
|
+
if (misplacedIndex === -1) {
|
|
401
|
+
return;
|
|
402
|
+
}
|
|
403
|
+
const misplacedInfo = functions[misplacedIndex];
|
|
404
|
+
const targetIndex = expectedNames.indexOf(misplacedInfo.name);
|
|
405
|
+
const anchorName = targetIndex > 0 ? expectedNames[targetIndex - 1] : null;
|
|
406
|
+
const dependencyReason = misplacedInfo.dependencies.length > 0
|
|
407
|
+
? normalizedOptions.dependencyDirection === 'callees-first'
|
|
408
|
+
? `it calls ${misplacedInfo.dependencies.join(', ')} and helpers should precede the callers that depend on them when dependencyDirection is "callees-first"`
|
|
409
|
+
: `it calls ${misplacedInfo.dependencies.join(', ')} and callers should sit above the helpers they invoke`
|
|
410
|
+
: 'keep related functions adjacent';
|
|
411
|
+
const exportReason = normalizedOptions.exportPlacement === 'ignore'
|
|
412
|
+
? ''
|
|
413
|
+
: normalizedOptions.exportPlacement === 'top'
|
|
414
|
+
? 'exports stay at the top of the file'
|
|
415
|
+
: 'exports stay at the bottom of the file';
|
|
416
|
+
const group = classifyGroup(misplacedInfo);
|
|
417
|
+
const groupOrder = normalizeGroupOrder(normalizedOptions.groupOrder);
|
|
418
|
+
const groupReason = groupOrder.indexOf(group) > 0
|
|
419
|
+
? `${group.replace('-', ' ')} should follow the configured group order`
|
|
420
|
+
: '';
|
|
421
|
+
const reasons = [dependencyReason, exportReason, groupReason]
|
|
422
|
+
.filter(Boolean)
|
|
423
|
+
.join('; ');
|
|
424
|
+
const placement = anchorName
|
|
425
|
+
? `after "${anchorName}"`
|
|
426
|
+
: 'at the start of the function block';
|
|
427
|
+
const sourceCode = context.getSourceCode();
|
|
428
|
+
const functionStatements = new Set(functions.map((fn) => fn.statementNode));
|
|
429
|
+
const sourceOrderedInfos = functions
|
|
430
|
+
.slice()
|
|
431
|
+
.sort((a, b) => a.originalIndex - b.originalIndex);
|
|
432
|
+
const consumedComments = new Set();
|
|
433
|
+
const statementRanges = new Map();
|
|
434
|
+
sourceOrderedInfos.forEach((info, idx) => {
|
|
435
|
+
const nextInfo = sourceOrderedInfos[idx + 1];
|
|
436
|
+
const [rangeStart, rangeEnd] = getStatementRangeWithComments(info.statementNode, sourceCode, consumedComments, nextInfo?.statementNode);
|
|
437
|
+
statementRanges.set(info.statementNode, [rangeStart, rangeEnd]);
|
|
438
|
+
});
|
|
439
|
+
const firstFunctionIndex = node.body.findIndex((statement) => functionStatements.has(statement));
|
|
440
|
+
const lastFunctionIndex = node.body.length -
|
|
441
|
+
1 -
|
|
442
|
+
[...node.body]
|
|
443
|
+
.reverse()
|
|
444
|
+
.findIndex((statement) => functionStatements.has(statement));
|
|
445
|
+
context.report({
|
|
446
|
+
node: misplacedInfo.statementNode,
|
|
447
|
+
messageId: 'misorderedFunction',
|
|
448
|
+
data: {
|
|
449
|
+
name: misplacedInfo.name,
|
|
450
|
+
reason: reasons,
|
|
451
|
+
placement,
|
|
452
|
+
},
|
|
453
|
+
fix(fixer) {
|
|
454
|
+
if (firstFunctionIndex === -1 ||
|
|
455
|
+
lastFunctionIndex === -1 ||
|
|
456
|
+
firstFunctionIndex > lastFunctionIndex) {
|
|
457
|
+
return null;
|
|
458
|
+
}
|
|
459
|
+
const slice = node.body.slice(firstFunctionIndex, lastFunctionIndex + 1);
|
|
460
|
+
const blockContainsOnlyFunctions = slice.every((statement) => functionStatements.has(statement));
|
|
461
|
+
if (!blockContainsOnlyFunctions) {
|
|
462
|
+
return null;
|
|
463
|
+
}
|
|
464
|
+
const [start] = statementRanges.get(node.body[firstFunctionIndex]) ||
|
|
465
|
+
getStatementRangeWithComments(node.body[firstFunctionIndex], sourceCode);
|
|
466
|
+
const [, end] = statementRanges.get(node.body[lastFunctionIndex]) ||
|
|
467
|
+
getStatementRangeWithComments(node.body[lastFunctionIndex], sourceCode);
|
|
468
|
+
const textParts = expectedOrderInfos.map((fn) => {
|
|
469
|
+
const [fnStart, fnEnd] = statementRanges.get(fn.statementNode) ||
|
|
470
|
+
getStatementRangeWithComments(fn.statementNode, sourceCode);
|
|
471
|
+
return sourceCode.text.slice(fnStart, fnEnd);
|
|
472
|
+
});
|
|
473
|
+
return fixer.replaceTextRange([start, end], textParts.join('\n\n'));
|
|
474
|
+
},
|
|
475
|
+
});
|
|
476
|
+
},
|
|
477
|
+
};
|
|
478
|
+
},
|
|
479
|
+
});
|
|
480
|
+
//# sourceMappingURL=vertically-group-related-functions.js.map
|
|
@@ -1,6 +1,10 @@
|
|
|
1
|
-
import { TSESTree } from '@typescript-eslint/utils';
|
|
1
|
+
import { TSESLint, TSESTree } from '@typescript-eslint/utils';
|
|
2
2
|
import { Graph } from './graph/ClassGraphBuilder';
|
|
3
3
|
export declare class ASTHelpers {
|
|
4
|
+
/**
|
|
5
|
+
* Finds a variable by name in the scope chain starting from the given scope.
|
|
6
|
+
*/
|
|
7
|
+
static findVariableInScope(scope: TSESLint.Scope.Scope, name: string): TSESLint.Scope.Variable | null;
|
|
4
8
|
static blockIncludesIdentifier(block: TSESTree.BlockStatement): boolean;
|
|
5
9
|
static declarationIncludesIdentifier(node: TSESTree.Node | null): boolean;
|
|
6
10
|
static classMethodDependenciesOf(node: TSESTree.Node | null, graph: Graph, className: string): string[];
|
|
@@ -9,4 +13,12 @@ export declare class ASTHelpers {
|
|
|
9
13
|
static isNodeExported(node: TSESTree.Node): boolean;
|
|
10
14
|
static returnsJSX(node: TSESTree.Node): boolean;
|
|
11
15
|
static hasParameters(node: TSESTree.ArrowFunctionExpression | TSESTree.FunctionExpression | TSESTree.FunctionDeclaration): boolean;
|
|
16
|
+
/**
|
|
17
|
+
* Helper to get ancestors of a node in a way that is compatible with both ESLint v8 and v9.
|
|
18
|
+
* In ESLint v9, context.getAncestors() is deprecated and moved to context.sourceCode.getAncestors(node).
|
|
19
|
+
*/
|
|
20
|
+
static getAncestors(context: {
|
|
21
|
+
sourceCode?: unknown;
|
|
22
|
+
getAncestors?: () => TSESTree.Node[];
|
|
23
|
+
}, node: TSESTree.Node): TSESTree.Node[];
|
|
12
24
|
}
|
package/lib/utils/ASTHelpers.js
CHANGED
|
@@ -2,6 +2,21 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.ASTHelpers = void 0;
|
|
4
4
|
class ASTHelpers {
|
|
5
|
+
/**
|
|
6
|
+
* Finds a variable by name in the scope chain starting from the given scope.
|
|
7
|
+
*/
|
|
8
|
+
static findVariableInScope(scope, name) {
|
|
9
|
+
let current = scope;
|
|
10
|
+
while (current) {
|
|
11
|
+
const variable = current.set?.get(name) ??
|
|
12
|
+
current.variables.find((v) => v.name === name);
|
|
13
|
+
if (variable) {
|
|
14
|
+
return variable;
|
|
15
|
+
}
|
|
16
|
+
current = current.upper;
|
|
17
|
+
}
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
5
20
|
static blockIncludesIdentifier(block) {
|
|
6
21
|
for (const statement of block.body) {
|
|
7
22
|
if (ASTHelpers.declarationIncludesIdentifier(statement)) {
|
|
@@ -14,9 +29,15 @@ class ASTHelpers {
|
|
|
14
29
|
if (!node) {
|
|
15
30
|
return false;
|
|
16
31
|
}
|
|
32
|
+
// Gracefully handle ParenthesizedExpression without widening AST node types
|
|
33
|
+
if (node.type === 'ParenthesizedExpression') {
|
|
34
|
+
return this.declarationIncludesIdentifier(node.expression);
|
|
35
|
+
}
|
|
17
36
|
switch (node.type) {
|
|
18
37
|
case 'TSNonNullExpression':
|
|
19
38
|
return this.declarationIncludesIdentifier(node.expression);
|
|
39
|
+
case 'TSSatisfiesExpression':
|
|
40
|
+
return this.declarationIncludesIdentifier(node.expression);
|
|
20
41
|
case 'ArrayPattern':
|
|
21
42
|
return node.elements.some((element) => ASTHelpers.declarationIncludesIdentifier(element));
|
|
22
43
|
case 'ObjectPattern':
|
|
@@ -114,6 +135,10 @@ class ASTHelpers {
|
|
|
114
135
|
if (!node) {
|
|
115
136
|
return dependencies;
|
|
116
137
|
}
|
|
138
|
+
// Gracefully handle ParenthesizedExpression without widening AST node types
|
|
139
|
+
if (node.type === 'ParenthesizedExpression') {
|
|
140
|
+
return ASTHelpers.classMethodDependenciesOf(node.expression, graph, className);
|
|
141
|
+
}
|
|
117
142
|
switch (node.type) {
|
|
118
143
|
case 'MethodDefinition':
|
|
119
144
|
const functionBody = node.value.body;
|
|
@@ -354,6 +379,15 @@ class ASTHelpers {
|
|
|
354
379
|
static hasParameters(node) {
|
|
355
380
|
return node.params && node.params.length > 0;
|
|
356
381
|
}
|
|
382
|
+
/**
|
|
383
|
+
* Helper to get ancestors of a node in a way that is compatible with both ESLint v8 and v9.
|
|
384
|
+
* In ESLint v9, context.getAncestors() is deprecated and moved to context.sourceCode.getAncestors(node).
|
|
385
|
+
*/
|
|
386
|
+
static getAncestors(context, node) {
|
|
387
|
+
const sourceCode = context.sourceCode;
|
|
388
|
+
return (sourceCode?.getAncestors?.(node) ??
|
|
389
|
+
(context.getAncestors ? context.getAncestors() : []));
|
|
390
|
+
}
|
|
357
391
|
}
|
|
358
392
|
exports.ASTHelpers = ASTHelpers;
|
|
359
393
|
//# sourceMappingURL=ASTHelpers.js.map
|
package/lib/utils/createRule.js
CHANGED
|
@@ -2,5 +2,5 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.createRule = void 0;
|
|
4
4
|
const utils_1 = require("@typescript-eslint/utils");
|
|
5
|
-
exports.createRule = utils_1.ESLintUtils.RuleCreator((name) => `https://github.com/BluMintInc/eslint-custom-rules/
|
|
5
|
+
exports.createRule = utils_1.ESLintUtils.RuleCreator((name) => `https://github.com/BluMintInc/eslint-custom-rules/docs/rules/${name}.md`);
|
|
6
6
|
//# sourceMappingURL=createRule.js.map
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { TSESTree } from '@typescript-eslint/utils';
|
|
2
|
+
/**
|
|
3
|
+
* Extracts a stable, human-readable name from method/property/member nodes.
|
|
4
|
+
* Use the options to align prefixing and computed-key handling with rule semantics.
|
|
5
|
+
*/
|
|
6
|
+
export type GetMethodNameOptions = {
|
|
7
|
+
/**
|
|
8
|
+
* Prefix applied to private identifiers:
|
|
9
|
+
* - undefined (default): empty prefix
|
|
10
|
+
* - null: return the raw name without any prefix
|
|
11
|
+
* - string (e.g., '#'): prepend the given prefix
|
|
12
|
+
*/
|
|
13
|
+
privateIdentifierPrefix?: string | null;
|
|
14
|
+
/**
|
|
15
|
+
* When true (default), computed keys fall back to source text; when false, returns empty string.
|
|
16
|
+
*/
|
|
17
|
+
computedFallbackToText?: boolean;
|
|
18
|
+
};
|
|
19
|
+
export declare const getMethodName: (method: TSESTree.MethodDefinition | TSESTree.TSAbstractMethodDefinition | TSESTree.PropertyDefinition, sourceCode: Readonly<{
|
|
20
|
+
getText: (node: TSESTree.Node) => string;
|
|
21
|
+
}>, options?: GetMethodNameOptions) => string;
|
|
22
|
+
export declare const getPropertyName: (key: TSESTree.PropertyName | TSESTree.PrivateIdentifier, sourceCode: Readonly<{
|
|
23
|
+
getText: (node: TSESTree.Node) => string;
|
|
24
|
+
}>, options?: GetMethodNameOptions) => string;
|
|
25
|
+
export declare const getMemberExpressionName: (member: TSESTree.MemberExpression, sourceCode: Readonly<{
|
|
26
|
+
getText: (node: TSESTree.Node) => string;
|
|
27
|
+
}>, options?: GetMethodNameOptions) => string;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.getMemberExpressionName = exports.getPropertyName = exports.getMethodName = void 0;
|
|
4
|
+
const utils_1 = require("@typescript-eslint/utils");
|
|
5
|
+
const getKeyName = (key, sourceCode, { privateIdentifierPrefix = '', computedFallbackToText = true, }) => {
|
|
6
|
+
if (key.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
7
|
+
return key.name;
|
|
8
|
+
}
|
|
9
|
+
if (key.type === utils_1.AST_NODE_TYPES.PrivateIdentifier) {
|
|
10
|
+
if (privateIdentifierPrefix === null) {
|
|
11
|
+
return key.name;
|
|
12
|
+
}
|
|
13
|
+
return `${privateIdentifierPrefix}${key.name}`;
|
|
14
|
+
}
|
|
15
|
+
if (key.type === utils_1.AST_NODE_TYPES.Literal) {
|
|
16
|
+
if (key.value === null || key.value === undefined) {
|
|
17
|
+
return '';
|
|
18
|
+
}
|
|
19
|
+
return String(key.value);
|
|
20
|
+
}
|
|
21
|
+
return computedFallbackToText ? sourceCode.getText(key) : '';
|
|
22
|
+
};
|
|
23
|
+
const getMethodName = (method, sourceCode, options = {}) => {
|
|
24
|
+
return getKeyName(method.key, sourceCode, options);
|
|
25
|
+
};
|
|
26
|
+
exports.getMethodName = getMethodName;
|
|
27
|
+
const getPropertyName = (key, sourceCode, options = {}) => {
|
|
28
|
+
return getKeyName(key, sourceCode, options);
|
|
29
|
+
};
|
|
30
|
+
exports.getPropertyName = getPropertyName;
|
|
31
|
+
const getMemberExpressionName = (member, sourceCode, options = {}) => {
|
|
32
|
+
return getKeyName(member.property, sourceCode, options);
|
|
33
|
+
};
|
|
34
|
+
exports.getMemberExpressionName = getMemberExpressionName;
|
|
35
|
+
//# sourceMappingURL=getMethodName.js.map
|
|
@@ -86,7 +86,6 @@ class ClassGraphSorterReadability extends ClassGraphSorter_1.ClassGraphSorter {
|
|
|
86
86
|
return dfsSortedNodes;
|
|
87
87
|
}
|
|
88
88
|
}
|
|
89
|
-
exports.ClassGraphSorterReadability = ClassGraphSorterReadability;
|
|
90
89
|
ClassGraphSorterReadability.MODIFIER_PRIORITY_MAP = {
|
|
91
90
|
isStatic: [true, false],
|
|
92
91
|
accessibility: ['public', undefined, 'private'],
|
|
@@ -98,4 +97,5 @@ ClassGraphSorterReadability.SEARCH_NODE_PRIORITY_FUNCTIONS = [
|
|
|
98
97
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
99
98
|
(method, _methods) => method.dependencies.length === 0,
|
|
100
99
|
];
|
|
100
|
+
exports.ClassGraphSorterReadability = ClassGraphSorterReadability;
|
|
101
101
|
//# sourceMappingURL=ClassGraphSorterReadability.js.map
|
package/lib/utils/ruleTester.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.ruleTesterJson = exports.ruleTesterJsx = exports.ruleTesterTs = void 0;
|
|
3
|
+
exports.ruleTesterMarkdown = exports.ruleTesterJson = exports.ruleTesterJsx = exports.ruleTesterTs = void 0;
|
|
4
4
|
const utils_1 = require("@typescript-eslint/utils");
|
|
5
5
|
const eslint_1 = require("eslint");
|
|
6
6
|
exports.ruleTesterTs = new utils_1.ESLintUtils.RuleTester({
|
|
@@ -20,4 +20,7 @@ exports.ruleTesterJson = new eslint_1.RuleTester({
|
|
|
20
20
|
ecmaVersion: 2020,
|
|
21
21
|
},
|
|
22
22
|
});
|
|
23
|
+
exports.ruleTesterMarkdown = new eslint_1.RuleTester({
|
|
24
|
+
parser: require.resolve('markdown-eslint-parser'),
|
|
25
|
+
});
|
|
23
26
|
//# sourceMappingURL=ruleTester.js.map
|