Skip to main content

callisto_graph/
walk.rs

1use std::cell::RefCell;
2use std::collections::BTreeMap;
3use std::path::{Path, PathBuf};
4use std::sync::Arc;
5
6use callisto_manifests::{
7    detect_npm_workspace_kind, Manifest, OpenContext, WorkspaceCargoResolver,
8};
9use callisto_model::{
10    CommandRunner, DepEdge, Ecosystem, ManifestDecl, ManifestFormat, ManifestRole, Package,
11    PackageId, PublishTarget, ReleaseTrigger,
12};
13
14use crate::config::ResolvedConfig;
15use crate::crosscheck::crosscheck_declared_edges;
16use crate::error::GraphError;
17use crate::identity::IdentityIndex;
18use crate::locate::ProjectLocator;
19use crate::manifest_cache::open_cached;
20use crate::resolver::ManifestWalkResolver;
21
22impl ManifestWalkResolver {
23    pub fn build<L: ProjectLocator, R: CommandRunner>(
24        root: &Path,
25        locator: &L,
26        _runner: &R,
27        cfg: &ResolvedConfig,
28        manifest_cache: &RefCell<BTreeMap<PathBuf, Arc<dyn Manifest>>>,
29    ) -> Result<Self, GraphError> {
30        let projects = locator.projects()?;
31
32        let cargo_workspace = if root.join("Cargo.toml").exists() {
33            if let Ok(resolver) = WorkspaceCargoResolver::load(&root.join("Cargo.toml")) {
34                resolver.inheritance().ok().map(Arc::new)
35            } else {
36                None
37            }
38        } else {
39            None
40        };
41
42        let npm_workspace_kind = detect_npm_workspace_kind(root).ok().flatten();
43
44        let ctx = OpenContext {
45            workspace_root: root,
46            cargo_workspace,
47            npm_workspace_kind,
48        };
49
50        let mut package_manifest_decls: BTreeMap<PackageId, (PathBuf, Vec<ManifestDecl>)> =
51            BTreeMap::new();
52        let mut index = IdentityIndex::default();
53        let mut diagnostics = Vec::new();
54
55        // Use the identity already resolved by the locator (`proj.id`) rather
56        // than re-reading manifests through `IdentityResolver::resolve`.
57        // The locator (e.g. `IgnoreWalkLocator`) already parsed each manifest
58        // to discover the project, so re-resolving from scratch is redundant
59        // and fragile — in particular, `IdentityResolver` historically had no
60        // `Ecosystem::Pypi` arm and would crash for Python projects.
61        let mut by_path: BTreeMap<PathBuf, Vec<(Ecosystem, PackageId)>> = BTreeMap::new();
62        for proj in &projects {
63            by_path
64                .entry(proj.path.clone())
65                .or_default()
66                .push((proj.ecosystem, proj.id.clone()));
67        }
68
69        for (rel_path, mut list) in by_path {
70            list.sort_by_key(|a| a.0);
71            let primary_id = list[0].1.clone();
72
73            index
74                .bare
75                .insert(primary_id.name().to_string(), primary_id.clone());
76
77            let mut decls = Vec::new();
78            for (eco, _id) in &list {
79                let (fmt, filename) = match eco {
80                    Ecosystem::Cargo => (ManifestFormat::CargoToml, "Cargo.toml"),
81                    Ecosystem::Npm => (ManifestFormat::PackageJson, "package.json"),
82                    Ecosystem::Pypi => (ManifestFormat::PyprojectToml, "pyproject.toml"),
83                    _ => (ManifestFormat::PackageJson, "package.json"),
84                };
85                let manifest_rel = rel_path.join(filename);
86                if let Ok(decl) = ManifestDecl::new(manifest_rel, ManifestRole::Canonical, fmt) {
87                    decls.push(decl);
88                }
89                index
90                    .native
91                    .insert((*eco, primary_id.name().to_string()), primary_id.clone());
92            }
93
94            package_manifest_decls.insert(primary_id, (rel_path, decls));
95        }
96
97        let mut packages = BTreeMap::new();
98        for (id, (rel_path, decls)) in package_manifest_decls {
99            let ch_path = rel_path.join("CHANGELOG.md");
100            let mut publish_to = Vec::new();
101            for decl in &decls {
102                if let Ok(editor) = open_cached(manifest_cache, decl, &ctx) {
103                    for target in editor.publish_targets() {
104                        if target != PublishTarget::None && !publish_to.contains(&target) {
105                            publish_to.push(target);
106                        }
107                    }
108                }
109            }
110            if publish_to.is_empty() {
111                publish_to.push(PublishTarget::None);
112            }
113
114            // Find the first [[package]] rule in callisto.toml whose pattern
115            // matches this package's ID (exact or bare-name match).
116            let pkg_override = cfg
117                .packages
118                .iter()
119                .find(|(pattern, _)| pattern.matches(&id))
120                .map(|(_, cfg)| cfg);
121
122            let release_trigger = pkg_override
123                .and_then(|o| o.release_trigger)
124                .unwrap_or(ReleaseTrigger::Changeset);
125
126            let tag_template = pkg_override.and_then(|o| o.tag_template.clone());
127
128            let changelog =
129                if let Some(override_path) = pkg_override.and_then(|o| o.changelog.as_ref()) {
130                    Some(rel_path.join(override_path))
131                } else {
132                    Some(ch_path)
133                };
134
135            let pkg = Package {
136                id: id.clone(),
137                manifests: decls,
138                changelog,
139                release_trigger,
140                publish_to,
141                tag_template,
142            };
143            packages.insert(id, pkg);
144        }
145
146        let mut edges = Vec::new();
147        let mut out_index: BTreeMap<PackageId, Vec<usize>> = BTreeMap::new();
148        let mut in_index: BTreeMap<PackageId, Vec<usize>> = BTreeMap::new();
149
150        for pkg in packages.values() {
151            for decl in &pkg.manifests {
152                if decl.role != ManifestRole::Canonical {
153                    continue;
154                }
155                if let Ok(m) = open_cached(manifest_cache, decl, &ctx) {
156                    for entry in m.iter_dependencies() {
157                        let (spec, declaring_path) = if entry.inherited {
158                            if let Some(ref inh) = ctx.cargo_workspace {
159                                if let Some(inherited_dep) = inh.inherited(&entry.name) {
160                                    (
161                                        inherited_dep.spec.clone(),
162                                        inherited_dep.declared_in.to_path_buf(),
163                                    )
164                                } else {
165                                    (entry.spec.clone(), decl.path.clone())
166                                }
167                            } else {
168                                (entry.spec.clone(), decl.path.clone())
169                            }
170                        } else {
171                            (entry.spec.clone(), decl.path.clone())
172                        };
173
174                        if let Some(to) = index.resolve_native_with_fallback(
175                            decl.ecosystem(),
176                            &entry.name,
177                            &mut diagnostics,
178                        ) {
179                            let idx = edges.len();
180                            let edge = DepEdge {
181                                from: pkg.id.clone(),
182                                to: to.clone(),
183                                kind: entry.kind,
184                                spec,
185                                from_manifest: declaring_path,
186                                inherited: entry.inherited,
187                            };
188                            edges.push(edge);
189
190                            out_index.entry(pkg.id.clone()).or_default().push(idx);
191                            in_index.entry(to.clone()).or_default().push(idx);
192                        }
193                    }
194                }
195            }
196        }
197
198        if let Some(declared) = locator.declared_edges() {
199            let cross_diags = crosscheck_declared_edges(&packages, &edges, &declared);
200            diagnostics.extend(cross_diags);
201        }
202
203        Ok(ManifestWalkResolver {
204            packages,
205            edges,
206            out_index,
207            in_index,
208            index,
209            diagnostics,
210        })
211    }
212}