Skip to main content

r/interpreter/packages/
namespace.rs

1//! Parser for R package NAMESPACE files.
2//!
3//! NAMESPACE files use a simple directive-based DSL with function-call syntax.
4//! Each directive is one of:
5//!
6//! - `export(name1, name2, ...)` — export symbols
7//! - `exportPattern("^[^.]")` — export symbols matching a regex
8//! - `import(pkg1, pkg2, ...)` — import all exports from packages
9//! - `importFrom(pkg, sym1, sym2, ...)` — import specific symbols from a package
10//! - `S3method(generic, class)` or `S3method(generic, class, method)` — register S3 methods
11//! - `useDynLib(pkg, ...)` — load a shared library
12//! - `exportClasses(cls1, cls2, ...)` — export S4 classes
13//! - `exportMethods(meth1, meth2, ...)` — export S4 methods
14//! - `importClassesFrom(pkg, cls1, cls2, ...)` — import S4 classes from a package
15//! - `importMethodsFrom(pkg, meth1, meth2, ...)` — import S4 methods from a package
16//!
17//! Lines starting with `#` are comments. Directives can span multiple lines
18//! (the parser collects text until balanced parentheses).
19
20/// A parsed R package NAMESPACE file.
21#[derive(Debug, Clone, Default, PartialEq, Eq)]
22pub struct PackageNamespace {
23    /// Symbols explicitly exported by name.
24    pub exports: Vec<String>,
25    /// Regex patterns for exported symbols.
26    pub export_patterns: Vec<String>,
27    /// Packages whose entire namespace is imported.
28    pub imports: Vec<String>,
29    /// Specific symbol imports: `(package, symbol)` pairs.
30    pub imports_from: Vec<(String, String)>,
31    /// S3 method registrations: `(generic, class, optional method_name)`.
32    pub s3_methods: Vec<S3MethodRegistration>,
33    /// Dynamic library loads: `(library_name, registrations)`.
34    pub use_dyn_libs: Vec<DynLibDirective>,
35    /// S4 classes exported.
36    pub export_classes: Vec<String>,
37    /// S4 methods exported.
38    pub export_methods: Vec<String>,
39    /// S4 classes imported from a package: `(package, class)`.
40    pub import_classes_from: Vec<(String, String)>,
41    /// S4 methods imported from a package: `(package, method)`.
42    pub import_methods_from: Vec<(String, String)>,
43}
44
45impl PackageNamespace {
46    /// Create a namespace that exports everything (used for built-in base packages).
47    pub fn export_all() -> Self {
48        PackageNamespace {
49            exports: vec![],
50            export_patterns: vec![".*".to_string()], // exportPattern(".*")
51            imports: vec![],
52            imports_from: vec![],
53            s3_methods: vec![],
54            use_dyn_libs: vec![],
55            export_classes: vec![],
56            export_methods: vec![],
57            import_classes_from: vec![],
58            import_methods_from: vec![],
59        }
60    }
61}
62
63/// An S3 method registration from the NAMESPACE file.
64#[derive(Debug, Clone, PartialEq, Eq)]
65pub struct S3MethodRegistration {
66    /// The generic function name (e.g. `print`).
67    pub generic: String,
68    /// The class name (e.g. `data.frame`).
69    pub class: String,
70    /// An optional explicit method function name. If absent, R assumes
71    /// the method is named `generic.class`.
72    pub method: Option<String>,
73}
74
75/// A `useDynLib` directive.
76#[derive(Debug, Clone, PartialEq, Eq)]
77pub struct DynLibDirective {
78    /// The shared library name.
79    pub library: String,
80    /// Additional registration entries (symbols, `.registration = TRUE`, etc.).
81    pub registrations: Vec<String>,
82}
83
84/// Errors that can occur when parsing a NAMESPACE file.
85#[derive(Debug, Clone, PartialEq, Eq)]
86pub enum NamespaceError {
87    /// Parentheses are not balanced (unclosed directive).
88    UnbalancedParens { line_number: usize },
89    /// An unknown directive was found.
90    UnknownDirective {
91        line_number: usize,
92        directive: String,
93    },
94    /// A directive had too few arguments.
95    TooFewArgs {
96        line_number: usize,
97        directive: String,
98        expected: usize,
99        got: usize,
100    },
101}
102
103impl std::fmt::Display for NamespaceError {
104    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
105        match self {
106            NamespaceError::UnbalancedParens { line_number } => {
107                write!(
108                    f,
109                    "NAMESPACE line {line_number}: unbalanced parentheses (unclosed directive)"
110                )
111            }
112            NamespaceError::UnknownDirective {
113                line_number,
114                directive,
115            } => {
116                write!(
117                    f,
118                    "NAMESPACE line {line_number}: unknown directive '{directive}'"
119                )
120            }
121            NamespaceError::TooFewArgs {
122                line_number,
123                directive,
124                expected,
125                got,
126            } => {
127                write!(
128                    f,
129                    "NAMESPACE line {line_number}: {directive}() requires at least {expected} argument(s), got {got}"
130                )
131            }
132        }
133    }
134}
135
136impl std::error::Error for NamespaceError {}
137
138impl PackageNamespace {
139    /// Parse a NAMESPACE file from its text content.
140    pub fn parse(input: &str) -> Result<Self, NamespaceError> {
141        let mut ns = PackageNamespace::default();
142        let directives = collect_directives(input)?;
143
144        for (line_number, name, args_str) in directives {
145            let args = parse_args(&args_str);
146
147            match name.as_str() {
148                "export" => {
149                    ns.exports.extend(args);
150                }
151                "exportPattern" => {
152                    ns.export_patterns.extend(args);
153                }
154                "import" => {
155                    ns.imports.extend(args);
156                }
157                "importFrom" => {
158                    if args.len() < 2 {
159                        return Err(NamespaceError::TooFewArgs {
160                            line_number,
161                            directive: name,
162                            expected: 2,
163                            got: args.len(),
164                        });
165                    }
166                    let pkg = &args[0];
167                    for sym in &args[1..] {
168                        ns.imports_from.push((pkg.clone(), sym.clone()));
169                    }
170                }
171                "S3method" => {
172                    if args.len() < 2 {
173                        return Err(NamespaceError::TooFewArgs {
174                            line_number,
175                            directive: name,
176                            expected: 2,
177                            got: args.len(),
178                        });
179                    }
180                    ns.s3_methods.push(S3MethodRegistration {
181                        generic: args[0].clone(),
182                        class: args[1].clone(),
183                        method: args.get(2).cloned(),
184                    });
185                }
186                "useDynLib" => {
187                    if args.is_empty() {
188                        return Err(NamespaceError::TooFewArgs {
189                            line_number,
190                            directive: name,
191                            expected: 1,
192                            got: 0,
193                        });
194                    }
195                    ns.use_dyn_libs.push(DynLibDirective {
196                        library: args[0].clone(),
197                        registrations: args[1..].to_vec(),
198                    });
199                }
200                "exportClasses" | "exportClass" => {
201                    ns.export_classes.extend(args);
202                }
203                "exportMethods" => {
204                    ns.export_methods.extend(args);
205                }
206                "importClassesFrom" => {
207                    if args.len() < 2 {
208                        return Err(NamespaceError::TooFewArgs {
209                            line_number,
210                            directive: name,
211                            expected: 2,
212                            got: args.len(),
213                        });
214                    }
215                    let pkg = &args[0];
216                    for cls in &args[1..] {
217                        ns.import_classes_from.push((pkg.clone(), cls.clone()));
218                    }
219                }
220                "importMethodsFrom" => {
221                    if args.len() < 2 {
222                        return Err(NamespaceError::TooFewArgs {
223                            line_number,
224                            directive: name,
225                            expected: 2,
226                            got: args.len(),
227                        });
228                    }
229                    let pkg = &args[0];
230                    for meth in &args[1..] {
231                        ns.import_methods_from.push((pkg.clone(), meth.clone()));
232                    }
233                }
234                _ => {
235                    // Unknown directives are warnings, not errors — new/uncommon
236                    // directives shouldn't block package loading.
237                    tracing::warn!(
238                        "NAMESPACE line {}: unknown directive '{}' (ignored)",
239                        line_number,
240                        name
241                    );
242                }
243            }
244        }
245
246        Ok(ns)
247    }
248}
249
250/// Collect complete directives from a NAMESPACE file.
251///
252/// A directive is `name(args)` which may span multiple lines. Comments (`#`)
253/// are stripped. Returns `(start_line, directive_name, args_content)` triples.
254fn collect_directives(input: &str) -> Result<Vec<(usize, String, String)>, NamespaceError> {
255    let mut directives = Vec::new();
256    let mut current_name: Option<String> = None;
257    let mut current_args = String::new();
258    let mut paren_depth: usize = 0;
259    let mut start_line: usize = 0;
260
261    for (line_idx, raw_line) in input.lines().enumerate() {
262        let line_number = line_idx + 1;
263
264        // Strip comments (but only outside of quoted strings in the directive)
265        let line = strip_comment(raw_line);
266        let line = line.trim();
267
268        if line.is_empty() {
269            continue;
270        }
271
272        if current_name.is_some() {
273            // We're inside a multi-line directive — accumulate
274            let mut in_quotes = false;
275            for ch in line.chars() {
276                if ch == '"' {
277                    in_quotes = !in_quotes;
278                    current_args.push(ch);
279                    continue;
280                }
281                if in_quotes {
282                    current_args.push(ch);
283                    continue;
284                }
285                if ch == '(' {
286                    paren_depth += 1;
287                    current_args.push(ch);
288                } else if ch == ')' {
289                    if paren_depth == 0 {
290                        continue;
291                    }
292                    paren_depth -= 1;
293                    if paren_depth == 0 {
294                        // Directive complete
295                        directives.push((
296                            start_line,
297                            current_name
298                                .take()
299                                .expect("current_name is Some (checked above)"),
300                            current_args.clone(),
301                        ));
302                        current_args.clear();
303                    } else {
304                        current_args.push(ch);
305                    }
306                } else {
307                    current_args.push(ch);
308                }
309            }
310        } else {
311            // Handle conditional directives: `if (getRversion() < "X.Y.Z") directive(args)`
312            // miniR is a modern R — treat all conditions as true, extract the directive.
313            let line = if line.starts_with("if ") || line.starts_with("if(") {
314                // Find the closing `)` of the condition, then the directive after it
315                if let Some(directive_start) = find_directive_after_if(line) {
316                    &line[directive_start..]
317                } else {
318                    continue; // malformed if — skip
319                }
320            } else {
321                line
322            };
323
324            // Process the line for one or more directives: `name(args) ; name2(args2)`
325            // Split on `;` to handle multiple directives per line
326            let segments: Vec<&str> = if current_name.is_some() {
327                // Continuation line — don't split
328                vec![line]
329            } else {
330                line.split(';').collect()
331            };
332            for segment in segments {
333                let segment = segment.trim();
334                if segment.is_empty() || segment.starts_with('#') {
335                    continue;
336                }
337                if current_name.is_none() {
338                    if let Some(paren_pos) = segment.find('(') {
339                        let name = segment[..paren_pos].trim().to_string();
340                        if name.is_empty() {
341                            continue;
342                        }
343                        start_line = line_number;
344                        current_name = Some(name);
345                        paren_depth = 0;
346
347                        // Process chars from the opening paren
348                        let mut in_quotes = false;
349                        for ch in segment[paren_pos..].chars() {
350                            if ch == '"' {
351                                in_quotes = !in_quotes;
352                                if paren_depth > 0 {
353                                    current_args.push(ch);
354                                }
355                                continue;
356                            }
357                            if in_quotes {
358                                if paren_depth > 0 {
359                                    current_args.push(ch);
360                                }
361                                continue;
362                            }
363                            if ch == '(' {
364                                paren_depth += 1;
365                                if paren_depth > 1 {
366                                    current_args.push(ch);
367                                }
368                            } else if ch == ')' {
369                                paren_depth -= 1;
370                                if paren_depth == 0 {
371                                    if let Some(name) = current_name.take() {
372                                        directives.push((start_line, name, current_args.clone()));
373                                    }
374                                    current_args.clear();
375                                } else {
376                                    current_args.push(ch);
377                                }
378                            } else if paren_depth > 0 {
379                                current_args.push(ch);
380                            }
381                        }
382                    }
383                } else {
384                    // Continuation of a multi-line directive
385                    let mut in_quotes = false;
386                    for ch in segment.chars() {
387                        if ch == '"' {
388                            in_quotes = !in_quotes;
389                            if paren_depth > 0 {
390                                current_args.push(ch);
391                            }
392                            continue;
393                        }
394                        if in_quotes {
395                            if paren_depth > 0 {
396                                current_args.push(ch);
397                            }
398                            continue;
399                        }
400                        if ch == '(' {
401                            paren_depth += 1;
402                            if paren_depth > 1 {
403                                current_args.push(ch);
404                            }
405                        } else if ch == ')' {
406                            paren_depth -= 1;
407                            if paren_depth == 0 {
408                                if let Some(name) = current_name.take() {
409                                    directives.push((start_line, name, current_args.clone()));
410                                }
411                                current_args.clear();
412                            } else {
413                                current_args.push(ch);
414                            }
415                        } else if paren_depth > 0 {
416                            current_args.push(ch);
417                        }
418                    }
419                }
420            }
421            // Lines without `(` that aren't continuations are ignored
422            // (could be stray text or formatting)
423        }
424    }
425
426    if current_name.is_some() {
427        return Err(NamespaceError::UnbalancedParens {
428            line_number: start_line,
429        });
430    }
431
432    Ok(directives)
433}
434
435/// Strip a `#` comment from a line, respecting quoted strings.
436fn strip_comment(line: &str) -> &str {
437    let mut in_double_quote = false;
438    let mut in_single_quote = false;
439    let mut prev_was_backslash = false;
440
441    for (i, ch) in line.char_indices() {
442        if prev_was_backslash {
443            prev_was_backslash = false;
444            continue;
445        }
446        match ch {
447            '\\' => {
448                prev_was_backslash = true;
449            }
450            '"' if !in_single_quote => {
451                in_double_quote = !in_double_quote;
452            }
453            '\'' if !in_double_quote => {
454                in_single_quote = !in_single_quote;
455            }
456            '#' if !in_double_quote && !in_single_quote => {
457                return &line[..i];
458            }
459            _ => {}
460        }
461    }
462    line
463}
464
465/// Parse the argument content of a directive into individual string tokens.
466///
467/// Given a line like `if (getRversion() < "3.2.0") export(anyNA)`,
468/// find the byte offset where the actual directive starts (after the if condition).
469/// Returns None if the line is malformed.
470fn find_directive_after_if(line: &str) -> Option<usize> {
471    // Find the opening `(` of the if condition
472    let cond_start = line.find('(')?;
473    // Walk forward counting parens to find the matching `)`
474    let mut depth = 0;
475    let mut cond_end = None;
476    for (i, ch) in line[cond_start..].char_indices() {
477        match ch {
478            '(' => depth += 1,
479            ')' => {
480                depth -= 1;
481                if depth == 0 {
482                    cond_end = Some(cond_start + i + 1);
483                    break;
484                }
485            }
486            _ => {}
487        }
488    }
489    let after_cond = cond_end?;
490    // Skip whitespace after the condition
491    let rest = line[after_cond..].trim_start();
492    if rest.is_empty() {
493        return None;
494    }
495    // Return offset into original line
496    Some(line.len() - rest.len())
497}
498
499/// Arguments are comma-separated. Surrounding quotes (single or double) are
500/// stripped. Named arguments like `.registration = TRUE` are preserved as
501/// single tokens.
502fn parse_args(args_str: &str) -> Vec<String> {
503    args_str
504        .split(',')
505        .filter_map(|arg| {
506            let arg = arg.trim();
507            if arg.is_empty() {
508                return None;
509            }
510            Some(unquote(arg).to_string())
511        })
512        .collect()
513}
514
515/// Remove surrounding quotes from a string.
516fn unquote(s: &str) -> &str {
517    let s = s.trim();
518    if (s.starts_with('"') && s.ends_with('"')) || (s.starts_with('\'') && s.ends_with('\'')) {
519        &s[1..s.len() - 1]
520    } else {
521        s
522    }
523}
524
525#[cfg(test)]
526mod tests {
527    use super::*;
528
529    #[test]
530    fn parse_simple_exports() {
531        let input = "\
532export(foo)
533export(bar, baz)
534";
535        let ns = PackageNamespace::parse(input).unwrap();
536        assert_eq!(ns.exports, vec!["foo", "bar", "baz"]);
537    }
538
539    #[test]
540    fn parse_export_pattern() {
541        let input = r#"exportPattern("^[^.]")"#;
542        let ns = PackageNamespace::parse(input).unwrap();
543        assert_eq!(ns.export_patterns, vec!["^[^.]"]);
544    }
545
546    #[test]
547    fn parse_import_and_import_from() {
548        let input = "\
549import(methods)
550import(graphics)
551importFrom(Matrix, t, mean, colMeans, colSums)
552";
553        let ns = PackageNamespace::parse(input).unwrap();
554        assert_eq!(ns.imports, vec!["methods", "graphics"]);
555        assert_eq!(
556            ns.imports_from,
557            vec![
558                ("Matrix".to_string(), "t".to_string()),
559                ("Matrix".to_string(), "mean".to_string()),
560                ("Matrix".to_string(), "colMeans".to_string()),
561                ("Matrix".to_string(), "colSums".to_string()),
562            ]
563        );
564    }
565
566    #[test]
567    fn parse_s3method() {
568        let input = "\
569S3method(print, myClass)
570S3method(within, myList, within.list)
571";
572        let ns = PackageNamespace::parse(input).unwrap();
573        assert_eq!(ns.s3_methods.len(), 2);
574        assert_eq!(ns.s3_methods[0].generic, "print");
575        assert_eq!(ns.s3_methods[0].class, "myClass");
576        assert_eq!(ns.s3_methods[0].method, None);
577        assert_eq!(ns.s3_methods[1].generic, "within");
578        assert_eq!(ns.s3_methods[1].class, "myList");
579        assert_eq!(ns.s3_methods[1].method.as_deref(), Some("within.list"));
580    }
581
582    #[test]
583    fn parse_use_dyn_lib() {
584        let input = "useDynLib(myPkg, .registration = TRUE)\n";
585        let ns = PackageNamespace::parse(input).unwrap();
586        assert_eq!(ns.use_dyn_libs.len(), 1);
587        assert_eq!(ns.use_dyn_libs[0].library, "myPkg");
588        assert_eq!(
589            ns.use_dyn_libs[0].registrations,
590            vec![".registration = TRUE"]
591        );
592    }
593
594    #[test]
595    fn parse_with_comments() {
596        let input = "\
597# This is a comment
598export(myList)
599
600##                       within.list is in base
601S3method(within, myList, within.list)
602";
603        let ns = PackageNamespace::parse(input).unwrap();
604        assert_eq!(ns.exports, vec!["myList"]);
605        assert_eq!(ns.s3_methods.len(), 1);
606        assert_eq!(ns.s3_methods[0].generic, "within");
607    }
608
609    #[test]
610    fn parse_inline_comments() {
611        let input = "\
612export(nil)
613export(search)# --> \"conflict message\"
614importClassesFrom(pkgA, \"classA\")# but not \"classApp\"
615";
616        let ns = PackageNamespace::parse(input).unwrap();
617        assert_eq!(ns.exports, vec!["nil", "search"]);
618        assert_eq!(
619            ns.import_classes_from,
620            vec![("pkgA".to_string(), "classA".to_string())]
621        );
622    }
623
624    #[test]
625    fn parse_multiline_directive() {
626        let input = "\
627exportMethods(
628 pubGenf, pubfn,
629 plot, show
630)
631";
632        let ns = PackageNamespace::parse(input).unwrap();
633        assert_eq!(ns.export_methods, vec!["pubGenf", "pubfn", "plot", "show"]);
634    }
635
636    #[test]
637    fn parse_quoted_args() {
638        let input = r#"
639importFrom("graphics", plot)
640exportClasses("classA", "classApp")
641"#;
642        let ns = PackageNamespace::parse(input).unwrap();
643        assert_eq!(
644            ns.imports_from,
645            vec![("graphics".to_string(), "plot".to_string())]
646        );
647        assert_eq!(ns.export_classes, vec!["classA", "classApp"]);
648    }
649
650    #[test]
651    fn parse_s3export_namespace() {
652        // Real NAMESPACE from tests/Pkgs/S3export
653        let input = "\
654export(myList)
655
656##                       within.list is in base
657S3method(within, myList, within.list)
658";
659        let ns = PackageNamespace::parse(input).unwrap();
660        assert_eq!(ns.exports, vec!["myList"]);
661        assert_eq!(ns.s3_methods.len(), 1);
662        assert_eq!(ns.s3_methods[0].generic, "within");
663        assert_eq!(ns.s3_methods[0].class, "myList");
664        assert_eq!(ns.s3_methods[0].method.as_deref(), Some("within.list"));
665    }
666
667    #[test]
668    fn parse_pkgb_namespace() {
669        // Real NAMESPACE from tests/Pkgs/pkgB
670        let input = "\
671import(methods)
672
673import(graphics)
674
675importMethodsFrom(pkgA, \"plot\")
676
677importClassesFrom(pkgA, \"classA\")# but not \"classApp\"
678";
679        let ns = PackageNamespace::parse(input).unwrap();
680        assert_eq!(ns.imports, vec!["methods", "graphics"]);
681        assert_eq!(
682            ns.import_methods_from,
683            vec![("pkgA".to_string(), "plot".to_string())]
684        );
685        assert_eq!(
686            ns.import_classes_from,
687            vec![("pkgA".to_string(), "classA".to_string())]
688        );
689    }
690
691    #[test]
692    fn parse_exnss4_namespace() {
693        // Real NAMESPACE from tests/Pkgs/exNSS4
694        let input = r#"
695importFrom("graphics", plot) # because we want to define methods on it
696
697## Generics and functions defined in this package
698export(pubGenf, pubfn, # generic functions
699       assertError)# and a simple one
700
701## own classes
702exportClasses(pubClass, subClass)# both classes
703
704exportMethods(
705 ## for own generics:
706 pubGenf, pubfn,
707 ## for other generics:
708 plot, show
709)
710
711## The "Matrix-like"
712exportClasses("atomicVector", "array_or_vector")
713exportClasses("M", "dM", "diagM", "ddiM") ## but *not* "mM" !
714"#;
715        let ns = PackageNamespace::parse(input).unwrap();
716        assert_eq!(
717            ns.imports_from,
718            vec![("graphics".to_string(), "plot".to_string())]
719        );
720        assert_eq!(ns.exports, vec!["pubGenf", "pubfn", "assertError"]);
721        assert_eq!(
722            ns.export_classes,
723            vec![
724                "pubClass",
725                "subClass",
726                "atomicVector",
727                "array_or_vector",
728                "M",
729                "dM",
730                "diagM",
731                "ddiM",
732            ]
733        );
734        assert_eq!(ns.export_methods, vec!["pubGenf", "pubfn", "plot", "show"]);
735    }
736
737    #[test]
738    fn parse_pkgd_namespace() {
739        // Real NAMESPACE from tests/Pkgs/pkgD
740        let input = "\
741import(methods)
742
743import(graphics)
744## instead of just
745## importFrom(\"graphics\", plot) # because we want to define methods on it
746## *Still* do not want warning from this
747
748## as \"mgcv\": this loads Matrix, but does not attach it  ==> Matrix methods \"semi-visible\"
749importFrom(Matrix, t,mean,colMeans,colSums)
750
751exportClasses(\"classA\", \"classApp\") # mother and sub-class R/pkgA.R
752
753exportMethods(\"plot\")
754
755export(nil)
756
757export(search)# --> \"conflict message\"
758";
759        let ns = PackageNamespace::parse(input).unwrap();
760        assert_eq!(ns.imports, vec!["methods", "graphics"]);
761        assert_eq!(
762            ns.imports_from,
763            vec![
764                ("Matrix".to_string(), "t".to_string()),
765                ("Matrix".to_string(), "mean".to_string()),
766                ("Matrix".to_string(), "colMeans".to_string()),
767                ("Matrix".to_string(), "colSums".to_string()),
768            ]
769        );
770        assert_eq!(ns.export_classes, vec!["classA", "classApp"]);
771        assert_eq!(ns.export_methods, vec!["plot"]);
772        assert_eq!(ns.exports, vec!["nil", "search"]);
773    }
774
775    #[test]
776    fn unbalanced_parens_error() {
777        let input = "export(foo, bar\n";
778        let err = PackageNamespace::parse(input).unwrap_err();
779        assert!(matches!(err, NamespaceError::UnbalancedParens { .. }));
780    }
781
782    #[test]
783    fn unknown_directive_is_ignored() {
784        // Unknown directives are now warnings (not errors) so packages
785        // with new/uncommon directives can still load.
786        let input = "frobnicate(foo)\nexport(bar)\n";
787        let ns = PackageNamespace::parse(input).expect("should parse with unknown directive");
788        assert_eq!(ns.exports, vec!["bar"]);
789    }
790
791    #[test]
792    fn import_from_too_few_args() {
793        let input = "importFrom(onlypkg)\n";
794        let err = PackageNamespace::parse(input).unwrap_err();
795        match err {
796            NamespaceError::TooFewArgs {
797                directive,
798                expected,
799                got,
800                ..
801            } => {
802                assert_eq!(directive, "importFrom");
803                assert_eq!(expected, 2);
804                assert_eq!(got, 1);
805            }
806            _ => panic!("expected TooFewArgs error"),
807        }
808    }
809
810    #[test]
811    fn empty_namespace() {
812        let input = "\n# just comments\n\n";
813        let ns = PackageNamespace::parse(input).unwrap();
814        assert_eq!(ns, PackageNamespace::default());
815    }
816}