rust_analyzer_modules/
analyzer.rs

1// This Source Code Form is subject to the terms of the Mozilla Public
2// License, v. 2.0. If a copy of the MPL was not distributed with this
3// file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
5use std::path::{Path, PathBuf};
6
7use ra_ap_cfg::{self as cfg};
8use ra_ap_hir::{self as hir, AsAssocItem as _, HasAttrs as _};
9use ra_ap_ide::{self as ide};
10use ra_ap_ide_db::{self as ide_db};
11use ra_ap_load_cargo::{self as load_cargo};
12use ra_ap_paths::{self as paths};
13use ra_ap_project_model::{self as project_model};
14use ra_ap_syntax::{self as syntax, AstNode as _, ast};
15use ra_ap_vfs::{self as vfs};
16
17use crate::{
18    item::{ItemCfgAttr, ItemTestAttr},
19    options::{GeneralOptions, ProjectOptions},
20};
21
22pub struct LoadOptions {
23    /// Analyze with `#[cfg(test)]` enabled (i.e as if built via `cargo test`).
24    pub cfg_test: bool,
25
26    /// Include sysroot crates (`std`, `core` & friends) in analysis.
27    pub sysroot: bool,
28}
29
30pub fn load_workspace(
31    general_options: &GeneralOptions,
32    project_options: &ProjectOptions,
33    load_options: &LoadOptions,
34) -> anyhow::Result<(hir::Crate, ide::AnalysisHost, vfs::Vfs, ide::Edition)> {
35    let project_path = project_options.manifest_path.as_path().canonicalize()?;
36
37    // See: https://github.com/rust-lang/cargo/pull/13909
38    // The `canonicalize` func on windows will return `r"\\?\"` verbatim prefix.
39    // "The Cargo makes a core assumption that verbatim paths aren't used."
40    // So we need to `simplify` the path as the non-verbatim path.
41    let project_path = dunce::simplified(&project_path).to_path_buf();
42
43    let cargo_config = cargo_config(project_options, load_options);
44    let load_config = load_config();
45
46    let progress = |string| {
47        tracing::info!("Cargo analysis progress: {}", string);
48        eprintln!("Cargo analysis progress: {string}");
49    };
50
51    let mut project_workspace = load_project_workspace(&project_path, &cargo_config, &progress)?;
52
53    let (package, target) = select_package_and_target(&project_workspace, project_options)?;
54
55    if general_options.verbose {
56        eprintln!();
57        eprintln!("crate");
58        eprintln!("└── package: {}", package.name);
59        eprintln!("    └── target: {}", target.name);
60        eprintln!();
61    }
62
63    let edition = package.edition;
64
65    if load_config.load_out_dirs_from_check {
66        let build_scripts = project_workspace.run_build_scripts(&cargo_config, &progress)?;
67        project_workspace.set_build_scripts(build_scripts)
68    }
69
70    let (db, vfs, _proc_macro_client) =
71        ra_ap_load_cargo::load_workspace(project_workspace, &cargo_config.extra_env, &load_config)?;
72
73    let host = ide::AnalysisHost::with_database(db);
74
75    let krate = find_crate(host.raw_database(), &vfs, &target)?;
76
77    Ok((krate, host, vfs, edition))
78}
79
80pub fn cargo_config(
81    project_options: &ProjectOptions,
82    load_options: &LoadOptions,
83) -> project_model::CargoConfig {
84    let all_targets = false;
85
86    // Crates to enable/disable `#[cfg(test)]` on
87    let cfg_overrides = match load_options.cfg_test {
88        true => project_model::CfgOverrides {
89            global: cfg::CfgDiff::new(
90                vec![cfg::CfgAtom::Flag(hir::Symbol::intern("test"))],
91                Vec::new(),
92            ),
93            selective: Default::default(),
94        },
95        false => project_model::CfgOverrides {
96            global: cfg::CfgDiff::new(
97                Vec::new(),
98                vec![cfg::CfgAtom::Flag(hir::Symbol::intern("test"))],
99            ),
100            selective: Default::default(),
101        },
102    };
103
104    let extra_args = vec![];
105
106    // FIXME: support extra environment variables via CLI:
107    let extra_env = ide_db::FxHashMap::default();
108
109    let extra_includes = vec![];
110
111    // List of features to activate (or deactivate).
112    let features = if project_options.all_features {
113        project_model::CargoFeatures::All
114    } else {
115        project_model::CargoFeatures::Selected {
116            features: project_options.features.clone(),
117            no_default_features: project_options.no_default_features,
118        }
119    };
120
121    let invocation_strategy = project_model::InvocationStrategy::PerWorkspace;
122
123    let no_deps: bool = true; // Skip external dependencies for performance
124
125    let run_build_script_command = None;
126
127    // Rustc private crate source
128    let rustc_source = None;
129
130    let set_test = load_options.cfg_test;
131
132    // Whether to load sysroot crates (`std`, `core` & friends).
133    let sysroot = if load_options.sysroot {
134        Some(project_model::RustLibSource::Discover)
135    } else {
136        None
137    };
138
139    let sysroot_src = None;
140
141    // Target triple
142    let target = project_options.target.clone();
143
144    let target_dir = None;
145
146    // Disable RUSTC_WRAPPER to avoid potential hanging during compilation
147    // This prevents rust-analyzer from trying to compile proc macros and build scripts
148    let wrap_rustc_in_build_scripts = false;
149
150    project_model::CargoConfig {
151        all_targets,
152        cfg_overrides,
153        extra_args,
154        extra_env,
155        extra_includes,
156        features,
157        invocation_strategy,
158        no_deps,
159        run_build_script_command,
160        rustc_source,
161        set_test,
162        sysroot_src,
163        sysroot,
164        target_dir,
165        target,
166        wrap_rustc_in_build_scripts,
167    }
168}
169
170pub fn load_config() -> load_cargo::LoadCargoConfig {
171    // Disable build script execution to prevent hanging
172    // This is the most common cause of hangs in rust-analyzer-based tools
173    let load_out_dirs_from_check = false;
174    let prefill_caches = false;
175    let with_proc_macro_server = load_cargo::ProcMacroServerChoice::Sysroot;
176
177    load_cargo::LoadCargoConfig {
178        load_out_dirs_from_check,
179        prefill_caches,
180        with_proc_macro_server,
181    }
182}
183
184pub fn load_project_workspace(
185    project_path: &Path,
186    cargo_config: &project_model::CargoConfig,
187    progress: &(dyn Fn(String) + Sync),
188) -> anyhow::Result<project_model::ProjectWorkspace> {
189    let path_buf = std::env::current_dir()?.join(project_path);
190    let utf8_path_buf = paths::Utf8PathBuf::from_path_buf(path_buf).unwrap();
191    let root = paths::AbsPathBuf::assert(utf8_path_buf);
192    let root = project_model::ProjectManifest::discover_single(root.as_path())?;
193
194    project_model::ProjectWorkspace::load(root, cargo_config, &progress)
195}
196
197pub fn select_package_and_target(
198    project_workspace: &project_model::ProjectWorkspace,
199    options: &ProjectOptions,
200) -> anyhow::Result<(project_model::PackageData, project_model::TargetData)> {
201    let cargo_workspace = match project_workspace.kind {
202        project_model::ProjectWorkspaceKind::Cargo { ref cargo, .. } => Ok(cargo),
203        project_model::ProjectWorkspaceKind::Json { .. } => {
204            Err(anyhow::anyhow!("Unexpected JSON workspace"))
205        }
206        project_model::ProjectWorkspaceKind::DetachedFile { .. } => {
207            Err(anyhow::anyhow!("Unexpected detached files"))
208        }
209    }?;
210
211    let package_idx = select_package(cargo_workspace, options)?;
212    let package = cargo_workspace[package_idx].clone();
213    tracing::debug!("Selected package: {:#?}", package.name);
214
215    let target_idx = select_target(cargo_workspace, package_idx, options)?;
216    let target = cargo_workspace[target_idx].clone();
217    tracing::debug!("Selected target: {:#?}", target.name);
218
219    Ok((package, target))
220}
221
222pub fn select_package(
223    workspace: &project_model::CargoWorkspace,
224    options: &ProjectOptions,
225) -> anyhow::Result<project_model::Package> {
226    let packages: Vec<_> = workspace
227        .packages()
228        .filter(|idx| workspace[*idx].is_member)
229        .collect();
230
231    let package_count = packages.len();
232
233    // If project contains no packages, bail out:
234
235    if package_count < 1 {
236        anyhow::bail!("no packages found");
237    }
238
239    // If no (or a non-existent) package was provided via options bail out:
240
241    let package_list_items: Vec<_> = packages
242        .iter()
243        .map(|package_idx| {
244            let package = &workspace[*package_idx];
245            format!("- {}", package.name)
246        })
247        .collect();
248
249    let package_list = package_list_items.join("\n");
250
251    // If project contains multiple packages, select the one provided via options:
252
253    if let Some(package_name) = &options.package {
254        let package_idx = packages.into_iter().find(|package_idx| {
255            let package = &workspace[*package_idx];
256            package.name == *package_name
257        });
258
259        return package_idx.ok_or_else(|| {
260            anyhow::anyhow!(
261                indoc::indoc! {
262                    "No package found with name {:?}.
263
264                        Packages present in workspace:
265                        {}
266                        "
267                },
268                package_name,
269                package_list,
270            )
271        });
272    }
273
274    // If project contains a single packages, just pick it:
275
276    if package_count == 1 {
277        return Ok(packages[0]);
278    }
279
280    Err(anyhow::anyhow!(
281        indoc::indoc! {
282            "Multiple packages present in workspace,
283                please explicitly select one via --package flag.
284
285                Packages present in workspace:
286                {}
287                "
288        },
289        package_list
290    ))
291}
292
293pub fn select_target(
294    workspace: &project_model::CargoWorkspace,
295    package_idx: project_model::Package,
296    options: &ProjectOptions,
297) -> anyhow::Result<project_model::Target> {
298    let package = &workspace[package_idx];
299
300    // Retrieve list of indices for bin/lib targets:
301
302    let targets: Vec<_> = package
303        .targets
304        .iter()
305        .cloned()
306        .filter(|target_idx| {
307            let target = &workspace[*target_idx];
308            match target.kind {
309                project_model::TargetKind::Bin => true,
310                project_model::TargetKind::Lib { .. } => true,
311                project_model::TargetKind::Example => false,
312                project_model::TargetKind::Test => false,
313                project_model::TargetKind::Bench => false,
314                project_model::TargetKind::Other => false,
315                project_model::TargetKind::BuildScript => false,
316            }
317        })
318        .collect();
319
320    let target_count = targets.len();
321
322    // If package contains no targets, bail out:
323
324    if target_count < 1 {
325        anyhow::bail!("no targets found");
326    }
327
328    // If no (or a non-existent) target was provided via options bail out:
329
330    let target_list_items: Vec<_> = targets
331        .iter()
332        .map(|target_idx| {
333            let target = &workspace[*target_idx];
334            match target.kind {
335                project_model::TargetKind::Bin => {
336                    format!("- {} (--bin {})", target.name, target.name)
337                }
338                project_model::TargetKind::Lib { .. } => format!("- {} (--lib)", target.name),
339                project_model::TargetKind::Example => unreachable!(),
340                project_model::TargetKind::Test => unreachable!(),
341                project_model::TargetKind::Bench => unreachable!(),
342                project_model::TargetKind::Other => unreachable!(),
343                project_model::TargetKind::BuildScript => unreachable!(),
344            }
345        })
346        .collect();
347
348    let target_list = target_list_items.join("\n");
349
350    // If package contains multiple targets, select the one provided via options:
351
352    if options.lib {
353        let target = targets.into_iter().find(|target_idx| {
354            let target = &workspace[*target_idx];
355            matches!(target.kind, project_model::TargetKind::Lib { .. })
356        });
357
358        return target.ok_or_else(|| {
359            anyhow::anyhow!(
360                indoc::indoc! {
361                    "No library target found.
362
363                        Targets present in package:
364                        {}
365                        "
366                },
367                target_list,
368            )
369        });
370    }
371
372    if let Some(bin_name) = &options.bin {
373        let target = targets.into_iter().find(|target_idx| {
374            let target = &workspace[*target_idx];
375            (target.kind == project_model::TargetKind::Bin) && (target.name == bin_name[..])
376        });
377
378        return target.ok_or_else(|| {
379            anyhow::anyhow!(
380                indoc::indoc! {
381                    "No binary target found with name {:?}.
382
383                        Targets present in package:
384                        {}
385                        "
386                },
387                bin_name,
388                target_list,
389            )
390        });
391    }
392
393    // If project contains a single target, just pick it:
394
395    if target_count == 1 {
396        return Ok(targets[0]);
397    }
398
399    Err(anyhow::anyhow!(
400        indoc::indoc! {
401            "Multiple targets present in package,
402                please explicitly select one via --lib or --bin flag.
403
404                Targets present in package:
405                {}
406                "
407        },
408        target_list
409    ))
410}
411
412pub fn find_crate(
413    db: &ide::RootDatabase,
414    vfs: &vfs::Vfs,
415    target: &project_model::TargetData,
416) -> anyhow::Result<hir::Crate> {
417    let crates = hir::Crate::all(db);
418
419    let target_root_path = target.root.as_path();
420
421    let krate = crates.into_iter().find(|krate| {
422        let vfs_path = vfs.file_path(krate.root_file(db));
423        let crate_root_path = vfs_path.as_path().unwrap();
424
425        crate_root_path == target_root_path
426    });
427
428    krate.ok_or_else(|| anyhow::anyhow!("Crate not found"))
429}
430
431pub(crate) fn crate_name(krate: hir::Crate, db: &ide::RootDatabase) -> String {
432    // Obtain the crate's declaration name:
433    let display_name = krate.display_name(db).unwrap().to_string();
434
435    // Since a crate's name may contain `-` we canonicalize it by replacing with `_`:
436    display_name.replace('-', "_")
437}
438
439pub(crate) fn krate(module_def_hir: hir::ModuleDef, db: &ide::RootDatabase) -> Option<hir::Crate> {
440    module(module_def_hir, db).map(|module| module.krate())
441}
442
443pub(crate) fn module(
444    module_def_hir: hir::ModuleDef,
445    db: &ide::RootDatabase,
446) -> Option<hir::Module> {
447    match module_def_hir {
448        hir::ModuleDef::Module(module) => Some(module),
449        module_def_hir => module_def_hir.module(db),
450    }
451}
452
453pub(crate) fn display_name(
454    module_def_hir: hir::ModuleDef,
455    db: &ide::RootDatabase,
456    edition: ide::Edition,
457) -> String {
458    match module_def_hir {
459        hir::ModuleDef::Module(module_hir) => {
460            if module_hir.is_crate_root() {
461                crate_name(module_hir.krate(), db)
462            } else {
463                module_hir
464                    .name(db)
465                    .map(|name| name.display(db, edition).to_string())
466                    .expect("name")
467            }
468        }
469        hir::ModuleDef::Const(const_hir) => {
470            if let Some(name) = const_hir.name(db) {
471                name.display(db, edition).to_string()
472            } else {
473                "_".to_owned()
474            }
475        }
476        module_def_hir => module_def_hir
477            .name(db)
478            .map(|name| name.display(db, edition).to_string())
479            .expect("name"),
480    }
481}
482
483pub(crate) fn display_path(
484    module_def_hir: hir::ModuleDef,
485    db: &ide::RootDatabase,
486    edition: ide::Edition,
487) -> String {
488    path(module_def_hir, db, edition).unwrap_or_else(|| "<anonymous>".to_owned())
489}
490
491pub(crate) fn path(
492    module_def_hir: hir::ModuleDef,
493    db: &ide::RootDatabase,
494    edition: ide::Edition,
495) -> Option<String> {
496    let mut path = String::new();
497
498    let krate = krate(module_def_hir, db);
499
500    // Obtain the crate's name (unless it's a builtin type, which have no crate):
501    if let Some(crate_name) = krate.map(|krate| crate_name(krate, db)) {
502        path.push_str(&crate_name);
503    }
504
505    // Obtain the item's canonicalized name:
506    let relative_path = match module_def_hir {
507        hir::ModuleDef::Function(function_hir) => {
508            if let Some(assoc_item_hir) = function_hir.as_assoc_item(db) {
509                assoc_item_path(assoc_item_hir, db, edition)
510            } else {
511                hir::ModuleDef::Function(function_hir).canonical_path(db, edition)
512            }
513        }
514        hir::ModuleDef::Const(const_hir) => {
515            if let Some(assoc_item_hir) = const_hir.as_assoc_item(db) {
516                assoc_item_path(assoc_item_hir, db, edition)
517            } else {
518                hir::ModuleDef::Const(const_hir).canonical_path(db, edition)
519            }
520        }
521        hir::ModuleDef::TypeAlias(type_alias_hir) => {
522            if let Some(assoc_item_hir) = type_alias_hir.as_assoc_item(db) {
523                assoc_item_path(assoc_item_hir, db, edition)
524            } else {
525                hir::ModuleDef::TypeAlias(type_alias_hir).canonical_path(db, edition)
526            }
527        }
528        hir::ModuleDef::BuiltinType(builtin_type_hir) => {
529            Some(builtin_type_hir.name().display(db, edition).to_string())
530        }
531        module_def_hir => module_def_hir.canonical_path(db, edition),
532    };
533
534    if let Some(relative_path) = relative_path {
535        if !path.is_empty() {
536            path.push_str("::");
537        }
538        path.push_str(&relative_path);
539    }
540
541    if path.is_empty() { None } else { Some(path) }
542}
543
544fn assoc_item_path(
545    assoc_item_hir: hir::AssocItem,
546    db: &ide::RootDatabase,
547    edition: ide::Edition,
548) -> Option<String> {
549    let name = match assoc_item_hir {
550        hir::AssocItem::Function(function_hir) => hir::ModuleDef::Function(function_hir)
551            .name(db)
552            .map(|name| name.display(db, edition).to_string()),
553        hir::AssocItem::Const(const_hir) => hir::ModuleDef::Const(const_hir)
554            .name(db)
555            .map(|name| name.display(db, edition).to_string()),
556        hir::AssocItem::TypeAlias(type_alias_hir) => hir::ModuleDef::TypeAlias(type_alias_hir)
557            .name(db)
558            .map(|name| name.display(db, edition).to_string()),
559    };
560
561    let name = name?;
562
563    let container_path = match assoc_item_hir.container(db) {
564        hir::AssocItemContainer::Trait(trait_hir) => {
565            hir::ModuleDef::Trait(trait_hir).canonical_path(db, edition)
566        }
567        hir::AssocItemContainer::Impl(impl_hir) => impl_hir
568            .self_ty(db)
569            .as_adt()
570            .and_then(|adt_hir| hir::ModuleDef::Adt(adt_hir).canonical_path(db, edition)),
571    };
572
573    let container_path = container_path?;
574
575    Some(format!("{container_path}::{name}"))
576}
577
578// https://github.com/rust-lang/rust-analyzer/blob/36a70b7435c48837018c71576d7bb4e8f763f501/crates/syntax/src/ast/make.rs#L821
579pub(crate) fn parse_ast<N: syntax::AstNode>(text: &str) -> N {
580    let parse = syntax::SourceFile::parse(text, ide::Edition::CURRENT);
581    let node = match parse.tree().syntax().descendants().find_map(N::cast) {
582        Some(it) => it,
583        None => {
584            let node = std::any::type_name::<N>();
585            panic!("Failed to make ast node `{node}` from text {text}")
586        }
587    };
588    let node = node.clone_subtree();
589    assert_eq!(node.syntax().text_range().start(), 0.into());
590    node
591}
592
593pub(crate) fn parse_use_tree(path_expr: &str) -> ast::UseTree {
594    parse_ast(&format!("use {path_expr};"))
595}
596
597pub(crate) fn is_test_function(function: hir::Function, db: &ide::RootDatabase) -> bool {
598    let attrs = function.attrs(db);
599    let key = hir::Symbol::intern("test");
600    attrs.by_key(key).exists()
601}
602
603pub fn cfgs(hir: hir::ModuleDef, db: &ide::RootDatabase) -> Vec<cfg::CfgExpr> {
604    let cfg = match cfg(hir, db) {
605        Some(cfg) => cfg,
606        None => return vec![],
607    };
608
609    match cfg {
610        cfg::CfgExpr::Invalid => vec![],
611        cfg @ cfg::CfgExpr::Atom(_) => vec![cfg],
612        cfg::CfgExpr::All(cfgs) => cfgs.to_vec(),
613        cfg @ cfg::CfgExpr::Any(_) => vec![cfg],
614        cfg @ cfg::CfgExpr::Not(_) => vec![cfg],
615    }
616}
617
618pub fn cfg(hir: hir::ModuleDef, db: &ide::RootDatabase) -> Option<cfg::CfgExpr> {
619    match hir {
620        hir::ModuleDef::Module(r#mod) => r#mod.attrs(db).cfg(),
621        hir::ModuleDef::Function(r#fn) => r#fn.attrs(db).cfg(),
622        hir::ModuleDef::Adt(adt) => adt.attrs(db).cfg(),
623        hir::ModuleDef::Variant(r#variant) => r#variant.attrs(db).cfg(),
624        hir::ModuleDef::Const(r#const) => r#const.attrs(db).cfg(),
625        hir::ModuleDef::Static(r#static) => r#static.attrs(db).cfg(),
626        hir::ModuleDef::Trait(r#trait) => r#trait.attrs(db).cfg(),
627        hir::ModuleDef::TraitAlias(trait_type) => trait_type.attrs(db).cfg(),
628        hir::ModuleDef::TypeAlias(type_alias) => type_alias.attrs(db).cfg(),
629        hir::ModuleDef::BuiltinType(_builtin_type) => None,
630        hir::ModuleDef::Macro(_) => None,
631    }
632}
633
634pub fn cfg_attrs(module_def_hir: hir::ModuleDef, db: &ide::RootDatabase) -> Vec<ItemCfgAttr> {
635    cfgs(module_def_hir, db)
636        .iter()
637        .filter_map(ItemCfgAttr::new)
638        .collect()
639}
640
641pub fn test_attr(module_def_hir: hir::ModuleDef, db: &ide::RootDatabase) -> Option<ItemTestAttr> {
642    let function = match module_def_hir {
643        hir::ModuleDef::Function(function) => function,
644        _ => return None,
645    };
646
647    if is_test_function(function, db) {
648        Some(ItemTestAttr)
649    } else {
650        None
651    }
652}
653
654pub fn module_file(module: hir::Module, db: &ide::RootDatabase, vfs: &vfs::Vfs) -> Option<PathBuf> {
655    let module_source = module.definition_source(db);
656    let is_file_module: bool = match &module_source.value {
657        hir::ModuleSource::SourceFile(_) => true,
658        hir::ModuleSource::Module(_) => false,
659        hir::ModuleSource::BlockExpr(_) => false,
660    };
661
662    if !is_file_module {
663        return None;
664    }
665
666    let file_id = module_source.file_id.original_file(db);
667    let vfs_path = vfs.file_path(file_id.file_id(db));
668    let abs_path = vfs_path.as_path().expect("Could not convert to path");
669
670    let path: &Path = abs_path.as_ref();
671
672    let file_extension = path.extension().and_then(|ext| ext.to_str());
673
674    if file_extension != Some("rs") {
675        return None;
676    }
677
678    Some(path.to_owned())
679}
680
681pub fn moduledef_is_crate(module_def_hir: hir::ModuleDef, _db: &ide::RootDatabase) -> bool {
682    let hir::ModuleDef::Module(module) = module_def_hir else {
683        return false;
684    };
685    module.is_crate_root()
686}
687
688#[allow(dead_code)]
689pub(crate) fn has_test_cfg(attrs: &[ItemCfgAttr]) -> bool {
690    attrs.iter().any(|attr| match attr {
691        ItemCfgAttr::Flag(flag) => flag == "test",
692        _ => false,
693    })
694}
695
696#[allow(dead_code)]
697pub(crate) fn name(
698    module_def_hir: hir::ModuleDef,
699    db: &ide::RootDatabase,
700    edition: ide::Edition,
701) -> String {
702    display_name(module_def_hir, db, edition)
703}
704
705#[allow(dead_code)]
706pub(crate) fn use_tree_matches_item_path(use_tree: &ast::UseTree, path: &str) -> bool {
707    // Check if a use tree matches the given path
708    let use_path = match use_tree.path() {
709        Some(p) => p,
710        None => return false,
711    };
712
713    let segments: Vec<String> = use_path
714        .segments()
715        .filter_map(|seg| seg.name_ref())
716        .map(|name| name.text().to_string())
717        .collect();
718
719    let path_parts: Vec<&str> = path.split("::").collect();
720
721    if segments.len() > path_parts.len() {
722        return false;
723    }
724
725    segments.iter().zip(path_parts.iter()).all(|(a, b)| a == b)
726}
727
728#[cfg(test)]
729mod tests {
730    use super::*;
731    use std::path::PathBuf;
732
733    #[test]
734    fn test_load_options_creation() {
735        let opts = LoadOptions {
736            cfg_test: true,
737            sysroot: false,
738        };
739        assert!(opts.cfg_test);
740        assert!(!opts.sysroot);
741    }
742
743    #[test]
744    fn test_cargo_config_with_test_enabled() {
745        let project_opts = ProjectOptions {
746            lib: true,
747            bin: None,
748            package: None,
749            no_default_features: false,
750            all_features: false,
751            features: vec![],
752            target: None,
753            manifest_path: PathBuf::from("Cargo.toml"),
754        };
755
756        let load_opts = LoadOptions {
757            cfg_test: true,
758            sysroot: false,
759        };
760
761        let _config = cargo_config(&project_opts, &load_opts);
762
763        // Just verify we can create a config with test enabled
764        assert!(load_opts.cfg_test);
765    }
766
767    #[test]
768    fn test_cargo_config_with_test_disabled() {
769        let project_opts = ProjectOptions {
770            lib: true,
771            bin: None,
772            package: None,
773            no_default_features: false,
774            all_features: false,
775            features: vec![],
776            target: None,
777            manifest_path: PathBuf::from("Cargo.toml"),
778        };
779
780        let load_opts = LoadOptions {
781            cfg_test: false,
782            sysroot: false,
783        };
784
785        let _config = cargo_config(&project_opts, &load_opts);
786
787        // Just verify we can create a config with test disabled
788        assert!(!load_opts.cfg_test);
789    }
790
791    #[test]
792    fn test_load_config_creation() {
793        let config = load_config();
794        // We disable load_out_dirs_from_check to prevent hangs
795        assert!(!config.load_out_dirs_from_check);
796    }
797
798    #[test]
799    fn test_parse_use_tree() {
800        let use_tree = parse_use_tree("std::collections::HashMap");
801        assert!(use_tree.path().is_some());
802
803        let path = use_tree.path().unwrap();
804        let segments: Vec<_> = path
805            .segments()
806            .filter_map(|seg| seg.name_ref())
807            .map(|name| name.text().to_string())
808            .collect();
809
810        assert_eq!(segments, vec!["std", "collections", "HashMap"]);
811    }
812
813    #[test]
814    fn test_use_tree_matches_item_path() {
815        let use_tree = parse_use_tree("std::collections");
816        assert!(use_tree_matches_item_path(
817            &use_tree,
818            "std::collections::HashMap"
819        ));
820        assert!(use_tree_matches_item_path(&use_tree, "std::collections"));
821        assert!(!use_tree_matches_item_path(&use_tree, "std::io"));
822
823        let use_tree = parse_use_tree("std");
824        assert!(use_tree_matches_item_path(&use_tree, "std::collections"));
825        assert!(!use_tree_matches_item_path(&use_tree, "core::mem"));
826    }
827
828    #[test]
829    fn test_parse_ast_function() {
830        use syntax::ast::HasName;
831        let func: ast::Fn = parse_ast("fn test() {}");
832        assert_eq!(func.name().unwrap().text(), "test");
833    }
834
835    #[test]
836    fn test_has_test_cfg() {
837        let attrs = vec![
838            ItemCfgAttr::Flag("test".to_string()),
839            ItemCfgAttr::KeyValue("target_os".to_string(), "linux".to_string()),
840        ];
841        assert!(has_test_cfg(&attrs));
842
843        let attrs = vec![ItemCfgAttr::KeyValue(
844            "target_os".to_string(),
845            "linux".to_string(),
846        )];
847        assert!(!has_test_cfg(&attrs));
848    }
849
850    #[test]
851    fn test_crate_name_canonicalization() {
852        // This would need a mock database to test properly
853        // For now just test the string replacement logic
854        let display_name = "my-crate-name".to_string();
855        let canonicalized = display_name.replace('-', "_");
856        assert_eq!(canonicalized, "my_crate_name");
857    }
858}