@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,277 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* @fileoverview Enforce the use of event.params over .ref.parent.id in Firebase change handlers
|
|
4
|
+
* @author BluMint
|
|
5
|
+
*/
|
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
exports.preferParamsOverParentId = void 0;
|
|
8
|
+
const utils_1 = require("@typescript-eslint/utils");
|
|
9
|
+
const createRule_1 = require("../utils/createRule");
|
|
10
|
+
const HANDLER_TYPES = new Set([
|
|
11
|
+
'DocumentChangeHandler',
|
|
12
|
+
'DocumentChangeHandlerTransaction',
|
|
13
|
+
'RealtimeDbChangeHandler',
|
|
14
|
+
'RealtimeDbChangeHandlerTransaction',
|
|
15
|
+
]);
|
|
16
|
+
const getQualifiedNameIdentifier = (typeName) => {
|
|
17
|
+
if (typeName.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
18
|
+
return typeName.name;
|
|
19
|
+
}
|
|
20
|
+
if (typeName.type === utils_1.AST_NODE_TYPES.TSQualifiedName &&
|
|
21
|
+
typeName.right.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
22
|
+
return typeName.right.name;
|
|
23
|
+
}
|
|
24
|
+
return null;
|
|
25
|
+
};
|
|
26
|
+
const checkTypeAnnotationForHandler = (typeNode) => {
|
|
27
|
+
switch (typeNode.type) {
|
|
28
|
+
case utils_1.AST_NODE_TYPES.TSTypeReference: {
|
|
29
|
+
const typeIdentifier = getQualifiedNameIdentifier(typeNode.typeName);
|
|
30
|
+
return typeIdentifier ? HANDLER_TYPES.has(typeIdentifier) : false;
|
|
31
|
+
}
|
|
32
|
+
case utils_1.AST_NODE_TYPES.TSUnionType:
|
|
33
|
+
return typeNode.types.some(checkTypeAnnotationForHandler);
|
|
34
|
+
case utils_1.AST_NODE_TYPES.TSIntersectionType:
|
|
35
|
+
return typeNode.types.some(checkTypeAnnotationForHandler);
|
|
36
|
+
default:
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
const findTypeAnnotationInContext = (node) => {
|
|
41
|
+
if (node.parent?.type === utils_1.AST_NODE_TYPES.VariableDeclarator &&
|
|
42
|
+
node.parent.id.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
43
|
+
node.parent.id.typeAnnotation) {
|
|
44
|
+
return node.parent.id.typeAnnotation;
|
|
45
|
+
}
|
|
46
|
+
if (node.parent?.type === utils_1.AST_NODE_TYPES.AssignmentExpression &&
|
|
47
|
+
node.parent.left.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
48
|
+
node.parent.left.typeAnnotation) {
|
|
49
|
+
return node.parent.left.typeAnnotation;
|
|
50
|
+
}
|
|
51
|
+
if (node.parent?.type === utils_1.AST_NODE_TYPES.Property &&
|
|
52
|
+
node.parent.value === node) {
|
|
53
|
+
let current = node.parent.parent;
|
|
54
|
+
while (current) {
|
|
55
|
+
if (current.type === utils_1.AST_NODE_TYPES.VariableDeclarator &&
|
|
56
|
+
current.id.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
57
|
+
current.id.typeAnnotation) {
|
|
58
|
+
return current.id.typeAnnotation;
|
|
59
|
+
}
|
|
60
|
+
current = current.parent;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return undefined;
|
|
64
|
+
};
|
|
65
|
+
const isFirebaseChangeHandler = (node) => {
|
|
66
|
+
if (node.type !== utils_1.AST_NODE_TYPES.ArrowFunctionExpression &&
|
|
67
|
+
node.type !== utils_1.AST_NODE_TYPES.FunctionExpression &&
|
|
68
|
+
node.type !== utils_1.AST_NODE_TYPES.FunctionDeclaration) {
|
|
69
|
+
return false;
|
|
70
|
+
}
|
|
71
|
+
const typeAnnotation = findTypeAnnotationInContext(node);
|
|
72
|
+
if (!typeAnnotation) {
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
75
|
+
return checkTypeAnnotationForHandler(typeAnnotation.typeAnnotation);
|
|
76
|
+
};
|
|
77
|
+
const isParentIdAccess = (node) => {
|
|
78
|
+
if (node.property.type !== utils_1.AST_NODE_TYPES.Identifier ||
|
|
79
|
+
node.property.name !== 'id') {
|
|
80
|
+
return { isMatch: false, depth: 0 };
|
|
81
|
+
}
|
|
82
|
+
const chain = [];
|
|
83
|
+
let current = node.object;
|
|
84
|
+
while (current && current.type === utils_1.AST_NODE_TYPES.MemberExpression) {
|
|
85
|
+
if (current.property.type !== utils_1.AST_NODE_TYPES.Identifier) {
|
|
86
|
+
return { isMatch: false, depth: 0 };
|
|
87
|
+
}
|
|
88
|
+
chain.unshift(current.property.name);
|
|
89
|
+
current = current.object;
|
|
90
|
+
}
|
|
91
|
+
if (chain.length < 2) {
|
|
92
|
+
return { isMatch: false, depth: 0 };
|
|
93
|
+
}
|
|
94
|
+
const refIndex = chain.lastIndexOf('ref');
|
|
95
|
+
if (refIndex === -1) {
|
|
96
|
+
return { isMatch: false, depth: 0 };
|
|
97
|
+
}
|
|
98
|
+
const parentSegment = chain.slice(refIndex + 1);
|
|
99
|
+
if (parentSegment.length === 0) {
|
|
100
|
+
return { isMatch: false, depth: 0 };
|
|
101
|
+
}
|
|
102
|
+
const invalidParent = parentSegment.some((segment) => segment !== 'parent');
|
|
103
|
+
if (invalidParent) {
|
|
104
|
+
return { isMatch: false, depth: 0 };
|
|
105
|
+
}
|
|
106
|
+
const depth = parentSegment.length;
|
|
107
|
+
return { isMatch: depth > 0, depth };
|
|
108
|
+
};
|
|
109
|
+
const getParentParamName = (depth) => {
|
|
110
|
+
if (depth === 1) {
|
|
111
|
+
return 'userId';
|
|
112
|
+
}
|
|
113
|
+
if (depth === 2) {
|
|
114
|
+
return 'parentId';
|
|
115
|
+
}
|
|
116
|
+
return `parent${depth}Id`;
|
|
117
|
+
};
|
|
118
|
+
const findHandlerFunction = (node, handlerNodes) => {
|
|
119
|
+
let current = node;
|
|
120
|
+
while (current) {
|
|
121
|
+
if (handlerNodes.has(current)) {
|
|
122
|
+
return current;
|
|
123
|
+
}
|
|
124
|
+
current = current.parent;
|
|
125
|
+
}
|
|
126
|
+
return null;
|
|
127
|
+
};
|
|
128
|
+
const hasOptionalChaining = (node) => {
|
|
129
|
+
let current = node;
|
|
130
|
+
while (current && current.type === utils_1.AST_NODE_TYPES.MemberExpression) {
|
|
131
|
+
if (current.optional) {
|
|
132
|
+
return true;
|
|
133
|
+
}
|
|
134
|
+
current = current.object;
|
|
135
|
+
}
|
|
136
|
+
return false;
|
|
137
|
+
};
|
|
138
|
+
const findParamsInPattern = (pattern) => {
|
|
139
|
+
for (const prop of pattern.properties) {
|
|
140
|
+
if (prop.type === utils_1.AST_NODE_TYPES.Property &&
|
|
141
|
+
prop.key.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
142
|
+
prop.key.name === 'params') {
|
|
143
|
+
if (prop.value.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
144
|
+
return { identifier: prop.value.name };
|
|
145
|
+
}
|
|
146
|
+
if (prop.value.type === utils_1.AST_NODE_TYPES.ObjectPattern) {
|
|
147
|
+
const properties = new Map();
|
|
148
|
+
for (const p of prop.value.properties) {
|
|
149
|
+
if (p.type === utils_1.AST_NODE_TYPES.Property &&
|
|
150
|
+
p.key.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
151
|
+
p.value.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
152
|
+
properties.set(p.key.name, p.value.name);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
return { properties };
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
return null;
|
|
160
|
+
};
|
|
161
|
+
const getParamsInScope = (handlerNode) => {
|
|
162
|
+
if (handlerNode.type !== utils_1.AST_NODE_TYPES.ArrowFunctionExpression &&
|
|
163
|
+
handlerNode.type !== utils_1.AST_NODE_TYPES.FunctionExpression &&
|
|
164
|
+
handlerNode.type !== utils_1.AST_NODE_TYPES.FunctionDeclaration) {
|
|
165
|
+
return null;
|
|
166
|
+
}
|
|
167
|
+
const firstParam = handlerNode.params[0];
|
|
168
|
+
if (!firstParam) {
|
|
169
|
+
return null;
|
|
170
|
+
}
|
|
171
|
+
if (firstParam.type === utils_1.AST_NODE_TYPES.ObjectPattern) {
|
|
172
|
+
const paramsInScope = findParamsInPattern(firstParam);
|
|
173
|
+
if (paramsInScope) {
|
|
174
|
+
return paramsInScope;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
if (handlerNode.body &&
|
|
178
|
+
handlerNode.body.type === utils_1.AST_NODE_TYPES.BlockStatement) {
|
|
179
|
+
const eventParamName = firstParam.type === utils_1.AST_NODE_TYPES.Identifier ? firstParam.name : 'event';
|
|
180
|
+
for (const statement of handlerNode.body.body) {
|
|
181
|
+
if (statement.type !== utils_1.AST_NODE_TYPES.VariableDeclaration) {
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
184
|
+
for (const declarator of statement.declarations) {
|
|
185
|
+
if (declarator.id.type !== utils_1.AST_NODE_TYPES.ObjectPattern ||
|
|
186
|
+
!declarator.init ||
|
|
187
|
+
declarator.init.type !== utils_1.AST_NODE_TYPES.Identifier ||
|
|
188
|
+
declarator.init.name !== eventParamName) {
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
191
|
+
const paramsInScope = findParamsInPattern(declarator.id);
|
|
192
|
+
if (paramsInScope) {
|
|
193
|
+
return paramsInScope;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
return null;
|
|
199
|
+
};
|
|
200
|
+
exports.preferParamsOverParentId = (0, createRule_1.createRule)({
|
|
201
|
+
name: 'prefer-params-over-parent-id',
|
|
202
|
+
meta: {
|
|
203
|
+
type: 'suggestion',
|
|
204
|
+
docs: {
|
|
205
|
+
description: 'Prefer handler params for parent IDs instead of traversing ref.parent.id so Firebase triggers stay aligned with path templates and type-safe.',
|
|
206
|
+
recommended: 'error',
|
|
207
|
+
},
|
|
208
|
+
fixable: 'code',
|
|
209
|
+
schema: [],
|
|
210
|
+
messages: {
|
|
211
|
+
preferParams: [
|
|
212
|
+
"What's wrong: This code reads an ID via `ref.parent...id` instead of using the trigger's params.",
|
|
213
|
+
'',
|
|
214
|
+
'Why it matters: Walking `ref.parent` ties the handler to the current path depth; when collections change, it can yield the wrong ID (or a collection name) and bypasses the typed params the trigger provides.',
|
|
215
|
+
'',
|
|
216
|
+
'How to fix: Read the ID from `params.{{paramName}}` (or destructure `const { params } = event` and then access `params.{{paramName}}`).',
|
|
217
|
+
].join('\n'),
|
|
218
|
+
},
|
|
219
|
+
},
|
|
220
|
+
defaultOptions: [],
|
|
221
|
+
create(context) {
|
|
222
|
+
const handlerNodes = new Set();
|
|
223
|
+
return {
|
|
224
|
+
// Track Firebase change handler functions
|
|
225
|
+
'FunctionDeclaration, FunctionExpression, ArrowFunctionExpression'(node) {
|
|
226
|
+
if (isFirebaseChangeHandler(node)) {
|
|
227
|
+
handlerNodes.add(node);
|
|
228
|
+
}
|
|
229
|
+
},
|
|
230
|
+
// Detect .ref.parent.id patterns
|
|
231
|
+
MemberExpression(node) {
|
|
232
|
+
const parentAccess = isParentIdAccess(node);
|
|
233
|
+
if (parentAccess.isMatch) {
|
|
234
|
+
const handlerNode = findHandlerFunction(node, handlerNodes);
|
|
235
|
+
if (handlerNode) {
|
|
236
|
+
const hasOptional = hasOptionalChaining(node);
|
|
237
|
+
const paramsInScope = getParamsInScope(handlerNode);
|
|
238
|
+
if (!paramsInScope) {
|
|
239
|
+
context.report({
|
|
240
|
+
node,
|
|
241
|
+
messageId: 'preferParams',
|
|
242
|
+
data: {
|
|
243
|
+
paramName: getParentParamName(parentAccess.depth),
|
|
244
|
+
},
|
|
245
|
+
});
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
const paramSuggestion = getParentParamName(parentAccess.depth);
|
|
249
|
+
let replacement = null;
|
|
250
|
+
if (paramsInScope.identifier) {
|
|
251
|
+
replacement = hasOptional
|
|
252
|
+
? `${paramsInScope.identifier}?.${paramSuggestion}`
|
|
253
|
+
: `${paramsInScope.identifier}.${paramSuggestion}`;
|
|
254
|
+
}
|
|
255
|
+
else if (paramsInScope.properties) {
|
|
256
|
+
const localName = paramsInScope.properties.get(paramSuggestion);
|
|
257
|
+
if (localName) {
|
|
258
|
+
replacement = localName;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
context.report({
|
|
262
|
+
node,
|
|
263
|
+
messageId: 'preferParams',
|
|
264
|
+
data: {
|
|
265
|
+
paramName: paramSuggestion,
|
|
266
|
+
},
|
|
267
|
+
fix: replacement
|
|
268
|
+
? (fixer) => fixer.replaceText(node, replacement)
|
|
269
|
+
: undefined,
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
},
|
|
274
|
+
};
|
|
275
|
+
},
|
|
276
|
+
});
|
|
277
|
+
//# sourceMappingURL=prefer-params-over-parent-id.js.map
|
|
@@ -40,8 +40,8 @@ exports.preferSettingsObject = (0, createRule_1.createRule)({
|
|
|
40
40
|
},
|
|
41
41
|
],
|
|
42
42
|
messages: {
|
|
43
|
-
tooManyParams:
|
|
44
|
-
sameTypeParams: 'Function
|
|
43
|
+
tooManyParams: "Function accepts {{count}} positional parameters (limit {{minimum}}). Long positional lists hide each argument's meaning and make call sites easy to mis-order. Pass a single settings object so callers name each field and keep the call readable.",
|
|
44
|
+
sameTypeParams: 'Function receives {{paramCount}} positional parameters including multiple "{{type}}" values. Repeated types in positional arguments invite swapped values and force callers to remember parameter order. Replace the positional list with a settings object so each value is labeled and self-documenting.',
|
|
45
45
|
},
|
|
46
46
|
},
|
|
47
47
|
defaultOptions: [defaultOptions],
|
|
@@ -60,10 +60,12 @@ exports.preferSettingsObject = (0, createRule_1.createRule)({
|
|
|
60
60
|
? typeNode.typeName.name
|
|
61
61
|
: 'unknown';
|
|
62
62
|
// If there are type parameters, include them in the type signature
|
|
63
|
-
if (typeNode.typeParameters &&
|
|
63
|
+
if (typeNode.typeParameters &&
|
|
64
|
+
typeNode.typeParameters.params.length > 0) {
|
|
65
|
+
const sourceCode = context.sourceCode;
|
|
64
66
|
const typeParams = typeNode.typeParameters.params
|
|
65
|
-
.map(param => param
|
|
66
|
-
.join('
|
|
67
|
+
.map((param) => sourceCode.getText(param))
|
|
68
|
+
.join(', ');
|
|
67
69
|
return `${typeName}<${typeParams}>`;
|
|
68
70
|
}
|
|
69
71
|
return typeName;
|
|
@@ -78,10 +80,12 @@ exports.preferSettingsObject = (0, createRule_1.createRule)({
|
|
|
78
80
|
? typeNode.typeName.name
|
|
79
81
|
: 'unknown';
|
|
80
82
|
// If there are type parameters, include them in the type signature
|
|
81
|
-
if (typeNode.typeParameters &&
|
|
83
|
+
if (typeNode.typeParameters &&
|
|
84
|
+
typeNode.typeParameters.params.length > 0) {
|
|
85
|
+
const sourceCode = context.sourceCode;
|
|
82
86
|
const typeParams = typeNode.typeParameters.params
|
|
83
|
-
.map(param => param
|
|
84
|
-
.join('
|
|
87
|
+
.map((param) => sourceCode.getText(param))
|
|
88
|
+
.join(', ');
|
|
85
89
|
return `${typeName}<${typeParams}>`;
|
|
86
90
|
}
|
|
87
91
|
return typeName;
|
|
@@ -96,17 +100,17 @@ exports.preferSettingsObject = (0, createRule_1.createRule)({
|
|
|
96
100
|
}
|
|
97
101
|
return 'unknown';
|
|
98
102
|
}
|
|
99
|
-
function
|
|
103
|
+
function findDuplicateParameterType(params) {
|
|
100
104
|
const typeMap = new Map();
|
|
101
105
|
for (const param of params) {
|
|
102
106
|
const type = getParameterType(param);
|
|
103
107
|
const count = (typeMap.get(type) || 0) + 1;
|
|
104
108
|
typeMap.set(type, count);
|
|
105
109
|
if (count > 1) {
|
|
106
|
-
return
|
|
110
|
+
return type;
|
|
107
111
|
}
|
|
108
112
|
}
|
|
109
|
-
return
|
|
113
|
+
return null;
|
|
110
114
|
}
|
|
111
115
|
function isBuiltInOrThirdParty(node) {
|
|
112
116
|
// Check if the node is part of a new expression (constructor call)
|
|
@@ -287,8 +291,10 @@ exports.preferSettingsObject = (0, createRule_1.createRule)({
|
|
|
287
291
|
// Check if the function name or its type annotation suggests it's a handler with transaction
|
|
288
292
|
if (functionName.includes('Transaction') ||
|
|
289
293
|
functionName.includes('WithTransaction') ||
|
|
290
|
-
(node.parent.id.typeAnnotation?.typeAnnotation.type ===
|
|
291
|
-
|
|
294
|
+
(node.parent.id.typeAnnotation?.typeAnnotation.type ===
|
|
295
|
+
utils_1.AST_NODE_TYPES.TSTypeReference &&
|
|
296
|
+
node.parent.id.typeAnnotation.typeAnnotation.typeName.type ===
|
|
297
|
+
utils_1.AST_NODE_TYPES.Identifier &&
|
|
292
298
|
(node.parent.id.typeAnnotation.typeAnnotation.typeName.name.includes('Transaction') ||
|
|
293
299
|
node.parent.id.typeAnnotation.typeAnnotation.typeName.name.includes('WithTransaction')))) {
|
|
294
300
|
return true;
|
|
@@ -306,16 +312,18 @@ exports.preferSettingsObject = (0, createRule_1.createRule)({
|
|
|
306
312
|
context.report({
|
|
307
313
|
node,
|
|
308
314
|
messageId: 'tooManyParams',
|
|
309
|
-
data: { count: params.length },
|
|
315
|
+
data: { count: params.length, minimum: minParams },
|
|
310
316
|
});
|
|
311
317
|
return;
|
|
312
318
|
}
|
|
313
319
|
// Then check for same type parameters if enabled
|
|
314
320
|
if (finalOptions.checkSameTypeParameters && params.length >= 2) {
|
|
315
|
-
|
|
321
|
+
const duplicateType = findDuplicateParameterType(params);
|
|
322
|
+
if (duplicateType) {
|
|
316
323
|
context.report({
|
|
317
324
|
node,
|
|
318
325
|
messageId: 'sameTypeParams',
|
|
326
|
+
data: { type: duplicateType, paramCount: params.length },
|
|
319
327
|
});
|
|
320
328
|
}
|
|
321
329
|
}
|
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.preferTypeAliasOverTypeofConstant = void 0;
|
|
4
|
+
const utils_1 = require("@typescript-eslint/utils");
|
|
5
|
+
const createRule_1 = require("../utils/createRule");
|
|
6
|
+
const PAREN_TYPE = utils_1.AST_NODE_TYPES.TSParenthesizedType ??
|
|
7
|
+
'TSParenthesizedType';
|
|
8
|
+
const isParenthesizedType = (node) => {
|
|
9
|
+
return node.type === PAREN_TYPE;
|
|
10
|
+
};
|
|
11
|
+
/** Utility to convert CONSTANT_CASE or snake/kebab to PascalCase */
|
|
12
|
+
function toPascalCase(name) {
|
|
13
|
+
return name
|
|
14
|
+
.split(/[_-]+/)
|
|
15
|
+
.filter(Boolean)
|
|
16
|
+
.map((part) => part.charAt(0).toUpperCase() + part.slice(1).toLowerCase())
|
|
17
|
+
.join('');
|
|
18
|
+
}
|
|
19
|
+
/** Determine if a value initializer represents a function or class (which we should ignore). */
|
|
20
|
+
function isFunctionOrClassInitializer(init) {
|
|
21
|
+
if (!init)
|
|
22
|
+
return false;
|
|
23
|
+
return (init.type === utils_1.AST_NODE_TYPES.FunctionExpression ||
|
|
24
|
+
init.type === utils_1.AST_NODE_TYPES.ArrowFunctionExpression ||
|
|
25
|
+
init.type === utils_1.AST_NODE_TYPES.ClassExpression);
|
|
26
|
+
}
|
|
27
|
+
/** Determine if node is a simple constant literal or object/array literal possibly with `as const` */
|
|
28
|
+
function isConstantLikeInitializer(init) {
|
|
29
|
+
if (!init)
|
|
30
|
+
return false;
|
|
31
|
+
if (isFunctionOrClassInitializer(init))
|
|
32
|
+
return false;
|
|
33
|
+
switch (init.type) {
|
|
34
|
+
case utils_1.AST_NODE_TYPES.Literal:
|
|
35
|
+
return true;
|
|
36
|
+
case utils_1.AST_NODE_TYPES.TemplateLiteral:
|
|
37
|
+
return init.expressions.length === 0;
|
|
38
|
+
case utils_1.AST_NODE_TYPES.ObjectExpression:
|
|
39
|
+
return true;
|
|
40
|
+
case utils_1.AST_NODE_TYPES.ArrayExpression:
|
|
41
|
+
return true;
|
|
42
|
+
case utils_1.AST_NODE_TYPES.TSAsExpression:
|
|
43
|
+
return isConstantLikeInitializer(init.expression);
|
|
44
|
+
case utils_1.AST_NODE_TYPES.UnaryExpression: {
|
|
45
|
+
// treat unary constants (e.g., -1) as constant-like; exclude typeof
|
|
46
|
+
if (init.operator === 'typeof')
|
|
47
|
+
return false;
|
|
48
|
+
return isConstantLikeInitializer(init.argument);
|
|
49
|
+
}
|
|
50
|
+
default:
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
/** Traverse a TSType and collect referenced identifier names (e.g., A, B in A | B). */
|
|
55
|
+
function collectReferencedTypeNames(node, acc = new Set()) {
|
|
56
|
+
switch (node.type) {
|
|
57
|
+
case utils_1.AST_NODE_TYPES.TSTypeReference: {
|
|
58
|
+
if (node.typeName.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
59
|
+
acc.add(node.typeName.name);
|
|
60
|
+
}
|
|
61
|
+
if (node.typeParameters) {
|
|
62
|
+
for (const p of node.typeParameters.params)
|
|
63
|
+
collectReferencedTypeNames(p, acc);
|
|
64
|
+
}
|
|
65
|
+
break;
|
|
66
|
+
}
|
|
67
|
+
case utils_1.AST_NODE_TYPES.TSUnionType:
|
|
68
|
+
case utils_1.AST_NODE_TYPES.TSIntersectionType: {
|
|
69
|
+
for (const t of node.types)
|
|
70
|
+
collectReferencedTypeNames(t, acc);
|
|
71
|
+
break;
|
|
72
|
+
}
|
|
73
|
+
case utils_1.AST_NODE_TYPES.TSArrayType: {
|
|
74
|
+
const arr = node;
|
|
75
|
+
collectReferencedTypeNames(arr.elementType, acc);
|
|
76
|
+
break;
|
|
77
|
+
}
|
|
78
|
+
case utils_1.AST_NODE_TYPES.TSTypeOperator: {
|
|
79
|
+
const op = node;
|
|
80
|
+
if (op.typeAnnotation)
|
|
81
|
+
collectReferencedTypeNames(op.typeAnnotation, acc);
|
|
82
|
+
break;
|
|
83
|
+
}
|
|
84
|
+
case utils_1.AST_NODE_TYPES.TSTupleType: {
|
|
85
|
+
const tup = node;
|
|
86
|
+
for (const e of tup.elementTypes)
|
|
87
|
+
collectReferencedTypeNames(e, acc);
|
|
88
|
+
break;
|
|
89
|
+
}
|
|
90
|
+
default: {
|
|
91
|
+
if (isParenthesizedType(node)) {
|
|
92
|
+
collectReferencedTypeNames(node.typeAnnotation, acc);
|
|
93
|
+
}
|
|
94
|
+
break;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return acc;
|
|
98
|
+
}
|
|
99
|
+
/** Collects module-level consts and type aliases for quick lookup */
|
|
100
|
+
function collectTopLevelContext(program) {
|
|
101
|
+
const topLevelConstInitByName = new Map();
|
|
102
|
+
const topLevelConstNodeByName = new Map();
|
|
103
|
+
const typeAliasByName = new Map();
|
|
104
|
+
const importedValueNames = new Set();
|
|
105
|
+
const importedTypeNames = new Set();
|
|
106
|
+
for (const stmt of program.body) {
|
|
107
|
+
if (stmt.type === utils_1.AST_NODE_TYPES.ImportDeclaration) {
|
|
108
|
+
const isTypeImport = stmt.importKind === 'type';
|
|
109
|
+
for (const spec of stmt.specifiers) {
|
|
110
|
+
// import type { Foo } from '...'
|
|
111
|
+
// import { Foo } from '...' as value import
|
|
112
|
+
if (spec.type === utils_1.AST_NODE_TYPES.ImportSpecifier) {
|
|
113
|
+
if (isTypeImport || spec.importKind === 'type') {
|
|
114
|
+
importedTypeNames.add(spec.local.name);
|
|
115
|
+
}
|
|
116
|
+
else {
|
|
117
|
+
importedValueNames.add(spec.local.name);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
else if (spec.type === utils_1.AST_NODE_TYPES.ImportDefaultSpecifier) {
|
|
121
|
+
if (isTypeImport) {
|
|
122
|
+
importedTypeNames.add(spec.local.name);
|
|
123
|
+
}
|
|
124
|
+
else {
|
|
125
|
+
importedValueNames.add(spec.local.name);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
else if (spec.type === utils_1.AST_NODE_TYPES.ImportNamespaceSpecifier) {
|
|
129
|
+
// Namespace imports expose names via MemberExpression; not tracked individually
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
else if (stmt.type === utils_1.AST_NODE_TYPES.VariableDeclaration &&
|
|
134
|
+
stmt.kind === 'const') {
|
|
135
|
+
for (const decl of stmt.declarations) {
|
|
136
|
+
if (decl.id.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
137
|
+
topLevelConstInitByName.set(decl.id.name, decl.init ?? null);
|
|
138
|
+
topLevelConstNodeByName.set(decl.id.name, decl);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
else if (stmt.type === utils_1.AST_NODE_TYPES.TSTypeAliasDeclaration) {
|
|
143
|
+
typeAliasByName.set(stmt.id.name, stmt);
|
|
144
|
+
}
|
|
145
|
+
else if (stmt.type === utils_1.AST_NODE_TYPES.ExportNamedDeclaration &&
|
|
146
|
+
stmt.declaration) {
|
|
147
|
+
// Handle exported declarations: export const FOO = ...; export type Foo = ...
|
|
148
|
+
const decl = stmt.declaration;
|
|
149
|
+
if (decl.type === utils_1.AST_NODE_TYPES.VariableDeclaration &&
|
|
150
|
+
decl.kind === 'const') {
|
|
151
|
+
for (const d of decl.declarations) {
|
|
152
|
+
if (d.id.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
153
|
+
topLevelConstInitByName.set(d.id.name, d.init ?? null);
|
|
154
|
+
topLevelConstNodeByName.set(d.id.name, d);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
else if (decl.type === utils_1.AST_NODE_TYPES.TSTypeAliasDeclaration) {
|
|
159
|
+
typeAliasByName.set(decl.id.name, decl);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
return {
|
|
164
|
+
topLevelConstInitByName,
|
|
165
|
+
topLevelConstNodeByName,
|
|
166
|
+
typeAliasByName,
|
|
167
|
+
importedValueNames,
|
|
168
|
+
importedTypeNames,
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
exports.preferTypeAliasOverTypeofConstant = (0, createRule_1.createRule)({
|
|
172
|
+
name: 'prefer-type-alias-over-typeof-constant',
|
|
173
|
+
meta: {
|
|
174
|
+
type: 'suggestion',
|
|
175
|
+
docs: {
|
|
176
|
+
description: 'Prefer named type aliases over `typeof` on same-file global constants; ensure types are declared before constants.',
|
|
177
|
+
recommended: 'error',
|
|
178
|
+
},
|
|
179
|
+
hasSuggestions: false,
|
|
180
|
+
schema: [],
|
|
181
|
+
messages: {
|
|
182
|
+
preferTypeAlias: 'Type derived from same-file constant "{{constName}}" couples the type to its runtime value and scatters literal unions across the file. Create a named alias such as "{{suggested}}" and reference that alias instead of `typeof {{constName}}` so the type stays stable even if the value changes.',
|
|
183
|
+
defineTypeBeforeConstant: 'Type alias "{{typeName}}" appears after constant "{{constName}}", which hides the shape from readers and risks using an undeclared alias. Declare "{{typeName}}" before "{{constName}}" so the type is visible where it is consumed and can be reused consistently.',
|
|
184
|
+
},
|
|
185
|
+
},
|
|
186
|
+
defaultOptions: [],
|
|
187
|
+
create(context) {
|
|
188
|
+
let collected;
|
|
189
|
+
return {
|
|
190
|
+
Program(program) {
|
|
191
|
+
collected = collectTopLevelContext(program);
|
|
192
|
+
},
|
|
193
|
+
// Enforce that type aliases referenced by const annotations appear before the const
|
|
194
|
+
VariableDeclarator(node) {
|
|
195
|
+
if (!collected)
|
|
196
|
+
return;
|
|
197
|
+
if (node.id.type !== utils_1.AST_NODE_TYPES.Identifier)
|
|
198
|
+
return;
|
|
199
|
+
const constName = node.id.name;
|
|
200
|
+
// Only for top-level consts
|
|
201
|
+
const decl = collected.topLevelConstNodeByName.get(constName);
|
|
202
|
+
if (!decl || decl !== node)
|
|
203
|
+
return;
|
|
204
|
+
const parentDecl = node.parent;
|
|
205
|
+
if (!parentDecl || parentDecl.kind !== 'const')
|
|
206
|
+
return;
|
|
207
|
+
const typeAnn = node.id.typeAnnotation?.typeAnnotation;
|
|
208
|
+
if (!typeAnn)
|
|
209
|
+
return;
|
|
210
|
+
for (const typeName of collectReferencedTypeNames(typeAnn)) {
|
|
211
|
+
// If imported type, allow any order
|
|
212
|
+
if (collected.importedTypeNames.has(typeName))
|
|
213
|
+
continue;
|
|
214
|
+
const typeDecl = collected.typeAliasByName.get(typeName);
|
|
215
|
+
if (typeDecl && typeDecl.range[0] > parentDecl.range[0]) {
|
|
216
|
+
context.report({
|
|
217
|
+
node: node.id,
|
|
218
|
+
messageId: 'defineTypeBeforeConstant',
|
|
219
|
+
data: { typeName, constName },
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
},
|
|
224
|
+
// Core rule: flag `typeof CONST` when CONST is a same-file top-level const and constant-like
|
|
225
|
+
TSTypeQuery(node) {
|
|
226
|
+
if (!collected)
|
|
227
|
+
return;
|
|
228
|
+
// Skip `keyof typeof X`
|
|
229
|
+
if (node.parent &&
|
|
230
|
+
node.parent.type === utils_1.AST_NODE_TYPES.TSTypeOperator &&
|
|
231
|
+
node.parent.operator === 'keyof') {
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
if (node.exprName.type !== utils_1.AST_NODE_TYPES.Identifier) {
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
const name = node.exprName.name;
|
|
238
|
+
// Imported values: do not enforce (may suggest in future)
|
|
239
|
+
if (collected.importedValueNames.has(name)) {
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
const init = collected.topLevelConstInitByName.get(name);
|
|
243
|
+
if (init === undefined) {
|
|
244
|
+
// Not a top-level const in this file
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
if (!isConstantLikeInitializer(init)) {
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
context.report({
|
|
251
|
+
node,
|
|
252
|
+
messageId: 'preferTypeAlias',
|
|
253
|
+
data: { constName: name, suggested: toPascalCase(name) },
|
|
254
|
+
});
|
|
255
|
+
},
|
|
256
|
+
};
|
|
257
|
+
},
|
|
258
|
+
});
|
|
259
|
+
//# sourceMappingURL=prefer-type-alias-over-typeof-constant.js.map
|
|
@@ -12,7 +12,9 @@ exports.preferTypeOverInterface = (0, createRule_1.createRule)({
|
|
|
12
12
|
},
|
|
13
13
|
schema: [],
|
|
14
14
|
messages: {
|
|
15
|
-
preferType: '
|
|
15
|
+
preferType: 'Interface "{{interfaceName}}" should be declared as a type alias. ' +
|
|
16
|
+
'Interfaces can merge across declarations and extend in chains, which fragments the resulting shape across files and makes composition harder to predict and trace. ' +
|
|
17
|
+
'Replace `interface` with `type` and use intersections (for example, `type {{interfaceName}} = Base & { field: string }`) to keep the contract closed and predictable.',
|
|
16
18
|
},
|
|
17
19
|
fixable: 'code',
|
|
18
20
|
},
|
|
@@ -23,8 +25,11 @@ exports.preferTypeOverInterface = (0, createRule_1.createRule)({
|
|
|
23
25
|
context.report({
|
|
24
26
|
node,
|
|
25
27
|
messageId: 'preferType',
|
|
28
|
+
data: {
|
|
29
|
+
interfaceName: node.id.name,
|
|
30
|
+
},
|
|
26
31
|
fix(fixer) {
|
|
27
|
-
const sourceCode = context.
|
|
32
|
+
const sourceCode = context.sourceCode;
|
|
28
33
|
const openingBrace = sourceCode.getTokenAfter(node.id, {
|
|
29
34
|
filter: (token) => token.value === '{',
|
|
30
35
|
});
|