Skip to main content

normalize_manifest/
go_mod.rs

1//! Parser for `go.mod` files (Go modules).
2
3use crate::{DeclaredDep, DepKind, ManifestError, ManifestParser, ParsedManifest};
4
5/// Information extracted from a go.mod file.
6#[derive(Debug, Clone)]
7pub struct GoModule {
8    /// Module path (e.g., `"github.com/user/project"`).
9    pub path: String,
10    /// Go version directive (e.g., `"1.21"`).
11    pub go_version: Option<String>,
12}
13
14/// Parse go.mod content to extract module information.
15///
16/// Exposed via `crate::go_module()` for `normalize-local-deps`.
17pub(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
34/// Parser for `go.mod` files.
35pub 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                // Single-line require: `require github.com/foo/bar v1.2.3`
66                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    // Strip inline comment: `github.com/foo/bar v1.2.3 // indirect`
88    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}