Skip to main content

callisto_graph/
walk.rs

1use std::collections::BTreeMap;
2use std::path::{Path, PathBuf};
3use std::sync::Arc;
4
5use callisto_manifests::{detect_npm_workspace_kind, open, OpenContext, WorkspaceCargoResolver};
6use callisto_model::{
7    CommandRunner, DepEdge, Ecosystem, ManifestDecl, ManifestFormat, ManifestRole, Package,
8    PackageId, PublishTarget, ReleaseTrigger,
9};
10
11use crate::config::ResolvedConfig;
12use crate::crosscheck::crosscheck_declared_edges;
13use crate::error::GraphError;
14use crate::identity::{IdentityIndex, IdentityResolver};
15use crate::locate::ProjectLocator;
16use crate::resolver::ManifestWalkResolver;
17
18impl ManifestWalkResolver {
19    pub fn build<L: ProjectLocator, R: CommandRunner>(
20        root: &Path,
21        locator: &L,
22        _runner: &R,
23        _cfg: &ResolvedConfig,
24    ) -> Result<Self, GraphError> {
25        let projects = locator.projects()?;
26
27        let cargo_workspace = if root.join("Cargo.toml").exists() {
28            if let Ok(resolver) = WorkspaceCargoResolver::load(&root.join("Cargo.toml")) {
29                resolver.inheritance().ok().map(Arc::new)
30            } else {
31                None
32            }
33        } else {
34            None
35        };
36
37        let npm_workspace_kind = detect_npm_workspace_kind(root).ok().flatten();
38
39        let ctx = OpenContext {
40            workspace_root: root,
41            cargo_workspace,
42            npm_workspace_kind,
43        };
44
45        let identity_resolver = IdentityResolver::new(root)?;
46        let mut package_manifest_decls: BTreeMap<PackageId, (PathBuf, Vec<ManifestDecl>)> =
47            BTreeMap::new();
48        let mut index = IdentityIndex::default();
49        let mut diagnostics = Vec::new();
50
51        let mut by_path: BTreeMap<PathBuf, Vec<(Ecosystem, PackageId)>> = BTreeMap::new();
52        for proj in &projects {
53            let id = identity_resolver.resolve(&proj.path, proj.ecosystem)?;
54            by_path
55                .entry(proj.path.clone())
56                .or_default()
57                .push((proj.ecosystem, id));
58        }
59
60        for (rel_path, mut list) in by_path {
61            list.sort_by_key(|a| a.0);
62            let primary_id = list[0].1.clone();
63
64            index
65                .bare
66                .insert(primary_id.name().to_string(), primary_id.clone());
67
68            let mut decls = Vec::new();
69            for (eco, _id) in &list {
70                let (fmt, filename) = match eco {
71                    Ecosystem::Cargo => (ManifestFormat::CargoToml, "Cargo.toml"),
72                    Ecosystem::Npm => (ManifestFormat::PackageJson, "package.json"),
73                    _ => (ManifestFormat::PackageJson, "package.json"),
74                };
75                let manifest_rel = rel_path.join(filename);
76                if let Ok(decl) = ManifestDecl::new(manifest_rel, ManifestRole::Canonical, fmt) {
77                    decls.push(decl);
78                }
79                index
80                    .native
81                    .insert((*eco, primary_id.name().to_string()), primary_id.clone());
82            }
83
84            package_manifest_decls.insert(primary_id, (rel_path, decls));
85        }
86
87        let mut packages = BTreeMap::new();
88        for (id, (rel_path, decls)) in package_manifest_decls {
89            let ch_path = rel_path.join("CHANGELOG.md");
90            let mut publish_to = Vec::new();
91            for decl in &decls {
92                if let Ok(editor) = callisto_manifests::open(decl, &ctx) {
93                    for target in editor.publish_targets() {
94                        if target != PublishTarget::None && !publish_to.contains(&target) {
95                            publish_to.push(target);
96                        }
97                    }
98                }
99            }
100            if publish_to.is_empty() {
101                publish_to.push(PublishTarget::None);
102            }
103
104            let pkg = Package {
105                id: id.clone(),
106                manifests: decls,
107                changelog: Some(ch_path),
108                release_trigger: ReleaseTrigger::Changeset,
109                publish_to,
110                tag_template: None,
111            };
112            packages.insert(id, pkg);
113        }
114
115        let mut edges = Vec::new();
116        let mut out_index: BTreeMap<PackageId, Vec<usize>> = BTreeMap::new();
117        let mut in_index: BTreeMap<PackageId, Vec<usize>> = BTreeMap::new();
118
119        for pkg in packages.values() {
120            for decl in &pkg.manifests {
121                if decl.role != ManifestRole::Canonical {
122                    continue;
123                }
124                if let Ok(m) = open(decl, &ctx) {
125                    for entry in m.iter_dependencies() {
126                        let (spec, declaring_path) = if entry.inherited {
127                            if let Some(ref inh) = ctx.cargo_workspace {
128                                if let Some(inherited_dep) = inh.inherited(&entry.name) {
129                                    (
130                                        inherited_dep.spec.clone(),
131                                        inherited_dep.declared_in.to_path_buf(),
132                                    )
133                                } else {
134                                    (entry.spec.clone(), decl.path.clone())
135                                }
136                            } else {
137                                (entry.spec.clone(), decl.path.clone())
138                            }
139                        } else {
140                            (entry.spec.clone(), decl.path.clone())
141                        };
142
143                        if let Some(to) = index.resolve_native(decl.ecosystem(), &entry.name) {
144                            let idx = edges.len();
145                            let edge = DepEdge {
146                                from: pkg.id.clone(),
147                                to: to.clone(),
148                                kind: entry.kind,
149                                spec,
150                                from_manifest: declaring_path,
151                                inherited: entry.inherited,
152                            };
153                            edges.push(edge);
154
155                            out_index.entry(pkg.id.clone()).or_default().push(idx);
156                            in_index.entry(to.clone()).or_default().push(idx);
157                        }
158                    }
159                }
160            }
161        }
162
163        if let Some(declared) = locator.declared_edges() {
164            let cross_diags = crosscheck_declared_edges(
165                &ManifestWalkResolver {
166                    packages: packages.clone(),
167                    edges: edges.clone(),
168                    out_index: out_index.clone(),
169                    in_index: in_index.clone(),
170                    index: index.clone(),
171                    diagnostics: Vec::new(),
172                },
173                &declared,
174            );
175            diagnostics.extend(cross_diags);
176        }
177
178        Ok(ManifestWalkResolver {
179            packages,
180            edges,
181            out_index,
182            in_index,
183            index,
184            diagnostics,
185        })
186    }
187}