use std::{
collections::{HashMap, HashSet},
io::{IsTerminal, Read},
path::PathBuf,
};
use anyhow::{Context, Result};
use clap::Parser;
use colored::Colorize;
use gobject_linter::{
ast_context,
config::{self, OutputFormat},
fixer,
meson::MesonIntrospection,
output, reporter,
rules::Category,
scanner,
};
use indicatif::{ProgressBar, ProgressStyle};
use unidiff::PatchSet;
#[derive(Parser, Debug)]
#[command(name = "gobject-linter")]
#[command(about = "A fast tree-sitter-based linter for GObject/C code", long_about = None)]
struct Args {
#[arg(value_name = "DIRECTORY", default_value = ".")]
directory: PathBuf,
#[arg(short, long, value_name = "FILE", default_value = concat!(env!("CARGO_PKG_NAME"), ".toml"))]
config: PathBuf,
#[arg(short, long, value_name = "PATTERN")]
ignore: Vec<String>,
#[arg(short, long)]
verbose: bool,
#[arg(long)]
list_rules: bool,
#[arg(long, value_name = "RULE")]
only: Vec<String>,
#[arg(long, value_name = "RULE")]
exclude: Vec<String>,
#[arg(long, value_name = "CATEGORY")]
category: Option<Category>,
#[arg(long, value_enum)]
format: Option<OutputFormat>,
#[arg(long)]
fix: bool,
#[arg(long)]
summary: bool,
#[arg(long, value_name = "VERSION", value_parser = parse_glib_version_arg)]
min_glib_version: Option<(u32, u32)>,
#[arg(long)]
msvc_compatible: bool,
#[arg(long, value_name = "FILE")]
diff: Option<PathBuf>,
}
fn parse_glib_version_arg(s: &str) -> Result<(u32, u32), String> {
config::parse_glib_version(s).ok_or_else(|| {
format!(
"Invalid GLib version format: '{}'. Expected format: 'major.minor' (e.g., '2.76')",
s
)
})
}
fn main() -> Result<()> {
let args = Args::parse();
tracing_subscriber::fmt()
.with_env_filter(tracing_subscriber::EnvFilter::from_default_env())
.with_target(true)
.with_line_number(true)
.init();
let default_config = std::path::Path::new("gobject-linter.toml");
let is_explicit = args.config != default_config;
let config_path = if !args.config.exists() && !is_explicit {
let legacy = std::path::Path::new("goblint.toml");
if legacy.exists() {
legacy.to_path_buf()
} else {
args.config.clone()
}
} else {
args.config.clone()
};
if is_explicit && !config_path.exists() {
eprintln!(
"{} config file not found: {}",
"error:".red().bold(),
config_path.display()
);
std::process::exit(1);
}
let mut config = config::Config::load(&config_path)?;
let format = args.format.or(config.format).unwrap_or_default();
if matches!(
format,
OutputFormat::Json | OutputFormat::Sarif | OutputFormat::Gcc
) {
colored::control::set_override(false);
} else if !std::io::stdout().is_terminal() {
colored::control::set_override(false);
}
config.ignore.extend(args.ignore.clone());
if let Some(version) = args.min_glib_version {
config.min_glib_version = Some(version);
}
if args.msvc_compatible {
config.msvc_compatible = true;
}
if !args.only.is_empty()
&& let Err(e) = config.enable_only_rules(&args.only)
{
eprintln!("{} {}", "error:".red().bold(), e);
std::process::exit(1);
}
if !args.exclude.is_empty()
&& let Err(e) = config.disable_rules(&args.exclude)
{
eprintln!("{} {}", "error:".red().bold(), e);
std::process::exit(1);
}
if let Some(category) = args.category {
config.filter_by_category(category)?;
}
scanner::validate_config(&config)?;
if args.list_rules {
match format {
OutputFormat::Json => {
println!("{}", scanner::list_all_rules_json(&config));
}
_ => {
scanner::list_all_rules(&config);
}
}
return Ok(());
}
if !args.directory.exists() {
eprintln!(
"{} path '{}' does not exist",
"error:".red().bold(),
args.directory.display()
);
std::process::exit(1);
}
let project_root = args
.directory
.canonicalize()
.unwrap_or(args.directory.clone());
let ignore_matcher = config.build_ignore_matcher()?;
let spinner = if args.verbose {
let sp = ProgressBar::new_spinner();
sp.set_style(
ProgressStyle::default_spinner()
.template("{spinner:.green} {msg}")
.unwrap(),
);
sp.enable_steady_tick(std::time::Duration::from_millis(100));
Some(sp)
} else {
None
};
if let Some(ref sp) = spinner {
sp.set_message("Running meson introspection...");
}
let meson_introspection = MesonIntrospection::new(&project_root, config.build_dir.as_deref())
.ok()
.flatten();
let compiler_map = meson_introspection
.as_ref()
.and_then(|i| i.load_compiler_map().ok());
if args.verbose {
if let Some(ref m) = meson_introspection {
let gir_count = m.get_introspected_headers().len();
let installed_count = m.get_installed_headers().len();
let compile_commands_count = compiler_map
.as_ref()
.map_or(0, std::collections::HashMap::len);
println!(
"Meson introspection: {} GIR headers, {} installed headers, {} compile commands",
gir_count, installed_count, compile_commands_count
);
} else {
println!(
"Meson introspection not available - proceeding without public/private distinction"
);
}
}
let analysis_start = std::time::Instant::now();
if let Some(ref sp) = spinner {
sp.set_message("Parsing files...");
}
let ast_context = ast_context::AstContext::build_with_ignore(
&project_root,
&ignore_matcher,
spinner.as_ref(),
meson_introspection,
)?;
let parse_duration = analysis_start.elapsed();
let scan_start = std::time::Instant::now();
let (mut violations, rule_timings) = scanner::scan_with_ast(
&ast_context,
&config,
&project_root,
spinner.as_ref(),
!args.summary && !args.fix,
)?;
let scan_duration = scan_start.elapsed();
let analysis_duration = parse_duration + scan_duration;
if let Some(sp) = spinner {
sp.finish_and_clear();
}
if let Some(diff_path) = &args.diff {
let diff_content = if diff_path == std::path::Path::new("-") {
let mut buf = String::new();
std::io::stdin().read_to_string(&mut buf)?;
buf
} else {
std::fs::read_to_string(diff_path)?
};
let mut patch = PatchSet::new();
patch.parse(&diff_content).context("Failed to parse diff")?;
let git_root = {
let mut dir = project_root.as_path();
loop {
if dir.join(".git").exists() {
break dir.to_path_buf();
}
match dir.parent() {
Some(p) => dir = p,
None => break project_root.clone(),
}
}
};
let mut changed_lines: HashMap<std::path::PathBuf, HashSet<usize>> = HashMap::new();
for file in patch {
let path = git_root.join(file.path().trim_start_matches("b/"));
let lines = changed_lines.entry(path).or_default();
for hunk in file {
for line in hunk {
if line.is_added()
&& let Some(line_no) = line.target_line_no
{
lines.insert(line_no);
}
}
}
}
violations.retain(|v| {
changed_lines
.get(&v.file)
.is_some_and(|lines| lines.contains(&v.line))
});
}
if args.verbose {
let total_functions: usize = ast_context
.project
.files
.values()
.map(|f| f.iter_function_declarations().count() + f.iter_function_definitions().count())
.sum();
let total_gobject_types: usize = ast_context
.project
.files
.values()
.map(|f| f.iter_all_gobject_types().count())
.sum();
println!(
"Parsed {} files, {} functions, {} GObject types in {}",
ast_context.project.files.len(),
total_functions,
total_gobject_types,
reporter::format_duration(parse_duration),
);
}
if args.fix {
let rules = scanner::create_all_rules(&config);
let has_fixable_rules = rules
.iter()
.any(|entry| entry.level.is_enabled() && entry.rule.fixable());
if !has_fixable_rules {
eprintln!(
"Warning: --fix was specified but no enabled rules are auto-fixable.\n\
Run `gobject-linter --list-rules` to see which rules support auto-fix."
);
} else {
let fixed_count = fixer::apply_fixes(&violations)?;
println!("Fixed {} violation(s)", fixed_count);
}
return Ok(());
}
if args.summary {
let rules = scanner::create_all_rules(&config);
let fixable: std::collections::HashMap<&str, bool> = rules
.iter()
.map(|e| (e.rule.name(), e.rule.fixable()))
.collect();
reporter::report_summary(&violations, &fixable, &rule_timings, analysis_duration);
let has_errors = violations.iter().any(|v| v.level.is_error());
if has_errors {
std::process::exit(1);
}
return Ok(());
}
match format {
OutputFormat::Text => {
reporter::report_violations(&violations, args.verbose, &config, analysis_duration);
}
OutputFormat::Json => {
let json_output = serde_json::to_string_pretty(&violations)
.expect("Failed to serialize violations to JSON");
println!("{}", json_output);
}
OutputFormat::Sarif => {
let sarif_output = output::sarif::generate_sarif(&violations, &config, &project_root);
println!("{}", sarif_output);
}
OutputFormat::Gcc => {
output::gcc::generate_gcc(&violations);
}
OutputFormat::GitlabCodequality => {
let json =
output::gitlab_codequality::generate_gitlab_codequality(&violations, &project_root);
println!("{}", json);
}
}
let has_errors = violations.iter().any(|v| v.level.is_error());
if has_errors {
std::process::exit(1);
}
Ok(())
}