@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
|
@@ -1,8 +1,90 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
2
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
6
|
exports.noFirestoreJestMock = void 0;
|
|
7
|
+
const path_1 = __importDefault(require("path"));
|
|
4
8
|
const utils_1 = require("@typescript-eslint/utils");
|
|
5
9
|
const createRule_1 = require("../utils/createRule");
|
|
10
|
+
const FIRESTORE_JEST_MOCK = 'firestore-jest-mock';
|
|
11
|
+
const MOCK_FIRESTORE_TARGET = '__test-utils__/mockFirestore';
|
|
12
|
+
const toPosixPath = (filePath) => filePath.replace(/\\/g, '/');
|
|
13
|
+
const ensureRelativeSpecifier = (specifier) => specifier.startsWith('.') ? specifier : `./${specifier}`;
|
|
14
|
+
const isWindowsDrivePath = (filePath) => /^[A-Za-z]:[\\/]/.test(filePath);
|
|
15
|
+
const isValidRelativePath = (relativePath) => !path_1.default.isAbsolute(relativePath) && !isWindowsDrivePath(relativePath);
|
|
16
|
+
const buildReplacementPath = (sourceFilePath, cwd) => {
|
|
17
|
+
const absoluteFilename = path_1.default.isAbsolute(sourceFilePath)
|
|
18
|
+
? sourceFilePath
|
|
19
|
+
: path_1.default.join(cwd, sourceFilePath);
|
|
20
|
+
const targetPath = path_1.default.join(cwd, MOCK_FIRESTORE_TARGET);
|
|
21
|
+
const relativePath = path_1.default.relative(path_1.default.dirname(absoluteFilename), targetPath);
|
|
22
|
+
if (!isValidRelativePath(relativePath)) {
|
|
23
|
+
return '';
|
|
24
|
+
}
|
|
25
|
+
return ensureRelativeSpecifier(toPosixPath(relativePath));
|
|
26
|
+
};
|
|
27
|
+
const findVariableDeclarator = (node) => {
|
|
28
|
+
const { parent } = node;
|
|
29
|
+
if (parent?.type === utils_1.AST_NODE_TYPES.VariableDeclarator) {
|
|
30
|
+
return parent;
|
|
31
|
+
}
|
|
32
|
+
if (parent?.type === utils_1.AST_NODE_TYPES.AwaitExpression &&
|
|
33
|
+
parent.parent?.type === utils_1.AST_NODE_TYPES.VariableDeclarator) {
|
|
34
|
+
return parent.parent;
|
|
35
|
+
}
|
|
36
|
+
return null;
|
|
37
|
+
};
|
|
38
|
+
const buildDestructuringFix = (fixer, pattern) => {
|
|
39
|
+
if (pattern.properties.length !== 1) {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
const [property] = pattern.properties;
|
|
43
|
+
if (property.type !== utils_1.AST_NODE_TYPES.Property) {
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
if (property.value.type === utils_1.AST_NODE_TYPES.AssignmentPattern) {
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
if (property.value.type !== utils_1.AST_NODE_TYPES.Identifier) {
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
const localName = property.value.name;
|
|
53
|
+
const replacement = localName === 'mockFirestore'
|
|
54
|
+
? '{ mockFirestore }'
|
|
55
|
+
: `{ mockFirestore: ${localName} }`;
|
|
56
|
+
return fixer.replaceText(pattern, replacement);
|
|
57
|
+
};
|
|
58
|
+
const constructImportStatement = (localName, replacementPath) => {
|
|
59
|
+
const binding = localName === 'mockFirestore'
|
|
60
|
+
? 'mockFirestore'
|
|
61
|
+
: `mockFirestore as ${localName}`;
|
|
62
|
+
return `import { ${binding} } from '${replacementPath}';`;
|
|
63
|
+
};
|
|
64
|
+
const getSingleValueImportSpecifier = (node) => {
|
|
65
|
+
const valueSpecifiers = node.specifiers.filter((specifier) => specifier.type === utils_1.AST_NODE_TYPES.ImportSpecifier &&
|
|
66
|
+
specifier.importKind !== 'type');
|
|
67
|
+
const hasTypeSpecifiers = node.specifiers.some((specifier) => specifier.type === utils_1.AST_NODE_TYPES.ImportSpecifier &&
|
|
68
|
+
specifier.importKind === 'type');
|
|
69
|
+
const hasUnsupportedSpecifier = node.specifiers.some((specifier) => specifier.type === utils_1.AST_NODE_TYPES.ImportDefaultSpecifier ||
|
|
70
|
+
specifier.type === utils_1.AST_NODE_TYPES.ImportNamespaceSpecifier);
|
|
71
|
+
if (valueSpecifiers.length !== 1 ||
|
|
72
|
+
hasTypeSpecifiers ||
|
|
73
|
+
hasUnsupportedSpecifier) {
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
return valueSpecifiers[0];
|
|
77
|
+
};
|
|
78
|
+
const buildImportDeclarationFix = (node, replacementPath) => {
|
|
79
|
+
if (!replacementPath) {
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
const specifier = getSingleValueImportSpecifier(node);
|
|
83
|
+
if (!specifier) {
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
return constructImportStatement(specifier.local.name, replacementPath);
|
|
87
|
+
};
|
|
6
88
|
exports.noFirestoreJestMock = (0, createRule_1.createRule)({
|
|
7
89
|
name: 'no-firestore-jest-mock',
|
|
8
90
|
meta: {
|
|
@@ -14,38 +96,72 @@ exports.noFirestoreJestMock = (0, createRule_1.createRule)({
|
|
|
14
96
|
fixable: 'code',
|
|
15
97
|
schema: [],
|
|
16
98
|
messages: {
|
|
17
|
-
noFirestoreJestMock: '
|
|
99
|
+
noFirestoreJestMock: 'What\'s wrong: This test imports "{{moduleName}}" directly → Why it matters: it bypasses the centralized mockFirestore helper that mirrors production schema and keeps seeding/cleanup consistent, which leads to inconsistent data and flaky tests → How to fix: import { mockFirestore } from "{{replacementPath}}" instead.',
|
|
18
100
|
},
|
|
19
101
|
},
|
|
20
102
|
defaultOptions: [],
|
|
21
103
|
create(context) {
|
|
22
|
-
const
|
|
104
|
+
const sourceFilePath = context.getFilename();
|
|
23
105
|
// Only apply rule to test files
|
|
24
|
-
if (!
|
|
106
|
+
if (!sourceFilePath.endsWith('.test.ts')) {
|
|
25
107
|
return {};
|
|
26
108
|
}
|
|
109
|
+
const cwd = typeof context.getCwd === 'function' ? context.getCwd() : process.cwd();
|
|
110
|
+
const replacementPath = buildReplacementPath(sourceFilePath, cwd);
|
|
111
|
+
const messageReplacementPath = replacementPath ||
|
|
112
|
+
ensureRelativeSpecifier(toPosixPath(MOCK_FIRESTORE_TARGET));
|
|
113
|
+
const reportData = {
|
|
114
|
+
moduleName: FIRESTORE_JEST_MOCK,
|
|
115
|
+
replacementPath: messageReplacementPath,
|
|
116
|
+
};
|
|
27
117
|
return {
|
|
28
118
|
ImportDeclaration(node) {
|
|
29
119
|
// Skip type imports completely
|
|
30
120
|
if (node.importKind === 'type') {
|
|
31
121
|
return;
|
|
32
122
|
}
|
|
33
|
-
if (node.source.value ===
|
|
123
|
+
if (node.source.value === FIRESTORE_JEST_MOCK) {
|
|
34
124
|
context.report({
|
|
35
125
|
node,
|
|
36
126
|
messageId: 'noFirestoreJestMock',
|
|
127
|
+
data: reportData,
|
|
37
128
|
fix: (fixer) => {
|
|
38
|
-
|
|
129
|
+
const replacementImport = buildImportDeclarationFix(node, replacementPath);
|
|
130
|
+
if (!replacementImport) {
|
|
131
|
+
return null;
|
|
132
|
+
}
|
|
133
|
+
return fixer.replaceText(node, replacementImport);
|
|
39
134
|
},
|
|
40
135
|
});
|
|
41
136
|
}
|
|
42
137
|
},
|
|
43
138
|
ImportExpression(node) {
|
|
44
139
|
if (node.source.type === utils_1.AST_NODE_TYPES.Literal &&
|
|
45
|
-
node.source.value ===
|
|
140
|
+
node.source.value === FIRESTORE_JEST_MOCK) {
|
|
141
|
+
const variableDeclarator = findVariableDeclarator(node);
|
|
46
142
|
context.report({
|
|
47
143
|
node,
|
|
48
144
|
messageId: 'noFirestoreJestMock',
|
|
145
|
+
data: reportData,
|
|
146
|
+
fix: (fixer) => {
|
|
147
|
+
if (!replacementPath) {
|
|
148
|
+
return null;
|
|
149
|
+
}
|
|
150
|
+
if (!variableDeclarator) {
|
|
151
|
+
return null;
|
|
152
|
+
}
|
|
153
|
+
if (variableDeclarator.id.type !== utils_1.AST_NODE_TYPES.ObjectPattern) {
|
|
154
|
+
return null;
|
|
155
|
+
}
|
|
156
|
+
const destructuringFix = buildDestructuringFix(fixer, variableDeclarator.id);
|
|
157
|
+
if (!destructuringFix) {
|
|
158
|
+
return null;
|
|
159
|
+
}
|
|
160
|
+
return [
|
|
161
|
+
fixer.replaceText(node.source, `'${replacementPath}'`),
|
|
162
|
+
destructuringFix,
|
|
163
|
+
];
|
|
164
|
+
},
|
|
49
165
|
});
|
|
50
166
|
}
|
|
51
167
|
},
|
|
@@ -58,10 +174,11 @@ exports.noFirestoreJestMock = (0, createRule_1.createRule)({
|
|
|
58
174
|
node.callee.property.name === 'mock' &&
|
|
59
175
|
node.arguments.length > 0 &&
|
|
60
176
|
node.arguments[0].type === utils_1.AST_NODE_TYPES.Literal &&
|
|
61
|
-
node.arguments[0].value ===
|
|
177
|
+
node.arguments[0].value === FIRESTORE_JEST_MOCK) {
|
|
62
178
|
context.report({
|
|
63
179
|
node,
|
|
64
180
|
messageId: 'noFirestoreJestMock',
|
|
181
|
+
data: reportData,
|
|
65
182
|
});
|
|
66
183
|
}
|
|
67
184
|
// Check for require('firestore-jest-mock')
|
|
@@ -69,10 +186,11 @@ exports.noFirestoreJestMock = (0, createRule_1.createRule)({
|
|
|
69
186
|
node.callee.name === 'require' &&
|
|
70
187
|
node.arguments.length > 0 &&
|
|
71
188
|
node.arguments[0].type === utils_1.AST_NODE_TYPES.Literal &&
|
|
72
|
-
node.arguments[0].value ===
|
|
189
|
+
node.arguments[0].value === FIRESTORE_JEST_MOCK) {
|
|
73
190
|
context.report({
|
|
74
191
|
node,
|
|
75
192
|
messageId: 'noFirestoreJestMock',
|
|
193
|
+
data: reportData,
|
|
76
194
|
});
|
|
77
195
|
}
|
|
78
196
|
},
|
|
@@ -13,44 +13,162 @@ const PRIMITIVE_TYPES = new Set([
|
|
|
13
13
|
'undefined',
|
|
14
14
|
'GeoPoint',
|
|
15
15
|
]);
|
|
16
|
+
const UNKNOWN_FIELD_LABEL = '<unknown field>';
|
|
16
17
|
const isInFirestoreTypesDirectory = (filename) => {
|
|
17
|
-
|
|
18
|
+
if (!filename || filename === '<input>')
|
|
19
|
+
return false;
|
|
20
|
+
const normalized = filename.replace(/\\/g, '/');
|
|
21
|
+
if (normalized.includes('/node_modules/') || normalized.includes('/dist/')) {
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
// Match typical mono/multi-repo layouts
|
|
25
|
+
return normalized.includes('/types/firestore/');
|
|
18
26
|
};
|
|
19
|
-
const
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
+
const getRightmostIdentifierName = (name) => {
|
|
28
|
+
let cur = name;
|
|
29
|
+
// Walk rightwards until we reach an Identifier
|
|
30
|
+
while (cur.type !== utils_1.AST_NODE_TYPES.Identifier) {
|
|
31
|
+
cur = cur.right;
|
|
32
|
+
}
|
|
33
|
+
return cur.name;
|
|
34
|
+
};
|
|
35
|
+
const isArrayGenericReference = (node) => {
|
|
36
|
+
return (node.typeName.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
37
|
+
(node.typeName.name === 'Array' || node.typeName.name === 'ReadonlyArray'));
|
|
38
|
+
};
|
|
39
|
+
const isParenthesizedType = (node) => {
|
|
40
|
+
if (!node || typeof node !== 'object')
|
|
41
|
+
return false;
|
|
42
|
+
const candidate = node;
|
|
43
|
+
return (candidate.type === 'TSParenthesizedType' &&
|
|
44
|
+
candidate.typeAnnotation !== undefined);
|
|
45
|
+
};
|
|
46
|
+
const unwrapArrayElementType = (node) => {
|
|
47
|
+
let current = node;
|
|
48
|
+
// Fixpoint loop: peel wrappers in any order until none remain
|
|
49
|
+
// Cap unwrap iterations to prevent accidental non-terminating edits if new cases are added.
|
|
50
|
+
// Wrappers are finite; 10 is generous but safe.
|
|
51
|
+
for (let i = 0; i < 10; i++) {
|
|
52
|
+
if (current.type === utils_1.AST_NODE_TYPES.TSTypeOperator &&
|
|
53
|
+
current.operator === 'readonly') {
|
|
54
|
+
current = current
|
|
55
|
+
.typeAnnotation;
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
if (isParenthesizedType(current)) {
|
|
59
|
+
const paren = current;
|
|
60
|
+
current = paren.typeAnnotation;
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
if (current.type === utils_1.AST_NODE_TYPES.TSArrayType) {
|
|
64
|
+
current = current.elementType;
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
if (current.type === utils_1.AST_NODE_TYPES.TSTypeReference &&
|
|
68
|
+
isArrayGenericReference(current) &&
|
|
69
|
+
current.typeParameters &&
|
|
70
|
+
current.typeParameters.params.length > 0) {
|
|
71
|
+
current = current.typeParameters.params[0];
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
break;
|
|
75
|
+
}
|
|
76
|
+
return current;
|
|
77
|
+
};
|
|
78
|
+
const literalToString = (node) => {
|
|
79
|
+
if (!node || node.type !== utils_1.AST_NODE_TYPES.Literal)
|
|
80
|
+
return null;
|
|
81
|
+
const value = node.value;
|
|
82
|
+
if (typeof value === 'string' ||
|
|
83
|
+
typeof value === 'number' ||
|
|
84
|
+
typeof value === 'bigint') {
|
|
85
|
+
return String(value);
|
|
86
|
+
}
|
|
87
|
+
return null;
|
|
88
|
+
};
|
|
89
|
+
/**
|
|
90
|
+
* Derives a field label for diagnostics by walking ancestors.
|
|
91
|
+
* Prefers TSPropertySignature keys and returns identifier or string/number/bigint literal keys when present.
|
|
92
|
+
* Falls back to the surrounding TSTypeAliasDeclaration or TSInterfaceDeclaration name when no property key is available.
|
|
93
|
+
* This ensures declaration-level array aliases (for example, `type Foo = Object[]`) surface a meaningful label and avoid unlabeled diagnostics.
|
|
94
|
+
* Returns a placeholder when resolution fails so downstream messaging remains readable.
|
|
95
|
+
*/
|
|
96
|
+
const getFieldName = (node) => {
|
|
97
|
+
let current = node;
|
|
98
|
+
while (current) {
|
|
99
|
+
if (current.type === utils_1.AST_NODE_TYPES.TSPropertySignature) {
|
|
100
|
+
const key = current.key;
|
|
101
|
+
if (key.type === utils_1.AST_NODE_TYPES.Identifier)
|
|
102
|
+
return key.name;
|
|
103
|
+
const literalKey = literalToString(key);
|
|
104
|
+
if (literalKey !== null)
|
|
105
|
+
return literalKey;
|
|
106
|
+
const computedKey = key;
|
|
107
|
+
if (computedKey.type === 'TSComputedPropertyName' &&
|
|
108
|
+
computedKey.expression) {
|
|
109
|
+
const expr = computedKey.expression;
|
|
110
|
+
if (expr.type === utils_1.AST_NODE_TYPES.Identifier)
|
|
111
|
+
return expr.name;
|
|
112
|
+
const literalExpr = literalToString(expr);
|
|
113
|
+
if (literalExpr !== null)
|
|
114
|
+
return literalExpr;
|
|
27
115
|
}
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
116
|
+
}
|
|
117
|
+
if (current.type === utils_1.AST_NODE_TYPES.TSTypeAliasDeclaration ||
|
|
118
|
+
current.type === utils_1.AST_NODE_TYPES.TSInterfaceDeclaration) {
|
|
119
|
+
const id = current.id;
|
|
120
|
+
if (id.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
121
|
+
return id.name;
|
|
31
122
|
}
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
123
|
+
}
|
|
124
|
+
current = current.parent;
|
|
125
|
+
}
|
|
126
|
+
return UNKNOWN_FIELD_LABEL;
|
|
127
|
+
};
|
|
128
|
+
// Determine whether this type node appears in a Firestore model type context
|
|
129
|
+
// i.e., within an interface or type alias declaration, and not within variable/function annotations
|
|
130
|
+
const isInModelTypeContext = (node) => {
|
|
131
|
+
let current = node.parent;
|
|
132
|
+
while (current) {
|
|
133
|
+
// Hard stop for non-type declaration contexts
|
|
134
|
+
if (current.type === utils_1.AST_NODE_TYPES.VariableDeclarator ||
|
|
135
|
+
current.type === utils_1.AST_NODE_TYPES.FunctionDeclaration ||
|
|
136
|
+
current.type === utils_1.AST_NODE_TYPES.FunctionExpression ||
|
|
137
|
+
current.type === utils_1.AST_NODE_TYPES.ArrowFunctionExpression ||
|
|
138
|
+
current.type === utils_1.AST_NODE_TYPES.MethodDefinition ||
|
|
139
|
+
current.type === utils_1.AST_NODE_TYPES.TSMethodSignature ||
|
|
140
|
+
current.type === utils_1.AST_NODE_TYPES.ClassDeclaration ||
|
|
141
|
+
current.type === utils_1.AST_NODE_TYPES.PropertyDefinition ||
|
|
142
|
+
current.type === utils_1.AST_NODE_TYPES.TSParameterProperty) {
|
|
40
143
|
return false;
|
|
144
|
+
}
|
|
145
|
+
if (current.type === utils_1.AST_NODE_TYPES.TSTypeAliasDeclaration ||
|
|
146
|
+
current.type === utils_1.AST_NODE_TYPES.TSInterfaceDeclaration) {
|
|
147
|
+
return true;
|
|
148
|
+
}
|
|
149
|
+
current = current.parent;
|
|
41
150
|
}
|
|
151
|
+
return false;
|
|
42
152
|
};
|
|
43
153
|
exports.noFirestoreObjectArrays = (0, createRule_1.createRule)({
|
|
44
154
|
name: 'no-firestore-object-arrays',
|
|
45
155
|
meta: {
|
|
46
156
|
type: 'problem',
|
|
47
157
|
docs: {
|
|
48
|
-
description: 'Disallow arrays of
|
|
158
|
+
description: 'Disallow arrays of object types in Firestore models. Prefer Record maps keyed by id with an index field, or subcollections/arrays of IDs.',
|
|
49
159
|
recommended: 'warn',
|
|
160
|
+
requiresTypeChecking: false,
|
|
50
161
|
},
|
|
51
162
|
schema: [],
|
|
52
163
|
messages: {
|
|
53
|
-
noObjectArrays:
|
|
164
|
+
noObjectArrays: [
|
|
165
|
+
// What's wrong
|
|
166
|
+
"What's wrong: {{fieldName}} stores an array of objects in a Firestore document.",
|
|
167
|
+
// Why it matters
|
|
168
|
+
'Why it matters: Firestore cannot query inside object arrays, and updating one item rewrites the whole array; concurrent writes can overwrite each other and lose data.',
|
|
169
|
+
// How to fix
|
|
170
|
+
'How to fix: Store items as Record<string, T> keyed by id with an index field for ordering (use toMap/toArr helpers), or move items into a subcollection or store only an array of IDs.',
|
|
171
|
+
].join('\n'),
|
|
54
172
|
},
|
|
55
173
|
},
|
|
56
174
|
defaultOptions: [],
|
|
@@ -58,25 +176,277 @@ exports.noFirestoreObjectArrays = (0, createRule_1.createRule)({
|
|
|
58
176
|
if (!isInFirestoreTypesDirectory(context.getFilename())) {
|
|
59
177
|
return {};
|
|
60
178
|
}
|
|
179
|
+
const sourceCode = context.sourceCode;
|
|
180
|
+
// Collect alias/interface/enum information within this file to refine object vs primitive classification
|
|
181
|
+
const aliasNameToType = new Map();
|
|
182
|
+
const interfaceNames = new Set();
|
|
183
|
+
const enumNames = new Set();
|
|
184
|
+
const visitNode = (n) => {
|
|
185
|
+
switch (n.type) {
|
|
186
|
+
case utils_1.AST_NODE_TYPES.TSTypeAliasDeclaration: {
|
|
187
|
+
aliasNameToType.set(n.id.name, n.typeAnnotation);
|
|
188
|
+
break;
|
|
189
|
+
}
|
|
190
|
+
case utils_1.AST_NODE_TYPES.TSInterfaceDeclaration: {
|
|
191
|
+
interfaceNames.add(n.id.name);
|
|
192
|
+
break;
|
|
193
|
+
}
|
|
194
|
+
case utils_1.AST_NODE_TYPES.TSEnumDeclaration: {
|
|
195
|
+
enumNames.add(n.id.name);
|
|
196
|
+
break;
|
|
197
|
+
}
|
|
198
|
+
case utils_1.AST_NODE_TYPES.ExportNamedDeclaration: {
|
|
199
|
+
if (n.declaration)
|
|
200
|
+
visitNode(n.declaration);
|
|
201
|
+
break;
|
|
202
|
+
}
|
|
203
|
+
case utils_1.AST_NODE_TYPES.ExportDefaultDeclaration: {
|
|
204
|
+
if (n.declaration) {
|
|
205
|
+
visitNode(n.declaration);
|
|
206
|
+
}
|
|
207
|
+
break;
|
|
208
|
+
}
|
|
209
|
+
case utils_1.AST_NODE_TYPES.TSExportAssignment: {
|
|
210
|
+
if (n.expression) {
|
|
211
|
+
visitNode(n.expression);
|
|
212
|
+
}
|
|
213
|
+
break;
|
|
214
|
+
}
|
|
215
|
+
case utils_1.AST_NODE_TYPES.TSModuleDeclaration: {
|
|
216
|
+
const body = n.body || null;
|
|
217
|
+
if (body && body.type === utils_1.AST_NODE_TYPES.TSModuleBlock) {
|
|
218
|
+
for (const stmt of body.body) {
|
|
219
|
+
visitNode(stmt);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
else if (body && body.type === utils_1.AST_NODE_TYPES.TSModuleDeclaration) {
|
|
223
|
+
visitNode(body);
|
|
224
|
+
}
|
|
225
|
+
break;
|
|
226
|
+
}
|
|
227
|
+
default: {
|
|
228
|
+
// Only traverse a shallow subset we care about
|
|
229
|
+
break;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
};
|
|
233
|
+
for (const stmt of sourceCode.ast.body) {
|
|
234
|
+
visitNode(stmt);
|
|
235
|
+
}
|
|
236
|
+
const seenAlias = new Set();
|
|
237
|
+
const visitingAliases = new Set();
|
|
238
|
+
const isPrimitiveLikeAlias = (name, recursionDepth) => {
|
|
239
|
+
if (seenAlias.has(name))
|
|
240
|
+
return true;
|
|
241
|
+
if (recursionDepth > 50)
|
|
242
|
+
return false;
|
|
243
|
+
if (PRIMITIVE_TYPES.has(name))
|
|
244
|
+
return true;
|
|
245
|
+
if (enumNames.has(name))
|
|
246
|
+
return true; // enums are non-object primitives for our purposes
|
|
247
|
+
const aliased = aliasNameToType.get(name);
|
|
248
|
+
if (!aliased)
|
|
249
|
+
return false;
|
|
250
|
+
if (visitingAliases.has(name)) {
|
|
251
|
+
return false;
|
|
252
|
+
}
|
|
253
|
+
visitingAliases.add(name);
|
|
254
|
+
seenAlias.add(name);
|
|
255
|
+
// Determine if the alias ultimately resolves to only primitive-like/literal types
|
|
256
|
+
const node = aliased;
|
|
257
|
+
const result = isPrimitiveLikeTypeNode(node, recursionDepth + 1);
|
|
258
|
+
visitingAliases.delete(name);
|
|
259
|
+
if (result) {
|
|
260
|
+
seenAlias.add(name);
|
|
261
|
+
}
|
|
262
|
+
else {
|
|
263
|
+
// Remove optimistic marking when resolution fails
|
|
264
|
+
seenAlias.delete(name);
|
|
265
|
+
}
|
|
266
|
+
return result;
|
|
267
|
+
};
|
|
268
|
+
const isPrimitiveLikeTypeNode = (node, recursionDepth = 0) => {
|
|
269
|
+
if (recursionDepth > 25) {
|
|
270
|
+
return false;
|
|
271
|
+
}
|
|
272
|
+
if (isParenthesizedType(node)) {
|
|
273
|
+
const paren = node;
|
|
274
|
+
return isPrimitiveLikeTypeNode(paren.typeAnnotation, recursionDepth + 1);
|
|
275
|
+
}
|
|
276
|
+
switch (node.type) {
|
|
277
|
+
case utils_1.AST_NODE_TYPES.TSStringKeyword:
|
|
278
|
+
case utils_1.AST_NODE_TYPES.TSNumberKeyword:
|
|
279
|
+
case utils_1.AST_NODE_TYPES.TSBooleanKeyword:
|
|
280
|
+
case utils_1.AST_NODE_TYPES.TSNullKeyword:
|
|
281
|
+
case utils_1.AST_NODE_TYPES.TSUndefinedKeyword:
|
|
282
|
+
case utils_1.AST_NODE_TYPES.TSNeverKeyword:
|
|
283
|
+
return true;
|
|
284
|
+
case utils_1.AST_NODE_TYPES.TSAnyKeyword:
|
|
285
|
+
case utils_1.AST_NODE_TYPES.TSUnknownKeyword:
|
|
286
|
+
return false;
|
|
287
|
+
case utils_1.AST_NODE_TYPES.TSLiteralType:
|
|
288
|
+
return true; // string/number/boolean literals
|
|
289
|
+
case utils_1.AST_NODE_TYPES.TSTypeReference: {
|
|
290
|
+
// Allow known primitive-like references and enums or primitive-like aliases
|
|
291
|
+
const ref = node;
|
|
292
|
+
if (ref.typeName.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
293
|
+
const name = ref.typeName.name;
|
|
294
|
+
if (PRIMITIVE_TYPES.has(name) || enumNames.has(name))
|
|
295
|
+
return true;
|
|
296
|
+
if (seenAlias.has(name))
|
|
297
|
+
return true;
|
|
298
|
+
return isPrimitiveLikeAlias(name, recursionDepth + 1);
|
|
299
|
+
}
|
|
300
|
+
if (ref.typeName.type === utils_1.AST_NODE_TYPES.TSQualifiedName) {
|
|
301
|
+
const name = getRightmostIdentifierName(ref.typeName);
|
|
302
|
+
if (PRIMITIVE_TYPES.has(name) || enumNames.has(name))
|
|
303
|
+
return true;
|
|
304
|
+
if (seenAlias.has(name))
|
|
305
|
+
return true;
|
|
306
|
+
return isPrimitiveLikeAlias(name, recursionDepth + 1);
|
|
307
|
+
}
|
|
308
|
+
return false;
|
|
309
|
+
}
|
|
310
|
+
case utils_1.AST_NODE_TYPES.TSUnionType: {
|
|
311
|
+
return node.types.every((t) => isPrimitiveLikeTypeNode(t, recursionDepth + 1));
|
|
312
|
+
}
|
|
313
|
+
case utils_1.AST_NODE_TYPES.TSTypeOperator: {
|
|
314
|
+
// Treat keyof/unique symbol/etc as primitive-like to avoid false positives
|
|
315
|
+
if (node.operator !== 'readonly')
|
|
316
|
+
return true;
|
|
317
|
+
return isPrimitiveLikeTypeNode(node
|
|
318
|
+
.typeAnnotation, recursionDepth + 1);
|
|
319
|
+
}
|
|
320
|
+
case utils_1.AST_NODE_TYPES.TSTemplateLiteralType: {
|
|
321
|
+
// Template literal types behave like strings
|
|
322
|
+
return true;
|
|
323
|
+
}
|
|
324
|
+
default:
|
|
325
|
+
return false;
|
|
326
|
+
}
|
|
327
|
+
};
|
|
328
|
+
const isObjectType = (node) => {
|
|
329
|
+
if (isParenthesizedType(node)) {
|
|
330
|
+
const paren = node;
|
|
331
|
+
return isObjectType(paren.typeAnnotation);
|
|
332
|
+
}
|
|
333
|
+
switch (node.type) {
|
|
334
|
+
case utils_1.AST_NODE_TYPES.TSTypeLiteral:
|
|
335
|
+
return true;
|
|
336
|
+
case utils_1.AST_NODE_TYPES.TSTypeReference: {
|
|
337
|
+
const tn = node.typeName;
|
|
338
|
+
if (tn.type !== utils_1.AST_NODE_TYPES.Identifier &&
|
|
339
|
+
tn.type !== utils_1.AST_NODE_TYPES.TSQualifiedName) {
|
|
340
|
+
// Unsupported reference (e.g., ThisType) — do not assume object to avoid false positives
|
|
341
|
+
return false;
|
|
342
|
+
}
|
|
343
|
+
const refName = tn.type === utils_1.AST_NODE_TYPES.Identifier
|
|
344
|
+
? tn.name
|
|
345
|
+
: getRightmostIdentifierName(tn);
|
|
346
|
+
if (PRIMITIVE_TYPES.has(refName))
|
|
347
|
+
return false;
|
|
348
|
+
if (interfaceNames.has(refName))
|
|
349
|
+
return true;
|
|
350
|
+
if (enumNames.has(refName))
|
|
351
|
+
return false;
|
|
352
|
+
if (aliasNameToType.has(refName)) {
|
|
353
|
+
return !isPrimitiveLikeAlias(refName, 0);
|
|
354
|
+
}
|
|
355
|
+
// Unknown reference: do not assume object to avoid false positives
|
|
356
|
+
return false;
|
|
357
|
+
}
|
|
358
|
+
case utils_1.AST_NODE_TYPES.TSIntersectionType:
|
|
359
|
+
case utils_1.AST_NODE_TYPES.TSUnionType:
|
|
360
|
+
return node.types.some(isObjectType);
|
|
361
|
+
case utils_1.AST_NODE_TYPES.TSMappedType:
|
|
362
|
+
return true;
|
|
363
|
+
case utils_1.AST_NODE_TYPES.TSIndexedAccessType:
|
|
364
|
+
// Treat indexed access as object-like to align with existing tests
|
|
365
|
+
return true;
|
|
366
|
+
case utils_1.AST_NODE_TYPES.TSTypeOperator:
|
|
367
|
+
if (node.operator === 'readonly') {
|
|
368
|
+
return isObjectType(node
|
|
369
|
+
.typeAnnotation);
|
|
370
|
+
}
|
|
371
|
+
return false;
|
|
372
|
+
case utils_1.AST_NODE_TYPES.TSAnyKeyword:
|
|
373
|
+
case utils_1.AST_NODE_TYPES.TSUnknownKeyword:
|
|
374
|
+
return true;
|
|
375
|
+
default:
|
|
376
|
+
return false;
|
|
377
|
+
}
|
|
378
|
+
};
|
|
379
|
+
const skipReadonlyAndParens = (parent) => {
|
|
380
|
+
let currentParent = parent;
|
|
381
|
+
while (currentParent) {
|
|
382
|
+
if (currentParent.type === utils_1.AST_NODE_TYPES.TSTypeOperator &&
|
|
383
|
+
currentParent.operator === 'readonly') {
|
|
384
|
+
currentParent = currentParent.parent;
|
|
385
|
+
continue;
|
|
386
|
+
}
|
|
387
|
+
if (isParenthesizedType(currentParent)) {
|
|
388
|
+
const paren = currentParent;
|
|
389
|
+
currentParent = paren.parent;
|
|
390
|
+
continue;
|
|
391
|
+
}
|
|
392
|
+
break;
|
|
393
|
+
}
|
|
394
|
+
return currentParent;
|
|
395
|
+
};
|
|
396
|
+
const isImmediatelyWrappedByArraySyntax = (node) => {
|
|
397
|
+
const parent = skipReadonlyAndParens(node.parent);
|
|
398
|
+
if (!parent)
|
|
399
|
+
return false;
|
|
400
|
+
if (parent.type === utils_1.AST_NODE_TYPES.TSArrayType)
|
|
401
|
+
return true;
|
|
402
|
+
if (parent.type === utils_1.AST_NODE_TYPES.TSTypeReference &&
|
|
403
|
+
isArrayGenericReference(parent))
|
|
404
|
+
return true;
|
|
405
|
+
return false;
|
|
406
|
+
};
|
|
61
407
|
return {
|
|
62
408
|
TSArrayType(node) {
|
|
63
|
-
|
|
409
|
+
// Only consider arrays within model type/interface declarations
|
|
410
|
+
if (!isInModelTypeContext(node)) {
|
|
411
|
+
return;
|
|
412
|
+
}
|
|
413
|
+
if (isImmediatelyWrappedByArraySyntax(node)) {
|
|
414
|
+
return;
|
|
415
|
+
}
|
|
416
|
+
const base = unwrapArrayElementType(node.elementType);
|
|
417
|
+
if (isObjectType(base)) {
|
|
64
418
|
context.report({
|
|
65
419
|
node,
|
|
66
420
|
messageId: 'noObjectArrays',
|
|
421
|
+
data: {
|
|
422
|
+
fieldName: getFieldName(node),
|
|
423
|
+
},
|
|
67
424
|
});
|
|
68
425
|
}
|
|
69
426
|
},
|
|
70
427
|
TSTypeReference(node) {
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
428
|
+
if (node.typeName.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
429
|
+
(node.typeName.name === 'Array' ||
|
|
430
|
+
node.typeName.name === 'ReadonlyArray') &&
|
|
74
431
|
node.typeParameters) {
|
|
75
|
-
|
|
76
|
-
if (
|
|
432
|
+
// Only consider arrays within model type/interface declarations
|
|
433
|
+
if (!isInModelTypeContext(node)) {
|
|
434
|
+
return;
|
|
435
|
+
}
|
|
436
|
+
if (isImmediatelyWrappedByArraySyntax(node)) {
|
|
437
|
+
return;
|
|
438
|
+
}
|
|
439
|
+
const elementType = node.typeParameters.params[0] ?? null;
|
|
440
|
+
if (!elementType)
|
|
441
|
+
return;
|
|
442
|
+
const base = unwrapArrayElementType(elementType);
|
|
443
|
+
if (isObjectType(base)) {
|
|
77
444
|
context.report({
|
|
78
445
|
node,
|
|
79
446
|
messageId: 'noObjectArrays',
|
|
447
|
+
data: {
|
|
448
|
+
fieldName: getFieldName(node),
|
|
449
|
+
},
|
|
80
450
|
});
|
|
81
451
|
}
|
|
82
452
|
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
type Options = [
|
|
2
|
+
{
|
|
3
|
+
ignoreClassMethods?: boolean;
|
|
4
|
+
ignoreInterfaceImplementations?: boolean;
|
|
5
|
+
interfaceAllowlist?: string[];
|
|
6
|
+
allowNames?: string[];
|
|
7
|
+
allowPatterns?: string[];
|
|
8
|
+
allowFilePatterns?: string[];
|
|
9
|
+
}
|
|
10
|
+
];
|
|
11
|
+
export declare const noHandlerSuffix: import("@typescript-eslint/utils/dist/ts-eslint/Rule").RuleModule<"handlerSuffix", Options, import("@typescript-eslint/utils/dist/ts-eslint/Rule").RuleListener>;
|
|
12
|
+
export {};
|