1#![warn(clippy::pedantic)]
34#![warn(clippy::nursery)]
35#![warn(clippy::cargo)]
36
37use crate::parser::{gomod, Directive};
38use std::collections::HashMap;
39use winnow::Parser;
40
41mod combinator;
42pub mod parser;
43
44#[derive(Debug, Default, PartialEq, Eq)]
45pub struct GoMod {
46 pub comment: Vec<String>,
47 pub module: String,
48 pub go: Option<String>,
49 pub godebug: HashMap<String, String>,
50 pub tool: Vec<String>,
51 pub toolchain: Option<String>,
52 pub require: Vec<ModuleDependency>,
53 pub exclude: Vec<ModuleDependency>,
54 pub replace: Vec<ModuleReplacement>,
55 pub retract: Vec<ModuleRetract>,
56 pub ignore: Vec<String>,
57}
58
59impl std::str::FromStr for GoMod {
60 type Err = String;
61
62 fn from_str(input: &str) -> Result<Self, Self::Err> {
63 let mut res = Self::default();
64
65 for directive in &mut gomod.parse(input).map_err(|e| e.to_string())? {
66 match directive {
67 Directive::Comment(d) => res.comment.push((**d).to_string()),
68 Directive::Module(d) => res.module = (**d).to_string(),
69 Directive::Go(d) => res.go = Some((**d).to_string()),
70 Directive::GoDebug(d) => res.godebug.extend((*d).clone()),
71 Directive::Tool(d) => res.tool.append(d),
72 Directive::Toolchain(d) => res.toolchain = Some((**d).to_string()),
73 Directive::Require(d) => res.require.append(d),
74 Directive::Exclude(d) => res.exclude.append(d),
75 Directive::Replace(d) => res.replace.append(d),
76 Directive::Retract(d) => res.retract.append(d),
77 Directive::Ignore(d) => res.ignore.append(d),
78 }
79 }
80
81 Ok(res)
82 }
83}
84
85#[derive(Debug, PartialEq, Eq)]
86pub struct Module {
87 pub module_path: String,
88 pub version: String,
89}
90
91#[derive(Debug, PartialEq, Eq)]
92pub struct ModuleDependency {
93 pub module: Module,
94 pub indirect: bool,
95}
96
97#[derive(Debug, PartialEq, Eq)]
98pub struct ModuleReplacement {
99 pub module_path: String,
100 pub version: Option<String>,
101 pub replacement: Replacement,
102}
103
104#[derive(Debug, PartialEq, Eq)]
105pub enum Replacement {
106 FilePath(String),
107 Module(Module),
108}
109
110#[derive(Debug, PartialEq, Eq)]
111pub enum ModuleRetract {
112 Single(String),
113 Range(String, String),
114}
115
116#[cfg(test)]
117mod tests {
118 use super::*;
119 use indoc::indoc;
120 use std::str::FromStr;
121
122 #[test]
123 fn test_parse_complete() {
124 let input = indoc! {r#"
125 // Complete example
126
127 module github.com/complete
128
129 go 1.21
130
131 toolchain go1.21.1
132
133 require golang.org/x/net v0.20.0
134
135 exclude golang.org/x/net v0.19.1
136
137 replace golang.org/x/net v0.19.0 => example.com/fork/net v0.19.1
138
139 retract v1.0.0
140 "#};
141
142 let go_mod = GoMod::from_str(input).unwrap();
143
144 assert_eq!(go_mod.module, "github.com/complete".to_string());
145 assert_eq!(go_mod.go, Some("1.21".to_string()));
146 assert_eq!(go_mod.toolchain, Some("go1.21.1".to_string()));
147 assert_eq!(
148 go_mod.require,
149 vec![ModuleDependency {
150 module: Module {
151 module_path: "golang.org/x/net".to_string(),
152 version: "v0.20.0".to_string()
153 },
154 indirect: false
155 }]
156 );
157 assert_eq!(
158 go_mod.exclude,
159 vec![ModuleDependency {
160 module: Module {
161 module_path: "golang.org/x/net".to_string(),
162 version: "v0.19.1".to_string()
163 },
164 indirect: false
165 }]
166 );
167 assert_eq!(
168 go_mod.replace,
169 vec![ModuleReplacement {
170 module_path: "golang.org/x/net".to_string(),
171 version: Some("v0.19.0".to_string()),
172 replacement: Replacement::Module(Module {
173 module_path: "example.com/fork/net".to_string(),
174 version: "v0.19.1".to_string(),
175 })
176 }]
177 );
178 assert_eq!(
179 go_mod.retract,
180 vec![ModuleRetract::Single("v1.0.0".to_string())]
181 );
182 assert_eq!(go_mod.comment, vec!["Complete example".to_string()]);
183 }
184
185 #[test]
186 fn test_invalid_content() {
187 let input = indoc! {r#"
188 modulegithub.com/no-space
189 "#};
190
191 let go_mod = GoMod::from_str(input);
192
193 assert!(go_mod.is_err());
194 }
195
196 #[test]
197 fn test_no_line_ending_after_module() {
198 let input = indoc! {r#"
199 module github.com/no-line-ending"#};
200
201 let go_mod = GoMod::from_str(input).unwrap();
202
203 assert_eq!(go_mod.module, "github.com/no-line-ending".to_string());
204 }
205
206 #[test]
207 fn test_no_line_ending_after_go() {
208 let input = indoc! {r#"
209 module github.com/no-line-ending
210
211 go 1.24"#};
212
213 let go_mod = GoMod::from_str(input).unwrap();
214
215 assert_eq!(go_mod.go, Some("1.24".to_string()));
216 }
217
218 #[test]
219 fn test_no_line_ending_after_godebug() {
220 let input = indoc! {r#"
221 module github.com/no-line-ending
222
223 godebug (
224 default=go1.21
225 panicnil=1
226 )"#};
227
228 let go_mod = GoMod::from_str(input).unwrap();
229
230 assert_eq!(
231 go_mod.godebug,
232 HashMap::from([
233 ("default".to_string(), "go1.21".to_string()),
234 ("panicnil".to_string(), "1".to_string())
235 ])
236 );
237 }
238
239 #[test]
240 fn test_no_line_ending_after_tool() {
241 let input = indoc! {r#"
242 module github.com/no-line-ending
243
244 tool example.com/mymodule/cmd/mytool1"#};
245
246 let go_mod = GoMod::from_str(input).unwrap();
247
248 assert_eq!(
249 go_mod.tool,
250 vec!["example.com/mymodule/cmd/mytool1".to_string()]
251 );
252 }
253
254 #[test]
255 fn test_no_line_ending_after_toolchain() {
256 let input = indoc! {r#"
257 module github.com/no-line-ending
258
259 toolchain go1.21.1"#};
260
261 let go_mod = GoMod::from_str(input).unwrap();
262
263 assert_eq!(go_mod.toolchain, Some("go1.21.1".to_string()));
264 }
265
266 #[test]
267 fn test_no_line_ending_after_require() {
268 let input = indoc! {r#"
269 module github.com/no-line-ending
270
271 require (
272 golang.org/x/net v0.20.0
273 )"#};
274
275 let go_mod = GoMod::from_str(input).unwrap();
276
277 assert_eq!(
278 go_mod.require,
279 vec![ModuleDependency {
280 module: Module {
281 module_path: "golang.org/x/net".to_string(),
282 version: "v0.20.0".to_string()
283 },
284 indirect: false
285 }]
286 );
287 }
288
289 #[test]
290 fn test_ignore_single() {
291 let input = indoc! {r#"
292 module github.com/ignore-single
293
294 go 1.24
295
296 ignore ./testdata
297 "#};
298
299 let go_mod = GoMod::from_str(input).unwrap();
300
301 assert_eq!(go_mod.ignore, vec!["./testdata".to_string()]);
302 }
303
304 #[test]
305 fn test_ignore_multi() {
306 let input = indoc! {r#"
307 module github.com/ignore-multi
308
309 go 1.24
310
311 ignore (
312 ./testdata
313 ./vendor/temp
314 ./node_modules
315 )
316 "#};
317
318 let go_mod = GoMod::from_str(input).unwrap();
319
320 assert_eq!(
321 go_mod.ignore,
322 vec![
323 "./testdata".to_string(),
324 "./vendor/temp".to_string(),
325 "./node_modules".to_string(),
326 ]
327 );
328 }
329
330 #[test]
331 fn test_ignore_repeated_singles() {
332 let input = indoc! {r#"
333 module github.com/ignore-repeated
334
335 go 1.24
336
337 ignore ./testdata
338 ignore ./vendor/temp
339 "#};
340
341 let go_mod = GoMod::from_str(input).unwrap();
342
343 assert_eq!(
344 go_mod.ignore,
345 vec!["./testdata".to_string(), "./vendor/temp".to_string()]
346 );
347 }
348
349 #[test]
350 fn test_no_line_ending_after_ignore() {
351 let input = indoc! {r#"
352 module github.com/no-line-ending
353
354 ignore (
355 ./testdata
356 )"#};
357
358 let go_mod = GoMod::from_str(input).unwrap();
359
360 assert_eq!(go_mod.ignore, vec!["./testdata".to_string()]);
361 }
362
363 #[test]
364 fn test_comments() {
365 let input = indoc! {r#"
366 module github.com/comments
367
368 // 1st comment
369 //2nd comment
370 // 3rd comment
371 //4th comment"#};
372
373 let go_mod = GoMod::from_str(input).unwrap();
374
375 assert_eq!(go_mod.module, "github.com/comments".to_string());
376 assert_eq!(
377 go_mod.comment,
378 vec![
379 "1st comment".to_string(),
380 "2nd comment".to_string(),
381 "3rd comment".to_string(),
382 "4th comment".to_string(),
383 ]
384 );
385 }
386}