Skip to main content

sofie_check/
workspace.rs

1use std::fs;
2use std::path::{Path, PathBuf};
3
4use proc_macro2::TokenStream;
5use sofie_model::syntax::{parse_tokens, tokenize};
6use sofie_model::{ModelError, rust_files, subdirectories};
7
8use crate::lines::{LineKind, classify_lines};
9
10/// The benchmark apps follow other frameworks' conventions; `node_modules`
11/// can be large, and the walk runs in every build script.
12const SKIPPED_DIRECTORIES: [&str; 3] = ["target", "bench", "node_modules"];
13
14const SOURCE_DIRECTORIES: [&str; 2] = ["src", "tests"];
15
16pub struct CrateSource {
17    pub name: String,
18    pub files: Vec<SourceFile>,
19}
20
21pub struct SourceFile {
22    pub path: PathBuf,
23    pub lines: Vec<LineKind>,
24    pub tokens: TokenStream,
25    pub syntax: syn::File,
26}
27
28impl SourceFile {
29    pub fn parse(path: PathBuf, text: &str) -> Result<Self, ModelError> {
30        let tokens = tokenize(&path, text)?;
31        let lines = classify_lines(text, &tokens);
32        let syntax = parse_tokens(&path, tokens.clone())?;
33        Ok(Self {
34            path,
35            lines,
36            tokens,
37            syntax,
38        })
39    }
40}
41
42pub fn load_workspace_crates(workspace_root: &Path) -> Result<Vec<CrateSource>, ModelError> {
43    let mut crate_directories = Vec::new();
44    find_crates(workspace_root, &mut crate_directories)?;
45    crate_directories
46        .iter()
47        .map(|directory| load_crate(workspace_root, directory))
48        .collect()
49}
50
51fn find_crates(directory: &Path, found: &mut Vec<PathBuf>) -> Result<(), ModelError> {
52    if is_package(directory)? {
53        found.push(directory.to_path_buf());
54    }
55    for subdirectory in subdirectories(directory)? {
56        if !is_skipped(&subdirectory) {
57            find_crates(&subdirectory, found)?;
58        }
59    }
60    Ok(())
61}
62
63fn is_skipped(directory: &Path) -> bool {
64    directory
65        .file_name()
66        .and_then(|name| name.to_str())
67        .is_some_and(|name| name.starts_with('.') || SKIPPED_DIRECTORIES.contains(&name))
68}
69
70fn is_package(directory: &Path) -> Result<bool, ModelError> {
71    let manifest = directory.join("Cargo.toml");
72    if !manifest.is_file() {
73        return Ok(false);
74    }
75    Ok(read_text(&manifest)?
76        .lines()
77        .any(|line| line.trim() == "[package]"))
78}
79
80fn load_crate(workspace_root: &Path, crate_directory: &Path) -> Result<CrateSource, ModelError> {
81    let mut files = Vec::new();
82    for source_directory in SOURCE_DIRECTORIES {
83        for path in rust_files(&crate_directory.join(source_directory))? {
84            files.push(load_file(workspace_root, &path)?);
85        }
86    }
87    Ok(CrateSource {
88        name: relative(workspace_root, crate_directory)
89            .display()
90            .to_string(),
91        files,
92    })
93}
94
95fn load_file(workspace_root: &Path, path: &Path) -> Result<SourceFile, ModelError> {
96    SourceFile::parse(relative(workspace_root, path), &read_text(path)?)
97}
98
99fn relative(workspace_root: &Path, path: &Path) -> PathBuf {
100    path.strip_prefix(workspace_root)
101        .map_or_else(|_| path.to_path_buf(), Path::to_path_buf)
102}
103
104pub(crate) fn read_text(path: &Path) -> Result<String, ModelError> {
105    fs::read_to_string(path)
106        .map_err(|error| ModelError::in_file(path, format!("cannot read the file: {error}")))
107}
108
109#[cfg(test)]
110mod tests {
111    use sofie_model::testing::TemporaryDirectory;
112
113    use super::load_workspace_crates;
114
115    #[test]
116    fn finds_every_package_at_any_depth_but_build_output_benchmarks_node_modules_and_hidden_ones() {
117        let workspace = TemporaryDirectory::new("sofie-check-workspace");
118        let package = "[package]\nname = \"a\"\n";
119        workspace.write("Cargo.toml", "[workspace]\nmembers = [\"*\"]\n");
120        for directory in [
121            "shop/app",
122            "shop/modules/order",
123            "shop/modules/order/contract",
124            "target/x",
125            "bench/y",
126            "shop/web/node_modules/w",
127            ".z",
128        ] {
129            workspace.write(&format!("{directory}/Cargo.toml"), package);
130            workspace.write(&format!("{directory}/src/lib.rs"), "fn f() {}\n");
131        }
132        workspace.write("notes/Cargo.toml", "[workspace]\n");
133
134        let crates = load_workspace_crates(workspace.root()).unwrap();
135
136        let names: Vec<&str> = crates.iter().map(|krate| krate.name.as_str()).collect();
137        assert_eq!(
138            names,
139            [
140                "shop/app",
141                "shop/modules/order",
142                "shop/modules/order/contract"
143            ]
144        );
145        assert_eq!(crates[1].files.len(), 1);
146    }
147}