use std::path::{Path, PathBuf};
use ra_ap_ide::{Analysis, AnalysisHost, FilePosition, FindAllRefsConfig, RaFixtureConfig};
use ra_ap_load_cargo::{LoadCargoConfig, ProcMacroServerChoice};
use ra_ap_project_model::CargoConfig;
use ra_ap_vfs::{AbsPathBuf, Vfs, VfsPath};
pub use ra_ap_ide::FileId;
pub use ra_ap_project_model::CargoFeatures;
#[derive(Debug)]
pub enum DeepError {
Load(Box<dyn std::error::Error + Send + Sync>),
Cancelled(String),
UnresolvedSymbol(String),
InvalidPath(PathBuf),
}
impl std::fmt::Display for DeepError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Load(err) => {
write!(f, "failed to load workspace for deep analysis: {err}")?;
let mut source = err.source();
while let Some(cause) = source {
write!(f, ": {cause}")?;
source = cause.source();
}
Ok(())
}
Self::Cancelled(msg) => write!(f, "semantic query canceled: {msg}"),
Self::UnresolvedSymbol(position) => write!(
f,
"semantic query at {position} did not resolve to a symbol — not the same as a \
symbol with zero references"
),
Self::InvalidPath(path) => write!(
f,
"path is not absolute UTF-8 and cannot be mapped into the analysis vfs: {}",
path.display()
),
}
}
}
impl std::error::Error for DeepError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Self::Load(err) => Some(err.as_ref()),
Self::Cancelled(_) | Self::UnresolvedSymbol(_) | Self::InvalidPath(_) => None,
}
}
}
pub struct DeepContext {
host: AnalysisHost,
vfs: Vfs,
}
impl DeepContext {
pub fn load(workspace_root: &Path) -> Result<Self, DeepError> {
Self::load_with_features(workspace_root, CargoFeatures::All)
}
pub fn load_with_features(
workspace_root: &Path,
features: CargoFeatures,
) -> Result<Self, DeepError> {
let cargo_config = CargoConfig {
sysroot: Some(ra_ap_project_model::RustLibSource::Discover),
set_test: true,
features,
..CargoConfig::default()
};
let load_config = LoadCargoConfig {
load_out_dirs_from_check: false,
with_proc_macro_server: ProcMacroServerChoice::None,
prefill_caches: false,
num_worker_threads: 1,
proc_macro_processes: 0,
};
let (db, vfs, _proc_macro_server) = ra_ap_load_cargo::load_workspace_at(
workspace_root,
&cargo_config,
&load_config,
&|_progress| {},
)
.map_err(|err| DeepError::Load(err.into()))?;
Ok(Self {
host: AnalysisHost::with_database(db),
vfs,
})
}
pub fn analysis(&self) -> Analysis {
self.host.analysis()
}
pub fn raw_database(&self) -> &ra_ap_ide::RootDatabase {
self.host.raw_database()
}
pub fn file_id(&self, path: &Path) -> Result<Option<FileId>, DeepError> {
let abs_path = validated_utf8_abs(path)?;
let vfs_path = VfsPath::from(abs_path);
Ok(self
.vfs
.file_id(&vfs_path)
.map(|(file_id, _excluded)| file_id))
}
}
pub(crate) fn validated_utf8_abs(path: &Path) -> Result<AbsPathBuf, DeepError> {
if path.is_absolute() && path.to_str().is_some() {
Ok(AbsPathBuf::assert_utf8(path.to_path_buf()))
} else {
Err(DeepError::InvalidPath(path.to_path_buf()))
}
}
fn find_refs(
analysis: &Analysis,
position: FilePosition,
include_tests: bool,
) -> Result<Vec<ra_ap_ide::ReferenceSearchResult>, DeepError> {
let config = FindAllRefsConfig {
search_scope: None,
ra_fixture: RaFixtureConfig::default(),
exclude_imports: false,
exclude_tests: !include_tests,
};
analysis
.find_all_refs(position, &config)
.map_err(|err| DeepError::Cancelled(format!("{err:?}")))?
.ok_or_else(|| DeepError::UnresolvedSymbol(describe_position(position)))
}
pub(crate) fn describe_position(position: FilePosition) -> String {
format!(
"{:?}, byte offset {}",
position.file_id,
u32::from(position.offset)
)
}
pub fn reference_count(
analysis: &Analysis,
position: FilePosition,
include_tests: bool,
) -> Result<usize, DeepError> {
let results = find_refs(analysis, position, include_tests)?;
Ok(results
.iter()
.flat_map(|result| result.references.values())
.map(Vec::len)
.sum())
}
pub fn referencing_files(
analysis: &Analysis,
position: FilePosition,
include_tests: bool,
) -> Result<std::collections::HashSet<FileId>, DeepError> {
let results = find_refs(analysis, position, include_tests)?;
Ok(results
.iter()
.flat_map(|result| result.references.keys().copied())
.collect())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::test_util::TempDir;
#[test]
fn reference_count_distinguishes_used_from_dead_pub_items() {
let dir = TempDir::new("deep-reference-count");
std::fs::write(
dir.join("Cargo.toml"),
r#"
[package]
name = "deep-fixture"
version = "0.1.0"
edition = "2021"
"#,
)
.unwrap();
std::fs::create_dir_all(dir.join("src")).unwrap();
let lib_source = r#"pub fn used() -> i32 {
1
}
pub fn dead() -> i32 {
2
}
pub fn caller() -> i32 {
used()
}
"#;
std::fs::write(dir.join("src/lib.rs"), lib_source).unwrap();
let ctx = DeepContext::load(&dir).expect("deep tier should load a trivial crate");
let analysis = ctx.analysis();
let file_id = ctx
.file_id(&dir.join("src/lib.rs"))
.expect("path should be valid for the vfs")
.expect("lib.rs should be indexed by the vfs");
let used_offset = lib_source.find("used").unwrap() as u32;
let dead_offset = lib_source.find("dead").unwrap() as u32;
let used_refs = reference_count(
&analysis,
FilePosition {
file_id,
offset: used_offset.into(),
},
true,
)
.unwrap();
let dead_refs = reference_count(
&analysis,
FilePosition {
file_id,
offset: dead_offset.into(),
},
true,
)
.unwrap();
assert_eq!(
dead_refs, 0,
"`dead` has no callers and must show 0 references"
);
assert_eq!(used_refs, 1, "`used` is called once, from `caller`");
assert!(
matches!(
ctx.file_id(Path::new("src/lib.rs")),
Err(DeepError::InvalidPath(path)) if path == Path::new("src/lib.rs")
),
"a relative path names no vfs file and must be InvalidPath, not a panic"
);
}
#[test]
fn a_relative_path_is_an_invalid_path_error_not_a_panic() {
let err = validated_utf8_abs(Path::new("src/lib.rs")).unwrap_err();
assert!(
matches!(&err, DeepError::InvalidPath(path) if path == Path::new("src/lib.rs")),
"expected InvalidPath carrying the offending path, got: {err:?}"
);
}
#[cfg(unix)]
#[test]
fn a_non_utf8_path_is_an_invalid_path_error_not_a_panic() {
use std::os::unix::ffi::OsStrExt;
let non_utf8 = Path::new(std::ffi::OsStr::from_bytes(b"/tmp/\xff-not-utf8.rs"));
assert!(
non_utf8.is_absolute() && non_utf8.to_str().is_none(),
"fixture must isolate the UTF-8 condition from the absoluteness condition"
);
let err = validated_utf8_abs(non_utf8).unwrap_err();
assert!(
matches!(&err, DeepError::InvalidPath(path) if path == non_utf8),
"expected InvalidPath carrying the offending path, got: {err:?}"
);
}
#[test]
fn reference_count_distinguishes_used_from_dead_pub_items_for_methods_too() {
let dir = TempDir::new("deep-reference-count-methods");
std::fs::write(
dir.join("Cargo.toml"),
r#"
[package]
name = "deep-fixture"
version = "0.1.0"
edition = "2021"
"#,
)
.unwrap();
std::fs::create_dir_all(dir.join("src")).unwrap();
std::fs::write(
dir.join("src/lib.rs"),
r#"pub mod a;
pub mod b;
"#,
)
.unwrap();
let a_source = r#"#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SourceKind {
Authored,
Generated,
}
impl SourceKind {
pub const fn used(self) -> bool {
matches!(self, Self::Authored)
}
pub const fn dead(self) -> bool {
matches!(self, Self::Generated)
}
}
pub struct SourceFile {
pub kind: SourceKind,
}
"#;
std::fs::write(dir.join("src/a.rs"), a_source).unwrap();
std::fs::write(
dir.join("src/b.rs"),
r#"use crate::a::SourceFile;
pub fn count_used(files: &[SourceFile]) -> usize {
files.iter().filter(|file| file.kind.used()).count()
}
"#,
)
.unwrap();
let ctx = DeepContext::load(&dir).expect("deep tier should load a trivial crate");
let analysis = ctx.analysis();
let file_id = ctx
.file_id(&dir.join("src/a.rs"))
.expect("path should be valid for the vfs")
.expect("a.rs should be indexed by the vfs");
let used_offset = a_source.find("used").unwrap() as u32;
let dead_offset = a_source.find("dead").unwrap() as u32;
let used_refs = referencing_files(
&analysis,
FilePosition {
file_id,
offset: used_offset.into(),
},
true,
)
.unwrap();
let dead_refs = referencing_files(
&analysis,
FilePosition {
file_id,
offset: dead_offset.into(),
},
true,
)
.unwrap();
assert_eq!(
dead_refs.len(),
0,
"`dead` has no callers and must show 0 referencing files"
);
assert_eq!(
used_refs.len(),
1,
"`used` is called once from b.rs, through an unannotated closure passed to \
`Iterator::filter` — the shape that undercounted to zero without an explicit sysroot"
);
}
#[test]
fn a_position_not_on_a_symbol_is_an_unresolved_error_not_zero_references() {
let dir = TempDir::new("deep-unresolved-position");
std::fs::write(
dir.join("Cargo.toml"),
r#"
[package]
name = "deep-fixture"
version = "0.1.0"
edition = "2021"
"#,
)
.unwrap();
std::fs::create_dir_all(dir.join("src")).unwrap();
let lib_source = r#"// a comment, not a symbol
pub fn item() -> i32 {
1
}
"#;
std::fs::write(dir.join("src/lib.rs"), lib_source).unwrap();
let ctx = DeepContext::load(&dir).expect("deep tier should load a trivial crate");
let analysis = ctx.analysis();
let file_id = ctx
.file_id(&dir.join("src/lib.rs"))
.expect("path should be valid for the vfs")
.expect("lib.rs should be indexed by the vfs");
let comment_offset = lib_source.find("comment").unwrap() as u32;
let whitespace_offset = lib_source.find(" 1").unwrap() as u32 + 1;
for offset in [comment_offset, whitespace_offset] {
let position = FilePosition {
file_id,
offset: offset.into(),
};
let count_err = reference_count(&analysis, position, true).unwrap_err();
assert!(
matches!(count_err, DeepError::UnresolvedSymbol(_)),
"offset {offset} names no symbol and must be UnresolvedSymbol, got: {count_err:?}"
);
let files_err = referencing_files(&analysis, position, true).unwrap_err();
assert!(
matches!(files_err, DeepError::UnresolvedSymbol(_)),
"offset {offset} names no symbol and must be UnresolvedSymbol, got: {files_err:?}"
);
}
let item_offset = lib_source.find("item").unwrap() as u32;
let refs = reference_count(
&analysis,
FilePosition {
file_id,
offset: item_offset.into(),
},
true,
)
.unwrap();
assert_eq!(refs, 0, "`item` resolves fine and simply has no references");
}
#[test]
fn reference_count_sees_a_call_originating_inside_cfg_test_code() {
let dir = TempDir::new("deep-reference-count-cfg-test");
std::fs::write(
dir.join("Cargo.toml"),
r#"
[package]
name = "deep-fixture"
version = "0.1.0"
edition = "2021"
"#,
)
.unwrap();
std::fs::create_dir_all(dir.join("src")).unwrap();
let lib_source = r#"pub fn helper() -> i32 {
1
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_test() {
helper();
}
}
"#;
std::fs::write(dir.join("src/lib.rs"), lib_source).unwrap();
let ctx = DeepContext::load(&dir).expect("deep tier should load a trivial crate");
let analysis = ctx.analysis();
let file_id = ctx
.file_id(&dir.join("src/lib.rs"))
.expect("path should be valid for the vfs")
.expect("lib.rs should be indexed by the vfs");
let helper_offset = lib_source.find("helper").unwrap() as u32;
let position = FilePosition {
file_id,
offset: helper_offset.into(),
};
let with_tests = reference_count(&analysis, position, true).unwrap();
let without_tests = reference_count(&analysis, position, false).unwrap();
assert_eq!(
with_tests, 1,
"the call inside `#[cfg(test)] mod tests` containing `#[test] fn a_test` must be \
visible when include_tests is true"
);
assert_eq!(
without_tests, 0,
"the same call must not count in production-only mode"
);
}
#[test]
fn deep_error_load_source_preserves_the_underlying_error() {
let err = DeepError::Load(Box::new(std::io::Error::other("boom")));
let source = std::error::Error::source(&err).expect("Load must carry a source");
assert!(source.downcast_ref::<std::io::Error>().is_some());
assert_eq!(
err.to_string(),
"failed to load workspace for deep analysis: boom"
);
assert!(std::error::Error::source(&DeepError::InvalidPath(PathBuf::from("x"))).is_none());
}
}