ruggle_engine/
search.rs

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