rscheck-cli 0.1.0-alpha.3

CLI frontend for the rscheck policy engine.
Documentation
use crate::config::Policy;
use globset::{Glob, GlobSet, GlobSetBuilder};
use ignore::WalkBuilder;
use std::{fs, io, path::PathBuf};

#[derive(Clone)]
pub struct SourceFile {
    pub path: PathBuf,
    pub text: String,
    pub ast: Option<syn::File>,
    pub parse_error: Option<String>,
}

pub struct Workspace {
    pub root: PathBuf,
    pub files: Vec<SourceFile>,
}

#[derive(Debug, thiserror::Error)]
pub enum DiscoverError {
    #[error("failed to build glob matcher: {pattern}")]
    Glob {
        pattern: String,
        #[source]
        source: globset::Error,
    },
    #[error("failed to read file: {path}")]
    Read {
        path: PathBuf,
        #[source]
        source: io::Error,
    },
}

impl Workspace {
    #[must_use]
    pub fn new(root: PathBuf) -> Self {
        Self {
            root,
            files: Vec::new(),
        }
    }

    pub fn load_files(mut self, policy: &Policy) -> Result<Self, DiscoverError> {
        let include = build_globset(&policy.workspace.include)?;
        let exclude = build_globset(&policy.workspace.exclude)?;

        let walker = WalkBuilder::new(&self.root)
            .hidden(false)
            .git_ignore(true)
            .git_global(true)
            .git_exclude(true)
            .build();

        for entry in walker {
            let entry = match entry {
                Ok(e) => e,
                Err(_) => continue,
            };
            let path = entry.path();
            if !path.is_file() {
                continue;
            }
            let rel = path.strip_prefix(&self.root).unwrap_or(path);
            if !include.is_match(rel) || exclude.is_match(rel) {
                continue;
            }
            if path.extension().is_none_or(|ext| ext != "rs") {
                continue;
            }

            let text = fs::read_to_string(path).map_err(|source| DiscoverError::Read {
                path: path.to_path_buf(),
                source,
            })?;
            let mut parse_error = None;
            let ast = match syn::parse_file(&text) {
                Ok(ast) => Some(ast),
                Err(err) => {
                    parse_error = Some(err.to_string());
                    None
                }
            };
            self.files.push(SourceFile {
                path: path.to_path_buf(),
                text,
                ast,
                parse_error,
            });
        }

        Ok(self)
    }
}

fn build_globset(patterns: &[String]) -> Result<GlobSet, DiscoverError> {
    let mut builder = GlobSetBuilder::new();
    for pattern in patterns {
        let glob = Glob::new(pattern).map_err(|source| DiscoverError::Glob {
            pattern: pattern.clone(),
            source,
        })?;
        builder.add(glob);
    }
    builder.build().map_err(|source| DiscoverError::Glob {
        pattern: "<globset>".to_string(),
        source,
    })
}