miniextendr_lint/
description.rs1use std::collections::HashSet;
9use std::fs;
10use std::path::{Path, PathBuf};
11
12const DEPENDENCY_FIELDS: &[&str] = &["Depends", "Imports", "Suggests", "Enhances", "LinkingTo"];
14
15const R_DISTRIBUTED_PACKAGES: &[&str] = &[
17 "base",
18 "compiler",
19 "datasets",
20 "graphics",
21 "grDevices",
22 "grid",
23 "methods",
24 "parallel",
25 "splines",
26 "stats",
27 "stats4",
28 "tcltk",
29 "tools",
30 "utils",
31 "boot",
32 "class",
33 "cluster",
34 "codetools",
35 "foreign",
36 "KernSmooth",
37 "lattice",
38 "MASS",
39 "Matrix",
40 "mgcv",
41 "nlme",
42 "nnet",
43 "rpart",
44 "spatial",
45 "survival",
46];
47
48#[derive(#[automatically_derived]
impl ::core::clone::Clone for Description {
#[inline]
fn clone(&self) -> Self {
Description {
package: ::core::clone::Clone::clone(&self.package),
declared: ::core::clone::Clone::clone(&self.declared),
}
}
}Clone, #[automatically_derived]
impl ::core::fmt::Debug for Description {
#[inline]
fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result {
::core::fmt::Formatter::debug_struct_field2_finish(f, "Description",
"package", &self.package, "declared", &&self.declared)
}
}Debug, #[automatically_derived]
impl ::core::marker::StructuralPartialEq for Description { }
#[automatically_derived]
impl ::core::cmp::PartialEq for Description {
#[inline]
fn eq(&self, other: &Self) -> bool {
self.package == other.package && self.declared == other.declared
}
}PartialEq, #[automatically_derived]
impl ::core::cmp::Eq for Description {
#[inline]
#[doc(hidden)]
#[coverage(off)]
fn assert_fields_are_eq(&self) {
let _: ::core::cmp::AssertParamIsEq<String>;
let _: ::core::cmp::AssertParamIsEq<HashSet<String>>;
}
}Eq)]
50pub struct Description {
51 pub package: String,
53 pub declared: HashSet<String>,
56}
57
58impl Description {
59 pub fn resolves(&self, pkg: &str) -> bool {
62 pkg == self.package || self.declared.contains(pkg) || R_DISTRIBUTED_PACKAGES.contains(&pkg)
63 }
64
65 pub fn parse(text: &str) -> Option<Self> {
68 let mut package = None;
69 let mut declared = HashSet::new();
70 for (field, value) in fields(text) {
71 if field == "Package" {
72 package = Some(value.trim().to_string());
73 } else if DEPENDENCY_FIELDS.contains(&field.as_str()) {
74 declared.extend(
75 value
76 .split(',')
77 .map(|dep| dep.split('(').next().unwrap_or("").trim())
78 .filter(|dep| !dep.is_empty())
79 .map(str::to_string),
80 );
81 }
82 }
83 Some(Self {
84 package: package.filter(|p| !p.is_empty())?,
85 declared,
86 })
87 }
88}
89
90pub fn locate(manifest_dir: &Path) -> Option<PathBuf> {
93 let path = manifest_dir.join("../../DESCRIPTION");
94 path.is_file().then_some(path)
95}
96
97pub fn read(manifest_dir: &Path) -> Option<Description> {
99 Description::parse(&fs::read_to_string(locate(manifest_dir)?).ok()?)
100}
101
102fn fields(text: &str) -> Vec<(String, String)> {
104 let mut out: Vec<(String, String)> = Vec::new();
105 for line in text.lines() {
106 if line.starts_with([' ', '\t']) {
107 if let Some((_, value)) = out.last_mut() {
108 value.push(' ');
109 value.push_str(line.trim());
110 }
111 } else if let Some((field, value)) = line.split_once(':') {
112 out.push((field.trim().to_string(), value.trim().to_string()));
113 }
114 }
115 out
116}
117
118#[cfg(test)]
119mod tests {
120 use super::*;
121
122 #[test]
123 fn parses_package_and_dependencies_across_continuation_lines() {
124 let d = Description::parse(
125 "Package: mypkg\nImports: cli (>= 3.0),\n rlang,\n vctrs\nSuggests: testthat\nLinkingTo: cpp11\nTitle: x, y\n",
126 )
127 .unwrap();
128 assert_eq!(d.package, "mypkg");
129 for dep in ["cli", "rlang", "vctrs", "testthat", "cpp11"] {
130 assert!(d.declared.contains(dep), "{dep}");
131 }
132 assert!(!d.declared.contains("x"));
133 assert!(d.resolves("mypkg") && d.resolves("stats") && d.resolves("MASS"));
134 assert!(!d.resolves("Sources") && !d.resolves("dplyr"));
135 }
136
137 #[test]
138 fn a_file_without_a_package_field_is_none() {
139 assert_eq!(Description::parse("Title: x\n"), None);
140 }
141}