pelicode_core/scan.rs
1//! Discovery and parallel extraction of source files under a directory.
2
3use std::path::{Path, PathBuf};
4
5use rayon::prelude::*;
6
7use crate::Error;
8use crate::config::Config;
9use crate::extract::{FileFeatures, extract_file};
10use crate::language::Language;
11
12/// A source file found by [`discover`].
13#[derive(Debug, Clone)]
14pub struct SourceFile {
15 /// Path of the file, as found on disk.
16 pub path: PathBuf,
17 /// Path relative to the scanned root (the path itself when a file was given),
18 /// with `/` as the separator on every platform.
19 pub relative: PathBuf,
20 /// Language of the file.
21 pub language: Language,
22}
23
24/// `path` with `/` as the separator, so that the paths recorded in a
25/// database and printed in reports are the same on every platform.
26///
27/// # Examples
28///
29/// ```
30/// use std::path::Path;
31/// use pelicode_core::slash_path;
32///
33/// assert_eq!(slash_path(Path::new("src/main.rs")), Path::new("src/main.rs"));
34/// ```
35pub fn slash_path(path: &Path) -> PathBuf {
36 if std::path::MAIN_SEPARATOR == '/' {
37 path.to_path_buf()
38 } else {
39 PathBuf::from(
40 path.to_string_lossy()
41 .replace(std::path::MAIN_SEPARATOR, "/"),
42 )
43 }
44}
45
46/// Lists the files of supported languages under `root`, honoring `.gitignore`.
47/// `root` may also be a single file.
48///
49/// Hidden files are skipped, and so are, in Git repositories, files ignored by
50/// `.gitignore`. Files are sorted by their relative path.
51///
52/// # Errors
53///
54/// [`Error::Io`] when `root` does not exist, and [`Error::Walk`] when a directory
55/// cannot be read.
56///
57/// # Examples
58///
59/// ```
60/// use std::path::Path;
61/// use pelicode_core::{Language, discover};
62///
63/// let dir = tempfile::tempdir().unwrap();
64/// std::fs::create_dir(dir.path().join("src")).unwrap();
65/// std::fs::write(dir.path().join("src/lib.rs"), "fn f() {}").unwrap();
66/// std::fs::write(dir.path().join("README.md"), "# readme").unwrap();
67///
68/// let files = discover(dir.path()).unwrap();
69/// assert_eq!(files.len(), 1);
70/// assert_eq!(files[0].relative, Path::new("src/lib.rs"));
71/// assert_eq!(files[0].language, Language::Rust);
72/// ```
73pub fn discover(root: &Path) -> Result<Vec<SourceFile>, Error> {
74 if root.is_file() {
75 return Ok(Language::from_path(root)
76 .map(|language| SourceFile {
77 path: root.to_path_buf(),
78 relative: slash_path(root),
79 language,
80 })
81 .into_iter()
82 .collect());
83 }
84 if !root.is_dir() {
85 return Err(Error::Io(
86 root.to_path_buf(),
87 std::io::Error::new(std::io::ErrorKind::NotFound, "no such file or directory"),
88 ));
89 }
90 let mut files = Vec::new();
91 for entry in ignore::WalkBuilder::new(root).build() {
92 let entry = entry.map_err(|e| Error::Walk(e.to_string()))?;
93 if !entry.file_type().is_some_and(|t| t.is_file()) {
94 continue;
95 }
96 let path = entry.into_path();
97 if let Some(language) = Language::from_path(&path) {
98 let relative = slash_path(path.strip_prefix(root).unwrap_or(&path));
99 files.push(SourceFile {
100 path,
101 relative,
102 language,
103 });
104 }
105 }
106 files.sort_by(|a, b| a.relative.cmp(&b.relative));
107 Ok(files)
108}
109
110/// Extracts features from `files` in parallel. Results keep the order of `files`.
111///
112/// # Examples
113///
114/// ```
115/// use pelicode_core::{Config, discover, extract_all};
116///
117/// let dir = tempfile::tempdir().unwrap();
118/// std::fs::write(dir.path().join("a.py"), "def f(x):\n return x + 1\n").unwrap();
119/// std::fs::write(dir.path().join("b.py"), "def g(y):\n return y - 1\n").unwrap();
120///
121/// let files = discover(dir.path()).unwrap();
122/// let features: Vec<_> = extract_all(&files, &Config::default())
123/// .into_iter()
124/// .collect::<Result<_, _>>()
125/// .unwrap();
126/// assert_eq!(features[0].functions[0].name, "f");
127/// assert_eq!(features[1].functions[0].name, "g");
128/// ```
129pub fn extract_all(files: &[SourceFile], config: &Config) -> Vec<Result<FileFeatures, Error>> {
130 files
131 .par_iter()
132 .map(|f| extract_file(&f.path, &f.relative, f.language, config))
133 .collect()
134}