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