Skip to content

Commit 7f9ea95

Browse files
committed
Add INDEX names to set of exported symbols
1 parent 099a6ab commit 7f9ea95

File tree

4 files changed

+168
-7
lines changed

4 files changed

+168
-7
lines changed

crates/ark/src/lsp/diagnostics.rs

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1850,4 +1850,40 @@ foo
18501850
assert_eq!(diagnostics.len(), 1);
18511851
});
18521852
}
1853+
1854+
#[test]
1855+
fn test_penguins_symbol_no_diagnostic() {
1856+
r_task(|| {
1857+
let palmerpenguins_dir = crate::lsp::inputs::package::temp_palmerpenguin();
1858+
let palmerpenguins_pkg = Package::load_from_folder(palmerpenguins_dir.path())
1859+
.unwrap()
1860+
.unwrap();
1861+
let library = Library::new(vec![]).insert("penguins", palmerpenguins_pkg);
1862+
1863+
// Simulate a world state with the penguins package installed and attached
1864+
let mut state = DEFAULT_STATE.clone();
1865+
state.library = library;
1866+
state.console_scopes = vec![vec!["library".to_string()]];
1867+
1868+
let code = r#"
1869+
library(penguins)
1870+
penguins
1871+
path_to_file
1872+
penguins_raw
1873+
"#;
1874+
let document = Document::new(code, None);
1875+
let diagnostics = generate_diagnostics(document, state.clone());
1876+
assert!(diagnostics.is_empty());
1877+
1878+
let code = r#"
1879+
penguins
1880+
path_to_file
1881+
penguins_raw
1882+
library(penguins)
1883+
"#;
1884+
let document = Document::new(code, None);
1885+
let diagnostics = generate_diagnostics(document, state);
1886+
assert_eq!(diagnostics.len(), 3);
1887+
})
1888+
}
18531889
}

crates/ark/src/lsp/inputs/package.rs

Lines changed: 120 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ use std::fs;
99
use std::path::PathBuf;
1010

1111
use crate::lsp::inputs::package_description::Description;
12+
use crate::lsp::inputs::package_index::Index;
1213
use crate::lsp::inputs::package_namespace::Namespace;
1314

1415
/// Represents an R package and its metadata relevant for static analysis.
@@ -22,17 +23,28 @@ pub struct Package {
2223
pub namespace: Namespace,
2324

2425
// List of symbols exported via NAMESPACE `export()` directives and via
25-
// `DocType{data}`. Note the latter should only apply to packages with
26-
// `LazyData: true` but currently applies to all packages, as a stopgap
27-
// to prevent spurious diagnostics (we accept false negatives to avoid
28-
// annoying false positives).
26+
// documented symbols listed in INDEX. The latter is a stopgap to ensure we
27+
// support exported datasets and prevent spurious diagnostics (we accept
28+
// false negatives to avoid annoying false positives).
2929
pub exported_symbols: Vec<String>,
3030
}
3131

3232
impl Package {
33-
pub fn new(path: PathBuf, description: Description, namespace: Namespace) -> Self {
33+
pub fn new(
34+
path: PathBuf,
35+
description: Description,
36+
namespace: Namespace,
37+
index: Index,
38+
) -> Self {
3439
// Compute exported symbols. Start from explicit NAMESPACE exports.
35-
let exported_symbols = namespace.exports.clone();
40+
let mut exported_symbols = namespace.exports.clone();
41+
42+
// Add all documented symbols. This should cover documented datasets.
43+
exported_symbols.extend(index.names.iter().cloned());
44+
45+
// Sort and deduplicate (we expect lots of duplicates)
46+
exported_symbols.sort();
47+
exported_symbols.dedup();
3648

3749
Self {
3850
path,
@@ -44,7 +56,7 @@ impl Package {
4456

4557
#[cfg(test)]
4658
pub fn from_parts(path: PathBuf, description: Description, namespace: Namespace) -> Self {
47-
Self::new(path, description, namespace)
59+
Self::new(path, description, namespace, Index::default())
4860
}
4961

5062
/// Load a package from a given path.
@@ -73,10 +85,22 @@ impl Package {
7385
Namespace::default()
7486
};
7587

88+
let index = match Index::load_from_folder(package_path) {
89+
Ok(index) => index,
90+
Err(err) => {
91+
tracing::warn!(
92+
"Can't load INDEX file from `{path}`: {err:?}",
93+
path = package_path.to_string_lossy()
94+
);
95+
Index::default()
96+
},
97+
};
98+
7699
Ok(Some(Self::new(
77100
package_path.to_path_buf(),
78101
description,
79102
namespace,
103+
index,
80104
)))
81105
}
82106

@@ -95,9 +119,98 @@ impl Package {
95119
"`Package` field in `DESCRIPTION` doesn't match folder name '{name}'"
96120
));
97121
}
122+
98123
Ok(Some(pkg))
99124
} else {
100125
Ok(None)
101126
}
102127
}
103128
}
129+
130+
#[cfg(test)]
131+
mod tests {
132+
use super::*;
133+
use crate::lsp::inputs::package_description::Description;
134+
use crate::lsp::inputs::package_index::Index;
135+
use crate::lsp::inputs::package_namespace::Namespace;
136+
137+
fn new_package(name: &str, ns: Namespace, index: Index) -> Package {
138+
Package::new(
139+
std::path::PathBuf::from("/fake"),
140+
Description {
141+
name: name.to_string(),
142+
..Description::default()
143+
},
144+
ns,
145+
index,
146+
)
147+
}
148+
149+
#[test]
150+
fn exported_symbols_are_sorted_and_unique() {
151+
let mut ns = Namespace::default();
152+
ns.exports = vec!["b".to_string(), "a".to_string(), "a".to_string()];
153+
154+
let mut index = Index::default();
155+
index.names = vec!["c".to_string(), "a".to_string(), "a".to_string()];
156+
157+
let pkg = new_package("foo", ns, index);
158+
assert_eq!(pkg.exported_symbols, vec!["a", "b", "c"]);
159+
}
160+
161+
#[test]
162+
fn exported_symbols_empty_when_none() {
163+
let ns = Namespace::default();
164+
let idx = Index::default();
165+
let pkg = new_package("foo", ns, idx);
166+
assert!(pkg.exported_symbols.is_empty());
167+
}
168+
169+
#[test]
170+
fn load_from_folder_reads_description_namespace_and_index() {
171+
let dir = temp_palmerpenguin();
172+
173+
let pkg = Package::load_from_folder(dir.path()).unwrap().unwrap();
174+
175+
// Should include all exports and all index names, sorted and deduped
176+
assert_eq!(pkg.exported_symbols, vec![
177+
"path_to_file",
178+
"penguins",
179+
"penguins_raw"
180+
]);
181+
assert_eq!(pkg.description.name, "penguins");
182+
}
183+
}
184+
185+
#[cfg(test)]
186+
pub(crate) fn temp_palmerpenguin() -> tempfile::TempDir {
187+
let dir = tempfile::tempdir().unwrap();
188+
189+
// Write DESCRIPTION
190+
let description = "\
191+
Package: penguins
192+
Version: 1.0
193+
";
194+
fs::write(dir.path().join("DESCRIPTION"), description).unwrap();
195+
196+
// Write NAMESPACE
197+
let namespace = "\
198+
export(path_to_file)
199+
export(penguins)
200+
";
201+
fs::write(dir.path().join("NAMESPACE"), namespace).unwrap();
202+
203+
// Write INDEX
204+
let index = "\
205+
path_to_file Get file path to 'penguins.csv' and
206+
'penguins_raw.csv' files
207+
penguins Size measurements for adult foraging penguins
208+
near Palmer Station, Antarctica
209+
penguins_raw Penguin size, clutch, and blood isotope data
210+
for foraging adults near Palmer Station,
211+
Antarctica
212+
";
213+
fs::write(dir.path().join("INDEX"), index).unwrap();
214+
215+
dir
216+
}

crates/ark/src/lsp/inputs/package_description.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,17 @@ pub struct Description {
4848
pub fields: Dcf,
4949
}
5050

51+
impl Default for Description {
52+
fn default() -> Self {
53+
Description {
54+
name: String::new(),
55+
version: String::new(),
56+
depends: Vec::new(),
57+
fields: Dcf::default(),
58+
}
59+
}
60+
}
61+
5162
impl Description {
5263
/// Parse a DESCRIPTION file in DCF format
5364
pub fn parse(contents: &str) -> anyhow::Result<Self> {

crates/ark/src/lsp/inputs/package_index.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
use std::path::Path;
99

10+
#[derive(Default, Clone, Debug)]
1011
pub struct Index {
1112
pub names: Vec<String>,
1213
}

0 commit comments

Comments
 (0)