@blumintinc/eslint-plugin-blumint 1.20.55 → 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
@@ -223,7 +223,7 @@ function noFrontendImportsFromFunctionsPatterns(pattern) {
223
223
  module.exports = {
224
224
  meta: {
225
225
  name: '@blumintinc/eslint-plugin-blumint',
226
- version: '1.20.55',
226
+ version: '1.20.56',
227
227
  },
228
228
  parseOptions: {
229
229
  ecmaVersion: 2020,
@@ -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(callbackArg.body, objectName);
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blumintinc/eslint-plugin-blumint",
3
- "version": "1.20.55",
3
+ "version": "1.20.56",
4
4
  "description": "Custom eslint rules for use within BluMint",
5
5
  "author": {
6
6
  "name": "Brodie McGuire",
@@ -1,4 +1,18 @@
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
+ },
2
16
  {
3
17
  "version": "1.20.55",
4
18
  "date": "2026-08-01T02:47:13.295Z",