use std::path::{Path, PathBuf};
use anyhow::{Context, anyhow};
pub(crate) fn ensure_semantic_data(
explicit_semantic: Option<&Path>,
analysis_path: Option<&Path>,
_socket: Option<&Path>,
) -> anyhow::Result<descendit::SemanticOverlay> {
if let Some(path) = explicit_semantic {
return load_semantic_overlay(path);
}
let path = analysis_path.ok_or_else(|| anyhow!("no analysis path provided"))?;
#[cfg(feature = "semantic")]
{
catch_ra_panic(|| run_ra_analysis(path, _socket))
}
#[cfg(not(feature = "semantic"))]
{
let _ = path;
anyhow::bail!(
"semantic analysis is required but the `semantic` feature is not enabled. \
Rebuild with `cargo install descendit` (default features)."
);
}
}
pub(crate) fn resolve_semantic(
explicit: Option<&Path>,
anchor: Option<&Path>,
) -> anyhow::Result<Option<descendit::SemanticOverlay>> {
if let Some(path) = explicit {
return load_semantic_overlay(path).map(Some);
}
let Some(anchor) = anchor else {
return Ok(None);
};
resolve_near_anchor(anchor)
}
fn resolve_near_anchor(anchor: &Path) -> anyhow::Result<Option<descendit::SemanticOverlay>> {
let start = if anchor.is_file() {
match anchor.parent() {
Some(parent) => parent,
None => return Ok(None),
}
} else {
anchor
};
let mut dir = start;
let mut depth = 0u32;
loop {
if depth >= 32 {
return Ok(None);
}
let candidate = dir.join("target/descendit/semantic.json");
if candidate.is_file() {
return match load_semantic_overlay(&candidate) {
Ok(overlay) => Ok(Some(overlay)),
Err(e) => {
eprintln!("warning: failed to load semantic data: {e:#}");
Ok(None)
}
};
}
dir = match dir.parent() {
Some(parent) => parent,
None => return Ok(None),
};
depth += 1;
}
}
fn load_semantic_overlay(path: &Path) -> anyhow::Result<descendit::SemanticOverlay> {
descendit::SemanticOverlay::load(path)
.map_err(anyhow::Error::msg)
.with_context(|| format!("failed to load semantic data from {}", path.display()))
}
#[cfg(feature = "semantic")]
fn catch_ra_panic<F, T>(f: F) -> anyhow::Result<T>
where
F: FnOnce() -> anyhow::Result<T> + std::panic::UnwindSafe,
{
match std::panic::catch_unwind(f) {
Ok(result) => result,
Err(payload) => {
let msg = if let Some(s) = payload.downcast_ref::<&str>() {
s.to_string()
} else if let Some(s) = payload.downcast_ref::<String>() {
s.clone()
} else {
"unknown panic".to_string()
};
Err(anyhow!("rust-analyzer panicked: {msg}"))
}
}
}
#[cfg(feature = "semantic")]
fn run_ra_analysis(
analysis_path: &Path,
socket: Option<&Path>,
) -> anyhow::Result<descendit::SemanticOverlay> {
let manifest = find_nearest_manifest(manifest_search_start(analysis_path))
.ok_or_else(|| anyhow!("could not find Cargo.toml near {}", analysis_path.display()))?;
let manifest_dir = manifest
.parent()
.ok_or_else(|| anyhow!("Cargo.toml has no parent directory"))?;
#[cfg(unix)]
if let Some(socket_path) = socket {
let ra_data = crate::client::analyze(socket_path, manifest_dir)
.context("server-backed semantic analysis failed")?;
let json = serde_json::to_string(&ra_data)?;
let data: descendit::SemanticData = serde_json::from_str(&json)?;
return Ok(descendit::SemanticOverlay::from_data(&data));
}
#[cfg(not(unix))]
if socket.is_some() {
anyhow::bail!("socket-based analysis is only supported on Unix platforms");
}
let json = descendit_ra::analyze_to_json(manifest_dir).with_context(|| {
format!(
"rust-analyzer semantic analysis failed for {}.",
manifest_dir.display()
)
})?;
let data: descendit::SemanticData =
serde_json::from_str(&json).context("failed to parse RA semantic output")?;
Ok(descendit::SemanticOverlay::from_data(&data))
}
#[cfg(feature = "semantic")]
fn manifest_search_start(path: &Path) -> &Path {
if path.is_dir() {
path
} else {
path.parent().unwrap_or(path)
}
}
pub(crate) fn find_nearest_manifest(start: &Path) -> Option<PathBuf> {
let mut dir = start;
for _ in 0..32 {
let candidate = dir.join("Cargo.toml");
if candidate.is_file() {
return Some(candidate);
}
dir = dir.parent()?;
}
None
}
#[cfg(feature = "semantic")]
pub(crate) fn run_ra_analysis_batch(
paths: &[PathBuf],
socket: Option<&Path>,
) -> anyhow::Result<Vec<(PathBuf, descendit_ra::SemanticData)>> {
if paths.is_empty() {
return Ok(Vec::new());
}
#[cfg(unix)]
if let Some(socket_path) = socket {
return paths
.iter()
.map(|path| {
let manifest = find_nearest_manifest(manifest_search_start(path))
.ok_or_else(|| anyhow!("could not find Cargo.toml near {}", path.display()))?;
let manifest_dir = manifest
.parent()
.ok_or_else(|| anyhow!("Cargo.toml has no parent directory"))?;
let data =
crate::client::analyze(socket_path, manifest_dir).with_context(|| {
format!(
"server-backed semantic analysis failed for {}",
path.display()
)
})?;
Ok((path.clone(), data))
})
.collect();
}
#[cfg(not(unix))]
if socket.is_some() {
anyhow::bail!("socket-based analysis is only supported on Unix platforms");
}
let first = &paths[0];
let first_manifest = find_nearest_manifest(manifest_search_start(first))
.ok_or_else(|| anyhow!("could not find Cargo.toml near {}", first.display()))?;
let first_manifest_dir = first_manifest
.parent()
.ok_or_else(|| anyhow!("Cargo.toml has no parent directory"))?;
let mut session = descendit_ra::RaSession::load(first_manifest_dir)
.context("failed to load RA workspace session")?;
paths
.iter()
.map(|path| {
let manifest = find_nearest_manifest(manifest_search_start(path))
.ok_or_else(|| anyhow!("could not find Cargo.toml near {}", path.display()))?;
let manifest_dir = manifest
.parent()
.ok_or_else(|| anyhow!("Cargo.toml has no parent directory"))?;
let data = session
.extract_for_subcrate(manifest_dir)
.with_context(|| format!("semantic extraction failed for {}", path.display()))?;
Ok((path.clone(), data))
})
.collect()
}
#[cfg(not(feature = "semantic"))]
pub(crate) fn run_ra_analysis_batch(
_paths: &[PathBuf],
_socket: Option<&Path>,
) -> anyhow::Result<Vec<(PathBuf, descendit::SemanticData)>> {
anyhow::bail!(
"semantic analysis is required but the `semantic` feature is not enabled. \
Rebuild with `cargo install descendit` (default features)."
);
}
#[cfg(test)]
#[allow(clippy::expect_used)]
mod tests {
use super::*;
use tempfile::tempdir;
#[test]
fn find_nearest_manifest_walks_up() {
let temp = tempdir().expect("tempdir");
let root = temp.path();
std::fs::write(root.join("Cargo.toml"), "[package]\nname = \"test\"\n")
.expect("write manifest");
std::fs::create_dir_all(root.join("src/deep")).expect("create nested dirs");
let found = find_nearest_manifest(&root.join("src/deep"));
assert_eq!(found, Some(root.join("Cargo.toml")),);
}
#[test]
fn find_nearest_manifest_returns_none_at_root() {
let temp = tempdir().expect("tempdir");
let empty = temp.path().join("empty");
std::fs::create_dir_all(&empty).expect("create empty dir");
assert!(find_nearest_manifest(&empty).is_none());
}
}