rustdoc_index/
location.rs

1use crate::{
2    doc::{ItemType, ParseItemTypeError, FILETYPE, STD_PRIMITIVES},
3    Error
4};
5use std::{
6    path::{Path, PathBuf},
7    str::FromStr
8};
9
10#[derive(Debug, Error)]
11pub enum LocationError {
12    #[error("Invalid format")]
13    InvalidFormat,
14    #[error(transparent)]
15    ParseItemTypeError(#[from] ParseItemTypeError),
16    #[error("File not found")]
17    FileNotFound,
18    #[error("Item not found")]
19    ItemNotFound,
20    #[error("Doc dir not found")]
21    DocNotFound
22}
23
24pub const STD_CRATES: &[&str] = &["alloc", "core", "proc_macro", "std", "test"];
25
26pub async fn location_from_line(line: &str, current_dir: Option<PathBuf>) -> Result<String, Error> {
27    let (path_components, ty) = parse_line(line)?;
28    let (krate_name, tail): (_, &[&str]) = split_krate(&path_components)?;
29    let search_index: PathBuf = find_search_index(krate_name, current_dir)?;
30    find(&search_index, krate_name, tail, ty)
31}
32
33fn find(
34    search_index: &Path,
35    krate_name: &str,
36    tail: &[&str],
37    ty: ItemType
38) -> Result<String, Error> {
39    let doc_dir: &Path = search_index.parent().unwrap();
40    let krate_dir: PathBuf = cd_krate_dir(doc_dir, krate_name)?;
41    if krate_name != "std" && krate_name != "core" {
42        let (file, rest) = find_file(&krate_dir, tail, ty).ok_or(LocationError::FileNotFound)?;
43        let url = item_url(&file, rest, ty);
44        return Ok(url);
45    }
46    let (file, rest) = match tail.len() {
47        1 if ty == ItemType::Primitive && STD_PRIMITIVES.iter().any(|p| *p == tail[0]) => {
48            ls_file(&krate_dir, tail).ok_or(LocationError::FileNotFound)?
49        }
50        2 if ty == ItemType::Method || ty == ItemType::AssocConst => {
51            ls_file(&krate_dir, tail).ok_or(LocationError::FileNotFound)?
52        }
53        _ => find_file(&krate_dir, tail, ty).ok_or(LocationError::FileNotFound)?
54    };
55    let url = item_url(&file, rest, ty);
56    Ok(url)
57}
58
59fn parse_line(line: &str) -> Result<(Vec<&str>, ItemType), Error> {
60    let (fst, snd) = {
61        let mut a = line.split_whitespace();
62        a.next()
63            .and_then(|fst| a.next().map(|snd| (fst, snd)))
64            .ok_or(LocationError::InvalidFormat)
65    }?;
66    let ty = ItemType::from_str(snd).map_err(LocationError::from)?;
67    let path_components = fst.split("::").collect::<Vec<_>>();
68    Ok((path_components, ty))
69}
70
71fn split_krate<'a, 'b>(
72    path_components: &'a [&'b str]
73) -> Result<(&'b str, &'a [&'b str]), LocationError> {
74    let krate_name = path_components
75        .first()
76        .ok_or(LocationError::InvalidFormat)?;
77    Ok((krate_name, &path_components[1..]))
78}
79
80fn find_search_index(krate_name: &str, current_dir: Option<PathBuf>) -> Result<PathBuf, Error> {
81    let search_index: PathBuf = if is_std_krate(krate_name) {
82        crate::search_index::find_std()
83    } else {
84        crate::search_index::find_local(current_dir)
85    }?
86    .ok_or(LocationError::DocNotFound)?;
87    Ok(search_index)
88}
89
90#[inline]
91fn is_std_krate(name: &str) -> bool { STD_CRATES.iter().any(|c| *c == name) }
92
93fn cd_krate_dir(doc_dir: &Path, krate_name: &str) -> Result<PathBuf, LocationError> {
94    let krate_dir: PathBuf = Some(doc_dir.join(krate_name))
95        .filter(|p| p.is_dir())
96        .ok_or(LocationError::DocNotFound)?;
97    Ok(krate_dir)
98}
99
100fn find_file<'a, 'b>(
101    dir: &Path,
102    path_components: &'a [&'b str],
103    ty: ItemType
104) -> Option<(PathBuf, &'a [&'b str])> {
105    let (cd, rest) = step_into_module(dir, path_components, ty);
106    if rest.is_empty() || ty == ItemType::Import {
107        return Some((cd.join("index.html"), rest));
108    }
109    ls_file(&cd, rest)
110}
111
112fn ls_file<'a, 'b>(cd: &Path, rest: &'a [&'b str]) -> Option<(PathBuf, &'a [&'b str])> {
113    let top = rest[0];
114    let rest = &rest[1..];
115    let found = FILETYPE
116        .iter()
117        .map(|ty| cd.join(format!("{}.{}.html", ty.as_str(), top)))
118        .find(|p| p.is_file())?;
119    Some((found, rest))
120}
121
122fn step_into_module<'a, 'b>(
123    dir: &Path,
124    path_components: &'a [&'b str],
125    ty: ItemType
126) -> (PathBuf, &'a [&'b str]) {
127    let mut cd: PathBuf = dir.into();
128    let mut rest: &[&str] = path_components;
129    while !rest.is_empty() {
130        if rest.len() == 1 && FILETYPE.iter().any(|t| t == &ty) {
131            break;
132        }
133        let top = rest[0];
134        let attempt = cd.join(top);
135        if !attempt.is_dir() {
136            break;
137        }
138        rest = &rest[1..];
139        cd = attempt;
140    }
141    (cd, rest)
142}
143
144fn item_url(file: &Path, rest: &[&str], ty: ItemType) -> String {
145    if let Some(id) = item_id(rest, ty) {
146        format!("file://{}#{}", file.display(), id)
147    } else {
148        format!("file://{}", file.display())
149    }
150}
151
152fn item_id(rest: &[&str], ty: ItemType) -> Option<String> {
153    if rest.is_empty() {
154        return None;
155    }
156    if ty == ItemType::Import {
157        return Some(format!("reexport.{}", rest[0]));
158    }
159    if rest.len() == 1 {
160        return if ty == ItemType::StructField && rest[0].parse::<i32>().is_ok() {
161            None
162        } else {
163            Some(format!("{}.{}", ty.as_str(), rest[0]))
164        };
165    }
166    if rest.len() == 2 && ty == ItemType::StructField {
167        return if rest[1].parse::<i32>().is_ok() {
168            Some(format!("variant.{}", rest[0]))
169        } else {
170            Some(format!("variant.{}.field.{}", rest[0], rest[1]))
171        };
172    }
173    unreachable!()
174}
175
176#[cfg(test)]
177mod tests {
178    use super::*;
179    use std::{
180        io::{BufRead, BufReader, Lines},
181        path::PathBuf,
182        process::{Child, ChildStdout, Command, Stdio}
183    };
184
185    #[tokio::test]
186    async fn item_exists_for_every_line() {
187        env_logger::builder().is_test(true).try_init().ok();
188        let mut source = source();
189        let search_indexes = crate::search_index::search_indexes(None).await.unwrap();
190        for line in list(&mut source) {
191            let line = line.unwrap();
192            item_exists_for_every_line_impl(&search_indexes, &line, true);
193        }
194        if !source.wait().unwrap().success() {
195            panic!("list failed");
196        }
197    }
198
199    fn source() -> Child {
200        let child = Command::new("./target/debug/cargo-listdoc")
201            .args(&["listdoc", "show"])
202            .stdout(Stdio::piped())
203            .spawn()
204            .unwrap();
205        child
206    }
207
208    fn list(child: &mut Child) -> Lines<BufReader<ChildStdout>> {
209        let stdout = child.stdout.take().unwrap();
210        BufReader::new(stdout).lines()
211    }
212
213    fn item_exists_for_every_line_impl(search_indexes: &[PathBuf], line: &str, check_item: bool) {
214        let (path_components, ty) = parse_line(line).unwrap();
215        let (krate_name, tail): (_, &[&str]) = split_krate(&path_components).unwrap();
216        log::debug!("{} {:?}", krate_name, tail);
217        let maybe_file = search_indexes
218            .iter()
219            .find_map(|s| find(s, krate_name, tail, ty).ok());
220        let file = match maybe_file {
221            None => panic!("Not found {}", line),
222            Some(x) => x
223        };
224        if check_item {
225            item_exists(&file);
226        }
227    }
228
229    fn item_exists(url: &str) {
230        let idx = match url.find('#') {
231            Some(x) => x,
232            None => return
233        };
234        log::debug!("{}", url);
235        let (file, item) = url.split_at(idx);
236        let item = &item[1..];
237        let file = file.strip_prefix("file://").unwrap();
238        let id = format!(r#"id="{}""#, item);
239        let contents = std::fs::read_to_string(file).unwrap();
240        if !contents.contains(&id) {
241            if is_reference(file) {
242                log::error!("Not found {} in {}", id, file);
243            } else {
244                panic!("Not found {} in {}", id, file);
245            }
246        }
247    }
248
249    fn is_reference(path: &str) -> bool {
250        let xs = path.split('/').collect::<Vec<_>>();
251        if xs.len() < 2 {
252            return false;
253        }
254        (xs[xs.len() - 2] == "core" || xs[xs.len() - 2] == "std")
255            && xs[xs.len() - 1] == "primitive.reference.html"
256    }
257
258    // fn lines(s: String) -> Vec<String> {
259    //    let mut lines = Vec::new();
260    //    let mut buf = s;
261    //    while let Some(idx) = buf.find('\n') {
262    //        let new = buf.split_off(idx);
263    //        lines.push(buf);
264    //        buf = new;
265    //    }
266    //    if !buf.is_empty() {
267    //        lines.push(buf);
268    //    }
269    //    lines
270    //}
271}