@blumintinc/eslint-plugin-blumint 1.20.66 → 1.20.67
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/lib/index.js +1 -1
- package/lib/rules/enforce-microdiff.js +5 -0
- package/lib/rules/memo-nested-react-components.js +6 -2
- package/lib/rules/no-compositing-layer-props.js +4 -0
- package/lib/rules/no-jsx-in-hooks.js +5 -2
- package/lib/rules/optimize-object-boolean-conditions.d.ts +2 -1
- package/lib/rules/optimize-object-boolean-conditions.js +310 -35
- package/lib/rules/prefer-getter-over-parameterless-method.js +1 -1
- package/lib/rules/prefer-sx-prop-over-system-props.d.ts +1 -0
- package/lib/rules/prefer-sx-prop-over-system-props.js +499 -82
- package/lib/rules/prefer-union-from-const-array.d.ts +6 -1
- package/lib/rules/prefer-union-from-const-array.js +103 -9
- package/lib/utils/ruleTester.d.ts +26 -0
- package/lib/utils/ruleTester.js +28 -1
- package/package.json +1 -1
- package/release-manifest.json +71 -0
package/lib/index.js
CHANGED
|
@@ -559,6 +559,11 @@ exports.enforceMicrodiff = (0, createRule_1.createRule)({
|
|
|
559
559
|
if (isJsonStringify(node.left) && isJsonStringify(node.right)) {
|
|
560
560
|
const leftArg = node.left.arguments[0];
|
|
561
561
|
const rightArg = node.right.arguments[0];
|
|
562
|
+
// A zero-argument JSON.stringify() leaves these undefined. The
|
|
563
|
+
// indexed read is typed non-optional, so nothing forces the check.
|
|
564
|
+
if (!leftArg || !rightArg) {
|
|
565
|
+
return;
|
|
566
|
+
}
|
|
562
567
|
if (isObjectOrArrayType(leftArg) && isObjectOrArrayType(rightArg)) {
|
|
563
568
|
reportedNodes.add(node);
|
|
564
569
|
const isEqual = node.operator === '===';
|
|
@@ -712,8 +712,12 @@ See: https://react.dev/learn/your-first-component#nesting-and-organizing-compone
|
|
|
712
712
|
if (node.name.type !== utils_1.AST_NODE_TYPES.JSXIdentifier)
|
|
713
713
|
return;
|
|
714
714
|
const attrName = node.name.name;
|
|
715
|
-
// Check if it's a component-type prop
|
|
716
|
-
|
|
715
|
+
// Check if it's a component-type prop. A non-PascalCase prop (e.g.
|
|
716
|
+
// renderHeader) is a render callback used with a render={...} prop, not
|
|
717
|
+
// a component—skip it, mirroring the binding-side carve-out above. The
|
|
718
|
+
// suffix alone is not enough: it matches the tail of renderHeader.
|
|
719
|
+
if (!isPascalCaseName(attrName) ||
|
|
720
|
+
!/(Wrapper|Component|Template|Header|Footer)$/.test(attrName)) {
|
|
717
721
|
return;
|
|
718
722
|
}
|
|
719
723
|
if (!node.value ||
|
|
@@ -46,6 +46,10 @@ const NON_COMPOSITING_VALUES = {
|
|
|
46
46
|
perspective: new Set(['none']),
|
|
47
47
|
'will-change': new Set(['auto', 'unset', 'initial', 'inherit', 'revert']),
|
|
48
48
|
'backface-visibility': new Set(['visible']),
|
|
49
|
+
// `normal` is the initial value and creates no stacking context. Because
|
|
50
|
+
// mix-blend-mode is not inherited, `initial`/`unset`/`revert` all resolve to
|
|
51
|
+
// it. `inherit` is excluded: it can resolve to a blending value set higher up.
|
|
52
|
+
'mix-blend-mode': new Set(['normal', 'initial', 'unset', 'revert']),
|
|
49
53
|
};
|
|
50
54
|
exports.noCompositingLayerProps = (0, createRule_1.createRule)({
|
|
51
55
|
name: 'no-compositing-layer-props',
|
|
@@ -114,9 +114,12 @@ const containsJsxInUseMemo = (node) => {
|
|
|
114
114
|
body.callee.type === utils_1.AST_NODE_TYPES.MemberExpression &&
|
|
115
115
|
body.callee.property.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
116
116
|
body.callee.property.name === 'map') {
|
|
117
|
+
// A zero-argument .map() leaves this undefined. The indexed read is
|
|
118
|
+
// typed non-optional, so nothing forces the check.
|
|
117
119
|
const mapCallback = body.arguments[0];
|
|
118
|
-
if (
|
|
119
|
-
mapCallback.type === utils_1.AST_NODE_TYPES.
|
|
120
|
+
if (mapCallback &&
|
|
121
|
+
(mapCallback.type === utils_1.AST_NODE_TYPES.ArrowFunctionExpression ||
|
|
122
|
+
mapCallback.type === utils_1.AST_NODE_TYPES.FunctionExpression) &&
|
|
120
123
|
isJsxElement(mapCallback.body)) {
|
|
121
124
|
return true;
|
|
122
125
|
}
|
|
@@ -1 +1,2 @@
|
|
|
1
|
-
|
|
1
|
+
import { TSESLint } from '@typescript-eslint/utils';
|
|
2
|
+
export declare const optimizeObjectBooleanConditions: TSESLint.RuleModule<"extractBooleanCondition", [], TSESLint.RuleListener>;
|
|
@@ -4,6 +4,270 @@ exports.optimizeObjectBooleanConditions = void 0;
|
|
|
4
4
|
const utils_1 = require("@typescript-eslint/utils");
|
|
5
5
|
const createRule_1 = require("../utils/createRule");
|
|
6
6
|
const HOOK_NAMES = new Set(['useEffect', 'useCallback', 'useMemo']);
|
|
7
|
+
const STATE_HOOK_NAMES = new Set(['useState']);
|
|
8
|
+
/**
|
|
9
|
+
* Approved boolean-name prefixes, kept in sync with `DEFAULT_BOOLEAN_PREFIXES`
|
|
10
|
+
* in `enforce-boolean-naming-prefixes`. The list is duplicated rather than
|
|
11
|
+
* imported because that rule keeps it module-private, and exporting it would
|
|
12
|
+
* make a single change span two rule files, which the repository's
|
|
13
|
+
* one-rule-per-commit scope contract forbids.
|
|
14
|
+
*
|
|
15
|
+
* The duplication matters for correctness, not style: `enforce-boolean-naming-prefixes`
|
|
16
|
+
* *requires* booleans to carry one of these prefixes, so treating `!isCollapsed`
|
|
17
|
+
* as an object dependency would make the two rules contradict each other.
|
|
18
|
+
*
|
|
19
|
+
* `have` and `were` are the plural forms that rule derives at runtime from
|
|
20
|
+
* `has` and `was`; `are` already ships in its base list.
|
|
21
|
+
*/
|
|
22
|
+
const BOOLEAN_NAME_PREFIXES = [
|
|
23
|
+
'is',
|
|
24
|
+
'are',
|
|
25
|
+
'has',
|
|
26
|
+
'have',
|
|
27
|
+
'does',
|
|
28
|
+
'can',
|
|
29
|
+
'should',
|
|
30
|
+
'will',
|
|
31
|
+
'was',
|
|
32
|
+
'were',
|
|
33
|
+
'had',
|
|
34
|
+
'did',
|
|
35
|
+
'would',
|
|
36
|
+
'must',
|
|
37
|
+
'allows',
|
|
38
|
+
'supports',
|
|
39
|
+
'needs',
|
|
40
|
+
'asserts',
|
|
41
|
+
'includes',
|
|
42
|
+
];
|
|
43
|
+
/**
|
|
44
|
+
* A name carries a boolean prefix only when the prefix ends on a word boundary,
|
|
45
|
+
* so `isVisible` matches while object names that merely start with the same
|
|
46
|
+
* letters (`isolatedConfig`, `canvasContext`, `willowTree`) do not.
|
|
47
|
+
*/
|
|
48
|
+
function hasBooleanNamePrefix(name) {
|
|
49
|
+
const normalized = name.startsWith('_') ? name.slice(1) : name;
|
|
50
|
+
const lowerName = normalized.toLowerCase();
|
|
51
|
+
return BOOLEAN_NAME_PREFIXES.some((prefix) => {
|
|
52
|
+
if (!lowerName.startsWith(prefix)) {
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
if (normalized.length === prefix.length) {
|
|
56
|
+
return true;
|
|
57
|
+
}
|
|
58
|
+
const nextChar = normalized.charAt(prefix.length);
|
|
59
|
+
const isScreamingSnakeCase = normalized === normalized.toUpperCase() && /[a-z]/i.test(normalized);
|
|
60
|
+
if (isScreamingSnakeCase) {
|
|
61
|
+
return nextChar === '_';
|
|
62
|
+
}
|
|
63
|
+
return (nextChar === '_' ||
|
|
64
|
+
nextChar === '$' ||
|
|
65
|
+
(nextChar >= '0' && nextChar <= '9') ||
|
|
66
|
+
(nextChar === nextChar.toUpperCase() &&
|
|
67
|
+
nextChar !== nextChar.toLowerCase()));
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
const PRIMITIVE_TYPE_KEYWORDS = new Set([
|
|
71
|
+
utils_1.AST_NODE_TYPES.TSBooleanKeyword,
|
|
72
|
+
utils_1.AST_NODE_TYPES.TSNumberKeyword,
|
|
73
|
+
utils_1.AST_NODE_TYPES.TSStringKeyword,
|
|
74
|
+
utils_1.AST_NODE_TYPES.TSBigIntKeyword,
|
|
75
|
+
]);
|
|
76
|
+
const NULLISH_TYPE_KEYWORDS = new Set([
|
|
77
|
+
utils_1.AST_NODE_TYPES.TSNullKeyword,
|
|
78
|
+
utils_1.AST_NODE_TYPES.TSUndefinedKeyword,
|
|
79
|
+
]);
|
|
80
|
+
function isPrimitiveTypeNode(node) {
|
|
81
|
+
if (!node) {
|
|
82
|
+
return false;
|
|
83
|
+
}
|
|
84
|
+
if (PRIMITIVE_TYPE_KEYWORDS.has(node.type)) {
|
|
85
|
+
return true;
|
|
86
|
+
}
|
|
87
|
+
// Literal types (`true`, `'grid'`, `2`) are primitives too.
|
|
88
|
+
if (node.type === utils_1.AST_NODE_TYPES.TSLiteralType) {
|
|
89
|
+
return true;
|
|
90
|
+
}
|
|
91
|
+
// `boolean | undefined` stays a primitive; a union qualifies only when every
|
|
92
|
+
// member is primitive or nullish, and at least one is an actual primitive.
|
|
93
|
+
if (node.type === utils_1.AST_NODE_TYPES.TSUnionType) {
|
|
94
|
+
return (node.types.some((member) => isPrimitiveTypeNode(member)) &&
|
|
95
|
+
node.types.every((member) => isPrimitiveTypeNode(member) || NULLISH_TYPE_KEYWORDS.has(member.type)));
|
|
96
|
+
}
|
|
97
|
+
return false;
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Every expression form handled here evaluates to a primitive regardless of its
|
|
101
|
+
* operands: comparisons and arithmetic yield numbers/strings/booleans, and `!`,
|
|
102
|
+
* `typeof` and friends yield booleans/strings/numbers.
|
|
103
|
+
*/
|
|
104
|
+
function isPrimitiveExpression(node) {
|
|
105
|
+
if (!node) {
|
|
106
|
+
return false;
|
|
107
|
+
}
|
|
108
|
+
switch (node.type) {
|
|
109
|
+
case utils_1.AST_NODE_TYPES.Literal: {
|
|
110
|
+
const { value } = node;
|
|
111
|
+
return (typeof value === 'string' ||
|
|
112
|
+
typeof value === 'number' ||
|
|
113
|
+
typeof value === 'boolean' ||
|
|
114
|
+
typeof value === 'bigint');
|
|
115
|
+
}
|
|
116
|
+
case utils_1.AST_NODE_TYPES.TemplateLiteral:
|
|
117
|
+
return true;
|
|
118
|
+
case utils_1.AST_NODE_TYPES.UnaryExpression:
|
|
119
|
+
return ['!', '-', '+', '~', 'typeof', 'void'].includes(node.operator);
|
|
120
|
+
case utils_1.AST_NODE_TYPES.BinaryExpression:
|
|
121
|
+
return true;
|
|
122
|
+
case utils_1.AST_NODE_TYPES.TSAsExpression:
|
|
123
|
+
return isPrimitiveTypeNode(node.typeAnnotation);
|
|
124
|
+
default:
|
|
125
|
+
return false;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
function propertyKeyNameOf(key, computed) {
|
|
129
|
+
if (!computed && key.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
130
|
+
return key.name;
|
|
131
|
+
}
|
|
132
|
+
if (key.type === utils_1.AST_NODE_TYPES.Literal && typeof key.value === 'string') {
|
|
133
|
+
return key.value;
|
|
134
|
+
}
|
|
135
|
+
return undefined;
|
|
136
|
+
}
|
|
137
|
+
function memberTypeOf(members, keyName) {
|
|
138
|
+
for (const member of members) {
|
|
139
|
+
if (member.type !== utils_1.AST_NODE_TYPES.TSPropertySignature) {
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
if (propertyKeyNameOf(member.key, member.computed === true) === keyName) {
|
|
143
|
+
return member.typeAnnotation?.typeAnnotation;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
return undefined;
|
|
147
|
+
}
|
|
148
|
+
function findVariableInScopes(scope, name) {
|
|
149
|
+
let currentScope = scope;
|
|
150
|
+
while (currentScope) {
|
|
151
|
+
const variable = currentScope.variables.find((v) => v.name === name);
|
|
152
|
+
if (variable) {
|
|
153
|
+
return variable;
|
|
154
|
+
}
|
|
155
|
+
currentScope = currentScope.upper;
|
|
156
|
+
}
|
|
157
|
+
return undefined;
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Resolves the declared type of a property inside an object type, following at
|
|
161
|
+
* most a couple of hops through same-file type aliases and interfaces. Anything
|
|
162
|
+
* that needs cross-file resolution stays unresolved: the rule then falls back to
|
|
163
|
+
* reporting, which is the conservative direction for a suggestion-only rule.
|
|
164
|
+
*/
|
|
165
|
+
function propertyTypeOf(typeNode, keyName, scope, depth = 0) {
|
|
166
|
+
if (!typeNode || depth > 2) {
|
|
167
|
+
return undefined;
|
|
168
|
+
}
|
|
169
|
+
if (typeNode.type === utils_1.AST_NODE_TYPES.TSTypeLiteral) {
|
|
170
|
+
return memberTypeOf(typeNode.members, keyName);
|
|
171
|
+
}
|
|
172
|
+
if (typeNode.type === utils_1.AST_NODE_TYPES.TSIntersectionType) {
|
|
173
|
+
for (const member of typeNode.types) {
|
|
174
|
+
const resolved = propertyTypeOf(member, keyName, scope, depth + 1);
|
|
175
|
+
if (resolved) {
|
|
176
|
+
return resolved;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
return undefined;
|
|
180
|
+
}
|
|
181
|
+
if (typeNode.type === utils_1.AST_NODE_TYPES.TSTypeReference &&
|
|
182
|
+
typeNode.typeName.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
183
|
+
const variable = findVariableInScopes(scope, typeNode.typeName.name);
|
|
184
|
+
for (const def of variable?.defs ?? []) {
|
|
185
|
+
if (def.node.type === utils_1.AST_NODE_TYPES.TSTypeAliasDeclaration) {
|
|
186
|
+
return propertyTypeOf(def.node.typeAnnotation, keyName, scope, depth + 1);
|
|
187
|
+
}
|
|
188
|
+
if (def.node.type === utils_1.AST_NODE_TYPES.TSInterfaceDeclaration) {
|
|
189
|
+
return memberTypeOf(def.node.body.body, keyName);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
return undefined;
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* Finds the type annotation attached to a binding, including the annotation a
|
|
197
|
+
* destructured prop inherits from its object pattern (`({ isOpen }: Props)`).
|
|
198
|
+
*/
|
|
199
|
+
function declaredTypeOf(nameNode, scope) {
|
|
200
|
+
if (nameNode.typeAnnotation) {
|
|
201
|
+
return nameNode.typeAnnotation.typeAnnotation;
|
|
202
|
+
}
|
|
203
|
+
let binding = nameNode;
|
|
204
|
+
const defaulted = binding.parent;
|
|
205
|
+
if (defaulted?.type === utils_1.AST_NODE_TYPES.AssignmentPattern &&
|
|
206
|
+
defaulted.left === binding) {
|
|
207
|
+
if (defaulted.typeAnnotation) {
|
|
208
|
+
return defaulted.typeAnnotation.typeAnnotation;
|
|
209
|
+
}
|
|
210
|
+
binding = defaulted;
|
|
211
|
+
}
|
|
212
|
+
const property = binding.parent;
|
|
213
|
+
if (property?.type !== utils_1.AST_NODE_TYPES.Property ||
|
|
214
|
+
property.value !== binding ||
|
|
215
|
+
property.parent?.type !== utils_1.AST_NODE_TYPES.ObjectPattern) {
|
|
216
|
+
return undefined;
|
|
217
|
+
}
|
|
218
|
+
const keyName = propertyKeyNameOf(property.key, property.computed);
|
|
219
|
+
if (!keyName) {
|
|
220
|
+
return undefined;
|
|
221
|
+
}
|
|
222
|
+
return propertyTypeOf(property.parent.typeAnnotation?.typeAnnotation, keyName, scope);
|
|
223
|
+
}
|
|
224
|
+
function isPrimitiveStateHookCall(node) {
|
|
225
|
+
if (node.type !== utils_1.AST_NODE_TYPES.CallExpression) {
|
|
226
|
+
return false;
|
|
227
|
+
}
|
|
228
|
+
const { callee } = node;
|
|
229
|
+
const isStateHook = (callee.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
230
|
+
STATE_HOOK_NAMES.has(callee.name)) ||
|
|
231
|
+
(callee.type === utils_1.AST_NODE_TYPES.MemberExpression &&
|
|
232
|
+
!callee.computed &&
|
|
233
|
+
callee.property.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
234
|
+
STATE_HOOK_NAMES.has(callee.property.name));
|
|
235
|
+
if (!isStateHook) {
|
|
236
|
+
return false;
|
|
237
|
+
}
|
|
238
|
+
const explicitType = node.typeParameters?.params[0];
|
|
239
|
+
if (explicitType) {
|
|
240
|
+
return isPrimitiveTypeNode(explicitType);
|
|
241
|
+
}
|
|
242
|
+
return isPrimitiveExpression(node.arguments[0]);
|
|
243
|
+
}
|
|
244
|
+
function isPrimitiveInitializedVariable(nameNode, declarator) {
|
|
245
|
+
if (!declarator.init) {
|
|
246
|
+
return false;
|
|
247
|
+
}
|
|
248
|
+
if (declarator.id === nameNode) {
|
|
249
|
+
return isPrimitiveExpression(declarator.init);
|
|
250
|
+
}
|
|
251
|
+
// `const [isOpen, setIsOpen] = useState(false)` binds the state value first.
|
|
252
|
+
if (declarator.id.type === utils_1.AST_NODE_TYPES.ArrayPattern &&
|
|
253
|
+
declarator.id.elements[0] === nameNode) {
|
|
254
|
+
return isPrimitiveStateHookCall(declarator.init);
|
|
255
|
+
}
|
|
256
|
+
return false;
|
|
257
|
+
}
|
|
258
|
+
function isPrimitiveBinding(variable, scope) {
|
|
259
|
+
return variable.defs.some((def) => {
|
|
260
|
+
const nameNode = def.name;
|
|
261
|
+
if (nameNode.type !== utils_1.AST_NODE_TYPES.Identifier) {
|
|
262
|
+
return false;
|
|
263
|
+
}
|
|
264
|
+
if (isPrimitiveTypeNode(declaredTypeOf(nameNode, scope))) {
|
|
265
|
+
return true;
|
|
266
|
+
}
|
|
267
|
+
return (def.node.type === utils_1.AST_NODE_TYPES.VariableDeclarator &&
|
|
268
|
+
isPrimitiveInitializedVariable(nameNode, def.node));
|
|
269
|
+
});
|
|
270
|
+
}
|
|
7
271
|
function isHookCall(node) {
|
|
8
272
|
const callee = node.callee;
|
|
9
273
|
return ((callee.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
@@ -29,20 +293,20 @@ function generateBooleanVariableName(objectName, conditionType, isNegated) {
|
|
|
29
293
|
function capitalize(str) {
|
|
30
294
|
return str.charAt(0).toUpperCase() + str.slice(1);
|
|
31
295
|
}
|
|
32
|
-
function isObjectExistenceCheck(node) {
|
|
296
|
+
function isObjectExistenceCheck(node, isObjectIdentifier) {
|
|
33
297
|
// Check for patterns like !obj
|
|
34
298
|
if (node.type === utils_1.AST_NODE_TYPES.UnaryExpression) {
|
|
35
299
|
// !obj
|
|
36
300
|
if (node.operator === '!' &&
|
|
37
301
|
node.argument.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
38
|
-
return
|
|
302
|
+
return isObjectIdentifier(node.argument);
|
|
39
303
|
}
|
|
40
304
|
// !!obj
|
|
41
305
|
if (node.operator === '!' &&
|
|
42
306
|
node.argument.type === utils_1.AST_NODE_TYPES.UnaryExpression &&
|
|
43
307
|
node.argument.operator === '!' &&
|
|
44
308
|
node.argument.argument.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
45
|
-
return
|
|
309
|
+
return isObjectIdentifier(node.argument.argument);
|
|
46
310
|
}
|
|
47
311
|
}
|
|
48
312
|
return false;
|
|
@@ -75,21 +339,25 @@ function isObjectKeyCountCheck(node) {
|
|
|
75
339
|
}
|
|
76
340
|
return false;
|
|
77
341
|
}
|
|
78
|
-
function isComplexBooleanExpression(node) {
|
|
342
|
+
function isComplexBooleanExpression(node, isObjectIdentifier) {
|
|
79
343
|
// Check for complex expressions involving objects
|
|
80
344
|
if (node.type === utils_1.AST_NODE_TYPES.LogicalExpression) {
|
|
81
|
-
return (containsObjectCondition(node.left) ||
|
|
345
|
+
return (containsObjectCondition(node.left, isObjectIdentifier) ||
|
|
346
|
+
containsObjectCondition(node.right, isObjectIdentifier));
|
|
82
347
|
}
|
|
83
348
|
return false;
|
|
84
349
|
}
|
|
85
|
-
function containsObjectCondition(node) {
|
|
86
|
-
return (isObjectExistenceCheck(node) ||
|
|
350
|
+
function containsObjectCondition(node, isObjectIdentifier) {
|
|
351
|
+
return (isObjectExistenceCheck(node, isObjectIdentifier) ||
|
|
87
352
|
isObjectKeyCountCheck(node) ||
|
|
88
|
-
isComplexBooleanExpression(node));
|
|
353
|
+
isComplexBooleanExpression(node, isObjectIdentifier));
|
|
89
354
|
}
|
|
90
|
-
function extractObjectName(node) {
|
|
355
|
+
function extractObjectName(node, isObjectIdentifier) {
|
|
91
356
|
if (node.type === utils_1.AST_NODE_TYPES.UnaryExpression) {
|
|
92
|
-
|
|
357
|
+
// Mirror the existence matcher so a primitive negation inside a larger
|
|
358
|
+
// expression never becomes the reported "object".
|
|
359
|
+
if (node.argument.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
360
|
+
isObjectIdentifier(node.argument)) {
|
|
93
361
|
return node.argument.name;
|
|
94
362
|
}
|
|
95
363
|
}
|
|
@@ -105,18 +373,18 @@ function extractObjectName(node) {
|
|
|
105
373
|
}
|
|
106
374
|
else if (node.type === utils_1.AST_NODE_TYPES.LogicalExpression) {
|
|
107
375
|
// For complex expressions, try to extract from the first object condition found
|
|
108
|
-
const leftObj = extractObjectName(node.left);
|
|
376
|
+
const leftObj = extractObjectName(node.left, isObjectIdentifier);
|
|
109
377
|
if (leftObj)
|
|
110
378
|
return leftObj;
|
|
111
|
-
return extractObjectName(node.right);
|
|
379
|
+
return extractObjectName(node.right, isObjectIdentifier);
|
|
112
380
|
}
|
|
113
381
|
return null;
|
|
114
382
|
}
|
|
115
|
-
function analyzeBooleanCondition(node, context) {
|
|
383
|
+
function analyzeBooleanCondition(node, context, isObjectIdentifier) {
|
|
116
384
|
const sourceCode = context.sourceCode;
|
|
117
385
|
const expression = sourceCode.getText(node);
|
|
118
|
-
if (isObjectExistenceCheck(node)) {
|
|
119
|
-
const objectName = extractObjectName(node);
|
|
386
|
+
if (isObjectExistenceCheck(node, isObjectIdentifier)) {
|
|
387
|
+
const objectName = extractObjectName(node, isObjectIdentifier);
|
|
120
388
|
if (objectName) {
|
|
121
389
|
const isNegated = node.type === utils_1.AST_NODE_TYPES.UnaryExpression &&
|
|
122
390
|
node.operator === '!' &&
|
|
@@ -132,7 +400,7 @@ function analyzeBooleanCondition(node, context) {
|
|
|
132
400
|
}
|
|
133
401
|
}
|
|
134
402
|
else if (isObjectKeyCountCheck(node)) {
|
|
135
|
-
const objectName = extractObjectName(node);
|
|
403
|
+
const objectName = extractObjectName(node, isObjectIdentifier);
|
|
136
404
|
if (objectName) {
|
|
137
405
|
const isNegated = expression.includes('=== 0') || expression.includes('<= 0');
|
|
138
406
|
return {
|
|
@@ -144,8 +412,8 @@ function analyzeBooleanCondition(node, context) {
|
|
|
144
412
|
};
|
|
145
413
|
}
|
|
146
414
|
}
|
|
147
|
-
else if (isComplexBooleanExpression(node)) {
|
|
148
|
-
const objectName = extractObjectName(node);
|
|
415
|
+
else if (isComplexBooleanExpression(node, isObjectIdentifier)) {
|
|
416
|
+
const objectName = extractObjectName(node, isObjectIdentifier);
|
|
149
417
|
if (objectName) {
|
|
150
418
|
return {
|
|
151
419
|
type: 'complex',
|
|
@@ -158,12 +426,12 @@ function analyzeBooleanCondition(node, context) {
|
|
|
158
426
|
}
|
|
159
427
|
return null;
|
|
160
428
|
}
|
|
161
|
-
function findBooleanConditionsInDependencies(depsArray, context) {
|
|
429
|
+
function findBooleanConditionsInDependencies(depsArray, context, isObjectIdentifier) {
|
|
162
430
|
const patterns = [];
|
|
163
431
|
for (const element of depsArray.elements) {
|
|
164
432
|
if (!element)
|
|
165
433
|
continue; // Skip holes in array
|
|
166
|
-
const pattern = analyzeBooleanCondition(element, context);
|
|
434
|
+
const pattern = analyzeBooleanCondition(element, context, isObjectIdentifier);
|
|
167
435
|
if (pattern) {
|
|
168
436
|
patterns.push(pattern);
|
|
169
437
|
}
|
|
@@ -172,20 +440,7 @@ function findBooleanConditionsInDependencies(depsArray, context) {
|
|
|
172
440
|
}
|
|
173
441
|
function isAlreadyBooleanVariable(node) {
|
|
174
442
|
// Check if the dependency is already a boolean variable (starts with is, has, can, etc.)
|
|
175
|
-
|
|
176
|
-
const name = node.name;
|
|
177
|
-
const booleanPrefixes = [
|
|
178
|
-
'is',
|
|
179
|
-
'has',
|
|
180
|
-
'can',
|
|
181
|
-
'should',
|
|
182
|
-
'will',
|
|
183
|
-
'was',
|
|
184
|
-
'were',
|
|
185
|
-
];
|
|
186
|
-
return booleanPrefixes.some((prefix) => name.toLowerCase().startsWith(prefix.toLowerCase()));
|
|
187
|
-
}
|
|
188
|
-
return false;
|
|
443
|
+
return (node.type === utils_1.AST_NODE_TYPES.Identifier && hasBooleanNamePrefix(node.name));
|
|
189
444
|
}
|
|
190
445
|
exports.optimizeObjectBooleanConditions = (0, createRule_1.createRule)({
|
|
191
446
|
name: 'optimize-object-boolean-conditions',
|
|
@@ -212,8 +467,28 @@ exports.optimizeObjectBooleanConditions = (0, createRule_1.createRule)({
|
|
|
212
467
|
if (!depsArg || depsArg.type !== utils_1.AST_NODE_TYPES.ArrayExpression) {
|
|
213
468
|
return;
|
|
214
469
|
}
|
|
470
|
+
const scope = context.getScope();
|
|
471
|
+
const primitiveByName = new Map();
|
|
472
|
+
// A negated identifier only carries the cost this rule targets when it
|
|
473
|
+
// can actually hold an object. Approved boolean prefixes (which
|
|
474
|
+
// enforce-boolean-naming-prefixes mandates) and same-file primitive
|
|
475
|
+
// evidence both rule that out; unresolved identifiers keep reporting.
|
|
476
|
+
const isObjectIdentifier = (identifier) => {
|
|
477
|
+
const { name } = identifier;
|
|
478
|
+
if (hasBooleanNamePrefix(name)) {
|
|
479
|
+
return false;
|
|
480
|
+
}
|
|
481
|
+
const cached = primitiveByName.get(name);
|
|
482
|
+
if (cached !== undefined) {
|
|
483
|
+
return !cached;
|
|
484
|
+
}
|
|
485
|
+
const variable = findVariableInScopes(scope, name);
|
|
486
|
+
const isPrimitive = !!variable && isPrimitiveBinding(variable, scope);
|
|
487
|
+
primitiveByName.set(name, isPrimitive);
|
|
488
|
+
return !isPrimitive;
|
|
489
|
+
};
|
|
215
490
|
// Find boolean conditions in dependencies
|
|
216
|
-
const patterns = findBooleanConditionsInDependencies(depsArg, context);
|
|
491
|
+
const patterns = findBooleanConditionsInDependencies(depsArg, context, isObjectIdentifier);
|
|
217
492
|
for (const pattern of patterns) {
|
|
218
493
|
// Skip if it's already a boolean variable
|
|
219
494
|
if (isAlreadyBooleanVariable(pattern.node)) {
|
|
@@ -199,7 +199,7 @@ exports.preferGetterOverParameterlessMethod = (0, createRule_1.createRule)({
|
|
|
199
199
|
],
|
|
200
200
|
messages: {
|
|
201
201
|
preferGetter: 'Problem: Method "{{name}}" is a parameterless synchronous method that returns a value. → Impact: Calling it with parentheses disguises that it behaves like a computed property, increasing the chance callers omit parentheses or treat it like an action. → Solution: Convert it to a getter: "get {{suggestedName}}()" to signal property semantics and remove call-site parentheses.',
|
|
202
|
-
preferGetterSideEffect: 'Problem: Method "{{name}}" looks like a computed value. → Impact: However, {{reason}}, so converting it to a getter would execute side effects on every property access and violate the principle of least surprise. → Solution:
|
|
202
|
+
preferGetterSideEffect: 'Problem: Method "{{name}}" looks like a computed value. → Impact: However, {{reason}}, so converting it to a getter would execute side effects on every property access and violate the principle of least surprise. → Solution: If the side effects are intentional, declare them with an "@sideEffect" (or "@mutates") tag inside a /** */ JSDoc block placed immediately above "{{name}}" with no blank line between: the tag warns callers that reading this value performs work, and the rule honors it (option "respectJsDocSideEffects", enabled by default) and stops reporting the method. If the side effects are not intentional, remove them and convert the method to "get {{suggestedName}}()".',
|
|
203
203
|
},
|
|
204
204
|
},
|
|
205
205
|
defaultOptions: [DEFAULT_OPTIONS],
|