1#![allow(dead_code)]
45
46use std::collections::{BTreeMap, BTreeSet};
47
48use syn::{Item, Path, UseTree};
49
50use crate::types::{EmitError, TypePath};
51
52#[derive(Debug, Clone, Default)]
54pub(crate) struct FileImports {
55 pub(crate) simple: BTreeMap<String, TypePath>,
58 pub(crate) globs: BTreeSet<TypePath>,
63}
64
65impl FileImports {
66 pub(crate) fn resolve_ident(&self, ident: &str) -> Option<TypePath> {
69 self.simple.get(ident).cloned()
70 }
71}
72
73pub(crate) fn parse_imports(file: &syn::File) -> FileImports {
76 let mut out = FileImports::default();
77 imports_from_items(&file.items, &mut out);
78 out
79}
80
81fn imports_from_items(items: &[Item], out: &mut FileImports) {
85 for item in items {
86 if let Item::Use(item_use) = item {
87 walk_use_tree(&item_use.tree, &mut Vec::new(), out);
88 }
89 }
90}
91
92#[derive(Debug, Clone, Default)]
97pub struct ModuleImports {
98 by_module: BTreeMap<Vec<String>, FileImports>,
99}
100
101impl ModuleImports {
102 pub(crate) fn get(&self, module: &[String]) -> Option<&FileImports> {
104 self.by_module.get(module)
105 }
106
107 pub fn merge(&mut self, other: ModuleImports) {
111 for (module, imports) in other.by_module {
112 self.by_module.entry(module).or_insert(imports);
113 }
114 }
115}
116
117pub(crate) fn collect_module_imports(file: &syn::File, prefix: &[String], out: &mut ModuleImports) {
122 collect_items_into(&file.items, prefix, out);
123}
124
125fn collect_items_into(items: &[Item], prefix: &[String], out: &mut ModuleImports) {
126 let entry = out.by_module.entry(prefix.to_vec()).or_default();
127 imports_from_items(items, entry);
128 for item in items {
129 if let Item::Mod(m) = item
130 && let Some((_, inner)) = &m.content
131 {
132 let mut sub = prefix.to_vec();
133 sub.push(m.ident.to_string());
134 collect_items_into(inner, &sub, out);
135 }
136 }
137}
138
139const MAX_IMPORT_DEPTH: u8 = 16;
142
143#[derive(Debug, Clone, PartialEq, Eq)]
145pub enum Resolution {
146 Resolved(TypePath),
148 NotInPool,
151 Ambiguous(Vec<TypePath>),
156}
157
158pub fn resolve_reference(
170 segments: &[String],
171 module: &[String],
172 pool: &BTreeMap<TypePath, syn::Item>,
173 imports: &ModuleImports,
174) -> Resolution {
175 match segments {
176 [] => Resolution::NotInPool,
177 [only] => resolve_bare_ident(only, module, pool, imports, 0),
178 multi => match absolutize(multi, module, pool, imports) {
184 Some(abs) => match TypePath::new(abs) {
185 Ok(tp) if pool.contains_key(&tp) => Resolution::Resolved(tp),
186 _ => Resolution::NotInPool,
189 },
190 None => Resolution::NotInPool,
191 },
192 }
193}
194
195fn resolve_bare_ident(
197 ident: &str,
198 module: &[String],
199 pool: &BTreeMap<TypePath, syn::Item>,
200 imports: &ModuleImports,
201 depth: u8,
202) -> Resolution {
203 if let Some(file_imports) = imports.get(module)
206 && let Some(target) = file_imports.resolve_ident(ident)
207 {
208 return resolve_import_target(target.segments(), module, pool, imports, depth);
209 }
210 let mut same_module = module.to_vec();
212 same_module.push(ident.to_string());
213 if let Ok(path) = TypePath::new(same_module)
214 && pool.contains_key(&path)
215 {
216 return Resolution::Resolved(path);
217 }
218 terminal_resolution(ident, pool, module.first().map(String::as_str))
222}
223
224fn resolve_import_target(
228 target: &[String],
229 in_module: &[String],
230 pool: &BTreeMap<TypePath, syn::Item>,
231 imports: &ModuleImports,
232 depth: u8,
233) -> Resolution {
234 let Some(leaf) = target.last().cloned() else {
236 return Resolution::NotInPool;
237 };
238 let Some(abs) = absolutize(target, in_module, pool, imports) else {
245 return terminal_resolution(&leaf, pool, in_module.first().map(String::as_str));
246 };
247 let Ok(abs_path) = TypePath::new(abs.clone()) else {
248 return Resolution::NotInPool;
249 };
250 if pool.contains_key(&abs_path) {
252 return Resolution::Resolved(abs_path);
253 }
254 if depth < MAX_IMPORT_DEPTH && abs.len() >= 2 {
257 let reexport_module = &abs[..abs.len() - 1];
258 match resolve_bare_ident(&leaf, reexport_module, pool, imports, depth + 1) {
259 resolved @ (Resolution::Resolved(_) | Resolution::Ambiguous(_)) => return resolved,
261 Resolution::NotInPool => {}
263 }
264 }
265 terminal_resolution(&leaf, pool, in_module.first().map(String::as_str))
266}
267
268fn absolutize(
272 target: &[String],
273 in_module: &[String],
274 pool: &BTreeMap<TypePath, syn::Item>,
275 imports: &ModuleImports,
276) -> Option<Vec<String>> {
277 let (first, rest) = target.split_first()?;
278 match first.as_str() {
279 "crate" => Some([&in_module[..1.min(in_module.len())], rest].concat()),
282 "self" => Some([in_module, rest].concat()),
283 "super" => {
284 let parent = in_module.split_last().map(|(_, p)| p)?;
286 Some([parent, rest].concat())
287 }
288 _ => {
289 let mut candidate_module = in_module.to_vec();
295 candidate_module.push(first.clone());
296 if is_known_module(&candidate_module, pool, imports) {
297 return Some([in_module, target].concat());
298 }
299 if is_known_module(std::slice::from_ref(first), pool, imports) {
304 return Some(target.to_vec());
305 }
306 None
307 }
308 }
309}
310
311fn is_known_module(prefix: &[String], pool: &BTreeMap<TypePath, syn::Item>, imports: &ModuleImports) -> bool {
314 if imports.get(prefix).is_some() {
315 return true;
316 }
317 pool.keys().any(|k| {
318 let segs = k.segments();
319 segs.len() > prefix.len() && &segs[..prefix.len()] == prefix
320 })
321}
322
323fn terminal_resolution(ident: &str, pool: &BTreeMap<TypePath, syn::Item>, home_root: Option<&str>) -> Resolution {
340 let matches: Vec<TypePath> = pool.keys().filter(|p| p.terminal() == ident).cloned().collect();
341 let home: Vec<TypePath> = match home_root {
342 Some(root) => {
343 matches.iter().filter(|p| p.segments().first().map(String::as_str) == Some(root)).cloned().collect()
344 }
345 None => Vec::new(),
346 };
347 let candidates = if home.is_empty() { matches } else { home };
348 match candidates.len() {
349 0 => Resolution::NotInPool,
350 1 => Resolution::Resolved(candidates.into_iter().next().expect("len checked")),
351 _ => Resolution::Ambiguous(candidates),
352 }
353}
354
355fn walk_use_tree(tree: &UseTree, prefix: &mut Vec<String>, out: &mut FileImports) {
358 match tree {
359 UseTree::Path(p) => {
360 prefix.push(p.ident.to_string());
361 walk_use_tree(&p.tree, prefix, out);
362 prefix.pop();
363 }
364 UseTree::Name(name) => {
365 let ident = name.ident.to_string();
367 let mut segments = prefix.clone();
368 segments.push(ident.clone());
369 if let Ok(path) = TypePath::new(segments) {
370 out.simple.insert(ident, path);
371 }
372 }
373 UseTree::Rename(rename) => {
374 let canonical_ident = rename.ident.to_string();
377 let local_ident = rename.rename.to_string();
378 let mut segments = prefix.clone();
379 segments.push(canonical_ident);
380 if let Ok(path) = TypePath::new(segments) {
381 out.simple.insert(local_ident, path);
382 }
383 }
384 UseTree::Glob(_) => {
385 if !prefix.is_empty()
389 && let Ok(path) = TypePath::new(prefix.clone())
390 {
391 out.globs.insert(path);
392 }
393 }
394 UseTree::Group(group) => {
395 for inner in &group.items {
396 walk_use_tree(inner, prefix, out);
397 }
398 }
399 }
400}
401
402fn path_segments(path: &Path) -> Vec<String> {
406 path.segments.iter().map(|seg| seg.ident.to_string()).collect()
407}
408
409pub(crate) fn canonicalize(
418 path: &Path,
419 imports: &FileImports,
420 referenced_by: &TypePath,
421) -> Result<TypePath, EmitError> {
422 let mut segments = path_segments(path);
423
424 if segments.is_empty() {
425 return Err(EmitError::UnresolvedReference {
426 name: "<empty path>".to_string(),
427 referenced_by: referenced_by.clone(),
428 });
429 }
430
431 if segments.len() > 1 {
433 if segments.first().map(String::as_str) == Some("crate") {
434 segments.remove(0);
435 }
436 return TypePath::new(segments).map_err(|_| EmitError::UnresolvedReference {
437 name: "<empty after crate:: stripped>".to_string(),
438 referenced_by: referenced_by.clone(),
439 });
440 }
441
442 let ident = &segments[0];
444 if let Some(path) = imports.resolve_ident(ident) {
445 return Ok(path);
446 }
447
448 if !imports.globs.is_empty() {
452 let globs_rendered: Vec<String> =
453 imports.globs.iter().map(|p| format!("use {}::*;", p.segments().join("::"))).collect();
454 return Err(EmitError::UnresolvedReference {
455 name: format!(
456 "`{ident}` (may come from {}; qualify the reference (e.g., chrono::{ident}) or replace the glob with \
457 an explicit `use`)",
458 globs_rendered.join(", ")
459 ),
460 referenced_by: referenced_by.clone(),
461 });
462 }
463
464 TypePath::new(vec![ident.clone()])
469 .map_err(|_| EmitError::UnresolvedReference { name: ident.clone(), referenced_by: referenced_by.clone() })
470}
471
472#[cfg(test)]
473mod tests {
474 use super::*;
475
476 fn parse_file(src: &str) -> syn::File {
477 syn::parse_str(src).expect("parse file")
478 }
479
480 fn rooted(segments: &[&str]) -> Vec<String> {
482 std::iter::once("crate".to_string()).chain(segments.iter().map(|s| (*s).to_string())).collect()
483 }
484
485 fn tp(segments: &[&str]) -> TypePath {
487 TypePath::new(rooted(segments)).expect("non-empty")
488 }
489
490 fn tp_in(segments: &[&str]) -> TypePath {
492 TypePath::new(segments.iter().map(|s| (*s).to_string()).collect()).expect("non-empty")
493 }
494
495 fn parse_path(src: &str) -> Path {
496 syn::parse_str(src).expect("parse path")
497 }
498
499 fn pool_from(entries: &[(&[&str], &str)]) -> BTreeMap<TypePath, syn::Item> {
501 entries
502 .iter()
503 .map(|(segs, src)| {
504 (TypePath::new(rooted(segs)).expect("non-empty"), syn::parse_str::<syn::Item>(src).expect("parse item"))
505 })
506 .collect()
507 }
508
509 fn with_root(
512 mut pool: BTreeMap<TypePath, syn::Item>,
513 entries: &[(&[&str], &str)],
514 ) -> BTreeMap<TypePath, syn::Item> {
515 for (segs, src) in entries {
516 let key = TypePath::new(segs.iter().map(|s| (*s).to_string()).collect()).expect("non-empty");
517 pool.insert(key, syn::parse_str::<syn::Item>(src).expect("parse item"));
518 }
519 pool
520 }
521
522 fn imports_from(entries: &[(&[&str], &str)]) -> ModuleImports {
523 let mut imports = ModuleImports::default();
524 for (module, src) in entries {
525 let file = parse_file(src);
526 collect_module_imports(&file, &rooted(module), &mut imports);
527 }
528 imports
529 }
530
531 fn imports_in(entries: &[(&[&str], &str)]) -> ModuleImports {
533 let mut imports = ModuleImports::default();
534 for (module, src) in entries {
535 let file = parse_file(src);
536 let prefix: Vec<String> = module.iter().map(|s| (*s).to_string()).collect();
537 collect_module_imports(&file, &prefix, &mut imports);
538 }
539 imports
540 }
541
542 fn seg(parts: &[&str]) -> Vec<String> {
544 parts.iter().map(|s| (*s).to_string()).collect()
545 }
546
547 fn md(parts: &[&str]) -> Vec<String> {
549 rooted(parts)
550 }
551
552 #[test]
555 fn reference_resolves_relative_reexport_chain() {
556 let pool = pool_from(&[
565 (&["schema", "vault", "VaultConfig"], "pub struct VaultConfig { pub template: String }"),
566 (&["vault", "VaultConfig"], "pub struct VaultConfig { pub enabled: bool }"),
567 ]);
568 let imports = imports_from(&[
569 (&["api", "v1", "vault"], "use crate::schema::VaultConfig;"),
570 (&["schema"], "pub use vault::VaultConfig;"),
571 ]);
572 let r = resolve_reference(&seg(&["VaultConfig"]), &md(&["api", "v1", "vault"]), &pool, &imports);
573 assert_eq!(r, Resolution::Resolved(tp(&["schema", "vault", "VaultConfig"])), "got {r:?}");
574 }
575
576 #[test]
577 fn reference_without_disambiguating_use_is_ambiguous() {
578 let pool = pool_from(&[
581 (&["schema", "vault", "VaultConfig"], "pub struct VaultConfig { pub template: String }"),
582 (&["vault", "VaultConfig"], "pub struct VaultConfig { pub enabled: bool }"),
583 ]);
584 let imports = ModuleImports::default();
585 let r = resolve_reference(&seg(&["VaultConfig"]), &md(&["api", "v1", "vault"]), &pool, &imports);
586 match r {
587 Resolution::Ambiguous(cands) => assert_eq!(cands.len(), 2, "got {cands:?}"),
588 other => panic!("expected Ambiguous, got {other:?}"),
589 }
590 }
591
592 #[test]
593 fn reference_through_crate_absolute_reexport_chain() {
594 let pool = pool_from(&[(&["core", "Foo"], "pub struct Foo { pub x: u32 }")]);
597 let imports = imports_from(&[(&["c"], "use crate::facade::Foo;"), (&["facade"], "pub use crate::core::Foo;")]);
598 let r = resolve_reference(&seg(&["Foo"]), &md(&["c"]), &pool, &imports);
599 assert_eq!(r, Resolution::Resolved(tp(&["core", "Foo"])), "got {r:?}");
600 }
601
602 #[test]
603 fn cross_crate_use_hits_the_sibling_key_exactly() {
604 let pool = with_root(
608 BTreeMap::new(),
609 &[(&["pumice_config", "ui", "ThemePreference"], "pub enum ThemePreference { Light, Dark }")],
610 );
611 let imports = imports_from(&[(&["schema", "settings"], "use pumice_config::ThemePreference;")]);
612 let r = resolve_reference(&seg(&["ThemePreference"]), &md(&["schema", "settings"]), &pool, &imports);
613 assert_eq!(r, Resolution::Resolved(tp_in(&["pumice_config", "ui", "ThemePreference"])), "got {r:?}");
614 }
615
616 #[test]
617 fn cross_crate_qualified_path_resolves_without_a_use() {
618 let pool =
622 with_root(BTreeMap::new(), &[(&["vaultpolish_core", "lint", "Severity"], "pub enum Severity { Error }")]);
623 let r = resolve_reference(
624 &seg(&["vaultpolish_core", "lint", "Severity"]),
625 &md(&["api", "v1", "scan"]),
626 &pool,
627 &ModuleImports::default(),
628 );
629 assert_eq!(r, Resolution::Resolved(tp_in(&["vaultpolish_core", "lint", "Severity"])), "got {r:?}");
630 }
631
632 #[test]
633 fn bare_ident_colliding_across_roots_takes_the_local_one() {
634 let pool = with_root(
641 pool_from(&[(&["schema", "scan", "Severity"], "pub enum Severity { Error, Warning, Info }")]),
642 &[(&["vaultpolish_core", "lint", "Severity"], "pub enum Severity { Error }")],
643 );
644 let r = resolve_reference(&seg(&["Severity"]), &md(&["api", "v1", "scan"]), &pool, &ModuleImports::default());
645 assert_eq!(r, Resolution::Resolved(tp(&["schema", "scan", "Severity"])), "got {r:?}");
646 }
647
648 #[test]
649 fn a_sibling_referencing_itself_stays_in_its_own_crate() {
650 let pool = with_root(
655 pool_from(&[(&["lint", "Severity"], "pub enum Severity { Local }")]),
656 &[(&["vaultpolish_core", "lint", "Severity"], "pub enum Severity { Sibling }")],
657 );
658 let r = resolve_reference(
659 &seg(&["crate", "lint", "Severity"]),
660 &["vaultpolish_core".to_string(), "scan".to_string()],
661 &pool,
662 &ModuleImports::default(),
663 );
664 assert_eq!(r, Resolution::Resolved(tp_in(&["vaultpolish_core", "lint", "Severity"])), "got {r:?}");
665
666 let r =
668 resolve_reference(&seg(&["crate", "lint", "Severity"]), &md(&["scan"]), &pool, &ModuleImports::default());
669 assert_eq!(r, Resolution::Resolved(tp(&["lint", "Severity"])), "got {r:?}");
670 }
671
672 #[test]
673 fn same_root_collision_is_still_ambiguous() {
674 let pool = with_root(
678 pool_from(&[
679 (&["a", "Severity"], "pub enum Severity { X }"),
680 (&["b", "Severity"], "pub enum Severity { Y }"),
681 ]),
682 &[(&["vaultpolish_core", "lint", "Severity"], "pub enum Severity { Z }")],
683 );
684 let r = resolve_reference(&seg(&["Severity"]), &md(&["api"]), &pool, &ModuleImports::default());
685 match r {
686 Resolution::Ambiguous(cands) => {
687 assert_eq!(cands.len(), 2, "only the two local candidates compete: {cands:?}");
688 assert!(cands.iter().all(|p| p.segments()[0] == "crate"), "got {cands:?}");
689 }
690 other => panic!("expected Ambiguous, got {other:?}"),
691 }
692 }
693
694 #[test]
695 fn external_use_with_no_pool_match_is_not_in_pool() {
696 let pool = pool_from(&[(&["models", "Workout"], "pub struct Workout { pub id: u64 }")]);
699 let imports = imports_from(&[(&["c"], "use chrono::DateTime;")]);
700 let r = resolve_reference(&seg(&["DateTime"]), &md(&["c"]), &pool, &imports);
701 assert_eq!(r, Resolution::NotInPool, "got {r:?}");
702 }
703
704 #[test]
705 fn reference_unique_terminal_without_imports_resolves() {
706 let pool = pool_from(&[(&["schema", "backup", "BackupManifest"], "pub struct BackupManifest { pub v: u32 }")]);
708 let r = resolve_reference(&seg(&["BackupManifest"]), &md(&["api"]), &pool, &ModuleImports::default());
709 assert_eq!(r, Resolution::Resolved(tp(&["schema", "backup", "BackupManifest"])), "got {r:?}");
710 }
711
712 #[test]
713 fn reference_qualified_crate_path_matches_exact_key() {
714 let pool = pool_from(&[(&["models", "Workout"], "pub struct Workout { pub id: u64 }")]);
715 let r =
716 resolve_reference(&seg(&["crate", "models", "Workout"]), &md(&["api"]), &pool, &ModuleImports::default());
717 assert_eq!(r, Resolution::Resolved(tp(&["models", "Workout"])), "got {r:?}");
718 }
719
720 #[test]
723 fn parse_simple_use() {
724 let f = parse_file("use chrono::DateTime;");
725 let imports = parse_imports(&f);
726 assert_eq!(imports.simple.get("DateTime"), Some(&tp_in(&["chrono", "DateTime"])));
727 }
728
729 #[test]
730 fn parse_use_with_rename() {
731 let f = parse_file("use chrono::DateTime as Moment;");
732 let imports = parse_imports(&f);
733 assert_eq!(imports.simple.get("Moment"), Some(&tp_in(&["chrono", "DateTime"])));
734 assert!(!imports.simple.contains_key("DateTime"));
736 }
737
738 #[test]
739 fn parse_use_with_group() {
740 let f = parse_file("use chrono::{DateTime, NaiveDate, NaiveTime};");
741 let imports = parse_imports(&f);
742 assert_eq!(imports.simple.get("DateTime"), Some(&tp_in(&["chrono", "DateTime"])));
743 assert_eq!(imports.simple.get("NaiveDate"), Some(&tp_in(&["chrono", "NaiveDate"])));
744 assert_eq!(imports.simple.get("NaiveTime"), Some(&tp_in(&["chrono", "NaiveTime"])));
745 }
746
747 #[test]
748 fn parse_nested_group() {
749 let f = parse_file("use foo::{bar::Baz, qux::{Quux, Quuux as Q}};");
750 let imports = parse_imports(&f);
751 assert_eq!(imports.simple.get("Baz"), Some(&tp_in(&["foo", "bar", "Baz"])));
752 assert_eq!(imports.simple.get("Quux"), Some(&tp_in(&["foo", "qux", "Quux"])));
753 assert_eq!(imports.simple.get("Q"), Some(&tp_in(&["foo", "qux", "Quuux"])));
754 }
755
756 #[test]
757 fn parse_glob_import() {
758 let f = parse_file("use chrono::*;");
759 let imports = parse_imports(&f);
760 assert!(imports.globs.contains(&tp_in(&["chrono"])));
761 assert!(imports.simple.is_empty());
762 }
763
764 #[test]
765 fn parse_multiple_glob_imports() {
766 let f = parse_file("use chrono::*; use uuid::*;");
767 let imports = parse_imports(&f);
768 assert!(imports.globs.contains(&tp_in(&["chrono"])));
769 assert!(imports.globs.contains(&tp_in(&["uuid"])));
770 }
771
772 #[test]
775 fn canonicalize_single_segment_via_imports() {
776 let f = parse_file("use chrono::DateTime;");
777 let imports = parse_imports(&f);
778 let path = parse_path("DateTime");
779 let resolved = canonicalize(&path, &imports, &tp_in(&["Foo"])).unwrap();
780 assert_eq!(resolved, tp_in(&["chrono", "DateTime"]));
781 }
782
783 #[test]
784 fn canonicalize_single_segment_via_rename() {
785 let f = parse_file("use chrono::DateTime as Moment;");
786 let imports = parse_imports(&f);
787 let path = parse_path("Moment");
788 let resolved = canonicalize(&path, &imports, &tp_in(&["Foo"])).unwrap();
789 assert_eq!(resolved, tp_in(&["chrono", "DateTime"]));
790 }
791
792 #[test]
793 fn canonicalize_unresolved_single_segment_falls_through() {
794 let f = parse_file("");
796 let imports = parse_imports(&f);
797 let path = parse_path("MyWorkout");
798 let resolved = canonicalize(&path, &imports, &tp_in(&["Foo"])).unwrap();
799 assert_eq!(resolved, tp_in(&["MyWorkout"]));
800 }
801
802 #[test]
803 fn canonicalize_unresolved_with_glob_emits_hint() {
804 let f = parse_file("use chrono::*;");
805 let imports = parse_imports(&f);
806 let path = parse_path("DateTime");
807 let err = canonicalize(&path, &imports, &tp_in(&["Foo"])).unwrap_err();
808 match err {
809 EmitError::UnresolvedReference { name, .. } => {
810 assert!(name.contains("DateTime"), "name was: {name}");
811 assert!(name.contains("chrono"), "name was: {name}");
812 assert!(name.contains("glob") || name.contains("qualify"), "hint missing: {name}");
813 }
814 other => panic!("expected UnresolvedReference, got {other:?}"),
815 }
816 }
817
818 #[test]
819 fn canonicalize_multi_segment_taken_as_qualified() {
820 let f = parse_file("");
821 let imports = parse_imports(&f);
822 let path = parse_path("chrono::DateTime");
823 let resolved = canonicalize(&path, &imports, &tp_in(&["Foo"])).unwrap();
824 assert_eq!(resolved, tp_in(&["chrono", "DateTime"]));
825 }
826
827 #[test]
828 fn canonicalize_strips_crate_prefix() {
829 let f = parse_file("");
830 let imports = parse_imports(&f);
831 let path = parse_path("crate::models::Workout");
832 let resolved = canonicalize(&path, &imports, &tp_in(&["Foo"])).unwrap();
833 assert_eq!(resolved, tp_in(&["models", "Workout"]));
835 }
836
837 #[test]
838 fn canonicalize_strips_generic_args() {
839 let f = parse_file("");
840 let imports = parse_imports(&f);
841 let path = parse_path("chrono::DateTime<Utc>");
842 let resolved = canonicalize(&path, &imports, &tp_in(&["Foo"])).unwrap();
843 assert_eq!(resolved, tp_in(&["chrono", "DateTime"]));
845 }
846}