| //! Loads a Cargo project into a static instance of analysis, without support |
| //! for incorporating changes. |
| // Note, don't remove any public api from this. This API is consumed by external tools |
| // to run rust-analyzer as a library. |
| use std::{any::Any, collections::hash_map::Entry, mem, path::Path, sync}; |
| |
| use crossbeam_channel::{Receiver, unbounded}; |
| use hir_expand::proc_macro::{ |
| ProcMacro, ProcMacroExpander, ProcMacroExpansionError, ProcMacroKind, ProcMacroLoadResult, |
| ProcMacrosBuilder, |
| }; |
| use ide_db::{ |
| ChangeWithProcMacros, FxHashMap, RootDatabase, |
| base_db::{CrateGraphBuilder, Env, ProcMacroLoadingError, SourceRoot, SourceRootId}, |
| prime_caches, |
| }; |
| use itertools::Itertools; |
| use proc_macro_api::{MacroDylib, ProcMacroClient}; |
| use project_model::{CargoConfig, PackageRoot, ProjectManifest, ProjectWorkspace}; |
| use span::Span; |
| use vfs::{ |
| AbsPath, AbsPathBuf, VfsPath, |
| file_set::FileSetConfig, |
| loader::{Handle, LoadingProgress}, |
| }; |
| |
| #[derive(Debug)] |
| pub struct LoadCargoConfig { |
| pub load_out_dirs_from_check: bool, |
| pub with_proc_macro_server: ProcMacroServerChoice, |
| pub prefill_caches: bool, |
| } |
| |
| #[derive(Debug, Clone, PartialEq, Eq)] |
| pub enum ProcMacroServerChoice { |
| Sysroot, |
| Explicit(AbsPathBuf), |
| None, |
| } |
| |
| pub fn load_workspace_at( |
| root: &Path, |
| cargo_config: &CargoConfig, |
| load_config: &LoadCargoConfig, |
| progress: &(dyn Fn(String) + Sync), |
| ) -> anyhow::Result<(RootDatabase, vfs::Vfs, Option<ProcMacroClient>)> { |
| let root = AbsPathBuf::assert_utf8(std::env::current_dir()?.join(root)); |
| let root = ProjectManifest::discover_single(&root)?; |
| let manifest_path = root.manifest_path().clone(); |
| let mut workspace = ProjectWorkspace::load(root, cargo_config, progress)?; |
| |
| if load_config.load_out_dirs_from_check { |
| let build_scripts = workspace.run_build_scripts(cargo_config, progress)?; |
| if let Some(error) = build_scripts.error() { |
| tracing::debug!( |
| "Errors occurred while running build scripts for {}: {}", |
| manifest_path, |
| error |
| ); |
| } |
| workspace.set_build_scripts(build_scripts) |
| } |
| |
| load_workspace(workspace, &cargo_config.extra_env, load_config) |
| } |
| |
| pub fn load_workspace( |
| ws: ProjectWorkspace, |
| extra_env: &FxHashMap<String, Option<String>>, |
| load_config: &LoadCargoConfig, |
| ) -> anyhow::Result<(RootDatabase, vfs::Vfs, Option<ProcMacroClient>)> { |
| let lru_cap = std::env::var("RA_LRU_CAP").ok().and_then(|it| it.parse::<u16>().ok()); |
| let mut db = RootDatabase::new(lru_cap); |
| |
| let (vfs, proc_macro_server) = load_workspace_into_db(ws, extra_env, load_config, &mut db)?; |
| |
| Ok((db, vfs, proc_macro_server)) |
| } |
| |
| // This variant of `load_workspace` allows deferring the loading of rust-analyzer |
| // into an existing database, which is useful in certain third-party scenarios, |
| // now that `salsa` supports extending foreign databases (e.g. `RootDatabase`). |
| pub fn load_workspace_into_db( |
| ws: ProjectWorkspace, |
| extra_env: &FxHashMap<String, Option<String>>, |
| load_config: &LoadCargoConfig, |
| db: &mut RootDatabase, |
| ) -> anyhow::Result<(vfs::Vfs, Option<ProcMacroClient>)> { |
| let (sender, receiver) = unbounded(); |
| let mut vfs = vfs::Vfs::default(); |
| let mut loader = { |
| let loader = vfs_notify::NotifyHandle::spawn(sender); |
| Box::new(loader) |
| }; |
| |
| tracing::debug!(?load_config, "LoadCargoConfig"); |
| let proc_macro_server = match &load_config.with_proc_macro_server { |
| ProcMacroServerChoice::Sysroot => ws.find_sysroot_proc_macro_srv().map(|it| { |
| it.and_then(|it| ProcMacroClient::spawn(&it, extra_env).map_err(Into::into)).map_err( |
| |e| ProcMacroLoadingError::ProcMacroSrvError(e.to_string().into_boxed_str()), |
| ) |
| }), |
| ProcMacroServerChoice::Explicit(path) => { |
| Some(ProcMacroClient::spawn(path, extra_env).map_err(|e| { |
| ProcMacroLoadingError::ProcMacroSrvError(e.to_string().into_boxed_str()) |
| })) |
| } |
| ProcMacroServerChoice::None => Some(Err(ProcMacroLoadingError::Disabled)), |
| }; |
| match &proc_macro_server { |
| Some(Ok(server)) => { |
| tracing::info!(manifest=%ws.manifest_or_root(), path=%server.server_path(), "Proc-macro server started") |
| } |
| Some(Err(e)) => { |
| tracing::info!(manifest=%ws.manifest_or_root(), %e, "Failed to start proc-macro server") |
| } |
| None => { |
| tracing::info!(manifest=%ws.manifest_or_root(), "No proc-macro server started") |
| } |
| } |
| |
| let (crate_graph, proc_macros) = ws.to_crate_graph( |
| &mut |path: &AbsPath| { |
| let contents = loader.load_sync(path); |
| let path = vfs::VfsPath::from(path.to_path_buf()); |
| vfs.set_file_contents(path.clone(), contents); |
| vfs.file_id(&path).and_then(|(file_id, excluded)| { |
| (excluded == vfs::FileExcluded::No).then_some(file_id) |
| }) |
| }, |
| extra_env, |
| ); |
| let proc_macros = { |
| let proc_macro_server = match &proc_macro_server { |
| Some(Ok(it)) => Ok(it), |
| Some(Err(e)) => { |
| Err(ProcMacroLoadingError::ProcMacroSrvError(e.to_string().into_boxed_str())) |
| } |
| None => Err(ProcMacroLoadingError::ProcMacroSrvError( |
| "proc-macro-srv is not running, workspace is missing a sysroot".into(), |
| )), |
| }; |
| proc_macros |
| .into_iter() |
| .map(|(crate_id, path)| { |
| ( |
| crate_id, |
| path.map_or_else(Err, |(_, path)| { |
| proc_macro_server.as_ref().map_err(Clone::clone).and_then( |
| |proc_macro_server| load_proc_macro(proc_macro_server, &path, &[]), |
| ) |
| }), |
| ) |
| }) |
| .collect() |
| }; |
| |
| let project_folders = ProjectFolders::new(std::slice::from_ref(&ws), &[], None); |
| loader.set_config(vfs::loader::Config { |
| load: project_folders.load, |
| watch: vec![], |
| version: 0, |
| }); |
| |
| load_crate_graph_into_db( |
| crate_graph, |
| proc_macros, |
| project_folders.source_root_config, |
| &mut vfs, |
| &receiver, |
| db, |
| ); |
| |
| if load_config.prefill_caches { |
| prime_caches::parallel_prime_caches(db, 1, &|_| ()); |
| } |
| |
| Ok((vfs, proc_macro_server.and_then(Result::ok))) |
| } |
| |
| #[derive(Default)] |
| pub struct ProjectFolders { |
| pub load: Vec<vfs::loader::Entry>, |
| pub watch: Vec<usize>, |
| pub source_root_config: SourceRootConfig, |
| } |
| |
| impl ProjectFolders { |
| pub fn new( |
| workspaces: &[ProjectWorkspace], |
| global_excludes: &[AbsPathBuf], |
| user_config_dir_path: Option<&AbsPath>, |
| ) -> ProjectFolders { |
| let mut res = ProjectFolders::default(); |
| let mut fsc = FileSetConfig::builder(); |
| let mut local_filesets = vec![]; |
| |
| // Dedup source roots |
| // Depending on the project setup, we can have duplicated source roots, or for example in |
| // the case of the rustc workspace, we can end up with two source roots that are almost the |
| // same but not quite, like: |
| // PackageRoot { is_local: false, include: [AbsPathBuf(".../rust/src/tools/miri/cargo-miri")], exclude: [] } |
| // PackageRoot { |
| // is_local: true, |
| // include: [AbsPathBuf(".../rust/src/tools/miri/cargo-miri"), AbsPathBuf(".../rust/build/x86_64-pc-windows-msvc/stage0-tools/x86_64-pc-windows-msvc/release/build/cargo-miri-85801cd3d2d1dae4/out")], |
| // exclude: [AbsPathBuf(".../rust/src/tools/miri/cargo-miri/.git"), AbsPathBuf(".../rust/src/tools/miri/cargo-miri/target")] |
| // } |
| // |
| // The first one comes from the explicit rustc workspace which points to the rustc workspace itself |
| // The second comes from the rustc workspace that we load as the actual project workspace |
| // These `is_local` differing in this kind of way gives us problems, especially when trying to filter diagnostics as we don't report diagnostics for external libraries. |
| // So we need to deduplicate these, usually it would be enough to deduplicate by `include`, but as the rustc example shows here that doesn't work, |
| // so we need to also coalesce the includes if they overlap. |
| |
| let mut roots: Vec<_> = workspaces |
| .iter() |
| .flat_map(|ws| ws.to_roots()) |
| .update(|root| root.include.sort()) |
| .sorted_by(|a, b| a.include.cmp(&b.include)) |
| .collect(); |
| |
| // map that tracks indices of overlapping roots |
| let mut overlap_map = FxHashMap::<_, Vec<_>>::default(); |
| let mut done = false; |
| |
| while !mem::replace(&mut done, true) { |
| // maps include paths to indices of the corresponding root |
| let mut include_to_idx = FxHashMap::default(); |
| // Find and note down the indices of overlapping roots |
| for (idx, root) in roots.iter().enumerate().filter(|(_, it)| !it.include.is_empty()) { |
| for include in &root.include { |
| match include_to_idx.entry(include) { |
| Entry::Occupied(e) => { |
| overlap_map.entry(*e.get()).or_default().push(idx); |
| } |
| Entry::Vacant(e) => { |
| e.insert(idx); |
| } |
| } |
| } |
| } |
| for (k, v) in overlap_map.drain() { |
| done = false; |
| for v in v { |
| let r = mem::replace( |
| &mut roots[v], |
| PackageRoot { is_local: false, include: vec![], exclude: vec![] }, |
| ); |
| roots[k].is_local |= r.is_local; |
| roots[k].include.extend(r.include); |
| roots[k].exclude.extend(r.exclude); |
| } |
| roots[k].include.sort(); |
| roots[k].exclude.sort(); |
| roots[k].include.dedup(); |
| roots[k].exclude.dedup(); |
| } |
| } |
| |
| for root in roots.into_iter().filter(|it| !it.include.is_empty()) { |
| let file_set_roots: Vec<VfsPath> = |
| root.include.iter().cloned().map(VfsPath::from).collect(); |
| |
| let entry = { |
| let mut dirs = vfs::loader::Directories::default(); |
| dirs.extensions.push("rs".into()); |
| dirs.extensions.push("toml".into()); |
| dirs.include.extend(root.include); |
| dirs.exclude.extend(root.exclude); |
| for excl in global_excludes { |
| if dirs |
| .include |
| .iter() |
| .any(|incl| incl.starts_with(excl) || excl.starts_with(incl)) |
| { |
| dirs.exclude.push(excl.clone()); |
| } |
| } |
| |
| vfs::loader::Entry::Directories(dirs) |
| }; |
| |
| if root.is_local { |
| res.watch.push(res.load.len()); |
| } |
| res.load.push(entry); |
| |
| if root.is_local { |
| local_filesets.push(fsc.len() as u64); |
| } |
| fsc.add_file_set(file_set_roots) |
| } |
| |
| for ws in workspaces.iter() { |
| let mut file_set_roots: Vec<VfsPath> = vec![]; |
| let mut entries = vec![]; |
| |
| for buildfile in ws.buildfiles() { |
| file_set_roots.push(VfsPath::from(buildfile.to_owned())); |
| entries.push(buildfile.to_owned()); |
| } |
| |
| if !file_set_roots.is_empty() { |
| let entry = vfs::loader::Entry::Files(entries); |
| res.watch.push(res.load.len()); |
| res.load.push(entry); |
| local_filesets.push(fsc.len() as u64); |
| fsc.add_file_set(file_set_roots) |
| } |
| } |
| |
| if let Some(user_config_path) = user_config_dir_path { |
| let ratoml_path = { |
| let mut p = user_config_path.to_path_buf(); |
| p.push("rust-analyzer.toml"); |
| p |
| }; |
| |
| let file_set_roots = vec![VfsPath::from(ratoml_path.to_owned())]; |
| let entry = vfs::loader::Entry::Files(vec![ratoml_path]); |
| |
| res.watch.push(res.load.len()); |
| res.load.push(entry); |
| local_filesets.push(fsc.len() as u64); |
| fsc.add_file_set(file_set_roots) |
| } |
| |
| let fsc = fsc.build(); |
| res.source_root_config = SourceRootConfig { fsc, local_filesets }; |
| |
| res |
| } |
| } |
| |
| #[derive(Default, Debug)] |
| pub struct SourceRootConfig { |
| pub fsc: FileSetConfig, |
| pub local_filesets: Vec<u64>, |
| } |
| |
| impl SourceRootConfig { |
| pub fn partition(&self, vfs: &vfs::Vfs) -> Vec<SourceRoot> { |
| self.fsc |
| .partition(vfs) |
| .into_iter() |
| .enumerate() |
| .map(|(idx, file_set)| { |
| let is_local = self.local_filesets.contains(&(idx as u64)); |
| if is_local { |
| SourceRoot::new_local(file_set) |
| } else { |
| SourceRoot::new_library(file_set) |
| } |
| }) |
| .collect() |
| } |
| |
| /// Maps local source roots to their parent source roots by bytewise comparing of root paths . |
| /// If a `SourceRoot` doesn't have a parent and is local then it is not contained in this mapping but it can be asserted that it is a root `SourceRoot`. |
| pub fn source_root_parent_map(&self) -> FxHashMap<SourceRootId, SourceRootId> { |
| let roots = self.fsc.roots(); |
| |
| let mut map = FxHashMap::default(); |
| |
| // See https://github.com/rust-lang/rust-analyzer/issues/17409 |
| // |
| // We can view the connections between roots as a graph. The problem is |
| // that this graph may contain cycles, so when adding edges, it is necessary |
| // to check whether it will lead to a cycle. |
| // |
| // Since we ensure that each node has at most one outgoing edge (because |
| // each SourceRoot can have only one parent), we can use a disjoint-set to |
| // maintain the connectivity between nodes. If an edge’s two nodes belong |
| // to the same set, they are already connected. |
| let mut dsu = FxHashMap::default(); |
| fn find_parent(dsu: &mut FxHashMap<u64, u64>, id: u64) -> u64 { |
| if let Some(&parent) = dsu.get(&id) { |
| let parent = find_parent(dsu, parent); |
| dsu.insert(id, parent); |
| parent |
| } else { |
| id |
| } |
| } |
| |
| for (idx, (root, root_id)) in roots.iter().enumerate() { |
| if !self.local_filesets.contains(root_id) |
| || map.contains_key(&SourceRootId(*root_id as u32)) |
| { |
| continue; |
| } |
| |
| for (root2, root2_id) in roots[..idx].iter().rev() { |
| if self.local_filesets.contains(root2_id) |
| && root_id != root2_id |
| && root.starts_with(root2) |
| { |
| // check if the edge will create a cycle |
| if find_parent(&mut dsu, *root_id) != find_parent(&mut dsu, *root2_id) { |
| map.insert(SourceRootId(*root_id as u32), SourceRootId(*root2_id as u32)); |
| dsu.insert(*root_id, *root2_id); |
| } |
| |
| break; |
| } |
| } |
| } |
| |
| map |
| } |
| } |
| |
| /// Load the proc-macros for the given lib path, disabling all expanders whose names are in `ignored_macros`. |
| pub fn load_proc_macro( |
| server: &ProcMacroClient, |
| path: &AbsPath, |
| ignored_macros: &[Box<str>], |
| ) -> ProcMacroLoadResult { |
| let res: Result<Vec<_>, _> = (|| { |
| let dylib = MacroDylib::new(path.to_path_buf()); |
| let vec = server.load_dylib(dylib).map_err(|e| { |
| ProcMacroLoadingError::ProcMacroSrvError(format!("{e}").into_boxed_str()) |
| })?; |
| if vec.is_empty() { |
| return Err(ProcMacroLoadingError::NoProcMacros); |
| } |
| Ok(vec |
| .into_iter() |
| .map(|expander| expander_to_proc_macro(expander, ignored_macros)) |
| .collect()) |
| })(); |
| match res { |
| Ok(proc_macros) => { |
| tracing::info!( |
| "Loaded proc-macros for {path}: {:?}", |
| proc_macros.iter().map(|it| it.name.clone()).collect::<Vec<_>>() |
| ); |
| Ok(proc_macros) |
| } |
| Err(e) => { |
| tracing::warn!("proc-macro loading for {path} failed: {e}"); |
| Err(e) |
| } |
| } |
| } |
| |
| fn load_crate_graph_into_db( |
| crate_graph: CrateGraphBuilder, |
| proc_macros: ProcMacrosBuilder, |
| source_root_config: SourceRootConfig, |
| vfs: &mut vfs::Vfs, |
| receiver: &Receiver<vfs::loader::Message>, |
| db: &mut RootDatabase, |
| ) { |
| let mut analysis_change = ChangeWithProcMacros::default(); |
| |
| db.enable_proc_attr_macros(); |
| |
| // wait until Vfs has loaded all roots |
| for task in receiver { |
| match task { |
| vfs::loader::Message::Progress { n_done, .. } => { |
| if n_done == LoadingProgress::Finished { |
| break; |
| } |
| } |
| vfs::loader::Message::Loaded { files } | vfs::loader::Message::Changed { files } => { |
| let _p = |
| tracing::info_span!("load_cargo::load_crate_craph/LoadedChanged").entered(); |
| for (path, contents) in files { |
| vfs.set_file_contents(path.into(), contents); |
| } |
| } |
| } |
| } |
| let changes = vfs.take_changes(); |
| for (_, file) in changes { |
| if let vfs::Change::Create(v, _) | vfs::Change::Modify(v, _) = file.change { |
| if let Ok(text) = String::from_utf8(v) { |
| analysis_change.change_file(file.file_id, Some(text)) |
| } |
| } |
| } |
| let source_roots = source_root_config.partition(vfs); |
| analysis_change.set_roots(source_roots); |
| |
| analysis_change.set_crate_graph(crate_graph); |
| analysis_change.set_proc_macros(proc_macros); |
| |
| db.apply_change(analysis_change); |
| } |
| |
| fn expander_to_proc_macro( |
| expander: proc_macro_api::ProcMacro, |
| ignored_macros: &[Box<str>], |
| ) -> ProcMacro { |
| let name = expander.name(); |
| let kind = match expander.kind() { |
| proc_macro_api::ProcMacroKind::CustomDerive => ProcMacroKind::CustomDerive, |
| proc_macro_api::ProcMacroKind::Bang => ProcMacroKind::Bang, |
| proc_macro_api::ProcMacroKind::Attr => ProcMacroKind::Attr, |
| }; |
| let disabled = ignored_macros.iter().any(|replace| **replace == *name); |
| ProcMacro { |
| name: intern::Symbol::intern(name), |
| kind, |
| expander: sync::Arc::new(Expander(expander)), |
| disabled, |
| } |
| } |
| |
| #[derive(Debug, PartialEq, Eq)] |
| struct Expander(proc_macro_api::ProcMacro); |
| |
| impl ProcMacroExpander for Expander { |
| fn expand( |
| &self, |
| subtree: &tt::TopSubtree<Span>, |
| attrs: Option<&tt::TopSubtree<Span>>, |
| env: &Env, |
| def_site: Span, |
| call_site: Span, |
| mixed_site: Span, |
| current_dir: String, |
| ) -> Result<tt::TopSubtree<Span>, ProcMacroExpansionError> { |
| match self.0.expand( |
| subtree.view(), |
| attrs.map(|attrs| attrs.view()), |
| env.clone().into(), |
| def_site, |
| call_site, |
| mixed_site, |
| current_dir, |
| ) { |
| Ok(Ok(subtree)) => Ok(subtree), |
| Ok(Err(err)) => Err(ProcMacroExpansionError::Panic(err.0)), |
| Err(err) => Err(ProcMacroExpansionError::System(err.to_string())), |
| } |
| } |
| |
| fn eq_dyn(&self, other: &dyn ProcMacroExpander) -> bool { |
| (other as &dyn Any).downcast_ref::<Self>() == Some(self) |
| } |
| } |
| |
| #[cfg(test)] |
| mod tests { |
| use ide_db::base_db::RootQueryDb; |
| use vfs::file_set::FileSetConfigBuilder; |
| |
| use super::*; |
| |
| #[test] |
| fn test_loading_rust_analyzer() { |
| let path = Path::new(env!("CARGO_MANIFEST_DIR")).parent().unwrap().parent().unwrap(); |
| let cargo_config = CargoConfig { set_test: true, ..CargoConfig::default() }; |
| let load_cargo_config = LoadCargoConfig { |
| load_out_dirs_from_check: false, |
| with_proc_macro_server: ProcMacroServerChoice::None, |
| prefill_caches: false, |
| }; |
| let (db, _vfs, _proc_macro) = |
| load_workspace_at(path, &cargo_config, &load_cargo_config, &|_| {}).unwrap(); |
| |
| let n_crates = db.all_crates().len(); |
| // RA has quite a few crates, but the exact count doesn't matter |
| assert!(n_crates > 20); |
| } |
| |
| #[test] |
| fn unrelated_sources() { |
| let mut builder = FileSetConfigBuilder::default(); |
| builder.add_file_set(vec![VfsPath::new_virtual_path("/ROOT/abc".to_owned())]); |
| builder.add_file_set(vec![VfsPath::new_virtual_path("/ROOT/def".to_owned())]); |
| let fsc = builder.build(); |
| let src = SourceRootConfig { fsc, local_filesets: vec![0, 1] }; |
| let vc = src.source_root_parent_map().into_iter().collect::<Vec<_>>(); |
| |
| assert_eq!(vc, vec![]) |
| } |
| |
| #[test] |
| fn unrelated_source_sharing_dirname() { |
| let mut builder = FileSetConfigBuilder::default(); |
| builder.add_file_set(vec![VfsPath::new_virtual_path("/ROOT/abc".to_owned())]); |
| builder.add_file_set(vec![VfsPath::new_virtual_path("/ROOT/def/abc".to_owned())]); |
| let fsc = builder.build(); |
| let src = SourceRootConfig { fsc, local_filesets: vec![0, 1] }; |
| let vc = src.source_root_parent_map().into_iter().collect::<Vec<_>>(); |
| |
| assert_eq!(vc, vec![]) |
| } |
| |
| #[test] |
| fn basic_child_parent() { |
| let mut builder = FileSetConfigBuilder::default(); |
| builder.add_file_set(vec![VfsPath::new_virtual_path("/ROOT/abc".to_owned())]); |
| builder.add_file_set(vec![VfsPath::new_virtual_path("/ROOT/abc/def".to_owned())]); |
| let fsc = builder.build(); |
| let src = SourceRootConfig { fsc, local_filesets: vec![0, 1] }; |
| let vc = src.source_root_parent_map().into_iter().collect::<Vec<_>>(); |
| |
| assert_eq!(vc, vec![(SourceRootId(1), SourceRootId(0))]) |
| } |
| |
| #[test] |
| fn basic_child_parent_with_unrelated_parents_sib() { |
| let mut builder = FileSetConfigBuilder::default(); |
| builder.add_file_set(vec![VfsPath::new_virtual_path("/ROOT/abc".to_owned())]); |
| builder.add_file_set(vec![VfsPath::new_virtual_path("/ROOT/def".to_owned())]); |
| builder.add_file_set(vec![VfsPath::new_virtual_path("/ROOT/def/abc".to_owned())]); |
| let fsc = builder.build(); |
| let src = SourceRootConfig { fsc, local_filesets: vec![0, 1, 2] }; |
| let vc = src.source_root_parent_map().into_iter().collect::<Vec<_>>(); |
| |
| assert_eq!(vc, vec![(SourceRootId(2), SourceRootId(1))]) |
| } |
| |
| #[test] |
| fn deep_sources_with_parent_missing() { |
| let mut builder = FileSetConfigBuilder::default(); |
| builder.add_file_set(vec![VfsPath::new_virtual_path("/ROOT/abc".to_owned())]); |
| builder.add_file_set(vec![VfsPath::new_virtual_path("/ROOT/ghi".to_owned())]); |
| builder.add_file_set(vec![VfsPath::new_virtual_path("/ROOT/def/abc".to_owned())]); |
| let fsc = builder.build(); |
| let src = SourceRootConfig { fsc, local_filesets: vec![0, 1, 2] }; |
| let vc = src.source_root_parent_map().into_iter().collect::<Vec<_>>(); |
| |
| assert_eq!(vc, vec![]) |
| } |
| |
| #[test] |
| fn ancestor_can_be_parent() { |
| let mut builder = FileSetConfigBuilder::default(); |
| builder.add_file_set(vec![VfsPath::new_virtual_path("/ROOT/abc".to_owned())]); |
| builder.add_file_set(vec![VfsPath::new_virtual_path("/ROOT/def".to_owned())]); |
| builder.add_file_set(vec![VfsPath::new_virtual_path("/ROOT/def/ghi/jkl".to_owned())]); |
| let fsc = builder.build(); |
| let src = SourceRootConfig { fsc, local_filesets: vec![0, 1, 2] }; |
| let vc = src.source_root_parent_map().into_iter().collect::<Vec<_>>(); |
| |
| assert_eq!(vc, vec![(SourceRootId(2), SourceRootId(1))]) |
| } |
| |
| #[test] |
| fn ancestor_can_be_parent_2() { |
| let mut builder = FileSetConfigBuilder::default(); |
| builder.add_file_set(vec![VfsPath::new_virtual_path("/ROOT/abc".to_owned())]); |
| builder.add_file_set(vec![VfsPath::new_virtual_path("/ROOT/def".to_owned())]); |
| builder.add_file_set(vec![VfsPath::new_virtual_path("/ROOT/def/ghi/jkl".to_owned())]); |
| builder.add_file_set(vec![VfsPath::new_virtual_path("/ROOT/def/ghi/klm".to_owned())]); |
| let fsc = builder.build(); |
| let src = SourceRootConfig { fsc, local_filesets: vec![0, 1, 2, 3] }; |
| let mut vc = src.source_root_parent_map().into_iter().collect::<Vec<_>>(); |
| vc.sort_by(|x, y| x.0.0.cmp(&y.0.0)); |
| |
| assert_eq!(vc, vec![(SourceRootId(2), SourceRootId(1)), (SourceRootId(3), SourceRootId(1))]) |
| } |
| |
| #[test] |
| fn non_locals_are_skipped() { |
| let mut builder = FileSetConfigBuilder::default(); |
| builder.add_file_set(vec![VfsPath::new_virtual_path("/ROOT/abc".to_owned())]); |
| builder.add_file_set(vec![VfsPath::new_virtual_path("/ROOT/def".to_owned())]); |
| builder.add_file_set(vec![VfsPath::new_virtual_path("/ROOT/def/ghi/jkl".to_owned())]); |
| builder.add_file_set(vec![VfsPath::new_virtual_path("/ROOT/def/klm".to_owned())]); |
| let fsc = builder.build(); |
| let src = SourceRootConfig { fsc, local_filesets: vec![0, 1, 3] }; |
| let mut vc = src.source_root_parent_map().into_iter().collect::<Vec<_>>(); |
| vc.sort_by(|x, y| x.0.0.cmp(&y.0.0)); |
| |
| assert_eq!(vc, vec![(SourceRootId(3), SourceRootId(1)),]) |
| } |
| |
| #[test] |
| fn child_binds_ancestor_if_parent_nonlocal() { |
| let mut builder = FileSetConfigBuilder::default(); |
| builder.add_file_set(vec![VfsPath::new_virtual_path("/ROOT/abc".to_owned())]); |
| builder.add_file_set(vec![VfsPath::new_virtual_path("/ROOT/def".to_owned())]); |
| builder.add_file_set(vec![VfsPath::new_virtual_path("/ROOT/def/klm".to_owned())]); |
| builder.add_file_set(vec![VfsPath::new_virtual_path("/ROOT/def/klm/jkl".to_owned())]); |
| let fsc = builder.build(); |
| let src = SourceRootConfig { fsc, local_filesets: vec![0, 1, 3] }; |
| let mut vc = src.source_root_parent_map().into_iter().collect::<Vec<_>>(); |
| vc.sort_by(|x, y| x.0.0.cmp(&y.0.0)); |
| |
| assert_eq!(vc, vec![(SourceRootId(3), SourceRootId(1)),]) |
| } |
| |
| #[test] |
| fn parents_with_identical_root_id() { |
| let mut builder = FileSetConfigBuilder::default(); |
| builder.add_file_set(vec![ |
| VfsPath::new_virtual_path("/ROOT/def".to_owned()), |
| VfsPath::new_virtual_path("/ROOT/def/abc/def".to_owned()), |
| ]); |
| builder.add_file_set(vec![VfsPath::new_virtual_path("/ROOT/def/abc/def/ghi".to_owned())]); |
| let fsc = builder.build(); |
| let src = SourceRootConfig { fsc, local_filesets: vec![0, 1] }; |
| let mut vc = src.source_root_parent_map().into_iter().collect::<Vec<_>>(); |
| vc.sort_by(|x, y| x.0.0.cmp(&y.0.0)); |
| |
| assert_eq!(vc, vec![(SourceRootId(1), SourceRootId(0)),]) |
| } |
| |
| #[test] |
| fn circular_reference() { |
| let mut builder = FileSetConfigBuilder::default(); |
| builder.add_file_set(vec![ |
| VfsPath::new_virtual_path("/ROOT/def".to_owned()), |
| VfsPath::new_virtual_path("/ROOT/def/abc/def".to_owned()), |
| ]); |
| builder.add_file_set(vec![VfsPath::new_virtual_path("/ROOT/def/abc".to_owned())]); |
| let fsc = builder.build(); |
| let src = SourceRootConfig { fsc, local_filesets: vec![0, 1] }; |
| let mut vc = src.source_root_parent_map().into_iter().collect::<Vec<_>>(); |
| vc.sort_by(|x, y| x.0.0.cmp(&y.0.0)); |
| |
| assert_eq!(vc, vec![(SourceRootId(1), SourceRootId(0)),]) |
| } |
| } |