Skip to content

Commit 3a216d8

Browse files
committed
feat: flag most rules when *any* - not *all* - upstreams are internal
typically still correct I think, and it significantly simplifies handling upstream edge cases. requires some testing in the wild. #53 #38
1 parent 5287f57 commit 3a216d8

22 files changed

+578
-545
lines changed

README.md

Lines changed: 1 addition & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -237,20 +237,6 @@ function Component() {
237237
}
238238
```
239239

240-
### `no-manage-parent`
241-
242-
Disallow effects that only use props:
243-
244-
```js
245-
function Child({ isOpen, onClose }) {
246-
useEffect(() => {
247-
if (!isOpen) {
248-
onClose();
249-
}
250-
}, [isOpen, onClose]);
251-
}
252-
```
253-
254240
### `no-empty-effect`
255241

256242
Disallow empty effects:
@@ -263,7 +249,7 @@ function Component() {
263249

264250
## 💬 Feedback
265251

266-
The ways to (mis)use an effect in real-world code are practically endless! This plugin is not exhaustive, and minimizes false positives at the expense of occasional false negatives. If you encounter unexpected behavior or see opportunities for improvement, please open an issue. Your feedback helps improve the plugin for everyone!
252+
The ways to (mis)use an effect in real-world code are practically endless! This plugin is not exhaustive. If you encounter unexpected behavior or see opportunities for improvement, please open an issue. Your feedback helps improve the plugin for everyone!
267253

268254
## 📖 Learn More
269255

src/index.js

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ import noInitializeState from "./rules/no-initialize-state.js";
77
import noChainStateUpdates from "./rules/no-chain-state-updates.js";
88
import noDerivedState from "./rules/no-derived-state.js";
99
import noPassDataToParent from "./rules/no-pass-data-to-parent.js";
10-
import noManageParent from "./rules/no-manage-parent.js";
1110
import noPassRefToParent from "./rules/no-pass-ref-to-parent.js";
1211
import globals from "globals";
1312

@@ -26,7 +25,6 @@ const plugin = {
2625
"no-event-handler": noEventHandler,
2726
"no-pass-live-state-to-parent": noPassLiveStateToParent,
2827
"no-pass-data-to-parent": noPassDataToParent,
29-
"no-manage-parent": noManageParent,
3028
"no-pass-ref-to-parent": noPassRefToParent,
3129
"no-initialize-state": noInitializeState,
3230
"no-chain-state-updates": noChainStateUpdates,
@@ -69,8 +67,3 @@ Object.assign(plugin.configs, {
6967
});
7068

7169
export default plugin;
72-
73-
// Wraps `Array.every()` to return false for empty arrays.
74-
Array.prototype.notEmptyEvery = function (predicate) {
75-
return this.length > 0 && this.every(predicate);
76-
};

src/rules/no-adjust-state-on-prop-change.js

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
1-
import { getCallExpr, getUpstreamRefs } from "../util/ast.js";
21
import {
2+
getCallExpr,
3+
getDownstreamRefs,
4+
getUpstreamRefs,
35
getEffectDepsRefs,
46
getEffectFnRefs,
5-
isArgsAllLiterals,
67
isImmediateCall,
78
isProp,
89
isStateSetter,
@@ -32,18 +33,28 @@ export default {
3233
const depsRefs = getEffectDepsRefs(context, node);
3334
if (!effectFnRefs || !depsRefs) return;
3435

35-
const isAllDepsProps = depsRefs
36+
const isSomeDepsProps = depsRefs
3637
.flatMap((ref) => getUpstreamRefs(context, ref))
37-
.notEmptyEvery((ref) => isProp(ref));
38+
.some((ref) => isProp(ref));
3839

3940
effectFnRefs
4041
.filter((ref) => isStateSetter(context, ref))
4142
.filter((ref) => isImmediateCall(ref.identifier))
4243
.forEach((ref) => {
4344
const callExpr = getCallExpr(ref);
4445

45-
// TODO: Flag non-literals too? e.g. I think this is the correct warning for https://github.com/getsentry/sentry/pull/100177/files#diff-cf3aceaba5cdab4553d92644581e23d54914923199d31807fe090e0d49b786caR97
46-
if (isAllDepsProps && isArgsAllLiterals(context, callExpr)) {
46+
const argsUpstreamRefs = callExpr.arguments
47+
.flatMap((arg) => getDownstreamRefs(context, arg))
48+
.flatMap((ref) => getUpstreamRefs(context, ref));
49+
// Avoid overlap with no-derived-state
50+
const isSomeArgsNotProps =
51+
// TODO: literals check may be less reliable with *all* upstream refs...
52+
// What if that was `getUpstreamNodes()` instead, returning AST nodes?
53+
// Could get complicated though. Ideally we may restructure the rules to not need this at all?
54+
argsUpstreamRefs.length === 0 || // All literals
55+
argsUpstreamRefs.some((ref) => !isProp(ref));
56+
57+
if (isSomeDepsProps && isSomeArgsNotProps) {
4758
context.report({
4859
node: callExpr,
4960
messageId: "avoidAdjustingStateWhenAPropChanges",

src/rules/no-chain-state-updates.js

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
1-
import { getCallExpr, getUpstreamRefs } from "../util/ast.js";
21
import {
2+
getCallExpr,
3+
getDownstreamRefs,
4+
getUpstreamRefs,
35
getEffectDepsRefs,
46
getEffectFnRefs,
57
hasCleanup,
6-
isArgsAllLiterals,
78
isImmediateCall,
89
isUseState,
910
isStateSetter,
@@ -33,19 +34,25 @@ export default {
3334
const depsRefs = getEffectDepsRefs(context, node);
3435
if (!effectFnRefs || !depsRefs) return;
3536

36-
// TODO: Should filter out setters before checking?
37-
// exhaustive-deps doesn't enforce one way or the other.
38-
const isAllDepsState = depsRefs
37+
const isSomeDepsState = depsRefs
3938
.flatMap((ref) => getUpstreamRefs(context, ref))
40-
.notEmptyEvery((ref) => isUseState(ref));
39+
.some((ref) => isUseState(ref));
4140

4241
effectFnRefs
4342
.filter((ref) => isStateSetter(context, ref))
4443
.filter((ref) => isImmediateCall(ref.identifier))
4544
.forEach((ref) => {
4645
const callExpr = getCallExpr(ref);
4746

48-
if (isAllDepsState && isArgsAllLiterals(context, callExpr)) {
47+
const argsUpstreamRefs = callExpr.arguments
48+
.flatMap((arg) => getDownstreamRefs(context, arg))
49+
.flatMap((ref) => getUpstreamRefs(context, ref));
50+
// Avoid overlap with no-derived-state
51+
const isSomeArgsState = argsUpstreamRefs.some((ref) =>
52+
isUseState(ref),
53+
);
54+
55+
if (isSomeDepsState && !isSomeArgsState) {
4956
context.report({
5057
node: callExpr,
5158
messageId: "avoidChainingStateUpdates",

src/rules/no-derived-state.js

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -54,18 +54,20 @@ export default {
5454
const depsUpstreamRefs = depsRefs.flatMap((ref) =>
5555
getUpstreamRefs(context, ref),
5656
);
57-
const isAllArgsInternal = argsUpstreamRefs.notEmptyEvery(
57+
const isSomeArgsInternal = argsUpstreamRefs.some(
5858
(ref) => isUseState(ref) || isProp(ref),
5959
);
6060

61-
const isAllArgsInDeps = argsUpstreamRefs.notEmptyEvery((argRef) =>
62-
depsUpstreamRefs.some(
63-
(depRef) => argRef.resolved == depRef.resolved,
64-
),
65-
);
61+
const isAllArgsInDeps =
62+
argsUpstreamRefs.length &&
63+
argsUpstreamRefs.every((argRef) =>
64+
depsUpstreamRefs.some(
65+
(depRef) => argRef.resolved == depRef.resolved,
66+
),
67+
);
6668
const isValueAlwaysInSync = isAllArgsInDeps && countCalls(ref) === 1;
6769

68-
if (isAllArgsInternal) {
70+
if (isSomeArgsInternal) {
6971
context.report({
7072
node: callExpr,
7173
messageId: "avoidDerivedState",

src/rules/no-event-handler.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ export default {
4040
getDownstreamRefs(context, ifNode.test)
4141
.flatMap((ref) => getUpstreamRefs(context, ref))
4242
// TODO: Should flag props too, but maybe with a different message?
43-
.notEmptyEvery((ref) => isUseState(ref)),
43+
.some((ref) => isUseState(ref)),
4444
)
4545
.forEach((ifNode) => {
4646
context.report({

src/rules/no-manage-parent.js

Lines changed: 0 additions & 46 deletions
This file was deleted.

src/rules/no-pass-data-to-parent.js

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
isUseEffect,
1010
getUpstreamRefs,
1111
isImmediateCall,
12+
isRefCall,
1213
} from "../util/ast.js";
1314
import { getCallExpr, getDownstreamRefs } from "../util/ast.js";
1415

@@ -37,20 +38,22 @@ export default {
3738

3839
effectFnRefs
3940
.filter((ref) => isPropCallback(context, ref))
41+
.filter((ref) => !isRefCall(context, ref))
4042
.filter((ref) => isImmediateCall(ref.identifier))
4143
.forEach((ref) => {
4244
const callExpr = getCallExpr(ref);
4345

44-
const isAllData =
45-
callExpr.arguments.length &
46-
callExpr.arguments
47-
.flatMap((arg) => getDownstreamRefs(context, arg))
48-
.flatMap((ref) => getUpstreamRefs(context, ref))
49-
.notEmptyEvery(
50-
(ref) => !isUseState(ref) && !isProp(ref) && !isUseRef(ref),
51-
);
46+
const argsUpstreamRefs = callExpr.arguments
47+
.flatMap((arg) => getDownstreamRefs(context, arg))
48+
.flatMap((ref) => getUpstreamRefs(context, ref));
49+
// TODO: Includes literals. I think that makes sense, but could have a better message.
50+
const isSomeArgsData =
51+
callExpr.arguments.length & argsUpstreamRefs.length &&
52+
argsUpstreamRefs.some(
53+
(ref) => !isUseState(ref) && !isProp(ref) && !isUseRef(ref),
54+
);
5255

53-
if (isAllData) {
56+
if (isSomeArgsData) {
5457
context.report({
5558
node: callExpr,
5659
messageId: "avoidPassingDataToParent",

src/rules/no-pass-ref-to-parent.js

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ export default {
2727
"Avoid passing refs to parents in an effect. Use `forwardRef` instead.",
2828
avoidPropCallbackInRefCallback:
2929
"Avoid calling props inside callbacks registered on refs in an effect. Use `forwardRef` to register the callback in the parent instead.",
30+
avoidReceivingRefFromParent:
31+
"Avoid receiving refs from parents to use in an effect. Use `forwardRef` instead.",
3032
},
3133
},
3234
create: (context) => ({
@@ -59,18 +61,31 @@ export default {
5961
.forEach((ref) => {
6062
const callExpr = getCallExpr(ref);
6163

62-
const passesCallbackDataToParent = callExpr.arguments
64+
const passesDataToParent = callExpr.arguments
6365
.flatMap((arg) => getDownstreamRefs(context, arg))
6466
.flatMap((ref) => getUpstreamRefs(context, ref))
6567
.some((ref) => isPropCallback(context, ref));
6668

67-
if (passesCallbackDataToParent) {
69+
if (passesDataToParent) {
6870
context.report({
69-
node: getCallExpr(ref),
71+
node: callExpr,
7072
messageId: "avoidPropCallbackInRefCallback",
7173
});
7274
}
7375
});
76+
77+
effectFnRefs
78+
.filter(
79+
(ref) => isPropCallback(context, ref) && isRefCall(context, ref),
80+
)
81+
.forEach((ref) => {
82+
const callExpr = getCallExpr(ref);
83+
84+
context.report({
85+
node: callExpr,
86+
messageId: "avoidReceivingRefFromParent",
87+
});
88+
});
7489
},
7590
}),
7691
};

0 commit comments

Comments
 (0)