Skip to main content

kdo_resolver/
anchor.rs

1//! Parser for `Anchor.toml` manifests (Solana Anchor framework).
2
3use crate::ManifestParser;
4use kdo_core::{DepKind, Dependency, KdoError, Language, Project};
5use std::path::Path;
6use tracing::debug;
7
8/// Parses Anchor workspace `Anchor.toml` manifests.
9pub struct AnchorParser;
10
11impl ManifestParser for AnchorParser {
12    fn manifest_name(&self) -> &str {
13        "Anchor.toml"
14    }
15
16    fn can_parse(&self, manifest_path: &Path) -> bool {
17        manifest_path
18            .file_name()
19            .map(|f| f == "Anchor.toml")
20            .unwrap_or(false)
21    }
22
23    fn parse(
24        &self,
25        manifest_path: &Path,
26        workspace_root: &Path,
27    ) -> Result<(Project, Vec<Dependency>), KdoError> {
28        let content = std::fs::read_to_string(manifest_path)?;
29        let doc: toml::Value = toml::from_str(&content).map_err(|e| KdoError::ParseError {
30            path: manifest_path.to_path_buf(),
31            source: e.into(),
32        })?;
33
34        let project_dir = manifest_path
35            .parent()
36            .unwrap_or(Path::new("."))
37            .to_path_buf();
38
39        // Anchor.toml has [programs.<network>] with program_name = "address"
40        // and [workspace] with members = ["programs/*"]
41        let programs = doc
42            .get("programs")
43            .and_then(|p| p.as_table())
44            .and_then(|t| {
45                // Get the first network's programs (usually "localnet")
46                t.values().next().and_then(|v| v.as_table())
47            });
48
49        let program_names: Vec<String> = programs
50            .map(|p| p.keys().cloned().collect())
51            .unwrap_or_default();
52
53        // Use first program name or directory name
54        let name = program_names.first().cloned().unwrap_or_else(|| {
55            project_dir
56                .file_name()
57                .map(|f| f.to_string_lossy().to_string())
58                .unwrap_or_else(|| "anchor-project".to_string())
59        });
60
61        debug!(name = %name, programs = ?program_names, "parsed Anchor.toml");
62
63        // Scan for sub-program Cargo.toml files to find CPI dependencies
64        let mut deps = Vec::new();
65        if let Some(workspace) = doc.get("workspace") {
66            if let Some(members) = workspace.get("members").and_then(|m| m.as_array()) {
67                for member in members {
68                    if let Some(member_str) = member.as_str() {
69                        // Members can be globs like "programs/*"
70                        let member_path = workspace_root.join(member_str);
71                        if member_path.is_dir() {
72                            deps.push(Dependency {
73                                name: member_path
74                                    .file_name()
75                                    .map(|f| f.to_string_lossy().to_string())
76                                    .unwrap_or_default(),
77                                version_req: "path".to_string(),
78                                kind: DepKind::Source,
79                                is_workspace: true,
80                                resolved_path: Some(member_path),
81                            });
82                        }
83                    }
84                }
85            }
86        }
87
88        let project = Project {
89            name,
90            path: project_dir,
91            language: Language::Anchor,
92            manifest_path: manifest_path.to_path_buf(),
93            context_summary: None,
94            public_api_files: Vec::new(),
95            internal_files: Vec::new(),
96            content_hash: [0u8; 32],
97        };
98
99        Ok((project, deps))
100    }
101}