Skip to content

Commit 646d020

Browse files
committed
fix(linter/exhaustive-dependencies): prevent is_callee_of_call_expr flag from leaking into nested expressions (#15832)
1 parent 2191ae9 commit 646d020

File tree

2 files changed

+66
-48
lines changed

2 files changed

+66
-48
lines changed

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

Lines changed: 63 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -1348,66 +1348,65 @@ 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+
self.skip_reporting_dependency = true;
1401+
self.is_callee_of_call_expr = false;
1402+
self.visit_expression(&it.object);
1403+
self.skip_reporting_dependency = cur_skip_reporting_dependency;
1404+
} else {
14051405
// this means that some part of the chain could not be analyzed
14061406
// for example `foo.bar.baz().abc`. `baz()` cannot be statically analyzed
14071407
// instead, continue to go down, looking at the object to gather dependencies
1408-
Err(()) => {
1409-
self.visit_expression(&it.object);
1410-
}
1408+
self.is_callee_of_call_expr = false;
1409+
self.visit_expression(&it.object);
14111410
}
14121411
}
14131412

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

26612679
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)