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
10const 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}