git_conventional/
parser.rs

1#![allow(clippy::let_unit_value)] // for clarify and to ensure the right type is selected
2
3use std::str;
4
5use winnow::ascii::line_ending;
6use winnow::combinator::alt;
7use winnow::combinator::repeat;
8use winnow::combinator::trace;
9use winnow::combinator::{cut_err, eof, fail, opt, peek};
10use winnow::combinator::{delimited, preceded, terminated};
11use winnow::error::{AddContext, ErrMode, ParserError, StrContext};
12use winnow::prelude::*;
13use winnow::token::{take, take_till, take_while};
14
15type CommitDetails<'a> = (
16    &'a str,
17    Option<&'a str>,
18    bool,
19    &'a str,
20    Option<&'a str>,
21    Vec<(&'a str, &'a str, &'a str)>,
22);
23
24pub(crate) fn parse<
25    'a,
26    E: ParserError<&'a str> + AddContext<&'a str, StrContext> + std::fmt::Debug,
27>(
28    i: &mut &'a str,
29) -> ModalResult<CommitDetails<'a>, E> {
30    message.parse_next(i)
31}
32
33// <CR>              ::= "0x000D"
34// <LF>              ::= "0x000A"
35// <newline>         ::= [<CR>], <LF>
36fn is_line_ending(c: char) -> bool {
37    c == '\n' || c == '\r'
38}
39
40// <parens>          ::= "(" | ")"
41fn is_parens(c: char) -> bool {
42    c == '(' || c == ')'
43}
44
45// <ZWNBSP>          ::= "U+FEFF"
46// <TAB>             ::= "U+0009"
47// <VT>              ::= "U+000B"
48// <FF>              ::= "U+000C"
49// <SP>              ::= "U+0020"
50// <NBSP>            ::= "U+00A0"
51// /* See: https://www.ecma-international.org/ecma-262/11.0/index.html#sec-white-space */
52// <USP>             ::= "Any other Unicode 'Space_Separator' code point"
53// /* Any non-newline whitespace: */
54// <whitespace>      ::= <ZWNBSP> | <TAB> | <VT> | <FF> | <SP> | <NBSP> | <USP>
55fn is_whitespace(c: char) -> bool {
56    c.is_whitespace()
57}
58
59fn whitespace<'a, E: ParserError<&'a str> + AddContext<&'a str, StrContext> + std::fmt::Debug>(
60    i: &mut &'a str,
61) -> ModalResult<&'a str, E> {
62    take_while(0.., is_whitespace).parse_next(i)
63}
64
65// <message>         ::= <summary>, <newline>+, <body>, (<newline>+, <footer>)*
66//                    |  <summary>, (<newline>+, <footer>)*
67//                    |  <summary>, <newline>*
68pub(crate) fn message<
69    'a,
70    E: ParserError<&'a str> + AddContext<&'a str, StrContext> + std::fmt::Debug,
71>(
72    i: &mut &'a str,
73) -> ModalResult<CommitDetails<'a>, E> {
74    trace("message", move |i: &mut &'a str| {
75        let summary =
76            terminated(trace("summary", summary), alt((line_ending, eof))).parse_next(i)?;
77        let (type_, scope, breaking, description) = summary;
78
79        // The body MUST begin one blank line after the description.
80        let _ = alt((line_ending, eof))
81            .context(StrContext::Label(BODY))
82            .parse_next(i)?;
83
84        let _extra: () = repeat(0.., line_ending).parse_next(i)?;
85
86        let body = opt(body).parse_next(i)?;
87
88        let footers = repeat(0.., footer).parse_next(i)?;
89
90        let _: () = repeat(0.., line_ending).parse_next(i)?;
91
92        Ok((type_, scope, breaking.is_some(), description, body, footers))
93    })
94    .parse_next(i)
95}
96
97// <type>            ::= <any UTF8-octets except newline or parens or ":" or "!:" or whitespace>+
98pub(crate) fn type_<
99    'a,
100    E: ParserError<&'a str> + AddContext<&'a str, StrContext> + std::fmt::Debug,
101>(
102    i: &mut &'a str,
103) -> ModalResult<&'a str, E> {
104    trace(
105        "type",
106        take_while(1.., |c: char| {
107            !is_line_ending(c) && !is_parens(c) && c != ':' && c != '!' && !is_whitespace(c)
108        })
109        .context(StrContext::Label(TYPE)),
110    )
111    .parse_next(i)
112}
113
114pub(crate) const TYPE: &str = "type";
115
116// <scope>           ::= <any UTF8-octets except newline or parens>+
117pub(crate) fn scope<
118    'a,
119    E: ParserError<&'a str> + AddContext<&'a str, StrContext> + std::fmt::Debug,
120>(
121    i: &mut &'a str,
122) -> ModalResult<&'a str, E> {
123    trace(
124        "scope",
125        take_while(1.., |c: char| !is_line_ending(c) && !is_parens(c))
126            .context(StrContext::Label(SCOPE)),
127    )
128    .parse_next(i)
129}
130
131pub(crate) const SCOPE: &str = "scope";
132
133// /* "!" should be added to the AST as a <breaking-change> node with the value "!" */
134// <summary>         ::= <type>, "(", <scope>, ")", ["!"], ":", <whitespace>*, <text>
135//                    |  <type>, ["!"], ":", <whitespace>*, <text>
136#[allow(clippy::type_complexity)]
137fn summary<'a, E: ParserError<&'a str> + AddContext<&'a str, StrContext> + std::fmt::Debug>(
138    i: &mut &'a str,
139) -> ModalResult<(&'a str, Option<&'a str>, Option<&'a str>, &'a str), E> {
140    trace(
141        "summary",
142        (
143            type_,
144            opt(delimited('(', cut_err(scope), ')')),
145            opt(exclamation_mark),
146            preceded(
147                (':', whitespace),
148                text.context(StrContext::Label(DESCRIPTION)),
149            ),
150        ),
151    )
152    .context(StrContext::Label(SUMMARY))
153    .parse_next(i)
154}
155
156pub(crate) const SUMMARY: &str = "SUMMARY";
157pub(crate) const DESCRIPTION: &str = "description";
158
159// <text>            ::= <any UTF8-octets except newline>*
160fn text<'a, E: ParserError<&'a str> + AddContext<&'a str, StrContext> + std::fmt::Debug>(
161    i: &mut &'a str,
162) -> ModalResult<&'a str, E> {
163    trace("text", take_till(1.., is_line_ending)).parse_next(i)
164}
165
166fn body<'a, E: ParserError<&'a str> + AddContext<&'a str, StrContext> + std::fmt::Debug>(
167    i: &mut &'a str,
168) -> ModalResult<&'a str, E> {
169    trace("body", move |i: &mut &'a str| {
170        if i.is_empty() {
171            let start = i.checkpoint();
172            let err = E::from_input(i);
173            let err = err.add_context(i, &start, StrContext::Label(BODY));
174            return Err(ErrMode::Backtrack(err));
175        }
176
177        let mut offset = 0;
178        let mut prior_is_empty = true;
179        for line in crate::lines::LinesWithTerminator::new(i) {
180            if prior_is_empty
181                && peek::<_, _, ErrMode<E>, _>((token, separator))
182                    .parse_peek(line.trim_end())
183                    .is_ok()
184            {
185                break;
186            }
187            prior_is_empty = line.trim().is_empty();
188
189            offset += line.chars().count();
190        }
191        if offset == 0 {
192            fail::<_, (), _>(i)?;
193        }
194
195        take(offset).map(str::trim_end).parse_next(i)
196    })
197    .parse_next(i)
198}
199
200pub(crate) const BODY: &str = "body";
201
202// <footer>          ::= <token>, <separator>, <whitespace>*, <value>
203fn footer<'a, E: ParserError<&'a str> + AddContext<&'a str, StrContext> + std::fmt::Debug>(
204    i: &mut &'a str,
205) -> ModalResult<(&'a str, &'a str, &'a str), E> {
206    trace(
207        "footer",
208        (token, separator, whitespace, value).map(|(ft, s, _, fv)| (ft, s, fv)),
209    )
210    .parse_next(i)
211}
212
213// <token>           ::= <breaking-change>
214//                    |  <type>
215pub(crate) fn token<
216    'a,
217    E: ParserError<&'a str> + AddContext<&'a str, StrContext> + std::fmt::Debug,
218>(
219    i: &mut &'a str,
220) -> ModalResult<&'a str, E> {
221    trace("token", alt(("BREAKING CHANGE", type_))).parse_next(i)
222}
223
224// <separator>       ::= ":" | " #"
225fn separator<'a, E: ParserError<&'a str> + AddContext<&'a str, StrContext> + std::fmt::Debug>(
226    i: &mut &'a str,
227) -> ModalResult<&'a str, E> {
228    trace("sep", alt((":", " #"))).parse_next(i)
229}
230
231pub(crate) fn value<
232    'a,
233    E: ParserError<&'a str> + AddContext<&'a str, StrContext> + std::fmt::Debug,
234>(
235    i: &mut &'a str,
236) -> ModalResult<&'a str, E> {
237    if i.is_empty() {
238        let start = i.checkpoint();
239        let err = E::from_input(i);
240        let err = err.add_context(i, &start, StrContext::Label("value"));
241        return Err(ErrMode::Cut(err));
242    }
243
244    let mut offset = 0;
245    for (i, line) in crate::lines::LinesWithTerminator::new(i).enumerate() {
246        if 0 < i
247            && peek::<_, _, ErrMode<E>, _>((token, separator))
248                .parse_peek(line.trim_end())
249                .is_ok()
250        {
251            break;
252        }
253
254        offset += line.chars().count();
255    }
256
257    take(offset).map(str::trim_end).parse_next(i)
258}
259
260fn exclamation_mark<
261    'a,
262    E: ParserError<&'a str> + AddContext<&'a str, StrContext> + std::fmt::Debug,
263>(
264    i: &mut &'a str,
265) -> ModalResult<&'a str, E> {
266    "!".context(StrContext::Label(BREAKER)).parse_next(i)
267}
268
269pub(crate) const BREAKER: &str = "exclamation_mark";
270
271#[cfg(test)]
272#[allow(clippy::non_ascii_literal)]
273mod tests {
274    use super::*;
275
276    use winnow::error::ContextError;
277
278    mod message {
279        use super::*;
280        #[test]
281        fn errors() {
282            let mut p = message::<ContextError>;
283
284            let input = "Hello World";
285            let err = p.parse(input).unwrap_err();
286            let err = crate::Error::with_nom(input, err);
287            assert_eq!(err.to_string(), crate::ErrorKind::MissingType.to_string());
288
289            let input = "fix Improved error messages\n";
290            let err = p.parse(input).unwrap_err();
291            let err = crate::Error::with_nom(input, err);
292            assert_eq!(err.to_string(), crate::ErrorKind::MissingType.to_string());
293        }
294    }
295
296    mod summary {
297        use super::*;
298
299        #[test]
300        fn test_type() {
301            let mut p = type_::<ContextError>;
302
303            // valid
304            assert_eq!(p.parse_peek("foo").unwrap(), ("", "foo"));
305            assert_eq!(p.parse_peek("Foo").unwrap(), ("", "Foo"));
306            assert_eq!(p.parse_peek("FOO").unwrap(), ("", "FOO"));
307            assert_eq!(p.parse_peek("fOO").unwrap(), ("", "fOO"));
308            assert_eq!(p.parse_peek("foo2bar").unwrap(), ("", "foo2bar"));
309            assert_eq!(p.parse_peek("foo-bar").unwrap(), ("", "foo-bar"));
310            assert_eq!(p.parse_peek("foo bar").unwrap(), (" bar", "foo"));
311            assert_eq!(p.parse_peek("foo: bar").unwrap(), (": bar", "foo"));
312            assert_eq!(p.parse_peek("foo!: bar").unwrap(), ("!: bar", "foo"));
313            assert_eq!(p.parse_peek("foo(bar").unwrap(), ("(bar", "foo"));
314            assert_eq!(p.parse_peek("foo ").unwrap(), (" ", "foo"));
315
316            // invalid
317            assert!(p.parse_peek("").is_err());
318            assert!(p.parse_peek(" ").is_err());
319            assert!(p.parse_peek("  ").is_err());
320            assert!(p.parse_peek(")").is_err());
321            assert!(p.parse_peek(" feat").is_err());
322            assert!(p.parse_peek(" feat ").is_err());
323        }
324
325        #[test]
326        fn test_scope() {
327            let mut p = scope::<ContextError>;
328
329            // valid
330            assert_eq!(p.parse_peek("foo").unwrap(), ("", "foo"));
331            assert_eq!(p.parse_peek("Foo").unwrap(), ("", "Foo"));
332            assert_eq!(p.parse_peek("FOO").unwrap(), ("", "FOO"));
333            assert_eq!(p.parse_peek("fOO").unwrap(), ("", "fOO"));
334            assert_eq!(p.parse_peek("foo bar").unwrap(), ("", "foo bar"));
335            assert_eq!(p.parse_peek("foo-bar").unwrap(), ("", "foo-bar"));
336            assert_eq!(p.parse_peek("x86").unwrap(), ("", "x86"));
337
338            // invalid
339            assert!(p.parse_peek("").is_err());
340            assert!(p.parse_peek(")").is_err());
341        }
342
343        #[test]
344        fn test_text() {
345            let mut p = text::<ContextError>;
346
347            // valid
348            assert_eq!(p.parse_peek("foo").unwrap(), ("", "foo"));
349            assert_eq!(p.parse_peek("Foo").unwrap(), ("", "Foo"));
350            assert_eq!(p.parse_peek("FOO").unwrap(), ("", "FOO"));
351            assert_eq!(p.parse_peek("fOO").unwrap(), ("", "fOO"));
352            assert_eq!(p.parse_peek("foo bar").unwrap(), ("", "foo bar"));
353            assert_eq!(p.parse_peek("foo bar\n").unwrap(), ("\n", "foo bar"));
354            assert_eq!(
355                p.parse_peek("foo\nbar\nbaz").unwrap(),
356                ("\nbar\nbaz", "foo")
357            );
358
359            // invalid
360            assert!(p.parse_peek("").is_err());
361        }
362
363        #[test]
364        fn test_summary() {
365            let mut p = summary::<ContextError>;
366
367            // valid
368            assert_eq!(
369                p.parse_peek("foo: bar").unwrap(),
370                ("", ("foo", None, None, "bar"))
371            );
372            assert_eq!(
373                p.parse_peek("foo(bar): baz").unwrap(),
374                ("", ("foo", Some("bar"), None, "baz"))
375            );
376            assert_eq!(
377                p.parse_peek("foo(bar):     baz").unwrap(),
378                ("", ("foo", Some("bar"), None, "baz"))
379            );
380            assert_eq!(
381                p.parse_peek("foo(bar-baz): qux").unwrap(),
382                ("", ("foo", Some("bar-baz"), None, "qux"))
383            );
384            assert_eq!(
385                p.parse_peek("foo!: bar").unwrap(),
386                ("", ("foo", None, Some("!"), "bar"))
387            );
388
389            // invalid
390            assert!(p.parse_peek("").is_err());
391            assert!(p.parse_peek(" ").is_err());
392            assert!(p.parse_peek("  ").is_err());
393            assert!(p.parse_peek("foo").is_err());
394            assert!(p.parse_peek("foo bar").is_err());
395            assert!(p.parse_peek("foo : bar").is_err());
396            assert!(p.parse_peek("foo bar: baz").is_err());
397            assert!(p.parse_peek("foo(: bar").is_err());
398            assert!(p.parse_peek("foo): bar").is_err());
399            assert!(p.parse_peek("foo(): bar").is_err());
400            assert!(p.parse_peek("foo(bar)").is_err());
401            assert!(p.parse_peek("foo(bar):").is_err());
402            assert!(p.parse_peek("foo(bar): ").is_err());
403            assert!(p.parse_peek("foo(bar):  ").is_err());
404            assert!(p.parse_peek("foo(bar) :baz").is_err());
405            assert!(p.parse_peek("foo(bar) : baz").is_err());
406            assert!(p.parse_peek("foo (bar): baz").is_err());
407            assert!(p.parse_peek("foo bar(baz): qux").is_err());
408        }
409    }
410
411    mod body {
412        use super::*;
413
414        #[test]
415        fn test_body() {
416            let mut p = body::<ContextError>;
417
418            // valid
419            assert_eq!(p.parse_peek("foo").unwrap(), ("", "foo"));
420            assert_eq!(p.parse_peek("Foo").unwrap(), ("", "Foo"));
421            assert_eq!(p.parse_peek("FOO").unwrap(), ("", "FOO"));
422            assert_eq!(p.parse_peek("fOO").unwrap(), ("", "fOO"));
423            assert_eq!(
424                p.parse_peek("    code block").unwrap(),
425                ("", "    code block")
426            );
427            assert_eq!(p.parse_peek("💃🏽").unwrap(), ("", "💃🏽"));
428            assert_eq!(p.parse_peek("foo bar").unwrap(), ("", "foo bar"));
429            assert_eq!(
430                p.parse_peek("foo\nbar\n\nbaz").unwrap(),
431                ("", "foo\nbar\n\nbaz")
432            );
433            assert_eq!(
434                p.parse_peek("foo\n\nBREAKING CHANGE: oops!").unwrap(),
435                ("BREAKING CHANGE: oops!", "foo")
436            );
437            assert_eq!(
438                p.parse_peek("foo\n\nBREAKING-CHANGE: bar").unwrap(),
439                ("BREAKING-CHANGE: bar", "foo")
440            );
441            assert_eq!(
442                p.parse_peek("foo\n\nMy-Footer: bar").unwrap(),
443                ("My-Footer: bar", "foo")
444            );
445            assert_eq!(
446                p.parse_peek("foo\n\nMy-Footer #bar").unwrap(),
447                ("My-Footer #bar", "foo")
448            );
449
450            // invalid
451            assert!(p.parse_peek("").is_err());
452        }
453
454        #[test]
455        fn test_footer() {
456            let mut p = footer::<ContextError>;
457
458            // valid
459            assert_eq!(
460                p.parse_peek("hello: world").unwrap(),
461                ("", ("hello", ":", "world"))
462            );
463            assert_eq!(
464                p.parse_peek("BREAKING CHANGE: woops!").unwrap(),
465                ("", ("BREAKING CHANGE", ":", "woops!"))
466            );
467            assert_eq!(
468                p.parse_peek("Co-Authored-By: Marge Simpson <marge@simpsons.com>")
469                    .unwrap(),
470                (
471                    "",
472                    ("Co-Authored-By", ":", "Marge Simpson <marge@simpsons.com>")
473                )
474            );
475            assert_eq!(
476                p.parse_peek("Closes #12").unwrap(),
477                ("", ("Closes", " #", "12"))
478            );
479            assert_eq!(
480                p.parse_peek("BREAKING-CHANGE: broken").unwrap(),
481                ("", ("BREAKING-CHANGE", ":", "broken"))
482            );
483
484            // invalid
485            assert!(p.parse_peek("").is_err());
486            assert!(p.parse_peek(" ").is_err());
487            assert!(p.parse_peek("  ").is_err());
488            assert!(p.parse_peek("foo").is_err());
489            assert!(p.parse_peek("foo:").is_err());
490            assert!(p.parse_peek("foo: ").is_err());
491            assert!(p.parse_peek("foo ").is_err());
492            assert!(p.parse_peek("foo #").is_err());
493            assert!(p.parse_peek("BREAKING CHANGE").is_err());
494            assert!(p.parse_peek("BREAKING CHANGE:").is_err());
495            assert!(p.parse_peek("Foo-Bar").is_err());
496            assert!(p.parse_peek("Foo-Bar: ").is_err());
497            assert!(p.parse_peek("foo").is_err());
498        }
499    }
500}