@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,262 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.enforceFieldPathSyntaxInDocSetter = void 0;
|
|
4
|
+
const utils_1 = require("@typescript-eslint/utils");
|
|
5
|
+
const createRule_1 = require("../utils/createRule");
|
|
6
|
+
const getMethodName_1 = require("../utils/getMethodName");
|
|
7
|
+
exports.enforceFieldPathSyntaxInDocSetter = (0, createRule_1.createRule)({
|
|
8
|
+
name: 'enforce-fieldpath-syntax-in-docsetter',
|
|
9
|
+
meta: {
|
|
10
|
+
type: 'suggestion',
|
|
11
|
+
docs: {
|
|
12
|
+
description: 'Enforce the use of Firestore FieldPath syntax when passing documentData into DocSetter. Instead of using nested object syntax, developers should use dot notation for deeply nested fields.',
|
|
13
|
+
recommended: 'error',
|
|
14
|
+
requiresTypeChecking: false,
|
|
15
|
+
extendsBaseRule: false,
|
|
16
|
+
},
|
|
17
|
+
fixable: 'code',
|
|
18
|
+
schema: [],
|
|
19
|
+
messages: {
|
|
20
|
+
enforceFieldPathSyntax: 'What’s wrong: DocSetter {{methodName}} receives nested object data under "{{topLevelKey}}". → Why it matters: Firestore treats that nested map as a whole sub-document write, so partial updates can overwrite sibling fields you did not include. → How to fix: Flatten nested properties into FieldPath (dot) keys before passing documentData (e.g., "{{exampleFieldPath}}") so only the intended leaves are written.',
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
defaultOptions: [],
|
|
24
|
+
create(context) {
|
|
25
|
+
const sourceCode = context.getSourceCode();
|
|
26
|
+
// Track DocSetter variables
|
|
27
|
+
const docSetterVariables = new Set();
|
|
28
|
+
// Helper function to check if a node is a DocSetter method call
|
|
29
|
+
function isDocSetterMethodCall(node) {
|
|
30
|
+
if (node.callee.type !== utils_1.AST_NODE_TYPES.MemberExpression) {
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
const { object, property } = node.callee;
|
|
34
|
+
// Only enforce for set/updateIfExists; skip overwrite (full-document replacement)
|
|
35
|
+
if (property.type !== utils_1.AST_NODE_TYPES.Identifier ||
|
|
36
|
+
!['set', 'updateIfExists'].includes(property.name)) {
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
// Check if the object is a DocSetter instance
|
|
40
|
+
if (object.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
41
|
+
return docSetterVariables.has(object.name);
|
|
42
|
+
}
|
|
43
|
+
// Support chained instantiation: new DocSetter(...).set(...)
|
|
44
|
+
if (object.type === utils_1.AST_NODE_TYPES.NewExpression &&
|
|
45
|
+
object.callee.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
46
|
+
object.callee.name === 'DocSetter') {
|
|
47
|
+
return true;
|
|
48
|
+
}
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
// Helper: detect spread or computed properties in an object literal
|
|
52
|
+
function isSpreadOrComputed(prop) {
|
|
53
|
+
return (prop.type === utils_1.AST_NODE_TYPES.SpreadElement ||
|
|
54
|
+
(prop.type === utils_1.AST_NODE_TYPES.Property && prop.computed === true));
|
|
55
|
+
}
|
|
56
|
+
function isNumericKey(property) {
|
|
57
|
+
return (property.key.type === utils_1.AST_NODE_TYPES.Literal &&
|
|
58
|
+
(typeof property.key.value === 'number' ||
|
|
59
|
+
(typeof property.key.value === 'string' &&
|
|
60
|
+
/^\d+$/.test(property.key.value))));
|
|
61
|
+
}
|
|
62
|
+
function hasRootNumericKey(node) {
|
|
63
|
+
return node.properties.some((property) => property.type === utils_1.AST_NODE_TYPES.Property && isNumericKey(property));
|
|
64
|
+
}
|
|
65
|
+
// Helper function to flatten nested objects into FieldPath syntax
|
|
66
|
+
function flattenObject(obj, sourceCode, prefix = '') {
|
|
67
|
+
const result = {};
|
|
68
|
+
for (const property of obj.properties) {
|
|
69
|
+
// Skip spread elements
|
|
70
|
+
if (property.type === utils_1.AST_NODE_TYPES.SpreadElement) {
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
if (property.type !== utils_1.AST_NODE_TYPES.Property) {
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
// Skip computed properties (dynamic keys)
|
|
77
|
+
if (property.computed) {
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
let key;
|
|
81
|
+
if (property.key.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
82
|
+
key = property.key.name;
|
|
83
|
+
}
|
|
84
|
+
else if (property.key.type === utils_1.AST_NODE_TYPES.Literal) {
|
|
85
|
+
// Handle both string and numeric literal keys
|
|
86
|
+
if (typeof property.key.value === 'string') {
|
|
87
|
+
key = property.key.value;
|
|
88
|
+
}
|
|
89
|
+
else if (typeof property.key.value === 'number') {
|
|
90
|
+
key = String(property.key.value);
|
|
91
|
+
}
|
|
92
|
+
else {
|
|
93
|
+
// Skip other literal types
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
else {
|
|
98
|
+
// Skip other key types
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
const numericKey = isNumericKey(property);
|
|
102
|
+
if (prefix === '' && numericKey) {
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
const fullKey = prefix ? `${prefix}.${key}` : key;
|
|
106
|
+
// If the value is a nested object, recursively flatten it
|
|
107
|
+
if (property.value.type === utils_1.AST_NODE_TYPES.ObjectExpression) {
|
|
108
|
+
const nestedResult = flattenObject(property.value, sourceCode, fullKey);
|
|
109
|
+
Object.assign(result, nestedResult);
|
|
110
|
+
}
|
|
111
|
+
else {
|
|
112
|
+
// For non-object values, use the key as is
|
|
113
|
+
result[fullKey] = sourceCode.getText(property.value);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
return result;
|
|
117
|
+
}
|
|
118
|
+
// Helper to decide if a key needs quoting (contains dot or is not IdentifierName)
|
|
119
|
+
function needsQuoting(key) {
|
|
120
|
+
return key.includes('.') || !/^(?:[$_A-Za-z][$\w]*)$/u.test(key);
|
|
121
|
+
}
|
|
122
|
+
// Helper function to convert an object to FieldPath syntax
|
|
123
|
+
function convertToFieldPathSyntax(node, sourceCode, preflattened) {
|
|
124
|
+
const idProperty = node.properties.find((prop) => prop.type === utils_1.AST_NODE_TYPES.Property &&
|
|
125
|
+
prop.key.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
126
|
+
prop.key.name === 'id');
|
|
127
|
+
const flattenedProperties = {
|
|
128
|
+
...(preflattened ?? flattenObject(node, sourceCode)),
|
|
129
|
+
};
|
|
130
|
+
if (idProperty && idProperty.type === utils_1.AST_NODE_TYPES.Property) {
|
|
131
|
+
delete flattenedProperties['id'];
|
|
132
|
+
}
|
|
133
|
+
const entries = Object.entries(flattenedProperties);
|
|
134
|
+
const propertyComma = ',';
|
|
135
|
+
// Start with the id property if it exists
|
|
136
|
+
let result = '{\n';
|
|
137
|
+
if (idProperty && idProperty.type === utils_1.AST_NODE_TYPES.Property) {
|
|
138
|
+
result += ` id: ${sourceCode.getText(idProperty.value)}${propertyComma}\n`;
|
|
139
|
+
}
|
|
140
|
+
// Add the flattened properties (always include trailing commas for multiline objects)
|
|
141
|
+
entries.forEach(([key, value]) => {
|
|
142
|
+
const printedKey = needsQuoting(key) ? `'${key}'` : key;
|
|
143
|
+
result += ` ${printedKey}: ${value}${propertyComma}\n`;
|
|
144
|
+
});
|
|
145
|
+
result += '}';
|
|
146
|
+
return result;
|
|
147
|
+
}
|
|
148
|
+
function getPropertyKeyText(property) {
|
|
149
|
+
if (property.key.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
150
|
+
return property.key.name;
|
|
151
|
+
}
|
|
152
|
+
if (property.key.type === utils_1.AST_NODE_TYPES.Literal &&
|
|
153
|
+
(typeof property.key.value === 'string' ||
|
|
154
|
+
typeof property.key.value === 'number')) {
|
|
155
|
+
return String(property.key.value);
|
|
156
|
+
}
|
|
157
|
+
return undefined;
|
|
158
|
+
}
|
|
159
|
+
function getFirstNestedObjectProperty(node, keyPredicate) {
|
|
160
|
+
for (const property of node.properties) {
|
|
161
|
+
if (isSpreadOrComputed(property)) {
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
if (property.type !== utils_1.AST_NODE_TYPES.Property) {
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
const propertyKeyText = getPropertyKeyText(property);
|
|
168
|
+
if (!propertyKeyText) {
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
if (keyPredicate && !keyPredicate(propertyKeyText)) {
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
// Root-level numeric keys typically model array-style buckets rather than
|
|
175
|
+
// Firestore document fields, so ignore them when identifying nested objects
|
|
176
|
+
if (node.parent?.type !== utils_1.AST_NODE_TYPES.Property &&
|
|
177
|
+
isNumericKey(property)) {
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
if (property.value.type !== utils_1.AST_NODE_TYPES.ObjectExpression) {
|
|
181
|
+
continue;
|
|
182
|
+
}
|
|
183
|
+
const hasSpreadOrComputed = property.value.properties.some((prop) => isSpreadOrComputed(prop));
|
|
184
|
+
if (hasSpreadOrComputed) {
|
|
185
|
+
continue;
|
|
186
|
+
}
|
|
187
|
+
return property;
|
|
188
|
+
}
|
|
189
|
+
return undefined;
|
|
190
|
+
}
|
|
191
|
+
function extractViolationDetails(firstArg, sourceCode) {
|
|
192
|
+
const firstNestedPropertyWithoutDots = getFirstNestedObjectProperty(firstArg, (key) => !key.includes('.'));
|
|
193
|
+
const firstNestedProperty = firstNestedPropertyWithoutDots ||
|
|
194
|
+
getFirstNestedObjectProperty(firstArg);
|
|
195
|
+
if (!firstNestedProperty) {
|
|
196
|
+
return null;
|
|
197
|
+
}
|
|
198
|
+
const propertyKeyText = getPropertyKeyText(firstNestedProperty);
|
|
199
|
+
const exampleFieldPathFromProperty = propertyKeyText &&
|
|
200
|
+
flattenObject(firstNestedProperty.value, sourceCode, propertyKeyText);
|
|
201
|
+
const flattenedProperties = flattenObject(firstArg, sourceCode);
|
|
202
|
+
const exampleFieldPath = (exampleFieldPathFromProperty &&
|
|
203
|
+
Object.keys(exampleFieldPathFromProperty).find((key) => key.includes('.'))) ??
|
|
204
|
+
(propertyKeyText &&
|
|
205
|
+
Object.keys(flattenedProperties).find((key) => key.startsWith(`${propertyKeyText}.`))) ??
|
|
206
|
+
Object.keys(flattenedProperties).find((key) => key.includes('.')) ??
|
|
207
|
+
'field.nested';
|
|
208
|
+
return {
|
|
209
|
+
topLevelKey: propertyKeyText ?? 'nested field',
|
|
210
|
+
exampleFieldPath,
|
|
211
|
+
flattenedProperties,
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
return {
|
|
215
|
+
// Track DocSetter variable declarations
|
|
216
|
+
VariableDeclarator(node) {
|
|
217
|
+
if (node.init?.type === utils_1.AST_NODE_TYPES.NewExpression &&
|
|
218
|
+
node.init.callee.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
219
|
+
node.init.callee.name === 'DocSetter' &&
|
|
220
|
+
node.id.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
221
|
+
docSetterVariables.add(node.id.name);
|
|
222
|
+
}
|
|
223
|
+
},
|
|
224
|
+
// Check DocSetter method calls
|
|
225
|
+
CallExpression(node) {
|
|
226
|
+
if (!isDocSetterMethodCall(node)) {
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
// Check if the first argument is an object literal
|
|
230
|
+
const firstArg = node.arguments[0];
|
|
231
|
+
if (firstArg?.type !== utils_1.AST_NODE_TYPES.ObjectExpression) {
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
if (hasRootNumericKey(firstArg)) {
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
const violationDetails = extractViolationDetails(firstArg, sourceCode);
|
|
238
|
+
if (!violationDetails) {
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
// Report and fix the issue
|
|
242
|
+
const callee = node.callee;
|
|
243
|
+
context.report({
|
|
244
|
+
node: firstArg,
|
|
245
|
+
messageId: 'enforceFieldPathSyntax',
|
|
246
|
+
data: {
|
|
247
|
+
methodName: `${(0, getMethodName_1.getMemberExpressionName)(callee, sourceCode, {
|
|
248
|
+
computedFallbackToText: false,
|
|
249
|
+
}) || 'set'}()`,
|
|
250
|
+
topLevelKey: violationDetails.topLevelKey,
|
|
251
|
+
exampleFieldPath: violationDetails.exampleFieldPath,
|
|
252
|
+
},
|
|
253
|
+
fix(fixer) {
|
|
254
|
+
const newText = convertToFieldPathSyntax(firstArg, sourceCode, violationDetails.flattenedProperties);
|
|
255
|
+
return fixer.replaceText(firstArg, newText);
|
|
256
|
+
},
|
|
257
|
+
});
|
|
258
|
+
},
|
|
259
|
+
};
|
|
260
|
+
},
|
|
261
|
+
});
|
|
262
|
+
//# sourceMappingURL=enforce-fieldpath-syntax-in-docsetter.js.map
|
|
@@ -24,8 +24,20 @@ exports.enforceFirestoreDocRefGeneric = (0, createRule_1.createRule)({
|
|
|
24
24
|
},
|
|
25
25
|
schema: [],
|
|
26
26
|
messages: {
|
|
27
|
-
missingGeneric:
|
|
28
|
-
|
|
27
|
+
missingGeneric: [
|
|
28
|
+
"What's wrong: {{ type }} is missing its document schema generic (the document data type).",
|
|
29
|
+
'',
|
|
30
|
+
'Why it matters: Without the generic, Firestore references fall back to loose DocumentData, so TypeScript cannot catch field typos or missing required properties before they reach Firestore.',
|
|
31
|
+
'',
|
|
32
|
+
'How to fix: Add the document interface/type as the generic (e.g., const ref: {{ type }}<UserDoc> = ... or doc<UserDoc>(collection)).',
|
|
33
|
+
].join('\n'),
|
|
34
|
+
invalidGeneric: [
|
|
35
|
+
'What\'s wrong: {{ type }} uses "any" or an empty object ({}) in its schema generic.',
|
|
36
|
+
'',
|
|
37
|
+
'Why it matters: This erases the document schema and disables TypeScript checks on Firestore reads and writes, so malformed payloads and missing fields can pass silently.',
|
|
38
|
+
'',
|
|
39
|
+
'How to fix: Define a concrete interface/type for the document (e.g., interface UserDoc { name: string }) and use it as the generic instead of "any" or {}.',
|
|
40
|
+
].join('\n'),
|
|
29
41
|
},
|
|
30
42
|
},
|
|
31
43
|
defaultOptions: [],
|
|
@@ -61,7 +73,7 @@ exports.enforceFirestoreDocRefGeneric = (0, createRule_1.createRule)({
|
|
|
61
73
|
}
|
|
62
74
|
// Prevent infinite recursion
|
|
63
75
|
typeCache.set(typeName, false);
|
|
64
|
-
const program = context.
|
|
76
|
+
const program = context.sourceCode.ast;
|
|
65
77
|
const interfaceDecl = program.body.find((n) => n.type === utils_1.AST_NODE_TYPES.TSInterfaceDeclaration &&
|
|
66
78
|
n.id.name === typeName);
|
|
67
79
|
if (interfaceDecl) {
|
|
@@ -194,6 +206,308 @@ exports.enforceFirestoreDocRefGeneric = (0, createRule_1.createRule)({
|
|
|
194
206
|
}
|
|
195
207
|
return false;
|
|
196
208
|
}
|
|
209
|
+
const isTypedCollectionReferenceCache = new Map();
|
|
210
|
+
function isTypedCollectionReference(node) {
|
|
211
|
+
if (!node)
|
|
212
|
+
return false;
|
|
213
|
+
if (isTypedCollectionReferenceCache.has(node)) {
|
|
214
|
+
return isTypedCollectionReferenceCache.get(node);
|
|
215
|
+
}
|
|
216
|
+
let result = false;
|
|
217
|
+
if (node.type === utils_1.AST_NODE_TYPES.CallExpression &&
|
|
218
|
+
node.callee.type === utils_1.AST_NODE_TYPES.MemberExpression &&
|
|
219
|
+
node.callee.property.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
220
|
+
node.callee.property.name === 'collection' &&
|
|
221
|
+
node.typeParameters &&
|
|
222
|
+
node.typeParameters.params.length > 0) {
|
|
223
|
+
result = true;
|
|
224
|
+
}
|
|
225
|
+
else if (node.type === utils_1.AST_NODE_TYPES.MemberExpression) {
|
|
226
|
+
result = checkMemberExpressionForCollectionReference(node);
|
|
227
|
+
}
|
|
228
|
+
else if (node.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
229
|
+
result = checkIdentifierForCollectionReference(node);
|
|
230
|
+
}
|
|
231
|
+
else if (node.type === utils_1.AST_NODE_TYPES.CallExpression) {
|
|
232
|
+
result = checkCallExpressionForCollectionReference(node);
|
|
233
|
+
}
|
|
234
|
+
isTypedCollectionReferenceCache.set(node, result);
|
|
235
|
+
return result;
|
|
236
|
+
}
|
|
237
|
+
function checkMemberExpressionForCollectionReference(node) {
|
|
238
|
+
const obj = node.object;
|
|
239
|
+
const property = node.property;
|
|
240
|
+
// Handle this.property access
|
|
241
|
+
if (obj.type === utils_1.AST_NODE_TYPES.ThisExpression &&
|
|
242
|
+
property.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
243
|
+
const classNode = findParentClass(node);
|
|
244
|
+
if (classNode) {
|
|
245
|
+
const classProp = classNode.body.body.find((member) => member.type === utils_1.AST_NODE_TYPES.PropertyDefinition &&
|
|
246
|
+
member.key.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
247
|
+
member.key.name === property.name);
|
|
248
|
+
if (classProp?.typeAnnotation) {
|
|
249
|
+
return hasCollectionReferenceType(classProp.typeAnnotation.typeAnnotation);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
// Handle nested property access like this.collections.tasks
|
|
254
|
+
if (obj.type === utils_1.AST_NODE_TYPES.MemberExpression) {
|
|
255
|
+
const parentType = getTypeOfMemberExpression(obj);
|
|
256
|
+
if (parentType && property.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
257
|
+
// Check if the parent object has a property with CollectionReference type
|
|
258
|
+
return checkObjectPropertyForCollectionReference(parentType, property.name);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
// Handle identifier.property access
|
|
262
|
+
if (obj.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
263
|
+
property.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
264
|
+
const objType = getTypeOfIdentifier(obj);
|
|
265
|
+
if (objType) {
|
|
266
|
+
return checkObjectPropertyForCollectionReference(objType, property.name);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
// Handle getter methods like this.collection where collection is a getter
|
|
270
|
+
if (obj.type === utils_1.AST_NODE_TYPES.ThisExpression &&
|
|
271
|
+
property.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
272
|
+
const classNode = findParentClass(node);
|
|
273
|
+
if (classNode) {
|
|
274
|
+
const getter = classNode.body.body.find((member) => member.type === utils_1.AST_NODE_TYPES.MethodDefinition &&
|
|
275
|
+
member.kind === 'get' &&
|
|
276
|
+
member.key.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
277
|
+
member.key.name === property.name);
|
|
278
|
+
if (getter) {
|
|
279
|
+
// Check explicit return type
|
|
280
|
+
if (getter.value.returnType) {
|
|
281
|
+
return hasCollectionReferenceType(getter.value.returnType.typeAnnotation);
|
|
282
|
+
}
|
|
283
|
+
// Check return statement to infer type
|
|
284
|
+
if (getter.value.body &&
|
|
285
|
+
getter.value.body.type === utils_1.AST_NODE_TYPES.BlockStatement) {
|
|
286
|
+
const returnStmt = getter.value.body.body.find((stmt) => stmt.type === utils_1.AST_NODE_TYPES.ReturnStatement);
|
|
287
|
+
if (returnStmt?.argument?.type === utils_1.AST_NODE_TYPES.MemberExpression) {
|
|
288
|
+
return checkMemberExpressionForCollectionReference(returnStmt.argument);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
return false;
|
|
295
|
+
}
|
|
296
|
+
function checkIdentifierForCollectionReference(node) {
|
|
297
|
+
// Check function parameters
|
|
298
|
+
const functionParam = findFunctionParameter(node);
|
|
299
|
+
if (functionParam &&
|
|
300
|
+
'typeAnnotation' in functionParam &&
|
|
301
|
+
functionParam.typeAnnotation) {
|
|
302
|
+
return hasCollectionReferenceType(functionParam.typeAnnotation.typeAnnotation);
|
|
303
|
+
}
|
|
304
|
+
// Check variable declarations in current scope and parent scopes
|
|
305
|
+
if (findVariableDeclaration(node)) {
|
|
306
|
+
return true;
|
|
307
|
+
}
|
|
308
|
+
return false;
|
|
309
|
+
}
|
|
310
|
+
function checkCallExpressionForCollectionReference(node) {
|
|
311
|
+
// Check if this is a method call that returns CollectionReference
|
|
312
|
+
if (node.callee.type === utils_1.AST_NODE_TYPES.MemberExpression) {
|
|
313
|
+
const obj = node.callee.object;
|
|
314
|
+
const property = node.callee.property;
|
|
315
|
+
if (property.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
316
|
+
// Check if this is a getter or method that returns CollectionReference
|
|
317
|
+
if (obj.type === utils_1.AST_NODE_TYPES.ThisExpression) {
|
|
318
|
+
const classNode = findParentClass(node);
|
|
319
|
+
if (classNode) {
|
|
320
|
+
const method = classNode.body.body.find((member) => member.type === utils_1.AST_NODE_TYPES.MethodDefinition &&
|
|
321
|
+
member.key.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
322
|
+
member.key.name === property.name &&
|
|
323
|
+
!!member.value.returnType);
|
|
324
|
+
if (method?.value.returnType) {
|
|
325
|
+
return hasCollectionReferenceType(method.value.returnType.typeAnnotation);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
return false;
|
|
332
|
+
}
|
|
333
|
+
function getTypeOfMemberExpression(node) {
|
|
334
|
+
if (node.object.type === utils_1.AST_NODE_TYPES.ThisExpression &&
|
|
335
|
+
node.property.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
336
|
+
const classNode = findParentClass(node);
|
|
337
|
+
if (classNode) {
|
|
338
|
+
const classProp = classNode.body.body.find((member) => member.type === utils_1.AST_NODE_TYPES.PropertyDefinition &&
|
|
339
|
+
member.key.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
340
|
+
'name' in node.property &&
|
|
341
|
+
member.key.name === node.property.name);
|
|
342
|
+
return classProp?.typeAnnotation?.typeAnnotation || null;
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
return null;
|
|
346
|
+
}
|
|
347
|
+
function getTypeOfIdentifier(node) {
|
|
348
|
+
const functionParam = findFunctionParameter(node);
|
|
349
|
+
if (functionParam &&
|
|
350
|
+
'typeAnnotation' in functionParam &&
|
|
351
|
+
functionParam.typeAnnotation) {
|
|
352
|
+
return functionParam.typeAnnotation.typeAnnotation;
|
|
353
|
+
}
|
|
354
|
+
const varDecl = findVariableDeclarationNode(node);
|
|
355
|
+
if (varDecl?.typeAnnotation) {
|
|
356
|
+
return varDecl.typeAnnotation.typeAnnotation;
|
|
357
|
+
}
|
|
358
|
+
return null;
|
|
359
|
+
}
|
|
360
|
+
function checkObjectPropertyForCollectionReference(objectType, propertyName) {
|
|
361
|
+
if (objectType.type === utils_1.AST_NODE_TYPES.TSTypeLiteral) {
|
|
362
|
+
const property = objectType.members.find((member) => member.type === utils_1.AST_NODE_TYPES.TSPropertySignature &&
|
|
363
|
+
member.key.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
364
|
+
member.key.name === propertyName &&
|
|
365
|
+
!!member.typeAnnotation);
|
|
366
|
+
if (property?.typeAnnotation) {
|
|
367
|
+
return hasCollectionReferenceType(property.typeAnnotation.typeAnnotation);
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
// Handle Record<string, CollectionReference<T>> types
|
|
371
|
+
if (objectType.type === utils_1.AST_NODE_TYPES.TSTypeReference &&
|
|
372
|
+
objectType.typeName.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
373
|
+
objectType.typeName.name === 'Record' &&
|
|
374
|
+
objectType.typeParameters &&
|
|
375
|
+
objectType.typeParameters.params.length === 2) {
|
|
376
|
+
const valueType = objectType.typeParameters.params[1];
|
|
377
|
+
return hasCollectionReferenceType(valueType);
|
|
378
|
+
}
|
|
379
|
+
// Handle array types like CollectionReference<T>[]
|
|
380
|
+
if (objectType.type === utils_1.AST_NODE_TYPES.TSArrayType) {
|
|
381
|
+
return hasCollectionReferenceType(objectType.elementType);
|
|
382
|
+
}
|
|
383
|
+
return false;
|
|
384
|
+
}
|
|
385
|
+
function findFunctionParameter(node) {
|
|
386
|
+
const isFunctionNode = (n) => n.type === utils_1.AST_NODE_TYPES.FunctionDeclaration ||
|
|
387
|
+
n.type === utils_1.AST_NODE_TYPES.FunctionExpression ||
|
|
388
|
+
n.type === utils_1.AST_NODE_TYPES.ArrowFunctionExpression;
|
|
389
|
+
const findParamInFunction = (func) => {
|
|
390
|
+
const param = func.params.find((p) => p.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
391
|
+
p.name === node.name &&
|
|
392
|
+
'typeAnnotation' in p &&
|
|
393
|
+
p.typeAnnotation !== undefined);
|
|
394
|
+
return param || null;
|
|
395
|
+
};
|
|
396
|
+
const functionScopes = [];
|
|
397
|
+
let current = node;
|
|
398
|
+
while (current) {
|
|
399
|
+
if (isFunctionNode(current)) {
|
|
400
|
+
functionScopes.push(current);
|
|
401
|
+
}
|
|
402
|
+
current = current.parent;
|
|
403
|
+
}
|
|
404
|
+
for (const func of functionScopes) {
|
|
405
|
+
const param = findParamInFunction(func);
|
|
406
|
+
if (param) {
|
|
407
|
+
return param;
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
return null;
|
|
411
|
+
}
|
|
412
|
+
function findVariableDeclaration(node) {
|
|
413
|
+
let current = node;
|
|
414
|
+
while (current) {
|
|
415
|
+
// Check in current scope
|
|
416
|
+
if (current.type === utils_1.AST_NODE_TYPES.Program ||
|
|
417
|
+
current.type === utils_1.AST_NODE_TYPES.BlockStatement ||
|
|
418
|
+
current.type === utils_1.AST_NODE_TYPES.FunctionDeclaration ||
|
|
419
|
+
current.type === utils_1.AST_NODE_TYPES.FunctionExpression ||
|
|
420
|
+
current.type === utils_1.AST_NODE_TYPES.ArrowFunctionExpression) {
|
|
421
|
+
const varDecl = findVariableInScope(current, node.name);
|
|
422
|
+
if (varDecl && varDecl.typeAnnotation) {
|
|
423
|
+
return hasCollectionReferenceType(varDecl.typeAnnotation.typeAnnotation);
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
current = current.parent;
|
|
427
|
+
}
|
|
428
|
+
return false;
|
|
429
|
+
}
|
|
430
|
+
function findVariableDeclarationNode(node) {
|
|
431
|
+
let current = node;
|
|
432
|
+
while (current) {
|
|
433
|
+
if (current.type === utils_1.AST_NODE_TYPES.Program ||
|
|
434
|
+
current.type === utils_1.AST_NODE_TYPES.BlockStatement ||
|
|
435
|
+
current.type === utils_1.AST_NODE_TYPES.FunctionDeclaration ||
|
|
436
|
+
current.type === utils_1.AST_NODE_TYPES.FunctionExpression ||
|
|
437
|
+
current.type === utils_1.AST_NODE_TYPES.ArrowFunctionExpression) {
|
|
438
|
+
const varDecl = findVariableInScope(current, node.name);
|
|
439
|
+
if (varDecl) {
|
|
440
|
+
return varDecl;
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
current = current.parent;
|
|
444
|
+
}
|
|
445
|
+
return null;
|
|
446
|
+
}
|
|
447
|
+
function findVariableInScope(scope, varName) {
|
|
448
|
+
const body = getNodeBody(scope);
|
|
449
|
+
if (!body)
|
|
450
|
+
return null;
|
|
451
|
+
for (const stmt of body) {
|
|
452
|
+
if (stmt.type === utils_1.AST_NODE_TYPES.VariableDeclaration) {
|
|
453
|
+
for (const decl of stmt.declarations) {
|
|
454
|
+
if (decl.type === utils_1.AST_NODE_TYPES.VariableDeclarator &&
|
|
455
|
+
decl.id.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
456
|
+
decl.id.name === varName &&
|
|
457
|
+
decl.id.typeAnnotation) {
|
|
458
|
+
return decl.id;
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
return null;
|
|
464
|
+
}
|
|
465
|
+
function getNodeBody(node) {
|
|
466
|
+
if (node.type === utils_1.AST_NODE_TYPES.Program) {
|
|
467
|
+
return node.body;
|
|
468
|
+
}
|
|
469
|
+
if (node.type === utils_1.AST_NODE_TYPES.BlockStatement) {
|
|
470
|
+
return node.body;
|
|
471
|
+
}
|
|
472
|
+
if (node.type === utils_1.AST_NODE_TYPES.FunctionDeclaration ||
|
|
473
|
+
node.type === utils_1.AST_NODE_TYPES.FunctionExpression) {
|
|
474
|
+
return node.body?.body || null;
|
|
475
|
+
}
|
|
476
|
+
if (node.type === utils_1.AST_NODE_TYPES.ArrowFunctionExpression) {
|
|
477
|
+
return node.body.type === utils_1.AST_NODE_TYPES.BlockStatement
|
|
478
|
+
? node.body.body
|
|
479
|
+
: null;
|
|
480
|
+
}
|
|
481
|
+
return null;
|
|
482
|
+
}
|
|
483
|
+
function hasCollectionReferenceType(typeNode) {
|
|
484
|
+
if (typeNode.type === utils_1.AST_NODE_TYPES.TSTypeReference &&
|
|
485
|
+
((typeNode.typeName.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
486
|
+
typeNode.typeName.name === 'CollectionReference') ||
|
|
487
|
+
(typeNode.typeName.type === utils_1.AST_NODE_TYPES.TSQualifiedName &&
|
|
488
|
+
typeNode.typeName.right.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
489
|
+
typeNode.typeName.right.name === 'CollectionReference')) &&
|
|
490
|
+
typeNode.typeParameters &&
|
|
491
|
+
typeNode.typeParameters.params.length > 0) {
|
|
492
|
+
return true;
|
|
493
|
+
}
|
|
494
|
+
if (typeNode.type === utils_1.AST_NODE_TYPES.TSUnionType) {
|
|
495
|
+
return typeNode.types.some(hasCollectionReferenceType);
|
|
496
|
+
}
|
|
497
|
+
if (typeNode.type === utils_1.AST_NODE_TYPES.TSIntersectionType) {
|
|
498
|
+
return typeNode.types.some(hasCollectionReferenceType);
|
|
499
|
+
}
|
|
500
|
+
// TSParenthesizedType may appear even though AST_NODE_TYPES omits it
|
|
501
|
+
if (typeNode.type === 'TSParenthesizedType') {
|
|
502
|
+
const inner = typeNode
|
|
503
|
+
.typeAnnotation;
|
|
504
|
+
return hasCollectionReferenceType(inner);
|
|
505
|
+
}
|
|
506
|
+
if (typeNode.type === utils_1.AST_NODE_TYPES.TSArrayType) {
|
|
507
|
+
return hasCollectionReferenceType(typeNode.elementType);
|
|
508
|
+
}
|
|
509
|
+
return false;
|
|
510
|
+
}
|
|
197
511
|
return {
|
|
198
512
|
TSTypeReference(node) {
|
|
199
513
|
if (node.typeName.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
@@ -231,6 +545,39 @@ exports.enforceFirestoreDocRefGeneric = (0, createRule_1.createRule)({
|
|
|
231
545
|
node.callee.property.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
232
546
|
node.callee.property.name === 'doc') {
|
|
233
547
|
const typeAnnotation = node.typeParameters;
|
|
548
|
+
const isOnTypedCollection = isTypedCollectionReference(node.callee.object);
|
|
549
|
+
// If this is a .doc() call on a typed CollectionReference,
|
|
550
|
+
// only check for invalid generics, not missing generics
|
|
551
|
+
if (isOnTypedCollection) {
|
|
552
|
+
if (typeAnnotation && hasInvalidType(typeAnnotation.params[0])) {
|
|
553
|
+
context.report({
|
|
554
|
+
node,
|
|
555
|
+
messageId: 'invalidGeneric',
|
|
556
|
+
data: { type: 'DocumentReference' },
|
|
557
|
+
});
|
|
558
|
+
}
|
|
559
|
+
return; // Skip the missing generic check for typed CollectionReference.doc() calls
|
|
560
|
+
}
|
|
561
|
+
// For standalone doc() calls or calls on untyped collections
|
|
562
|
+
if (!typeAnnotation) {
|
|
563
|
+
context.report({
|
|
564
|
+
node,
|
|
565
|
+
messageId: 'missingGeneric',
|
|
566
|
+
data: { type: 'DocumentReference' },
|
|
567
|
+
});
|
|
568
|
+
}
|
|
569
|
+
else if (hasInvalidType(typeAnnotation.params[0])) {
|
|
570
|
+
context.report({
|
|
571
|
+
node,
|
|
572
|
+
messageId: 'invalidGeneric',
|
|
573
|
+
data: { type: 'DocumentReference' },
|
|
574
|
+
});
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
// Check for standalone doc() function calls
|
|
578
|
+
else if (node.callee.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
579
|
+
node.callee.name === 'doc') {
|
|
580
|
+
const typeAnnotation = node.typeParameters;
|
|
234
581
|
if (!typeAnnotation) {
|
|
235
582
|
context.report({
|
|
236
583
|
node,
|