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 }