Skip to content

Commit 1488b9e

Browse files
refactor(oxlint-plugin): consolidate duplicate DATA_SINK_METHOD_NAMES (was ITERATOR_METHOD_NAMES)
Two rules — `no-pass-data-to-parent` and `no-pass-live-state-to-parent` — each carried a 100+ element set named `ITERATOR_METHOD_NAMES` to filter out method-call shapes that 'consume' data rather than handing it back to a parent (Array iterators, EventEmitter subscriptions, Promise then/catch/finally, Set/Map ops, logger/telemetry, imperative action verbs). The two sets were byte-identical. The previous name `ITERATOR_METHOD_NAMES` was misleading — iterators were only one of the seven categories. Promote to a new shared `constants/data-sink-method-names.ts` module under the more accurate name `DATA_SINK_METHOD_NAMES`. Adding a new EventEmitter variant / imperative-action verb now propagates to both detectors automatically instead of drifting in one and not the other. The third local `ITERATOR_METHOD_NAMES` in `js-batch-dom-css.ts` is unaffected — it's a tiny subset (just the 6 Array.prototype iterators) used for a structurally different purpose (DOM-style batch detection), so consolidating would over-broaden it. Left in place. Net: 1 new 110-line module, ~210 lines removed from the two rule files. Behaviour-neutral. Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
1 parent a260da6 commit 1488b9e

3 files changed

Lines changed: 122 additions & 214 deletions

File tree

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
/**
2+
* Method names that conventionally "consume" or "sink" the value
3+
* passed to them rather than handing it BACK to a parent — used by
4+
* `no-pass-data-to-parent` and `no-pass-live-state-to-parent` to
5+
* filter out call shapes that aren't actually the data-handoff
6+
* anti-pattern they detect.
7+
*
8+
* Was duplicated verbatim in both rule files; promoted to this
9+
* shared module so adding a new method name (a new EventEmitter
10+
* variant, a new imperative-action verb) propagates to both
11+
* detectors without drift.
12+
*
13+
* The previous name `ITERATOR_METHOD_NAMES` was misleading —
14+
* iterators are only one of the seven categories covered.
15+
*/
16+
export const DATA_SINK_METHOD_NAMES: ReadonlySet<string> = new Set([
17+
// Array.prototype iterators
18+
"forEach",
19+
"map",
20+
"filter",
21+
"reduce",
22+
"reduceRight",
23+
"flatMap",
24+
"some",
25+
"every",
26+
"find",
27+
"findIndex",
28+
"findLast",
29+
"findLastIndex",
30+
// Observer / EventEmitter / event bus patterns — these are
31+
// hand-off calls (the consumer keeps a subscription / dispatches
32+
// to subscribers) not the "pass derived data to a parent"
33+
// anti-pattern the rule targets.
34+
"subscribe",
35+
"unsubscribe",
36+
"addEventListener",
37+
"addListener",
38+
"removeEventListener",
39+
"removeListener",
40+
"on",
41+
"once",
42+
"off",
43+
"emit",
44+
"dispatch",
45+
"publish",
46+
"notify",
47+
"trigger",
48+
"fire",
49+
"broadcast",
50+
"send",
51+
// Promise
52+
"then",
53+
"catch",
54+
"finally",
55+
// Set / Map / cache
56+
"add",
57+
"delete",
58+
"has",
59+
"get",
60+
"set",
61+
"clear",
62+
"put",
63+
"push",
64+
"pop",
65+
"shift",
66+
"unshift",
67+
// Logger / telemetry shapes — `props.logger.info(...)` is reporting,
68+
// not data hand-off.
69+
"log",
70+
"info",
71+
"warn",
72+
"error",
73+
"debug",
74+
"trace",
75+
"track",
76+
"capture",
77+
// Imperative action methods on stateful objects — `animationLoop.start()`,
78+
// `subscription.cancel()`, `controller.abort()`. The arg (if any) is
79+
// a configuration value, not the child's derived state.
80+
"start",
81+
"stop",
82+
"play",
83+
"pause",
84+
"resume",
85+
"cancel",
86+
"abort",
87+
"commit",
88+
"rollback",
89+
"reset",
90+
"focus",
91+
"blur",
92+
"scroll",
93+
"scrollTo",
94+
"scrollIntoView",
95+
"close",
96+
"open",
97+
"show",
98+
"hide",
99+
"expand",
100+
"collapse",
101+
"toggle",
102+
"refresh",
103+
"reload",
104+
"rerender",
105+
"refetch",
106+
"invalidate",
107+
"select",
108+
"deselect",
109+
"click",
110+
"press",
111+
"tap",
112+
"submit",
113+
"validate",
114+
"format",
115+
"parse",
116+
"serialize",
117+
"deserialize",
118+
]);

packages/oxlint-plugin-react-doctor/src/plugin/rules/state-and-effects/no-pass-data-to-parent.ts

Lines changed: 2 additions & 110 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { defineRule } from "../../utils/define-rule.js";
22
import type { EsTreeNode } from "../../utils/es-tree-node.js";
33
import type { EsTreeNodeOfType } from "../../utils/es-tree-node-of-type.js";
44
import { isNamespacedApiCallee } from "../../utils/is-namespaced-api-call.js";
5+
import { DATA_SINK_METHOD_NAMES } from "../../constants/data-sink-method-names.js";
56
import type { Rule } from "../../utils/rule.js";
67
import type { RuleContext } from "../../utils/rule-context.js";
78
import {
@@ -28,115 +29,6 @@ import { isNodeOfType } from "../../utils/is-node-of-type.js";
2829

2930
// 1:1 port of upstream `src/rules/no-pass-data-to-parent.js`.
3031

31-
// Method names that are clearly NOT "callbacks that pass data to a
32-
// parent" even when called on a prop value — JS prototype iterators,
33-
// observer-pattern subscriptions, promise chaining, native Set/Map/
34-
// EventEmitter methods. The rule's intent is to catch
35-
// `props.onDataLoaded(data)` style callbacks; `props.items.forEach(fn)`,
36-
// `props.store.subscribe(fn)`, `props.fetcher.then(fn)` aren't that.
37-
const ITERATOR_METHOD_NAMES: ReadonlySet<string> = new Set([
38-
// Array.prototype iterators
39-
"forEach",
40-
"map",
41-
"filter",
42-
"reduce",
43-
"reduceRight",
44-
"flatMap",
45-
"some",
46-
"every",
47-
"find",
48-
"findIndex",
49-
"findLast",
50-
"findLastIndex",
51-
// Observer / EventEmitter / event bus patterns — these are
52-
// hand-off calls (the consumer keeps a subscription / dispatches
53-
// to subscribers) not the "pass derived data to a parent"
54-
// anti-pattern the rule targets.
55-
"subscribe",
56-
"unsubscribe",
57-
"addEventListener",
58-
"addListener",
59-
"removeEventListener",
60-
"removeListener",
61-
"on",
62-
"once",
63-
"off",
64-
"emit",
65-
"dispatch",
66-
"publish",
67-
"notify",
68-
"trigger",
69-
"fire",
70-
"broadcast",
71-
"send",
72-
// Promise
73-
"then",
74-
"catch",
75-
"finally",
76-
// Set / Map / cache
77-
"add",
78-
"delete",
79-
"has",
80-
"get",
81-
"set",
82-
"clear",
83-
"put",
84-
"push",
85-
"pop",
86-
"shift",
87-
"unshift",
88-
// Logger / telemetry shapes — `props.logger.info(...)` is reporting,
89-
// not data hand-off.
90-
"log",
91-
"info",
92-
"warn",
93-
"error",
94-
"debug",
95-
"trace",
96-
"track",
97-
"capture",
98-
// Imperative action methods on stateful objects — `animationLoop.start()`,
99-
// `subscription.cancel()`, `controller.abort()`. The arg (if any) is
100-
// a configuration value, not the child's derived state.
101-
"start",
102-
"stop",
103-
"play",
104-
"pause",
105-
"resume",
106-
"cancel",
107-
"abort",
108-
"commit",
109-
"rollback",
110-
"reset",
111-
"focus",
112-
"blur",
113-
"scroll",
114-
"scrollTo",
115-
"scrollIntoView",
116-
"close",
117-
"open",
118-
"show",
119-
"hide",
120-
"expand",
121-
"collapse",
122-
"toggle",
123-
"refresh",
124-
"reload",
125-
"rerender",
126-
"refetch",
127-
"invalidate",
128-
"select",
129-
"deselect",
130-
"click",
131-
"press",
132-
"tap",
133-
"submit",
134-
"validate",
135-
"format",
136-
"parse",
137-
"serialize",
138-
"deserialize",
139-
]);
14032

14133
const getCallMethodName = (callee: EsTreeNode): string | null => {
14234
if (
@@ -218,7 +110,7 @@ export const noPassDataToParent = defineRule<Rule>({
218110
// which never uses these method names.
219111
const calleeNode = (callExpr as unknown as { callee?: EsTreeNode }).callee;
220112
const methodName = calleeNode ? getCallMethodName(calleeNode) : null;
221-
if (methodName && ITERATOR_METHOD_NAMES.has(methodName)) continue;
113+
if (methodName && DATA_SINK_METHOD_NAMES.has(methodName)) continue;
222114
// `editor.commands.setSelection(...)`, `props.store.dispatch(...)`,
223115
// `props.queryClient.invalidate(...)` etc. — calling a method
224116
// on a namespaced API object, not handing data back to a parent.

packages/oxlint-plugin-react-doctor/src/plugin/rules/state-and-effects/no-pass-live-state-to-parent.ts

Lines changed: 2 additions & 104 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import type { EsTreeNode } from "../../utils/es-tree-node.js";
33
import type { EsTreeNodeOfType } from "../../utils/es-tree-node-of-type.js";
44
import { isNamespacedApiCallee } from "../../utils/is-namespaced-api-call.js";
55
import { isNodeOfType } from "../../utils/is-node-of-type.js";
6+
import { DATA_SINK_METHOD_NAMES } from "../../constants/data-sink-method-names.js";
67
import type { Rule } from "../../utils/rule.js";
78
import type { RuleContext } from "../../utils/rule-context.js";
89
import { getArgsUpstreamRefs, getCallExpr, isSynchronous } from "./utils/effect/ast.js";
@@ -17,109 +18,6 @@ import {
1718
isUseEffect,
1819
} from "./utils/effect/react.js";
1920

20-
// 1:1 port of upstream `src/rules/no-pass-live-state-to-parent.js`.
21-
22-
// Method names that are clearly NOT "callbacks that pass state to a
23-
// parent" — JS prototype iterators, observer subscriptions, promise
24-
// chaining, native Set/Map, event-bus dispatch, logger/telemetry, and
25-
// imperative actions on stateful objects. Mirrors `no-pass-data-to-parent`.
26-
const ITERATOR_METHOD_NAMES: ReadonlySet<string> = new Set([
27-
// Array.prototype iterators
28-
"forEach",
29-
"map",
30-
"filter",
31-
"reduce",
32-
"reduceRight",
33-
"flatMap",
34-
"some",
35-
"every",
36-
"find",
37-
"findIndex",
38-
"findLast",
39-
"findLastIndex",
40-
// Observer / EventEmitter / event bus
41-
"subscribe",
42-
"unsubscribe",
43-
"addEventListener",
44-
"addListener",
45-
"removeEventListener",
46-
"removeListener",
47-
"on",
48-
"once",
49-
"off",
50-
"emit",
51-
"dispatch",
52-
"publish",
53-
"notify",
54-
"trigger",
55-
"fire",
56-
"broadcast",
57-
"send",
58-
// Promise
59-
"then",
60-
"catch",
61-
"finally",
62-
// Set / Map / cache
63-
"add",
64-
"delete",
65-
"has",
66-
"get",
67-
"set",
68-
"clear",
69-
"put",
70-
"push",
71-
"pop",
72-
"shift",
73-
"unshift",
74-
// Logger / telemetry
75-
"log",
76-
"info",
77-
"warn",
78-
"error",
79-
"debug",
80-
"trace",
81-
"track",
82-
"capture",
83-
// Imperative actions on stateful objects
84-
"start",
85-
"stop",
86-
"play",
87-
"pause",
88-
"resume",
89-
"cancel",
90-
"abort",
91-
"commit",
92-
"rollback",
93-
"reset",
94-
"focus",
95-
"blur",
96-
"scroll",
97-
"scrollTo",
98-
"scrollIntoView",
99-
"close",
100-
"open",
101-
"show",
102-
"hide",
103-
"expand",
104-
"collapse",
105-
"toggle",
106-
"refresh",
107-
"reload",
108-
"rerender",
109-
"refetch",
110-
"invalidate",
111-
"select",
112-
"deselect",
113-
"click",
114-
"press",
115-
"tap",
116-
"submit",
117-
"validate",
118-
"format",
119-
"parse",
120-
"serialize",
121-
"deserialize",
122-
]);
12321

12422
const getCallMethodName = (callee: EsTreeNode): string | null => {
12523
if (
@@ -158,7 +56,7 @@ export const noPassLiveStateToParent = defineRule<Rule>({
15856
// `no-pass-data-to-parent` for the full rationale.
15957
const calleeNode = (callExpr as unknown as { callee?: EsTreeNode }).callee;
16058
const methodName = calleeNode ? getCallMethodName(calleeNode) : null;
161-
if (methodName && ITERATOR_METHOD_NAMES.has(methodName)) continue;
59+
if (methodName && DATA_SINK_METHOD_NAMES.has(methodName)) continue;
16260
if (calleeNode && isNamespacedApiCallee(calleeNode)) continue;
16361

16462
const isStateInArgs = getArgsUpstreamRefs(analysis, ref).some((argRef) =>

0 commit comments

Comments
 (0)