rune/doc/
visitor.rs

1use crate::alloc;
2use crate::alloc::hash_map::{self, HashMap};
3use crate::alloc::prelude::*;
4use crate::alloc::{Box, String, Vec};
5use crate::compile::meta;
6use crate::compile::{CompileVisitor, Located, MetaError, MetaRef, Names};
7use crate::item::IntoComponent;
8use crate::{Hash, Item, ItemBuf};
9
10pub(crate) struct VisitorData {
11    #[cfg_attr(not(feature = "cli"), allow(dead_code))]
12    pub(crate) item: ItemBuf,
13    #[cfg_attr(not(feature = "cli"), allow(dead_code))]
14    pub(crate) hash: Hash,
15    pub(crate) kind: Option<meta::Kind>,
16    #[cfg_attr(not(feature = "cli"), allow(dead_code))]
17    pub(crate) deprecated: Option<String>,
18    pub(crate) docs: Vec<String>,
19    pub(crate) field_docs: HashMap<Box<str>, Vec<String>>,
20}
21
22impl VisitorData {
23    fn new(item: ItemBuf, hash: Hash, kind: Option<meta::Kind>) -> Self {
24        Self {
25            item,
26            hash,
27            kind,
28            deprecated: None,
29            docs: Vec::new(),
30            field_docs: HashMap::new(),
31        }
32    }
33}
34
35/// Visitor used to collect documentation from rune sources.
36pub struct Visitor {
37    pub(crate) base: ItemBuf,
38    pub(crate) names: Names,
39    pub(crate) data: HashMap<Hash, VisitorData>,
40    pub(crate) item_to_hash: HashMap<ItemBuf, Hash>,
41    /// Associated items.
42    pub(crate) associated: HashMap<Hash, Vec<Hash>>,
43}
44
45impl Visitor {
46    /// Construct a visitor with the given base component.
47    pub fn new(base: impl IntoIterator<Item: IntoComponent>) -> alloc::Result<Self> {
48        let mut this = Self {
49            base: base.into_iter().try_collect::<ItemBuf>()?,
50            names: Names::default(),
51            data: HashMap::default(),
52            item_to_hash: HashMap::new(),
53            associated: HashMap::new(),
54        };
55
56        this.names.insert(&this.base)?;
57
58        let mut it = this.base.iter();
59
60        while !it.as_item().is_empty() {
61            let hash = Hash::type_hash(it.as_item());
62
63            this.data.try_insert(
64                hash,
65                VisitorData::new(it.as_item().try_to_owned()?, hash, Some(meta::Kind::Module)),
66            )?;
67
68            this.item_to_hash
69                .try_insert(it.as_item().try_to_owned()?, hash)?;
70            it.next_back();
71        }
72
73        Ok(this)
74    }
75
76    /// Get meta by item.
77    #[cfg(feature = "cli")]
78    pub(crate) fn get(&self, item: &Item) -> Option<&VisitorData> {
79        let hash = self.item_to_hash.get(item)?;
80        self.data.get(hash)
81    }
82
83    /// Get meta by hash.
84    pub(crate) fn get_by_hash(&self, hash: Hash) -> Option<&VisitorData> {
85        self.data.get(&hash)
86    }
87
88    fn get_or_insert(&mut self, item: &Item) -> Result<&mut VisitorData, MetaError> {
89        let item = self.base.join(item)?;
90        let hash = Hash::type_hash(&item);
91
92        tracing::trace!(?item, ?hash, "getting");
93
94        let data = match self.data.entry(hash) {
95            hash_map::Entry::Occupied(e) => e.into_mut(),
96            hash_map::Entry::Vacant(e) => {
97                e.try_insert(VisitorData::new(item.try_to_owned()?, hash, None))?
98            }
99        };
100
101        Ok(data)
102    }
103}
104
105impl CompileVisitor for Visitor {
106    fn register_meta(&mut self, meta: MetaRef<'_>) -> Result<(), MetaError> {
107        // Skip over context meta, since we pick that up separately.
108        if meta.context {
109            return Ok(());
110        }
111
112        let item = self.base.join(meta.item)?;
113        let hash = Hash::type_hash(&item);
114
115        tracing::trace!(base = ?self.base, meta = ?meta.item, ?item, ?hash, "register meta");
116
117        self.names.insert(&item)?;
118        self.item_to_hash.try_insert(item.try_to_owned()?, hash)?;
119
120        match self.data.entry(hash) {
121            hash_map::Entry::Occupied(e) => {
122                e.into_mut().kind = Some(meta.kind.try_clone()?);
123            }
124            hash_map::Entry::Vacant(e) => {
125                e.try_insert(VisitorData::new(item, hash, Some(meta.kind.try_clone()?)))?;
126            }
127        }
128
129        if let Some(container) = meta.kind.associated_container() {
130            self.associated
131                .entry(container)
132                .or_try_default()?
133                .try_push(hash)?;
134        }
135
136        Ok(())
137    }
138
139    fn visit_doc_comment(
140        &mut self,
141        _location: &dyn Located,
142        item: &Item,
143        _: Hash,
144        string: &str,
145    ) -> Result<(), MetaError> {
146        let data = self.get_or_insert(item)?;
147
148        data.docs
149            .try_push(string.trim_end_matches(newlines).try_to_owned()?)?;
150
151        Ok(())
152    }
153
154    fn visit_field_doc_comment(
155        &mut self,
156        _location: &dyn Located,
157        item: &Item,
158        _: Hash,
159        field: &str,
160        string: &str,
161    ) -> Result<(), MetaError> {
162        let data = self.get_or_insert(item)?;
163
164        data.field_docs
165            .entry(field.try_into()?)
166            .or_try_default()?
167            .try_push(string.trim_end_matches(newlines).try_to_owned()?)?;
168
169        Ok(())
170    }
171}
172
173// Documentation comments are literal source lines, so they're newline
174// terminated. Since we perform our own internal newlines conversion
175// these need to be trimmed - at least between each doc item.
176fn newlines(c: char) -> bool {
177    matches!(c, '\n' | '\r')
178}