1use async_trait::async_trait;
29use deps_core::error::{DepsError, Result};
30use deps_core::lockfile::{
31 LockFileProvider, ResolvedPackage, ResolvedPackages, ResolvedSource,
32 locate_lockfile_for_manifest,
33};
34use std::path::{Path, PathBuf};
35use tower_lsp_server::ls_types::Uri;
36
37pub struct GoSumParser;
67
68impl GoSumParser {
69 const LOCKFILE_NAMES: &'static [&'static str] = &["go.sum"];
71}
72
73#[async_trait]
74impl LockFileProvider for GoSumParser {
75 fn locate_lockfile(&self, manifest_uri: &Uri) -> Option<PathBuf> {
76 locate_lockfile_for_manifest(manifest_uri, Self::LOCKFILE_NAMES)
77 }
78
79 async fn parse_lockfile(&self, lockfile_path: &Path) -> Result<ResolvedPackages> {
80 let content = tokio::fs::read_to_string(lockfile_path)
81 .await
82 .map_err(|e| DepsError::ParseError {
83 file_type: format!("go.sum at {}", lockfile_path.display()),
84 source: Box::new(e),
85 })?;
86
87 Ok(parse_go_sum(&content))
88 }
89}
90
91pub fn parse_go_sum(content: &str) -> ResolvedPackages {
123 let mut packages = ResolvedPackages::new();
124
125 for line in content.lines() {
126 let line = line.trim();
127 if line.is_empty() {
128 continue;
129 }
130
131 if line.contains("/go.mod ") {
133 continue;
134 }
135
136 let parts: Vec<&str> = line.split_whitespace().collect();
139 if parts.len() >= 3 {
140 let module_path = parts[0];
141 let version = parts[1];
142 let checksum = parts[2];
143
144 if !checksum.starts_with("h1:") {
147 continue;
148 }
149
150 packages.insert(ResolvedPackage {
153 name: module_path.to_string(),
154 version: version.to_string(),
155 source: ResolvedSource::Registry {
156 url: "https://proxy.golang.org".to_string(),
157 checksum: checksum.to_string(),
158 },
159 dependencies: vec![],
160 });
161 }
162 }
163
164 packages
165}
166
167#[cfg(test)]
168mod tests {
169 use super::*;
170
171 #[test]
172 fn test_parse_simple_go_sum() {
173 let content = r"
174github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg=
175github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL9t9/HBtKc7e/Q7Nb2nqKqTW8mHZy6E7k8m4dLvs=
176";
177 let packages = parse_go_sum(content);
178 assert_eq!(
179 packages.get_version("github.com/gin-gonic/gin"),
180 Some("v1.9.1")
181 );
182 }
183
184 #[test]
185 fn test_parse_multiple_modules() {
186 let content = r"
187github.com/gin-gonic/gin v1.9.1 h1:hash1=
188golang.org/x/sync v0.5.0 h1:hash2=
189github.com/stretchr/testify v1.8.4 h1:hash3=
190";
191 let packages = parse_go_sum(content);
192 assert_eq!(packages.len(), 3);
193 assert_eq!(
194 packages.get_version("github.com/gin-gonic/gin"),
195 Some("v1.9.1")
196 );
197 assert_eq!(packages.get_version("golang.org/x/sync"), Some("v0.5.0"));
198 assert_eq!(
199 packages.get_version("github.com/stretchr/testify"),
200 Some("v1.8.4")
201 );
202 }
203
204 #[test]
205 fn test_skip_go_mod_entries() {
206 let content = r"
207github.com/gin-gonic/gin v1.9.1/go.mod h1:mod_hash=
208github.com/gin-gonic/gin v1.9.1 h1:actual_hash=
209";
210 let packages = parse_go_sum(content);
211 assert_eq!(packages.len(), 1);
212 assert_eq!(
213 packages.get_version("github.com/gin-gonic/gin"),
214 Some("v1.9.1")
215 );
216 }
217
218 #[test]
219 fn test_last_version_wins() {
220 let content = r"
221github.com/pkg/errors v0.8.0 h1:hash1=
222github.com/pkg/errors v0.9.1 h1:hash2=
223";
224 let packages = parse_go_sum(content);
225 assert_eq!(packages.len(), 1);
226 assert_eq!(
228 packages.get_version("github.com/pkg/errors"),
229 Some("v0.9.1")
230 );
231 }
232
233 #[test]
234 fn test_empty_content() {
235 let packages = parse_go_sum("");
236 assert!(packages.is_empty());
237 }
238
239 #[test]
240 fn test_whitespace_handling() {
241 let content = " github.com/gin-gonic/gin v1.9.1 h1:hash= \n";
242 let packages = parse_go_sum(content);
243 assert_eq!(
244 packages.get_version("github.com/gin-gonic/gin"),
245 Some("v1.9.1")
246 );
247 }
248
249 #[test]
250 fn test_lockfile_provider_trait() {
251 let parser = GoSumParser;
252 let manifest_path = "/test/go.mod";
253 let uri = Uri::from_file_path(manifest_path).unwrap();
254
255 let _ = parser.locate_lockfile(&uri);
257 }
258
259 #[test]
260 fn test_pseudo_version() {
261 let content = "golang.org/x/tools v0.0.0-20191109021931-daa7c04131f5 h1:hash=\n";
262 let packages = parse_go_sum(content);
263 assert_eq!(
264 packages.get_version("golang.org/x/tools"),
265 Some("v0.0.0-20191109021931-daa7c04131f5")
266 );
267 }
268
269 #[test]
270 fn test_incompatible_version() {
271 let content = "github.com/some/module v2.0.0+incompatible h1:hash=\n";
272 let packages = parse_go_sum(content);
273 assert_eq!(
274 packages.get_version("github.com/some/module"),
275 Some("v2.0.0+incompatible")
276 );
277 }
278
279 #[test]
280 fn test_malformed_line_ignored() {
281 let content = r"
282github.com/gin-gonic/gin v1.9.1 h1:hash=
283invalid line with only one part
284github.com/valid/pkg v1.0.0 h1:valid_hash=
285";
286 let packages = parse_go_sum(content);
287 assert_eq!(packages.len(), 2);
289 assert_eq!(
290 packages.get_version("github.com/gin-gonic/gin"),
291 Some("v1.9.1")
292 );
293 assert_eq!(packages.get_version("github.com/valid/pkg"), Some("v1.0.0"));
294 }
295
296 #[tokio::test]
297 async fn test_parse_lockfile_simple() {
298 let lockfile_content = r"
299github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg=
300github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL9t9/HBtKc7e/Q7Nb2nqKqTW8mHZy6E7k8m4dLvs=
301golang.org/x/sync v0.5.0 h1:60k92dhOjHxJkrq=
302golang.org/x/sync v0.5.0/go.mod h1:RxMgew5V=
303";
304
305 let temp_dir = tempfile::tempdir().unwrap();
306 let lockfile_path = temp_dir.path().join("go.sum");
307 std::fs::write(&lockfile_path, lockfile_content).unwrap();
308
309 let parser = GoSumParser;
310 let resolved = parser.parse_lockfile(&lockfile_path).await.unwrap();
311
312 assert_eq!(resolved.len(), 2);
313 assert_eq!(
314 resolved.get_version("github.com/gin-gonic/gin"),
315 Some("v1.9.1")
316 );
317 assert_eq!(resolved.get_version("golang.org/x/sync"), Some("v0.5.0"));
318 }
319
320 #[tokio::test]
321 async fn test_parse_lockfile_empty() {
322 let lockfile_content = "";
323
324 let temp_dir = tempfile::tempdir().unwrap();
325 let lockfile_path = temp_dir.path().join("go.sum");
326 std::fs::write(&lockfile_path, lockfile_content).unwrap();
327
328 let parser = GoSumParser;
329 let resolved = parser.parse_lockfile(&lockfile_path).await.unwrap();
330
331 assert_eq!(resolved.len(), 0);
332 assert!(resolved.is_empty());
333 }
334
335 #[tokio::test]
336 async fn test_parse_lockfile_not_found() {
337 let temp_dir = tempfile::tempdir().unwrap();
338 let lockfile_path = temp_dir.path().join("nonexistent.sum");
339
340 let parser = GoSumParser;
341 let result = parser.parse_lockfile(&lockfile_path).await;
342
343 assert!(result.is_err());
344 }
345
346 #[test]
347 fn test_locate_lockfile_same_directory() {
348 let temp_dir = tempfile::tempdir().unwrap();
349 let manifest_path = temp_dir.path().join("go.mod");
350 let lock_path = temp_dir.path().join("go.sum");
351
352 std::fs::write(&manifest_path, "module test").unwrap();
353 std::fs::write(&lock_path, "").unwrap();
354
355 let manifest_uri = Uri::from_file_path(&manifest_path).unwrap();
356 let parser = GoSumParser;
357
358 let located = parser.locate_lockfile(&manifest_uri);
359 assert!(located.is_some());
360 assert_eq!(located.unwrap(), lock_path);
361 }
362
363 #[test]
364 fn test_locate_lockfile_workspace_root() {
365 let temp_dir = tempfile::tempdir().unwrap();
366 let workspace_lock = temp_dir.path().join("go.sum");
367 let member_dir = temp_dir.path().join("packages").join("member");
368 std::fs::create_dir_all(&member_dir).unwrap();
369 let member_manifest = member_dir.join("go.mod");
370
371 std::fs::write(&workspace_lock, "").unwrap();
372 std::fs::write(&member_manifest, "module member").unwrap();
373
374 let manifest_uri = Uri::from_file_path(&member_manifest).unwrap();
375 let parser = GoSumParser;
376
377 let located = parser.locate_lockfile(&manifest_uri);
378 assert!(located.is_some());
379 assert_eq!(located.unwrap(), workspace_lock);
380 }
381
382 #[test]
383 fn test_locate_lockfile_not_found() {
384 let temp_dir = tempfile::tempdir().unwrap();
385 let manifest_path = temp_dir.path().join("go.mod");
386 std::fs::write(&manifest_path, "module test").unwrap();
387
388 let manifest_uri = Uri::from_file_path(&manifest_path).unwrap();
389 let parser = GoSumParser;
390
391 let located = parser.locate_lockfile(&manifest_uri);
392 assert!(located.is_none());
393 }
394
395 #[test]
396 fn test_is_lockfile_stale_not_modified() {
397 let temp_dir = tempfile::tempdir().unwrap();
398 let lockfile_path = temp_dir.path().join("go.sum");
399 std::fs::write(&lockfile_path, "").unwrap();
400
401 let mtime = std::fs::metadata(&lockfile_path)
402 .unwrap()
403 .modified()
404 .unwrap();
405 let parser = GoSumParser;
406
407 assert!(
408 !parser.is_lockfile_stale(&lockfile_path, mtime),
409 "Lock file should not be stale when mtime matches"
410 );
411 }
412
413 #[test]
414 fn test_is_lockfile_stale_modified() {
415 let temp_dir = tempfile::tempdir().unwrap();
416 let lockfile_path = temp_dir.path().join("go.sum");
417 std::fs::write(&lockfile_path, "").unwrap();
418
419 let old_time = std::time::UNIX_EPOCH;
420 let parser = GoSumParser;
421
422 assert!(
423 parser.is_lockfile_stale(&lockfile_path, old_time),
424 "Lock file should be stale when last_modified is old"
425 );
426 }
427
428 #[test]
429 fn test_is_lockfile_stale_deleted() {
430 let parser = GoSumParser;
431 let non_existent = std::path::Path::new("/nonexistent/go.sum");
432
433 assert!(
434 parser.is_lockfile_stale(non_existent, std::time::SystemTime::now()),
435 "Non-existent lock file should be considered stale"
436 );
437 }
438
439 #[test]
440 fn test_is_lockfile_stale_future_time() {
441 let temp_dir = tempfile::tempdir().unwrap();
442 let lockfile_path = temp_dir.path().join("go.sum");
443 std::fs::write(&lockfile_path, "").unwrap();
444
445 let future_time = std::time::SystemTime::now() + std::time::Duration::from_secs(86400); let parser = GoSumParser;
448
449 assert!(
450 !parser.is_lockfile_stale(&lockfile_path, future_time),
451 "Lock file should not be stale when last_modified is in the future"
452 );
453 }
454
455 #[test]
456 fn test_parse_go_sum_with_checksum() {
457 let content =
458 "github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg=\n";
459 let packages = parse_go_sum(content);
460
461 let pkg = packages.get("github.com/gin-gonic/gin").unwrap();
462 assert_eq!(pkg.version, "v1.9.1");
463
464 match &pkg.source {
465 ResolvedSource::Registry { url, checksum } => {
466 assert_eq!(url, "https://proxy.golang.org");
467 assert_eq!(checksum, "h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg=");
468 }
469 _ => panic!("Expected Registry source"),
470 }
471 }
472
473 #[test]
474 fn test_parse_go_sum_dependencies_empty() {
475 let content = "github.com/gin-gonic/gin v1.9.1 h1:hash=\n";
476 let packages = parse_go_sum(content);
477
478 let pkg = packages.get("github.com/gin-gonic/gin").unwrap();
479 assert!(pkg.dependencies.is_empty());
480 }
481}