1use std::collections::{HashMap, HashSet};
2use std::path::{Path, PathBuf};
3
4use crate::package_imports::{acquire_package_snapshots, resolve_import_path_with_snapshots};
5use crate::package_snapshot::PackageSnapshot;
6use harn_lexer::Span;
7use harn_parser::{BindingPattern, Node, Parser, SNode};
8
9pub mod asset_paths;
10pub mod fingerprint;
11pub mod package_execution;
12mod package_imports;
13pub mod package_snapshot;
14pub mod personas;
15mod stdlib;
16
17pub use package_imports::{
18 resolve_import_path, resolve_import_path_with_guard, resolve_import_path_with_snapshot,
19};
20
21#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
23pub enum DefKind {
24 Function,
25 Pipeline,
26 Tool,
27 Skill,
28 Struct,
29 Enum,
30 Interface,
31 Type,
32 Variable,
33 Parameter,
34}
35
36#[derive(Debug, Clone)]
38pub struct DefSite {
39 pub name: String,
40 pub file: PathBuf,
41 pub kind: DefKind,
42 pub span: Span,
43}
44
45#[derive(Debug, Clone)]
47pub enum WildcardResolution {
48 Resolved(HashSet<String>),
50 Unknown,
52}
53
54#[derive(Debug, Default)]
56pub struct ModuleGraph {
57 modules: HashMap<PathBuf, ModuleInfo>,
58 _package_snapshots: Vec<PackageSnapshot>,
60}
61
62#[derive(Debug, Clone)]
63pub struct ParsedModuleSource {
64 pub source: String,
65 pub program: Vec<SNode>,
66}
67
68#[derive(Debug, Default)]
69pub struct ModuleGraphBuild {
70 pub graph: ModuleGraph,
71 pub parsed_sources: HashMap<PathBuf, ParsedModuleSource>,
72}
73
74#[derive(Debug, Default)]
75struct ModuleInfo {
76 declarations: HashMap<String, DefSite>,
79 exports: HashSet<String>,
84 own_exports: HashSet<String>,
87 selective_re_exports: HashMap<String, Vec<PathBuf>>,
94 wildcard_re_export_paths: Vec<PathBuf>,
98 selective_import_names: HashSet<String>,
100 imports: Vec<ImportRef>,
102 has_unresolved_wildcard_import: bool,
104 has_unresolved_selective_import: bool,
108 type_declarations: Vec<SNode>,
111 callable_declarations: Vec<SNode>,
114 load_error: Option<ModuleLoadError>,
120}
121
122#[derive(Debug, Clone)]
128pub struct ModuleLoadError {
129 pub message: String,
131 pub span: Span,
133}
134
135#[derive(Debug, Clone)]
138pub struct ImportCompileFailure {
139 pub import_raw_path: String,
141 pub import_span: Span,
143 pub module_path: PathBuf,
145 pub error: ModuleLoadError,
147}
148
149#[derive(Debug, Clone)]
150struct ImportRef {
151 raw_path: String,
152 path: Option<PathBuf>,
153 selective_names: Option<HashSet<String>>,
154 import_span: Span,
155}
156
157#[derive(Debug, Clone, PartialEq, Eq)]
159pub struct ModuleImport {
160 pub raw_path: String,
162 pub resolved_path: Option<PathBuf>,
164 pub selective_names: Option<Vec<String>>,
166}
167
168pub fn read_module_source(path: &Path) -> Option<String> {
174 if let Some(stdlib_module) = stdlib_module_from_path(path) {
175 return stdlib::get_stdlib_source(stdlib_module).map(ToString::to_string);
176 }
177 std::fs::read_to_string(path).ok()
178}
179
180pub fn build(files: &[PathBuf]) -> ModuleGraph {
186 build_inner(files, None).graph
187}
188
189pub fn build_with_parsed_sources(files: &[PathBuf]) -> ModuleGraphBuild {
195 let parsed_source_targets = files.iter().map(|file| normalize_path(file)).collect();
196 build_inner(files, Some(&parsed_source_targets))
197}
198
199fn build_inner(
200 files: &[PathBuf],
201 parsed_source_targets: Option<&HashSet<PathBuf>>,
202) -> ModuleGraphBuild {
203 let package_snapshots = acquire_package_snapshots(files);
204 let mut modules: HashMap<PathBuf, ModuleInfo> = HashMap::new();
205 let mut parsed_sources: HashMap<PathBuf, ParsedModuleSource> = HashMap::new();
206 let mut seen: HashSet<PathBuf> = HashSet::new();
207 let mut wave: Vec<PathBuf> = Vec::new();
208 for file in files {
209 let canonical = normalize_path(file);
210 if seen.insert(canonical.clone()) {
211 wave.push(canonical);
212 }
213 }
214 while !wave.is_empty() {
222 let loaded = load_wave(&wave, &package_snapshots);
223 let mut next_wave: Vec<PathBuf> = Vec::new();
224 for (path, (module, parsed)) in wave.drain(..).zip(loaded) {
225 let retain_parsed_source =
226 parsed_source_targets.is_some_and(|targets| targets.contains(&path));
227 if retain_parsed_source {
228 if let Some(parsed) = parsed {
229 parsed_sources.insert(path.clone(), parsed);
230 }
231 }
232 for import in &module.imports {
249 if let Some(import_path) = &import.path {
250 let canonical = normalize_path(import_path);
251 if seen.insert(canonical.clone()) {
252 next_wave.push(canonical);
253 }
254 }
255 }
256 modules.insert(path, module);
257 }
258 wave = next_wave;
259 }
260 resolve_re_exports(&mut modules);
261 ModuleGraphBuild {
262 graph: ModuleGraph {
263 modules,
264 _package_snapshots: package_snapshots,
265 },
266 parsed_sources,
267 }
268}
269
270pub const MODULE_GRAPH_JOBS_ENV: &str = "HARN_MODULE_GRAPH_JOBS";
273
274fn load_wave(
277 paths: &[PathBuf],
278 package_snapshots: &[PackageSnapshot],
279) -> Vec<(ModuleInfo, Option<ParsedModuleSource>)> {
280 const MIN_PARALLEL_WAVE: usize = 8;
281 let configured = std::env::var(MODULE_GRAPH_JOBS_ENV)
282 .ok()
283 .and_then(|value| value.parse::<usize>().ok())
284 .filter(|&jobs| jobs > 0);
285 let workers = configured
286 .unwrap_or_else(|| {
287 std::thread::available_parallelism()
288 .map(std::num::NonZeroUsize::get)
289 .unwrap_or(1)
290 })
291 .min(paths.len());
292 if workers <= 1 || paths.len() < MIN_PARALLEL_WAVE {
293 return paths
294 .iter()
295 .map(|path| load_module(path, package_snapshots))
296 .collect();
297 }
298 let next = std::sync::atomic::AtomicUsize::new(0);
299 let mut produced: Vec<(usize, (ModuleInfo, Option<ParsedModuleSource>))> =
300 std::thread::scope(|scope| {
301 let handles: Vec<_> = (0..workers)
302 .map(|_| {
303 scope.spawn(|| {
304 let mut local = Vec::new();
305 loop {
306 let index = next.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
307 let Some(path) = paths.get(index) else {
308 break;
309 };
310 local.push((index, load_module(path, package_snapshots)));
311 }
312 local
313 })
314 })
315 .collect();
316 handles
317 .into_iter()
318 .flat_map(|handle| match handle.join() {
319 Ok(local) => local,
320 Err(panic) => std::panic::resume_unwind(panic),
321 })
322 .collect()
323 });
324 produced.sort_unstable_by_key(|(index, _)| *index);
325 produced.into_iter().map(|(_, loaded)| loaded).collect()
326}
327
328fn resolve_re_exports(modules: &mut HashMap<PathBuf, ModuleInfo>) {
333 let keys: Vec<PathBuf> = modules.keys().cloned().collect();
334 loop {
335 let mut changed = false;
336 for path in &keys {
337 let wildcard_paths = modules
340 .get(path)
341 .map(|m| m.wildcard_re_export_paths.clone())
342 .unwrap_or_default();
343 if wildcard_paths.is_empty() {
344 continue;
345 }
346 let mut additions: Vec<String> = Vec::new();
347 for src in &wildcard_paths {
348 let src_canonical = normalize_path(src);
349 if let Some(src_module) = modules.get(src).or_else(|| modules.get(&src_canonical)) {
350 additions.extend(src_module.exports.iter().cloned());
351 }
352 }
353 if let Some(module) = modules.get_mut(path) {
354 for name in additions {
355 if module.exports.insert(name) {
356 changed = true;
357 }
358 }
359 }
360 }
361 if !changed {
362 break;
363 }
364 }
365}
366
367impl ModuleGraph {
368 pub fn module_paths(&self) -> Vec<PathBuf> {
373 let mut paths: Vec<PathBuf> = self.modules.keys().cloned().collect();
374 paths.sort();
375 paths
376 }
377
378 pub fn contains_module(&self, path: &Path) -> bool {
381 self.modules.contains_key(path) || self.modules.contains_key(&normalize_path(path))
382 }
383
384 pub fn all_selective_import_names(&self) -> HashSet<&str> {
386 let mut names = HashSet::new();
387 for module in self.modules.values() {
388 for name in &module.selective_import_names {
389 names.insert(name.as_str());
390 }
391 }
392 names
393 }
394
395 pub fn importers_of(&self, target: &Path) -> Vec<PathBuf> {
398 let target = normalize_path(target);
399 let mut out: Vec<PathBuf> = self
400 .modules
401 .iter()
402 .filter(|(_, info)| {
403 info.imports.iter().any(|import| {
404 import
405 .path
406 .as_ref()
407 .is_some_and(|p| normalize_path(p) == target)
408 })
409 })
410 .map(|(path, _)| path.clone())
411 .collect();
412 out.sort();
413 out
414 }
415
416 pub fn imports_for_module(&self, file: &Path) -> Vec<ModuleImport> {
418 let file = normalize_path(file);
419 let Some(module) = self.modules.get(&file) else {
420 return Vec::new();
421 };
422 let mut imports: Vec<ModuleImport> = module
423 .imports
424 .iter()
425 .map(|import| {
426 let mut selective_names = import
427 .selective_names
428 .as_ref()
429 .map(|names| names.iter().cloned().collect::<Vec<_>>());
430 if let Some(names) = selective_names.as_mut() {
431 names.sort();
432 }
433 ModuleImport {
434 raw_path: import.raw_path.clone(),
435 resolved_path: import.path.as_ref().map(|path| normalize_path(path)),
436 selective_names,
437 }
438 })
439 .collect();
440 imports.sort_by(|left, right| {
441 left.raw_path
442 .cmp(&right.raw_path)
443 .then_with(|| left.selective_names.cmp(&right.selective_names))
444 .then_with(|| left.resolved_path.cmp(&right.resolved_path))
445 });
446 imports
447 }
448
449 pub fn exports_for_module(&self, file: &Path) -> Vec<String> {
451 let file = normalize_path(file);
452 let Some(module) = self.modules.get(&file) else {
453 return Vec::new();
454 };
455 let mut exports: Vec<String> = module.exports.iter().cloned().collect();
456 exports.sort();
457 exports
458 }
459
460 pub fn wildcard_exports_for(&self, file: &Path) -> WildcardResolution {
465 let file = normalize_path(file);
466 let Some(module) = self.modules.get(&file) else {
467 return WildcardResolution::Unknown;
468 };
469 if module.has_unresolved_wildcard_import {
470 return WildcardResolution::Unknown;
471 }
472
473 let mut names = HashSet::new();
474 for import in module
475 .imports
476 .iter()
477 .filter(|import| import.selective_names.is_none())
478 {
479 let Some(import_path) = &import.path else {
480 return WildcardResolution::Unknown;
481 };
482 let imported = self.modules.get(import_path).or_else(|| {
483 let normalized = normalize_path(import_path);
484 self.modules.get(&normalized)
485 });
486 let Some(imported) = imported else {
487 return WildcardResolution::Unknown;
488 };
489 names.extend(imported.exports.iter().cloned());
490 }
491 WildcardResolution::Resolved(names)
492 }
493
494 #[must_use]
515 pub fn import_compile_failures(&self, file: &Path) -> Vec<ImportCompileFailure> {
516 let file = normalize_path(file);
517 let Some(module) = self.modules.get(&file) else {
518 return Vec::new();
519 };
520 let mut failures = Vec::new();
521 for import in &module.imports {
522 let Some(import_path) = &import.path else {
523 continue;
524 };
525 let Some(target) = self
526 .modules
527 .get(import_path)
528 .or_else(|| self.modules.get(&normalize_path(import_path)))
529 else {
530 continue;
531 };
532 if let Some(error) = &target.load_error {
533 failures.push(ImportCompileFailure {
534 import_raw_path: import.raw_path.clone(),
535 import_span: import.import_span,
536 module_path: normalize_path(import_path),
537 error: error.clone(),
538 });
539 }
540 }
541 failures
542 }
543
544 pub fn imported_names_for_file(&self, file: &Path) -> Option<HashSet<String>> {
545 let file = normalize_path(file);
546 let module = self.modules.get(&file)?;
547 if module.has_unresolved_wildcard_import || module.has_unresolved_selective_import {
548 return None;
549 }
550
551 let mut names = HashSet::new();
552 for import in &module.imports {
553 let import_path = import.path.as_ref()?;
554 let imported = self
555 .modules
556 .get(import_path)
557 .or_else(|| self.modules.get(&normalize_path(import_path)))?;
558 if imported.load_error.is_some() {
564 return None;
565 }
566 match &import.selective_names {
567 None => {
568 names.extend(imported.exports.iter().cloned());
569 }
570 Some(selective) => {
571 for name in selective {
580 if imported.declarations.contains_key(name)
581 || imported.exports.contains(name)
582 {
583 names.insert(name.clone());
584 }
585 }
586 }
587 }
588 }
589 Some(names)
590 }
591
592 pub fn imported_type_declarations_for_file(&self, file: &Path) -> Option<Vec<SNode>> {
596 let file = normalize_path(file);
597 let module = self.modules.get(&file)?;
598 if module.has_unresolved_wildcard_import || module.has_unresolved_selective_import {
599 return None;
600 }
601
602 let mut decls = Vec::new();
603 for import in &module.imports {
604 let import_path = import.path.as_ref()?;
605 let imported = self
606 .modules
607 .get(import_path)
608 .or_else(|| self.modules.get(&normalize_path(import_path)))?;
609 if imported.load_error.is_some() {
615 return None;
616 }
617 let names_to_collect: Vec<String> = match &import.selective_names {
618 None => imported.exports.iter().cloned().collect(),
619 Some(selective) => selective.iter().cloned().collect(),
620 };
621 for name in &names_to_collect {
622 let mut visited = HashSet::new();
623 if let Some(decl) = self.find_exported_type_decl(import_path, name, &mut visited) {
624 decls.push(decl);
625 }
626 }
627 for ty_decl in &imported.type_declarations {
637 if type_decl_name(ty_decl).is_some() {
638 decls.push(ty_decl.clone());
639 }
640 }
641 }
642 Some(decls)
643 }
644
645 pub fn imported_callable_declarations_for_file(&self, file: &Path) -> Option<Vec<SNode>> {
649 let file = normalize_path(file);
650 let module = self.modules.get(&file)?;
651 if module.has_unresolved_wildcard_import || module.has_unresolved_selective_import {
652 return None;
653 }
654
655 let mut decls = Vec::new();
656 for import in &module.imports {
657 let import_path = import.path.as_ref()?;
658 let imported = self
659 .modules
660 .get(import_path)
661 .or_else(|| self.modules.get(&normalize_path(import_path)))?;
662 if imported.load_error.is_some() {
668 return None;
669 }
670 let selective_import = import.selective_names.is_some();
671 let names_to_collect: Vec<String> = match &import.selective_names {
672 None => imported.exports.iter().cloned().collect(),
673 Some(selective) => selective.iter().cloned().collect(),
674 };
675 for name in &names_to_collect {
676 if selective_import || imported.own_exports.contains(name) {
677 if let Some(decl) = imported
678 .callable_declarations
679 .iter()
680 .find(|decl| callable_decl_name(decl) == Some(name.as_str()))
681 {
682 decls.push(decl.clone());
683 continue;
684 }
685 }
686 let mut visited = HashSet::new();
687 if let Some(decl) =
688 self.find_exported_callable_decl(import_path, name, &mut visited)
689 {
690 decls.push(decl);
691 }
692 }
693 }
694 Some(decls)
695 }
696
697 fn find_exported_type_decl(
700 &self,
701 path: &Path,
702 name: &str,
703 visited: &mut HashSet<PathBuf>,
704 ) -> Option<SNode> {
705 let canonical = normalize_path(path);
706 if !visited.insert(canonical.clone()) {
707 return None;
708 }
709 let module = self
710 .modules
711 .get(&canonical)
712 .or_else(|| self.modules.get(path))?;
713 for decl in &module.type_declarations {
714 if type_decl_name(decl) == Some(name) && module.own_exports.contains(name) {
715 return Some(decl.clone());
716 }
717 }
718 if let Some(sources) = module.selective_re_exports.get(name) {
719 for source in sources {
720 if let Some(decl) = self.find_exported_type_decl(source, name, visited) {
721 return Some(decl);
722 }
723 }
724 }
725 for source in &module.wildcard_re_export_paths {
726 if let Some(decl) = self.find_exported_type_decl(source, name, visited) {
727 return Some(decl);
728 }
729 }
730 None
731 }
732
733 fn find_exported_callable_decl(
734 &self,
735 path: &Path,
736 name: &str,
737 visited: &mut HashSet<PathBuf>,
738 ) -> Option<SNode> {
739 let canonical = normalize_path(path);
740 if !visited.insert(canonical.clone()) {
741 return None;
742 }
743 let module = self
744 .modules
745 .get(&canonical)
746 .or_else(|| self.modules.get(path))?;
747 for decl in &module.callable_declarations {
748 if callable_decl_name(decl) == Some(name) && module.own_exports.contains(name) {
749 return Some(decl.clone());
750 }
751 }
752 if let Some(sources) = module.selective_re_exports.get(name) {
753 for source in sources {
754 if let Some(decl) = self.find_exported_callable_decl(source, name, visited) {
755 return Some(decl);
756 }
757 }
758 }
759 for source in &module.wildcard_re_export_paths {
760 if let Some(decl) = self.find_exported_callable_decl(source, name, visited) {
761 return Some(decl);
762 }
763 }
764 None
765 }
766
767 pub fn definition_of(&self, file: &Path, name: &str) -> Option<DefSite> {
773 let mut visited = HashSet::new();
774 self.definition_of_inner(file, name, &mut visited)
775 }
776
777 pub fn declared_names_for_file(&self, file: &Path) -> Option<Vec<&str>> {
781 let module = self.modules.get(&normalize_path(file))?;
782 let mut names: Vec<&str> = module.declarations.keys().map(String::as_str).collect();
783 names.sort_unstable();
784 Some(names)
785 }
786
787 fn definition_of_inner(
788 &self,
789 file: &Path,
790 name: &str,
791 visited: &mut HashSet<PathBuf>,
792 ) -> Option<DefSite> {
793 let file = normalize_path(file);
794 if !visited.insert(file.clone()) {
795 return None;
796 }
797 let current = self.modules.get(&file)?;
798
799 if let Some(local) = current.declarations.get(name) {
800 return Some(local.clone());
801 }
802
803 if let Some(sources) = current.selective_re_exports.get(name) {
808 for source in sources {
809 if let Some(def) = self.definition_of_inner(source, name, visited) {
810 return Some(def);
811 }
812 }
813 }
814
815 for source in ¤t.wildcard_re_export_paths {
817 if let Some(def) = self.definition_of_inner(source, name, visited) {
818 return Some(def);
819 }
820 }
821
822 for import in ¤t.imports {
824 let Some(selective_names) = &import.selective_names else {
825 continue;
826 };
827 if !selective_names.contains(name) {
828 continue;
829 }
830 if let Some(path) = &import.path {
831 if let Some(def) = self.definition_of_inner(path, name, visited) {
832 return Some(def);
833 }
834 }
835 }
836
837 for import in ¤t.imports {
839 if import.selective_names.is_some() {
840 continue;
841 }
842 if let Some(path) = &import.path {
843 if let Some(def) = self.definition_of_inner(path, name, visited) {
844 return Some(def);
845 }
846 }
847 }
848
849 None
850 }
851
852 pub fn re_export_conflicts(&self, file: &Path) -> Vec<ReExportConflict> {
856 let file = normalize_path(file);
857 let Some(module) = self.modules.get(&file) else {
858 return Vec::new();
859 };
860
861 let mut sources: HashMap<String, Vec<PathBuf>> = HashMap::new();
865
866 for (name, srcs) in &module.selective_re_exports {
867 sources
868 .entry(name.clone())
869 .or_default()
870 .extend(srcs.iter().cloned());
871 }
872 for src in &module.wildcard_re_export_paths {
873 let canonical = normalize_path(src);
874 let Some(src_module) = self
875 .modules
876 .get(&canonical)
877 .or_else(|| self.modules.get(src))
878 else {
879 continue;
880 };
881 for name in &src_module.exports {
882 sources
883 .entry(name.clone())
884 .or_default()
885 .push(canonical.clone());
886 }
887 }
888
889 for name in &module.own_exports {
893 if let Some(entry) = sources.get_mut(name) {
894 entry.push(file.clone());
895 }
896 }
897
898 let mut conflicts = Vec::new();
899 for (name, mut srcs) in sources {
900 srcs.sort();
901 srcs.dedup();
902 if srcs.len() > 1 {
903 conflicts.push(ReExportConflict {
904 name,
905 sources: srcs,
906 });
907 }
908 }
909 conflicts.sort_by(|a, b| a.name.cmp(&b.name));
910 conflicts
911 }
912
913 pub fn non_exported_selective_imports(&self, file: &Path) -> Vec<NonExportedImport> {
925 let file = normalize_path(file);
926 let Some(module) = self.modules.get(&file) else {
927 return Vec::new();
928 };
929
930 let mut out = Vec::new();
931 for import in &module.imports {
932 let Some(selective) = &import.selective_names else {
933 continue;
934 };
935 let Some(import_path) = &import.path else {
936 continue;
937 };
938 let Some(target) = self
939 .modules
940 .get(import_path)
941 .or_else(|| self.modules.get(&normalize_path(import_path)))
942 else {
943 continue;
944 };
945 for name in selective {
946 if target.declarations.contains_key(name) && !target.exports.contains(name) {
950 out.push(NonExportedImport {
951 name: name.clone(),
952 module: import.raw_path.clone(),
953 });
954 }
955 }
956 }
957 out.sort_by(|a, b| (&a.name, &a.module).cmp(&(&b.name, &b.module)));
958 out.dedup();
959 out
960 }
961}
962
963#[derive(Debug, Clone, PartialEq, Eq)]
966pub struct ReExportConflict {
967 pub name: String,
968 pub sources: Vec<PathBuf>,
969}
970
971#[derive(Debug, Clone, PartialEq, Eq)]
974pub struct NonExportedImport {
975 pub name: String,
977 pub module: String,
979}
980
981fn load_module(
982 path: &Path,
983 package_snapshots: &[PackageSnapshot],
984) -> (ModuleInfo, Option<ParsedModuleSource>) {
985 let Some(source) = read_module_source(path) else {
986 return (ModuleInfo::default(), None);
987 };
988 let mut lexer = harn_lexer::Lexer::new(&source);
989 let tokens = match lexer.tokenize() {
990 Ok(tokens) => tokens,
991 Err(error) => {
992 let module = ModuleInfo {
993 load_error: Some(ModuleLoadError {
994 message: error.to_string(),
995 span: error.span(),
996 }),
997 ..ModuleInfo::default()
998 };
999 return (module, None);
1000 }
1001 };
1002 let mut parser = Parser::new(tokens);
1003 let program = match parser.parse() {
1004 Ok(program) => program,
1005 Err(error) => {
1006 let module = ModuleInfo {
1007 load_error: Some(ModuleLoadError {
1008 message: error.to_string(),
1009 span: error.span(),
1010 }),
1011 ..ModuleInfo::default()
1012 };
1013 return (module, None);
1014 }
1015 };
1016
1017 let mut module = ModuleInfo::default();
1018 for node in &program {
1019 collect_module_info(path, node, &mut module, package_snapshots);
1020 collect_type_declarations(node, &mut module.type_declarations);
1021 collect_callable_declarations(node, &mut module.callable_declarations);
1022 }
1023 module.exports.extend(module.own_exports.iter().cloned());
1027 module
1028 .exports
1029 .extend(module.selective_re_exports.keys().cloned());
1030 let parsed = ParsedModuleSource { source, program };
1031 (module, Some(parsed))
1032}
1033
1034fn stdlib_module_from_path(path: &Path) -> Option<&str> {
1037 let s = path.to_str()?;
1038 s.strip_prefix("<std>/")
1039}
1040
1041fn collect_module_info(
1042 file: &Path,
1043 snode: &SNode,
1044 module: &mut ModuleInfo,
1045 package_snapshots: &[PackageSnapshot],
1046) {
1047 match &snode.node {
1048 Node::FnDecl {
1049 name,
1050 params,
1051 is_pub,
1052 ..
1053 } => {
1054 if *is_pub {
1055 module.own_exports.insert(name.clone());
1056 }
1057 module.declarations.insert(
1058 name.clone(),
1059 decl_site(file, snode.span, name, DefKind::Function),
1060 );
1061 for param_name in params.iter().map(|param| param.name.clone()) {
1062 module.declarations.insert(
1063 param_name.clone(),
1064 decl_site(file, snode.span, ¶m_name, DefKind::Parameter),
1065 );
1066 }
1067 }
1068 Node::Pipeline { name, is_pub, .. } => {
1069 if *is_pub {
1070 module.own_exports.insert(name.clone());
1071 }
1072 module.declarations.insert(
1073 name.clone(),
1074 decl_site(file, snode.span, name, DefKind::Pipeline),
1075 );
1076 }
1077 Node::ToolDecl { name, is_pub, .. } => {
1078 if *is_pub {
1079 module.own_exports.insert(name.clone());
1080 }
1081 module.declarations.insert(
1082 name.clone(),
1083 decl_site(file, snode.span, name, DefKind::Tool),
1084 );
1085 }
1086 Node::SkillDecl { name, is_pub, .. } => {
1087 if *is_pub {
1088 module.own_exports.insert(name.clone());
1089 }
1090 module.declarations.insert(
1091 name.clone(),
1092 decl_site(file, snode.span, name, DefKind::Skill),
1093 );
1094 }
1095 Node::StructDecl { name, is_pub, .. } => {
1096 if *is_pub {
1097 module.own_exports.insert(name.clone());
1098 }
1099 module.declarations.insert(
1100 name.clone(),
1101 decl_site(file, snode.span, name, DefKind::Struct),
1102 );
1103 }
1104 Node::EnumDecl { name, is_pub, .. } => {
1105 if *is_pub {
1106 module.own_exports.insert(name.clone());
1107 }
1108 module.declarations.insert(
1109 name.clone(),
1110 decl_site(file, snode.span, name, DefKind::Enum),
1111 );
1112 }
1113 Node::InterfaceDecl { name, .. } => {
1114 module.own_exports.insert(name.clone());
1115 module.declarations.insert(
1116 name.clone(),
1117 decl_site(file, snode.span, name, DefKind::Interface),
1118 );
1119 }
1120 Node::TypeDecl { name, is_pub, .. } => {
1121 if *is_pub {
1122 module.own_exports.insert(name.clone());
1123 }
1124 module.declarations.insert(
1125 name.clone(),
1126 decl_site(file, snode.span, name, DefKind::Type),
1127 );
1128 }
1129 Node::LetBinding {
1130 pattern, is_pub, ..
1131 }
1132 | Node::ConstBinding {
1133 pattern, is_pub, ..
1134 } => {
1135 for name in pattern_names(pattern) {
1136 if *is_pub {
1140 module.own_exports.insert(name.clone());
1141 }
1142 module.declarations.insert(
1143 name.clone(),
1144 decl_site(file, snode.span, &name, DefKind::Variable),
1145 );
1146 }
1147 }
1148 Node::ImportDecl { path, is_pub } => {
1149 let import_path = resolve_import_path_with_snapshots(file, path, package_snapshots);
1150 if import_path.is_none() {
1151 module.has_unresolved_wildcard_import = true;
1152 }
1153 if *is_pub {
1154 if let Some(resolved) = &import_path {
1155 module
1156 .wildcard_re_export_paths
1157 .push(normalize_path(resolved));
1158 }
1159 }
1160 module.imports.push(ImportRef {
1161 raw_path: path.clone(),
1162 path: import_path,
1163 selective_names: None,
1164 import_span: snode.span,
1165 });
1166 }
1167 Node::SelectiveImport {
1168 names,
1169 path,
1170 is_pub,
1171 } => {
1172 let import_path = resolve_import_path_with_snapshots(file, path, package_snapshots);
1173 if import_path.is_none() {
1174 module.has_unresolved_selective_import = true;
1175 }
1176 if *is_pub {
1177 if let Some(resolved) = &import_path {
1178 let canonical = normalize_path(resolved);
1179 for name in names {
1180 module
1181 .selective_re_exports
1182 .entry(name.clone())
1183 .or_default()
1184 .push(canonical.clone());
1185 }
1186 }
1187 }
1188 let names: HashSet<String> = names.iter().cloned().collect();
1189 module.selective_import_names.extend(names.iter().cloned());
1190 module.imports.push(ImportRef {
1191 raw_path: path.clone(),
1192 path: import_path,
1193 selective_names: Some(names),
1194 import_span: snode.span,
1195 });
1196 }
1197 Node::AttributedDecl { inner, .. } => {
1198 collect_module_info(file, inner, module, package_snapshots);
1199 }
1200 _ => {}
1201 }
1202}
1203
1204fn collect_type_declarations(snode: &SNode, decls: &mut Vec<SNode>) {
1205 match &snode.node {
1206 Node::TypeDecl { .. }
1207 | Node::StructDecl { .. }
1208 | Node::EnumDecl { .. }
1209 | Node::InterfaceDecl { .. } => decls.push(snode.clone()),
1210 Node::AttributedDecl { inner, .. } => collect_type_declarations(inner, decls),
1211 _ => {}
1212 }
1213}
1214
1215fn collect_callable_declarations(snode: &SNode, decls: &mut Vec<SNode>) {
1216 match &snode.node {
1217 Node::FnDecl { .. } | Node::Pipeline { .. } | Node::ToolDecl { .. } => {
1218 decls.push(snode.clone());
1219 }
1220 Node::AttributedDecl { inner, .. } => collect_callable_declarations(inner, decls),
1221 _ => {}
1222 }
1223}
1224
1225fn type_decl_name(snode: &SNode) -> Option<&str> {
1226 match &snode.node {
1227 Node::TypeDecl { name, .. }
1228 | Node::StructDecl { name, .. }
1229 | Node::EnumDecl { name, .. }
1230 | Node::InterfaceDecl { name, .. } => Some(name.as_str()),
1231 _ => None,
1232 }
1233}
1234
1235fn callable_decl_name(snode: &SNode) -> Option<&str> {
1236 match &snode.node {
1237 Node::FnDecl { name, .. } | Node::Pipeline { name, .. } | Node::ToolDecl { name, .. } => {
1238 Some(name.as_str())
1239 }
1240 Node::AttributedDecl { inner, .. } => callable_decl_name(inner),
1241 _ => None,
1242 }
1243}
1244
1245fn decl_site(file: &Path, span: Span, name: &str, kind: DefKind) -> DefSite {
1246 DefSite {
1247 name: name.to_string(),
1248 file: file.to_path_buf(),
1249 kind,
1250 span,
1251 }
1252}
1253
1254fn pattern_names(pattern: &BindingPattern) -> Vec<String> {
1255 match pattern {
1256 BindingPattern::Identifier(name) => vec![name.clone()],
1257 BindingPattern::Dict(fields) => fields
1258 .iter()
1259 .filter_map(|field| field.alias.as_ref().or(Some(&field.key)).cloned())
1260 .collect(),
1261 BindingPattern::List(elements) => elements
1262 .iter()
1263 .map(|element| element.name.clone())
1264 .collect(),
1265 BindingPattern::Pair(a, b) => vec![a.clone(), b.clone()],
1266 }
1267}
1268
1269fn normalize_path(path: &Path) -> PathBuf {
1270 canonical_path(path)
1271}
1272
1273pub fn canonical_path(path: &Path) -> PathBuf {
1286 use std::sync::OnceLock;
1287 if stdlib_module_from_path(path).is_some() {
1288 return path.to_path_buf();
1289 }
1290 static MEMO: OnceLock<std::sync::Mutex<HashMap<PathBuf, PathBuf>>> = OnceLock::new();
1291 let memo = MEMO.get_or_init(|| std::sync::Mutex::new(HashMap::new()));
1292 if let Some(hit) = memo
1293 .lock()
1294 .expect("canonical path memo lock poisoned")
1295 .get(path)
1296 .cloned()
1297 {
1298 return hit;
1299 }
1300 match path.canonicalize() {
1301 Ok(canonical) => {
1302 memo.lock()
1303 .expect("canonical path memo lock poisoned")
1304 .insert(path.to_path_buf(), canonical.clone());
1305 canonical
1306 }
1307 Err(_) => path.to_path_buf(),
1308 }
1309}
1310
1311#[cfg(test)]
1312#[path = "tests.rs"]
1313mod tests;