Skip to content

Commit 1005264

Browse files
committed
fix(linter/exhaustive-dependencies): prevent is_callee_of_call_expr flag from leaking into nested expressions
1 parent 66f5d6b commit 1005264

File tree

2 files changed

+70
-48
lines changed

2 files changed

+70
-48
lines changed

crates/oxc_linter/src/rules/react/exhaustive_deps.rs

Lines changed: 67 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -1348,66 +1348,69 @@ impl<'a> Visit<'a> for ExhaustiveDepsVisitor<'a, '_> {
13481348

13491349
let is_parent_call_expr = self.is_callee_of_call_expr;
13501350

1351-
match analyze_property_chain(&it.object, self.semantic) {
1352-
Ok(source) => {
1353-
if let Some(source) = source {
1354-
if is_parent_call_expr {
1355-
self.found_dependencies.insert(source);
1356-
} else {
1357-
let new_chain = Vec::from([it.property.name]);
1358-
1359-
let mut destructured_props: Vec<Atom<'a>> = vec![];
1360-
let mut did_see_ref = false;
1361-
let needs_full_chain = self
1362-
.iter_destructure_bindings(|id| {
1363-
if let Cow::Borrowed(id) = id {
1364-
if id == "current" {
1365-
did_see_ref = true;
1366-
} else {
1367-
destructured_props.push(id.into());
1368-
}
1351+
if let Ok(source) = analyze_property_chain(&it.object, self.semantic) {
1352+
if let Some(source) = source {
1353+
if is_parent_call_expr {
1354+
self.found_dependencies.insert(source);
1355+
} else {
1356+
let new_chain = Vec::from([it.property.name]);
1357+
1358+
let mut destructured_props: Vec<Atom<'a>> = vec![];
1359+
let mut did_see_ref = false;
1360+
let needs_full_chain = self
1361+
.iter_destructure_bindings(|id| {
1362+
if let Cow::Borrowed(id) = id {
1363+
if id == "current" {
1364+
did_see_ref = true;
13691365
} else {
1370-
// todo
1366+
destructured_props.push(id.into());
13711367
}
1372-
})
1373-
.unwrap_or(true);
1374-
1375-
let symbol_id =
1376-
self.semantic.scoping().get_reference(source.reference_id).symbol_id();
1377-
if needs_full_chain || (destructured_props.is_empty() && !did_see_ref) {
1368+
} else {
1369+
// todo
1370+
}
1371+
})
1372+
.unwrap_or(true);
1373+
1374+
let symbol_id =
1375+
self.semantic.scoping().get_reference(source.reference_id).symbol_id();
1376+
if needs_full_chain || (destructured_props.is_empty() && !did_see_ref) {
1377+
self.found_dependencies.insert(Dependency {
1378+
name: source.name,
1379+
reference_id: source.reference_id,
1380+
span: source.span,
1381+
chain: [source.chain.clone(), new_chain].concat(),
1382+
symbol_id,
1383+
});
1384+
} else {
1385+
for prop in destructured_props {
13781386
self.found_dependencies.insert(Dependency {
13791387
name: source.name,
13801388
reference_id: source.reference_id,
13811389
span: source.span,
1382-
chain: [source.chain.clone(), new_chain].concat(),
1390+
chain: [source.chain.clone(), new_chain.clone(), vec![prop]]
1391+
.concat(),
13831392
symbol_id,
13841393
});
1385-
} else {
1386-
for prop in destructured_props {
1387-
self.found_dependencies.insert(Dependency {
1388-
name: source.name,
1389-
reference_id: source.reference_id,
1390-
span: source.span,
1391-
chain: [source.chain.clone(), new_chain.clone(), vec![prop]]
1392-
.concat(),
1393-
symbol_id,
1394-
});
1395-
}
13961394
}
13971395
}
13981396
}
1399-
1400-
let cur_skip_reporting_dependency = self.skip_reporting_dependency;
1401-
self.skip_reporting_dependency = true;
1402-
self.visit_expression(&it.object);
1403-
self.skip_reporting_dependency = cur_skip_reporting_dependency;
14041397
}
1398+
1399+
let cur_skip_reporting_dependency = self.skip_reporting_dependency;
1400+
let cur_is_callee_of_call_expr = self.is_callee_of_call_expr;
1401+
self.skip_reporting_dependency = true;
1402+
self.is_callee_of_call_expr = false;
1403+
self.visit_expression(&it.object);
1404+
self.skip_reporting_dependency = cur_skip_reporting_dependency;
1405+
self.is_callee_of_call_expr = cur_is_callee_of_call_expr;
1406+
} else {
14051407
// this means that some part of the chain could not be analyzed
14061408
// for example `foo.bar.baz().abc`. `baz()` cannot be statically analyzed
14071409
// instead, continue to go down, looking at the object to gather dependencies
1408-
Err(()) => {
1409-
self.visit_expression(&it.object);
1410-
}
1410+
let cur_is_callee_of_call_expr = self.is_callee_of_call_expr;
1411+
self.is_callee_of_call_expr = false;
1412+
self.visit_expression(&it.object);
1413+
self.is_callee_of_call_expr = cur_is_callee_of_call_expr;
14111414
}
14121415
}
14131416

@@ -2656,6 +2659,25 @@ fn test() {
26562659
onStuff();
26572660
}, []);
26582661
}",
2662+
// Issue #15796 - object property access should work correctly
2663+
r"export const FileSize = ({ file, showSize = true }) => {
2664+
const fileSizeInMB = useMemo(
2665+
() => (showSize ? (file.size / (1024 * 1024)).toFixed(2) : undefined),
2666+
[showSize, file.size],
2667+
);
2668+
return fileSizeInMB;
2669+
}",
2670+
// Additional tests for nested property access within expressions
2671+
r"function MyComponent({ obj }) {
2672+
useMemo(() => {
2673+
return (obj.value * 2).toFixed(2);
2674+
}, [obj.value]);
2675+
}",
2676+
r"function MyComponent({ data }) {
2677+
useCallback(() => {
2678+
console.log((data.count + 1).toString());
2679+
}, [data.count]);
2680+
}",
26592681
];
26602682

26612683
let fail = vec![

crates/oxc_linter/src/snapshots/react_exhaustive_deps.snap

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -393,13 +393,13 @@ source: crates/oxc_linter/src/tester.rs
393393
╰────
394394
help: Either include it or remove the dependency array.
395395

396-
eslint-plugin-react-hooks(exhaustive-deps): React Hook useEffect has a missing dependency: 'history.foo'
396+
eslint-plugin-react-hooks(exhaustive-deps): React Hook useEffect has missing dependencies: 'history.foo', and 'history.foo.bar'
397397
╭─[exhaustive_deps.tsx:7:14]
398398
3return [
399399
4history.foo.bar[2].dobedo.listen(),
400-
· ─────┬─────
401-
· ╰── useEffect uses `history.foo` here
400+
· ───────────
402401
5history.foo.bar().dobedo.listen[2]
402+
· ───────────
403403
6 │ ];
404404
7 │ }, []);
405405
· ──

0 commit comments

Comments
 (0)