code_moniker_core/lang/go/
build.rs1#[derive(Clone, Debug, Eq, PartialEq)]
2pub struct Dep {
3 pub name: String,
4 pub version: Option<String>,
5 pub dep_kind: String,
6 pub import_root: String,
7}
8
9#[derive(Debug)]
10pub enum GoModError {
11 Schema(String),
12}
13
14impl std::fmt::Display for GoModError {
15 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
16 match self {
17 Self::Schema(s) => write!(f, "go.mod schema error: {s}"),
18 }
19 }
20}
21
22impl std::error::Error for GoModError {}
23
24pub fn parse(content: &str) -> Result<Vec<Dep>, GoModError> {
25 let mut out = Vec::new();
26 let mut block: Option<Block> = None;
27
28 for raw_line in content.lines() {
29 let (clean, comment) = split_comment(raw_line);
30 let trimmed = clean.trim();
31 if trimmed.is_empty() {
32 continue;
33 }
34
35 if let Some(b) = block {
36 if trimmed == ")" {
37 block = None;
38 continue;
39 }
40 match b {
41 Block::Require => {
42 if let Some(dep) = parse_require_entry(trimmed, comment) {
43 out.push(dep);
44 }
45 }
46 Block::Other => {}
47 }
48 block = Some(b);
49 continue;
50 }
51
52 if let Some(rest) = trimmed.strip_prefix("module") {
53 let path = rest.trim().trim_matches('"');
54 if !path.is_empty() {
55 out.push(Dep {
56 name: path.into(),
57 version: None,
58 dep_kind: "package".into(),
59 import_root: path.into(),
60 });
61 }
62 continue;
63 }
64
65 if let Some(rest) = trimmed.strip_prefix("require") {
66 let rest = rest.trim();
67 if rest == "(" {
68 block = Some(Block::Require);
69 continue;
70 }
71 if let Some(dep) = parse_require_entry(rest, comment) {
72 out.push(dep);
73 }
74 continue;
75 }
76
77 if trimmed.starts_with("replace")
78 || trimmed.starts_with("exclude")
79 || trimmed.starts_with("retract")
80 {
81 let rest = trimmed.split_whitespace().nth(1).unwrap_or("");
82 if rest == "(" {
83 block = Some(Block::Other);
84 }
85 continue;
86 }
87
88 if trimmed.starts_with("go ") || trimmed.starts_with("toolchain ") {
89 continue;
90 }
91 }
92
93 Ok(out)
94}
95
96#[derive(Clone, Copy)]
97enum Block {
98 Require,
99 Other,
100}
101
102fn parse_require_entry(line: &str, comment: &str) -> Option<Dep> {
103 let mut parts = line.split_whitespace();
104 let path = parts.next()?.trim_matches('"');
105 let version = parts.next()?.trim_matches('"');
106 if path.is_empty() || version.is_empty() {
107 return None;
108 }
109 let dep_kind = if comment.trim() == "indirect" {
110 "indirect"
111 } else {
112 "normal"
113 };
114 Some(Dep {
115 name: path.into(),
116 version: Some(version.into()),
117 dep_kind: dep_kind.into(),
118 import_root: path.into(),
119 })
120}
121
122pub fn package_moniker(project: &[u8], import_root: &str) -> crate::core::moniker::Moniker {
123 let mut b = crate::core::moniker::MonikerBuilder::new();
124 b.project(project);
125 let mut pieces = import_root.split('/').filter(|s| !s.is_empty());
126 if let Some(head) = pieces.next() {
127 b.segment(crate::lang::kinds::EXTERNAL_PKG, head.as_bytes());
128 for piece in pieces {
129 b.segment(crate::lang::kinds::PATH, piece.as_bytes());
130 }
131 }
132 b.build()
133}
134
135fn split_comment(line: &str) -> (&str, &str) {
136 if let Some(idx) = line.find("//") {
137 (&line[..idx], &line[idx + 2..])
138 } else {
139 (line, "")
140 }
141}
142
143#[cfg(test)]
144mod tests {
145 use super::*;
146
147 #[test]
148 fn parse_empty_returns_empty_vec() {
149 assert!(parse("").unwrap().is_empty());
150 }
151
152 #[test]
153 fn parse_module_only_emits_package_dep() {
154 let src = "module github.com/foo/bar\n\ngo 1.21\n";
155 let deps = parse(src).unwrap();
156 assert_eq!(
157 deps,
158 vec![Dep {
159 name: "github.com/foo/bar".into(),
160 version: None,
161 dep_kind: "package".into(),
162 import_root: "github.com/foo/bar".into(),
163 }]
164 );
165 }
166
167 #[test]
168 fn parse_single_line_require() {
169 let src = "module foo\n\nrequire gopkg.in/x v1.0.0\n";
170 let deps = parse(src).unwrap();
171 let req = deps.iter().find(|d| d.name == "gopkg.in/x").unwrap();
172 assert_eq!(req.version.as_deref(), Some("v1.0.0"));
173 assert_eq!(req.dep_kind, "normal");
174 assert_eq!(req.import_root, "gopkg.in/x");
175 }
176
177 #[test]
178 fn parse_block_require_multiple_entries() {
179 let src = "module foo\n\nrequire (\n\tgithub.com/x/y v1.2.3\n\tgithub.com/a/b v0.5.0\n)\n";
180 let deps = parse(src).unwrap();
181 assert!(
182 deps.iter()
183 .any(|d| d.name == "github.com/x/y" && d.version.as_deref() == Some("v1.2.3"))
184 );
185 assert!(
186 deps.iter()
187 .any(|d| d.name == "github.com/a/b" && d.version.as_deref() == Some("v0.5.0"))
188 );
189 }
190
191 #[test]
192 fn parse_indirect_marker_sets_dep_kind() {
193 let src = "module foo\n\nrequire (\n\tgithub.com/x/y v1.2.3 // indirect\n)\n";
194 let deps = parse(src).unwrap();
195 let req = deps.iter().find(|d| d.name == "github.com/x/y").unwrap();
196 assert_eq!(req.dep_kind, "indirect");
197 }
198
199 #[test]
200 fn parse_inline_indirect_on_single_line_require() {
201 let src = "module foo\n\nrequire github.com/x/y v1.0.0 // indirect\n";
202 let deps = parse(src).unwrap();
203 let req = deps.iter().find(|d| d.name == "github.com/x/y").unwrap();
204 assert_eq!(req.dep_kind, "indirect");
205 }
206
207 #[test]
208 fn parse_skips_replace_block() {
209 let src = "module foo\n\nrequire github.com/x v1.0.0\n\nreplace (\n\tgithub.com/old => github.com/new v2.0.0\n)\n\nrequire github.com/z v3.0.0\n";
210 let deps = parse(src).unwrap();
211 let names: Vec<&str> = deps.iter().map(|d| d.name.as_str()).collect();
212 assert!(names.contains(&"github.com/x"));
213 assert!(names.contains(&"github.com/z"));
214 assert!(!names.contains(&"github.com/old"));
215 assert!(!names.contains(&"github.com/new"));
216 }
217
218 #[test]
219 fn parse_skips_replace_single_line() {
220 let src = "module foo\n\nreplace github.com/old => github.com/new v2.0.0\n\nrequire github.com/x v1.0.0\n";
221 let deps = parse(src).unwrap();
222 assert!(deps.iter().any(|d| d.name == "github.com/x"));
223 assert!(!deps.iter().any(|d| d.name == "github.com/old"));
224 }
225
226 #[test]
227 fn parse_skips_go_and_toolchain_directives() {
228 let src = "module foo\n\ngo 1.21\ntoolchain go1.22.0\n";
229 let deps = parse(src).unwrap();
230 assert_eq!(deps.len(), 1);
231 assert_eq!(deps[0].name, "foo");
232 }
233
234 #[test]
235 fn parse_strips_inline_comments_outside_indirect_marker() {
236 let src = "module foo // some comment\n\nrequire github.com/x v1.0.0 // some other text\n";
237 let deps = parse(src).unwrap();
238 let req = deps.iter().find(|d| d.name == "github.com/x").unwrap();
239 assert_eq!(req.dep_kind, "normal");
240 }
241}