load_cargo/
lib.rs

1//! Loads a Cargo project into a static instance of analysis, without support
2//! for incorporating changes.
3// Note, don't remove any public api from this. This API is consumed by external tools
4// to run rust-analyzer as a library.
5
6#![cfg_attr(feature = "in-rust-tree", feature(rustc_private))]
7
8#[cfg(feature = "in-rust-tree")]
9extern crate rustc_driver as _;
10
11use std::{any::Any, collections::hash_map::Entry, mem, path::Path, sync};
12
13use crossbeam_channel::{Receiver, unbounded};
14use hir_expand::proc_macro::{
15    ProcMacro, ProcMacroExpander, ProcMacroExpansionError, ProcMacroKind, ProcMacroLoadResult,
16    ProcMacrosBuilder,
17};
18use ide_db::{
19    ChangeWithProcMacros, FxHashMap, RootDatabase,
20    base_db::{CrateGraphBuilder, Env, ProcMacroLoadingError, SourceRoot, SourceRootId},
21    prime_caches,
22};
23use itertools::Itertools;
24use proc_macro_api::{MacroDylib, ProcMacroClient};
25use project_model::{CargoConfig, PackageRoot, ProjectManifest, ProjectWorkspace};
26use span::Span;
27use vfs::{
28    AbsPath, AbsPathBuf, VfsPath,
29    file_set::FileSetConfig,
30    loader::{Handle, LoadingProgress},
31};
32
33#[derive(Debug)]
34pub struct LoadCargoConfig {
35    pub load_out_dirs_from_check: bool,
36    pub with_proc_macro_server: ProcMacroServerChoice,
37    pub prefill_caches: bool,
38}
39
40#[derive(Debug, Clone, PartialEq, Eq)]
41pub enum ProcMacroServerChoice {
42    Sysroot,
43    Explicit(AbsPathBuf),
44    None,
45}
46
47pub fn load_workspace_at(
48    root: &Path,
49    cargo_config: &CargoConfig,
50    load_config: &LoadCargoConfig,
51    progress: &(dyn Fn(String) + Sync),
52) -> anyhow::Result<(RootDatabase, vfs::Vfs, Option<ProcMacroClient>)> {
53    let root = AbsPathBuf::assert_utf8(std::env::current_dir()?.join(root));
54    let root = ProjectManifest::discover_single(&root)?;
55    let manifest_path = root.manifest_path().clone();
56    let mut workspace = ProjectWorkspace::load(root, cargo_config, progress)?;
57
58    if load_config.load_out_dirs_from_check {
59        let build_scripts = workspace.run_build_scripts(cargo_config, progress)?;
60        if let Some(error) = build_scripts.error() {
61            tracing::debug!(
62                "Errors occurred while running build scripts for {}: {}",
63                manifest_path,
64                error
65            );
66        }
67        workspace.set_build_scripts(build_scripts)
68    }
69
70    load_workspace(workspace, &cargo_config.extra_env, load_config)
71}
72
73pub fn load_workspace(
74    ws: ProjectWorkspace,
75    extra_env: &FxHashMap<String, Option<String>>,
76    load_config: &LoadCargoConfig,
77) -> anyhow::Result<(RootDatabase, vfs::Vfs, Option<ProcMacroClient>)> {
78    let lru_cap = std::env::var("RA_LRU_CAP").ok().and_then(|it| it.parse::<u16>().ok());
79    let mut db = RootDatabase::new(lru_cap);
80
81    let (vfs, proc_macro_server) = load_workspace_into_db(ws, extra_env, load_config, &mut db)?;
82
83    Ok((db, vfs, proc_macro_server))
84}
85
86// This variant of `load_workspace` allows deferring the loading of rust-analyzer
87// into an existing database, which is useful in certain third-party scenarios,
88// now that `salsa` supports extending foreign databases (e.g. `RootDatabase`).
89pub fn load_workspace_into_db(
90    ws: ProjectWorkspace,
91    extra_env: &FxHashMap<String, Option<String>>,
92    load_config: &LoadCargoConfig,
93    db: &mut RootDatabase,
94) -> anyhow::Result<(vfs::Vfs, Option<ProcMacroClient>)> {
95    let (sender, receiver) = unbounded();
96    let mut vfs = vfs::Vfs::default();
97    let mut loader = {
98        let loader = vfs_notify::NotifyHandle::spawn(sender);
99        Box::new(loader)
100    };
101
102    tracing::debug!(?load_config, "LoadCargoConfig");
103    let proc_macro_server = match &load_config.with_proc_macro_server {
104        ProcMacroServerChoice::Sysroot => ws.find_sysroot_proc_macro_srv().map(|it| {
105            it.and_then(|it| {
106                ProcMacroClient::spawn(&it, extra_env, ws.toolchain.as_ref()).map_err(Into::into)
107            })
108            .map_err(|e| ProcMacroLoadingError::ProcMacroSrvError(e.to_string().into_boxed_str()))
109        }),
110        ProcMacroServerChoice::Explicit(path) => {
111            Some(ProcMacroClient::spawn(path, extra_env, ws.toolchain.as_ref()).map_err(|e| {
112                ProcMacroLoadingError::ProcMacroSrvError(e.to_string().into_boxed_str())
113            }))
114        }
115        ProcMacroServerChoice::None => Some(Err(ProcMacroLoadingError::Disabled)),
116    };
117    match &proc_macro_server {
118        Some(Ok(server)) => {
119            tracing::info!(manifest=%ws.manifest_or_root(), path=%server.server_path(), "Proc-macro server started")
120        }
121        Some(Err(e)) => {
122            tracing::info!(manifest=%ws.manifest_or_root(), %e, "Failed to start proc-macro server")
123        }
124        None => {
125            tracing::info!(manifest=%ws.manifest_or_root(), "No proc-macro server started")
126        }
127    }
128
129    let (crate_graph, proc_macros) = ws.to_crate_graph(
130        &mut |path: &AbsPath| {
131            let contents = loader.load_sync(path);
132            let path = vfs::VfsPath::from(path.to_path_buf());
133            vfs.set_file_contents(path.clone(), contents);
134            vfs.file_id(&path).and_then(|(file_id, excluded)| {
135                (excluded == vfs::FileExcluded::No).then_some(file_id)
136            })
137        },
138        extra_env,
139    );
140    let proc_macros = {
141        let proc_macro_server = match &proc_macro_server {
142            Some(Ok(it)) => Ok(it),
143            Some(Err(e)) => {
144                Err(ProcMacroLoadingError::ProcMacroSrvError(e.to_string().into_boxed_str()))
145            }
146            None => Err(ProcMacroLoadingError::ProcMacroSrvError(
147                "proc-macro-srv is not running, workspace is missing a sysroot".into(),
148            )),
149        };
150        proc_macros
151            .into_iter()
152            .map(|(crate_id, path)| {
153                (
154                    crate_id,
155                    path.map_or_else(Err, |(_, path)| {
156                        proc_macro_server.as_ref().map_err(Clone::clone).and_then(
157                            |proc_macro_server| load_proc_macro(proc_macro_server, &path, &[]),
158                        )
159                    }),
160                )
161            })
162            .collect()
163    };
164
165    let project_folders = ProjectFolders::new(std::slice::from_ref(&ws), &[], None);
166    loader.set_config(vfs::loader::Config {
167        load: project_folders.load,
168        watch: vec![],
169        version: 0,
170    });
171
172    load_crate_graph_into_db(
173        crate_graph,
174        proc_macros,
175        project_folders.source_root_config,
176        &mut vfs,
177        &receiver,
178        db,
179    );
180
181    if load_config.prefill_caches {
182        prime_caches::parallel_prime_caches(db, 1, &|_| ());
183    }
184
185    Ok((vfs, proc_macro_server.and_then(Result::ok)))
186}
187
188#[derive(Default)]
189pub struct ProjectFolders {
190    pub load: Vec<vfs::loader::Entry>,
191    pub watch: Vec<usize>,
192    pub source_root_config: SourceRootConfig,
193}
194
195impl ProjectFolders {
196    pub fn new(
197        workspaces: &[ProjectWorkspace],
198        global_excludes: &[AbsPathBuf],
199        user_config_dir_path: Option<&AbsPath>,
200    ) -> ProjectFolders {
201        let mut res = ProjectFolders::default();
202        let mut fsc = FileSetConfig::builder();
203        let mut local_filesets = vec![];
204
205        // Dedup source roots
206        // Depending on the project setup, we can have duplicated source roots, or for example in
207        // the case of the rustc workspace, we can end up with two source roots that are almost the
208        // same but not quite, like:
209        // PackageRoot { is_local: false, include: [AbsPathBuf(".../rust/src/tools/miri/cargo-miri")], exclude: [] }
210        // PackageRoot {
211        //     is_local: true,
212        //     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")],
213        //     exclude: [AbsPathBuf(".../rust/src/tools/miri/cargo-miri/.git"), AbsPathBuf(".../rust/src/tools/miri/cargo-miri/target")]
214        // }
215        //
216        // The first one comes from the explicit rustc workspace which points to the rustc workspace itself
217        // The second comes from the rustc workspace that we load as the actual project workspace
218        // 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.
219        // 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,
220        // so we need to also coalesce the includes if they overlap.
221
222        let mut roots: Vec<_> = workspaces
223            .iter()
224            .flat_map(|ws| ws.to_roots())
225            .update(|root| root.include.sort())
226            .sorted_by(|a, b| a.include.cmp(&b.include))
227            .collect();
228
229        // map that tracks indices of overlapping roots
230        let mut overlap_map = FxHashMap::<_, Vec<_>>::default();
231        let mut done = false;
232
233        while !mem::replace(&mut done, true) {
234            // maps include paths to indices of the corresponding root
235            let mut include_to_idx = FxHashMap::default();
236            // Find and note down the indices of overlapping roots
237            for (idx, root) in roots.iter().enumerate().filter(|(_, it)| !it.include.is_empty()) {
238                for include in &root.include {
239                    match include_to_idx.entry(include) {
240                        Entry::Occupied(e) => {
241                            overlap_map.entry(*e.get()).or_default().push(idx);
242                        }
243                        Entry::Vacant(e) => {
244                            e.insert(idx);
245                        }
246                    }
247                }
248            }
249            for (k, v) in overlap_map.drain() {
250                done = false;
251                for v in v {
252                    let r = mem::replace(
253                        &mut roots[v],
254                        PackageRoot { is_local: false, include: vec![], exclude: vec![] },
255                    );
256                    roots[k].is_local |= r.is_local;
257                    roots[k].include.extend(r.include);
258                    roots[k].exclude.extend(r.exclude);
259                }
260                roots[k].include.sort();
261                roots[k].exclude.sort();
262                roots[k].include.dedup();
263                roots[k].exclude.dedup();
264            }
265        }
266
267        for root in roots.into_iter().filter(|it| !it.include.is_empty()) {
268            let file_set_roots: Vec<VfsPath> =
269                root.include.iter().cloned().map(VfsPath::from).collect();
270
271            let entry = {
272                let mut dirs = vfs::loader::Directories::default();
273                dirs.extensions.push("rs".into());
274                dirs.extensions.push("toml".into());
275                dirs.include.extend(root.include);
276                dirs.exclude.extend(root.exclude);
277                for excl in global_excludes {
278                    if dirs
279                        .include
280                        .iter()
281                        .any(|incl| incl.starts_with(excl) || excl.starts_with(incl))
282                    {
283                        dirs.exclude.push(excl.clone());
284                    }
285                }
286
287                vfs::loader::Entry::Directories(dirs)
288            };
289
290            if root.is_local {
291                res.watch.push(res.load.len());
292            }
293            res.load.push(entry);
294
295            if root.is_local {
296                local_filesets.push(fsc.len() as u64);
297            }
298            fsc.add_file_set(file_set_roots)
299        }
300
301        for ws in workspaces.iter() {
302            let mut file_set_roots: Vec<VfsPath> = vec![];
303            let mut entries = vec![];
304
305            for buildfile in ws.buildfiles() {
306                file_set_roots.push(VfsPath::from(buildfile.to_owned()));
307                entries.push(buildfile.to_owned());
308            }
309
310            if !file_set_roots.is_empty() {
311                let entry = vfs::loader::Entry::Files(entries);
312                res.watch.push(res.load.len());
313                res.load.push(entry);
314                local_filesets.push(fsc.len() as u64);
315                fsc.add_file_set(file_set_roots)
316            }
317        }
318
319        if let Some(user_config_path) = user_config_dir_path {
320            let ratoml_path = {
321                let mut p = user_config_path.to_path_buf();
322                p.push("rust-analyzer.toml");
323                p
324            };
325
326            let file_set_roots = vec![VfsPath::from(ratoml_path.to_owned())];
327            let entry = vfs::loader::Entry::Files(vec![ratoml_path]);
328
329            res.watch.push(res.load.len());
330            res.load.push(entry);
331            local_filesets.push(fsc.len() as u64);
332            fsc.add_file_set(file_set_roots)
333        }
334
335        let fsc = fsc.build();
336        res.source_root_config = SourceRootConfig { fsc, local_filesets };
337
338        res
339    }
340}
341
342#[derive(Default, Debug)]
343pub struct SourceRootConfig {
344    pub fsc: FileSetConfig,
345    pub local_filesets: Vec<u64>,
346}
347
348impl SourceRootConfig {
349    pub fn partition(&self, vfs: &vfs::Vfs) -> Vec<SourceRoot> {
350        self.fsc
351            .partition(vfs)
352            .into_iter()
353            .enumerate()
354            .map(|(idx, file_set)| {
355                let is_local = self.local_filesets.contains(&(idx as u64));
356                if is_local {
357                    SourceRoot::new_local(file_set)
358                } else {
359                    SourceRoot::new_library(file_set)
360                }
361            })
362            .collect()
363    }
364
365    /// Maps local source roots to their parent source roots by bytewise comparing of root paths .
366    /// 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`.
367    pub fn source_root_parent_map(&self) -> FxHashMap<SourceRootId, SourceRootId> {
368        let roots = self.fsc.roots();
369
370        let mut map = FxHashMap::default();
371
372        // See https://github.com/rust-lang/rust-analyzer/issues/17409
373        //
374        // We can view the connections between roots as a graph. The problem is
375        // that this graph may contain cycles, so when adding edges, it is necessary
376        // to check whether it will lead to a cycle.
377        //
378        // Since we ensure that each node has at most one outgoing edge (because
379        // each SourceRoot can have only one parent), we can use a disjoint-set to
380        // maintain the connectivity between nodes. If an edge’s two nodes belong
381        // to the same set, they are already connected.
382        let mut dsu = FxHashMap::default();
383        fn find_parent(dsu: &mut FxHashMap<u64, u64>, id: u64) -> u64 {
384            if let Some(&parent) = dsu.get(&id) {
385                let parent = find_parent(dsu, parent);
386                dsu.insert(id, parent);
387                parent
388            } else {
389                id
390            }
391        }
392
393        for (idx, (root, root_id)) in roots.iter().enumerate() {
394            if !self.local_filesets.contains(root_id)
395                || map.contains_key(&SourceRootId(*root_id as u32))
396            {
397                continue;
398            }
399
400            for (root2, root2_id) in roots[..idx].iter().rev() {
401                if self.local_filesets.contains(root2_id)
402                    && root_id != root2_id
403                    && root.starts_with(root2)
404                {
405                    // check if the edge will create a cycle
406                    if find_parent(&mut dsu, *root_id) != find_parent(&mut dsu, *root2_id) {
407                        map.insert(SourceRootId(*root_id as u32), SourceRootId(*root2_id as u32));
408                        dsu.insert(*root_id, *root2_id);
409                    }
410
411                    break;
412                }
413            }
414        }
415
416        map
417    }
418}
419
420/// Load the proc-macros for the given lib path, disabling all expanders whose names are in `ignored_macros`.
421pub fn load_proc_macro(
422    server: &ProcMacroClient,
423    path: &AbsPath,
424    ignored_macros: &[Box<str>],
425) -> ProcMacroLoadResult {
426    let res: Result<Vec<_>, _> = (|| {
427        let dylib = MacroDylib::new(path.to_path_buf());
428        let vec = server.load_dylib(dylib).map_err(|e| {
429            ProcMacroLoadingError::ProcMacroSrvError(format!("{e}").into_boxed_str())
430        })?;
431        if vec.is_empty() {
432            return Err(ProcMacroLoadingError::NoProcMacros);
433        }
434        Ok(vec
435            .into_iter()
436            .map(|expander| expander_to_proc_macro(expander, ignored_macros))
437            .collect())
438    })();
439    match res {
440        Ok(proc_macros) => {
441            tracing::info!(
442                "Loaded proc-macros for {path}: {:?}",
443                proc_macros.iter().map(|it| it.name.clone()).collect::<Vec<_>>()
444            );
445            Ok(proc_macros)
446        }
447        Err(e) => {
448            tracing::warn!("proc-macro loading for {path} failed: {e}");
449            Err(e)
450        }
451    }
452}
453
454fn load_crate_graph_into_db(
455    crate_graph: CrateGraphBuilder,
456    proc_macros: ProcMacrosBuilder,
457    source_root_config: SourceRootConfig,
458    vfs: &mut vfs::Vfs,
459    receiver: &Receiver<vfs::loader::Message>,
460    db: &mut RootDatabase,
461) {
462    let mut analysis_change = ChangeWithProcMacros::default();
463
464    db.enable_proc_attr_macros();
465
466    // wait until Vfs has loaded all roots
467    for task in receiver {
468        match task {
469            vfs::loader::Message::Progress { n_done, .. } => {
470                if n_done == LoadingProgress::Finished {
471                    break;
472                }
473            }
474            vfs::loader::Message::Loaded { files } | vfs::loader::Message::Changed { files } => {
475                let _p =
476                    tracing::info_span!("load_cargo::load_crate_craph/LoadedChanged").entered();
477                for (path, contents) in files {
478                    vfs.set_file_contents(path.into(), contents);
479                }
480            }
481        }
482    }
483    let changes = vfs.take_changes();
484    for (_, file) in changes {
485        if let vfs::Change::Create(v, _) | vfs::Change::Modify(v, _) = file.change
486            && let Ok(text) = String::from_utf8(v)
487        {
488            analysis_change.change_file(file.file_id, Some(text))
489        }
490    }
491    let source_roots = source_root_config.partition(vfs);
492    analysis_change.set_roots(source_roots);
493
494    analysis_change.set_crate_graph(crate_graph);
495    analysis_change.set_proc_macros(proc_macros);
496
497    db.apply_change(analysis_change);
498}
499
500fn expander_to_proc_macro(
501    expander: proc_macro_api::ProcMacro,
502    ignored_macros: &[Box<str>],
503) -> ProcMacro {
504    let name = expander.name();
505    let kind = match expander.kind() {
506        proc_macro_api::ProcMacroKind::CustomDerive => ProcMacroKind::CustomDerive,
507        proc_macro_api::ProcMacroKind::Bang => ProcMacroKind::Bang,
508        proc_macro_api::ProcMacroKind::Attr => ProcMacroKind::Attr,
509    };
510    let disabled = ignored_macros.iter().any(|replace| **replace == *name);
511    ProcMacro {
512        name: intern::Symbol::intern(name),
513        kind,
514        expander: sync::Arc::new(Expander(expander)),
515        disabled,
516    }
517}
518
519#[derive(Debug, PartialEq, Eq)]
520struct Expander(proc_macro_api::ProcMacro);
521
522impl ProcMacroExpander for Expander {
523    fn expand(
524        &self,
525        subtree: &tt::TopSubtree<Span>,
526        attrs: Option<&tt::TopSubtree<Span>>,
527        env: &Env,
528        def_site: Span,
529        call_site: Span,
530        mixed_site: Span,
531        current_dir: String,
532    ) -> Result<tt::TopSubtree<Span>, ProcMacroExpansionError> {
533        match self.0.expand(
534            subtree.view(),
535            attrs.map(|attrs| attrs.view()),
536            env.clone().into(),
537            def_site,
538            call_site,
539            mixed_site,
540            current_dir,
541        ) {
542            Ok(Ok(subtree)) => Ok(subtree),
543            Ok(Err(err)) => Err(ProcMacroExpansionError::Panic(err)),
544            Err(err) => Err(ProcMacroExpansionError::System(err.to_string())),
545        }
546    }
547
548    fn eq_dyn(&self, other: &dyn ProcMacroExpander) -> bool {
549        (other as &dyn Any).downcast_ref::<Self>() == Some(self)
550    }
551}
552
553#[cfg(test)]
554mod tests {
555    use ide_db::base_db::RootQueryDb;
556    use vfs::file_set::FileSetConfigBuilder;
557
558    use super::*;
559
560    #[test]
561    fn test_loading_rust_analyzer() {
562        let path = Path::new(env!("CARGO_MANIFEST_DIR")).parent().unwrap().parent().unwrap();
563        let cargo_config = CargoConfig { set_test: true, ..CargoConfig::default() };
564        let load_cargo_config = LoadCargoConfig {
565            load_out_dirs_from_check: false,
566            with_proc_macro_server: ProcMacroServerChoice::None,
567            prefill_caches: false,
568        };
569        let (db, _vfs, _proc_macro) =
570            load_workspace_at(path, &cargo_config, &load_cargo_config, &|_| {}).unwrap();
571
572        let n_crates = db.all_crates().len();
573        // RA has quite a few crates, but the exact count doesn't matter
574        assert!(n_crates > 20);
575    }
576
577    #[test]
578    fn unrelated_sources() {
579        let mut builder = FileSetConfigBuilder::default();
580        builder.add_file_set(vec![VfsPath::new_virtual_path("/ROOT/abc".to_owned())]);
581        builder.add_file_set(vec![VfsPath::new_virtual_path("/ROOT/def".to_owned())]);
582        let fsc = builder.build();
583        let src = SourceRootConfig { fsc, local_filesets: vec![0, 1] };
584        let vc = src.source_root_parent_map().into_iter().collect::<Vec<_>>();
585
586        assert_eq!(vc, vec![])
587    }
588
589    #[test]
590    fn unrelated_source_sharing_dirname() {
591        let mut builder = FileSetConfigBuilder::default();
592        builder.add_file_set(vec![VfsPath::new_virtual_path("/ROOT/abc".to_owned())]);
593        builder.add_file_set(vec![VfsPath::new_virtual_path("/ROOT/def/abc".to_owned())]);
594        let fsc = builder.build();
595        let src = SourceRootConfig { fsc, local_filesets: vec![0, 1] };
596        let vc = src.source_root_parent_map().into_iter().collect::<Vec<_>>();
597
598        assert_eq!(vc, vec![])
599    }
600
601    #[test]
602    fn basic_child_parent() {
603        let mut builder = FileSetConfigBuilder::default();
604        builder.add_file_set(vec![VfsPath::new_virtual_path("/ROOT/abc".to_owned())]);
605        builder.add_file_set(vec![VfsPath::new_virtual_path("/ROOT/abc/def".to_owned())]);
606        let fsc = builder.build();
607        let src = SourceRootConfig { fsc, local_filesets: vec![0, 1] };
608        let vc = src.source_root_parent_map().into_iter().collect::<Vec<_>>();
609
610        assert_eq!(vc, vec![(SourceRootId(1), SourceRootId(0))])
611    }
612
613    #[test]
614    fn basic_child_parent_with_unrelated_parents_sib() {
615        let mut builder = FileSetConfigBuilder::default();
616        builder.add_file_set(vec![VfsPath::new_virtual_path("/ROOT/abc".to_owned())]);
617        builder.add_file_set(vec![VfsPath::new_virtual_path("/ROOT/def".to_owned())]);
618        builder.add_file_set(vec![VfsPath::new_virtual_path("/ROOT/def/abc".to_owned())]);
619        let fsc = builder.build();
620        let src = SourceRootConfig { fsc, local_filesets: vec![0, 1, 2] };
621        let vc = src.source_root_parent_map().into_iter().collect::<Vec<_>>();
622
623        assert_eq!(vc, vec![(SourceRootId(2), SourceRootId(1))])
624    }
625
626    #[test]
627    fn deep_sources_with_parent_missing() {
628        let mut builder = FileSetConfigBuilder::default();
629        builder.add_file_set(vec![VfsPath::new_virtual_path("/ROOT/abc".to_owned())]);
630        builder.add_file_set(vec![VfsPath::new_virtual_path("/ROOT/ghi".to_owned())]);
631        builder.add_file_set(vec![VfsPath::new_virtual_path("/ROOT/def/abc".to_owned())]);
632        let fsc = builder.build();
633        let src = SourceRootConfig { fsc, local_filesets: vec![0, 1, 2] };
634        let vc = src.source_root_parent_map().into_iter().collect::<Vec<_>>();
635
636        assert_eq!(vc, vec![])
637    }
638
639    #[test]
640    fn ancestor_can_be_parent() {
641        let mut builder = FileSetConfigBuilder::default();
642        builder.add_file_set(vec![VfsPath::new_virtual_path("/ROOT/abc".to_owned())]);
643        builder.add_file_set(vec![VfsPath::new_virtual_path("/ROOT/def".to_owned())]);
644        builder.add_file_set(vec![VfsPath::new_virtual_path("/ROOT/def/ghi/jkl".to_owned())]);
645        let fsc = builder.build();
646        let src = SourceRootConfig { fsc, local_filesets: vec![0, 1, 2] };
647        let vc = src.source_root_parent_map().into_iter().collect::<Vec<_>>();
648
649        assert_eq!(vc, vec![(SourceRootId(2), SourceRootId(1))])
650    }
651
652    #[test]
653    fn ancestor_can_be_parent_2() {
654        let mut builder = FileSetConfigBuilder::default();
655        builder.add_file_set(vec![VfsPath::new_virtual_path("/ROOT/abc".to_owned())]);
656        builder.add_file_set(vec![VfsPath::new_virtual_path("/ROOT/def".to_owned())]);
657        builder.add_file_set(vec![VfsPath::new_virtual_path("/ROOT/def/ghi/jkl".to_owned())]);
658        builder.add_file_set(vec![VfsPath::new_virtual_path("/ROOT/def/ghi/klm".to_owned())]);
659        let fsc = builder.build();
660        let src = SourceRootConfig { fsc, local_filesets: vec![0, 1, 2, 3] };
661        let mut vc = src.source_root_parent_map().into_iter().collect::<Vec<_>>();
662        vc.sort_by(|x, y| x.0.0.cmp(&y.0.0));
663
664        assert_eq!(vc, vec![(SourceRootId(2), SourceRootId(1)), (SourceRootId(3), SourceRootId(1))])
665    }
666
667    #[test]
668    fn non_locals_are_skipped() {
669        let mut builder = FileSetConfigBuilder::default();
670        builder.add_file_set(vec![VfsPath::new_virtual_path("/ROOT/abc".to_owned())]);
671        builder.add_file_set(vec![VfsPath::new_virtual_path("/ROOT/def".to_owned())]);
672        builder.add_file_set(vec![VfsPath::new_virtual_path("/ROOT/def/ghi/jkl".to_owned())]);
673        builder.add_file_set(vec![VfsPath::new_virtual_path("/ROOT/def/klm".to_owned())]);
674        let fsc = builder.build();
675        let src = SourceRootConfig { fsc, local_filesets: vec![0, 1, 3] };
676        let mut vc = src.source_root_parent_map().into_iter().collect::<Vec<_>>();
677        vc.sort_by(|x, y| x.0.0.cmp(&y.0.0));
678
679        assert_eq!(vc, vec![(SourceRootId(3), SourceRootId(1)),])
680    }
681
682    #[test]
683    fn child_binds_ancestor_if_parent_nonlocal() {
684        let mut builder = FileSetConfigBuilder::default();
685        builder.add_file_set(vec![VfsPath::new_virtual_path("/ROOT/abc".to_owned())]);
686        builder.add_file_set(vec![VfsPath::new_virtual_path("/ROOT/def".to_owned())]);
687        builder.add_file_set(vec![VfsPath::new_virtual_path("/ROOT/def/klm".to_owned())]);
688        builder.add_file_set(vec![VfsPath::new_virtual_path("/ROOT/def/klm/jkl".to_owned())]);
689        let fsc = builder.build();
690        let src = SourceRootConfig { fsc, local_filesets: vec![0, 1, 3] };
691        let mut vc = src.source_root_parent_map().into_iter().collect::<Vec<_>>();
692        vc.sort_by(|x, y| x.0.0.cmp(&y.0.0));
693
694        assert_eq!(vc, vec![(SourceRootId(3), SourceRootId(1)),])
695    }
696
697    #[test]
698    fn parents_with_identical_root_id() {
699        let mut builder = FileSetConfigBuilder::default();
700        builder.add_file_set(vec![
701            VfsPath::new_virtual_path("/ROOT/def".to_owned()),
702            VfsPath::new_virtual_path("/ROOT/def/abc/def".to_owned()),
703        ]);
704        builder.add_file_set(vec![VfsPath::new_virtual_path("/ROOT/def/abc/def/ghi".to_owned())]);
705        let fsc = builder.build();
706        let src = SourceRootConfig { fsc, local_filesets: vec![0, 1] };
707        let mut vc = src.source_root_parent_map().into_iter().collect::<Vec<_>>();
708        vc.sort_by(|x, y| x.0.0.cmp(&y.0.0));
709
710        assert_eq!(vc, vec![(SourceRootId(1), SourceRootId(0)),])
711    }
712
713    #[test]
714    fn circular_reference() {
715        let mut builder = FileSetConfigBuilder::default();
716        builder.add_file_set(vec![
717            VfsPath::new_virtual_path("/ROOT/def".to_owned()),
718            VfsPath::new_virtual_path("/ROOT/def/abc/def".to_owned()),
719        ]);
720        builder.add_file_set(vec![VfsPath::new_virtual_path("/ROOT/def/abc".to_owned())]);
721        let fsc = builder.build();
722        let src = SourceRootConfig { fsc, local_filesets: vec![0, 1] };
723        let mut vc = src.source_root_parent_map().into_iter().collect::<Vec<_>>();
724        vc.sort_by(|x, y| x.0.0.cmp(&y.0.0));
725
726        assert_eq!(vc, vec![(SourceRootId(1), SourceRootId(0)),])
727    }
728}