normalize_manifest/
go_mod.rs1use crate::{DeclaredDep, DepKind, ManifestError, ManifestParser, ParsedManifest};
4
5#[derive(Debug, Clone)]
7pub struct GoModule {
8 pub path: String,
10 pub go_version: Option<String>,
12}
13
14pub(crate) fn parse_go_module(content: &str) -> Option<GoModule> {
18 let mut path = None;
19 let mut go_version = None;
20
21 for line in content.lines() {
22 let line = line.trim();
23 if line.starts_with("module ") {
24 path = Some(line.trim_start_matches("module ").trim().to_string());
25 }
26 if line.starts_with("go ") {
27 go_version = Some(line.trim_start_matches("go ").trim().to_string());
28 }
29 }
30
31 path.map(|path| GoModule { path, go_version })
32}
33
34pub struct GoModParser;
36
37impl ManifestParser for GoModParser {
38 fn filename(&self) -> &'static str {
39 "go.mod"
40 }
41
42 fn parse(&self, content: &str) -> Result<ParsedManifest, ManifestError> {
43 let module = parse_go_module(content)
44 .ok_or_else(|| ManifestError("no module directive found".to_string()))?;
45
46 let mut deps = Vec::new();
47 let mut in_require_block = false;
48
49 for line in content.lines() {
50 let line = line.trim();
51
52 if line == "require (" {
53 in_require_block = true;
54 continue;
55 }
56 if in_require_block && line == ")" {
57 in_require_block = false;
58 continue;
59 }
60 if in_require_block {
61 if let Some(dep) = parse_require_line(line) {
62 deps.push(dep);
63 }
64 } else if line.starts_with("require ") && !line.contains('(') {
65 let rest = line.trim_start_matches("require ").trim();
67 if let Some(dep) = parse_require_line(rest) {
68 deps.push(dep);
69 }
70 }
71 }
72
73 Ok(ParsedManifest {
74 ecosystem: "go",
75 name: Some(module.path),
76 version: module.go_version,
77 dependencies: deps,
78 })
79 }
80}
81
82fn parse_require_line(line: &str) -> Option<DeclaredDep> {
83 let line = line.trim();
84 if line.is_empty() || line.starts_with("//") {
85 return None;
86 }
87 let without_comment = match line.find(" // ") {
89 Some(idx) => &line[..idx],
90 None => line,
91 };
92 let mut parts = without_comment.split_whitespace();
93 let name = parts.next()?.to_string();
94 let version_req = parts.next().map(|v| v.to_string());
95 Some(DeclaredDep {
96 name,
97 version_req,
98 kind: DepKind::Normal,
99 })
100}
101
102#[cfg(test)]
103mod tests {
104 use super::*;
105 use crate::ManifestParser;
106
107 #[test]
108 fn test_module_and_version() {
109 let content = "module github.com/user/project\n\ngo 1.21\n";
110 let m = GoModParser.parse(content).unwrap();
111 assert_eq!(m.ecosystem, "go");
112 assert_eq!(m.name.as_deref(), Some("github.com/user/project"));
113 assert_eq!(m.version.as_deref(), Some("1.21"));
114 assert!(m.dependencies.is_empty());
115 }
116
117 #[test]
118 fn test_require_block() {
119 let content = r#"module github.com/user/project
120
121go 1.21
122
123require (
124 github.com/pkg/errors v0.9.1
125 golang.org/x/sync v0.3.0 // indirect
126)
127"#;
128 let m = GoModParser.parse(content).unwrap();
129 assert_eq!(m.dependencies.len(), 2);
130 assert_eq!(m.dependencies[0].name, "github.com/pkg/errors");
131 assert_eq!(m.dependencies[0].version_req.as_deref(), Some("v0.9.1"));
132 assert_eq!(m.dependencies[1].name, "golang.org/x/sync");
133 assert_eq!(m.dependencies[1].version_req.as_deref(), Some("v0.3.0"));
134 }
135
136 #[test]
137 fn test_single_line_require() {
138 let content = "module example.com/m\ngo 1.20\nrequire github.com/foo/bar v1.2.3\n";
139 let m = GoModParser.parse(content).unwrap();
140 assert_eq!(m.dependencies.len(), 1);
141 assert_eq!(m.dependencies[0].name, "github.com/foo/bar");
142 assert_eq!(m.dependencies[0].version_req.as_deref(), Some("v1.2.3"));
143 }
144
145 #[test]
146 fn test_parse_go_module_helper() {
147 let content = "module mymod\ngo 1.22\n";
148 let gm = parse_go_module(content).unwrap();
149 assert_eq!(gm.path, "mymod");
150 assert_eq!(gm.go_version.as_deref(), Some("1.22"));
151 }
152}