Skip to main content

r_description/
lossy.rs

1//! A library for parsing and manipulating R DESCRIPTION files.
2//!
3//! See https://r-pkgs.org/description.html and https://cran.r-project.org/doc/manuals/R-exts.html
4//! for more information
5//!
6//! See the ``lossless`` module for a lossless parser that is
7//! forgiving in the face of errors and preserves formatting while editing
8//! at the expense of a more complex API.
9use deb822_derive::{FromDeb822, ToDeb822};
10use deb822_fast::{FromDeb822Paragraph, ToDeb822Paragraph};
11
12use crate::RCode;
13use std::iter::Peekable;
14
15use crate::relations::SyntaxKind::*;
16use crate::relations::{lex, SyntaxKind, VersionConstraint};
17use crate::version::Version;
18
19#[derive(Debug, Clone, PartialEq, Eq)]
20/// A URL entry in the URL field.
21pub struct UrlEntry {
22    /// URL
23    pub url: url::Url,
24
25    /// Optional label for the URL.
26    pub label: Option<String>,
27}
28
29impl std::fmt::Display for UrlEntry {
30    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
31        write!(f, "{}", self.url.as_str())?;
32        if let Some(label) = &self.label {
33            write!(f, " ({label})")?;
34        }
35        Ok(())
36    }
37}
38
39impl std::str::FromStr for UrlEntry {
40    type Err = String;
41
42    fn from_str(s: &str) -> Result<Self, Self::Err> {
43        if let Some(pos) = s.find('(') {
44            let url = s[..pos].trim();
45            let label = s[pos + 1..s.len() - 1].trim();
46            Ok(UrlEntry {
47                url: url::Url::parse(url).map_err(|e| e.to_string())?,
48                label: Some(label.to_string()),
49            })
50        } else {
51            Ok(UrlEntry {
52                url: url::Url::parse(s).map_err(|e| e.to_string())?,
53                label: None,
54            })
55        }
56    }
57}
58
59fn serialize_url_list(urls: &[UrlEntry]) -> String {
60    let mut s = String::new();
61    for (i, url) in urls.iter().enumerate() {
62        if i > 0 {
63            s.push_str(", ");
64        }
65        s.push_str(url.to_string().as_str());
66    }
67    s
68}
69
70fn deserialize_url_list(s: &str) -> Result<Vec<UrlEntry>, String> {
71    s.split([',', '\n'].as_ref())
72        .filter(|s| !s.trim().is_empty())
73        .map(|s| s.trim().parse())
74        .collect::<Result<Vec<_>, String>>()
75        .map_err(|e| e.to_string())
76}
77
78#[derive(FromDeb822, ToDeb822, Debug, PartialEq, Eq)]
79/// A DESCRIPTION file.
80pub struct RDescription {
81    /// The name of the package.
82    #[deb822(field = "Package")]
83    pub name: String,
84
85    /// A short description of the package.
86    #[deb822(field = "Description")]
87    pub description: String,
88
89    #[deb822(field = "Title")]
90    /// The title of the package.
91    pub title: String,
92
93    #[deb822(field = "Maintainer")]
94    /// The maintainer of the package.
95    pub maintainer: Option<String>,
96
97    #[deb822(field = "Author")]
98    /// Who wrote the the package
99    pub author: Option<String>,
100
101    /// 'Authors@R' is a special field that can contain R code
102    /// that is evaluated to get the authors and maintainers.
103    #[deb822(field = "Authors@R")]
104    pub authors: Option<RCode>,
105
106    #[deb822(field = "Version")]
107    /// The version of the package.
108    pub version: Version,
109
110    /// If the DESCRIPTION file is not written in pure ASCII, the encoding
111    /// field must be used to specify the encoding.
112    #[deb822(field = "Encoding")]
113    pub encoding: Option<String>,
114
115    #[deb822(field = "License")]
116    /// The license of the package.
117    pub license: String,
118
119    #[deb822(field = "URL", serialize_with = serialize_url_list, deserialize_with = deserialize_url_list)]
120    // TODO: parse this as a list of URLs, separated by commas
121    /// URLs related to the package.
122    pub url: Option<Vec<UrlEntry>>,
123
124    #[deb822(field = "BugReports")]
125    /// The URL or email address where bug reports should be sent.
126    pub bug_reports: Option<String>,
127
128    #[deb822(field = "Imports")]
129    /// The packages that this package depends on.
130    pub imports: Option<Relations>,
131
132    #[deb822(field = "Suggests")]
133    /// The packages that this package suggests.
134    pub suggests: Option<Relations>,
135
136    #[deb822(field = "Depends")]
137    /// The packages that this package depends on.
138    pub depends: Option<Relations>,
139
140    #[deb822(field = "LinkingTo")]
141    /// The packages that this package links to.
142    pub linking_to: Option<Relations>,
143
144    #[deb822(field = "LazyData")]
145    /// Whether the package has lazy data.
146    pub lazy_data: Option<String>,
147
148    #[deb822(field = "Collate")]
149    /// The order in which R scripts are loaded.
150    pub collate: Option<String>,
151
152    #[deb822(field = "VignetteBuilder")]
153    /// The package used to build vignettes.
154    pub vignette_builder: Option<String>,
155
156    #[deb822(field = "SystemRequirements")]
157    /// The system requirements for the package.
158    pub system_requirements: Option<String>,
159
160    #[deb822(field = "Date")]
161    /// The release date of the current version of the package.
162    /// Strongly recommended to use the ISO 8601 format: YYYY-MM-DD
163    pub date: Option<String>,
164
165    #[deb822(field = "Language")]
166    /// Indicates the package documentation is not in English.
167    /// This should be a comma-separated list of IETF language
168    /// tags as defined by RFC5646
169    pub language: Option<String>,
170
171    #[deb822(field = "Repository")]
172    /// The R Repository to use for this package. E.g. "CRAN" or "Bioconductor"
173    pub repository: Option<String>,
174}
175
176/// A relation entry in a relationship field.
177#[derive(Debug, Clone, PartialEq, Eq, Hash)]
178pub struct Relation {
179    /// Package name.
180    pub name: String,
181    /// Version constraint and version.
182    pub version: Option<(VersionConstraint, Version)>,
183}
184
185impl Default for Relation {
186    fn default() -> Self {
187        Self::new()
188    }
189}
190
191impl Relation {
192    /// Create an empty relation.
193    pub fn new() -> Self {
194        Self {
195            name: String::new(),
196            version: None,
197        }
198    }
199
200    /// Check if this entry is satisfied by the given package versions.
201    ///
202    /// # Arguments
203    /// * `package_version` - A function that returns the version of a package.
204    ///
205    /// # Example
206    /// ```
207    /// use r_description::lossy::Relation;
208    /// use r_description::Version;
209    /// let entry: Relation = "cli (>= 2.0)".parse().unwrap();
210    /// assert!(entry.satisfied_by(|name: &str| -> Option<Version> {
211    ///    match name {
212    ///    "cli" => Some("2.0".parse().unwrap()),
213    ///    _ => None
214    /// }}));
215    /// ```
216    pub fn satisfied_by(&self, package_version: impl crate::relations::VersionLookup) -> bool {
217        let actual = package_version.lookup_version(self.name.as_str());
218        if let Some((vc, version)) = &self.version {
219            if let Some(actual) = actual {
220                match vc {
221                    VersionConstraint::GreaterThanEqual => actual.as_ref() >= version,
222                    VersionConstraint::LessThanEqual => actual.as_ref() <= version,
223                    VersionConstraint::Equal => actual.as_ref() == version,
224                    VersionConstraint::NotEqual => actual.as_ref() != version,
225                    VersionConstraint::GreaterThan => actual.as_ref() > version,
226                    VersionConstraint::LessThan => actual.as_ref() < version,
227                }
228            } else {
229                false
230            }
231        } else {
232            actual.is_some()
233        }
234    }
235}
236
237impl std::fmt::Display for Relation {
238    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
239        write!(f, "{}", self.name)?;
240        if let Some((constraint, version)) = &self.version {
241            write!(f, " ({constraint} {version})")?;
242        }
243        Ok(())
244    }
245}
246
247#[cfg(feature = "serde")]
248impl<'de> serde::Deserialize<'de> for Relation {
249    fn deserialize<D>(deserializer: D) -> Result<Relation, D::Error>
250    where
251        D: serde::Deserializer<'de>,
252    {
253        let s = String::deserialize(deserializer)?;
254        s.parse().map_err(serde::de::Error::custom)
255    }
256}
257
258#[cfg(feature = "serde")]
259impl serde::Serialize for Relation {
260    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
261    where
262        S: serde::Serializer,
263    {
264        self.to_string().serialize(serializer)
265    }
266}
267
268/// A collection of relation entries in a relationship field.
269#[derive(Debug, Clone, PartialEq, Eq, Hash)]
270pub struct Relations(pub Vec<Relation>);
271
272impl std::ops::Index<usize> for Relations {
273    type Output = Relation;
274
275    fn index(&self, index: usize) -> &Self::Output {
276        &self.0[index]
277    }
278}
279
280impl std::ops::IndexMut<usize> for Relations {
281    fn index_mut(&mut self, index: usize) -> &mut Self::Output {
282        &mut self.0[index]
283    }
284}
285
286impl FromIterator<Relation> for Relations {
287    fn from_iter<I: IntoIterator<Item = Relation>>(iter: I) -> Self {
288        Self(iter.into_iter().collect())
289    }
290}
291
292impl Default for Relations {
293    fn default() -> Self {
294        Self::new()
295    }
296}
297
298impl Relations {
299    /// Create an empty relations.
300    pub fn new() -> Self {
301        Self(Vec::new())
302    }
303
304    /// Remove an entry from the relations.
305    pub fn remove(&mut self, index: usize) {
306        self.0.remove(index);
307    }
308
309    /// Iterate over the entries in the relations.
310    pub fn iter(&self) -> impl Iterator<Item = &Relation> {
311        self.0.iter()
312    }
313
314    /// Number of entries in the relations.
315    pub fn len(&self) -> usize {
316        self.0.len()
317    }
318
319    /// Check if the relations are empty.
320    pub fn is_empty(&self) -> bool {
321        self.0.is_empty()
322    }
323
324    /// Check if the relations are satisfied by the given package versions.
325    pub fn satisfied_by(
326        &self,
327        package_version: impl crate::relations::VersionLookup + Copy,
328    ) -> bool {
329        self.0.iter().all(|r| r.satisfied_by(package_version))
330    }
331}
332
333impl std::fmt::Display for Relations {
334    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
335        for (i, relation) in self.0.iter().enumerate() {
336            if i > 0 {
337                f.write_str(", ")?;
338            }
339            write!(f, "{relation}")?;
340        }
341        Ok(())
342    }
343}
344
345impl std::str::FromStr for Relation {
346    type Err = String;
347
348    fn from_str(s: &str) -> Result<Self, Self::Err> {
349        let tokens = lex(s);
350        let mut tokens = tokens.into_iter().peekable();
351
352        fn eat_whitespace(tokens: &mut Peekable<impl Iterator<Item = (SyntaxKind, String)>>) {
353            while let Some((k, _)) = tokens.peek() {
354                match k {
355                    WHITESPACE | NEWLINE => {
356                        tokens.next();
357                    }
358                    _ => break,
359                }
360            }
361        }
362
363        let name = match tokens.next() {
364            Some((IDENT, name)) => name,
365            _ => return Err("Expected package name".to_string()),
366        };
367
368        eat_whitespace(&mut tokens);
369
370        let version = if let Some((L_PARENS, _)) = tokens.peek() {
371            tokens.next();
372            eat_whitespace(&mut tokens);
373            let mut constraint = String::new();
374            while let Some((kind, t)) = tokens.peek() {
375                match kind {
376                    EQUAL | L_ANGLE | R_ANGLE | NOT => {
377                        constraint.push_str(t);
378                        tokens.next();
379                    }
380                    _ => break,
381                }
382            }
383            let constraint = constraint.parse()?;
384            eat_whitespace(&mut tokens);
385            // Read IDENT and COLON tokens until we see R_PARENS
386            let version_string = match tokens.next() {
387                Some((IDENT, s)) => s,
388                _ => return Err("Expected version string".to_string()),
389            };
390            let version: Version = version_string.parse().map_err(|e: String| e.to_string())?;
391            eat_whitespace(&mut tokens);
392            if let Some((R_PARENS, _)) = tokens.next() {
393            } else {
394                return Err(format!("Expected ')', found {:?}", tokens.next()));
395            }
396            Some((constraint, version))
397        } else {
398            None
399        };
400
401        eat_whitespace(&mut tokens);
402
403        if let Some((kind, _)) = tokens.next() {
404            return Err(format!("Unexpected token: {kind:?}"));
405        }
406
407        Ok(Relation { name, version })
408    }
409}
410
411impl std::str::FromStr for Relations {
412    type Err = String;
413
414    fn from_str(s: &str) -> Result<Self, Self::Err> {
415        let mut relations = Vec::new();
416        if s.is_empty() {
417            return Ok(Relations(relations));
418        }
419        for relation in s.split(',') {
420            let relation = relation.trim();
421            if relation.is_empty() {
422                // Ignore empty entries.
423                continue;
424            }
425            relations.push(relation.parse()?);
426        }
427        Ok(Relations(relations))
428    }
429}
430
431#[cfg(feature = "serde")]
432impl<'de> serde::Deserialize<'de> for Relations {
433    fn deserialize<D>(deserializer: D) -> Result<Relations, D::Error>
434    where
435        D: serde::Deserializer<'de>,
436    {
437        let s = String::deserialize(deserializer)?;
438        s.parse().map_err(serde::de::Error::custom)
439    }
440}
441
442#[cfg(feature = "serde")]
443impl serde::Serialize for Relations {
444    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
445    where
446        S: serde::Serializer,
447    {
448        self.to_string().serialize(serializer)
449    }
450}
451
452impl std::str::FromStr for RDescription {
453    type Err = String;
454
455    fn from_str(s: &str) -> Result<Self, Self::Err> {
456        let para = deb822_fast::Paragraph::from_str(s).map_err(|e| e.to_string())?;
457        Self::from_paragraph(&para)
458    }
459}
460
461impl std::fmt::Display for RDescription {
462    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
463        let para: deb822_fast::Paragraph = self.to_paragraph();
464        f.write_str(&para.to_string())?;
465        Ok(())
466    }
467}
468
469#[cfg(test)]
470mod tests {
471    use super::*;
472
473    #[test]
474    fn test_parse() {
475        let s = r###"Package: mypackage
476Title: What the Package Does (One Line, Title Case)
477Version: 0.0.0.9000
478Authors@R: 
479    person("First", "Last", , "first.last@example.com", role = c("aut", "cre"),
480           comment = c(ORCID = "YOUR-ORCID-ID"))
481Description: What the package does (one paragraph).
482License: `use_mit_license()`, `use_gpl3_license()` or friends to pick a
483    license
484Encoding: UTF-8
485Roxygen: list(markdown = TRUE)
486RoxygenNote: 7.3.2
487"###;
488        let desc: RDescription = s.parse().unwrap();
489
490        assert_eq!(desc.name, "mypackage".to_string());
491        assert_eq!(
492            desc.title,
493            "What the Package Does (One Line, Title Case)".to_string()
494        );
495        assert_eq!(desc.version, "0.0.0.9000".parse().unwrap());
496        assert_eq!(
497            desc.authors,
498            Some(RCode(
499                r#"
500person("First", "Last", , "first.last@example.com", role = c("aut", "cre"),
501comment = c(ORCID = "YOUR-ORCID-ID"))"#
502                    .to_string()
503            ))
504        );
505        assert_eq!(
506            desc.description,
507            "What the package does (one paragraph).".to_string()
508        );
509        assert_eq!(
510            desc.license,
511            "`use_mit_license()`, `use_gpl3_license()` or friends to pick a\nlicense".to_string()
512        );
513        assert_eq!(desc.encoding, Some("UTF-8".to_string()));
514
515        assert_eq!(
516            desc.to_string(),
517            r###"Package: mypackage
518Description: What the package does (one paragraph).
519Title: What the Package Does (One Line, Title Case)
520Authors@R: 
521 person("First", "Last", , "first.last@example.com", role = c("aut", "cre"),
522 comment = c(ORCID = "YOUR-ORCID-ID"))
523Version: 0.0.0.9000
524Encoding: UTF-8
525License: `use_mit_license()`, `use_gpl3_license()` or friends to pick a
526 license
527"###
528        );
529    }
530
531    #[test]
532    fn test_parse_dplyr() {
533        let s = include_str!("../testdata/dplyr.desc");
534        let desc: RDescription = s.parse().unwrap();
535
536        assert_eq!(desc.name, "dplyr".to_string());
537    }
538
539    #[test]
540    fn test_parse_relations() {
541        let input = "cli";
542        let parsed: Relations = input.parse().unwrap();
543        assert_eq!(parsed.to_string(), input);
544        assert_eq!(parsed.len(), 1);
545        let relation = &parsed[0];
546        assert_eq!(relation.to_string(), "cli");
547        assert_eq!(relation.version, None);
548
549        let input = "cli (>= 0.20.21)";
550        let parsed: Relations = input.parse().unwrap();
551        assert_eq!(parsed.to_string(), input);
552        assert_eq!(parsed.len(), 1);
553        let relation = &parsed[0];
554        assert_eq!(relation.to_string(), "cli (>= 0.20.21)");
555        assert_eq!(
556            relation.version,
557            Some((
558                VersionConstraint::GreaterThanEqual,
559                "0.20.21".parse().unwrap()
560            ))
561        );
562
563        let parsed: Relations = "xml2 (> 1.0.0)".parse().unwrap();
564        assert_eq!(parsed.len(), 1);
565        assert_eq!(parsed[0].name, "xml2");
566        assert_eq!(
567            parsed[0].version,
568            Some((VersionConstraint::GreaterThan, "1.0.0".parse().unwrap()))
569        );
570
571        let parsed: Relations = "xml2 (< 2.0.0)".parse().unwrap();
572        assert_eq!(parsed.len(), 1);
573        assert_eq!(parsed[0].name, "xml2");
574        assert_eq!(
575            parsed[0].version,
576            Some((VersionConstraint::LessThan, "2.0.0".parse().unwrap()))
577        );
578
579        let parsed: Relations = "xml2 (== 2.0.0)".parse().unwrap();
580        assert_eq!(parsed.len(), 1);
581        assert_eq!(
582            parsed[0].version,
583            Some((VersionConstraint::Equal, "2.0.0".parse().unwrap()))
584        );
585        assert_eq!(parsed.to_string(), "xml2 (== 2.0.0)");
586
587        let parsed: Relations = "xml2 (!= 2.0.0)".parse().unwrap();
588        assert_eq!(parsed.len(), 1);
589        assert_eq!(
590            parsed[0].version,
591            Some((VersionConstraint::NotEqual, "2.0.0".parse().unwrap()))
592        );
593        assert_eq!(parsed.to_string(), "xml2 (!= 2.0.0)");
594    }
595
596    #[test]
597    fn test_multiple() {
598        let input = "cli (>= 0.20.21), cli (< 0.21)";
599        let parsed: Relations = input.parse().unwrap();
600        assert_eq!(parsed.to_string(), input);
601        assert_eq!(parsed.len(), 2);
602        let relation = &parsed[0];
603        assert_eq!(relation.to_string(), "cli (>= 0.20.21)");
604        assert_eq!(
605            relation.version,
606            Some((
607                VersionConstraint::GreaterThanEqual,
608                "0.20.21".parse().unwrap()
609            ))
610        );
611        let relation = &parsed[1];
612        assert_eq!(relation.to_string(), "cli (< 0.21)");
613        assert_eq!(
614            relation.version,
615            Some((VersionConstraint::LessThan, "0.21".parse().unwrap()))
616        );
617    }
618
619    #[cfg(feature = "serde")]
620    #[test]
621    fn test_serde_relations() {
622        let input = "cli (>= 0.20.21), cli (< 0.21)";
623        let parsed: Relations = input.parse().unwrap();
624        let serialized = serde_json::to_string(&parsed).unwrap();
625        assert_eq!(serialized, r#""cli (>= 0.20.21), cli (< 0.21)""#);
626        let deserialized: Relations = serde_json::from_str(&serialized).unwrap();
627        assert_eq!(deserialized, parsed);
628    }
629
630    #[cfg(feature = "serde")]
631    #[test]
632    fn test_serde_relation() {
633        let input = "cli (>= 0.20.21)";
634        let parsed: Relation = input.parse().unwrap();
635        let serialized = serde_json::to_string(&parsed).unwrap();
636        assert_eq!(serialized, r#""cli (>= 0.20.21)""#);
637        let deserialized: Relation = serde_json::from_str(&serialized).unwrap();
638        assert_eq!(deserialized, parsed);
639    }
640
641    #[test]
642    fn test_relations_is_empty() {
643        let input = "cli (>= 0.20.21)";
644        let parsed: Relations = input.parse().unwrap();
645        assert!(!parsed.is_empty());
646        let input = "";
647        let parsed: Relations = input.parse().unwrap();
648        assert!(parsed.is_empty());
649    }
650
651    #[test]
652    fn test_relations_len() {
653        let input = "cli (>= 0.20.21), cli (< 0.21)";
654        let parsed: Relations = input.parse().unwrap();
655        assert_eq!(parsed.len(), 2);
656    }
657
658    #[test]
659    fn test_relations_remove() {
660        let input = "cli (>= 0.20.21), cli (< 0.21)";
661        let mut parsed: Relations = input.parse().unwrap();
662        parsed.remove(1);
663        assert_eq!(parsed.len(), 1);
664        assert_eq!(parsed.to_string(), "cli (>= 0.20.21)");
665    }
666
667    #[test]
668    fn test_relations_satisfied_by() {
669        let input = "cli (>= 0.20.21), cli (< 0.21)";
670        let parsed: Relations = input.parse().unwrap();
671        assert!(parsed.satisfied_by(|name: &str| -> Option<Version> {
672            match name {
673                "cli" => Some("0.20.21".parse().unwrap()),
674                _ => None,
675            }
676        }));
677        assert!(!parsed.satisfied_by(|name: &str| -> Option<Version> {
678            match name {
679                "cli" => Some("0.21".parse().unwrap()),
680                _ => None,
681            }
682        }));
683    }
684
685    #[test]
686    fn test_relation_satisfied_by() {
687        let input = "cli (>= 0.20.21)";
688        let parsed: Relation = input.parse().unwrap();
689        assert!(parsed.satisfied_by(|name: &str| -> Option<Version> {
690            match name {
691                "cli" => Some("0.20.21".parse().unwrap()),
692                _ => None,
693            }
694        }));
695        assert!(!parsed.satisfied_by(|name: &str| -> Option<Version> {
696            match name {
697                "cli" => Some("0.20.20".parse().unwrap()),
698                _ => None,
699            }
700        }));
701    }
702
703    #[test]
704    fn test_parse_url_entry() {
705        let input = "https://example.com/";
706        let parsed: UrlEntry = input.parse().unwrap();
707        assert_eq!(parsed.url.as_str(), input);
708        assert_eq!(parsed.label, None);
709
710        let input = "https://example.com (Example)";
711        let parsed: UrlEntry = input.parse().unwrap();
712        assert_eq!(parsed.url.as_str(), "https://example.com/");
713        assert_eq!(parsed.label, Some("Example".to_string()));
714    }
715
716    #[test]
717    fn test_deserialize_url_list() {
718        let input = "https://example.com/, https://example.org (Example)";
719        let parsed = deserialize_url_list(input).unwrap();
720        assert_eq!(parsed.len(), 2);
721        assert_eq!(parsed[0].url.as_str(), "https://example.com/");
722        assert_eq!(parsed[0].label, None);
723        assert_eq!(parsed[1].url.as_str(), "https://example.org/");
724        assert_eq!(parsed[1].label, Some("Example".to_string()));
725    }
726
727    #[test]
728    fn test_deserialize_url_list2() {
729        let input = "https://example.com/\n https://example.org (Example)\n https://example.net";
730        let parsed = deserialize_url_list(input).unwrap();
731        assert_eq!(parsed.len(), 3);
732        assert_eq!(parsed[0].url.as_str(), "https://example.com/");
733        assert_eq!(parsed[0].label, None);
734        assert_eq!(parsed[1].url.as_str(), "https://example.org/");
735        assert_eq!(parsed[1].label, Some("Example".to_string()));
736        assert_eq!(parsed[2].url.as_str(), "https://example.net/");
737        assert_eq!(parsed[2].label, None);
738    }
739}