@blumintinc/eslint-plugin-blumint 1.13.0 → 1.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +169 -139
- package/lib/index.js +200 -9
- package/lib/rules/array-methods-this-context.js +1 -1
- package/lib/rules/avoid-utils-directory.js +0 -4
- package/lib/rules/class-methods-read-top-to-bottom.js +24 -5
- package/lib/rules/consistent-callback-naming.js +43 -4
- package/lib/rules/dynamic-https-errors.d.ts +1 -1
- package/lib/rules/dynamic-https-errors.js +134 -44
- package/lib/rules/enforce-assert-safe-object-key.js +11 -11
- package/lib/rules/enforce-boolean-naming-prefixes.js +11 -235
- package/lib/rules/enforce-callback-memo.js +1 -1
- package/lib/rules/enforce-centralized-mock-firestore.js +1 -1
- package/lib/rules/enforce-console-error.js +35 -7
- package/lib/rules/enforce-date-ttime.d.ts +1 -0
- package/lib/rules/enforce-date-ttime.js +156 -0
- 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.d.ts +2 -1
- package/lib/rules/enforce-dynamic-firebase-imports.js +3 -2
- package/lib/rules/enforce-early-destructuring.d.ts +2 -0
- package/lib/rules/enforce-early-destructuring.js +980 -0
- package/lib/rules/enforce-empty-object-check.d.ts +11 -0
- package/lib/rules/enforce-empty-object-check.js +502 -0
- package/lib/rules/enforce-exported-function-types.js +1 -1
- package/lib/rules/enforce-f-extension-for-entry-points.d.ts +8 -0
- package/lib/rules/enforce-f-extension-for-entry-points.js +283 -0
- package/lib/rules/enforce-fieldpath-syntax-in-docsetter.js +100 -50
- package/lib/rules/enforce-firestore-doc-ref-generic.js +15 -3
- package/lib/rules/enforce-firestore-facade.js +4 -2
- package/lib/rules/enforce-firestore-set-merge.js +3 -4
- package/lib/rules/enforce-global-constants.js +4 -4
- package/lib/rules/enforce-id-capitalization.js +1 -1
- package/lib/rules/enforce-identifiable-firestore-type.js +108 -58
- package/lib/rules/enforce-memoize-async.js +71 -21
- package/lib/rules/enforce-memoize-getters.js +39 -15
- package/lib/rules/enforce-microdiff.js +1 -1
- package/lib/rules/enforce-object-literal-as-const.js +4 -4
- package/lib/rules/enforce-positive-naming.js +4 -0
- package/lib/rules/enforce-props-argument-name.js +44 -18
- package/lib/rules/enforce-safe-stringify.js +1 -1
- package/lib/rules/enforce-serializable-params.js +3 -3
- package/lib/rules/enforce-singular-type-names.js +1 -1
- package/lib/rules/enforce-stable-hash-spread-props.d.ts +13 -0
- package/lib/rules/enforce-stable-hash-spread-props.js +342 -0
- package/lib/rules/enforce-storage-context.d.ts +9 -0
- package/lib/rules/enforce-storage-context.js +646 -0
- package/lib/rules/enforce-timestamp-now.js +23 -7
- package/lib/rules/enforce-transform-memoization.d.ts +4 -0
- package/lib/rules/enforce-transform-memoization.js +416 -0
- package/lib/rules/enforce-typescript-markdown-code-blocks.js +7 -3
- package/lib/rules/enforce-unique-cursor-headers.d.ts +16 -0
- package/lib/rules/enforce-unique-cursor-headers.js +365 -0
- package/lib/rules/enforce-verb-noun-naming.js +3818 -4644
- package/lib/rules/ensure-pointer-events-none.js +28 -2
- package/lib/rules/export-if-in-doubt.js +27 -4
- package/lib/rules/extract-global-constants.js +40 -13
- package/lib/rules/fast-deep-equal-over-microdiff.js +29 -39
- package/lib/rules/firestore-transaction-reads-before-writes.js +50 -4
- package/lib/rules/flatten-push-calls.d.ts +2 -0
- package/lib/rules/flatten-push-calls.js +428 -0
- package/lib/rules/global-const-style.js +66 -23
- 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 +801 -0
- package/lib/rules/memo-nested-react-components.d.ts +8 -0
- package/lib/rules/memo-nested-react-components.js +415 -0
- package/lib/rules/no-always-true-false-conditions.d.ts +2 -1
- package/lib/rules/no-always-true-false-conditions.js +68 -8
- package/lib/rules/no-array-length-in-deps.js +9 -3
- package/lib/rules/no-async-foreach.js +215 -12
- package/lib/rules/no-circular-references.d.ts +2 -1
- package/lib/rules/no-circular-references.js +31 -32
- package/lib/rules/no-class-instance-destructuring.js +55 -12
- package/lib/rules/no-complex-cloud-params.js +7 -3
- package/lib/rules/no-compositing-layer-props.js +2 -2
- package/lib/rules/no-conditional-literals-in-jsx.js +37 -12
- package/lib/rules/no-console-error.d.ts +9 -0
- package/lib/rules/no-console-error.js +527 -0
- package/lib/rules/no-curly-brackets-around-commented-properties.d.ts +2 -0
- package/lib/rules/no-curly-brackets-around-commented-properties.js +258 -0
- package/lib/rules/no-empty-dependency-use-callbacks.d.ts +11 -0
- package/lib/rules/no-empty-dependency-use-callbacks.js +576 -0
- package/lib/rules/no-entire-object-hook-deps.js +52 -7
- package/lib/rules/no-excessive-parent-chain.js +4 -1
- package/lib/rules/no-explicit-return-type.d.ts +2 -1
- package/lib/rules/no-explicit-return-type.js +215 -26
- package/lib/rules/no-filter-without-return.js +5 -1
- package/lib/rules/no-firestore-jest-mock.d.ts +2 -1
- package/lib/rules/no-firestore-jest-mock.js +126 -8
- package/lib/rules/no-firestore-object-arrays.js +67 -12
- package/lib/rules/no-handler-suffix.d.ts +12 -0
- package/lib/rules/no-handler-suffix.js +305 -0
- package/lib/rules/no-hungarian.js +1 -1
- package/lib/rules/no-inline-component-prop.d.ts +10 -0
- package/lib/rules/no-inline-component-prop.js +465 -0
- package/lib/rules/no-jsx-in-hooks.js +6 -1
- package/lib/rules/no-jsx-whitespace-literal.js +8 -2
- package/lib/rules/no-margin-properties.js +6 -6
- package/lib/rules/no-memoize-on-static.js +9 -1
- package/lib/rules/no-misleading-boolean-prefixes.js +7 -3
- package/lib/rules/no-misused-switch-case.js +22 -1
- package/lib/rules/no-mixed-firestore-transactions.d.ts +3 -1
- package/lib/rules/no-mixed-firestore-transactions.js +296 -34
- package/lib/rules/no-mock-firebase-admin.js +5 -2
- package/lib/rules/no-object-values-on-strings.js +10 -2
- package/lib/rules/no-overridable-method-calls-in-constructor.js +29 -17
- package/lib/rules/no-passthrough-getters.d.ts +2 -2
- package/lib/rules/no-passthrough-getters.js +120 -2
- package/lib/rules/no-redundant-annotation-assertion.d.ts +2 -0
- package/lib/rules/no-redundant-annotation-assertion.js +402 -0
- package/lib/rules/no-redundant-param-types.js +16 -6
- package/lib/rules/no-redundant-this-params.d.ts +3 -0
- package/lib/rules/no-redundant-this-params.js +508 -0
- package/lib/rules/no-redundant-usecallback-wrapper.js +16 -3
- package/lib/rules/no-res-error-status-in-onrequest.d.ts +4 -0
- package/lib/rules/no-res-error-status-in-onrequest.js +521 -0
- package/lib/rules/no-restricted-properties-fix.js +11 -10
- package/lib/rules/no-separate-loading-state.js +7 -16
- package/lib/rules/no-stale-state-across-await.js +1 -1
- package/lib/rules/no-type-assertion-returns.js +43 -49
- package/lib/rules/no-undefined-null-passthrough.js +61 -31
- package/lib/rules/no-unmemoized-memo-without-props.d.ts +8 -0
- package/lib/rules/no-unmemoized-memo-without-props.js +426 -0
- package/lib/rules/no-unnecessary-destructuring-rename.d.ts +2 -0
- package/lib/rules/no-unnecessary-destructuring-rename.js +344 -0
- package/lib/rules/no-unnecessary-destructuring.js +14 -4
- package/lib/rules/no-unnecessary-verb-suffix.js +2 -1
- package/lib/rules/no-unpinned-dependencies.js +36 -5
- package/lib/rules/no-unused-props.d.ts +2 -2
- package/lib/rules/no-unused-props.js +295 -91
- package/lib/rules/no-unused-usestate.js +6 -2
- package/lib/rules/no-useless-fragment.js +28 -2
- package/lib/rules/no-useless-usememo-primitives.d.ts +9 -0
- package/lib/rules/no-useless-usememo-primitives.js +393 -0
- package/lib/rules/no-usememo-for-pass-by-value.d.ts +13 -0
- package/lib/rules/no-usememo-for-pass-by-value.js +757 -0
- package/lib/rules/no-uuidv4-base62-as-key.js +18 -13
- package/lib/rules/omit-index-html.d.ts +2 -1
- package/lib/rules/omit-index-html.js +62 -7
- package/lib/rules/optimize-object-boolean-conditions.js +6 -4
- package/lib/rules/parallelize-async-operations.js +143 -59
- package/lib/rules/prefer-batch-operations.d.ts +1 -3
- package/lib/rules/prefer-batch-operations.js +32 -5
- package/lib/rules/prefer-block-comments-for-declarations.js +9 -4
- package/lib/rules/prefer-clone-deep.js +3 -3
- package/lib/rules/prefer-destructuring-no-class.d.ts +2 -1
- package/lib/rules/prefer-destructuring-no-class.js +244 -59
- package/lib/rules/prefer-docsetter-setall.d.ts +2 -0
- package/lib/rules/prefer-docsetter-setall.js +243 -0
- package/lib/rules/prefer-field-paths-in-transforms.js +4 -3
- package/lib/rules/prefer-fragment-component.js +1 -1
- package/lib/rules/prefer-fragment-shorthand.js +2 -1
- package/lib/rules/prefer-getter-over-parameterless-method.d.ts +13 -0
- package/lib/rules/prefer-getter-over-parameterless-method.js +648 -0
- package/lib/rules/prefer-global-router-state-key.js +88 -13
- package/lib/rules/prefer-memoized-props.d.ts +3 -0
- package/lib/rules/prefer-memoized-props.js +445 -0
- package/lib/rules/prefer-next-dynamic.js +60 -69
- package/lib/rules/prefer-nullish-coalescing-boolean-props.js +118 -8
- package/lib/rules/prefer-nullish-coalescing-override.d.ts +2 -1
- package/lib/rules/prefer-nullish-coalescing-override.js +11 -5
- package/lib/rules/prefer-params-over-parent-id.js +221 -172
- package/lib/rules/prefer-settings-object.js +27 -10
- package/lib/rules/prefer-type-alias-over-typeof-constant.js +9 -0
- package/lib/rules/prefer-type-over-interface.js +7 -2
- package/lib/rules/prefer-url-tostring-over-tojson.js +6 -3
- package/lib/rules/prefer-use-deep-compare-memo.js +8 -5
- package/lib/rules/prefer-usecallback-over-usememo-for-functions.js +1 -1
- package/lib/rules/prefer-usememo-over-useeffect-usestate.js +1 -1
- package/lib/rules/prefer-utility-function-over-private-static.js +34 -2
- package/lib/rules/prevent-children-clobber.d.ts +2 -0
- package/lib/rules/prevent-children-clobber.js +540 -0
- package/lib/rules/react-memoize-literals.d.ts +4 -0
- package/lib/rules/react-memoize-literals.js +614 -0
- package/lib/rules/react-usememo-should-be-component.js +2 -2
- package/lib/rules/require-hooks-default-params.js +17 -16
- package/lib/rules/require-https-error-cause.d.ts +4 -0
- package/lib/rules/require-https-error-cause.js +155 -0
- package/lib/rules/require-https-error.js +43 -21
- package/lib/rules/require-image-optimized.js +1 -1
- package/lib/rules/require-memo.js +18 -10
- package/lib/rules/require-memoize-jsx-returners.d.ts +3 -0
- package/lib/rules/require-memoize-jsx-returners.js +485 -0
- package/lib/rules/require-usememo-object-literals.js +2 -3
- package/lib/rules/test-file-location-enforcement.js +1 -1
- package/lib/rules/use-latest-callback.js +6 -5
- package/lib/rules/vertically-group-related-functions.d.ts +16 -0
- package/lib/rules/vertically-group-related-functions.js +480 -0
- package/lib/utils/ASTHelpers.d.ts +47 -2
- package/lib/utils/ASTHelpers.js +370 -112
- package/lib/utils/getMethodName.d.ts +27 -0
- package/lib/utils/getMethodName.js +35 -0
- package/lib/utils/tsTypeClassifier.d.ts +30 -0
- package/lib/utils/tsTypeClassifier.js +149 -0
- package/package.json +3 -1
|
@@ -13,210 +13,259 @@ const HANDLER_TYPES = new Set([
|
|
|
13
13
|
'RealtimeDbChangeHandler',
|
|
14
14
|
'RealtimeDbChangeHandlerTransaction',
|
|
15
15
|
]);
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
create(context) {
|
|
32
|
-
// Track functions that are Firebase change handlers
|
|
33
|
-
const handlerFunctions = new Set();
|
|
34
|
-
function findTypeAnnotationInContext(node) {
|
|
35
|
-
// Check variable declarator type annotation
|
|
36
|
-
if (node.parent?.type === utils_1.AST_NODE_TYPES.VariableDeclarator &&
|
|
37
|
-
node.parent.id.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
38
|
-
node.parent.id.typeAnnotation) {
|
|
39
|
-
return node.parent.id.typeAnnotation;
|
|
40
|
-
}
|
|
41
|
-
// Check assignment expression with type annotation
|
|
42
|
-
if (node.parent?.type === utils_1.AST_NODE_TYPES.AssignmentExpression &&
|
|
43
|
-
node.parent.left.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
44
|
-
node.parent.left.typeAnnotation) {
|
|
45
|
-
return node.parent.left.typeAnnotation;
|
|
46
|
-
}
|
|
47
|
-
// Check property definition type annotation
|
|
48
|
-
if (node.parent?.type === utils_1.AST_NODE_TYPES.Property &&
|
|
49
|
-
node.parent.value === node) {
|
|
50
|
-
// Look up the tree for type annotation
|
|
51
|
-
let current = node.parent.parent;
|
|
52
|
-
while (current) {
|
|
53
|
-
if (current.type === utils_1.AST_NODE_TYPES.VariableDeclarator &&
|
|
54
|
-
current.id.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
55
|
-
current.id.typeAnnotation) {
|
|
56
|
-
return current.id.typeAnnotation;
|
|
57
|
-
}
|
|
58
|
-
current = current.parent;
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
return undefined;
|
|
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;
|
|
62
31
|
}
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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;
|
|
72
59
|
}
|
|
73
|
-
|
|
60
|
+
current = current.parent;
|
|
74
61
|
}
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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;
|
|
81
123
|
}
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
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 };
|
|
87
145
|
}
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
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
|
+
}
|
|
93
154
|
}
|
|
94
|
-
|
|
95
|
-
current = current.object;
|
|
96
|
-
}
|
|
97
|
-
if (chain.length < 2) {
|
|
98
|
-
return { isMatch: false, depth: 0 };
|
|
99
|
-
}
|
|
100
|
-
const refIndex = chain.indexOf('ref');
|
|
101
|
-
if (refIndex === -1) {
|
|
102
|
-
return { isMatch: false, depth: 0 };
|
|
103
|
-
}
|
|
104
|
-
const parentSegment = chain.slice(refIndex + 1);
|
|
105
|
-
if (parentSegment.length === 0) {
|
|
106
|
-
return { isMatch: false, depth: 0 };
|
|
155
|
+
return { properties };
|
|
107
156
|
}
|
|
108
|
-
const invalidParent = parentSegment.some((segment) => segment !== 'parent');
|
|
109
|
-
if (invalidParent) {
|
|
110
|
-
return { isMatch: false, depth: 0 };
|
|
111
|
-
}
|
|
112
|
-
const depth = parentSegment.length;
|
|
113
|
-
return { isMatch: depth > 0, depth };
|
|
114
157
|
}
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
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;
|
|
124
175
|
}
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
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;
|
|
132
183
|
}
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
handlerNode.type === utils_1.AST_NODE_TYPES.FunctionDeclaration) {
|
|
140
|
-
const firstParam = handlerNode.params[0];
|
|
141
|
-
if (!firstParam)
|
|
142
|
-
return false;
|
|
143
|
-
// Check for destructuring pattern: ({ params }) or ({ data, params })
|
|
144
|
-
if (firstParam.type === utils_1.AST_NODE_TYPES.ObjectPattern) {
|
|
145
|
-
return firstParam.properties.some((prop) => {
|
|
146
|
-
if (prop.type === utils_1.AST_NODE_TYPES.Property &&
|
|
147
|
-
prop.key.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
148
|
-
prop.key.name === 'params') {
|
|
149
|
-
return true;
|
|
150
|
-
}
|
|
151
|
-
return false;
|
|
152
|
-
});
|
|
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;
|
|
153
190
|
}
|
|
154
|
-
|
|
155
|
-
if (
|
|
156
|
-
|
|
157
|
-
for (const statement of handlerNode.body.body) {
|
|
158
|
-
if (statement.type === utils_1.AST_NODE_TYPES.VariableDeclaration) {
|
|
159
|
-
for (const declarator of statement.declarations) {
|
|
160
|
-
if (declarator.id.type === utils_1.AST_NODE_TYPES.ObjectPattern &&
|
|
161
|
-
declarator.init &&
|
|
162
|
-
declarator.init.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
163
|
-
// Check if destructuring from event parameter
|
|
164
|
-
const eventParamName = firstParam.type === utils_1.AST_NODE_TYPES.Identifier
|
|
165
|
-
? firstParam.name
|
|
166
|
-
: 'event';
|
|
167
|
-
if (declarator.init.name === eventParamName) {
|
|
168
|
-
return declarator.id.properties.some((prop) => {
|
|
169
|
-
if (prop.type === utils_1.AST_NODE_TYPES.Property &&
|
|
170
|
-
prop.key.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
171
|
-
prop.key.name === 'params') {
|
|
172
|
-
return true;
|
|
173
|
-
}
|
|
174
|
-
return false;
|
|
175
|
-
});
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
}
|
|
191
|
+
const paramsInScope = findParamsInPattern(declarator.id);
|
|
192
|
+
if (paramsInScope) {
|
|
193
|
+
return paramsInScope;
|
|
181
194
|
}
|
|
182
195
|
}
|
|
183
|
-
return false;
|
|
184
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 event.params over ref.parent.id for type-safe Firebase trigger paths.',
|
|
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();
|
|
185
223
|
return {
|
|
186
224
|
// Track Firebase change handler functions
|
|
187
225
|
'FunctionDeclaration, FunctionExpression, ArrowFunctionExpression'(node) {
|
|
188
226
|
if (isFirebaseChangeHandler(node)) {
|
|
189
|
-
|
|
227
|
+
handlerNodes.add(node);
|
|
190
228
|
}
|
|
191
229
|
},
|
|
192
230
|
// Detect .ref.parent.id patterns
|
|
193
231
|
MemberExpression(node) {
|
|
194
232
|
const parentAccess = isParentIdAccess(node);
|
|
195
233
|
if (parentAccess.isMatch) {
|
|
196
|
-
const handlerNode = findHandlerFunction(node);
|
|
234
|
+
const handlerNode = findHandlerFunction(node, handlerNodes);
|
|
197
235
|
if (handlerNode) {
|
|
198
236
|
const hasOptional = hasOptionalChaining(node);
|
|
199
|
-
const paramsInScope =
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
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
|
+
}
|
|
207
261
|
context.report({
|
|
208
262
|
node,
|
|
209
263
|
messageId: 'preferParams',
|
|
210
264
|
data: {
|
|
211
265
|
paramName: paramSuggestion,
|
|
212
266
|
},
|
|
213
|
-
fix:
|
|
214
|
-
? (fixer) =>
|
|
215
|
-
const replacement = hasOptional
|
|
216
|
-
? `params?.${paramSuggestion}`
|
|
217
|
-
: `params.${paramSuggestion}`;
|
|
218
|
-
return fixer.replaceText(node, replacement);
|
|
219
|
-
}
|
|
267
|
+
fix: replacement
|
|
268
|
+
? (fixer) => fixer.replaceText(node, replacement)
|
|
220
269
|
: undefined,
|
|
221
270
|
});
|
|
222
271
|
}
|
|
@@ -47,6 +47,7 @@ exports.preferSettingsObject = (0, createRule_1.createRule)({
|
|
|
47
47
|
defaultOptions: [defaultOptions],
|
|
48
48
|
create(context, [options]) {
|
|
49
49
|
const finalOptions = { ...defaultOptions, ...options };
|
|
50
|
+
const { sourceCode } = context;
|
|
50
51
|
function getParameterType(param) {
|
|
51
52
|
if (param.type === utils_1.AST_NODE_TYPES.AssignmentPattern) {
|
|
52
53
|
return getParameterType(param.left);
|
|
@@ -56,13 +57,10 @@ exports.preferSettingsObject = (0, createRule_1.createRule)({
|
|
|
56
57
|
const typeNode = param.typeAnnotation.typeAnnotation;
|
|
57
58
|
if (typeNode.type === utils_1.AST_NODE_TYPES.TSTypeReference) {
|
|
58
59
|
// Include type parameters in the type signature to differentiate generic types
|
|
59
|
-
const typeName = typeNode.typeName
|
|
60
|
-
? typeNode.typeName.name
|
|
61
|
-
: 'unknown';
|
|
60
|
+
const typeName = sourceCode.getText(typeNode.typeName);
|
|
62
61
|
// If there are type parameters, include them in the type signature
|
|
63
62
|
if (typeNode.typeParameters &&
|
|
64
63
|
typeNode.typeParameters.params.length > 0) {
|
|
65
|
-
const sourceCode = context.getSourceCode();
|
|
66
64
|
const typeParams = typeNode.typeParameters.params
|
|
67
65
|
.map((param) => sourceCode.getText(param))
|
|
68
66
|
.join(', ');
|
|
@@ -76,13 +74,10 @@ exports.preferSettingsObject = (0, createRule_1.createRule)({
|
|
|
76
74
|
const typeNode = param.typeAnnotation.typeAnnotation;
|
|
77
75
|
if (typeNode.type === utils_1.AST_NODE_TYPES.TSTypeReference) {
|
|
78
76
|
// Include type parameters in the type signature to differentiate generic types
|
|
79
|
-
const typeName = typeNode.typeName
|
|
80
|
-
? typeNode.typeName.name
|
|
81
|
-
: 'unknown';
|
|
77
|
+
const typeName = sourceCode.getText(typeNode.typeName);
|
|
82
78
|
// If there are type parameters, include them in the type signature
|
|
83
79
|
if (typeNode.typeParameters &&
|
|
84
80
|
typeNode.typeParameters.params.length > 0) {
|
|
85
|
-
const sourceCode = context.getSourceCode();
|
|
86
81
|
const typeParams = typeNode.typeParameters.params
|
|
87
82
|
.map((param) => sourceCode.getText(param))
|
|
88
83
|
.join(', ');
|
|
@@ -98,12 +93,14 @@ exports.preferSettingsObject = (0, createRule_1.createRule)({
|
|
|
98
93
|
return 'boolean';
|
|
99
94
|
return typeNode.type;
|
|
100
95
|
}
|
|
101
|
-
return
|
|
96
|
+
return null;
|
|
102
97
|
}
|
|
103
98
|
function findDuplicateParameterType(params) {
|
|
104
99
|
const typeMap = new Map();
|
|
105
100
|
for (const param of params) {
|
|
106
101
|
const type = getParameterType(param);
|
|
102
|
+
if (type === null)
|
|
103
|
+
continue;
|
|
107
104
|
const count = (typeMap.get(type) || 0) + 1;
|
|
108
105
|
typeMap.set(type, count);
|
|
109
106
|
if (count > 1) {
|
|
@@ -248,6 +245,24 @@ exports.preferSettingsObject = (0, createRule_1.createRule)({
|
|
|
248
245
|
}
|
|
249
246
|
return false;
|
|
250
247
|
}
|
|
248
|
+
function hasImplicitlyTypedParams(params) {
|
|
249
|
+
return params.some((param) => {
|
|
250
|
+
let current = param;
|
|
251
|
+
if (current.type === utils_1.AST_NODE_TYPES.TSParameterProperty) {
|
|
252
|
+
current = current.parameter;
|
|
253
|
+
}
|
|
254
|
+
if (current.type === utils_1.AST_NODE_TYPES.AssignmentPattern) {
|
|
255
|
+
current = current.left;
|
|
256
|
+
}
|
|
257
|
+
if (current.type === utils_1.AST_NODE_TYPES.Identifier ||
|
|
258
|
+
current.type === utils_1.AST_NODE_TYPES.ObjectPattern ||
|
|
259
|
+
current.type === utils_1.AST_NODE_TYPES.ArrayPattern ||
|
|
260
|
+
current.type === utils_1.AST_NODE_TYPES.RestElement) {
|
|
261
|
+
return !current.typeAnnotation;
|
|
262
|
+
}
|
|
263
|
+
return true;
|
|
264
|
+
});
|
|
265
|
+
}
|
|
251
266
|
function shouldIgnoreNode(node) {
|
|
252
267
|
// Ignore built-in objects and third-party modules
|
|
253
268
|
if (isBuiltInOrThirdParty(node))
|
|
@@ -270,7 +285,7 @@ exports.preferSettingsObject = (0, createRule_1.createRule)({
|
|
|
270
285
|
parent = parent.parent;
|
|
271
286
|
}
|
|
272
287
|
}
|
|
273
|
-
// Ignore functions with A/B pattern parameters
|
|
288
|
+
// Ignore functions with A/B pattern parameters or missing type annotations
|
|
274
289
|
if ((node.type === utils_1.AST_NODE_TYPES.FunctionDeclaration ||
|
|
275
290
|
node.type === utils_1.AST_NODE_TYPES.FunctionExpression ||
|
|
276
291
|
node.type === utils_1.AST_NODE_TYPES.ArrowFunctionExpression ||
|
|
@@ -279,6 +294,8 @@ exports.preferSettingsObject = (0, createRule_1.createRule)({
|
|
|
279
294
|
Array.isArray(node.params)) {
|
|
280
295
|
if (hasABPattern(node.params))
|
|
281
296
|
return true;
|
|
297
|
+
if (hasImplicitlyTypedParams(node.params))
|
|
298
|
+
return true;
|
|
282
299
|
}
|
|
283
300
|
// Check if the function is a handler with transaction parameter
|
|
284
301
|
// This is a common pattern in Firebase/Firestore handlers
|
|
@@ -225,6 +225,15 @@ exports.preferTypeAliasOverTypeofConstant = (0, createRule_1.createRule)({
|
|
|
225
225
|
TSTypeQuery(node) {
|
|
226
226
|
if (!collected)
|
|
227
227
|
return;
|
|
228
|
+
// Skip if inside a type alias declaration (Issue #1117)
|
|
229
|
+
// This allows 'type T = typeof CONST' as the canonical way to define the alias.
|
|
230
|
+
let current = node.parent;
|
|
231
|
+
while (current) {
|
|
232
|
+
if (current.type === utils_1.AST_NODE_TYPES.TSTypeAliasDeclaration) {
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
current = current.parent;
|
|
236
|
+
}
|
|
228
237
|
// Skip `keyof typeof X`
|
|
229
238
|
if (node.parent &&
|
|
230
239
|
node.parent.type === utils_1.AST_NODE_TYPES.TSTypeOperator &&
|
|
@@ -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
|
});
|
|
@@ -50,7 +50,7 @@ function isOptionalMemberExpression(expr) {
|
|
|
50
50
|
}
|
|
51
51
|
exports.preferUrlToStringOverToJson = (0, createRule_1.createRule)({
|
|
52
52
|
create(context) {
|
|
53
|
-
const sourceCode = context.
|
|
53
|
+
const sourceCode = context.sourceCode;
|
|
54
54
|
const parserServices = sourceCode.parserServices;
|
|
55
55
|
const urlIdentifierNames = new Set();
|
|
56
56
|
let checker = null;
|
|
@@ -132,6 +132,9 @@ exports.preferUrlToStringOverToJson = (0, createRule_1.createRule)({
|
|
|
132
132
|
context.report({
|
|
133
133
|
node,
|
|
134
134
|
messageId: 'preferToString',
|
|
135
|
+
data: {
|
|
136
|
+
urlText: sourceCode.getText(objectExpr),
|
|
137
|
+
},
|
|
135
138
|
fix(fixer) {
|
|
136
139
|
// If inside JSON.stringify and not optional chain, replace the entire call with just the object
|
|
137
140
|
if (insideJSONStringify &&
|
|
@@ -151,14 +154,14 @@ exports.preferUrlToStringOverToJson = (0, createRule_1.createRule)({
|
|
|
151
154
|
meta: {
|
|
152
155
|
type: 'suggestion',
|
|
153
156
|
docs: {
|
|
154
|
-
description: 'Enforce the use of toString() over toJSON() on URL objects.
|
|
157
|
+
description: 'Enforce the use of toString() over toJSON() on URL objects.',
|
|
155
158
|
recommended: 'error',
|
|
156
159
|
requiresTypeChecking: false,
|
|
157
160
|
},
|
|
158
161
|
fixable: 'code',
|
|
159
162
|
schema: [],
|
|
160
163
|
messages: {
|
|
161
|
-
preferToString: '
|
|
164
|
+
preferToString: 'URL value {{urlText}} calls toJSON() explicitly. URL.toJSON() delegates directly to toString(), so the call is redundant. Inside JSON.stringify it is unnecessary because JSON.stringify already invokes toJSON automatically; elsewhere it hides intent because readers must remember the delegation to see it produces the same string as toString. Use toString() when you need an explicit string, or pass the URL directly to JSON.stringify to rely on its built-in serialization.',
|
|
162
165
|
},
|
|
163
166
|
},
|
|
164
167
|
defaultOptions: [],
|