1use deps_core::error::{DepsError, Result};
4use deps_core::lockfile::{
5 LockFileProvider, ResolvedPackage, ResolvedPackages, ResolvedSource,
6 locate_lockfile_for_manifest,
7};
8use std::path::{Path, PathBuf};
9use tower_lsp_server::ls_types::Uri;
10use yaml_rust2::{Yaml, YamlLoader};
11
12pub struct PubspecLockParser;
13
14impl PubspecLockParser {
15 const LOCKFILE_NAMES: &'static [&'static str] = &["pubspec.lock"];
16}
17
18impl LockFileProvider for PubspecLockParser {
19 fn locate_lockfile(&self, manifest_uri: &Uri) -> Option<PathBuf> {
20 locate_lockfile_for_manifest(manifest_uri, Self::LOCKFILE_NAMES)
21 }
22
23 fn parse_lockfile<'a>(
24 &'a self,
25 lockfile_path: &'a Path,
26 ) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<ResolvedPackages>> + Send + 'a>>
27 {
28 Box::pin(async move {
29 tracing::debug!("Parsing pubspec.lock: {}", lockfile_path.display());
30
31 let content = tokio::fs::read_to_string(lockfile_path)
32 .await
33 .map_err(|e| DepsError::ParseError {
34 file_type: format!("pubspec.lock at {}", lockfile_path.display()),
35 source: Box::new(e),
36 })?;
37
38 parse_pubspec_lock(&content)
39 })
40 }
41}
42
43pub fn parse_pubspec_lock(content: &str) -> Result<ResolvedPackages> {
44 let mut packages = ResolvedPackages::new();
45
46 let docs = YamlLoader::load_from_str(content).map_err(|e| DepsError::ParseError {
47 file_type: "pubspec.lock".into(),
48 source: Box::new(std::io::Error::other(e.to_string())),
49 })?;
50
51 let doc = match docs.first() {
52 Some(d) => d,
53 None => return Ok(packages),
54 };
55
56 if let Yaml::Hash(pkgs) = &doc["packages"] {
57 for (name_yaml, entry) in pkgs {
58 let Some(name) = name_yaml.as_str() else {
59 continue;
60 };
61 let Some(version) = entry["version"].as_str() else {
62 continue;
63 };
64
65 let source_type = entry["source"].as_str().unwrap_or("hosted");
66 let source = match source_type {
67 "hosted" => {
68 let url = entry["description"]["url"]
69 .as_str()
70 .unwrap_or("https://pub.dev")
71 .to_string();
72 ResolvedSource::Registry {
73 url,
74 checksum: String::new(),
75 }
76 }
77 "git" => {
78 let url = entry["description"]["url"]
79 .as_str()
80 .unwrap_or("")
81 .to_string();
82 let rev = entry["description"]["resolved-ref"]
83 .as_str()
84 .unwrap_or("")
85 .to_string();
86 ResolvedSource::Git { url, rev }
87 }
88 "path" => {
89 let path = entry["description"]["path"]
90 .as_str()
91 .unwrap_or("")
92 .to_string();
93 ResolvedSource::Path { path }
94 }
95 _ => ResolvedSource::Registry {
96 url: "https://pub.dev".to_string(),
97 checksum: String::new(),
98 },
99 };
100
101 let version = version.trim_matches('"').to_string();
103
104 packages.insert(ResolvedPackage {
105 name: name.to_string(),
106 version,
107 source,
108 dependencies: vec![],
109 });
110 }
111 }
112
113 tracing::info!("Parsed pubspec.lock: {} packages", packages.len());
114
115 Ok(packages)
116}
117
118#[cfg(test)]
119mod tests {
120 use super::*;
121
122 #[test]
123 fn test_parse_simple_lock() {
124 let lock = r#"
125packages:
126 http:
127 dependency: "direct main"
128 description:
129 name: http
130 url: "https://pub.dev"
131 source: hosted
132 version: "1.2.0"
133 provider:
134 dependency: "direct main"
135 description:
136 name: provider
137 url: "https://pub.dev"
138 source: hosted
139 version: "6.1.2"
140"#;
141 let packages = parse_pubspec_lock(lock).unwrap();
142 assert_eq!(packages.len(), 2);
143 assert_eq!(packages.get_version("http"), Some("1.2.0"));
144 assert_eq!(packages.get_version("provider"), Some("6.1.2"));
145 }
146
147 #[test]
148 fn test_parse_git_source() {
149 let lock = r#"
150packages:
151 my_pkg:
152 dependency: "direct main"
153 description:
154 url: "https://github.com/user/repo.git"
155 resolved-ref: abc123
156 source: git
157 version: "0.1.0"
158"#;
159 let packages = parse_pubspec_lock(lock).unwrap();
160 let pkg = packages.get("my_pkg").unwrap();
161 match &pkg.source {
162 ResolvedSource::Git { url, rev } => {
163 assert_eq!(url, "https://github.com/user/repo.git");
164 assert_eq!(rev, "abc123");
165 }
166 _ => panic!("Expected Git source"),
167 }
168 }
169
170 #[test]
171 fn test_parse_path_source() {
172 let lock = r#"
173packages:
174 local_pkg:
175 dependency: "direct main"
176 description:
177 path: "../local_pkg"
178 source: path
179 version: "0.1.0"
180"#;
181 let packages = parse_pubspec_lock(lock).unwrap();
182 let pkg = packages.get("local_pkg").unwrap();
183 match &pkg.source {
184 ResolvedSource::Path { path } => {
185 assert_eq!(path, "../local_pkg");
186 }
187 _ => panic!("Expected Path source"),
188 }
189 }
190
191 #[test]
192 fn test_parse_empty_lock() {
193 let lock = "";
194 let packages = parse_pubspec_lock(lock).unwrap();
195 assert!(packages.is_empty());
196 }
197
198 #[test]
199 fn test_locate_lockfile() {
200 let temp_dir = tempfile::tempdir().unwrap();
201 let manifest_path = temp_dir.path().join("pubspec.yaml");
202 let lock_path = temp_dir.path().join("pubspec.lock");
203
204 std::fs::write(&manifest_path, "name: test").unwrap();
205 std::fs::write(&lock_path, "packages:\n").unwrap();
206
207 let manifest_uri = Uri::from_file_path(&manifest_path).unwrap();
208 let parser = PubspecLockParser;
209
210 let located = parser.locate_lockfile(&manifest_uri);
211 assert!(located.is_some());
212 assert_eq!(located.unwrap(), lock_path);
213 }
214
215 #[test]
216 fn test_locate_lockfile_not_found() {
217 let temp_dir = tempfile::tempdir().unwrap();
218 let manifest_path = temp_dir.path().join("pubspec.yaml");
219 std::fs::write(&manifest_path, "name: test").unwrap();
220
221 let manifest_uri = Uri::from_file_path(&manifest_path).unwrap();
222 let parser = PubspecLockParser;
223
224 assert!(parser.locate_lockfile(&manifest_uri).is_none());
225 }
226
227 #[tokio::test]
228 async fn test_parse_lockfile_from_file() {
229 let temp_dir = tempfile::tempdir().unwrap();
230 let lock_path = temp_dir.path().join("pubspec.lock");
231
232 let content = r#"
233packages:
234 http:
235 dependency: "direct main"
236 description:
237 name: http
238 url: "https://pub.dev"
239 source: hosted
240 version: "1.2.0"
241"#;
242 std::fs::write(&lock_path, content).unwrap();
243
244 let parser = PubspecLockParser;
245 let packages = parser.parse_lockfile(&lock_path).await.unwrap();
246 assert_eq!(packages.len(), 1);
247 assert_eq!(packages.get_version("http"), Some("1.2.0"));
248 }
249}