@blumintinc/eslint-plugin-blumint 1.20.56 → 1.20.57
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/no-entire-object-hook-deps.js +110 -0
- package/package.json +1 -1
- package/release-manifest.json +14 -0
package/lib/index.js
CHANGED
|
@@ -20,6 +20,58 @@ function isEffectHookCall(node) {
|
|
|
20
20
|
return (callee.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
21
21
|
EFFECT_HOOK_NAMES.has(callee.name));
|
|
22
22
|
}
|
|
23
|
+
/**
|
|
24
|
+
* The rule whose suppression marks a dependency array as hand-maintained.
|
|
25
|
+
*/
|
|
26
|
+
const EXHAUSTIVE_DEPS_RULE = 'react-hooks/exhaustive-deps';
|
|
27
|
+
/**
|
|
28
|
+
* Matches the keyword of an `eslint-disable`, `eslint-disable-next-line` or
|
|
29
|
+
* `eslint-disable-line` directive, leaving the rule list as the remainder. The
|
|
30
|
+
* lookahead keeps `eslint-disabled-something` (and prose that merely opens with
|
|
31
|
+
* the same letters) from parsing as a directive.
|
|
32
|
+
*/
|
|
33
|
+
const DISABLE_DIRECTIVE = /^\s*eslint-disable(-next-line|-line)?(?![\w-])/u;
|
|
34
|
+
/**
|
|
35
|
+
* ESLint splits a directive's rule list from its ` -- justification` suffix on
|
|
36
|
+
* this separator, so the rule list must be read the same way.
|
|
37
|
+
*/
|
|
38
|
+
const JUSTIFICATION_SEPARATOR = /\s-{2,}\s/u;
|
|
39
|
+
/**
|
|
40
|
+
* The line an exhaustive-deps disable comment covers, `'file'` for the
|
|
41
|
+
* whole-file form, or null when the comment is not such a directive.
|
|
42
|
+
*
|
|
43
|
+
* why: `eslint-disable-next-line` covers the line after the comment ends, while
|
|
44
|
+
* `eslint-disable-line` covers the comment's own line. Only the block form of
|
|
45
|
+
* the bare `eslint-disable` is a file-level directive to ESLint, so a line
|
|
46
|
+
* comment starting with it is not treated as one here either. The file form
|
|
47
|
+
* counts for the whole file rather than from its own position onward: erring
|
|
48
|
+
* toward exempting a hook keeps a hand-managed array intact, which is the safe
|
|
49
|
+
* direction for a deleting fixer.
|
|
50
|
+
*/
|
|
51
|
+
function readExhaustiveDepsDisable(comment) {
|
|
52
|
+
const [directive] = comment.value.split(JUSTIFICATION_SEPARATOR);
|
|
53
|
+
const match = DISABLE_DIRECTIVE.exec(directive);
|
|
54
|
+
if (!match) {
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
// why: a bare directive with no rule list says nothing about dependency
|
|
58
|
+
// management, so only an explicit mention of exhaustive-deps counts.
|
|
59
|
+
const namesExhaustiveDeps = directive
|
|
60
|
+
.slice(match[0].length)
|
|
61
|
+
.split(',')
|
|
62
|
+
.some((ruleId) => ruleId.trim() === EXHAUSTIVE_DEPS_RULE);
|
|
63
|
+
if (!namesExhaustiveDeps) {
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
const scope = match[1];
|
|
67
|
+
if (scope === '-next-line') {
|
|
68
|
+
return comment.loc.end.line + 1;
|
|
69
|
+
}
|
|
70
|
+
if (scope === '-line') {
|
|
71
|
+
return comment.loc.start.line;
|
|
72
|
+
}
|
|
73
|
+
return comment.type === utils_1.AST_TOKEN_TYPES.Block ? 'file' : null;
|
|
74
|
+
}
|
|
23
75
|
/** `channelGroupActive` -> `setChannelGroupActive`, `a` -> `setA`. */
|
|
24
76
|
function toSetterName(dependencyName) {
|
|
25
77
|
return `set${dependencyName.charAt(0).toUpperCase()}${dependencyName.slice(1)}`;
|
|
@@ -609,6 +661,53 @@ exports.noEntireObjectHookDeps = (0, createRule_1.createRule)({
|
|
|
609
661
|
// In a real environment, we would want to enforce this
|
|
610
662
|
// throw new Error('You have to enable the `project` setting in parser options to use this rule');
|
|
611
663
|
}
|
|
664
|
+
const sourceCode = context.getSourceCode();
|
|
665
|
+
// why: scanning every comment once per file rather than once per hook call
|
|
666
|
+
// keeps the check off the hot path of files with many hooks.
|
|
667
|
+
let manuallyManagedLines = null;
|
|
668
|
+
let disabledForWholeFile = false;
|
|
669
|
+
function collectDisableDirectives() {
|
|
670
|
+
if (manuallyManagedLines) {
|
|
671
|
+
return manuallyManagedLines;
|
|
672
|
+
}
|
|
673
|
+
const lines = new Set();
|
|
674
|
+
for (const comment of sourceCode.getAllComments()) {
|
|
675
|
+
const scope = readExhaustiveDepsDisable(comment);
|
|
676
|
+
if (scope === 'file') {
|
|
677
|
+
disabledForWholeFile = true;
|
|
678
|
+
}
|
|
679
|
+
else if (scope !== null) {
|
|
680
|
+
lines.add(scope);
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
manuallyManagedLines = lines;
|
|
684
|
+
return lines;
|
|
685
|
+
}
|
|
686
|
+
/**
|
|
687
|
+
* Whether the author has taken manual control of this hook's dependency
|
|
688
|
+
* array by suppressing `react-hooks/exhaustive-deps` for it.
|
|
689
|
+
*
|
|
690
|
+
* why: exhaustive-deps is the rule that would otherwise force every read
|
|
691
|
+
* value into the array, so disabling it declares the array hand-maintained.
|
|
692
|
+
* Entries in such an array are load-bearing by construction — an unread one
|
|
693
|
+
* is a deliberate recompute trigger (a hydration flag, a change-detecting
|
|
694
|
+
* hash) whose deletion silently returns a stale value. The comment can sit
|
|
695
|
+
* above the hook call, above the dependency array, or above the closing
|
|
696
|
+
* `}, [...])` line, so any directive landing anywhere within the call
|
|
697
|
+
* counts.
|
|
698
|
+
*/
|
|
699
|
+
function hasManuallyManagedDeps(node) {
|
|
700
|
+
const lines = collectDisableDirectives();
|
|
701
|
+
if (disabledForWholeFile) {
|
|
702
|
+
return true;
|
|
703
|
+
}
|
|
704
|
+
for (let line = node.loc.start.line; line <= node.loc.end.line; line += 1) {
|
|
705
|
+
if (lines.has(line)) {
|
|
706
|
+
return true;
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
return false;
|
|
710
|
+
}
|
|
612
711
|
return {
|
|
613
712
|
CallExpression(node) {
|
|
614
713
|
if (!isHookCall(node)) {
|
|
@@ -628,6 +727,7 @@ exports.noEntireObjectHookDeps = (0, createRule_1.createRule)({
|
|
|
628
727
|
}
|
|
629
728
|
const callbackBody = callbackArg.body;
|
|
630
729
|
const isEffect = isEffectHookCall(node);
|
|
730
|
+
const manuallyManagedDeps = hasManuallyManagedDeps(node);
|
|
631
731
|
// Check each dependency in the array
|
|
632
732
|
depsArg.elements.forEach((element) => {
|
|
633
733
|
const unwrappedElement = element ? unwrapExpression(element) : null;
|
|
@@ -648,6 +748,16 @@ exports.noEntireObjectHookDeps = (0, createRule_1.createRule)({
|
|
|
648
748
|
const result = getObjectUsagesInHook(callbackBody, objectName);
|
|
649
749
|
// If the object is not used at all, suggest removing it
|
|
650
750
|
if (result.notUsed) {
|
|
751
|
+
// why: deleting an entry from an array the author maintains by
|
|
752
|
+
// hand is presumptuous — the suppression is the declaration that
|
|
753
|
+
// the entries were chosen deliberately, and an unread one is a
|
|
754
|
+
// recompute trigger whose removal yields a stale value. Narrowing
|
|
755
|
+
// an entire object (avoidEntireObject) is a different transform
|
|
756
|
+
// and stays enabled: it preserves the dependency, it does not
|
|
757
|
+
// drop it.
|
|
758
|
+
if (manuallyManagedDeps) {
|
|
759
|
+
return;
|
|
760
|
+
}
|
|
651
761
|
// why: an effect reruns for its side effects, so a dependency the
|
|
652
762
|
// body never reads is normally a deliberate re-run trigger
|
|
653
763
|
// (React's reset-on-scope-change idiom) — deleting it silently
|
package/package.json
CHANGED
package/release-manifest.json
CHANGED
|
@@ -1,4 +1,18 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"version": "1.20.57",
|
|
4
|
+
"date": "2026-08-01T03:42:42.343Z",
|
|
5
|
+
"rules": [
|
|
6
|
+
{
|
|
7
|
+
"name": "no-entire-object-hook-deps",
|
|
8
|
+
"changeType": "fix",
|
|
9
|
+
"issues": [
|
|
10
|
+
1547
|
|
11
|
+
],
|
|
12
|
+
"summary": "never prune deps from a hand-maintained dependency array (closes #1547)"
|
|
13
|
+
}
|
|
14
|
+
]
|
|
15
|
+
},
|
|
2
16
|
{
|
|
3
17
|
"version": "1.20.56",
|
|
4
18
|
"date": "2026-08-01T03:08:15.343Z",
|