Skip to main content

aiproof_cli/
run.rs

1use aiproof_config::Config;
2use aiproof_core::rule::{Ctx, Rule};
3use aiproof_core::severity::Severity;
4use aiproof_report::{Color, Format, Report};
5use std::path::PathBuf;
6
7pub struct RunArgs<'a> {
8    pub paths: &'a [PathBuf],
9    pub config: Config,
10    pub format: crate::cli::Format,
11    pub color: Color,
12}
13
14pub fn run(args: RunArgs) -> anyhow::Result<i32> {
15    let files = crate::discovery::discover(args.paths, &args.config)?;
16    let rules = filtered_rules(&args.config);
17    let ctx = Ctx {
18        target_models: args.config.target_models.as_slice(),
19        max_tokens_budget: args.config.max_tokens_budget,
20    };
21    let mut entries = Vec::new();
22    let mut max_severity: Option<Severity> = None;
23
24    for f in files {
25        let Ok(source) = std::fs::read_to_string(&f.path) else {
26            continue;
27        };
28        let Ok(docs) = aiproof_parse::parse_file(&f.path, &source) else {
29            continue;
30        };
31        let mut file_diags: Vec<_> = Vec::new();
32        for doc in &docs {
33            for rule in &rules {
34                for d in rule.check(doc, &ctx) {
35                    max_severity = Some(match max_severity {
36                        Some(cur) if cur >= d.severity => cur,
37                        _ => d.severity,
38                    });
39                    file_diags.push(d);
40                }
41            }
42        }
43        if !file_diags.is_empty() {
44            entries.push((f.path.clone(), source, file_diags));
45        }
46    }
47
48    let report = Report {
49        file_entries: entries,
50        _phantom: Default::default(),
51    };
52    let format = match args.format {
53        crate::cli::Format::Pretty => Format::Pretty,
54        crate::cli::Format::Json => Format::Json,
55        crate::cli::Format::Sarif => Format::Sarif,
56    };
57    let stdout = std::io::stdout();
58    let mut out = stdout.lock();
59    aiproof_report::render(&report, format, args.color, &mut out)?;
60
61    Ok(max_severity.map(|s| s.exit_code()).unwrap_or(0))
62}
63
64pub fn filtered_rules(config: &Config) -> Vec<Box<dyn Rule>> {
65    let all = aiproof_rules::all_rules();
66    all.into_iter()
67        .filter(|r| {
68            let code = r.code();
69            if !config.select.is_empty() && !config.select.iter().any(|s| code_matches(s, code)) {
70                return false;
71            }
72            if config.ignore.iter().any(|s| code_matches(s, code)) {
73                return false;
74            }
75            true
76        })
77        .collect()
78}
79
80pub fn code_matches(pattern: &str, code: &str) -> bool {
81    // Support "AIP*" wildcard and exact match.
82    if let Some(prefix) = pattern.strip_suffix('*') {
83        code.starts_with(prefix)
84    } else {
85        code == pattern
86    }
87}