~ireas/rustdoc-parser-rs

95a944ae36aeb785ee4260a4babef8b66c490759 — Robin Krahl 6 months ago a63a9e0 master
Add support for parsing modules

This patch adds the Crate::open_item method that opens the documentation
for an item.  It is so far only implemented for modules.
M src/html.rs => src/html.rs +91 -1
@@ 44,7 44,7 @@ mod util;

use std::{convert::TryFrom, error, fmt, fs, io, path};

use crate::{Error, Fqn, Ident, Item};
use crate::{Error, Fqn, FullItem, Ident, Item, ItemType, Members};

/// A parser for HTML documentation generated by rustdoc.
///


@@ 133,6 133,26 @@ impl Crate {
            path: path.into(),
        }
    }

    fn item_path(&self, fqn: &Fqn, ty: ItemType) -> Result<path::PathBuf, ParseError> {
        if fqn[0] != self.ident {
            return Err(ParseError::ItemNotFound(fqn.clone()));
        }
        let mut path = self.path.clone();
        if ty == ItemType::Module {
            for ident in fqn.iter().skip(1) {
                path.push(ident.as_str());
            }
            path.push("index.html");
        } else {
            unimplemented!();
        }
        if path.exists() {
            Ok(path)
        } else {
            Err(ParseError::ItemNotFound(fqn.clone()))
        }
    }
}

impl crate::Crate for Crate {


@@ 151,6 171,20 @@ impl crate::Crate for Crate {
        modules.push(root);
        Ok(modules)
    }

    fn open_item(&self, fqn: Fqn, ty: ItemType) -> Result<FullItem, Error> {
        let path = self.item_path(&fqn, ty)?;
        let item = Document::from_file(path)?.parse_item(ty)?;
        if item.fqn == fqn {
            Ok(item)
        } else {
            Err(ParseError::UnexpectedItem {
                expected: fqn,
                actual: item.fqn,
            }
            .into())
        }
    }
}

fn extend_modules(modules: &mut Vec<Fqn>, parent: &Fqn, path: &path::Path) -> Result<(), Error> {


@@ 249,6 283,45 @@ impl Document {
        }
        Ok(items)
    }

    /// Parses the documentation for an item with the given type.
    ///
    /// In the standard rustdoc output, the item documentation is placed in the `index.html` for
    /// modules and in the `type.name.html` file for other items with the type `type` and the name
    /// `name`.
    ///
    /// # Example
    ///
    /// ```no_run
    /// let document = rustdoc_parser::html::Document::from_file("target/doc/log/index.html").unwrap();
    /// let module = document.parse_item(rustdoc_parser::ItemType::Module).unwrap();
    /// ```
    pub fn parse_item(&self, ty: ItemType) -> Result<FullItem, Error> {
        let main = self.main()?;
        let fqn = Fqn::try_from(main.fqn()?)?;
        let members = match ty {
            ItemType::Module => {
                let mut reexports = Vec::new();
                let mut items = Vec::new();
                for group in main.module_items()? {
                    let ty = group.ty()?;
                    if ty == ItemType::Import {
                        for item in group.items()? {
                            reexports.push(item.import()?);
                        }
                    } else {
                        for item in group.items()? {
                            let fqn = fqn.clone().child(item.ident()?);
                            items.push(Item { fqn, ty });
                        }
                    }
                }
                Some(Members::Module { reexports, items })
            }
            _ => None,
        };
        Ok(FullItem { fqn, ty, members })
    }
}

impl From<dom::Document> for Document {


@@ 270,6 343,15 @@ impl TryFrom<kuchiki::NodeRef> for Document {
pub enum ParseError {
    /// The HTML document has an invalid structure.
    InvalidStructure(String),
    /// The item with the given name was not found.
    ItemNotFound(Fqn),
    /// The documentation for an unexpected item was read.
    UnexpectedItem {
        /// The name of the expected item.
        expected: Fqn,
        /// The name of the actual item.
        actual: Fqn,
    },
}

impl error::Error for ParseError {}


@@ 280,6 362,14 @@ impl fmt::Display for ParseError {
            Self::InvalidStructure(info) => {
                write!(f, "the HTML document has an invalid structure: {}", info)
            }
            Self::ItemNotFound(fqn) => write!(f, "the item with the name {} was not found", fqn),
            Self::UnexpectedItem { expected, actual } => {
                write!(
                    f,
                    "expected to read documentation for {}, found {}",
                    expected, actual
                )
            }
        }
    }
}

M src/html/dom.rs => src/html/dom.rs +21 -0
@@ 87,6 87,15 @@ impl Element {
        self.0.as_node().descendants().elements().map(From::from)
    }

    pub fn next_sibling(&self) -> Option<Element> {
        self.0
            .as_node()
            .following_siblings()
            .elements()
            .next()
            .map(From::from)
    }

    pub fn text_contents(&self) -> String {
        self.0.text_contents()
    }


@@ 128,6 137,18 @@ impl Element {
            )))
        }
    }

    pub fn ensure_class(&self, class: &str) -> Result<(), ParseError> {
        if self.has_class(class) {
            Ok(())
        } else {
            Err(err(format!(
                "expected element with class {}, got {:?}",
                class,
                self.classes()
            )))
        }
    }
}

impl From<kuchiki::NodeDataRef<kuchiki::ElementData>> for Element {

M src/html/elements.rs => src/html/elements.rs +160 -3
@@ 7,8 7,9 @@ use super::{
    dom::{Document, Element},
    util, ParseError,
};
use crate::{Fqn, Ident, ItemType, ParseIdentError};
use crate::{Error, Ident, Import, ItemType, ParseIdentError};

#[derive(Debug)]
pub struct Html(Element);

impl Html {


@@ 41,6 42,7 @@ impl TryFrom<Element> for Html {
    }
}

#[derive(Debug)]
pub struct Main(Element);

impl Main {


@@ 51,6 53,22 @@ impl Main {
            .map(IndexGroup::try_from)
            .collect()
    }

    pub fn module_items(&self) -> Result<Vec<ModuleItemsHeading>, ParseError> {
        self.0
            .children()
            .filter(|element| element.name() == "h2")
            .map(ModuleItemsHeading::try_from)
            .collect()
    }

    pub fn fqn(&self) -> Result<Fqn, ParseError> {
        self.0
            .children()
            .find(|element| element.has_class("fqn"))
            .ok_or_else(|| ParseError::InvalidStructure("missing fqn for main".to_owned()))
            .and_then(Fqn::try_from)
    }
}

impl TryFrom<Element> for Main {


@@ 63,6 81,144 @@ impl TryFrom<Element> for Main {
    }
}

#[derive(Debug)]
pub struct Fqn(Element);

impl TryFrom<Element> for Fqn {
    type Error = ParseError;

    fn try_from(element: Element) -> Result<Self, Self::Error> {
        element.ensure_name("h1")?;
        element.ensure_class("fqn")?;
        Ok(Self(element))
    }
}

impl TryFrom<Fqn> for crate::Fqn {
    type Error = Error;

    fn try_from(fqn: Fqn) -> Result<Self, Self::Error> {
        let in_band =
            fqn.0.children().next().ok_or_else(|| {
                ParseError::InvalidStructure("missing in-band for fqn".to_owned())
            })?;
        let in_band = FqnInBand::try_from(in_band)?;
        in_band.fqn().map_err(From::from)
    }
}

#[derive(Debug)]
pub struct FqnInBand(Element);

impl FqnInBand {
    fn fqn(&self) -> Result<crate::Fqn, ParseIdentError> {
        let fqn: Vec<Ident> = self
            .0
            .children()
            .filter(|element| element.name() == "a")
            .map(|element| element.text_contents().parse())
            .collect::<Result<_, _>>()?;
        crate::Fqn::try_from(fqn)
    }
}

impl TryFrom<Element> for FqnInBand {
    type Error = ParseError;

    fn try_from(element: Element) -> Result<Self, Self::Error> {
        element.ensure_name("span")?;
        element.ensure_class("in-band")?;
        Ok(Self(element))
    }
}

#[derive(Debug)]
pub struct ModuleItemsHeading(Element);

impl ModuleItemsHeading {
    pub fn ty(&self) -> Result<ItemType, ParseError> {
        let id = self.0.id().ok_or_else(|| {
            ParseError::InvalidStructure("no id for module items heading".to_owned())
        })?;
        util::parse_item_type_from_module_group_id(&id)
    }

    pub fn items(&self) -> Result<Vec<ModuleItem>, ParseError> {
        self.0
            .next_sibling()
            .ok_or_else(|| {
                ParseError::InvalidStructure(
                    "module items heading does not have a sibling element".to_owned(),
                )
            })
            .and_then(ModuleItemsTable::try_from)?
            .items()
    }
}

impl TryFrom<Element> for ModuleItemsHeading {
    type Error = ParseError;

    fn try_from(element: Element) -> Result<Self, Self::Error> {
        element.ensure_name("h2")?;
        element.ensure_class("section-header")?;
        Ok(Self(element))
    }
}

#[derive(Debug)]
pub struct ModuleItemsTable(Element);

impl ModuleItemsTable {
    fn items(&self) -> Result<Vec<ModuleItem>, ParseError> {
        self.0
            .children()
            .filter(|element| element.has_class("item-left"))
            .map(ModuleItem::try_from)
            .collect()
    }
}

impl TryFrom<Element> for ModuleItemsTable {
    type Error = ParseError;

    fn try_from(element: Element) -> Result<Self, Self::Error> {
        element.ensure_name("div")?;
        element.ensure_class("item-table")?;
        Ok(Self(element))
    }
}

#[derive(Debug)]
pub struct ModuleItem(Element);

impl ModuleItem {
    pub fn ident(&self) -> Result<Ident, ParseIdentError> {
        self.0
            .children()
            .filter(|element| element.name() == "a")
            .map(|element| element.text_contents())
            .next()
            .unwrap_or_default()
            .parse()
    }

    pub fn import(&self) -> Result<Import, Error> {
        util::parse_import(&self.0.text_contents())
    }
}

impl TryFrom<Element> for ModuleItem {
    type Error = ParseError;

    fn try_from(element: Element) -> Result<Self, Self::Error> {
        element.ensure_name("div")?;
        element.ensure_class("item-left")?;
        Ok(Self(element))
    }
}

#[derive(Debug)]
pub struct IndexGroup(Element);

impl IndexGroup {


@@ 101,16 257,17 @@ impl TryFrom<Element> for IndexGroup {
    }
}

#[derive(Debug)]
pub struct IndexItem(Element);

impl IndexItem {
    pub fn fqn(&self, krate: Ident) -> Result<Fqn, ParseIdentError> {
    pub fn fqn(&self, krate: Ident) -> Result<crate::Fqn, ParseIdentError> {
        let mut fqn = vec1::Vec1::new(krate);
        // TODO: check if empty?
        for part in self.0.text_contents().split("::") {
            fqn.push(Ident::try_from(part)?);
        }
        Ok(Fqn::from(fqn))
        Ok(crate::Fqn::from(fqn))
    }
}


M src/html/util.rs => src/html/util.rs +128 -4
@@ 2,11 2,11 @@
// SPDX-License-Identifier: Apache-2.0 or MIT

use super::ParseError;
use crate::ItemType;
use crate::{Error, Import, ItemType};

pub fn parse_item_type_from_index_group_class(id: &str) -> Result<ItemType, ParseError> {
pub fn parse_item_type_from_index_group_class(class: &str) -> Result<ItemType, ParseError> {
    // https://github.com/rust-lang/rust/blob/3e3890c9d4064253aaa8c51f5d5458d2dc6dab77/src/librustdoc/html/render/mod.rs#L329
    match id {
    match class {
        "structs" => Ok(ItemType::Struct),
        "enums" => Ok(ItemType::Enum),
        "unions" => Ok(ItemType::Union),


@@ 22,8 22,132 @@ pub fn parse_item_type_from_index_group_class(id: &str) -> Result<ItemType, Pars
        "statics" => Ok(ItemType::Static),
        "constants" => Ok(ItemType::Constant),
        _ => Err(ParseError::InvalidStructure(format!(
            "unexpected item group id {}",
            "unexpected item group class {}",
            class
        ))),
    }
}

pub fn parse_item_type_from_module_group_id(id: &str) -> Result<ItemType, ParseError> {
    // https://github.com/rust-lang/rust/blob/3e3890c9d4064253aaa8c51f5d5458d2dc6dab77/src/librustdoc/html/render/mod.rs#L2426
    match id {
        "reexports" => Ok(ItemType::Import),
        "modules" => Ok(ItemType::Module),
        "structs" => Ok(ItemType::Struct),
        "unions" => Ok(ItemType::Union),
        "enums" => Ok(ItemType::Enum),
        "functions" => Ok(ItemType::Function),
        "types" => Ok(ItemType::Typedef),
        "statics" => Ok(ItemType::Static),
        "constants" => Ok(ItemType::Constant),
        "traits" => Ok(ItemType::Trait),
        "impls" => Ok(ItemType::Impl),
        "tymethods" => Ok(ItemType::TyMethod),
        "methods" => Ok(ItemType::Method),
        "fields" => Ok(ItemType::StructField),
        "variants" => Ok(ItemType::Variant),
        "macros" => Ok(ItemType::Macro),
        "primitives" => Ok(ItemType::Primitive),
        "associated-types" => Ok(ItemType::AssocType),
        "associated-consts" => Ok(ItemType::AssocConst),
        "foreign-types" => Ok(ItemType::ForeignType),
        "keywords" => Ok(ItemType::Keyword),
        "opaque-types" => Ok(ItemType::OpaqueTy),
        "attributes" => Ok(ItemType::ProcAttribute),
        "derives" => Ok(ItemType::ProcDerive),
        "trait-aliases" => Ok(ItemType::TraitAlias),
        _ => Err(ParseError::InvalidStructure(format!(
            "unexpected module group id {}",
            id
        ))),
    }
}

pub fn parse_import(s: &str) -> Result<Import, Error> {
    const AS: &str = " as ";
    let s = s.strip_prefix("pub use ").ok_or_else(|| {
        ParseError::InvalidStructure("import does not start with pub use".to_owned())
    })?;
    let s = s
        .strip_suffix(';')
        .ok_or_else(|| ParseError::InvalidStructure("import does not end with ;".to_owned()))?;
    if let Some(n) = s.find(AS) {
        // "pub use source as name;"
        let fqn = &s[..n];
        let name = &s[n + AS.len()..];
        Ok(Import {
            source: fqn.parse()?,
            name: name.parse()?,
            glob: false,
        })
    } else {
        let (source, glob) = if let Some(s) = s.strip_suffix("::*") {
            // "pub use source::*;"
            (s, true)
        } else {
            // "pub use source;"
            (s, false)
        };
        let source: crate::Fqn = source.parse()?;
        let name = source.ident().clone();
        Ok(Import { source, name, glob })
    }
}

#[cfg(test)]
mod tests {
    use super::parse_import;
    use crate::Import;

    #[test]
    fn test_parse_import() {
        assert_eq!(
            parse_import("pub use source as name;").unwrap(),
            Import {
                source: "source".parse().unwrap(),
                name: "name".parse().unwrap(),
                glob: false,
            },
        );
        assert_eq!(
            parse_import("pub use multi::part::path as name;").unwrap(),
            Import {
                source: "multi::part::path".parse().unwrap(),
                name: "name".parse().unwrap(),
                glob: false,
            },
        );
        assert_eq!(
            parse_import("pub use source;").unwrap(),
            Import {
                source: "source".parse().unwrap(),
                name: "source".parse().unwrap(),
                glob: false,
            },
        );
        assert_eq!(
            parse_import("pub use multi::part::path;").unwrap(),
            Import {
                source: "multi::part::path".parse().unwrap(),
                name: "path".parse().unwrap(),
                glob: false,
            },
        );
        assert_eq!(
            parse_import("pub use source::*;").unwrap(),
            Import {
                source: "source".parse().unwrap(),
                name: "source".parse().unwrap(),
                glob: true,
            },
        );
        assert_eq!(
            parse_import("pub use multi::part::path::*;").unwrap(),
            Import {
                source: "multi::part::path".parse().unwrap(),
                name: "path".parse().unwrap(),
                glob: true,
            },
        );
    }
}

M src/json.rs => src/json.rs +81 -1
@@ 52,7 52,7 @@ mod util;

use std::{collections, convert::TryFrom, error, fmt, fs, io, path};

use crate::{Error, Fqn, Ident, Item, ItemType};
use crate::{Error, Fqn, FullItem, Ident, Import, Item, ItemType, Members};
use util::{CrateExt, ItemExt};

/// A parser for JSON documentation generated by rustdoc.


@@ 186,6 186,70 @@ impl Crate {
            })
            .transpose()
    }

    fn find_item(&self, fqn: &Fqn, ty: ItemType) -> Result<&model::Item, ParseError> {
        let root = self.krate.get(&self.krate.root)?;
        if root.name.as_deref() == Some(fqn.krate().as_str()) {
            self.find_child(root, &fqn[1..], ty)
                .ok_or_else(|| ParseError::ItemNotFound(fqn.clone()))
        } else {
            Err(ParseError::ItemNotFound(fqn.clone()))
        }
    }

    fn find_child<'a>(
        &'a self,
        parent: &'a model::Item,
        name: &[Ident],
        ty: ItemType,
    ) -> Option<&'a model::Item> {
        if let Some(first) = name.first() {
            let matches: Vec<_> = parent
                .members()
                .flat_map(|id| self.krate.get(id))
                .filter(|item| item.name.as_deref() == Some(first.as_str()))
                .filter(|item| {
                    if name.len() == 1 {
                        item.ty() == ty
                    } else {
                        item.ty() == ItemType::Module
                    }
                })
                .collect();
            if matches.len() == 1 {
                self.find_child(matches[0], &name[1..], ty)
            } else {
                None
            }
        } else {
            Some(parent)
        }
    }

    fn reexports(&self, item: &model::Item) -> Result<Vec<Import>, Error> {
        item.members()
            .flat_map(|id| self.krate.get(id))
            .flat_map(|item| match &item.inner {
                model::ItemEnum::Import(import) => Some(import),
                _ => None,
            })
            .map(|import| {
                Ok(Import {
                    source: import.source.parse()?,
                    name: import.name.parse()?,
                    glob: import.glob,
                })
            })
            .collect()
    }

    fn module_items(&self, item: &model::Item) -> Result<Vec<Item>, Error> {
        item.members()
            .flat_map(|id| self.krate.get(id))
            .filter(|item| item.name.is_some())
            .map(|item| self.item(&item.id))
            .collect()
    }
}

impl crate::Crate for Crate {


@@ 223,6 287,18 @@ impl crate::Crate for Crate {
            .map(|item| self.fqn(item))
            .collect()
    }

    fn open_item(&self, fqn: Fqn, ty: ItemType) -> Result<FullItem, Error> {
        let item = self.find_item(&fqn, ItemType::Module)?;
        let members = match ty {
            ItemType::Module => Some(Members::Module {
                reexports: self.reexports(item)?,
                items: self.module_items(item)?,
            }),
            _ => None,
        };
        Ok(FullItem { fqn, ty, members })
    }
}

impl TryFrom<model::Crate> for Crate {


@@ 255,6 331,8 @@ impl TryFrom<model::Crate> for Crate {
pub enum ParseError {
    /// The JSON deserialization failed.
    DeserializationError(serde_json::Error),
    /// The item with the given name could not be found.
    ItemNotFound(Fqn),
    /// The item with this ID is missing.
    MissingItem(String),
    /// The item with this ID has multiple parents.


@@ 275,6 353,7 @@ impl error::Error for ParseError {
    fn source(&self) -> Option<&(dyn error::Error + 'static)> {
        match self {
            Self::DeserializationError(error) => Some(error),
            Self::ItemNotFound(_) => None,
            Self::MissingItem(_) => None,
            Self::MultipleParents(_) => None,
            Self::UnnamedItem(_) => None,


@@ 287,6 366,7 @@ impl fmt::Display for ParseError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::DeserializationError(_) => "failed to deserialize the JSON crate data".fmt(f),
            Self::ItemNotFound(fqn) => write!(f, "item {} not found", fqn),
            Self::MissingItem(item) => write!(f, "missing item {} in crate", item),
            Self::MultipleParents(item) => write!(f, "item {} has multiple parents", item),
            Self::UnnamedItem(item) => write!(f, "item {} does not have a name", item),

M src/model.rs => src/model.rs +54 -0
@@ 81,6 81,23 @@ pub trait Crate: fmt::Debug {
    /// }
    /// ```
    fn modules(&self) -> Result<Vec<Fqn>, Error>;

    /// Opens the documentation for the given item with the given item type in this crate.
    ///
    /// # Example
    ///
    /// ```no_run
    /// let dir = rustdoc_parser::Dir::open("target/doc");
    /// for krate in dir.list_crates().expect("failed to list crates") {
    ///     let krate = dir.open_crate(krate).expect("failed to open crate documentation");
    ///     for module in krate.modules().unwrap() {
    ///         println!("{}", module);
    ///         let item = krate.open_item(module, rustdoc_parser::ItemType::Module).unwrap();
    ///         println!("{:?}", item.members);
    ///     }
    /// }
    /// ```
    fn open_item(&self, fqn: Fqn, ty: ItemType) -> Result<FullItem, Error>;
}

/// A Rust item.


@@ 92,3 109,40 @@ pub struct Item {
    /// The type of this item.
    pub ty: ItemType,
}

/// A Rust import.
#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
#[non_exhaustive]
pub struct Import {
    /// The item being imported.
    pub source: Fqn,
    /// The name of the imported item.
    pub name: Ident,
    /// Whether this import uses a glob.
    pub glob: bool,
}

/// The full documentation for a Rust item.
#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
#[non_exhaustive]
pub struct FullItem {
    /// The name of this item.
    pub fqn: Fqn,
    /// The type of this item.
    pub ty: ItemType,
    /// The members of this item.
    pub members: Option<Members>,
}

/// The members of a Rust item.
#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
#[non_exhaustive]
pub enum Members {
    /// The members of a module.
    Module {
        /// The re-exports in this module.
        reexports: Vec<Import>,
        /// The items in this module.
        items: Vec<Item>,
    },
}

M src/types.rs => src/types.rs +11 -3
@@ 331,6 331,16 @@ impl<'a> TryFrom<&'a str> for Fqn {
    }
}

impl TryFrom<Vec<Ident>> for Fqn {
    type Error = ParseIdentError;

    fn try_from(vec: Vec<Ident>) -> Result<Self, Self::Error> {
        vec1::Vec1::try_from_vec(vec)
            .map_err(|_| ParseIdentError::Empty)
            .map(Self)
    }
}

impl IntoIterator for Fqn {
    type IntoIter = std::vec::IntoIter<Self::Item>;
    type Item = Ident;


@@ 377,9 387,7 @@ impl str::FromStr for Fqn {

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        let result: Result<Vec<_>, _> = s.split("::").map(Ident::from_str).collect();
        result
            .and_then(|vec| vec1::Vec1::try_from_vec(vec).map_err(|_| ParseIdentError::Empty))
            .map(Self)
        result.and_then(Self::try_from)
    }
}


M tests/parser.rs => tests/parser.rs +17 -0
@@ 66,3 66,20 @@ fn test_modules() {
        insta::assert_debug_snapshot!(format!("modules__{}", krate.ident()), modules);
    });
}

#[test]
fn test_module() {
    test_with_crates(&["anyhow", "log", "rand_core"], |krate| {
        for fqn in krate.modules().unwrap() {
            let mut module = krate
                .open_item(fqn.clone(), rustdoc_parser::ItemType::Module)
                .unwrap();
            if let Some(rustdoc_parser::Members::Module { reexports, items }) = &mut module.members
            {
                reexports.sort();
                items.sort();
            }
            insta::assert_debug_snapshot!(format!("module__{}", fqn), module);
        }
    });
}

A tests/snapshots/parser__module__anyhow.snap => tests/snapshots/parser__module__anyhow.snap +52 -0
@@ 0,0 1,52 @@
---
source: tests/parser.rs
expression: module

---
FullItem {
    fqn: Fqn("anyhow"),
    ty: Module,
    members: Some(
        Module {
            reexports: [
                Import {
                    source: Fqn("anyhow"),
                    name: Ident(
                        "format_err",
                    ),
                    glob: false,
                },
            ],
            items: [
                Item {
                    fqn: Fqn("anyhow::Chain"),
                    ty: Struct,
                },
                Item {
                    fqn: Fqn("anyhow::Context"),
                    ty: Trait,
                },
                Item {
                    fqn: Fqn("anyhow::Error"),
                    ty: Struct,
                },
                Item {
                    fqn: Fqn("anyhow::Result"),
                    ty: Typedef,
                },
                Item {
                    fqn: Fqn("anyhow::anyhow"),
                    ty: Macro,
                },
                Item {
                    fqn: Fqn("anyhow::bail"),
                    ty: Macro,
                },
                Item {
                    fqn: Fqn("anyhow::ensure"),
                    ty: Macro,
                },
            ],
        },
    ),
}

A tests/snapshots/parser__module__log.snap => tests/snapshots/parser__module__log.snap +104 -0
@@ 0,0 1,104 @@
---
source: tests/parser.rs
expression: module

---
FullItem {
    fqn: Fqn("log"),
    ty: Module,
    members: Some(
        Module {
            reexports: [],
            items: [
                Item {
                    fqn: Fqn("log::Level"),
                    ty: Enum,
                },
                Item {
                    fqn: Fqn("log::LevelFilter"),
                    ty: Enum,
                },
                Item {
                    fqn: Fqn("log::Log"),
                    ty: Trait,
                },
                Item {
                    fqn: Fqn("log::Metadata"),
                    ty: Struct,
                },
                Item {
                    fqn: Fqn("log::MetadataBuilder"),
                    ty: Struct,
                },
                Item {
                    fqn: Fqn("log::ParseLevelError"),
                    ty: Struct,
                },
                Item {
                    fqn: Fqn("log::Record"),
                    ty: Struct,
                },
                Item {
                    fqn: Fqn("log::RecordBuilder"),
                    ty: Struct,
                },
                Item {
                    fqn: Fqn("log::STATIC_MAX_LEVEL"),
                    ty: Constant,
                },
                Item {
                    fqn: Fqn("log::SetLoggerError"),
                    ty: Struct,
                },
                Item {
                    fqn: Fqn("log::debug"),
                    ty: Macro,
                },
                Item {
                    fqn: Fqn("log::error"),
                    ty: Macro,
                },
                Item {
                    fqn: Fqn("log::info"),
                    ty: Macro,
                },
                Item {
                    fqn: Fqn("log::log"),
                    ty: Macro,
                },
                Item {
                    fqn: Fqn("log::log_enabled"),
                    ty: Macro,
                },
                Item {
                    fqn: Fqn("log::logger"),
                    ty: Function,
                },
                Item {
                    fqn: Fqn("log::max_level"),
                    ty: Function,
                },
                Item {
                    fqn: Fqn("log::set_logger"),
                    ty: Function,
                },
                Item {
                    fqn: Fqn("log::set_logger_racy"),
                    ty: Function,
                },
                Item {
                    fqn: Fqn("log::set_max_level"),
                    ty: Function,
                },
                Item {
                    fqn: Fqn("log::trace"),
                    ty: Macro,
                },
                Item {
                    fqn: Fqn("log::warn"),
                    ty: Macro,
                },
            ],
        },
    ),
}

A tests/snapshots/parser__module__rand_core.snap => tests/snapshots/parser__module__rand_core.snap +44 -0
@@ 0,0 1,44 @@
---
source: tests/parser.rs
expression: module

---
FullItem {
    fqn: Fqn("rand_core"),
    ty: Module,
    members: Some(
        Module {
            reexports: [],
            items: [
                Item {
                    fqn: Fqn("rand_core::CryptoRng"),
                    ty: Trait,
                },
                Item {
                    fqn: Fqn("rand_core::Error"),
                    ty: Struct,
                },
                Item {
                    fqn: Fqn("rand_core::RngCore"),
                    ty: Trait,
                },
                Item {
                    fqn: Fqn("rand_core::SeedableRng"),
                    ty: Trait,
                },
                Item {
                    fqn: Fqn("rand_core::block"),
                    ty: Module,
                },
                Item {
                    fqn: Fqn("rand_core::impls"),
                    ty: Module,
                },
                Item {
                    fqn: Fqn("rand_core::le"),
                    ty: Module,
                },
            ],
        },
    ),
}

A tests/snapshots/parser__module__rand_core::block.snap => tests/snapshots/parser__module__rand_core::block.snap +28 -0
@@ 0,0 1,28 @@
---
source: tests/parser.rs
expression: module

---
FullItem {
    fqn: Fqn("rand_core::block"),
    ty: Module,
    members: Some(
        Module {
            reexports: [],
            items: [
                Item {
                    fqn: Fqn("rand_core::block::BlockRng"),
                    ty: Struct,
                },
                Item {
                    fqn: Fqn("rand_core::block::BlockRng64"),
                    ty: Struct,
                },
                Item {
                    fqn: Fqn("rand_core::block::BlockRngCore"),
                    ty: Trait,
                },
            ],
        },
    ),
}

A tests/snapshots/parser__module__rand_core::impls.snap => tests/snapshots/parser__module__rand_core::impls.snap +40 -0
@@ 0,0 1,40 @@
---
source: tests/parser.rs
expression: module

---
FullItem {
    fqn: Fqn("rand_core::impls"),
    ty: Module,
    members: Some(
        Module {
            reexports: [],
            items: [
                Item {
                    fqn: Fqn("rand_core::impls::fill_bytes_via_next"),
                    ty: Function,
                },
                Item {
                    fqn: Fqn("rand_core::impls::fill_via_u32_chunks"),
                    ty: Function,
                },
                Item {
                    fqn: Fqn("rand_core::impls::fill_via_u64_chunks"),
                    ty: Function,
                },
                Item {
                    fqn: Fqn("rand_core::impls::next_u32_via_fill"),
                    ty: Function,
                },
                Item {
                    fqn: Fqn("rand_core::impls::next_u64_via_fill"),
                    ty: Function,
                },
                Item {
                    fqn: Fqn("rand_core::impls::next_u64_via_u32"),
                    ty: Function,
                },
            ],
        },
    ),
}

A tests/snapshots/parser__module__rand_core::le.snap => tests/snapshots/parser__module__rand_core::le.snap +24 -0
@@ 0,0 1,24 @@
---
source: tests/parser.rs
expression: module

---
FullItem {
    fqn: Fqn("rand_core::le"),
    ty: Module,
    members: Some(
        Module {
            reexports: [],
            items: [
                Item {
                    fqn: Fqn("rand_core::le::read_u32_into"),
                    ty: Function,
                },
                Item {
                    fqn: Fqn("rand_core::le::read_u64_into"),
                    ty: Function,
                },
            ],
        },
    ),
}