1use std::collections::{HashMap, HashSet};
8
9use quote::quote;
10use syn::visit::{self, Visit};
11use syn::visit_mut::{self, VisitMut};
12
13use crate::migrate_v2::rewriter::FileRewriter;
14
15pub struct Rule;
17
18impl FileRewriter for Rule {
19 fn name(&self) -> &'static str {
20 "use_effect_deps"
21 }
22
23 fn rewrite(&self, mut file: syn::File) -> syn::File {
24 rewrite_scope(&mut file.items);
25 file
26 }
27}
28
29#[derive(Clone, Copy, PartialEq, Eq)]
30struct HookSpec {
31 name: &'static str,
32 callback_index: usize,
33 deps_index: usize,
34 omitted_arity: usize,
35 explicit_arity: usize,
36}
37
38const HOOK_SPECS: &[HookSpec] = &[
39 HookSpec {
40 name: "use_effect",
41 callback_index: 0,
42 deps_index: 1,
43 omitted_arity: 1,
44 explicit_arity: 2,
45 },
46 HookSpec {
47 name: "use_layout_effect",
48 callback_index: 0,
49 deps_index: 1,
50 omitted_arity: 1,
51 explicit_arity: 2,
52 },
53 HookSpec {
54 name: "use_retained_effect",
55 callback_index: 0,
56 deps_index: 1,
57 omitted_arity: 1,
58 explicit_arity: 2,
59 },
60 HookSpec {
61 name: "use_retained_layout_effect",
62 callback_index: 0,
63 deps_index: 1,
64 omitted_arity: 1,
65 explicit_arity: 2,
66 },
67 HookSpec {
68 name: "use_memo",
69 callback_index: 0,
70 deps_index: 1,
71 omitted_arity: 1,
72 explicit_arity: 2,
73 },
74 HookSpec {
75 name: "use_callback",
76 callback_index: 0,
77 deps_index: 1,
78 omitted_arity: 1,
79 explicit_arity: 2,
80 },
81 HookSpec {
82 name: "use_callback_with",
83 callback_index: 0,
84 deps_index: 1,
85 omitted_arity: 1,
86 explicit_arity: 2,
87 },
88 HookSpec {
89 name: "use_resource",
90 callback_index: 0,
91 deps_index: 1,
92 omitted_arity: 1,
93 explicit_arity: 2,
94 },
95 HookSpec {
96 name: "use_resource_with_key",
97 callback_index: 1,
98 deps_index: 2,
99 omitted_arity: 2,
100 explicit_arity: 3,
101 },
102];
103
104#[derive(Clone)]
105struct UseEntry {
106 original: Vec<syn::Ident>,
107 bound: syn::Ident,
108 attrs: Vec<syn::Attribute>,
109}
110
111#[derive(Clone)]
112struct GeneratedDepsImport {
113 facade: syn::Path,
114 attrs: Vec<syn::Attribute>,
115}
116
117struct HookResolution {
118 spec: HookSpec,
119 imports: Option<Vec<GeneratedDepsImport>>,
120}
121
122struct HookVisitor {
123 generated_imports: Vec<GeneratedDepsImport>,
124 imports: Vec<UseEntry>,
125 local_modules: HashSet<String>,
126}
127
128fn rewrite_scope(items: &mut Vec<syn::Item>) {
129 let imports = collect_imports(items);
130 let local_modules = items
131 .iter()
132 .filter_map(|item| match item {
133 syn::Item::Mod(module) => Some(module.ident.to_string()),
134 _ => None,
135 })
136 .collect();
137 let mut visitor = HookVisitor {
138 generated_imports: Vec::new(),
139 imports: imports.clone(),
140 local_modules,
141 };
142 for item in items.iter_mut() {
143 visitor.visit_item_mut(item);
144 }
145 if !visitor.generated_imports.is_empty() {
146 let insertion = items
147 .iter()
148 .position(|item| matches!(item, syn::Item::Use(_)))
149 .unwrap_or(0);
150 let unconditional_facades = visitor
151 .generated_imports
152 .iter()
153 .filter(|generated| generated.attrs.is_empty())
154 .map(|generated| facade_key(&generated.facade))
155 .collect::<HashSet<_>>();
156 let mut inserted = 0;
157 for generated in visitor.generated_imports {
158 if !generated.attrs.is_empty()
159 && unconditional_facades.contains(&facade_key(&generated.facade))
160 {
161 continue;
162 }
163 if has_deps_import(&imports, &generated.facade, &generated.attrs) {
164 continue;
165 }
166 let attrs = generated.attrs;
167 let facade = generated.facade;
168 let import = syn::parse2(quote!(#(#attrs)* use #facade::deps;))
169 .expect("generated deps import must parse");
170 items.insert(insertion + inserted, import);
171 inserted += 1;
172 }
173 }
174 for item in items.iter_mut() {
175 if let syn::Item::Mod(module) = item
176 && let Some((_, nested)) = &mut module.content
177 {
178 rewrite_scope(nested);
179 }
180 }
181}
182
183impl VisitMut for HookVisitor {
184 fn visit_item_mod_mut(&mut self, _module: &mut syn::ItemMod) {}
185
186 fn visit_block_mut(&mut self, block: &mut syn::Block) {
187 let previous_imports = self.imports.clone();
188 let mut local_imports = Vec::new();
189 for stmt in &block.stmts {
190 if let syn::Stmt::Item(syn::Item::Use(item_use)) = stmt {
191 flatten_use_tree(&item_use.tree, &[], &item_use.attrs, &mut local_imports);
192 }
193 }
194 for local in local_imports {
195 self.imports.retain(|entry| entry.bound != local.bound);
196 self.imports.push(local);
197 }
198 visit_mut::visit_block_mut(self, block);
199 self.imports = previous_imports;
200 }
201
202 fn visit_expr_call_mut(&mut self, call: &mut syn::ExprCall) {
203 if let Some(resolution) = self.resolve_hook(call) {
204 let spec = resolution.spec;
205 if call.args.len() == spec.omitted_arity {
206 let callback = call
207 .args
208 .iter()
209 .nth(spec.callback_index)
210 .expect("hook callback must exist");
211 let deps = resolution
212 .imports
213 .as_ref()
214 .and_then(|_| dependency_list(callback));
215 if deps.is_some()
216 && let Some(imports) = &resolution.imports
217 {
218 for import in imports {
219 self.record_generated_import(import.facade.clone(), import.attrs.clone());
220 }
221 }
222 call.args
223 .insert(spec.deps_index, deps.unwrap_or_else(review_marker));
224 } else if call.args.len() == spec.explicit_arity
225 && let Some(deps) = tuple_dependency_list(
226 call.args
227 .iter()
228 .nth(spec.deps_index)
229 .expect("hook dependencies must exist"),
230 ) {
231 if let Some(imports) = &resolution.imports {
232 for import in imports {
233 self.record_generated_import(import.facade.clone(), import.attrs.clone());
234 }
235 *call
236 .args
237 .iter_mut()
238 .nth(spec.deps_index)
239 .expect("hook dependencies must exist") = deps;
240 } else {
241 *call
242 .args
243 .iter_mut()
244 .nth(spec.deps_index)
245 .expect("hook dependencies must exist") = review_marker();
246 }
247 }
248 }
249 visit_mut::visit_expr_call_mut(self, call);
250 }
251}
252
253impl HookVisitor {
254 fn record_generated_import(&mut self, facade: syn::Path, attrs: Vec<syn::Attribute>) {
255 let key = import_key(&facade, &attrs);
256 if self
257 .generated_imports
258 .iter()
259 .all(|generated| import_key(&generated.facade, &generated.attrs) != key)
260 {
261 self.generated_imports
262 .push(GeneratedDepsImport { facade, attrs });
263 }
264 }
265
266 fn resolve_hook(&self, call: &syn::ExprCall) -> Option<HookResolution> {
267 let syn::Expr::Path(path) = &*call.func else {
268 return None;
269 };
270 let segments: Vec<_> = path
271 .path
272 .segments
273 .iter()
274 .map(|segment| segment.ident.clone())
275 .collect();
276 let called = segments.last()?;
277 if segments.len() == 1 {
278 let candidates = self
279 .imports
280 .iter()
281 .filter(|entry| entry.bound == *called)
282 .filter_map(|entry| {
283 let original = entry.original.last()?;
284 let spec = hook_spec(&original.to_string())?;
285 let import = facade_from_segments(&entry.original)
286 .filter(|_| !self.is_local_facade(&entry.original))
287 .map(|facade| GeneratedDepsImport {
288 facade,
289 attrs: entry.attrs.clone(),
290 });
291 Some((spec, import))
292 })
293 .collect::<Vec<_>>();
294 if let Some((spec, _)) = candidates.first() {
295 let spec = *spec;
296 if candidates.iter().any(|(candidate, _)| *candidate != spec) {
297 return Some(HookResolution {
298 spec: select_hook_spec(&candidates, call.args.len()),
299 imports: None,
300 });
301 }
302 let matching = candidates
303 .into_iter()
304 .filter(|(candidate, _)| *candidate == spec)
305 .collect::<Vec<_>>();
306 let imports = matching
307 .iter()
308 .map(|(_, import)| import.clone())
309 .collect::<Option<Vec<_>>>();
310 return Some(HookResolution { spec, imports });
311 }
312 hook_spec(&called.to_string())?;
313 return None;
314 }
315 let mut candidates = Vec::new();
316 for (index, segment) in segments.iter().enumerate().take(segments.len() - 1) {
317 for entry in self.imports.iter().filter(|entry| entry.bound == *segment) {
318 let mut resolved = entry.original.clone();
319 resolved.extend(segments.iter().skip(index + 1).cloned());
320 let Some(original) = resolved.last() else {
321 continue;
322 };
323 let Some(spec) = hook_spec(&original.to_string()) else {
324 continue;
325 };
326 let import = facade_from_segments(&resolved)
327 .filter(|_| !self.is_local_facade(&resolved))
328 .map(|facade| GeneratedDepsImport {
329 facade,
330 attrs: entry.attrs.clone(),
331 });
332 candidates.push((spec, import));
333 }
334 }
335 if let Some((spec, _)) = candidates.first() {
336 let spec = *spec;
337 if candidates.iter().any(|(candidate, _)| *candidate != spec) {
338 return Some(HookResolution {
339 spec: select_hook_spec(&candidates, call.args.len()),
340 imports: None,
341 });
342 }
343 let matching = candidates
344 .into_iter()
345 .filter(|(candidate, _)| *candidate == spec)
346 .collect::<Vec<_>>();
347 let imports = matching
348 .iter()
349 .map(|(_, import)| import.clone())
350 .collect::<Option<Vec<_>>>();
351 return Some(HookResolution { spec, imports });
352 }
353 let spec = hook_spec(&called.to_string())?;
354 let facade = facade_from_segments(&segments).filter(|_| !self.is_local_facade(&segments));
355 Some(HookResolution {
356 spec,
357 imports: facade.map(|facade| {
358 vec![GeneratedDepsImport {
359 facade,
360 attrs: Vec::new(),
361 }]
362 }),
363 })
364 }
365
366 fn is_local_facade(&self, segments: &[syn::Ident]) -> bool {
367 let Some(first) = segments.first() else {
368 return false;
369 };
370 if first == "crate" {
371 return segments
372 .iter()
373 .any(|segment| self.local_modules.contains(&segment.to_string()));
374 }
375 self.local_modules.contains(&first.to_string())
376 }
377}
378
379fn hook_spec(name: &str) -> Option<HookSpec> {
380 HOOK_SPECS.iter().copied().find(|spec| spec.name == name)
381}
382
383fn select_hook_spec(
384 candidates: &[(HookSpec, Option<GeneratedDepsImport>)],
385 arity: usize,
386) -> HookSpec {
387 candidates
388 .iter()
389 .find(|(spec, _)| spec.omitted_arity == arity || spec.explicit_arity == arity)
390 .map(|(spec, _)| *spec)
391 .unwrap_or(candidates[0].0)
392}
393
394fn facade_from_segments(segments: &[syn::Ident]) -> Option<syn::Path> {
395 if segments.is_empty() {
396 return None;
397 }
398 if segments
399 .first()
400 .is_some_and(|ident| ident == "self" || ident == "super")
401 {
402 return None;
403 }
404 let end = segments
405 .iter()
406 .position(|ident| ident == "reactive")
407 .unwrap_or(segments.len() - 1);
408 if end == 0 {
409 return None;
410 }
411 let facade = &segments[..end];
412 syn::parse2(quote!(#(#facade)::* )).ok()
413}
414
415fn tuple_dependency_list(deps: &syn::Expr) -> Option<syn::Expr> {
416 let syn::Expr::Tuple(tuple) = deps else {
417 return None;
418 };
419 let dependencies = tuple.elems.iter().map(strip_clone);
420 Some(
421 syn::parse2(quote!(deps![#(#dependencies),*]))
422 .expect("generated dependency list must parse"),
423 )
424}
425
426fn strip_clone(expr: &syn::Expr) -> &syn::Expr {
427 if let syn::Expr::MethodCall(call) = expr
428 && call.method == "clone"
429 && call.args.is_empty()
430 {
431 &call.receiver
432 } else {
433 expr
434 }
435}
436
437fn dependency_list(callback: &syn::Expr) -> Option<syn::Expr> {
438 let callback = strip_parens(callback);
439 let (closure, aliases) = match callback {
440 syn::Expr::Closure(closure) => (closure, HashMap::new()),
441 syn::Expr::Block(block) => {
442 let closure = block.block.stmts.last().and_then(stmt_closure)?;
443 (
444 closure,
445 clone_aliases(&block.block.stmts[..block.block.stmts.len() - 1]),
446 )
447 }
448 _ => return None,
449 };
450 let mut reads = OuterTrackedReadVisitor::default();
451 for input in &closure.inputs {
452 collect_pattern_idents(input, &mut reads.shadowed);
453 }
454 reads.visit_expr(&closure.body);
455 if reads.nested_tracked_read || reads.shadowed_tracked_read {
456 return None;
457 }
458 let mut dependencies = Vec::new();
459 let mut seen = HashSet::new();
460 for read in reads.reads {
461 let dependency = resolve_alias(&read, &aliases)?;
462 let key = quote!(#dependency).to_string();
463 if seen.insert(key) {
464 dependencies.push(dependency);
465 }
466 }
467 Some(
468 syn::parse2(quote!(deps![#(#dependencies),*]))
469 .expect("generated dependency list must parse"),
470 )
471}
472
473fn review_marker() -> syn::Expr {
474 syn::parse_quote! {
475 compile_error!(
476 "manouche-v2 codemod: add explicit deps list here, e.g. `deps![count]`"
477 )
478 }
479}
480
481#[derive(Default)]
482struct OuterTrackedReadVisitor {
483 reads: Vec<String>,
484 seen: HashSet<String>,
485 shadowed: HashSet<String>,
486 nested_tracked_read: bool,
487 shadowed_tracked_read: bool,
488}
489
490impl<'ast> Visit<'ast> for OuterTrackedReadVisitor {
491 fn visit_expr_closure(&mut self, closure: &'ast syn::ExprClosure) {
492 let mut nested = AnyTrackedReadVisitor::default();
493 nested.visit_expr_closure(closure);
494 self.nested_tracked_read |= nested.found;
495 }
496
497 fn visit_expr_method_call(&mut self, call: &'ast syn::ExprMethodCall) {
498 let method = call.method.to_string();
499 let tracked_read = (matches!(method.as_str(), "get" | "into_value")
500 && call.args.is_empty())
501 || (method == "with" && call.args.len() == 1);
502 if tracked_read && let Some(ident) = base_ident_of(&call.receiver) {
503 let ident = ident.to_string();
504 if self.shadowed.contains(&ident) {
505 self.shadowed_tracked_read = true;
506 } else if self.seen.insert(ident.clone()) {
507 self.reads.push(ident);
508 }
509 }
510 visit::visit_expr_method_call(self, call);
511 }
512
513 fn visit_expr_for_loop(&mut self, expr: &'ast syn::ExprForLoop) {
514 collect_pattern_idents(&expr.pat, &mut self.shadowed);
515 visit::visit_expr_for_loop(self, expr);
516 }
517
518 fn visit_arm(&mut self, arm: &'ast syn::Arm) {
519 collect_pattern_idents(&arm.pat, &mut self.shadowed);
520 visit::visit_arm(self, arm);
521 }
522
523 fn visit_expr_let(&mut self, expr: &'ast syn::ExprLet) {
524 collect_pattern_idents(&expr.pat, &mut self.shadowed);
525 visit::visit_expr_let(self, expr);
526 }
527
528 fn visit_local(&mut self, local: &'ast syn::Local) {
529 collect_pattern_idents(&local.pat, &mut self.shadowed);
530 visit::visit_local(self, local);
531 }
532}
533
534#[derive(Default)]
535struct AnyTrackedReadVisitor {
536 found: bool,
537}
538
539impl<'ast> Visit<'ast> for AnyTrackedReadVisitor {
540 fn visit_expr_method_call(&mut self, call: &'ast syn::ExprMethodCall) {
541 if is_tracked_read(call) {
542 self.found = true;
543 }
544 visit::visit_expr_method_call(self, call);
545 }
546}
547
548fn is_tracked_read(call: &syn::ExprMethodCall) -> bool {
549 let method = call.method.to_string();
550 (matches!(method.as_str(), "get" | "into_value") && call.args.is_empty())
551 || (method == "with" && call.args.len() == 1)
552}
553
554fn strip_parens(mut expr: &syn::Expr) -> &syn::Expr {
555 while let syn::Expr::Paren(paren) = expr {
556 expr = &paren.expr;
557 }
558 expr
559}
560
561fn stmt_closure(stmt: &syn::Stmt) -> Option<&syn::ExprClosure> {
562 let syn::Stmt::Expr(expr, _) = stmt else {
563 return None;
564 };
565 let syn::Expr::Closure(closure) = strip_parens(expr) else {
566 return None;
567 };
568 Some(closure)
569}
570
571fn clone_aliases(stmts: &[syn::Stmt]) -> HashMap<String, syn::Expr> {
572 let mut aliases = HashMap::new();
573 for stmt in stmts {
574 let syn::Stmt::Local(local) = stmt else {
575 continue;
576 };
577 let mut bindings = HashSet::new();
578 collect_pattern_idents(&local.pat, &mut bindings);
579 let syn::Pat::Ident(alias) = &local.pat else {
580 invalidate_shadowed_aliases(&mut aliases, &bindings);
581 for binding in bindings {
582 aliases.remove(&binding);
583 }
584 continue;
585 };
586 let alias_name = alias.ident.to_string();
587 let Some(init) = &local.init else {
588 invalidate_shadowed_aliases(&mut aliases, &bindings);
589 aliases.remove(&alias_name);
590 continue;
591 };
592 let syn::Expr::MethodCall(call) = strip_parens(&init.expr) else {
593 invalidate_shadowed_aliases(&mut aliases, &bindings);
594 aliases.remove(&alias_name);
595 continue;
596 };
597 if call.method != "clone" || !call.args.is_empty() {
598 invalidate_shadowed_aliases(&mut aliases, &bindings);
599 aliases.remove(&alias_name);
600 continue;
601 }
602 let Some(receiver) = safe_clone_place(&call.receiver) else {
603 invalidate_shadowed_aliases(&mut aliases, &bindings);
604 aliases.remove(&alias_name);
605 continue;
606 };
607 let receiver_was_self = base_ident_of(&receiver).is_some_and(|ident| ident == &alias.ident);
608 let receiver = resolve_place(&receiver, &aliases, &mut Vec::new());
609 let receiver = receiver.filter(|resolved| {
610 base_ident_of(resolved).is_none_or(|ident| ident != &alias.ident) || receiver_was_self
611 });
612 if let Some(receiver) = receiver {
613 invalidate_shadowed_aliases(&mut aliases, &bindings);
614 aliases.insert(alias_name, receiver);
615 } else {
616 invalidate_shadowed_aliases(&mut aliases, &bindings);
617 aliases.remove(&alias.ident.to_string());
618 }
619 }
620 aliases
621}
622
623fn invalidate_shadowed_aliases(
624 aliases: &mut HashMap<String, syn::Expr>,
625 bindings: &HashSet<String>,
626) {
627 let stale = aliases
628 .iter()
629 .filter_map(|(name, expr)| {
630 base_ident_of(expr)
631 .is_some_and(|base| bindings.contains(&base.to_string()))
632 .then_some(name.clone())
633 })
634 .collect::<Vec<_>>();
635 for name in stale {
636 aliases.remove(&name);
637 }
638}
639
640fn safe_clone_place(expr: &syn::Expr) -> Option<syn::Expr> {
641 match strip_parens(expr) {
642 syn::Expr::Path(path) if path.qself.is_none() && !path.path.segments.is_empty() => {
643 Some(syn::Expr::Path(path.clone()))
644 }
645 syn::Expr::Field(field) => {
646 let mut field = field.clone();
647 field.base = Box::new(safe_clone_place(&field.base)?);
648 Some(syn::Expr::Field(field))
649 }
650 _ => None,
651 }
652}
653
654fn resolve_alias(name: &str, aliases: &HashMap<String, syn::Expr>) -> Option<syn::Expr> {
655 let mut stack = vec![name.to_owned()];
656 let expr = aliases.get(name)?;
657 resolve_alias_place(expr, aliases, &mut stack)
658}
659
660fn resolve_alias_place(
661 expr: &syn::Expr,
662 aliases: &HashMap<String, syn::Expr>,
663 stack: &mut Vec<String>,
664) -> Option<syn::Expr> {
665 match strip_parens(expr) {
666 syn::Expr::Path(path) if path.qself.is_none() && path.path.segments.len() == 1 => {
667 let ident = path.path.segments.first()?.ident.to_string();
668 let Some(alias) = aliases.get(&ident) else {
669 return Some(syn::Expr::Path(path.clone()));
670 };
671 if stack.last().is_some_and(|current| current == &ident) {
672 return Some(syn::Expr::Path(path.clone()));
673 }
674 if stack.iter().any(|current| current == &ident) {
675 return None;
676 }
677 stack.push(ident);
678 let resolved = resolve_alias_place(alias, aliases, stack);
679 stack.pop();
680 resolved
681 }
682 _ => Some(strip_parens(expr).clone()),
683 }
684}
685
686fn resolve_place(
687 expr: &syn::Expr,
688 aliases: &HashMap<String, syn::Expr>,
689 stack: &mut Vec<String>,
690) -> Option<syn::Expr> {
691 match strip_parens(expr) {
692 syn::Expr::Path(path) if path.qself.is_none() && path.path.segments.len() == 1 => {
693 let ident = path.path.segments.first()?.ident.to_string();
694 let Some(alias) = aliases.get(&ident) else {
695 return Some(syn::Expr::Path(path.clone()));
696 };
697 if stack.last().is_some_and(|current| current == &ident) {
698 return Some(syn::Expr::Path(path.clone()));
699 }
700 if stack.iter().any(|current| current == &ident) {
701 return None;
702 }
703 stack.push(ident);
704 let resolved = resolve_place(alias, aliases, stack);
705 stack.pop();
706 resolved
707 }
708 syn::Expr::Path(path) if path.qself.is_none() && !path.path.segments.is_empty() => {
709 Some(syn::Expr::Path(path.clone()))
710 }
711 syn::Expr::Field(field) => {
712 let mut field = field.clone();
713 field.base = Box::new(resolve_place(&field.base, aliases, stack)?);
714 Some(syn::Expr::Field(field))
715 }
716 _ => None,
717 }
718}
719
720fn base_ident_of(expr: &syn::Expr) -> Option<&syn::Ident> {
721 match expr {
722 syn::Expr::Path(path) if path.qself.is_none() && path.path.segments.len() == 1 => {
723 Some(&path.path.segments[0].ident)
724 }
725 syn::Expr::MethodCall(call) => base_ident_of(&call.receiver),
726 syn::Expr::Field(field) => base_ident_of(&field.base),
727 syn::Expr::Paren(paren) => base_ident_of(&paren.expr),
728 syn::Expr::Reference(reference) => base_ident_of(&reference.expr),
729 syn::Expr::Try(try_expr) => base_ident_of(&try_expr.expr),
730 _ => None,
731 }
732}
733
734fn collect_pattern_idents(pattern: &syn::Pat, bindings: &mut HashSet<String>) {
735 match pattern {
736 syn::Pat::Ident(ident) => {
737 bindings.insert(ident.ident.to_string());
738 if let Some((_, subpat)) = &ident.subpat {
739 collect_pattern_idents(subpat, bindings);
740 }
741 }
742 syn::Pat::Tuple(tuple) => {
743 for element in &tuple.elems {
744 collect_pattern_idents(element, bindings);
745 }
746 }
747 syn::Pat::TupleStruct(tuple_struct) => {
748 for element in &tuple_struct.elems {
749 collect_pattern_idents(element, bindings);
750 }
751 }
752 syn::Pat::Struct(struct_pattern) => {
753 for field in &struct_pattern.fields {
754 collect_pattern_idents(&field.pat, bindings);
755 }
756 }
757 syn::Pat::Reference(reference) => collect_pattern_idents(&reference.pat, bindings),
758 syn::Pat::Type(typed) => collect_pattern_idents(&typed.pat, bindings),
759 syn::Pat::Or(or_pattern) => {
760 for case in &or_pattern.cases {
761 collect_pattern_idents(case, bindings);
762 }
763 }
764 syn::Pat::Slice(slice) => {
765 for element in &slice.elems {
766 collect_pattern_idents(element, bindings);
767 }
768 }
769 syn::Pat::Paren(paren) => collect_pattern_idents(&paren.pat, bindings),
770 _ => {}
771 }
772}
773
774fn collect_imports(items: &[syn::Item]) -> Vec<UseEntry> {
775 let mut imports = Vec::new();
776 for item in items {
777 if let syn::Item::Use(item_use) = item {
778 flatten_use_tree(&item_use.tree, &[], &item_use.attrs, &mut imports);
779 }
780 }
781 imports
782}
783
784fn flatten_use_tree(
785 tree: &syn::UseTree,
786 prefix: &[syn::Ident],
787 attrs: &[syn::Attribute],
788 imports: &mut Vec<UseEntry>,
789) {
790 match tree {
791 syn::UseTree::Path(path) => {
792 let mut next = prefix.to_vec();
793 next.push(path.ident.clone());
794 flatten_use_tree(&path.tree, &next, attrs, imports);
795 }
796 syn::UseTree::Name(name) => {
797 let mut original = prefix.to_vec();
798 original.push(name.ident.clone());
799 imports.push(UseEntry {
800 original,
801 bound: name.ident.clone(),
802 attrs: attrs.to_vec(),
803 });
804 }
805 syn::UseTree::Rename(rename) => {
806 let mut original = prefix.to_vec();
807 original.push(rename.ident.clone());
808 imports.push(UseEntry {
809 original,
810 bound: rename.rename.clone(),
811 attrs: attrs.to_vec(),
812 });
813 }
814 syn::UseTree::Group(group) => {
815 for tree in &group.items {
816 flatten_use_tree(tree, prefix, attrs, imports);
817 }
818 }
819 syn::UseTree::Glob(_) => {}
820 }
821}
822
823fn import_key(facade: &syn::Path, attrs: &[syn::Attribute]) -> String {
824 quote!(#facade #(#attrs)*).to_string()
825}
826
827fn facade_key(facade: &syn::Path) -> String {
828 quote!(#facade).to_string()
829}
830
831fn has_deps_import(imports: &[UseEntry], facade: &syn::Path, attrs: &[syn::Attribute]) -> bool {
832 let key = import_key(facade, attrs);
833 let facade_path_key = facade_key(facade);
834 imports.iter().any(|entry| {
835 entry.bound == "deps"
836 && entry.original.last().is_some_and(|ident| ident == "deps")
837 && facade_from_segments(&entry.original).is_some_and(|entry_facade| {
838 facade_key(&entry_facade) == facade_path_key
839 && (entry.attrs.is_empty() || import_key(&entry_facade, &entry.attrs) == key)
840 })
841 })
842}
843
844#[cfg(test)]
845mod tests {
846 use super::*;
847
848 fn rewrite(source: &str) -> String {
849 let file = syn::parse_file(source).expect("test source must parse");
850 prettyplease::unparse(&Rule.rewrite(file))
851 }
852
853 fn compact(source: &str) -> String {
854 source.chars().filter(|ch| !ch.is_whitespace()).collect()
855 }
856
857 #[test]
858 fn hook_metadata_covers_all_supported_arities() {
859 let actual: Vec<_> = HOOK_SPECS
860 .iter()
861 .map(|spec| {
862 (
863 spec.name,
864 spec.callback_index,
865 spec.deps_index,
866 spec.omitted_arity,
867 spec.explicit_arity,
868 )
869 })
870 .collect();
871 assert_eq!(
872 actual,
873 vec![
874 ("use_effect", 0, 1, 1, 2),
875 ("use_layout_effect", 0, 1, 1, 2),
876 ("use_retained_effect", 0, 1, 1, 2),
877 ("use_retained_layout_effect", 0, 1, 1, 2),
878 ("use_memo", 0, 1, 1, 2),
879 ("use_callback", 0, 1, 1, 2),
880 ("use_callback_with", 0, 1, 1, 2),
881 ("use_resource", 0, 1, 1, 2),
882 ("use_resource_with_key", 1, 2, 2, 3),
883 ]
884 );
885 }
886
887 #[test]
888 fn keyed_resource_adds_or_converts_third_dependency_argument() {
889 let output = compact(&rewrite(
890 r#"
891use reinhardt_pages::reactive::use_resource_with_key;
892fn view(signal: Signal<i32>) {
893 let _ = use_resource_with_key("a", || async { Ok::<_, ()>(1) });
894 let _ = use_resource_with_key("b", || async { Ok::<_, ()>(2) }, ());
895 let _ = use_resource_with_key("c", || async { Ok::<_, ()>(3) }, (signal.clone(),));
896}
897"#,
898 ));
899 assert!(output.contains("use_resource_with_key(\"a\",||async{Ok::<_,()>(1)},deps![]);"));
900 assert!(output.contains("use_resource_with_key(\"b\",||async{Ok::<_,()>(2)},deps![]);"));
901 assert!(
902 output.contains("use_resource_with_key(\"c\",||async{Ok::<_,()>(3)},deps![signal]);")
903 );
904 }
905
906 #[test]
907 fn explicit_dependency_matrix_converts_empty_single_and_multiple_tuples() {
908 let output = compact(&rewrite(
909 r#"
910use reinhardt_pages::reactive::hooks::use_effect;
911fn view(a: Signal<i32>, b: Signal<i32>) {
912 let _ = use_effect(|| {}, ());
913 let _ = use_effect(|| {}, (a.clone(),));
914 let _ = use_effect(|| {}, (a.clone(), b.clone()));
915}
916"#,
917 ));
918 assert!(output.contains("use_effect(||{},deps![]);"));
919 assert!(output.contains("use_effect(||{},deps![a]);"));
920 assert!(output.contains("use_effect(||{},deps![a,b]);"));
921 }
922
923 #[test]
924 fn block_clone_alias_maps_back_to_outer_dependency() {
925 let output = compact(&rewrite(
926 r#"
927use reinhardt_pages::reactive::hooks::use_effect;
928fn view(original: Signal<i32>) {
929 let _ = use_effect({
930 let alias = original.clone();
931 move || { let _ = alias.get(); }
932 });
933}
934"#,
935 ));
936 assert!(output.contains("deps![original]"));
937 assert!(!output.contains("deps![alias]"));
938 }
939
940 #[test]
941 fn unsafe_omitted_forms_keep_review_marker() {
942 let output = compact(&rewrite(
943 r#"
944use reinhardt_pages::reactive::hooks::use_effect;
945fn view(count: Signal<i32>) {
946 let _ = use_effect(move || { let _ = count.get(); });
947 let _ = use_effect(build_callback());
948 let _ = use_effect({
949 let alias = count.clone();
950 move || Some(move || { let _ = alias.get(); })
951 });
952}
953"#,
954 ));
955 assert_eq!(output.matches("compile_error!").count(), 3);
956 assert_eq!(output.matches("deps![count]").count(), 3);
957 assert!(!output.contains("deps![alias]"));
958 }
959
960 #[test]
961 fn imports_deps_in_each_inline_module_scope() {
962 let output = rewrite(
963 r#"
964use reinhardt_pages::reactive::hooks::use_effect;
965fn root() { let _ = use_effect(|| {}); }
966mod nested {
967 use reinhardt_pages::reactive::hooks::use_effect;
968 fn view() { let _ = use_effect(|| {}); }
969}
970"#,
971 );
972 assert_eq!(output.matches("use reinhardt_pages::deps;").count(), 2);
973 }
974
975 #[test]
976 fn qualified_hook_preserves_full_facade_path() {
977 let output = compact(&rewrite(
978 r#"
979fn view() { let _ = reinhardt::pages::use_effect(|| {}); }
980"#,
981 ));
982 assert!(output.contains("usereinhardt::pages::deps;"));
983 assert!(!output.contains("usereinhardt::deps;"));
984 }
985
986 #[test]
987 fn renamed_deps_import_does_not_satisfy_bare_macro_import() {
988 let output = rewrite(
989 r#"
990use reinhardt_pages::deps as hook_deps;
991use reinhardt_pages::use_effect;
992fn view() { let _ = use_effect(|| {}); }
993"#,
994 );
995 assert!(output.contains("use reinhardt_pages::deps as hook_deps;"));
996 assert!(output.contains("use reinhardt_pages::deps;"));
997 }
998
999 #[test]
1000 fn clone_alias_chain_resolves_to_the_outer_dependency() {
1001 let output = compact(&rewrite(
1002 r#"
1003use reinhardt_pages::use_effect;
1004fn view(count: Signal<i32>) {
1005 let _ = use_effect({
1006 let first = count.clone();
1007 let second = first.clone();
1008 move || { let _ = second.get(); }
1009 });
1010}
1011"#,
1012 ));
1013 assert!(output.contains("deps![count]"));
1014 assert!(!output.contains("compile_error!"));
1015 }
1016
1017 #[test]
1018 fn clone_alias_chain_accepts_a_field_place() {
1019 let output = compact(&rewrite(
1020 r#"
1021use reinhardt_pages::use_effect;
1022fn view(state: State) {
1023 let _ = use_effect({
1024 let first = state.count.clone();
1025 let second = first.clone();
1026 move || { let _ = second.get(); }
1027 });
1028}
1029"#,
1030 ));
1031 assert!(output.contains("deps![state.count]"));
1032 assert!(!output.contains("compile_error!"));
1033 }
1034
1035 #[test]
1036 fn same_name_clone_shadow_resolves_through_the_previous_alias() {
1037 let output = compact(&rewrite(
1038 r#"
1039use reinhardt_pages::use_effect;
1040fn view(count: Signal<i32>) {
1041 let _ = use_effect({
1042 let first = count.clone();
1043 let first = first.clone();
1044 move || { let _ = first.get(); }
1045 });
1046}
1047"#,
1048 ));
1049 assert!(output.contains("deps![count]"));
1050 assert!(!output.contains("deps![first]"));
1051 assert!(!output.contains("compile_error!"));
1052 }
1053
1054 #[test]
1055 fn clone_aliases_snapshot_before_later_shadowing() {
1056 let output = compact(&rewrite(
1057 r#"
1058use reinhardt_pages::use_effect;
1059fn view(count: Signal<i32>, other: Signal<i32>) {
1060 let _ = use_effect({
1061 let first = count.clone();
1062 let second = first.clone();
1063 let first = other.clone();
1064 move || { let _ = second.get(); }
1065 });
1066}
1067"#,
1068 ));
1069 assert!(output.contains("deps![count]"));
1070 assert!(!output.contains("},deps![other]"));
1071 assert!(!output.contains("compile_error!"));
1072 }
1073
1074 #[test]
1075 fn clone_alias_dependents_are_invalidated_by_base_shadowing() {
1076 let output = compact(&rewrite(
1077 r#"
1078use reinhardt_pages::use_effect;
1079fn view(state: Signal<i32>, other: Signal<i32>) {
1080 let _ = use_effect({
1081 let first = state.clone();
1082 let second = first.clone();
1083 let state = other.clone();
1084 move || { let _ = second.get(); }
1085 });
1086}
1087"#,
1088 ));
1089 assert_eq!(output.matches("compile_error!").count(), 1);
1090 assert!(!output.contains("deps![other]"));
1091 }
1092
1093 #[test]
1094 fn field_alias_cycles_keep_the_review_marker() {
1095 let output = compact(&rewrite(
1096 r#"
1097use reinhardt_pages::use_effect;
1098fn view() {
1099 let _ = use_effect({
1100 let first = second.field.clone();
1101 let second = first.clone();
1102 move || { let _ = first.get(); }
1103 });
1104}
1105"#,
1106 ));
1107 assert_eq!(output.matches("compile_error!").count(), 1);
1108 assert!(!output.contains("deps![second.field]"));
1109 }
1110
1111 #[test]
1112 fn field_clone_aliases_snapshot_before_base_shadowing() {
1113 let output = compact(&rewrite(
1114 r#"
1115use reinhardt_pages::use_effect;
1116fn view(state: State, other: Signal<i32>) {
1117 let _ = use_effect({
1118 let first = state.count.clone();
1119 let state = other.clone();
1120 move || { let _ = first.get(); }
1121 });
1122}
1123"#,
1124 ));
1125 assert!(!output.contains("deps![other.count]"));
1126 assert_eq!(output.matches("compile_error!").count(), 1);
1127 }
1128
1129 #[test]
1130 fn side_effecting_or_indexed_clone_receivers_keep_review_markers() {
1131 let output = compact(&rewrite(
1132 r#"
1133use reinhardt_pages::use_effect;
1134fn view(signals: Vec<Signal<i32>>) {
1135 let _ = use_effect({
1136 let signal = make_signal().clone();
1137 move || { let _ = signal.get(); }
1138 });
1139 let _ = use_effect({
1140 let signal = signals[0].clone();
1141 move || { let _ = signal.get(); }
1142 });
1143}
1144"#,
1145 ));
1146 assert_eq!(output.matches("compile_error!").count(), 2);
1147 }
1148
1149 #[test]
1150 fn cyclic_clone_aliases_keep_the_review_marker() {
1151 let output = compact(&rewrite(
1152 r#"
1153use reinhardt_pages::use_effect;
1154fn view() {
1155 let _ = use_effect({
1156 let first = second.clone();
1157 let second = first.clone();
1158 move || { let _ = second.get(); }
1159 });
1160}
1161"#,
1162 ));
1163 assert_eq!(output.matches("compile_error!").count(), 1);
1164 }
1165
1166 #[test]
1167 fn module_alias_hook_call_restores_the_full_facade() {
1168 let output = compact(&rewrite(
1169 r#"
1170use reinhardt_pages::reactive::hooks as hooks;
1171fn view() { let _ = hooks::use_effect(|| {}); }
1172"#,
1173 ));
1174 assert!(output.contains("usereinhardt_pages::deps;"));
1175 assert!(output.contains("hooks::use_effect(||{},deps![]);"));
1176 }
1177
1178 #[test]
1179 fn module_name_and_self_qualified_hook_calls_restore_the_facade() {
1180 let output = compact(&rewrite(
1181 r#"
1182use reinhardt_pages::reactive::hooks;
1183fn one() { let _ = hooks::use_effect(|| {}); }
1184fn two() { let _ = self::hooks::use_effect(|| {}); }
1185"#,
1186 ));
1187 assert_eq!(output.matches("usereinhardt_pages::deps;").count(), 1);
1188 assert_eq!(output.matches("deps![]").count(), 2);
1189 }
1190
1191 #[test]
1192 fn generated_deps_import_inherits_hook_import_cfg() {
1193 let output = rewrite(
1194 r#"
1195#[cfg(wasm)]
1196use reinhardt_pages::reactive::hooks as hooks;
1197fn view() { let _ = hooks::use_effect(|| {}); }
1198"#,
1199 );
1200 assert!(output.contains("#[cfg(wasm)]\nuse reinhardt_pages::deps;"));
1201 }
1202
1203 #[test]
1204 fn generated_deps_imports_deduplicate_by_cfg_and_path() {
1205 let output = rewrite(
1206 r#"
1207#[cfg(wasm)]
1208use reinhardt_pages::reactive::hooks as wasm_hooks;
1209#[cfg(native)]
1210use reinhardt_pages::reactive::hooks as native_hooks;
1211fn view() {
1212 let _ = wasm_hooks::use_effect(|| {});
1213 let _ = wasm_hooks::use_layout_effect(|| {});
1214 let _ = native_hooks::use_effect(|| {});
1215}
1216"#,
1217 );
1218 assert_eq!(
1219 output
1220 .matches("#[cfg(wasm)]\nuse reinhardt_pages::deps;")
1221 .count(),
1222 1
1223 );
1224 assert_eq!(
1225 output
1226 .matches("#[cfg(native)]\nuse reinhardt_pages::deps;")
1227 .count(),
1228 1
1229 );
1230 }
1231
1232 #[test]
1233 fn unconditional_deps_import_supersedes_cfg_variant() {
1234 let output = rewrite(
1235 r#"
1236#[cfg(feature = "foo")]
1237use foo::reactive::hooks as wasm_hooks;
1238use foo::reactive::hooks as native_hooks;
1239fn view(signal: Signal<i32>) {
1240 let _ = wasm_hooks::use_effect({
1241 let signal = signal.clone();
1242 move || { let _ = signal.get(); }
1243 });
1244 let _ = native_hooks::use_effect({
1245 let signal = signal.clone();
1246 move || { let _ = signal.get(); }
1247 });
1248}
1249"#,
1250 );
1251 assert_eq!(output.matches("use foo::deps;").count(), 1);
1252 assert!(!output.contains("#[cfg(feature = \"foo\")]\nuse foo::deps;"));
1253 }
1254
1255 #[test]
1256 fn same_bound_hook_imports_generate_deps_for_each_cfg_path() {
1257 let output = rewrite(
1258 r#"
1259#[cfg(feature = "foo")]
1260use foo::reactive::hooks::use_effect;
1261#[cfg(not(feature = "foo"))]
1262use bar::reactive::hooks::use_effect;
1263fn view(signal: Signal<i32>) {
1264 let _ = use_effect({
1265 let signal = signal.clone();
1266 move || { let _ = signal.get(); }
1267 });
1268}
1269"#,
1270 );
1271 assert!(output.contains("#[cfg(feature = \"foo\")]\nuse foo::deps;"));
1272 assert!(output.contains("#[cfg(not(feature = \"foo\"))]\nuse bar::deps;"));
1273 assert_eq!(output.matches("deps![signal]").count(), 1);
1274 }
1275
1276 #[test]
1277 fn ambiguous_same_bound_hook_specs_keep_a_review_marker() {
1278 let output = rewrite(
1279 r#"
1280#[cfg(feature = "foo")]
1281use foo::reactive::hooks::use_effect as hook;
1282#[cfg(not(feature = "foo"))]
1283use bar::reactive::hooks::use_resource as hook;
1284fn view(signal: Signal<i32>) {
1285 let _ = hook({
1286 let signal = signal.clone();
1287 move || { let _ = signal.get(); }
1288 });
1289}
1290"#,
1291 );
1292 assert!(!output.contains("use foo::deps;"));
1293 assert!(!output.contains("use bar::deps;"));
1294 assert!(output.contains("compile_error!"));
1295 }
1296
1297 #[test]
1298 fn shadowed_clone_alias_keeps_the_review_marker() {
1299 let output = compact(&rewrite(
1300 r#"
1301use reinhardt_pages::use_effect;
1302fn view(count: Signal<i32>) {
1303 let _ = use_effect({
1304 let alias = count.clone();
1305 let alias = make_signal();
1306 move || { let _ = alias.get(); }
1307 });
1308}
1309"#,
1310 ));
1311 assert_eq!(output.matches("compile_error!").count(), 1);
1312 assert!(!output.contains(",deps![count]);"));
1313 }
1314
1315 #[test]
1316 fn closure_local_shadow_keeps_the_review_marker() {
1317 let output = compact(&rewrite(
1318 r#"
1319use reinhardt_pages::use_effect;
1320fn view(count: Signal<i32>) {
1321 let _ = use_effect({
1322 let alias = count.clone();
1323 move || {
1324 let alias = make_signal();
1325 let _ = alias.get();
1326 }
1327 });
1328}
1329"#,
1330 ));
1331 assert_eq!(output.matches("compile_error!").count(), 1);
1332 assert!(!output.contains(",deps![count]);"));
1333 }
1334
1335 #[test]
1336 fn closure_destructure_shadow_keeps_the_review_marker() {
1337 let output = compact(&rewrite(
1338 r#"
1339use reinhardt_pages::use_effect;
1340fn view(count: Signal<i32>) {
1341 let _ = use_effect({
1342 let alias = count.clone();
1343 move || {
1344 let (alias, _) = make_pair();
1345 let _ = alias.get();
1346 }
1347 });
1348}
1349"#,
1350 ));
1351 assert_eq!(output.matches("compile_error!").count(), 1);
1352 assert!(!output.contains(",deps![count]);"));
1353 }
1354
1355 #[test]
1356 fn control_flow_pattern_shadow_keeps_the_review_marker() {
1357 let output = compact(&rewrite(
1358 r#"
1359use reinhardt_pages::use_effect;
1360fn view(count: Signal<i32>) {
1361 let _ = use_effect({
1362 let alias = count.clone();
1363 move || {
1364 for alias in make_iter() { let _ = alias.get(); }
1365 if let Some(alias) = make_option() { let _ = alias.get(); }
1366 match make_option() {
1367 Some(alias) => { let _ = alias.get(); }
1368 None => {}
1369 }
1370 }
1371 });
1372}
1373"#,
1374 ));
1375 assert_eq!(output.matches("compile_error!").count(), 1);
1376 assert!(!output.contains(",deps![count]);"));
1377 }
1378
1379 #[test]
1380 fn relative_reexport_does_not_generate_an_unresolved_deps_import() {
1381 let output = rewrite(
1382 r#"
1383mod parent {
1384 use reinhardt_pages::use_effect;
1385 mod child {
1386 use super::use_effect;
1387 fn view(signal: Signal<i32>) {
1388 let _ = use_effect(|| { let _ = signal.get(); });
1389 }
1390 }
1391}
1392"#,
1393 );
1394 assert!(!output.contains("use super::deps;"));
1395 assert!(output.contains("compile_error!"));
1396 }
1397
1398 #[test]
1399 fn relative_reexport_tuple_is_replaced_with_a_review_marker() {
1400 let output = compact(&rewrite(
1401 r#"
1402mod parent {
1403 use reinhardt_pages::use_effect;
1404 mod child {
1405 use super::use_effect;
1406 fn view(signal: Signal<i32>) {
1407 let _ = use_effect(|| {}, (signal.clone(),));
1408 }
1409 }
1410}
1411"#,
1412 ));
1413 assert!(!output.contains("use super::deps;"));
1414 assert!(!output.contains("(signal.clone(),)"));
1415 assert!(output.contains("compile_error!"));
1416 }
1417
1418 #[test]
1419 fn unresolved_bare_hook_with_explicit_dependencies_is_not_rewritten() {
1420 let output = compact(&rewrite(
1421 r#"
1422fn use_effect<F, D>(callback: F, dependencies: D) {}
1423fn view(signal: Signal<i32>) {
1424 use_effect(|| {}, (signal.clone(),));
1425}
1426"#,
1427 ));
1428
1429 assert!(output.contains("use_effect(||{},(signal.clone(),));"));
1430 assert!(!output.contains("use reinhardt_pages::deps;"));
1431 }
1432
1433 #[test]
1434 fn unresolved_bare_hook_with_omitted_dependencies_is_not_rewritten() {
1435 let output = compact(&rewrite(
1436 r#"
1437fn use_effect<F>(callback: F) {}
1438fn view() {
1439 use_effect(|| {});
1440}
1441"#,
1442 ));
1443
1444 assert!(output.contains("use_effect(||{});"));
1445 assert!(!output.contains("reinhardt_pages::deps"));
1446 }
1447
1448 #[test]
1449 fn local_module_hook_does_not_generate_an_unresolved_deps_import() {
1450 let output = rewrite(
1451 r#"
1452mod custom {}
1453use custom::reactive::hooks::use_effect;
1454fn view(signal: Signal<i32>) {
1455 let _ = use_effect(|| { let _ = signal.get(); });
1456}
1457"#,
1458 );
1459 assert!(!output.contains("use custom::deps;"));
1460 assert!(output.contains("compile_error!"));
1461 }
1462
1463 #[test]
1464 fn crate_local_module_hook_does_not_generate_an_unresolved_deps_import() {
1465 let output = rewrite(
1466 r#"
1467mod custom {}
1468use crate::custom::reactive::hooks::use_effect;
1469fn view(signal: Signal<i32>) {
1470 let _ = use_effect(|| { let _ = signal.get(); });
1471}
1472"#,
1473 );
1474 assert!(!output.contains("use crate::custom::deps;"));
1475 assert!(output.contains("compile_error!"));
1476 }
1477
1478 #[test]
1479 fn nested_crate_local_module_hook_does_not_generate_an_unresolved_deps_import() {
1480 let output = rewrite(
1481 r#"
1482mod parent {
1483 mod custom {}
1484 use crate::parent::custom::reactive::hooks::use_effect;
1485 fn view(signal: Signal<i32>) {
1486 let _ = use_effect(|| { let _ = signal.get(); });
1487 }
1488}
1489"#,
1490 );
1491 assert!(!output.contains("use crate::parent::custom::deps;"));
1492 assert!(output.contains("compile_error!"));
1493 }
1494
1495 #[test]
1496 fn function_local_hook_import_does_not_fall_back_to_reinhardt_pages() {
1497 let output = rewrite(
1498 r#"
1499mod custom {}
1500fn view(signal: Signal<i32>) {
1501 use custom::reactive::hooks::use_effect;
1502 let _ = use_effect({
1503 let signal = signal.clone();
1504 move || { let _ = signal.get(); }
1505 });
1506 let _ = use_effect(|| {}, (signal.clone(),));
1507}
1508"#,
1509 );
1510 assert!(!output.contains("use reinhardt_pages::deps;"));
1511 assert!(!output.contains("use custom::deps;"));
1512 assert_eq!(output.matches("compile_error!").count(), 2);
1513 assert!(!output.contains("(signal.clone(),)"));
1514 }
1515}