Skip to main content

gomod_parser/
lib.rs

1//! A simple `go.mod` file parser
2//!
3//! # Example
4//!
5//! ```rust
6//! use gomod_parser::{GoMod, Module, ModuleDependency};
7//! use std::str::FromStr;
8//!
9//! let input = r#"
10//! module github.com/example
11//!
12//! go 1.21
13//!
14//! require golang.org/x/net v0.20.0
15//! "#;
16//!
17//! let go_mod = GoMod::from_str(input).unwrap();
18//!
19//! assert_eq!(go_mod.module, "github.com/example".to_string());
20//! assert_eq!(go_mod.go, Some("1.21".to_string()));
21//! assert_eq!(
22//!     go_mod.require,
23//!     vec![ModuleDependency {
24//!         module: Module {
25//!             module_path: "golang.org/x/net".to_string(),
26//!             version: "v0.20.0".to_string()
27//!         },
28//!         indirect: false
29//!     }]
30//! );
31//! ```
32
33#![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}