1#[derive(Debug, Clone, Default, PartialEq, Eq)]
22pub struct PackageNamespace {
23 pub exports: Vec<String>,
25 pub export_patterns: Vec<String>,
27 pub imports: Vec<String>,
29 pub imports_from: Vec<(String, String)>,
31 pub s3_methods: Vec<S3MethodRegistration>,
33 pub use_dyn_libs: Vec<DynLibDirective>,
35 pub export_classes: Vec<String>,
37 pub export_methods: Vec<String>,
39 pub import_classes_from: Vec<(String, String)>,
41 pub import_methods_from: Vec<(String, String)>,
43}
44
45impl PackageNamespace {
46 pub fn export_all() -> Self {
48 PackageNamespace {
49 exports: vec![],
50 export_patterns: vec![".*".to_string()], 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#[derive(Debug, Clone, PartialEq, Eq)]
65pub struct S3MethodRegistration {
66 pub generic: String,
68 pub class: String,
70 pub method: Option<String>,
73}
74
75#[derive(Debug, Clone, PartialEq, Eq)]
77pub struct DynLibDirective {
78 pub library: String,
80 pub registrations: Vec<String>,
82}
83
84#[derive(Debug, Clone, PartialEq, Eq)]
86pub enum NamespaceError {
87 UnbalancedParens { line_number: usize },
89 UnknownDirective {
91 line_number: usize,
92 directive: String,
93 },
94 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 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 tracing::warn!(
238 "NAMESPACE line {}: unknown directive '{}' (ignored)",
239 line_number,
240 name
241 );
242 }
243 }
244 }
245
246 Ok(ns)
247 }
248}
249
250fn 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 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 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 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 let line = if line.starts_with("if ") || line.starts_with("if(") {
314 if let Some(directive_start) = find_directive_after_if(line) {
316 &line[directive_start..]
317 } else {
318 continue; }
320 } else {
321 line
322 };
323
324 let segments: Vec<&str> = if current_name.is_some() {
327 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 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 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 }
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
435fn 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
465fn find_directive_after_if(line: &str) -> Option<usize> {
471 let cond_start = line.find('(')?;
473 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 let rest = line[after_cond..].trim_start();
492 if rest.is_empty() {
493 return None;
494 }
495 Some(line.len() - rest.len())
497}
498
499fn 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
515fn 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 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 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 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 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 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}