Skip to main content

code_moniker_core/lang/go/
build.rs

1#[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
122fn split_comment(line: &str) -> (&str, &str) {
123	if let Some(idx) = line.find("//") {
124		(&line[..idx], &line[idx + 2..])
125	} else {
126		(line, "")
127	}
128}
129
130#[cfg(test)]
131mod tests {
132	use super::*;
133
134	#[test]
135	fn parse_empty_returns_empty_vec() {
136		assert!(parse("").unwrap().is_empty());
137	}
138
139	#[test]
140	fn parse_module_only_emits_package_dep() {
141		let src = "module github.com/foo/bar\n\ngo 1.21\n";
142		let deps = parse(src).unwrap();
143		assert_eq!(
144			deps,
145			vec![Dep {
146				name: "github.com/foo/bar".into(),
147				version: None,
148				dep_kind: "package".into(),
149				import_root: "github.com/foo/bar".into(),
150			}]
151		);
152	}
153
154	#[test]
155	fn parse_single_line_require() {
156		let src = "module foo\n\nrequire gopkg.in/x v1.0.0\n";
157		let deps = parse(src).unwrap();
158		let req = deps.iter().find(|d| d.name == "gopkg.in/x").unwrap();
159		assert_eq!(req.version.as_deref(), Some("v1.0.0"));
160		assert_eq!(req.dep_kind, "normal");
161		assert_eq!(req.import_root, "gopkg.in/x");
162	}
163
164	#[test]
165	fn parse_block_require_multiple_entries() {
166		let src = "module foo\n\nrequire (\n\tgithub.com/x/y v1.2.3\n\tgithub.com/a/b v0.5.0\n)\n";
167		let deps = parse(src).unwrap();
168		assert!(
169			deps.iter()
170				.any(|d| d.name == "github.com/x/y" && d.version.as_deref() == Some("v1.2.3"))
171		);
172		assert!(
173			deps.iter()
174				.any(|d| d.name == "github.com/a/b" && d.version.as_deref() == Some("v0.5.0"))
175		);
176	}
177
178	#[test]
179	fn parse_indirect_marker_sets_dep_kind() {
180		let src = "module foo\n\nrequire (\n\tgithub.com/x/y v1.2.3 // indirect\n)\n";
181		let deps = parse(src).unwrap();
182		let req = deps.iter().find(|d| d.name == "github.com/x/y").unwrap();
183		assert_eq!(req.dep_kind, "indirect");
184	}
185
186	#[test]
187	fn parse_inline_indirect_on_single_line_require() {
188		let src = "module foo\n\nrequire github.com/x/y v1.0.0 // indirect\n";
189		let deps = parse(src).unwrap();
190		let req = deps.iter().find(|d| d.name == "github.com/x/y").unwrap();
191		assert_eq!(req.dep_kind, "indirect");
192	}
193
194	#[test]
195	fn parse_skips_replace_block() {
196		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";
197		let deps = parse(src).unwrap();
198		let names: Vec<&str> = deps.iter().map(|d| d.name.as_str()).collect();
199		assert!(names.contains(&"github.com/x"));
200		assert!(names.contains(&"github.com/z"));
201		assert!(!names.contains(&"github.com/old"));
202		assert!(!names.contains(&"github.com/new"));
203	}
204
205	#[test]
206	fn parse_skips_replace_single_line() {
207		let src = "module foo\n\nreplace github.com/old => github.com/new v2.0.0\n\nrequire github.com/x v1.0.0\n";
208		let deps = parse(src).unwrap();
209		assert!(deps.iter().any(|d| d.name == "github.com/x"));
210		assert!(!deps.iter().any(|d| d.name == "github.com/old"));
211	}
212
213	#[test]
214	fn parse_skips_go_and_toolchain_directives() {
215		let src = "module foo\n\ngo 1.21\ntoolchain go1.22.0\n";
216		let deps = parse(src).unwrap();
217		assert_eq!(deps.len(), 1);
218		assert_eq!(deps[0].name, "foo");
219	}
220
221	#[test]
222	fn parse_strips_inline_comments_outside_indirect_marker() {
223		let src = "module foo // some comment\n\nrequire github.com/x v1.0.0 // some other text\n";
224		let deps = parse(src).unwrap();
225		let req = deps.iter().find(|d| d.name == "github.com/x").unwrap();
226		assert_eq!(req.dep_kind, "normal");
227	}
228}