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
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}