1use crate::config::Policy;
2use globset::{Glob, GlobSet, GlobSetBuilder};
3use ignore::WalkBuilder;
4use std::{fs, io, path::PathBuf};
5
6#[derive(Clone)]
7pub struct SourceFile {
8 pub path: PathBuf,
9 pub text: String,
10 pub ast: Option<syn::File>,
11 pub parse_error: Option<String>,
12}
13
14pub struct Workspace {
15 pub root: PathBuf,
16 pub files: Vec<SourceFile>,
17}
18
19#[derive(Debug, thiserror::Error)]
20pub enum DiscoverError {
21 #[error("failed to build glob matcher: {pattern}")]
22 Glob {
23 pattern: String,
24 #[source]
25 source: globset::Error,
26 },
27 #[error("failed to read file: {path}")]
28 Read {
29 path: PathBuf,
30 #[source]
31 source: io::Error,
32 },
33}
34
35impl Workspace {
36 #[must_use]
37 pub fn new(root: PathBuf) -> Self {
38 Self {
39 root,
40 files: Vec::new(),
41 }
42 }
43
44 pub fn load_files(mut self, policy: &Policy) -> Result<Self, DiscoverError> {
45 let include = build_globset(&policy.workspace.include)?;
46 let exclude = build_globset(&policy.workspace.exclude)?;
47
48 let walker = WalkBuilder::new(&self.root)
49 .hidden(false)
50 .git_ignore(true)
51 .git_global(true)
52 .git_exclude(true)
53 .build();
54
55 for entry in walker {
56 let entry = match entry {
57 Ok(e) => e,
58 Err(_) => continue,
59 };
60 let path = entry.path();
61 if !path.is_file() {
62 continue;
63 }
64 let rel = path.strip_prefix(&self.root).unwrap_or(path);
65 if !include.is_match(rel) || exclude.is_match(rel) {
66 continue;
67 }
68 if path.extension().is_none_or(|ext| ext != "rs") {
69 continue;
70 }
71
72 let text = fs::read_to_string(path).map_err(|source| DiscoverError::Read {
73 path: path.to_path_buf(),
74 source,
75 })?;
76 let mut parse_error = None;
77 let ast = match syn::parse_file(&text) {
78 Ok(ast) => Some(ast),
79 Err(err) => {
80 parse_error = Some(err.to_string());
81 None
82 }
83 };
84 self.files.push(SourceFile {
85 path: path.to_path_buf(),
86 text,
87 ast,
88 parse_error,
89 });
90 }
91
92 Ok(self)
93 }
94}
95
96fn build_globset(patterns: &[String]) -> Result<GlobSet, DiscoverError> {
97 let mut builder = GlobSetBuilder::new();
98 for pattern in patterns {
99 let glob = Glob::new(pattern).map_err(|source| DiscoverError::Glob {
100 pattern: pattern.clone(),
101 source,
102 })?;
103 builder.add(glob);
104 }
105 builder.build().map_err(|source| DiscoverError::Glob {
106 pattern: "<globset>".to_string(),
107 source,
108 })
109}