#![allow(clippy::only_used_in_recursion)]
#![allow(clippy::needless_borrow)]
#![allow(clippy::too_many_arguments)]
#![allow(clippy::collapsible_if)]
pub mod prelude;
mod analyze;
mod export;
mod files;
mod manifest;
mod parser;
mod progress;
mod rules;
mod ui;
mod utility;
use crate::manifest::Severity;
use crate::prelude::*;
use clap::{Arg, Command};
use analyze::{analyze_project, handle_generate_suppression};
use export::export_all_violations;
use files::ProjectSource;
use progress::CLIProgressReporter;
use ui::TerminalUI;
use std::collections::HashSet;
use std::path::Path;
fn main() {
let result = run();
match result {
Ok(exit_code) => std::process::exit(exit_code),
Err(e) => {
eprintln!("Error: {:#}", e);
std::process::exit(2);
}
}
}
fn run() -> Result<i32> {
let matches = Command::new("sqc")
.about("Software Code Quality - CERT C compliance checker")
.version(env!("CARGO_PKG_VERSION"))
.arg(
Arg::new("path")
.help("Path to the file, directory, or git repository to analyze")
.value_name("PATH")
.default_value(".")
.index(1),
)
.arg(
Arg::new("manifest")
.long("manifest")
.short('m')
.help("Path to the rules manifest file")
.value_name("FILE")
.default_value("rules_templates/rules-all.toml"),
)
.arg(
Arg::new("interactive")
.long("interactive")
.short('i')
.help("Run in interactive terminal UI mode")
.action(clap::ArgAction::SetTrue),
)
.arg(
Arg::new("export")
.long("export")
.short('e')
.help("Export violations to file (CSV, Excel, JSON, or SARIF based on extension)")
.value_name("FILE"),
)
.arg(
Arg::new("generate_suppression")
.long("generate-suppression")
.help("Generate suppression comment for a specific file:line:rule")
.value_name("FILE:LINE:RULE")
.conflicts_with("interactive")
.conflicts_with("export"),
)
.arg(
Arg::new("directories")
.long("directories")
.short('d')
.help("Additional directories to pre-scan for function definitions (cross-file context)")
.value_name("DIR")
.action(clap::ArgAction::Append),
)
.arg(
Arg::new("include_paths")
.long("include-path")
.short('I')
.help("Include search paths for resolving #include directives (like compiler -I flag)")
.value_name("DIR")
.action(clap::ArgAction::Append),
)
.arg(
Arg::new("fail_on_violation")
.long("fail-on-violation")
.help("Exit with code 1 if any violations are found")
.action(clap::ArgAction::SetTrue),
)
.arg(
Arg::new("fail_on_severity")
.long("fail-on-severity")
.help("Exit with code 1 if any violation meets or exceeds this severity")
.value_name("LEVEL")
.value_parser(["Low", "Medium", "High", "Critical"]),
)
.arg(
Arg::new("min_severity")
.long("min-severity")
.help("Only report violations at or above this severity")
.value_name("LEVEL")
.value_parser(["Low", "Medium", "High", "Critical"]),
)
.arg(
Arg::new("rules")
.long("rules")
.help("Only report violations from these rules (comma-separated)")
.value_name("RULE1,RULE2,..."),
)
.arg(
Arg::new("diff")
.long("diff")
.help("Only analyze modified/new C files (git diff)")
.action(clap::ArgAction::SetTrue),
)
.arg(
Arg::new("suppress_file")
.long("suppress-file")
.help("Path to .sqc-suppress.toml file (auto-detected in project root if not specified)")
.value_name("FILE"),
)
.arg(
Arg::new("verbose")
.long("verbose")
.short('v')
.help("Increase output verbosity (-v: per-rule scanning progress)")
.action(clap::ArgAction::Count),
)
.arg(
Arg::new("save_prescan")
.long("save-prescan")
.help("Save prescan context to a binary cache file (for CI/CD caching)")
.value_name("FILE"),
)
.arg(
Arg::new("load_prescan")
.long("load-prescan")
.help("Load prescan context from cache instead of scanning -d directories")
.value_name("FILE"),
)
.arg(
Arg::new("jobs")
.long("jobs")
.short('j')
.help("Number of parallel analysis threads (0 = auto-detect, 1 = sequential)")
.value_name("N")
.default_value("0")
.value_parser(clap::value_parser!(usize)),
)
.get_matches();
let path = matches.get_one::<String>("path").unwrap();
let manifest_path = matches.get_one::<String>("manifest").unwrap();
let interactive = matches.get_flag("interactive");
let export_file = matches.get_one::<String>("export");
let generate_suppression = matches.get_one::<String>("generate_suppression");
let directories: Vec<String> = matches
.get_many::<String>("directories")
.map(|vals| vals.cloned().collect())
.unwrap_or_default();
let include_paths: Vec<String> = matches
.get_many::<String>("include_paths")
.map(|vals| vals.cloned().collect())
.unwrap_or_default();
let fail_on_violation = matches.get_flag("fail_on_violation");
let fail_on_severity: Option<Severity> = matches
.get_one::<String>("fail_on_severity")
.map(|s| s.parse().expect("clap validated severity"));
let min_severity: Option<Severity> = matches
.get_one::<String>("min_severity")
.map(|s| s.parse().expect("clap validated severity"));
let rule_filter: Option<HashSet<String>> = matches
.get_one::<String>("rules")
.map(|s| s.split(',').map(|r| r.trim().to_string()).collect());
let diff_only = matches.get_flag("diff");
let suppress_file = matches.get_one::<String>("suppress_file");
let verbosity = matches.get_count("verbose");
let save_prescan = matches.get_one::<String>("save_prescan");
let load_prescan = matches.get_one::<String>("load_prescan");
let jobs = *matches.get_one::<usize>("jobs").unwrap();
let project_source = ProjectSource::open(path)?;
println!("Detected {} at: {}", project_source.source_type(), path);
let manifest = RuleManifest::load(manifest_path)?;
if let Some(gen_spec) = generate_suppression {
handle_generate_suppression(gen_spec)?;
return Ok(0);
}
if interactive {
let mut ui = TerminalUI::new(path, manifest, &directories, &include_paths)?;
ui.run()?;
return Ok(0);
}
println!("Analyzing {} at: {}", project_source.source_type(), path);
println!("Using manifest: {}", manifest_path);
if diff_only {
println!("Mode: diff-only (analyzing modified files)");
}
let progress_reporter = CLIProgressReporter::new(verbosity);
let results = analyze_project(
&project_source,
&manifest,
Some(&progress_reporter),
&directories,
&include_paths,
diff_only,
suppress_file.map(|s| s.as_str()),
save_prescan.map(|s| s.as_str()),
load_prescan.map(|s| s.as_str()),
jobs,
)?;
let mut violations = results.violations;
let suppressed = results.suppressed;
if let Some(ref min_sev) = min_severity {
violations.retain(|v| v.severity >= *min_sev);
}
if let Some(ref rules) = rule_filter {
violations.retain(|v| rules.contains(&v.rule_id));
}
let cwd = std::env::current_dir().unwrap_or_default();
for v in &violations {
let display_path = Path::new(&v.file_path)
.strip_prefix(&cwd)
.map(|p| p.to_string_lossy().into_owned())
.unwrap_or_else(|_| v.file_path.clone());
let sev = if v.needs_manual_review() {
format!("{}?", v.severity.to_string().to_lowercase())
} else {
v.severity.to_string().to_lowercase()
};
println!(
"{}:{}:{}: [{}] {}: {}",
display_path, v.line, v.column, sev, v.rule_id, v.message
);
if let Some(ref hint) = v.suggestion {
println!(" note: {}", hint);
}
}
if let Some(export_path) = export_file {
export_all_violations(&violations, &suppressed, export_path, &manifest)?;
println!(
"Exported {} violations ({} suppressed) to: {}",
violations.len(),
suppressed.len(),
export_path
);
}
println!(
"Total violations: {} ({} suppressed)",
violations.len(),
suppressed.len()
);
if fail_on_violation && !violations.is_empty() {
return Ok(1);
}
if let Some(ref threshold) = fail_on_severity {
if violations.iter().any(|v| v.severity >= *threshold) {
return Ok(1);
}
}
Ok(0)
}