@blumintinc/eslint-plugin-blumint 1.20.54 → 1.20.56
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
CHANGED
|
@@ -1219,40 +1219,12 @@ exports.enforceBooleanNamingPrefixes = (0, createRule_1.createRule)({
|
|
|
1219
1219
|
}
|
|
1220
1220
|
}
|
|
1221
1221
|
/**
|
|
1222
|
-
* Check class property
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
// Check if it's a boolean property
|
|
1229
|
-
let isBooleanProperty = false;
|
|
1230
|
-
// Check if it has a boolean type annotation
|
|
1231
|
-
if (node.typeAnnotation?.typeAnnotation &&
|
|
1232
|
-
node.typeAnnotation.typeAnnotation.type ===
|
|
1233
|
-
utils_1.AST_NODE_TYPES.TSBooleanKeyword) {
|
|
1234
|
-
isBooleanProperty = true;
|
|
1235
|
-
}
|
|
1236
|
-
// Check if it's initialized with a boolean value
|
|
1237
|
-
if (node.value?.type === utils_1.AST_NODE_TYPES.Literal &&
|
|
1238
|
-
typeof node.value.value === 'boolean') {
|
|
1239
|
-
isBooleanProperty = true;
|
|
1240
|
-
}
|
|
1241
|
-
if (isBooleanProperty && !hasApprovedPrefix(propertyName)) {
|
|
1242
|
-
context.report({
|
|
1243
|
-
node: node.key,
|
|
1244
|
-
messageId: 'missingBooleanPrefix',
|
|
1245
|
-
data: {
|
|
1246
|
-
type: 'property',
|
|
1247
|
-
name: propertyName,
|
|
1248
|
-
capitalizedName: capitalizeFirst(propertyName),
|
|
1249
|
-
prefixes: formatPrefixes(),
|
|
1250
|
-
},
|
|
1251
|
-
});
|
|
1252
|
-
}
|
|
1253
|
-
}
|
|
1254
|
-
/**
|
|
1255
|
-
* Check class property declarations for boolean values
|
|
1222
|
+
* Check class property declarations for boolean values.
|
|
1223
|
+
*
|
|
1224
|
+
* Handles both concrete (`PropertyDefinition`) and abstract
|
|
1225
|
+
* (`TSAbstractPropertyDefinition`) fields: an abstract field is a
|
|
1226
|
+
* declaration the author owns and every implementer inherits the name from,
|
|
1227
|
+
* so it carries the same naming obligation as a concrete one.
|
|
1256
1228
|
*/
|
|
1257
1229
|
function checkClassPropertyDeclaration(node) {
|
|
1258
1230
|
if (node.key.type !== utils_1.AST_NODE_TYPES.Identifier)
|
|
@@ -1383,6 +1355,43 @@ exports.enforceBooleanNamingPrefixes = (0, createRule_1.createRule)({
|
|
|
1383
1355
|
}
|
|
1384
1356
|
}
|
|
1385
1357
|
}
|
|
1358
|
+
/**
|
|
1359
|
+
* Check constructor parameter properties (`constructor(private enabled: boolean) {}`)
|
|
1360
|
+
* for boolean values.
|
|
1361
|
+
*
|
|
1362
|
+
* An access modifier turns a constructor parameter into a declared class
|
|
1363
|
+
* field, so it is reported as a `property` rather than a `parameter`. The
|
|
1364
|
+
* generic parameter visitor never sees these names — `TSParameterProperty`
|
|
1365
|
+
* wraps the identifier — so this is the single report site for them.
|
|
1366
|
+
*/
|
|
1367
|
+
function checkParameterProperty(node) {
|
|
1368
|
+
// A default value wraps the identifier again: `private enabled = true`
|
|
1369
|
+
// parses as an AssignmentPattern whose left holds the name and annotation.
|
|
1370
|
+
const { parameter } = node;
|
|
1371
|
+
const isDefaulted = parameter.type === utils_1.AST_NODE_TYPES.AssignmentPattern;
|
|
1372
|
+
const target = isDefaulted ? parameter.left : parameter;
|
|
1373
|
+
if (target.type !== utils_1.AST_NODE_TYPES.Identifier)
|
|
1374
|
+
return;
|
|
1375
|
+
const propertyName = target.name;
|
|
1376
|
+
const hasBooleanAnnotation = target.typeAnnotation?.typeAnnotation.type ===
|
|
1377
|
+
utils_1.AST_NODE_TYPES.TSBooleanKeyword;
|
|
1378
|
+
const initializer = isDefaulted ? parameter.right : undefined;
|
|
1379
|
+
const hasBooleanInitializer = initializer?.type === utils_1.AST_NODE_TYPES.Literal &&
|
|
1380
|
+
typeof initializer.value === 'boolean';
|
|
1381
|
+
if ((hasBooleanAnnotation || hasBooleanInitializer) &&
|
|
1382
|
+
!hasApprovedPrefix(propertyName)) {
|
|
1383
|
+
context.report({
|
|
1384
|
+
node: target,
|
|
1385
|
+
messageId: 'missingBooleanPrefix',
|
|
1386
|
+
data: {
|
|
1387
|
+
type: 'property',
|
|
1388
|
+
name: propertyName,
|
|
1389
|
+
capitalizedName: capitalizeFirst(propertyName),
|
|
1390
|
+
prefixes: formatPrefixes(),
|
|
1391
|
+
},
|
|
1392
|
+
});
|
|
1393
|
+
}
|
|
1394
|
+
}
|
|
1386
1395
|
return {
|
|
1387
1396
|
VariableDeclarator: checkVariableDeclaration,
|
|
1388
1397
|
FunctionDeclaration: checkFunctionDeclaration,
|
|
@@ -1398,8 +1407,9 @@ exports.enforceBooleanNamingPrefixes = (0, createRule_1.createRule)({
|
|
|
1398
1407
|
},
|
|
1399
1408
|
MethodDefinition: checkMethodDefinition,
|
|
1400
1409
|
TSAbstractMethodDefinition: checkMethodDefinition,
|
|
1401
|
-
ClassProperty: checkClassProperty,
|
|
1402
1410
|
PropertyDefinition: checkClassPropertyDeclaration,
|
|
1411
|
+
TSAbstractPropertyDefinition: checkClassPropertyDeclaration,
|
|
1412
|
+
TSParameterProperty: checkParameterProperty,
|
|
1403
1413
|
TSPropertySignature: checkPropertySignature,
|
|
1404
1414
|
Identifier(node) {
|
|
1405
1415
|
// Check parameter names in function declarations
|
|
@@ -5,10 +5,25 @@ const utils_1 = require("@typescript-eslint/utils");
|
|
|
5
5
|
const createRule_1 = require("../utils/createRule");
|
|
6
6
|
const typescript_1 = require("typescript");
|
|
7
7
|
const HOOK_NAMES = new Set(['useEffect', 'useCallback', 'useMemo']);
|
|
8
|
+
/**
|
|
9
|
+
* Hooks that run for their side effects rather than producing a value. An
|
|
10
|
+
* unread dependency means something different here than in useMemo/useCallback
|
|
11
|
+
* — see `callsCorrespondingSetter`.
|
|
12
|
+
*/
|
|
13
|
+
const EFFECT_HOOK_NAMES = new Set(['useEffect']);
|
|
8
14
|
function isHookCall(node) {
|
|
9
15
|
const callee = node.callee;
|
|
10
16
|
return (callee.type === utils_1.AST_NODE_TYPES.Identifier && HOOK_NAMES.has(callee.name));
|
|
11
17
|
}
|
|
18
|
+
function isEffectHookCall(node) {
|
|
19
|
+
const callee = node.callee;
|
|
20
|
+
return (callee.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
21
|
+
EFFECT_HOOK_NAMES.has(callee.name));
|
|
22
|
+
}
|
|
23
|
+
/** `channelGroupActive` -> `setChannelGroupActive`, `a` -> `setA`. */
|
|
24
|
+
function toSetterName(dependencyName) {
|
|
25
|
+
return `set${dependencyName.charAt(0).toUpperCase()}${dependencyName.slice(1)}`;
|
|
26
|
+
}
|
|
12
27
|
function isArrayOrPrimitive(checker, esTreeNode, nodeMap) {
|
|
13
28
|
try {
|
|
14
29
|
const tsNode = nodeMap.get(esTreeNode);
|
|
@@ -80,6 +95,55 @@ function unwrapExpression(expr) {
|
|
|
80
95
|
}
|
|
81
96
|
return current;
|
|
82
97
|
}
|
|
98
|
+
/**
|
|
99
|
+
* Whether the hook body anywhere calls the state setter that corresponds to
|
|
100
|
+
* `dependencyName` (dep `count` -> `setCount(...)`).
|
|
101
|
+
*
|
|
102
|
+
* why: for an effect, an unread dependency is React's reset-on-scope-change
|
|
103
|
+
* idiom — a deliberate re-run trigger. The one shape where an unread dependency
|
|
104
|
+
* is genuinely wrong is the circular dependency, where the effect writes the
|
|
105
|
+
* very value it depends on and so re-triggers itself. The setter call is that
|
|
106
|
+
* signature. It can sit arbitrarily deep (inside an inner async function, a
|
|
107
|
+
* `startTransition` callback, a `.then()`), so the whole body is searched.
|
|
108
|
+
*/
|
|
109
|
+
function callsCorrespondingSetter(hookBody, dependencyName) {
|
|
110
|
+
const setterName = toSetterName(dependencyName);
|
|
111
|
+
const visited = new Set();
|
|
112
|
+
function visit(node) {
|
|
113
|
+
if (!node || visited.has(node))
|
|
114
|
+
return false;
|
|
115
|
+
visited.add(node);
|
|
116
|
+
if (node.type === utils_1.AST_NODE_TYPES.CallExpression) {
|
|
117
|
+
const callee = unwrapExpression(node.callee);
|
|
118
|
+
if (callee.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
119
|
+
callee.name === setterName) {
|
|
120
|
+
return true;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
for (const key in node) {
|
|
124
|
+
if (key === 'parent')
|
|
125
|
+
continue; // Skip parent references to avoid cycles
|
|
126
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
127
|
+
const child = node[key];
|
|
128
|
+
if (!child || typeof child !== 'object')
|
|
129
|
+
continue;
|
|
130
|
+
if (Array.isArray(child)) {
|
|
131
|
+
for (const item of child) {
|
|
132
|
+
if (item && typeof item === 'object' && 'type' in item) {
|
|
133
|
+
if (visit(item))
|
|
134
|
+
return true;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
else if ('type' in child) {
|
|
139
|
+
if (visit(child))
|
|
140
|
+
return true;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
return false;
|
|
144
|
+
}
|
|
145
|
+
return visit(hookBody);
|
|
146
|
+
}
|
|
83
147
|
function getObjectUsagesInHook(hookBody, objectName) {
|
|
84
148
|
const usages = new Map(); // Track usage and its position
|
|
85
149
|
// why: derived dependency paths (first-optional intermediate, array base)
|
|
@@ -562,6 +626,8 @@ exports.noEntireObjectHookDeps = (0, createRule_1.createRule)({
|
|
|
562
626
|
callbackArg.type !== utils_1.AST_NODE_TYPES.FunctionExpression)) {
|
|
563
627
|
return;
|
|
564
628
|
}
|
|
629
|
+
const callbackBody = callbackArg.body;
|
|
630
|
+
const isEffect = isEffectHookCall(node);
|
|
565
631
|
// Check each dependency in the array
|
|
566
632
|
depsArg.elements.forEach((element) => {
|
|
567
633
|
const unwrappedElement = element ? unwrapExpression(element) : null;
|
|
@@ -579,9 +645,20 @@ exports.noEntireObjectHookDeps = (0, createRule_1.createRule)({
|
|
|
579
645
|
}
|
|
580
646
|
}
|
|
581
647
|
// For testing without TypeScript services, we'll assume all identifiers are objects
|
|
582
|
-
const result = getObjectUsagesInHook(
|
|
648
|
+
const result = getObjectUsagesInHook(callbackBody, objectName);
|
|
583
649
|
// If the object is not used at all, suggest removing it
|
|
584
650
|
if (result.notUsed) {
|
|
651
|
+
// why: an effect reruns for its side effects, so a dependency the
|
|
652
|
+
// body never reads is normally a deliberate re-run trigger
|
|
653
|
+
// (React's reset-on-scope-change idiom) — deleting it silently
|
|
654
|
+
// stops the effect from rerunning. Only when the body also writes
|
|
655
|
+
// that value (setX for dep x) is the dependency a circular one
|
|
656
|
+
// worth removing. Value-producing hooks (useMemo/useCallback)
|
|
657
|
+
// gain nothing from an unread dependency, so they still report.
|
|
658
|
+
if (isEffect &&
|
|
659
|
+
!callsCorrespondingSetter(callbackBody, objectName)) {
|
|
660
|
+
return;
|
|
661
|
+
}
|
|
585
662
|
context.report({
|
|
586
663
|
node: element,
|
|
587
664
|
messageId: 'removeUnusedDependency',
|
package/package.json
CHANGED
package/release-manifest.json
CHANGED
|
@@ -1,4 +1,32 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"version": "1.20.56",
|
|
4
|
+
"date": "2026-08-01T03:08:15.343Z",
|
|
5
|
+
"rules": [
|
|
6
|
+
{
|
|
7
|
+
"name": "no-entire-object-hook-deps",
|
|
8
|
+
"changeType": "fix",
|
|
9
|
+
"issues": [
|
|
10
|
+
1546
|
|
11
|
+
],
|
|
12
|
+
"summary": "treat unread useEffect deps as re-run triggers unless the effect sets them (closes #1546)"
|
|
13
|
+
}
|
|
14
|
+
]
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
"version": "1.20.55",
|
|
18
|
+
"date": "2026-08-01T02:47:13.295Z",
|
|
19
|
+
"rules": [
|
|
20
|
+
{
|
|
21
|
+
"name": "enforce-boolean-naming-prefixes",
|
|
22
|
+
"changeType": "fix",
|
|
23
|
+
"issues": [
|
|
24
|
+
1545
|
|
25
|
+
],
|
|
26
|
+
"summary": "check abstract properties and constructor parameter properties (closes #1545)"
|
|
27
|
+
}
|
|
28
|
+
]
|
|
29
|
+
},
|
|
2
30
|
{
|
|
3
31
|
"version": "1.20.54",
|
|
4
32
|
"date": "2026-08-01T01:37:34.774Z",
|