@blumintinc/eslint-plugin-blumint 1.13.0 → 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 +170 -139
- package/lib/index.js +190 -1
- package/lib/rules/array-methods-this-context.js +1 -1
- 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.js +3 -4
- package/lib/rules/enforce-assert-safe-object-key.js +11 -11
- package/lib/rules/enforce-boolean-naming-prefixes.js +11 -23
- 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-dynamic-file-naming.d.ts +2 -0
- package/lib/rules/enforce-dynamic-file-naming.js +53 -28
- 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-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 +1 -1
- package/lib/rules/enforce-identifiable-firestore-type.js +108 -58
- package/lib/rules/enforce-memoize-async.js +2 -6
- 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 +2 -2
- 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 +5 -4
- 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 +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/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 +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 +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 +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.js +4 -6
- package/lib/rules/no-excessive-parent-chain.js +1 -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 +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.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.js +38 -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 +459 -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 +347 -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 +24 -5
- 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 +9 -4
- 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 +220 -171
- package/lib/rules/prefer-settings-object.js +2 -2
- 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-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.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 +136 -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 +1 -1
- 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 +13 -1
- package/lib/utils/ASTHelpers.js +24 -0
- 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
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { TSESLint } from '@typescript-eslint/utils';
|
|
2
|
+
type Options = [
|
|
3
|
+
{
|
|
4
|
+
objectNamePattern?: string[];
|
|
5
|
+
ignoreInLoops?: boolean;
|
|
6
|
+
emptyCheckFunctions?: string[];
|
|
7
|
+
}
|
|
8
|
+
];
|
|
9
|
+
type MessageIds = 'missingEmptyObjectCheck';
|
|
10
|
+
export declare const enforceEmptyObjectCheck: TSESLint.RuleModule<MessageIds, Options>;
|
|
11
|
+
export {};
|
|
@@ -0,0 +1,502 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || function (mod) {
|
|
19
|
+
if (mod && mod.__esModule) return mod;
|
|
20
|
+
var result = {};
|
|
21
|
+
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
|
|
22
|
+
__setModuleDefault(result, mod);
|
|
23
|
+
return result;
|
|
24
|
+
};
|
|
25
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
26
|
+
exports.enforceEmptyObjectCheck = void 0;
|
|
27
|
+
const utils_1 = require("@typescript-eslint/utils");
|
|
28
|
+
const ts = __importStar(require("typescript"));
|
|
29
|
+
const createRule_1 = require("../utils/createRule");
|
|
30
|
+
const DEFAULT_OBJECT_SUFFIXES = [
|
|
31
|
+
'Config',
|
|
32
|
+
'Configs',
|
|
33
|
+
'Data',
|
|
34
|
+
'Info',
|
|
35
|
+
'Settings',
|
|
36
|
+
'Options',
|
|
37
|
+
'Props',
|
|
38
|
+
'State',
|
|
39
|
+
'Response',
|
|
40
|
+
'Result',
|
|
41
|
+
'Payload',
|
|
42
|
+
'Map',
|
|
43
|
+
'Record',
|
|
44
|
+
'Object',
|
|
45
|
+
'Obj',
|
|
46
|
+
'Details',
|
|
47
|
+
'Meta',
|
|
48
|
+
'Profile',
|
|
49
|
+
'Request',
|
|
50
|
+
'Params',
|
|
51
|
+
'Context',
|
|
52
|
+
];
|
|
53
|
+
const DEFAULT_EMPTY_CHECK_FUNCTIONS = ['isEmpty'];
|
|
54
|
+
const BOOLEAN_PREFIXES = [
|
|
55
|
+
'is',
|
|
56
|
+
'has',
|
|
57
|
+
'can',
|
|
58
|
+
'should',
|
|
59
|
+
'was',
|
|
60
|
+
'were',
|
|
61
|
+
'will',
|
|
62
|
+
'did',
|
|
63
|
+
];
|
|
64
|
+
const NON_OBJECT_LIKE_NAMES = [
|
|
65
|
+
'count',
|
|
66
|
+
'index',
|
|
67
|
+
'idx',
|
|
68
|
+
'length',
|
|
69
|
+
'size',
|
|
70
|
+
'total',
|
|
71
|
+
'flag',
|
|
72
|
+
'enabled',
|
|
73
|
+
'ready',
|
|
74
|
+
'items',
|
|
75
|
+
'item',
|
|
76
|
+
'list',
|
|
77
|
+
'lists',
|
|
78
|
+
'array',
|
|
79
|
+
'arr',
|
|
80
|
+
];
|
|
81
|
+
function hasBooleanPrefixBoundary(name) {
|
|
82
|
+
const lower = name.toLowerCase();
|
|
83
|
+
return BOOLEAN_PREFIXES.some((prefix) => {
|
|
84
|
+
if (!lower.startsWith(prefix)) {
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
const boundary = name.charAt(prefix.length);
|
|
88
|
+
return boundary !== '' && boundary >= 'A' && boundary <= 'Z';
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
function isLoopLike(node) {
|
|
92
|
+
return (node.type === utils_1.AST_NODE_TYPES.ForStatement ||
|
|
93
|
+
node.type === utils_1.AST_NODE_TYPES.ForInStatement ||
|
|
94
|
+
node.type === utils_1.AST_NODE_TYPES.ForOfStatement ||
|
|
95
|
+
node.type === utils_1.AST_NODE_TYPES.WhileStatement ||
|
|
96
|
+
node.type === utils_1.AST_NODE_TYPES.DoWhileStatement);
|
|
97
|
+
}
|
|
98
|
+
function isInsideLoop(node) {
|
|
99
|
+
let current = node;
|
|
100
|
+
while (current && current.parent) {
|
|
101
|
+
if (isLoopLike(current.parent)) {
|
|
102
|
+
return true;
|
|
103
|
+
}
|
|
104
|
+
current = current.parent;
|
|
105
|
+
}
|
|
106
|
+
return false;
|
|
107
|
+
}
|
|
108
|
+
function isObjectLikeName(name, patterns) {
|
|
109
|
+
const lower = name.toLowerCase();
|
|
110
|
+
if (hasBooleanPrefixBoundary(name)) {
|
|
111
|
+
return false;
|
|
112
|
+
}
|
|
113
|
+
if (NON_OBJECT_LIKE_NAMES.includes(lower)) {
|
|
114
|
+
return false;
|
|
115
|
+
}
|
|
116
|
+
for (const pattern of patterns) {
|
|
117
|
+
if (name.endsWith(pattern) || lower.endsWith(pattern.toLowerCase())) {
|
|
118
|
+
return true;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return false;
|
|
122
|
+
}
|
|
123
|
+
function isNullableType(type) {
|
|
124
|
+
return ((type.flags & ts.TypeFlags.Null) !== 0 ||
|
|
125
|
+
(type.flags & ts.TypeFlags.Undefined) !== 0 ||
|
|
126
|
+
(type.flags & ts.TypeFlags.Void) !== 0);
|
|
127
|
+
}
|
|
128
|
+
function isNonObjectPrimitive(type) {
|
|
129
|
+
const flag = type.flags;
|
|
130
|
+
return ((flag & ts.TypeFlags.StringLike) !== 0 ||
|
|
131
|
+
(flag & ts.TypeFlags.NumberLike) !== 0 ||
|
|
132
|
+
(flag & ts.TypeFlags.BooleanLike) !== 0 ||
|
|
133
|
+
(flag & ts.TypeFlags.BigIntLike) !== 0 ||
|
|
134
|
+
(flag & ts.TypeFlags.ESSymbolLike) !== 0 ||
|
|
135
|
+
(flag & ts.TypeFlags.EnumLike) !== 0);
|
|
136
|
+
}
|
|
137
|
+
function isAnyOrUnknown(type) {
|
|
138
|
+
return ((type.flags & ts.TypeFlags.Any) !== 0 ||
|
|
139
|
+
(type.flags & ts.TypeFlags.Unknown) !== 0);
|
|
140
|
+
}
|
|
141
|
+
function hasRequiredProperties(type, checker) {
|
|
142
|
+
const properties = checker.getPropertiesOfType(type);
|
|
143
|
+
return properties.some((property) => (property.getFlags() & ts.SymbolFlags.Optional) === 0);
|
|
144
|
+
}
|
|
145
|
+
function isObjectLikeType(type, checker) {
|
|
146
|
+
if (type.isUnion()) {
|
|
147
|
+
let hasObject = false;
|
|
148
|
+
let hasUnknown = false;
|
|
149
|
+
let hasNonObject = false;
|
|
150
|
+
let hasNonNullable = false;
|
|
151
|
+
for (const part of type.types) {
|
|
152
|
+
if (isNullableType(part)) {
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
hasNonNullable = true;
|
|
156
|
+
if (isNonObjectPrimitive(part)) {
|
|
157
|
+
hasNonObject = true;
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
const analysis = isObjectLikeType(part, checker);
|
|
161
|
+
if (analysis === 'object') {
|
|
162
|
+
hasObject = true;
|
|
163
|
+
}
|
|
164
|
+
else if (analysis === 'unknown') {
|
|
165
|
+
hasUnknown = true;
|
|
166
|
+
}
|
|
167
|
+
else {
|
|
168
|
+
hasNonObject = true;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
if (hasObject) {
|
|
172
|
+
return 'object';
|
|
173
|
+
}
|
|
174
|
+
if (!hasNonNullable) {
|
|
175
|
+
return 'non-object';
|
|
176
|
+
}
|
|
177
|
+
if (hasUnknown) {
|
|
178
|
+
return 'unknown';
|
|
179
|
+
}
|
|
180
|
+
return hasNonObject ? 'non-object' : 'unknown';
|
|
181
|
+
}
|
|
182
|
+
if ((type.flags & ts.TypeFlags.Intersection) !== 0) {
|
|
183
|
+
const intersectionType = type;
|
|
184
|
+
let hasObject = false;
|
|
185
|
+
let hasUnknown = false;
|
|
186
|
+
for (const part of intersectionType.types) {
|
|
187
|
+
const analysis = isObjectLikeType(part, checker);
|
|
188
|
+
if (analysis === 'non-object') {
|
|
189
|
+
return 'non-object';
|
|
190
|
+
}
|
|
191
|
+
if (analysis === 'object') {
|
|
192
|
+
hasObject = true;
|
|
193
|
+
}
|
|
194
|
+
else {
|
|
195
|
+
hasUnknown = true;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
if (hasObject) {
|
|
199
|
+
return 'object';
|
|
200
|
+
}
|
|
201
|
+
return hasUnknown ? 'unknown' : 'non-object';
|
|
202
|
+
}
|
|
203
|
+
if (isAnyOrUnknown(type)) {
|
|
204
|
+
return 'unknown';
|
|
205
|
+
}
|
|
206
|
+
if (isNonObjectPrimitive(type) || isNullableType(type)) {
|
|
207
|
+
return 'non-object';
|
|
208
|
+
}
|
|
209
|
+
if ((type.flags & ts.TypeFlags.Object) === 0) {
|
|
210
|
+
return 'non-object';
|
|
211
|
+
}
|
|
212
|
+
if (checker.isArrayType(type) || checker.isTupleType(type)) {
|
|
213
|
+
return 'non-object';
|
|
214
|
+
}
|
|
215
|
+
if (type.getCallSignatures().length > 0) {
|
|
216
|
+
return 'non-object';
|
|
217
|
+
}
|
|
218
|
+
if (hasRequiredProperties(type, checker)) {
|
|
219
|
+
return 'non-object';
|
|
220
|
+
}
|
|
221
|
+
return 'object';
|
|
222
|
+
}
|
|
223
|
+
function isObjectKeysLengthExpression(node, name) {
|
|
224
|
+
if (node.type === utils_1.AST_NODE_TYPES.MemberExpression &&
|
|
225
|
+
!node.computed &&
|
|
226
|
+
node.property.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
227
|
+
node.property.name === 'length' &&
|
|
228
|
+
node.object.type === utils_1.AST_NODE_TYPES.CallExpression &&
|
|
229
|
+
node.object.callee.type === utils_1.AST_NODE_TYPES.MemberExpression &&
|
|
230
|
+
!node.object.callee.computed &&
|
|
231
|
+
node.object.callee.object.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
232
|
+
node.object.callee.object.name === 'Object' &&
|
|
233
|
+
node.object.callee.property.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
234
|
+
node.object.callee.property.name === 'keys' &&
|
|
235
|
+
node.object.arguments.length === 1 &&
|
|
236
|
+
node.object.arguments[0].type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
237
|
+
node.object.arguments[0].name === name) {
|
|
238
|
+
return true;
|
|
239
|
+
}
|
|
240
|
+
return false;
|
|
241
|
+
}
|
|
242
|
+
function isZeroLiteral(node) {
|
|
243
|
+
return node.type === utils_1.AST_NODE_TYPES.Literal && node.value === 0;
|
|
244
|
+
}
|
|
245
|
+
function isLengthZeroComparison(node, name) {
|
|
246
|
+
const { operator, left, right } = node;
|
|
247
|
+
const leftIsLength = isObjectKeysLengthExpression(left, name);
|
|
248
|
+
const rightIsLength = isObjectKeysLengthExpression(right, name);
|
|
249
|
+
const leftIsZero = isZeroLiteral(left);
|
|
250
|
+
const rightIsZero = isZeroLiteral(right);
|
|
251
|
+
/**
|
|
252
|
+
* Only zero-length checks signal emptiness: length never drops below zero, and
|
|
253
|
+
* `> 0` means data is present. Restrict operators to equality and zero-bound
|
|
254
|
+
* comparisons to avoid mistaking impossible `length < 0` or presence checks for
|
|
255
|
+
* valid emptiness guards.
|
|
256
|
+
*/
|
|
257
|
+
if (operator === '===' || operator === '==') {
|
|
258
|
+
return (leftIsLength && rightIsZero) || (rightIsLength && leftIsZero);
|
|
259
|
+
}
|
|
260
|
+
if (operator === '<=') {
|
|
261
|
+
return leftIsLength && rightIsZero;
|
|
262
|
+
}
|
|
263
|
+
if (operator === '>=') {
|
|
264
|
+
return leftIsZero && rightIsLength;
|
|
265
|
+
}
|
|
266
|
+
return false;
|
|
267
|
+
}
|
|
268
|
+
function conditionHasEmptyCheck(node, name, emptyCheckFunctions, negationDepth = 0) {
|
|
269
|
+
if (!node)
|
|
270
|
+
return false;
|
|
271
|
+
switch (node.type) {
|
|
272
|
+
case utils_1.AST_NODE_TYPES.LogicalExpression:
|
|
273
|
+
return (conditionHasEmptyCheck(node.left, name, emptyCheckFunctions, negationDepth) ||
|
|
274
|
+
conditionHasEmptyCheck(node.right, name, emptyCheckFunctions, negationDepth));
|
|
275
|
+
case utils_1.AST_NODE_TYPES.BinaryExpression:
|
|
276
|
+
if (isLengthZeroComparison(node, name)) {
|
|
277
|
+
return negationDepth % 2 === 0;
|
|
278
|
+
}
|
|
279
|
+
return (conditionHasEmptyCheck(node.left, name, emptyCheckFunctions, negationDepth) ||
|
|
280
|
+
conditionHasEmptyCheck(node.right, name, emptyCheckFunctions, negationDepth));
|
|
281
|
+
case utils_1.AST_NODE_TYPES.UnaryExpression:
|
|
282
|
+
if (node.operator === '!') {
|
|
283
|
+
return conditionHasEmptyCheck(node.argument, name, emptyCheckFunctions, negationDepth + 1);
|
|
284
|
+
}
|
|
285
|
+
return conditionHasEmptyCheck(node.argument, name, emptyCheckFunctions, negationDepth);
|
|
286
|
+
case utils_1.AST_NODE_TYPES.CallExpression: {
|
|
287
|
+
const callee = node.callee;
|
|
288
|
+
const firstArgIsTarget = node.arguments[0] &&
|
|
289
|
+
node.arguments[0].type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
290
|
+
node.arguments[0].name === name;
|
|
291
|
+
if (callee.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
292
|
+
emptyCheckFunctions.has(callee.name) &&
|
|
293
|
+
firstArgIsTarget) {
|
|
294
|
+
return negationDepth % 2 === 0;
|
|
295
|
+
}
|
|
296
|
+
if (callee.type === utils_1.AST_NODE_TYPES.MemberExpression &&
|
|
297
|
+
!callee.computed &&
|
|
298
|
+
callee.property.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
299
|
+
emptyCheckFunctions.has(callee.property.name) &&
|
|
300
|
+
firstArgIsTarget) {
|
|
301
|
+
return negationDepth % 2 === 0;
|
|
302
|
+
}
|
|
303
|
+
return (conditionHasEmptyCheck(callee, name, emptyCheckFunctions, negationDepth) ||
|
|
304
|
+
node.arguments.some((argument) => conditionHasEmptyCheck(argument, name, emptyCheckFunctions, negationDepth)));
|
|
305
|
+
}
|
|
306
|
+
case utils_1.AST_NODE_TYPES.MemberExpression:
|
|
307
|
+
/**
|
|
308
|
+
* `!Object.keys(name).length` counts as an emptiness check through negation
|
|
309
|
+
* depth; comparisons like `length < 0` or `length > 0` remain excluded because
|
|
310
|
+
* length is never negative and `> 0` signals presence rather than emptiness.
|
|
311
|
+
*/
|
|
312
|
+
if (isObjectKeysLengthExpression(node, name)) {
|
|
313
|
+
return negationDepth % 2 === 1;
|
|
314
|
+
}
|
|
315
|
+
return conditionHasEmptyCheck(node.object, name, emptyCheckFunctions, negationDepth);
|
|
316
|
+
case utils_1.AST_NODE_TYPES.ConditionalExpression:
|
|
317
|
+
return (conditionHasEmptyCheck(node.test, name, emptyCheckFunctions, negationDepth) ||
|
|
318
|
+
conditionHasEmptyCheck(node.consequent, name, emptyCheckFunctions, negationDepth) ||
|
|
319
|
+
conditionHasEmptyCheck(node.alternate, name, emptyCheckFunctions, negationDepth));
|
|
320
|
+
default:
|
|
321
|
+
return false;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
function collectNegations(node, results) {
|
|
325
|
+
if (node.type === utils_1.AST_NODE_TYPES.UnaryExpression && node.operator === '!') {
|
|
326
|
+
results.push(node);
|
|
327
|
+
}
|
|
328
|
+
else if (node.type === utils_1.AST_NODE_TYPES.LogicalExpression) {
|
|
329
|
+
collectNegations(node.left, results);
|
|
330
|
+
collectNegations(node.right, results);
|
|
331
|
+
}
|
|
332
|
+
else if (node.type === utils_1.AST_NODE_TYPES.ConditionalExpression) {
|
|
333
|
+
collectNegations(node.test, results);
|
|
334
|
+
collectNegations(node.consequent, results);
|
|
335
|
+
collectNegations(node.alternate, results);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
function getRootCondition(node) {
|
|
339
|
+
let current = node;
|
|
340
|
+
while (current && current.parent) {
|
|
341
|
+
const parent = current.parent;
|
|
342
|
+
if ((parent.type === utils_1.AST_NODE_TYPES.IfStatement && parent.test === current) ||
|
|
343
|
+
(parent.type === utils_1.AST_NODE_TYPES.WhileStatement &&
|
|
344
|
+
parent.test === current) ||
|
|
345
|
+
(parent.type === utils_1.AST_NODE_TYPES.DoWhileStatement &&
|
|
346
|
+
parent.test === current) ||
|
|
347
|
+
(parent.type === utils_1.AST_NODE_TYPES.ForStatement &&
|
|
348
|
+
parent.test === current) ||
|
|
349
|
+
(parent.type === utils_1.AST_NODE_TYPES.ConditionalExpression &&
|
|
350
|
+
parent.test === current)) {
|
|
351
|
+
return current;
|
|
352
|
+
}
|
|
353
|
+
if (parent.type === utils_1.AST_NODE_TYPES.LogicalExpression ||
|
|
354
|
+
parent.type === utils_1.AST_NODE_TYPES.BinaryExpression ||
|
|
355
|
+
parent.type === utils_1.AST_NODE_TYPES.UnaryExpression ||
|
|
356
|
+
parent.type === utils_1.AST_NODE_TYPES.ConditionalExpression) {
|
|
357
|
+
current = parent;
|
|
358
|
+
continue;
|
|
359
|
+
}
|
|
360
|
+
break;
|
|
361
|
+
}
|
|
362
|
+
return null;
|
|
363
|
+
}
|
|
364
|
+
exports.enforceEmptyObjectCheck = (0, createRule_1.createRule)({
|
|
365
|
+
name: 'enforce-empty-object-check',
|
|
366
|
+
meta: {
|
|
367
|
+
type: 'problem',
|
|
368
|
+
docs: {
|
|
369
|
+
description: 'Ensure object existence checks also guard against empty objects so that empty payloads are treated like missing data.',
|
|
370
|
+
recommended: 'error',
|
|
371
|
+
},
|
|
372
|
+
fixable: 'code',
|
|
373
|
+
schema: [
|
|
374
|
+
{
|
|
375
|
+
type: 'object',
|
|
376
|
+
properties: {
|
|
377
|
+
objectNamePattern: {
|
|
378
|
+
type: 'array',
|
|
379
|
+
items: { type: 'string' },
|
|
380
|
+
},
|
|
381
|
+
ignoreInLoops: {
|
|
382
|
+
type: 'boolean',
|
|
383
|
+
},
|
|
384
|
+
emptyCheckFunctions: {
|
|
385
|
+
type: 'array',
|
|
386
|
+
items: { type: 'string' },
|
|
387
|
+
},
|
|
388
|
+
},
|
|
389
|
+
additionalProperties: false,
|
|
390
|
+
},
|
|
391
|
+
],
|
|
392
|
+
messages: {
|
|
393
|
+
missingEmptyObjectCheck: 'What\'s wrong: "{{name}}" is only checked for falsiness, so `{}` passes the guard. Why it matters: empty payloads or configs behave like missing data and can execute "has data" logic incorrectly. How to fix: also check emptiness (for example, Object.keys({{name}}).length === 0 or a configured empty-check helper).',
|
|
394
|
+
},
|
|
395
|
+
},
|
|
396
|
+
defaultOptions: [{}],
|
|
397
|
+
create(context) {
|
|
398
|
+
const sourceCode = context.getSourceCode();
|
|
399
|
+
const parserServices = sourceCode.parserServices;
|
|
400
|
+
const checker = parserServices?.program?.getTypeChecker();
|
|
401
|
+
const options = context.options[0] ?? {};
|
|
402
|
+
const { objectNamePattern = [], ignoreInLoops = false, emptyCheckFunctions = [], } = options;
|
|
403
|
+
const patternSet = new Set([
|
|
404
|
+
...DEFAULT_OBJECT_SUFFIXES,
|
|
405
|
+
...objectNamePattern,
|
|
406
|
+
]);
|
|
407
|
+
const emptyCheckFunctionsSet = new Set([
|
|
408
|
+
...DEFAULT_EMPTY_CHECK_FUNCTIONS,
|
|
409
|
+
...emptyCheckFunctions,
|
|
410
|
+
]);
|
|
411
|
+
const processedExpressions = new WeakSet();
|
|
412
|
+
const processedNegations = new WeakSet();
|
|
413
|
+
function isLikelyObject(identifier) {
|
|
414
|
+
if (checker && parserServices?.esTreeNodeToTSNodeMap) {
|
|
415
|
+
try {
|
|
416
|
+
const tsNode = parserServices.esTreeNodeToTSNodeMap.get(identifier);
|
|
417
|
+
const type = checker.getTypeAtLocation(tsNode);
|
|
418
|
+
const analysis = isObjectLikeType(type, checker);
|
|
419
|
+
if (analysis === 'object') {
|
|
420
|
+
return true;
|
|
421
|
+
}
|
|
422
|
+
if (analysis === 'non-object') {
|
|
423
|
+
return false;
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
catch {
|
|
427
|
+
// TypeScript parser services can throw when AST-to-TS node mapping fails; fall back to naming heuristic so linting does not crash.
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
return isObjectLikeName(identifier.name, patternSet);
|
|
431
|
+
}
|
|
432
|
+
function reportNegation(node, identifier) {
|
|
433
|
+
if (processedNegations.has(node)) {
|
|
434
|
+
return;
|
|
435
|
+
}
|
|
436
|
+
processedNegations.add(node);
|
|
437
|
+
if (ignoreInLoops && isInsideLoop(node)) {
|
|
438
|
+
return;
|
|
439
|
+
}
|
|
440
|
+
const conditionRoot = getRootCondition(node);
|
|
441
|
+
if (conditionRoot &&
|
|
442
|
+
conditionHasEmptyCheck(conditionRoot, identifier.name, emptyCheckFunctionsSet)) {
|
|
443
|
+
return;
|
|
444
|
+
}
|
|
445
|
+
if (!isLikelyObject(identifier)) {
|
|
446
|
+
return;
|
|
447
|
+
}
|
|
448
|
+
context.report({
|
|
449
|
+
node,
|
|
450
|
+
messageId: 'missingEmptyObjectCheck',
|
|
451
|
+
data: {
|
|
452
|
+
name: identifier.name,
|
|
453
|
+
},
|
|
454
|
+
fix(fixer) {
|
|
455
|
+
const identifierText = sourceCode.getText(identifier);
|
|
456
|
+
const replacement = `(${node.operator}${identifierText} || Object.keys(${identifierText}).length === 0)`;
|
|
457
|
+
return fixer.replaceText(node, replacement);
|
|
458
|
+
},
|
|
459
|
+
});
|
|
460
|
+
}
|
|
461
|
+
function handleTestExpression(expression) {
|
|
462
|
+
if (processedExpressions.has(expression)) {
|
|
463
|
+
return;
|
|
464
|
+
}
|
|
465
|
+
processedExpressions.add(expression);
|
|
466
|
+
const negations = [];
|
|
467
|
+
collectNegations(expression, negations);
|
|
468
|
+
for (const negation of negations) {
|
|
469
|
+
if (negation.argument.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
470
|
+
negation.operator === '!') {
|
|
471
|
+
reportNegation(negation, negation.argument);
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
return {
|
|
476
|
+
IfStatement(node) {
|
|
477
|
+
if (node.test) {
|
|
478
|
+
handleTestExpression(node.test);
|
|
479
|
+
}
|
|
480
|
+
},
|
|
481
|
+
WhileStatement(node) {
|
|
482
|
+
if (node.test) {
|
|
483
|
+
handleTestExpression(node.test);
|
|
484
|
+
}
|
|
485
|
+
},
|
|
486
|
+
DoWhileStatement(node) {
|
|
487
|
+
if (node.test) {
|
|
488
|
+
handleTestExpression(node.test);
|
|
489
|
+
}
|
|
490
|
+
},
|
|
491
|
+
ForStatement(node) {
|
|
492
|
+
if (node.test) {
|
|
493
|
+
handleTestExpression(node.test);
|
|
494
|
+
}
|
|
495
|
+
},
|
|
496
|
+
ConditionalExpression(node) {
|
|
497
|
+
handleTestExpression(node.test);
|
|
498
|
+
},
|
|
499
|
+
};
|
|
500
|
+
},
|
|
501
|
+
});
|
|
502
|
+
//# sourceMappingURL=enforce-empty-object-check.js.map
|
|
@@ -402,7 +402,7 @@ exports.enforceExportedFunctionTypes = (0, createRule_1.createRule)({
|
|
|
402
402
|
if (isBuiltInType(typeName)) {
|
|
403
403
|
return true;
|
|
404
404
|
}
|
|
405
|
-
const sourceCode = context.
|
|
405
|
+
const sourceCode = context.sourceCode;
|
|
406
406
|
const program = sourceCode.ast;
|
|
407
407
|
// Check for imported types first - if found, return true immediately
|
|
408
408
|
// since imported types are already available to consumers
|