Skip to main content

rscheck_cli/
analysis.rs

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}