roogle_engine/
search.rs

1use std::collections::HashMap;
2
3use rustdoc_types as types;
4use serde::Serialize;
5use thiserror::Error;
6
7use crate::{
8    compare::{Compare, Similarities},
9    query::Query,
10    Index,
11};
12
13#[derive(Debug, Clone, PartialEq, Serialize)]
14pub struct Hit {
15    pub name: String,
16    pub path: Vec<String>,
17    pub link: Vec<String>,
18    pub docs: Option<String>,
19    #[serde(skip)]
20    similarities: Similarities,
21}
22
23impl Hit {
24    pub fn similarities(&self) -> &Similarities {
25        &self.similarities
26    }
27}
28
29impl PartialOrd for Hit {
30    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
31        self.similarities.partial_cmp(&other.similarities)
32    }
33}
34
35#[derive(Error, Debug)]
36pub enum SearchError {
37    #[error("crate `{0}` is not present in the index")]
38    CrateNotFound(String),
39
40    #[error("item with id `{0}` is not present in crate `{1}`")]
41    ItemNotFound(String, String),
42}
43
44pub type Result<T> = std::result::Result<T, SearchError>;
45
46/// Represents a scope to search in.
47#[derive(Debug, Clone)]
48pub enum Scope {
49    /// Represetns a single crate.
50    Crate(String),
51
52    /// Represents multiple crates.
53    ///
54    /// For example:
55    /// - `rustc_ast`, `rustc_ast_lowering`, `rustc_passes` and `rustc_ast_pretty`
56    /// - `std`, `core` and `alloc`
57    Set(Vec<String>),
58}
59
60impl Scope {
61    pub fn flatten(self) -> Vec<String> {
62        match self {
63            Scope::Crate(krate) => vec![krate],
64            Scope::Set(krates) => krates,
65        }
66    }
67}
68
69impl Index {
70    /// Perform search with given query and scope.
71    ///
72    /// Returns [`Hit`]s whose similarity score outperforms given `threshold`.
73    pub fn search(&self, query: &Query, scope: Scope, threshold: f32) -> Result<Vec<Hit>> {
74        let mut hits = vec![];
75
76        let krates = scope.flatten();
77        for krate_name in krates {
78            let krate = self
79                .crates
80                .get(&krate_name)
81                .ok_or(SearchError::CrateNotFound(krate_name.clone()))?;
82            for item in krate.index.values() {
83                match item.inner {
84                    types::ItemEnum::Function(_) => {
85                        let (path, link) = Self::path_and_link(krate, &krate_name, item, None)?;
86                        let sims = self.compare(query, item, krate, None);
87
88                        if sims.score() < threshold {
89                            hits.push(Hit {
90                                name: item.name.clone().unwrap(), // SAFETY: all functions has its name.
91                                path,
92                                link,
93                                docs: item.docs.clone(),
94                                similarities: sims,
95                            });
96                        }
97                    }
98                    types::ItemEnum::Impl(ref impl_) if impl_.trait_.is_none() => {
99                        let assoc_items = impl_
100                            .items
101                            .iter()
102                            .map(|id| {
103                                krate.index.get(id).ok_or(SearchError::ItemNotFound(
104                                    id.0.clone(),
105                                    krate_name.clone(),
106                                ))
107                            })
108                            .collect::<Result<Vec<_>>>()?;
109                        for assoc_item in assoc_items {
110                            if let types::ItemEnum::Method(_) = assoc_item.inner {
111                                let (path, link) = Self::path_and_link(
112                                    krate,
113                                    &krate_name,
114                                    assoc_item,
115                                    Some(impl_),
116                                )?;
117                                let sims = self.compare(query, assoc_item, krate, Some(impl_));
118
119                                if sims.score() < threshold {
120                                    hits.push(Hit {
121                                        name: assoc_item.name.clone().unwrap(), // SAFETY: all methods has its name.
122                                        path,
123                                        link,
124                                        docs: assoc_item.docs.clone(),
125                                        similarities: sims,
126                                    })
127                                }
128                            }
129                        }
130                    }
131                    // TODO(hkmatsumoto): Acknowledge trait method as well.
132                    _ => {}
133                }
134            }
135        }
136
137        hits.sort_unstable_by(|a, b| a.partial_cmp(b).unwrap());
138        Ok(hits)
139    }
140
141    #[tracing::instrument(skip(self, krate))]
142    fn compare(
143        &self,
144        query: &Query,
145        item: &types::Item,
146        krate: &types::Crate,
147        impl_: Option<&types::Impl>,
148    ) -> Similarities {
149        let mut generics;
150        if let Some(impl_) = impl_ {
151            generics = impl_.generics.clone();
152            generics
153                .where_predicates
154                .push(types::WherePredicate::EqPredicate {
155                    lhs: types::Type::Generic("Self".to_owned()),
156                    rhs: impl_.for_.clone(),
157                });
158        } else {
159            generics = types::Generics::default()
160        }
161        let mut substs = HashMap::default();
162
163        let sims = query.compare(item, krate, &mut generics, &mut substs);
164        Similarities(sims)
165    }
166
167    /// Given `item` and optional `impl_`, compute its path and rustdoc link to `item`.
168    ///
169    /// `item` must be a function or a method, otherwise assertions will fail.
170    fn path_and_link(
171        krate: &types::Crate,
172        krate_name: &str,
173        item: &types::Item,
174        impl_: Option<&types::Impl>,
175    ) -> Result<(Vec<String>, Vec<String>)> {
176        assert!(matches!(
177            item.inner,
178            types::ItemEnum::Function(_) | types::ItemEnum::Method(_)
179        ));
180
181        use types::Type;
182
183        let get_path = |id: &types::Id| -> Result<Vec<String>> {
184            let path = krate
185                .paths
186                .get(id)
187                .ok_or(SearchError::ItemNotFound(
188                    id.0.clone(),
189                    krate_name.to_owned(),
190                ))?
191                .path
192                .clone();
193
194            Ok(path)
195        };
196
197        // If `item` is a associated item, replace the last segment of the path for the link of the ADT
198        // it is binded to.
199        let mut path;
200        let mut link;
201        if let Some(impl_) = impl_ {
202            let recv;
203            match (&impl_.for_, &impl_.trait_) {
204                (_, Some(ref t)) => {
205                    if let Type::ResolvedPath { name, id, .. } = t {
206                        path = get_path(id)?;
207                        recv = format!("trait.{}.html", name);
208                    } else {
209                        // SAFETY: All traits are represented by `ResolvedPath`.
210                        unreachable!()
211                    }
212                }
213                (
214                    Type::ResolvedPath {
215                        ref name, ref id, ..
216                    },
217                    _,
218                ) => {
219                    path = get_path(id)?;
220                    let summary = krate.paths.get(id).ok_or(SearchError::ItemNotFound(
221                        id.0.clone(),
222                        krate_name.to_owned(),
223                    ))?;
224                    match summary.kind {
225                        types::ItemKind::Union => recv = format!("union.{}.html", name),
226                        types::ItemKind::Enum => recv = format!("enum.{}.html", name),
227                        types::ItemKind::Struct => recv = format!("struct.{}.html", name),
228                        // SAFETY: ADTs are either unions or enums or structs.
229                        _ => unreachable!(),
230                    }
231                }
232                (Type::Primitive(ref prim), _) => {
233                    path = vec![prim.clone()];
234                    recv = format!("primitive.{}.html", prim);
235                }
236                (Type::Tuple(_), _) => {
237                    path = vec!["tuple".to_owned()];
238                    recv = "primitive.tuple.html".to_owned();
239                }
240                (Type::Slice(_), _) => {
241                    path = vec!["slice".to_owned()];
242                    recv = "primitive.slice.html".to_owned();
243                }
244                (Type::Array { .. }, _) => {
245                    path = vec!["array".to_owned()];
246                    recv = "primitive.array.html".to_owned();
247                }
248                (Type::RawPointer { .. }, _) => {
249                    path = vec!["pointer".to_owned()];
250                    recv = "primitive.pointer.html".to_owned();
251                }
252                (Type::BorrowedRef { .. }, _) => {
253                    path = vec!["reference".to_owned()];
254                    recv = "primitive.reference.html".to_owned();
255                }
256                _ => unreachable!(),
257            }
258            link = path.clone();
259            if let Some(l) = link.last_mut() {
260                *l = recv;
261            }
262        } else {
263            path = get_path(&item.id)?;
264            link = path.clone();
265        }
266
267        match item.inner {
268            types::ItemEnum::Function(_) => {
269                if let Some(l) = link.last_mut() {
270                    *l = format!("fn.{}.html", l);
271                }
272                Ok((path.clone(), link))
273            }
274            types::ItemEnum::Method(_) => {
275                path.push(item.name.clone().unwrap()); // SAFETY: all methods has its name.
276                link.push(item.name.clone().unwrap());
277                if let Some(l) = link.last_mut() {
278                    *l = format!("#method.{}", l);
279                }
280                Ok((path.clone(), link))
281            }
282            // SAFETY: Already asserted at the beginning of this function.
283            _ => unreachable!(),
284        }
285    }
286}
287
288#[cfg(test)]
289mod tests {
290    use std::collections::HashSet;
291
292    use super::*;
293    use crate::compare::{DiscreteSimilarity::*, Similarity::*};
294    use crate::query::{FnDecl, FnRetTy, Function};
295
296    fn krate() -> types::Crate {
297        types::Crate {
298            root: types::Id("0:0".to_owned()),
299            crate_version: Some("0.0.0".to_owned()),
300            includes_private: false,
301            index: Default::default(),
302            paths: Default::default(),
303            external_crates: Default::default(),
304            format_version: 0,
305        }
306    }
307
308    fn item(name: String, inner: types::ItemEnum) -> types::Item {
309        types::Item {
310            id: types::Id("test".to_owned()),
311            crate_id: 0,
312            name: Some(name),
313            span: None,
314            visibility: types::Visibility::Public,
315            docs: None,
316            links: HashMap::default(),
317            attrs: vec![],
318            deprecation: None,
319            inner,
320        }
321    }
322
323    /// Returns a function which will be expressed as `fn foo() -> ()`.
324    fn foo() -> types::Function {
325        types::Function {
326            decl: types::FnDecl {
327                inputs: vec![],
328                output: None,
329                c_variadic: false,
330            },
331            generics: types::Generics {
332                params: vec![],
333                where_predicates: vec![],
334            },
335            header: HashSet::default(),
336            abi: "rust".to_owned(),
337        }
338    }
339
340    #[test]
341    fn compare_symbol() {
342        let query = Query {
343            name: Some("foo".to_owned()),
344            kind: None,
345        };
346
347        let function = foo();
348        let item = item("foo".to_owned(), types::ItemEnum::Function(function));
349        let krate = krate();
350        let mut generics = types::Generics::default();
351        let mut substs = HashMap::default();
352
353        assert_eq!(
354            query.compare(&item, &krate, &mut generics, &mut substs),
355            vec![Continuous(0.0)]
356        )
357    }
358
359    #[test]
360    fn compare_function() {
361        let q = Function {
362            decl: FnDecl {
363                inputs: Some(vec![]),
364                output: Some(FnRetTy::DefaultReturn),
365            },
366        };
367
368        let i = foo();
369
370        let krate = krate();
371        let mut generics = types::Generics::default();
372        let mut substs = HashMap::default();
373
374        assert_eq!(
375            q.compare(&i, &krate, &mut generics, &mut substs),
376            vec![Discrete(Equivalent), Discrete(Equivalent)]
377        )
378    }
379}