Skip to main content

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}