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