1use crate::config::{GoParseContext, GoProxyChain};
15use crate::types::{GoDependency, GoDirective};
16use deps_core::Result;
17use deps_core::lsp_helpers::LineOffsetTable;
18use regex::Regex;
19use tower_lsp_server::ls_types::{Range, Uri};
20
21#[derive(Debug, Clone)]
23pub struct GoParseResult {
24 pub dependencies: Vec<GoDependency>,
26 pub module_path: Option<String>,
28 pub go_version: Option<String>,
30 pub uri: Uri,
32 pub resolved_chains: Vec<GoProxyChain>,
36}
37
38pub fn parse_go_mod(content: &str, doc_uri: &Uri) -> Result<GoParseResult> {
45 parse_go_mod_with_context(content, doc_uri, &GoParseContext::default())
46}
47
48pub fn parse_go_mod_with_context(
57 content: &str,
58 doc_uri: &Uri,
59 ctx: &GoParseContext,
60) -> Result<GoParseResult> {
61 tracing::debug!(uri = ?doc_uri, "Parsing go.mod file");
62
63 let line_table = LineOffsetTable::new(content);
64 let mut dependencies = Vec::with_capacity(50);
65 let mut module_path = None;
66 let mut go_version = None;
67
68 static MODULE_PATTERN: std::sync::LazyLock<Regex> =
69 std::sync::LazyLock::new(|| Regex::new(r"^\s*module\s+(\S+)").unwrap());
70 static GO_PATTERN: std::sync::LazyLock<Regex> =
71 std::sync::LazyLock::new(|| Regex::new(r"^\s*go\s+(\S+)").unwrap());
72 static REQUIRE_SINGLE: std::sync::LazyLock<Regex> =
73 std::sync::LazyLock::new(|| Regex::new(r"^\s*require\s+(\S+)\s+(\S+)").unwrap());
74 static REQUIRE_BLOCK_START: std::sync::LazyLock<Regex> =
75 std::sync::LazyLock::new(|| Regex::new(r"^\s*require\s*\(").unwrap());
76 static REPLACE_PATTERN: std::sync::LazyLock<Regex> = std::sync::LazyLock::new(|| {
77 Regex::new(r"^\s*replace\s+(\S+)\s+(?:(\S+)\s+)?=>\s+(\S+)\s+(\S+)").unwrap()
78 });
79 static EXCLUDE_PATTERN: std::sync::LazyLock<Regex> =
80 std::sync::LazyLock::new(|| Regex::new(r"^\s*exclude\s+(\S+)\s+(\S+)").unwrap());
81
82 let mut in_require_block = false;
83 let mut line_offset = 0;
84
85 for line in content.lines() {
86 let line_without_comment = strip_line_comment(line);
87 let line_trimmed = line_without_comment.trim();
88
89 if let Some(caps) = MODULE_PATTERN.captures(line_trimmed) {
90 module_path = Some(caps[1].to_string());
91 }
92
93 if let Some(caps) = GO_PATTERN.captures(line_trimmed) {
94 go_version = Some(caps[1].to_string());
95 }
96
97 if REQUIRE_BLOCK_START.is_match(line_trimmed) {
98 in_require_block = true;
99 line_offset += line.len() + 1;
100 continue;
101 }
102
103 if in_require_block && line_trimmed.contains(')') {
104 in_require_block = false;
105 line_offset += line.len() + 1;
106 continue;
107 }
108
109 if (in_require_block || REQUIRE_SINGLE.is_match(line_trimmed))
110 && let Some(dep) = parse_require_line(line, line_offset, content, &line_table)
111 {
112 dependencies.push(dep);
113 }
114
115 if let Some(caps) = REPLACE_PATTERN.captures(line_trimmed) {
116 let module = &caps[1];
117 let version = caps.get(2).map(|m| m.as_str());
118 if let Some(dep) =
119 parse_replace_line(line, line_offset, module, version, content, &line_table)
120 {
121 dependencies.push(dep);
122 }
123 }
124
125 if let Some(caps) = EXCLUDE_PATTERN.captures(line_trimmed) {
126 let module = &caps[1];
127 let version = &caps[2];
128 if let Some(dep) =
129 parse_exclude_line(line, line_offset, module, version, content, &line_table)
130 {
131 dependencies.push(dep);
132 }
133 }
134
135 let line_end = line_offset + line.len();
136 let next_line_start = if line_end < content.len() && content.as_bytes()[line_end] == b'\n' {
137 line_end + 1
138 } else {
139 line_end
140 };
141 line_offset = next_line_start;
142 }
143
144 tracing::debug!(
145 dependencies = %dependencies.len(),
146 module = ?module_path,
147 go_version = ?go_version,
148 "Parsed go.mod successfully"
149 );
150
151 let go_config = crate::config::resolve_with_context(ctx);
152 for dep in &mut dependencies {
153 dep.source = go_config.resolve_source_for(dep.module_path.as_str());
154 }
155
156 Ok(GoParseResult {
157 dependencies,
158 module_path,
159 go_version,
160 uri: doc_uri.clone(),
161 resolved_chains: go_config.resolved_chains(),
162 })
163}
164
165fn strip_line_comment(line: &str) -> &str {
169 let mut in_url = false;
170 for (i, c) in line.char_indices() {
171 if c == ':' && line[i..].starts_with("://") {
172 in_url = true;
173 continue;
174 }
175 if in_url && c.is_whitespace() {
176 in_url = false;
177 }
178 if !in_url && line[i..].starts_with("//") {
179 return &line[..i];
180 }
181 }
182 line
183}
184
185fn parse_require_line(
187 line: &str,
188 line_start_offset: usize,
189 content: &str,
190 line_table: &LineOffsetTable,
191) -> Option<GoDependency> {
192 let parts: Vec<&str> = line.split_whitespace().collect();
193 if parts.is_empty() {
194 return None;
195 }
196
197 let (module_path, version) = if parts[0] == "require" {
198 if parts.len() < 3 {
199 return None;
200 }
201 (parts[1], parts[2])
202 } else {
203 if parts.len() < 2 {
204 return None;
205 }
206 (parts[0], parts[1])
207 };
208
209 let indirect = line.contains("// indirect");
210
211 let module_start = line.find(module_path)?;
212 let module_offset = line_start_offset + module_start;
213 let module_path_range = Range::new(
214 line_table.byte_offset_to_position(content, module_offset),
215 line_table.byte_offset_to_position(content, module_offset + module_path.len()),
216 );
217
218 let version_start = line.find(version)?;
219 let version_offset = line_start_offset + version_start;
220 let version_range = Range::new(
221 line_table.byte_offset_to_position(content, version_offset),
222 line_table.byte_offset_to_position(content, version_offset + version.len()),
223 );
224
225 Some(GoDependency {
226 module_path: module_path.into(),
227 module_path_range,
228 version: Some(version.into()),
229 version_range: Some(version_range),
230 directive: GoDirective::Require,
231 indirect,
232 source: deps_core::parser::DependencySource::Registry,
233 })
234}
235
236fn parse_replace_line(
238 line: &str,
239 line_start_offset: usize,
240 module: &str,
241 version: Option<&str>,
242 content: &str,
243 line_table: &LineOffsetTable,
244) -> Option<GoDependency> {
245 let module_start = line.find(module)?;
246 let module_offset = line_start_offset + module_start;
247 let module_path_range = Range::new(
248 line_table.byte_offset_to_position(content, module_offset),
249 line_table.byte_offset_to_position(content, module_offset + module.len()),
250 );
251
252 let (version_str, version_range) = if let Some(ver) = version {
253 let version_start = line.find(ver)?;
254 let version_offset = line_start_offset + version_start;
255 let range = Range::new(
256 line_table.byte_offset_to_position(content, version_offset),
257 line_table.byte_offset_to_position(content, version_offset + ver.len()),
258 );
259 (Some(ver.to_string()), Some(range))
260 } else {
261 (None, None)
262 };
263
264 Some(GoDependency {
265 module_path: module.into(),
266 module_path_range,
267 version: version_str.map(Into::into),
268 version_range,
269 directive: GoDirective::Replace,
270 indirect: false,
271 source: deps_core::parser::DependencySource::Registry,
272 })
273}
274
275fn parse_exclude_line(
277 line: &str,
278 line_start_offset: usize,
279 module: &str,
280 version: &str,
281 content: &str,
282 line_table: &LineOffsetTable,
283) -> Option<GoDependency> {
284 let module_start = line.find(module)?;
285 let module_offset = line_start_offset + module_start;
286 let module_path_range = Range::new(
287 line_table.byte_offset_to_position(content, module_offset),
288 line_table.byte_offset_to_position(content, module_offset + module.len()),
289 );
290
291 let version_start = line.find(version)?;
292 let version_offset = line_start_offset + version_start;
293 let version_range = Range::new(
294 line_table.byte_offset_to_position(content, version_offset),
295 line_table.byte_offset_to_position(content, version_offset + version.len()),
296 );
297
298 Some(GoDependency {
299 module_path: module.into(),
300 module_path_range,
301 version: Some(version.into()),
302 version_range: Some(version_range),
303 directive: GoDirective::Exclude,
304 indirect: false,
305 source: deps_core::parser::DependencySource::Registry,
306 })
307}
308
309deps_core::impl_parse_result!(
310 GoParseResult,
311 GoDependency {
312 dependencies: dependencies,
313 uri: uri,
314 }
315);
316
317#[cfg(test)]
318mod tests {
319 use super::*;
320 fn test_uri() -> Uri {
321 use std::str::FromStr;
322 Uri::from_str("file:///test/go.mod").unwrap()
323 }
324
325 #[test]
326 fn test_parse_single_require() {
327 let content = r"module example.com/myapp
328
329go 1.21
330
331require github.com/gin-gonic/gin v1.9.1
332";
333 let result = parse_go_mod(content, &test_uri()).unwrap();
334 assert_eq!(result.dependencies.len(), 1);
335 assert_eq!(
336 result.dependencies[0].module_path,
337 "github.com/gin-gonic/gin"
338 );
339 assert_eq!(
340 result.dependencies[0]
341 .version
342 .as_ref()
343 .map(deps_core::VersionReq::as_str),
344 Some("v1.9.1")
345 );
346 assert!(!result.dependencies[0].indirect);
347 }
348
349 #[test]
350 fn test_parse_module_directive() {
351 let content = "module example.com/myapp\n";
352 let result = parse_go_mod(content, &test_uri()).unwrap();
353 assert_eq!(result.module_path, Some("example.com/myapp".to_string()));
354 }
355
356 #[test]
357 fn test_parse_go_version() {
358 let content = "go 1.21\n";
359 let result = parse_go_mod(content, &test_uri()).unwrap();
360 assert_eq!(result.go_version, Some("1.21".to_string()));
361 }
362
363 #[test]
364 fn test_parse_require_block() {
365 let content = r"require (
366 github.com/gin-gonic/gin v1.9.1
367 golang.org/x/crypto v0.17.0 // indirect
368)
369";
370 let result = parse_go_mod(content, &test_uri()).unwrap();
371 assert_eq!(result.dependencies.len(), 2);
372 assert!(!result.dependencies[0].indirect);
373 assert!(result.dependencies[1].indirect);
374 }
375
376 #[test]
377 fn test_parse_replace_directive() {
378 let content = "replace github.com/old/module => github.com/new/module v1.2.3\n";
379 let result = parse_go_mod(content, &test_uri()).unwrap();
380 assert_eq!(result.dependencies.len(), 1);
381 assert_eq!(result.dependencies[0].directive, GoDirective::Replace);
382 assert_eq!(result.dependencies[0].module_path, "github.com/old/module");
383 }
384
385 #[test]
386 fn test_parse_exclude_directive() {
387 let content = "exclude github.com/bad/module v0.1.0\n";
388 let result = parse_go_mod(content, &test_uri()).unwrap();
389 assert_eq!(result.dependencies.len(), 1);
390 assert_eq!(result.dependencies[0].directive, GoDirective::Exclude);
391 assert_eq!(result.dependencies[0].module_path, "github.com/bad/module");
392 assert_eq!(
393 result.dependencies[0]
394 .version
395 .as_ref()
396 .map(deps_core::VersionReq::as_str),
397 Some("v0.1.0")
398 );
399 }
400
401 #[test]
402 fn test_parse_pseudo_version() {
403 let content = "require golang.org/x/crypto v0.0.0-20191109021931-daa7c04131f5\n";
404 let result = parse_go_mod(content, &test_uri()).unwrap();
405 assert_eq!(
406 result.dependencies[0].version,
407 Some(deps_core::VersionReq::new(
408 "v0.0.0-20191109021931-daa7c04131f5"
409 ))
410 );
411 }
412
413 #[test]
414 fn test_position_tracking() {
415 let content = "require github.com/gin-gonic/gin v1.9.1";
416 let result = parse_go_mod(content, &test_uri()).unwrap();
417 let dep = &result.dependencies[0];
418
419 assert_eq!(dep.module_path_range.start.line, 0);
420 assert!(dep.version_range.is_some());
421 }
422
423 #[test]
424 fn test_empty_file() {
425 let content = "";
426 let result = parse_go_mod(content, &test_uri()).unwrap();
427 assert_eq!(result.dependencies.len(), 0);
428 assert_eq!(result.module_path, None);
429 assert_eq!(result.go_version, None);
430 }
431
432 #[test]
433 fn test_comments_stripped() {
434 let content =
435 "// This is a comment\nrequire github.com/pkg/errors v0.9.1 // inline comment\n";
436 let result = parse_go_mod(content, &test_uri()).unwrap();
437 assert_eq!(result.dependencies.len(), 1);
438 assert_eq!(result.dependencies[0].module_path, "github.com/pkg/errors");
439 }
440
441 #[test]
442 fn test_complex_go_mod() {
443 let content = r"module example.com/myapp
444
445go 1.21
446
447require (
448 github.com/gin-gonic/gin v1.9.1
449 golang.org/x/crypto v0.17.0 // indirect
450)
451
452replace github.com/old/module => github.com/new/module v1.2.3
453
454exclude github.com/bad/module v0.1.0
455";
456 let result = parse_go_mod(content, &test_uri()).unwrap();
457 assert_eq!(result.dependencies.len(), 4);
458 assert_eq!(result.module_path, Some("example.com/myapp".to_string()));
459 assert_eq!(result.go_version, Some("1.21".to_string()));
460
461 let require_deps: Vec<_> = result
462 .dependencies
463 .iter()
464 .filter(|d| d.directive == GoDirective::Require)
465 .collect();
466 assert_eq!(require_deps.len(), 2);
467
468 let replace_deps: Vec<_> = result
469 .dependencies
470 .iter()
471 .filter(|d| d.directive == GoDirective::Replace)
472 .collect();
473 assert_eq!(replace_deps.len(), 1);
474
475 let exclude_deps: Vec<_> = result
476 .dependencies
477 .iter()
478 .filter(|d| d.directive == GoDirective::Exclude)
479 .collect();
480 assert_eq!(exclude_deps.len(), 1);
481 }
482
483 #[test]
484 fn test_position_tracking_no_trailing_newline() {
485 let content = "require github.com/gin-gonic/gin v1.9.1";
486 let result = parse_go_mod(content, &test_uri()).unwrap();
487 let dep = &result.dependencies[0];
488
489 assert_eq!(dep.module_path_range.start.character, 8);
490 assert_eq!(dep.module_path_range.end.character, 32);
491 assert_eq!(dep.version_range.as_ref().unwrap().start.character, 33);
492 assert_eq!(dep.version_range.as_ref().unwrap().end.character, 39);
493 }
494
495 #[test]
496 fn test_parse_complex_go_mod() {
497 let content = r"module example.com/myapp
498
499go 1.21
500
501require (
502 github.com/gin-gonic/gin v1.9.1
503 golang.org/x/crypto v0.17.0 // indirect
504)
505
506replace github.com/old/module => github.com/new/module v1.2.3
507
508exclude github.com/bad/module v0.1.0
509";
510 let result = parse_go_mod(content, &test_uri()).unwrap();
511
512 assert_eq!(result.module_path, Some("example.com/myapp".to_string()));
514 assert_eq!(result.go_version, Some("1.21".to_string()));
515
516 assert_eq!(result.dependencies.len(), 4);
518
519 let gin = &result.dependencies[0];
521 assert_eq!(gin.module_path, "github.com/gin-gonic/gin");
522 assert_eq!(
523 gin.version.as_ref().map(deps_core::VersionReq::as_str),
524 Some("v1.9.1")
525 );
526 assert_eq!(gin.directive, GoDirective::Require);
527 assert!(!gin.indirect);
528
529 let crypto = &result.dependencies[1];
531 assert_eq!(crypto.module_path, "golang.org/x/crypto");
532 assert_eq!(
533 crypto.version.as_ref().map(deps_core::VersionReq::as_str),
534 Some("v0.17.0")
535 );
536 assert_eq!(crypto.directive, GoDirective::Require);
537 assert!(crypto.indirect);
538
539 let replace = &result.dependencies[2];
541 assert_eq!(replace.module_path, "github.com/old/module");
542 assert_eq!(replace.version, None);
543 assert_eq!(replace.directive, GoDirective::Replace);
544
545 let exclude = &result.dependencies[3];
547 assert_eq!(exclude.module_path, "github.com/bad/module");
548 assert_eq!(
549 exclude.version.as_ref().map(deps_core::VersionReq::as_str),
550 Some("v0.1.0")
551 );
552 assert_eq!(exclude.directive, GoDirective::Exclude);
553 }
554
555 #[test]
556 fn test_strip_line_comment_with_url() {
557 let line = "replace github.com/old => https://github.com/new // comment";
558 let stripped = strip_line_comment(line);
559 assert_eq!(
560 stripped,
561 "replace github.com/old => https://github.com/new "
562 );
563 }
564
565 #[tokio::test]
569 async fn test_integration_parse_resolve_register_chain_get_versions() {
570 use crate::registry::GoRegistry;
571 use deps_core::net_policy::{RegistryAccessPolicy, WorkspaceRegistryAccess};
572 use deps_core::{FreshnessSettings, HttpCache, Registry};
573 use std::sync::Arc;
574
575 let mut alt_server = mockito::Server::new_async().await;
576 alt_server
577 .mock("GET", "/github.com/gin-gonic/gin/@v/list")
578 .with_status(200)
579 .with_body("v1.9.1\n")
580 .create_async()
581 .await;
582
583 let dir = tempfile::tempdir().unwrap();
584 let goenv_path = dir.path().join("env");
585 std::fs::write(
586 &goenv_path,
587 format!("GOPROXY={},direct\n", alt_server.url()),
588 )
589 .unwrap();
590
591 let cache = Arc::new(HttpCache::new());
592 cache.set_registry_policy(WorkspaceRegistryAccess::All);
593 let ctx = GoParseContext {
594 policy: Arc::new(RegistryAccessPolicy::new(WorkspaceRegistryAccess::All)),
595 goenv_path: Some(goenv_path),
596 ..Default::default()
597 };
598
599 let content = "module example.com/myapp\n\nrequire github.com/gin-gonic/gin v1.9.0\n";
600 let result = parse_go_mod_with_context(content, &test_uri(), &ctx).unwrap();
601 assert_eq!(result.resolved_chains.len(), 1);
602
603 let registry = Arc::new(GoRegistry::new(Arc::clone(&cache)));
604 for chain in &result.resolved_chains {
605 GoRegistry::register_chain(®istry, chain);
606 }
607
608 let source = result.dependencies[0].source.clone();
609 let versions = registry
610 .get_versions_from(
611 &deps_core::PackageName::new("github.com/gin-gonic/gin"),
612 &source,
613 FreshnessSettings::default(),
614 )
615 .await
616 .unwrap();
617 assert_eq!(versions.len(), 1);
618 }
619}