1use anyhow::{Context, Result};
12use std::collections::{HashMap, HashSet};
13use std::path::{Path, PathBuf};
14use walkdir::WalkDir;
15
16use crate::ir::SrcSpan;
17
18pub struct SourceFile {
19 pub path: PathBuf,
20 pub rel: String,
22}
23
24#[derive(Debug, Clone)]
25pub struct LoadDiagnostic {
26 pub path: String,
27 pub message: String,
28}
29
30#[derive(Debug, Clone)]
33pub struct TypeRef {
34 pub text: String,
36 pub base: String,
38 pub container: Container,
39}
40
41#[derive(Debug, Clone, Copy, PartialEq, Eq)]
42pub enum Container {
43 Plain,
44 Vec,
45 Option,
46 Box,
47}
48
49#[derive(Debug, Clone)]
50pub struct FieldDef {
51 pub name: String,
52 pub ty: TypeRef,
53 pub span: SrcSpan,
54}
55
56#[derive(Debug, Clone)]
57pub struct StructDef {
58 pub name: String,
59 pub module_path: String,
61 pub qualified_name: String,
63 pub visibility: String,
65 pub cfg_predicates: Vec<String>,
67 pub fields: Vec<FieldDef>,
68 pub span: SrcSpan,
69}
70
71#[derive(Clone)]
73pub struct ImplFn {
74 pub type_name: String,
75 pub fn_name: String,
76 pub trait_name: Option<String>,
79 pub module_path: String,
81 pub qualified_type_name: String,
83 pub qualified_name: String,
85 pub visibility: String,
87 pub cfg_predicates: Vec<String>,
89 pub params: Vec<String>,
91 pub param_types: Vec<String>,
95 pub return_type: String,
97 pub vb_params: Vec<usize>,
104 pub block: syn::Block,
105 pub span: SrcSpan,
106}
107
108#[derive(Debug, Clone, PartialEq, Eq)]
111pub struct PublicReexport {
112 pub name: String,
114 pub qualified_name: String,
116 pub target: String,
118 pub module_path: String,
119 pub span: SrcSpan,
120}
121
122#[derive(Debug, Clone, PartialEq, Eq)]
123pub struct ImportBinding {
124 pub module_path: String,
125 pub alias: String,
126 pub target: String,
127}
128
129#[derive(Default)]
130pub struct Crate {
131 pub files: Vec<SourceFile>,
132 pub diagnostics: Vec<LoadDiagnostic>,
134 pub structs: HashMap<String, StructDef>,
137 pub methods: HashMap<(String, String), ImplFn>,
139 pub functions: HashMap<String, ImplFn>,
141 pub qualified_structs: HashMap<String, StructDef>,
143 pub qualified_methods: HashMap<(String, String), ImplFn>,
144 pub qualified_functions: HashMap<String, ImplFn>,
145 pub public_reexports: Vec<PublicReexport>,
146 pub imports: Vec<ImportBinding>,
147 pub dependency_aliases: HashMap<String, String>,
149 all_structs: Vec<StructDef>,
152 all_methods: Vec<ImplFn>,
153 all_functions: Vec<ImplFn>,
154}
155
156impl Crate {
157 pub fn all_structs(&self) -> impl Iterator<Item = &StructDef> {
159 self.all_structs.iter()
160 }
161
162 pub fn all_methods(&self) -> impl Iterator<Item = &ImplFn> {
164 self.all_methods.iter()
165 }
166
167 pub fn all_functions(&self) -> impl Iterator<Item = &ImplFn> {
169 self.all_functions.iter()
170 }
171
172 pub fn field_type(&self, struct_name: &str, field: &str) -> Option<&TypeRef> {
173 self.structs
174 .get(struct_name)?
175 .fields
176 .iter()
177 .find(|f| f.name == field)
178 .map(|f| &f.ty)
179 }
180
181 pub fn file_label(&self, span: SrcSpan) -> String {
182 match self.files.get(span.file) {
183 Some(f) => format!("{}:{}", f.rel, span.line),
184 None => format!("<unknown>:{}", span.line),
185 }
186 }
187
188 pub fn struct_candidates(&self, name: &str) -> Vec<&StructDef> {
191 let qualified = name.contains("::");
192 let mut found: Vec<_> = self
193 .all_structs
194 .iter()
195 .filter(|def| {
196 if qualified {
197 def.qualified_name == name
198 } else {
199 def.name == name
200 }
201 })
202 .collect();
203 sort_struct_candidates(&mut found);
204 found
205 }
206
207 pub fn function_candidates(&self, name: &str) -> Vec<&ImplFn> {
209 let qualified = name.contains("::");
210 let mut found: Vec<_> = self
211 .all_functions
212 .iter()
213 .filter(|func| {
214 if qualified {
215 func.qualified_name == name
216 } else {
217 func.fn_name == name
218 }
219 })
220 .collect();
221 sort_fn_candidates(&mut found);
222 found
223 }
224
225 pub fn method_candidates(&self, type_name: &str, fn_name: &str) -> Vec<&ImplFn> {
227 let qualified = type_name.contains("::");
228 let mut found: Vec<_> = self
229 .all_methods
230 .iter()
231 .filter(|func| {
232 func.fn_name == fn_name
233 && if qualified {
234 func.qualified_type_name == type_name
235 } else {
236 func.type_name == type_name
237 }
238 })
239 .collect();
240 if found.iter().any(|func| func.trait_name.is_none()) {
244 found.retain(|func| func.trait_name.is_none());
245 }
246 sort_fn_candidates(&mut found);
247 found
248 }
249
250 pub fn reexport_candidates(&self, name: &str) -> Vec<&PublicReexport> {
252 let qualified = name.contains("::");
253 let mut found: Vec<_> = self
254 .public_reexports
255 .iter()
256 .filter(|item| {
257 if qualified {
258 item.qualified_name == name
259 } else {
260 item.name == name
261 }
262 })
263 .collect();
264 found.sort_by(|a, b| {
265 (&a.qualified_name, a.span.file, a.span.line, a.span.col).cmp(&(
266 &b.qualified_name,
267 b.span.file,
268 b.span.line,
269 b.span.col,
270 ))
271 });
272 found
273 }
274
275 pub fn resolve_import_path(&self, module_path: &str, segments: &[String]) -> Vec<String> {
277 let Some(first) = segments.first() else {
278 return Vec::new();
279 };
280 let Some(binding) = self
281 .imports
282 .iter()
283 .find(|binding| binding.module_path == module_path && binding.alias == *first)
284 else {
285 return self.resolve_dependency_alias(segments);
286 };
287 let mut resolved = binding
288 .target
289 .split("::")
290 .map(str::to_string)
291 .collect::<Vec<_>>();
292 resolved.extend(segments.iter().skip(1).cloned());
293 self.resolve_dependency_alias(&resolved)
294 }
295
296 pub fn resolve_unambiguous_import_path(&self, segments: &[String]) -> Vec<String> {
300 let Some(first) = segments.first() else {
301 return Vec::new();
302 };
303 let mut targets = self
304 .imports
305 .iter()
306 .filter(|binding| binding.alias == *first)
307 .map(|binding| binding.target.as_str())
308 .collect::<Vec<_>>();
309 targets.sort_unstable();
310 targets.dedup();
311 let [target] = targets.as_slice() else {
312 return self.resolve_dependency_alias(segments);
313 };
314 let mut resolved = target.split("::").map(str::to_string).collect::<Vec<_>>();
315 resolved.extend(segments.iter().skip(1).cloned());
316 self.resolve_dependency_alias(&resolved)
317 }
318
319 pub fn set_dependency_aliases(&mut self, aliases: impl IntoIterator<Item = (String, String)>) {
320 self.dependency_aliases = aliases.into_iter().collect();
321 for binding in &mut self.imports {
322 let parts = binding.target.split("::").collect::<Vec<_>>();
323 let dependency = parts
324 .first()
325 .and_then(|name| self.dependency_aliases.get(*name))
326 .map(|package| (0usize, package))
327 .or_else(|| {
328 parts
329 .last()
330 .and_then(|name| self.dependency_aliases.get(*name))
331 .map(|package| (parts.len().saturating_sub(1), package))
332 });
333 if let Some((index, package)) = dependency {
334 let mut resolved = if index == parts.len().saturating_sub(1) {
335 Vec::new()
336 } else {
337 parts[..index]
338 .iter()
339 .map(|part| (*part).to_string())
340 .collect()
341 };
342 resolved.push(package.replace('-', "_"));
343 resolved.extend(parts.iter().skip(index + 1).map(|part| (*part).to_string()));
344 binding.target = resolved.join("::");
345 }
346 }
347 }
348
349 fn resolve_dependency_alias(&self, segments: &[String]) -> Vec<String> {
350 let Some(first) = segments.first() else {
351 return Vec::new();
352 };
353 let Some(package) = self.dependency_aliases.get(first) else {
354 return segments.to_vec();
355 };
356 let mut resolved = vec![package.replace('-', "_")];
357 resolved.extend(segments.iter().skip(1).cloned());
358 resolved
359 }
360}
361
362fn sort_struct_candidates(found: &mut Vec<&StructDef>) {
363 found.sort_by(|a, b| {
364 (&a.qualified_name, a.span.file, a.span.line, a.span.col).cmp(&(
365 &b.qualified_name,
366 b.span.file,
367 b.span.line,
368 b.span.col,
369 ))
370 });
371}
372
373fn sort_fn_candidates(found: &mut Vec<&ImplFn>) {
374 found.sort_by(|a, b| {
375 (&a.qualified_name, a.span.file, a.span.line, a.span.col).cmp(&(
376 &b.qualified_name,
377 b.span.file,
378 b.span.line,
379 b.span.col,
380 ))
381 });
382}
383
384pub fn load(root: &Path) -> Result<Crate> {
386 let mut krate = Crate::default();
387
388 for entry in WalkDir::new(root)
389 .sort_by_file_name()
390 .into_iter()
391 .filter_entry(|e| {
392 let name = e.file_name().to_string_lossy();
393 !(matches!(
394 name.as_ref(),
395 "target" | "artifacts" | "runs" | "node_modules"
396 ) || name.starts_with('.'))
397 })
398 {
399 let entry = match entry {
400 Ok(entry) => entry,
401 Err(error) => {
402 krate.diagnostics.push(LoadDiagnostic {
403 path: error
404 .path()
405 .map(|path| path.to_string_lossy().into_owned())
406 .unwrap_or_else(|| root.to_string_lossy().into_owned()),
407 message: format!("filesystem traversal failed: {error}"),
408 });
409 continue;
410 }
411 };
412 if !entry.file_type().is_file() {
413 continue;
414 }
415 if entry.path().extension().and_then(|e| e.to_str()) != Some("rs") {
416 continue;
417 }
418
419 let path = entry.path().to_path_buf();
420 let rel = path
421 .strip_prefix(root)
422 .unwrap_or(&path)
423 .to_string_lossy()
424 .to_string();
425 let text = std::fs::read_to_string(&path)
426 .with_context(|| format!("reading {}", path.display()))?;
427
428 let ast = match syn::parse_file(&text) {
431 Ok(ast) => ast,
432 Err(err) => {
433 krate.diagnostics.push(LoadDiagnostic {
434 path: rel,
435 message: format!("parse error: {err}"),
436 });
437 continue;
438 }
439 };
440
441 let file_index = krate.files.len();
442 krate.files.push(SourceFile { path, rel });
443 let module_path = file_module_path(&krate.files[file_index].rel);
444 let inherited_cfg = cfg_predicates(&ast.attrs);
445 collect_items(
446 &mut krate,
447 file_index,
448 &module_path,
449 &inherited_cfg,
450 &ast.items,
451 );
452 }
453
454 Ok(krate)
455}
456
457pub fn load_from_roots(root: &Path, crate_roots: &[PathBuf]) -> Result<Crate> {
462 let mut krate = Crate::default();
463 let mut visited = HashSet::new();
464 let normalized_root = root.canonicalize().unwrap_or_else(|_| root.to_path_buf());
465 for crate_root in crate_roots {
466 load_module_file(
467 &mut krate,
468 &normalized_root,
469 crate_root,
470 "",
471 &[],
472 &mut visited,
473 )?;
474 }
475 Ok(krate)
476}
477
478fn load_module_file(
479 krate: &mut Crate,
480 scan_root: &Path,
481 path: &Path,
482 module_path: &str,
483 inherited_cfg: &[String],
484 visited: &mut HashSet<(PathBuf, String)>,
485) -> Result<()> {
486 let normalized = path.canonicalize().unwrap_or_else(|_| path.to_path_buf());
487 if !visited.insert((normalized, module_path.to_string())) {
488 return Ok(());
489 }
490
491 let rel = path
492 .strip_prefix(scan_root)
493 .unwrap_or(path)
494 .to_string_lossy()
495 .to_string();
496 let text = match std::fs::read_to_string(path) {
497 Ok(text) => text,
498 Err(error) => {
499 krate.diagnostics.push(LoadDiagnostic {
500 path: rel,
501 message: format!("read error: {error}"),
502 });
503 return Ok(());
504 }
505 };
506 let ast = match syn::parse_file(&text) {
507 Ok(ast) => ast,
508 Err(error) => {
509 krate.diagnostics.push(LoadDiagnostic {
510 path: rel,
511 message: format!("parse error: {error}"),
512 });
513 return Ok(());
514 }
515 };
516
517 let file_index = krate.files.len();
518 krate.files.push(SourceFile {
519 path: path.to_path_buf(),
520 rel,
521 });
522 let file_cfg = combined_cfg(inherited_cfg, &ast.attrs);
523 collect_items(krate, file_index, module_path, &file_cfg, &ast.items);
524
525 let module_dir = module_directory(path);
526 load_external_modules(
527 krate,
528 scan_root,
529 path,
530 &module_dir,
531 module_path,
532 &file_cfg,
533 &ast.items,
534 visited,
535 )
536}
537
538#[allow(clippy::too_many_arguments)]
539fn load_external_modules(
540 krate: &mut Crate,
541 scan_root: &Path,
542 declaring_file: &Path,
543 module_dir: &Path,
544 module_path: &str,
545 inherited_cfg: &[String],
546 items: &[syn::Item],
547 visited: &mut HashSet<(PathBuf, String)>,
548) -> Result<()> {
549 for item in items {
550 let syn::Item::Mod(module) = item else {
551 continue;
552 };
553 let child_name = module.ident.to_string();
554 let child_module = join_qualified(module_path, &child_name);
555 let child_cfg = combined_cfg(inherited_cfg, &module.attrs);
556 if let Some((_, inner)) = &module.content {
557 load_external_modules(
558 krate,
559 scan_root,
560 declaring_file,
561 &module_dir.join(&child_name),
562 &child_module,
563 &child_cfg,
564 inner,
565 visited,
566 )?;
567 continue;
568 }
569
570 let explicit_path = module.attrs.iter().find_map(|attr| {
571 if !attr.path().is_ident("path") {
572 return None;
573 }
574 let syn::Meta::NameValue(value) = &attr.meta else {
575 return None;
576 };
577 let syn::Expr::Lit(syn::ExprLit {
578 lit: syn::Lit::Str(value),
579 ..
580 }) = &value.value
581 else {
582 return None;
583 };
584 Some(value.value())
585 });
586 let candidates = if let Some(explicit) = explicit_path {
587 vec![declaring_file
588 .parent()
589 .unwrap_or_else(|| Path::new(""))
590 .join(explicit)]
591 } else {
592 vec![
593 module_dir.join(format!("{child_name}.rs")),
594 module_dir.join(&child_name).join("mod.rs"),
595 ]
596 };
597 match candidates.iter().find(|candidate| candidate.is_file()) {
598 Some(source) => {
599 load_module_file(krate, scan_root, source, &child_module, &child_cfg, visited)?
600 }
601 None => krate.diagnostics.push(LoadDiagnostic {
602 path: declaring_file
603 .strip_prefix(scan_root)
604 .unwrap_or(declaring_file)
605 .to_string_lossy()
606 .into_owned(),
607 message: format!(
608 "module `{child_module}` was declared but no source file was found"
609 ),
610 }),
611 }
612 }
613 Ok(())
614}
615
616fn module_directory(path: &Path) -> PathBuf {
617 let parent = path.parent().unwrap_or_else(|| Path::new(""));
618 match path.file_name().and_then(|name| name.to_str()) {
619 Some("lib.rs" | "main.rs" | "mod.rs") => parent.to_path_buf(),
620 _ => parent.join(
621 path.file_stem()
622 .and_then(|name| name.to_str())
623 .unwrap_or_default(),
624 ),
625 }
626}
627
628fn collect_items(
629 krate: &mut Crate,
630 file: usize,
631 module_path: &str,
632 inherited_cfg: &[String],
633 items: &[syn::Item],
634) {
635 for item in items {
638 if let syn::Item::Use(item_use) = item {
639 collect_use(krate, file, module_path, item_use, is_public(&item_use.vis));
640 }
641 }
642 for item in items {
643 match item {
644 syn::Item::Struct(s) => {
645 let def = struct_def(file, module_path, inherited_cfg, s);
646 krate.structs.insert(def.name.clone(), def.clone());
647 krate
648 .qualified_structs
649 .insert(def.qualified_name.clone(), def.clone());
650 krate.all_structs.push(def);
651 }
652 syn::Item::Impl(imp) => collect_impl(krate, file, module_path, inherited_cfg, imp),
653 syn::Item::Fn(f) => {
654 let predicates = combined_cfg(inherited_cfg, &f.attrs);
655 let mut func = impl_fn(
656 file,
657 module_path,
658 String::new(),
659 String::new(),
660 None,
661 &f.vis,
662 &f.sig,
663 &f.block,
664 predicates,
665 );
666 augment_builder_aliases(krate, module_path, &mut func);
667 krate.functions.insert(func.fn_name.clone(), func.clone());
668 krate
669 .qualified_functions
670 .insert(func.qualified_name.clone(), func.clone());
671 krate.all_functions.push(func);
672 }
673 syn::Item::Mod(m) => {
674 if let Some((_, inner)) = &m.content {
675 let child_module = join_qualified(module_path, &m.ident.to_string());
676 let child_cfg = combined_cfg(inherited_cfg, &m.attrs);
677 collect_items(krate, file, &child_module, &child_cfg, inner);
678 }
679 }
680 syn::Item::Use(_) => {}
681 _ => {}
682 }
683 }
684}
685
686fn collect_impl(
687 krate: &mut Crate,
688 file: usize,
689 module_path: &str,
690 inherited_cfg: &[String],
691 imp: &syn::ItemImpl,
692) {
693 let Some(type_name) = type_base_name(&imp.self_ty) else {
694 return;
695 };
696 let trait_name = imp.trait_.as_ref().map(|(_, path, _)| type_text(path));
697 let qualified_type_name = qualify_type(module_path, &imp.self_ty)
698 .unwrap_or_else(|| join_qualified(module_path, &type_name));
699 let impl_cfg = combined_cfg(inherited_cfg, &imp.attrs);
700 for item in &imp.items {
701 if let syn::ImplItem::Fn(f) = item {
702 let predicates = combined_cfg(&impl_cfg, &f.attrs);
703 let mut func = impl_fn(
704 file,
705 module_path,
706 type_name.clone(),
707 qualified_type_name.clone(),
708 trait_name.clone(),
709 &f.vis,
710 &f.sig,
711 &f.block,
712 predicates,
713 );
714 augment_builder_aliases(krate, module_path, &mut func);
715 krate
716 .methods
717 .insert((type_name.clone(), func.fn_name.clone()), func.clone());
718 krate.qualified_methods.insert(
719 (qualified_type_name.clone(), func.fn_name.clone()),
720 func.clone(),
721 );
722 krate.all_methods.push(func);
723 }
724 }
725}
726
727fn augment_builder_aliases(krate: &Crate, module_path: &str, function: &mut ImplFn) {
728 for (index, type_text) in function.param_types.iter().enumerate() {
729 if function.vb_params.contains(&index) {
730 continue;
731 }
732 let Some(builder_alias) = syn::parse_str::<syn::Type>(type_text)
733 .ok()
734 .is_some_and(|ty| contains_resolved_builder_type(&ty, krate, module_path))
735 .then_some(index)
736 else {
737 continue;
738 };
739 function.vb_params.push(builder_alias);
740 }
741 function.vb_params.sort_unstable();
742 function.vb_params.dedup();
743}
744
745fn contains_resolved_builder_type(ty: &syn::Type, krate: &Crate, module_path: &str) -> bool {
746 match ty {
747 syn::Type::Path(path) => path.path.segments.iter().any(|segment| {
748 let source = vec![segment.ident.to_string()];
749 let resolved = krate.resolve_import_path(module_path, &source);
750 matches!(
751 resolved.last().map(String::as_str),
752 Some("VarBuilder" | "VarBuilderArgs")
753 ) || match &segment.arguments {
754 syn::PathArguments::AngleBracketed(arguments) => {
755 arguments.args.iter().any(|argument| {
756 matches!(
757 argument,
758 syn::GenericArgument::Type(inner)
759 if contains_resolved_builder_type(inner, krate, module_path)
760 )
761 })
762 }
763 syn::PathArguments::Parenthesized(arguments) => arguments
764 .inputs
765 .iter()
766 .any(|inner| contains_resolved_builder_type(inner, krate, module_path)),
767 syn::PathArguments::None => false,
768 }
769 }),
770 syn::Type::Reference(reference) => {
771 contains_resolved_builder_type(&reference.elem, krate, module_path)
772 }
773 syn::Type::Paren(paren) => contains_resolved_builder_type(&paren.elem, krate, module_path),
774 syn::Type::Group(group) => contains_resolved_builder_type(&group.elem, krate, module_path),
775 syn::Type::Tuple(tuple) => tuple
776 .elems
777 .iter()
778 .any(|inner| contains_resolved_builder_type(inner, krate, module_path)),
779 _ => false,
780 }
781}
782
783#[allow(clippy::too_many_arguments)]
784fn impl_fn(
785 file: usize,
786 module_path: &str,
787 type_name: String,
788 qualified_type_name: String,
789 trait_name: Option<String>,
790 visibility: &syn::Visibility,
791 sig: &syn::Signature,
792 block: &syn::Block,
793 cfg_predicates: Vec<String>,
794) -> ImplFn {
795 let mut params = Vec::new();
796 let mut param_types = Vec::new();
797 let mut vb_params = Vec::new();
798
799 for (index, input) in sig.inputs.iter().enumerate() {
800 match input {
801 syn::FnArg::Receiver(receiver) => {
802 params.push("self".to_string());
803 param_types.push(type_text(receiver));
804 }
805 syn::FnArg::Typed(pat) => {
806 let name = match &*pat.pat {
807 syn::Pat::Ident(id) => id.ident.to_string(),
808 other => type_text(other),
809 };
810 if contains_named_type(&pat.ty, "VarBuilder")
813 || contains_named_type(&pat.ty, "VarBuilderArgs")
814 {
815 vb_params.push(index);
816 }
817 params.push(name);
818 param_types.push(type_text(&pat.ty));
819 }
820 }
821 }
822
823 let fn_name = sig.ident.to_string();
824 let qualified_name = if qualified_type_name.is_empty() {
825 join_qualified(module_path, &fn_name)
826 } else {
827 join_qualified(&qualified_type_name, &fn_name)
828 };
829 ImplFn {
830 type_name,
831 fn_name,
832 trait_name,
833 module_path: module_path.to_string(),
834 qualified_type_name,
835 qualified_name,
836 visibility: visibility_text(visibility),
837 cfg_predicates,
838 params,
839 param_types,
840 return_type: match &sig.output {
841 syn::ReturnType::Default => "()".to_string(),
842 syn::ReturnType::Type(_, ty) => type_text(ty),
843 },
844 vb_params,
845 block: block.clone(),
846 span: span_of(file, sig.ident.span()),
847 }
848}
849
850fn struct_def(
851 file: usize,
852 module_path: &str,
853 inherited_cfg: &[String],
854 s: &syn::ItemStruct,
855) -> StructDef {
856 let mut fields = Vec::new();
857 if let syn::Fields::Named(named) = &s.fields {
858 for f in &named.named {
859 let Some(ident) = &f.ident else { continue };
860 fields.push(FieldDef {
861 name: ident.to_string(),
862 ty: type_ref(&f.ty),
863 span: span_of(file, ident.span()),
864 });
865 }
866 }
867 StructDef {
868 name: s.ident.to_string(),
869 module_path: module_path.to_string(),
870 qualified_name: join_qualified(module_path, &s.ident.to_string()),
871 visibility: visibility_text(&s.vis),
872 cfg_predicates: combined_cfg(inherited_cfg, &s.attrs),
873 fields,
874 span: span_of(file, s.ident.span()),
875 }
876}
877
878fn combined_cfg(inherited: &[String], attrs: &[syn::Attribute]) -> Vec<String> {
879 let mut predicates = inherited.to_vec();
880 predicates.extend(cfg_predicates(attrs));
881 predicates.sort();
882 predicates.dedup();
883 predicates
884}
885
886fn cfg_predicates(attrs: &[syn::Attribute]) -> Vec<String> {
887 attrs
888 .iter()
889 .filter_map(|attribute| match &attribute.meta {
890 syn::Meta::List(list) if list.path.is_ident("cfg") => Some(list.tokens.to_string()),
891 _ => None,
892 })
893 .collect()
894}
895
896fn file_module_path(rel: &str) -> String {
902 let normalized = rel.replace('\\', "/");
903 let mut parts: Vec<&str> = normalized
904 .split('/')
905 .filter(|part| !part.is_empty())
906 .collect();
907 let Some(file) = parts.pop() else {
908 return String::new();
909 };
910 if parts.first() == Some(&"src") {
911 parts.remove(0);
912 }
913 let stem = file.strip_suffix(".rs").unwrap_or(file);
914 if !matches!(stem, "mod" | "lib" | "main") {
915 parts.push(stem);
916 }
917 parts.join("::")
918}
919
920fn join_qualified(module_path: &str, leaf: &str) -> String {
921 if module_path.is_empty() {
922 leaf.to_string()
923 } else if leaf.is_empty() {
924 module_path.to_string()
925 } else {
926 format!("{module_path}::{leaf}")
927 }
928}
929
930fn visibility_text(vis: &syn::Visibility) -> String {
931 type_text(vis).replace("pub (", "pub(").replace(" )", ")")
932}
933
934fn is_public(vis: &syn::Visibility) -> bool {
935 !matches!(vis, syn::Visibility::Inherited)
936}
937
938fn qualify_type(module_path: &str, ty: &syn::Type) -> Option<String> {
939 let syn::Type::Path(path) = ty else {
940 return match ty {
941 syn::Type::Reference(reference) => qualify_type(module_path, &reference.elem),
942 syn::Type::Paren(paren) => qualify_type(module_path, &paren.elem),
943 _ => None,
944 };
945 };
946 let parts: Vec<String> = path
947 .path
948 .segments
949 .iter()
950 .map(|segment| segment.ident.to_string())
951 .collect();
952 qualify_path_parts(module_path, &parts)
953}
954
955fn qualify_path_parts(module_path: &str, parts: &[String]) -> Option<String> {
956 let (first, rest) = parts.split_first()?;
957 match first.as_str() {
958 "crate" => Some(rest.join("::")),
959 "self" => Some(join_qualified(module_path, &rest.join("::"))),
960 "super" => {
961 let mut base: Vec<&str> = module_path
962 .split("::")
963 .filter(|part| !part.is_empty())
964 .collect();
965 let mut remaining = rest;
966 while remaining.first().map(String::as_str) == Some("super") {
967 base.pop();
968 remaining = &remaining[1..];
969 }
970 base.pop();
971 let suffix = remaining.join("::");
972 Some(join_qualified(&base.join("::"), &suffix))
973 }
974 _ if matches!(
975 first.as_str(),
976 "candle"
977 | "candle_core"
978 | "candle_nn"
979 | "candle_transformers"
980 | "std"
981 | "core"
982 | "alloc"
983 ) =>
984 {
985 Some(parts.join("::"))
986 }
987 _ if parts.len() == 1 => Some(join_qualified(module_path, first)),
988 _ => Some(parts.join("::")),
989 }
990}
991
992fn collect_use(
993 krate: &mut Crate,
994 file: usize,
995 module_path: &str,
996 item_use: &syn::ItemUse,
997 public: bool,
998) {
999 let mut leaves = Vec::new();
1000 flatten_use_tree(Vec::new(), &item_use.tree, &mut leaves);
1001 for (path, alias, span) in leaves {
1002 let Some(source_name) = path.last() else {
1003 continue;
1004 };
1005 let name = alias.unwrap_or_else(|| source_name.clone());
1006 let target = qualify_path_parts(module_path, &path).unwrap_or_else(|| path.join("::"));
1007 krate.imports.push(ImportBinding {
1008 module_path: module_path.to_string(),
1009 alias: name.clone(),
1010 target: target.clone(),
1011 });
1012 if public {
1013 krate.public_reexports.push(PublicReexport {
1014 qualified_name: join_qualified(module_path, &name),
1015 name,
1016 target,
1017 module_path: module_path.to_string(),
1018 span: span_of(file, span),
1019 });
1020 }
1021 }
1022}
1023
1024fn flatten_use_tree(
1025 prefix: Vec<String>,
1026 tree: &syn::UseTree,
1027 leaves: &mut Vec<(Vec<String>, Option<String>, proc_macro2::Span)>,
1028) {
1029 match tree {
1030 syn::UseTree::Path(path) => {
1031 let mut next = prefix;
1032 next.push(path.ident.to_string());
1033 flatten_use_tree(next, &path.tree, leaves);
1034 }
1035 syn::UseTree::Name(name) => {
1036 if name.ident == "self" {
1037 if !prefix.is_empty() {
1038 leaves.push((prefix, None, name.ident.span()));
1039 }
1040 } else {
1041 let mut path = prefix;
1042 path.push(name.ident.to_string());
1043 leaves.push((path, None, name.ident.span()));
1044 }
1045 }
1046 syn::UseTree::Rename(rename) => {
1047 let mut path = prefix;
1048 if rename.ident != "self" {
1049 path.push(rename.ident.to_string());
1050 }
1051 leaves.push((path, Some(rename.rename.to_string()), rename.rename.span()));
1052 }
1053 syn::UseTree::Group(group) => {
1054 for item in &group.items {
1055 flatten_use_tree(prefix.clone(), item, leaves);
1056 }
1057 }
1058 syn::UseTree::Glob(_) => {}
1060 }
1061}
1062
1063pub fn type_ref(ty: &syn::Type) -> TypeRef {
1065 let text = type_text(ty);
1066 let (base, container) = peel(ty);
1067 TypeRef {
1068 text,
1069 base,
1070 container,
1071 }
1072}
1073
1074fn peel(ty: &syn::Type) -> (String, Container) {
1075 let syn::Type::Path(p) = ty else {
1076 return (type_base_name(ty).unwrap_or_default(), Container::Plain);
1077 };
1078 let Some(last) = p.path.segments.last() else {
1079 return (String::new(), Container::Plain);
1080 };
1081 let container = match last.ident.to_string().as_str() {
1082 "Vec" => Container::Vec,
1083 "Option" => Container::Option,
1084 "Box" => Container::Box,
1085 _ => Container::Plain,
1086 };
1087 if container == Container::Plain {
1088 return (last.ident.to_string(), Container::Plain);
1089 }
1090 if let syn::PathArguments::AngleBracketed(args) = &last.arguments {
1091 for arg in &args.args {
1092 if let syn::GenericArgument::Type(inner) = arg {
1093 let (base, _) = peel(inner);
1094 return (base, container);
1095 }
1096 }
1097 }
1098 (last.ident.to_string(), container)
1099}
1100
1101pub fn type_base_name(ty: &syn::Type) -> Option<String> {
1103 match ty {
1104 syn::Type::Path(p) => p.path.segments.last().map(|s| s.ident.to_string()),
1105 syn::Type::Reference(r) => type_base_name(&r.elem),
1106 syn::Type::Paren(p) => type_base_name(&p.elem),
1107 _ => None,
1108 }
1109}
1110
1111fn contains_named_type(ty: &syn::Type, wanted: &str) -> bool {
1112 match ty {
1113 syn::Type::Path(path) => path.path.segments.iter().any(|segment| {
1114 segment.ident == wanted
1115 || match &segment.arguments {
1116 syn::PathArguments::AngleBracketed(arguments) => {
1117 arguments.args.iter().any(|argument| match argument {
1118 syn::GenericArgument::Type(inner) => contains_named_type(inner, wanted),
1119 _ => false,
1120 })
1121 }
1122 syn::PathArguments::Parenthesized(arguments) => {
1123 arguments
1124 .inputs
1125 .iter()
1126 .any(|inner| contains_named_type(inner, wanted))
1127 || match &arguments.output {
1128 syn::ReturnType::Default => false,
1129 syn::ReturnType::Type(_, output) => {
1130 contains_named_type(output, wanted)
1131 }
1132 }
1133 }
1134 syn::PathArguments::None => false,
1135 }
1136 }),
1137 syn::Type::Reference(reference) => contains_named_type(&reference.elem, wanted),
1138 syn::Type::Paren(paren) => contains_named_type(&paren.elem, wanted),
1139 syn::Type::Group(group) => contains_named_type(&group.elem, wanted),
1140 syn::Type::Tuple(tuple) => tuple
1141 .elems
1142 .iter()
1143 .any(|inner| contains_named_type(inner, wanted)),
1144 _ => false,
1145 }
1146}
1147
1148pub fn type_text<T: quote::ToTokens>(t: &T) -> String {
1149 let mut text = quote::quote!(#t).to_string();
1150 for (from, to) in [(" :: ", "::"), (" < ", "<"), (" > ", ">"), (" , ", ", ")] {
1153 text = text.replace(from, to);
1154 }
1155 text
1156}
1157
1158pub fn span_of(file: usize, span: proc_macro2::Span) -> SrcSpan {
1159 let start = span.start();
1160 SrcSpan {
1161 file,
1162 line: start.line,
1163 col: start.column,
1164 }
1165}