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 if std::fs::metadata(&f.path).is_ok_and(|m| m.len() > 10 * 1024 * 1024) {
29 continue;
30 }
31 let Ok(source) = std::fs::read_to_string(&f.path) else {
32 continue;
33 };
34 let Ok(docs) = aiproof_parse::parse_file(&f.path, &source) else {
35 continue;
36 };
37 let mut file_diags: Vec<_> = Vec::new();
38 for doc in &docs {
39 for rule in &rules {
40 for d in rule.check(doc, &ctx) {
41 max_severity = Some(match max_severity {
42 Some(cur) if cur >= d.severity => cur,
43 _ => d.severity,
44 });
45 file_diags.push(d);
46 }
47 }
48 }
49 if !file_diags.is_empty() {
50 entries.push((f.path.clone(), source, file_diags));
51 }
52 }
53
54 let report = Report {
55 file_entries: entries,
56 _phantom: Default::default(),
57 };
58 let format = match args.format {
59 crate::cli::Format::Pretty => Format::Pretty,
60 crate::cli::Format::Json => Format::Json,
61 crate::cli::Format::Sarif => Format::Sarif,
62 };
63 let stdout = std::io::stdout();
64 let mut out = stdout.lock();
65 aiproof_report::render(&report, format, args.color, &mut out)?;
66
67 Ok(max_severity.map(|s| s.exit_code()).unwrap_or(0))
68}
69
70pub fn filtered_rules(config: &Config) -> Vec<Box<dyn Rule>> {
71 let all = aiproof_rules::all_rules();
72 all.into_iter()
73 .filter(|r| {
74 let code = r.code();
75 if !config.select.is_empty() && !config.select.iter().any(|s| code_matches(s, code)) {
76 return false;
77 }
78 if config.ignore.iter().any(|s| code_matches(s, code)) {
79 return false;
80 }
81 true
82 })
83 .collect()
84}
85
86pub fn code_matches(pattern: &str, code: &str) -> bool {
87 if let Some(prefix) = pattern.strip_suffix('*') {
89 code.starts_with(prefix)
90 } else {
91 code == pattern
92 }
93}