use std::path::{Path, PathBuf};
use ra_ap_cfg::{self as cfg};
use ra_ap_hir::{self as hir, AsAssocItem as _, HasAttrs as _, db::HirDatabase};
use ra_ap_ide::{self as ide};
use ra_ap_ide_db::{self as ide_db};
use ra_ap_load_cargo::{self as load_cargo};
use ra_ap_paths::{self as paths};
use ra_ap_project_model::{self as project_model};
use ra_ap_syntax::{self as syntax, AstNode as _, ast};
use ra_ap_vfs::{self as vfs};
use crate::{
item::{ItemCfgAttr, ItemTestAttr},
options::{GeneralOptions, ProjectOptions},
};
pub struct LoadOptions {
pub cfg_test: bool,
pub sysroot: bool,
}
pub fn load_workspace(
general_options: &GeneralOptions,
project_options: &ProjectOptions,
load_options: &LoadOptions,
) -> anyhow::Result<(hir::Crate, ide::AnalysisHost, vfs::Vfs, ide::Edition)> {
let project_path = project_options.manifest_path.as_path().canonicalize()?;
let project_path = dunce::simplified(&project_path).to_path_buf();
let cargo_config = cargo_config(project_options, load_options);
let load_config = load_config();
let progress = |string| {
tracing::trace!("Progress: {}", string);
};
let mut project_workspace = load_project_workspace(&project_path, &cargo_config, &progress)?;
let (package, target) = select_package_and_target(&project_workspace, project_options)?;
if general_options.verbose {
eprintln!();
eprintln!("crate");
eprintln!("└── package: {}", package.name);
eprintln!(" └── target: {}", target.name);
eprintln!();
}
let edition = package.edition;
if load_config.load_out_dirs_from_check {
let build_scripts = project_workspace.run_build_scripts(&cargo_config, &progress)?;
project_workspace.set_build_scripts(build_scripts)
}
let (db, vfs, _proc_macro_client) =
ra_ap_load_cargo::load_workspace(project_workspace, &cargo_config.extra_env, &load_config)?;
let host = ide::AnalysisHost::with_database(db);
let krate = find_crate(host.raw_database(), &vfs, &target)?;
Ok((krate, host, vfs, edition))
}
pub fn cargo_config(
project_options: &ProjectOptions,
load_options: &LoadOptions,
) -> project_model::CargoConfig {
let all_targets = false;
let cfg_overrides = match load_options.cfg_test {
true => project_model::CfgOverrides {
global: cfg::CfgDiff::new(
vec![cfg::CfgAtom::Flag(hir::Symbol::intern("test"))],
Vec::new(),
),
selective: Default::default(),
},
false => project_model::CfgOverrides {
global: cfg::CfgDiff::new(
Vec::new(),
vec![cfg::CfgAtom::Flag(hir::Symbol::intern("test"))],
),
selective: Default::default(),
},
};
let extra_args = vec![];
let extra_env = ide_db::FxHashMap::default();
let extra_includes = vec![];
let features = if project_options.all_features {
project_model::CargoFeatures::All
} else {
project_model::CargoFeatures::Selected {
features: project_options.features.clone(),
no_default_features: project_options.no_default_features,
}
};
let invocation_strategy = project_model::InvocationStrategy::PerWorkspace;
let no_deps: bool = false;
let run_build_script_command = None;
let rustc_source = None;
let set_test = load_options.cfg_test;
let sysroot = if load_options.sysroot {
Some(project_model::RustLibSource::Discover)
} else {
None
};
let sysroot_src = None;
let target = project_options.target.clone();
let wrap_rustc_in_build_scripts = true;
project_model::CargoConfig {
all_targets,
cfg_overrides,
extra_args,
extra_env,
extra_includes,
features,
invocation_strategy,
no_deps,
run_build_script_command,
rustc_source,
set_test,
sysroot_src,
sysroot,
target_dir_config: project_model::TargetDirectoryConfig::default(),
metadata_extra_args: vec![],
target,
wrap_rustc_in_build_scripts,
}
}
pub fn load_config() -> load_cargo::LoadCargoConfig {
let load_out_dirs_from_check = true;
let prefill_caches = false;
let with_proc_macro_server = load_cargo::ProcMacroServerChoice::Sysroot;
load_cargo::LoadCargoConfig {
load_out_dirs_from_check,
prefill_caches,
with_proc_macro_server,
num_worker_threads: 1,
proc_macro_processes: 1,
}
}
pub fn load_project_workspace(
project_path: &Path,
cargo_config: &project_model::CargoConfig,
progress: &(dyn Fn(String) + Sync),
) -> anyhow::Result<project_model::ProjectWorkspace> {
let path_buf = std::env::current_dir()?.join(project_path);
let utf8_path_buf = paths::Utf8PathBuf::from_path_buf(path_buf).unwrap();
let root = paths::AbsPathBuf::assert(utf8_path_buf);
let root = project_model::ProjectManifest::discover_single(root.as_path())?;
project_model::ProjectWorkspace::load(root, cargo_config, &progress)
}
pub fn select_package_and_target(
project_workspace: &project_model::ProjectWorkspace,
options: &ProjectOptions,
) -> anyhow::Result<(project_model::PackageData, project_model::TargetData)> {
let cargo_workspace = match project_workspace.kind {
project_model::ProjectWorkspaceKind::Cargo { ref cargo, .. } => Ok(cargo),
project_model::ProjectWorkspaceKind::Json { .. } => {
Err(anyhow::anyhow!("Unexpected JSON workspace"))
}
project_model::ProjectWorkspaceKind::DetachedFile { .. } => {
Err(anyhow::anyhow!("Unexpected detached files"))
}
}?;
let package_idx = select_package(cargo_workspace, options)?;
let package = cargo_workspace[package_idx].clone();
tracing::debug!("Selected package: {:#?}", package.name);
let target_idx = select_target(cargo_workspace, package_idx, options)?;
let target = cargo_workspace[target_idx].clone();
tracing::debug!("Selected target: {:#?}", target.name);
Ok((package, target))
}
pub fn select_package(
workspace: &project_model::CargoWorkspace,
options: &ProjectOptions,
) -> anyhow::Result<project_model::Package> {
let packages: Vec<_> = workspace
.packages()
.filter(|idx| workspace[*idx].is_member)
.collect();
let package_count = packages.len();
if package_count < 1 {
anyhow::bail!("no packages found");
}
let package_list_items: Vec<_> = packages
.iter()
.map(|package_idx| {
let package = &workspace[*package_idx];
format!("- {}", package.name)
})
.collect();
let package_list = package_list_items.join("\n");
if let Some(package_name) = &options.package {
let package_idx = packages.into_iter().find(|package_idx| {
let package = &workspace[*package_idx];
package.name == *package_name
});
return package_idx.ok_or_else(|| {
anyhow::anyhow!(
indoc::indoc! {
"No package found with name {:?}.
Packages present in workspace:
{}
"
},
package_name,
package_list,
)
});
}
if package_count == 1 {
return Ok(packages[0]);
}
Err(anyhow::anyhow!(
indoc::indoc! {
"Multiple packages present in workspace,
please explicitly select one via --package flag.
Packages present in workspace:
{}
"
},
package_list
))
}
pub fn select_target(
workspace: &project_model::CargoWorkspace,
package_idx: project_model::Package,
options: &ProjectOptions,
) -> anyhow::Result<project_model::Target> {
let package = &workspace[package_idx];
let targets: Vec<_> = package
.targets
.iter()
.cloned()
.filter(|target_idx| {
let target = &workspace[*target_idx];
match target.kind {
project_model::TargetKind::Bin => true,
project_model::TargetKind::Lib { .. } => true,
project_model::TargetKind::Example => false,
project_model::TargetKind::Test => false,
project_model::TargetKind::Bench => false,
project_model::TargetKind::Other => false,
project_model::TargetKind::BuildScript => false,
}
})
.collect();
let target_count = targets.len();
if target_count < 1 {
anyhow::bail!("no targets found");
}
let target_list_items: Vec<_> = targets
.iter()
.map(|target_idx| {
let target = &workspace[*target_idx];
match target.kind {
project_model::TargetKind::Bin => {
format!("- {} (--bin {})", target.name, target.name)
}
project_model::TargetKind::Lib { .. } => format!("- {} (--lib)", target.name),
project_model::TargetKind::Example => unreachable!(),
project_model::TargetKind::Test => unreachable!(),
project_model::TargetKind::Bench => unreachable!(),
project_model::TargetKind::Other => unreachable!(),
project_model::TargetKind::BuildScript => unreachable!(),
}
})
.collect();
let target_list = target_list_items.join("\n");
if options.lib {
let target = targets.into_iter().find(|target_idx| {
let target = &workspace[*target_idx];
matches!(target.kind, project_model::TargetKind::Lib { .. })
});
return target.ok_or_else(|| {
anyhow::anyhow!(
indoc::indoc! {
"No library target found.
Targets present in package:
{}
"
},
target_list,
)
});
}
if let Some(bin_name) = &options.bin {
let target = targets.into_iter().find(|target_idx| {
let target = &workspace[*target_idx];
(target.kind == project_model::TargetKind::Bin) && (target.name == bin_name[..])
});
return target.ok_or_else(|| {
anyhow::anyhow!(
indoc::indoc! {
"No binary target found with name {:?}.
Targets present in package:
{}
"
},
bin_name,
target_list,
)
});
}
if target_count == 1 {
return Ok(targets[0]);
}
Err(anyhow::anyhow!(
indoc::indoc! {
"Multiple targets present in package,
please explicitly select one via --lib or --bin flag.
Targets present in package:
{}
"
},
target_list
))
}
pub fn find_crate(
db: &ide::RootDatabase,
vfs: &vfs::Vfs,
target: &project_model::TargetData,
) -> anyhow::Result<hir::Crate> {
let crates = hir::Crate::all(db);
let target_root_path = target.root.as_path();
let krate = crates.into_iter().find(|krate| {
let vfs_path = vfs.file_path(krate.root_file(db));
let crate_root_path = vfs_path.as_path().unwrap();
crate_root_path == target_root_path
});
krate.ok_or_else(|| anyhow::anyhow!("Crate not found"))
}
pub(crate) fn crate_name(krate: hir::Crate, db: &dyn HirDatabase) -> String {
let display_name = krate.display_name(db).unwrap().to_string();
display_name.replace('-', "_")
}
pub(crate) fn krate(module_def_hir: hir::ModuleDef, db: &dyn HirDatabase) -> Option<hir::Crate> {
module(module_def_hir, db).map(|module| module.krate(db))
}
pub(crate) fn module(module_def_hir: hir::ModuleDef, db: &dyn HirDatabase) -> Option<hir::Module> {
match module_def_hir {
hir::ModuleDef::Module(module) => Some(module),
module_def_hir => module_def_hir.module(db),
}
}
pub(crate) fn display_name(
module_def_hir: hir::ModuleDef,
db: &dyn HirDatabase,
edition: ide::Edition,
) -> String {
match module_def_hir {
hir::ModuleDef::Module(module_hir) => {
if module_hir.is_crate_root(db) {
crate_name(module_hir.krate(db), db)
} else {
module_hir
.name(db)
.map(|name| name.display(db, edition).to_string())
.expect("name")
}
}
hir::ModuleDef::Const(const_hir) => {
if let Some(name) = const_hir.name(db) {
name.display(db, edition).to_string()
} else {
"_".to_owned()
}
}
module_def_hir => module_def_hir
.name(db)
.map(|name| name.display(db, edition).to_string())
.expect("name"),
}
}
pub(crate) fn name(
module_def_hir: hir::ModuleDef,
db: &dyn HirDatabase,
edition: ide::Edition,
) -> Option<String> {
module_def_hir
.name(db)
.map(|name| name.display(db, edition).to_string())
}
pub(crate) fn display_path(
module_def_hir: hir::ModuleDef,
db: &dyn HirDatabase,
edition: ide::Edition,
) -> String {
path(module_def_hir, db, edition).unwrap_or_else(|| "<anonymous>".to_owned())
}
pub(crate) fn path(
module_def_hir: hir::ModuleDef,
db: &dyn HirDatabase,
edition: ide::Edition,
) -> Option<String> {
let mut path = String::new();
let krate = krate(module_def_hir, db);
if let Some(crate_name) = krate.map(|krate| crate_name(krate, db)) {
path.push_str(&crate_name);
}
let relative_path = match module_def_hir {
hir::ModuleDef::Function(function_hir) => {
if let Some(assoc_item_hir) = function_hir.as_assoc_item(db) {
assoc_item_path(assoc_item_hir, db, edition)
} else {
hir::ModuleDef::Function(function_hir).canonical_path(db, edition)
}
}
hir::ModuleDef::Const(const_hir) => {
if let Some(assoc_item_hir) = const_hir.as_assoc_item(db) {
assoc_item_path(assoc_item_hir, db, edition)
} else {
hir::ModuleDef::Const(const_hir).canonical_path(db, edition)
}
}
hir::ModuleDef::TypeAlias(type_alias_hir) => {
if let Some(assoc_item_hir) = type_alias_hir.as_assoc_item(db) {
assoc_item_path(assoc_item_hir, db, edition)
} else {
hir::ModuleDef::TypeAlias(type_alias_hir).canonical_path(db, edition)
}
}
hir::ModuleDef::BuiltinType(builtin_type_hir) => {
Some(builtin_type_hir.name().display(db, edition).to_string())
}
module_def_hir => module_def_hir.canonical_path(db, edition),
};
if let Some(relative_path) = relative_path {
if !path.is_empty() {
path.push_str("::");
}
path.push_str(&relative_path);
}
if path.is_empty() { None } else { Some(path) }
}
fn assoc_item_path(
assoc_item_hir: hir::AssocItem,
db: &dyn HirDatabase,
edition: ide::Edition,
) -> Option<String> {
let name = match assoc_item_hir {
hir::AssocItem::Function(function_hir) => hir::ModuleDef::Function(function_hir)
.name(db)
.map(|name| name.display(db, edition).to_string()),
hir::AssocItem::Const(const_hir) => hir::ModuleDef::Const(const_hir)
.name(db)
.map(|name| name.display(db, edition).to_string()),
hir::AssocItem::TypeAlias(type_alias_hir) => hir::ModuleDef::TypeAlias(type_alias_hir)
.name(db)
.map(|name| name.display(db, edition).to_string()),
};
let name = name?;
let container_path = match assoc_item_hir.container(db) {
hir::AssocItemContainer::Trait(trait_hir) => {
hir::ModuleDef::Trait(trait_hir).canonical_path(db, edition)
}
hir::AssocItemContainer::Impl(impl_hir) => impl_hir
.self_ty(db)
.as_adt()
.and_then(|adt_hir| hir::ModuleDef::Adt(adt_hir).canonical_path(db, edition)),
};
let container_path = container_path?;
Some(format!("{container_path}::{name}"))
}
pub(crate) fn parse_ast<N: syntax::AstNode>(text: &str) -> N {
let parse = syntax::SourceFile::parse(text, ide::Edition::CURRENT);
let node = match parse.tree().syntax().descendants().find_map(N::cast) {
Some(it) => it,
None => {
let node = std::any::type_name::<N>();
panic!("Failed to make ast node `{node}` from text {text}")
}
};
let node = node.clone_subtree();
assert_eq!(node.syntax().text_range().start(), 0.into());
node
}
pub(crate) fn parse_use_tree(path_expr: &str) -> ast::UseTree {
parse_ast(&format!("use {path_expr};"))
}
pub(crate) fn parse_path_expr(path_expr: &str) -> ast::Path {
parse_ast(path_expr)
}
pub(crate) fn use_tree_matches_item_path(use_tree: &ast::UseTree, item_path: &str) -> bool {
if item_path.is_empty() {
return false;
}
let node_path: ast::Path = parse_path_expr(item_path);
use_tree_matches_path(use_tree, &node_path)
}
pub(crate) fn use_tree_matches_path(use_tree: &ast::UseTree, path: &ast::Path) -> bool {
let mut path_segments_iter = path.segments();
if let Some(use_tree_path) = use_tree.path() {
for use_tree_segment in use_tree_path.segments() {
#[allow(clippy::collapsible_match)]
match path_segments_iter.next() {
Some(path_segment) => {
if use_tree_segment.syntax().text() == path_segment.syntax().text() {
continue;
} else {
return false;
}
}
None => {
return false;
}
}
}
}
let path_segments: Vec<_> = path_segments_iter.collect();
if path_segments.is_empty() {
return use_tree.is_simple_path() || tree_contains_self(use_tree);
}
if use_tree.star_token().is_some() {
return path_segments.len() == 1;
}
let path_suffix = ast::make::path_from_segments(path_segments, false);
use_tree
.use_tree_list()
.into_iter()
.flat_map(|list| list.use_trees())
.any(|use_tree| use_tree_matches_path(&use_tree, &path_suffix))
}
fn path_is_self(path: &ast::Path) -> bool {
path.segment().and_then(|seg| seg.self_token()).is_some() && path.qualifier().is_none()
}
fn tree_is_self(tree: &ast::UseTree) -> bool {
tree.path().as_ref().map(path_is_self).unwrap_or(false)
}
fn tree_contains_self(tree: &ast::UseTree) -> bool {
tree.use_tree_list()
.map(|tree_list| tree_list.use_trees().any(|tree| tree_is_self(&tree)))
.unwrap_or(false)
}
pub(crate) fn has_test_cfg(hir: hir::ModuleDef, db: &dyn HirDatabase) -> bool {
let Some(attrs) = hir.attrs(db) else {
return false;
};
let test_key = hir::Symbol::intern("test");
let Some(cfg_expr) = attrs.cfgs(db) else {
return false;
};
cfg_expr
.fold(&|cfg| {
use ra_ap_cfg::CfgAtom;
match cfg {
CfgAtom::Flag(symbol) => *symbol == test_key,
CfgAtom::KeyValue { .. } => false,
}
})
.unwrap_or_default()
}
pub(crate) fn is_test_function(function: hir::Function, db: &dyn HirDatabase) -> bool {
function.attrs(db).is_test()
}
pub fn cfgs(hir: hir::ModuleDef, db: &dyn HirDatabase) -> Vec<cfg::CfgExpr> {
let cfg = match cfg(hir, db) {
Some(cfg) => cfg,
None => return vec![],
};
match cfg {
cfg::CfgExpr::Invalid => vec![],
cfg @ cfg::CfgExpr::Atom(_) => vec![cfg],
cfg::CfgExpr::All(cfgs) => cfgs.to_vec(),
cfg @ cfg::CfgExpr::Any(_) => vec![cfg],
cfg @ cfg::CfgExpr::Not(_) => vec![cfg],
}
}
pub fn cfg(hir: hir::ModuleDef, db: &dyn HirDatabase) -> Option<cfg::CfgExpr> {
match hir {
hir::ModuleDef::Module(r#mod) => r#mod.attrs(db).cfgs(db).cloned(),
hir::ModuleDef::Function(r#fn) => r#fn.attrs(db).cfgs(db).cloned(),
hir::ModuleDef::Adt(adt) => adt.attrs(db).cfgs(db).cloned(),
hir::ModuleDef::EnumVariant(r#variant) => r#variant.attrs(db).cfgs(db).cloned(),
hir::ModuleDef::Const(r#const) => r#const.attrs(db).cfgs(db).cloned(),
hir::ModuleDef::Static(r#static) => r#static.attrs(db).cfgs(db).cloned(),
hir::ModuleDef::Trait(r#trait) => r#trait.attrs(db).cfgs(db).cloned(),
hir::ModuleDef::TypeAlias(type_alias) => type_alias.attrs(db).cfgs(db).cloned(),
hir::ModuleDef::BuiltinType(_builtin_type) => None,
hir::ModuleDef::Macro(_) => None,
}
}
pub fn cfg_attrs(module_def_hir: hir::ModuleDef, db: &dyn HirDatabase) -> Vec<ItemCfgAttr> {
cfgs(module_def_hir, db)
.iter()
.filter_map(ItemCfgAttr::new)
.collect()
}
pub fn test_attr(module_def_hir: hir::ModuleDef, db: &dyn HirDatabase) -> Option<ItemTestAttr> {
let function = match module_def_hir {
hir::ModuleDef::Function(function) => function,
_ => return None,
};
if is_test_function(function, db) {
Some(ItemTestAttr)
} else {
None
}
}
pub fn module_file(module: hir::Module, db: &dyn HirDatabase, vfs: &vfs::Vfs) -> Option<PathBuf> {
let module_source = module.definition_source(db);
let is_file_module: bool = match &module_source.value {
hir::ModuleSource::SourceFile(_) => true,
hir::ModuleSource::Module(_) => false,
hir::ModuleSource::BlockExpr(_) => false,
};
if !is_file_module {
return None;
}
let file_id = module_source.file_id.original_file(db);
let vfs_path = vfs.file_path(file_id.file_id(db));
let abs_path = vfs_path.as_path().expect("Could not convert to path");
let path: &Path = abs_path.as_ref();
let file_extension = path.extension().and_then(|ext| ext.to_str());
if file_extension != Some("rs") {
return None;
}
Some(path.to_owned())
}
pub fn moduledef_is_crate(module_def_hir: hir::ModuleDef, db: &dyn HirDatabase) -> bool {
let hir::ModuleDef::Module(module) = module_def_hir else {
return false;
};
module.is_crate_root(db)
}