use rustdoc_types::ItemKind;
use std::path::PathBuf;
use crate::{
CrateName, Navigator, RustdocData,
sources::{CrateProvenance, LocalSource, StdSource},
};
fn get_fixture_crate_path() -> PathBuf {
PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../tests/fixture-crate")
}
fn test_navigator() -> Navigator {
Navigator::default()
.with_local_source(LocalSource::load(&get_fixture_crate_path()).ok())
.with_std_source(StdSource::from_rustup())
}
fn resolve<'a>(nav: &'a Navigator, path: &str) -> crate::DocRef<'a, rustdoc_types::Item> {
nav.resolve_path(path, &mut vec![])
.unwrap_or_else(|| panic!("failed to resolve {path:?}"))
}
#[test]
fn discriminated_path_values() {
let nav = test_navigator();
let cases = [
("crate::TestStruct", "fixture-crate::struct@TestStruct"),
("crate::TestTrait", "fixture-crate::trait@TestTrait"),
("crate::test_function", "fixture-crate::fn@test_function"),
("crate::submodule", "fixture-crate::mod@submodule"),
("crate::TEST_CONSTANT", "fixture-crate::const@TEST_CONSTANT"),
("crate::TEST_STATIC", "fixture-crate::static@TEST_STATIC"),
("crate::GenericEnum", "fixture-crate::enum@GenericEnum"),
(
"crate::namespace_collisions",
"fixture-crate::mod@namespace_collisions",
),
];
for (path, expected_disc) in cases {
let item = resolve(&nav, path);
let disc = item
.discriminated_path()
.unwrap_or_else(|| panic!("no discriminated_path for {path:?}"));
assert_eq!(disc, expected_disc, "wrong discriminated_path for {path:?}");
}
}
#[test]
fn discriminated_path_round_trips() {
let nav = test_navigator();
let paths = [
"crate::TestStruct",
"crate::TestTrait",
"crate::test_function",
"crate::submodule",
"crate::TEST_CONSTANT",
"crate::GenericEnum",
"crate::submodule::SubStruct",
"crate::namespace_collisions",
];
for path in paths {
let item = resolve(&nav, path);
let disc_path = item
.discriminated_path()
.unwrap_or_else(|| panic!("no discriminated_path for {path:?}"));
let round_tripped = nav
.resolve_path(&disc_path, &mut vec![])
.unwrap_or_else(|| panic!("discriminated path {disc_path:?} failed to resolve"));
assert_eq!(
item, round_tripped,
"round-trip mismatch for {path:?} (discriminated: {disc_path:?})"
);
}
}
#[test]
fn discriminated_path_round_trips_method() {
let nav = test_navigator();
let item = resolve(&nav, "crate::submodule::SubStruct::new");
let disc_path = item
.discriminated_path()
.expect("discriminated_path should work for methods once the upstream bug is fixed");
let round_tripped = nav
.resolve_path(&disc_path, &mut vec![])
.unwrap_or_else(|| panic!("discriminated path {disc_path:?} failed to resolve"));
assert_eq!(item, round_tripped);
}
#[test]
fn discriminator_resolves_module_function_collision() {
let nav = test_navigator();
let by_mod = resolve(&nav, "crate::namespace_collisions::mod@both");
let by_fn = resolve(&nav, "crate::namespace_collisions::fn@both");
assert_eq!(
by_mod.kind(),
ItemKind::Module,
"mod@both should be a module"
);
assert_eq!(
by_fn.kind(),
ItemKind::Function,
"fn@both should be a function"
);
assert_ne!(
by_mod, by_fn,
"mod@both and fn@both should be different items"
);
}
#[test]
fn discriminated_path_round_trips_through_collision() {
let nav = test_navigator();
for disc_path in [
"fixture-crate::namespace_collisions::mod@both",
"fixture-crate::namespace_collisions::fn@both",
] {
let item = nav
.resolve_path(disc_path, &mut vec![])
.unwrap_or_else(|| panic!("failed to resolve {disc_path:?}"));
let generated = item
.discriminated_path()
.unwrap_or_else(|| panic!("no discriminated_path for {disc_path:?}"));
assert_eq!(
generated, disc_path,
"discriminated_path should reproduce the qualified path"
);
}
}
#[test]
fn discriminated_path_round_trips_method_on_private_module_struct() {
let nav = test_navigator();
let struct_item = resolve(&nav, "crate::ReachableViaPrivateModule");
let method = struct_item
.child_items()
.find(|c| c.name() == Some("private_module_method"))
.expect("private_module_method not found");
let disc = method
.discriminated_path()
.expect("discriminated_path should work: parent was set during child_items traversal");
assert_eq!(
disc,
"fixture-crate::private_detail::ReachableViaPrivateModule::fn@private_module_method"
);
let round_tripped = nav
.resolve_path(&disc, &mut vec![])
.unwrap_or_else(|| panic!("failed to resolve discriminated path {disc:?}"));
assert_eq!(method, round_tripped);
}
#[test]
fn private_module_path_resolves_via_index() {
let nav = test_navigator();
let via_reexport = resolve(&nav, "crate::ReachableViaPrivateModule");
let via_private_path = resolve(
&nav,
"fixture-crate::private_detail::ReachableViaPrivateModule",
);
assert_eq!(via_reexport.kind(), ItemKind::Struct, "should be a struct");
assert_eq!(
via_reexport, via_private_path,
"re-export and private-module path should resolve to the same item"
);
}
#[cfg(test)]
fn synth_crate(
name: &str,
root_id: u32,
modules: Vec<(u32, Option<&str>, Vec<u32>)>,
items: Vec<rustdoc_types::Item>,
) -> RustdocData {
use rustdoc_types::{Crate, Id, Item, ItemEnum, ItemSummary, Module, Target, Visibility};
use std::collections::HashMap;
let mut index: HashMap<Id, Item, rustc_hash::FxBuildHasher> = Default::default();
let mut paths: HashMap<Id, ItemSummary, rustc_hash::FxBuildHasher> = Default::default();
for (id, mod_name, children) in modules {
let ids: Vec<Id> = children.into_iter().map(Id).collect();
index.insert(
Id(id),
Item {
id: Id(id),
crate_id: 0,
name: mod_name.map(str::to_owned),
span: None,
visibility: Visibility::Public,
docs: None,
links: Default::default(),
attrs: vec![],
deprecation: None,
inner: ItemEnum::Module(Module {
is_crate: id == root_id,
items: ids,
is_stripped: false,
}),
},
);
let summary_path = if id == root_id {
vec![name.to_string()]
} else {
vec![
name.to_string(),
mod_name.expect("non-root module needs a name").to_string(),
]
};
paths.insert(
Id(id),
ItemSummary {
crate_id: 0,
path: summary_path,
kind: rustdoc_types::ItemKind::Module,
},
);
}
for item in items {
index.insert(item.id, item);
}
RustdocData {
crate_data: Crate {
root: Id(root_id),
crate_version: None,
includes_private: false,
index,
paths,
external_crates: Default::default(),
target: Target {
triple: String::new(),
target_features: vec![],
},
format_version: rustdoc_types::FORMAT_VERSION,
},
name: name.to_owned(),
provenance: CrateProvenance::DocsRs,
fs_path: PathBuf::new(),
version: None,
path_to_id: std::collections::HashMap::new(),
}
}
#[cfg(test)]
fn synth_item(id: u32, name: Option<&str>, inner: rustdoc_types::ItemEnum) -> rustdoc_types::Item {
use rustdoc_types::{Id, Item, Visibility};
Item {
id: Id(id),
crate_id: 0,
name: name.map(str::to_owned),
span: None,
visibility: Visibility::Public,
docs: None,
links: Default::default(),
attrs: vec![],
deprecation: None,
inner,
}
}
#[cfg(test)]
fn synth_unit_struct() -> rustdoc_types::ItemEnum {
use rustdoc_types::{Generics, ItemEnum, Struct, StructKind};
ItemEnum::Struct(Struct {
kind: StructKind::Unit,
generics: Generics {
params: vec![],
where_predicates: vec![],
},
impls: vec![],
})
}
#[cfg(test)]
fn synth_use(id: u32, name: &str, source: &str, target: Option<u32>) -> rustdoc_types::Item {
use rustdoc_types::{ItemEnum, Use};
synth_item(
id,
None,
ItemEnum::Use(Use {
source: source.to_owned(),
name: name.to_owned(),
id: target.map(rustdoc_types::Id),
is_glob: false,
}),
)
}
#[cfg(test)]
fn build_prefix_test_navigator() -> Navigator {
use rustdoc_types::{Id, ItemSummary};
let mut target = synth_crate(
"target_crate",
1,
vec![(1, None, vec![100])],
vec![synth_item(100, Some("TargetStruct"), synth_unit_struct())],
);
target.crate_data.paths.insert(
Id(100),
ItemSummary {
crate_id: 0,
path: vec!["target_crate".into(), "TargetStruct".into()],
kind: rustdoc_types::ItemKind::Struct,
},
);
const FOREIGN: Option<u32> = Some(100);
let home_items = vec![
synth_item(50, Some("RootTarget"), synth_unit_struct()),
synth_use(20, "BaseAlias", "target_crate::TargetStruct", FOREIGN),
synth_use(21, "AbsAlias", "target_crate::TargetStruct", FOREIGN),
synth_use(22, "CrateAlias", "crate::RootTarget", FOREIGN),
synth_use(23, "SelfAlias", "self::RootTarget", FOREIGN),
synth_use(24, "SuperAlias", "super::RootTarget", FOREIGN),
synth_item(60, Some("InnerTarget"), synth_unit_struct()),
synth_use(30, "AbsAliasInner", "target_crate::TargetStruct", FOREIGN),
synth_use(31, "CrateAliasInner", "crate::RootTarget", FOREIGN),
synth_use(32, "SelfAliasInner", "self::InnerTarget", FOREIGN),
synth_use(33, "SuperAliasInner", "super::RootTarget", FOREIGN),
];
let home = synth_crate(
"home_crate",
1,
vec![
(1, None, vec![50, 20, 21, 22, 23, 24, 2]),
(2, Some("inner"), vec![60, 30, 31, 32, 33]),
],
home_items,
);
let nav = Navigator::default();
nav.working_set
.insert(CrateName::from("home_crate"), Box::new(Some(home)));
nav.working_set
.insert(CrateName::from("target_crate"), Box::new(Some(target)));
nav
}
#[test]
fn cross_crate_prefix_resolves_at_root() {
let nav = build_prefix_test_navigator();
let root = nav
.load_crate("home_crate", &semver::VersionReq::STAR)
.expect("home_crate pre-populated")
.root_item(&nav);
let names: Vec<&str> = root.child_items().filter_map(|c| c.name()).collect();
assert_eq!(
names,
vec![
"RootTarget",
"BaseAlias",
"AbsAlias",
"CrateAlias",
"SelfAlias",
"inner",
],
);
}
#[test]
fn cross_crate_prefix_resolves_in_nested_module() {
let nav = build_prefix_test_navigator();
let inner = nav
.resolve_path("home_crate::inner", &mut vec![])
.expect("home_crate::inner should resolve");
let names: Vec<&str> = inner.child_items().filter_map(|c| c.name()).collect();
assert_eq!(
names,
vec![
"InnerTarget",
"AbsAliasInner",
"CrateAliasInner",
"SelfAliasInner",
"SuperAliasInner",
],
);
}
#[test]
fn iterator_skips_unresolvable_use_items() {
use rustdoc_types::{
Crate, Generics, Id, Item, ItemEnum, Module, Struct, StructKind, Target, Use, Visibility,
};
use std::collections::HashMap;
fn item(id: u32, name: Option<&str>, inner: ItemEnum) -> Item {
Item {
id: Id(id),
crate_id: 0,
name: name.map(str::to_owned),
span: None,
visibility: Visibility::Public,
docs: None,
links: Default::default(),
attrs: vec![],
deprecation: None,
inner,
}
}
fn unit_struct() -> ItemEnum {
ItemEnum::Struct(Struct {
kind: StructKind::Unit,
generics: Generics {
params: vec![],
where_predicates: vec![],
},
impls: vec![],
})
}
let root_id = Id(1);
let broken_use_a = Id(10);
let valid_struct_a = Id(11);
let broken_use_b = Id(12);
let valid_struct_b = Id(13);
let mut index: HashMap<Id, Item, rustc_hash::FxBuildHasher> = Default::default();
index.insert(
root_id,
item(
root_id.0,
Some("fake_crate"),
ItemEnum::Module(Module {
is_crate: true,
items: vec![broken_use_a, valid_struct_a, broken_use_b, valid_struct_b],
is_stripped: false,
}),
),
);
index.insert(
broken_use_a,
item(
broken_use_a.0,
None,
ItemEnum::Use(Use {
source: "___definitely_not_a_real_crate___::Thing".into(),
name: "Thing".into(),
id: Some(Id(9_999)), is_glob: false,
}),
),
);
index.insert(
valid_struct_a,
item(valid_struct_a.0, Some("KeepA"), unit_struct()),
);
index.insert(
broken_use_b,
item(
broken_use_b.0,
None,
ItemEnum::Use(Use {
source: "___also_not_a_real_crate___::Other".into(),
name: "Other".into(),
id: Some(Id(9_998)),
is_glob: false,
}),
),
);
index.insert(
valid_struct_b,
item(valid_struct_b.0, Some("KeepB"), unit_struct()),
);
let crate_data = Crate {
root: root_id,
crate_version: None,
includes_private: false,
index,
paths: Default::default(),
external_crates: Default::default(),
target: Target {
triple: String::new(),
target_features: vec![],
},
format_version: rustdoc_types::FORMAT_VERSION,
};
let data = RustdocData {
crate_data,
name: "fake_crate".into(),
provenance: CrateProvenance::DocsRs,
fs_path: PathBuf::new(),
version: None,
path_to_id: HashMap::new(),
};
let nav = Navigator::default();
nav.working_set
.insert(CrateName::from("fake_crate"), Box::new(Some(data)));
let root = nav
.load_crate("fake_crate", &semver::VersionReq::STAR)
.expect("pre-populated crate should be loadable")
.root_item(&nav);
let names: Vec<&str> = root.child_items().filter_map(|c| c.name()).collect();
assert_eq!(
names,
vec!["KeepA", "KeepB"],
"unresolvable `Use` items should be skipped, not terminate the iterator"
);
}