mirror of
https://github.com/dexoron/dcr.git
synced 2026-10-21 22:09:35 +00:00
190 lines
5.8 KiB
Rust
190 lines
5.8 KiB
Rust
// DCR — Cargo-like C/C++ project manager.
|
||
//
|
||
// Copyright (C) 2026 Dexoron (Bezotechestvo Vladimir) <main@dexoron.su>
|
||
//
|
||
// This program is free software: you can redistribute it and/or modify
|
||
// it under the terms of the GNU General Public License as published by
|
||
// the Free Software Foundation, either version 3 of the License, or
|
||
// (at your option) any later version.
|
||
//
|
||
// This program is distributed in the hope that it will be useful,
|
||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||
// GNU General Public License for more details.
|
||
//
|
||
// You should have received a copy of the GNU General Public License
|
||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||
|
||
use crate::core::build::common;
|
||
use crate::core::build_config::Config;
|
||
use crate::utils::build::{get_bool_with_profile, get_list_with_profile};
|
||
use crate::utils::fs::find_project_root;
|
||
use crate::utils::log::error;
|
||
use crate::utils::text::{BOLD_CYAN, BOLD_GREEN, BOLD_YELLOW, colored, printc};
|
||
use std::path::{Path, PathBuf};
|
||
use std::process::Command;
|
||
|
||
pub fn lint(args: &[String]) -> i32 {
|
||
if args.first().is_some_and(|a| a == "--help") {
|
||
printc("USAGE:", BOLD_GREEN);
|
||
printc(" dcr lint [--fix]", BOLD_CYAN);
|
||
println!();
|
||
printc("DESCRIPTION:", BOLD_GREEN);
|
||
println!(" Runs clang-tidy on all C/C++ source files.");
|
||
println!(" Scans source roots (configurable via build.roots) and tests/ directory.");
|
||
println!();
|
||
printc("OPTIONS:", BOLD_GREEN);
|
||
println!(" --fix Apply clang-tidy suggestions automatically");
|
||
return 0;
|
||
}
|
||
|
||
let do_fix = args.iter().any(|a| a == "--fix");
|
||
|
||
let start_dir = match std::env::current_dir() {
|
||
Ok(dir) => dir,
|
||
Err(_) => {
|
||
error("Failed to determine current directory");
|
||
return 1;
|
||
}
|
||
};
|
||
|
||
let root = match find_project_root(&start_dir) {
|
||
Ok(Some(r)) => r,
|
||
Ok(None) => {
|
||
error("dcr.toml not found");
|
||
return 1;
|
||
}
|
||
Err(_) => {
|
||
error("Failed to find project root");
|
||
return 1;
|
||
}
|
||
};
|
||
|
||
let config = match Config::open(&root.join("dcr.toml").to_string_lossy()) {
|
||
Ok(c) => c,
|
||
Err(e) => {
|
||
error(&format!("Failed to load dcr.toml: {e}"));
|
||
return 1;
|
||
}
|
||
};
|
||
|
||
let profile = "debug";
|
||
let build_roots = get_list_with_profile(&config, "roots", profile);
|
||
let src_disable = get_bool_with_profile(&config, "src_disable", profile, false);
|
||
|
||
let mut source_roots: Vec<String> = Vec::new();
|
||
for raw in &build_roots {
|
||
let p = Path::new(raw);
|
||
if p.is_absolute() {
|
||
source_roots.push(raw.clone());
|
||
} else {
|
||
source_roots.push(root.join(p).to_string_lossy().to_string());
|
||
}
|
||
}
|
||
if !src_disable && source_roots.is_empty() {
|
||
source_roots.push(root.join("src").to_string_lossy().to_string());
|
||
}
|
||
|
||
let cxx_extensions = ["c", "cpp", "cxx", "cc"];
|
||
let exclude_dirs: Vec<PathBuf> = Vec::new();
|
||
let include_paths: Vec<String> = Vec::new();
|
||
|
||
let roots: Vec<PathBuf> = source_roots.iter().map(PathBuf::from).collect();
|
||
let mut files =
|
||
match common::collect_sources(&roots, &cxx_extensions, &exclude_dirs, &include_paths) {
|
||
Ok(f) => f,
|
||
Err(e) => {
|
||
error(&format!("Failed to collect source files: {e}"));
|
||
return 1;
|
||
}
|
||
};
|
||
|
||
let tests_dir = root.join("tests");
|
||
if tests_dir.is_dir() {
|
||
let test_roots = vec![tests_dir];
|
||
if let Ok(mut test_files) =
|
||
common::collect_sources(&test_roots, &cxx_extensions, &exclude_dirs, &include_paths)
|
||
{
|
||
files.append(&mut test_files);
|
||
}
|
||
}
|
||
|
||
files.sort();
|
||
|
||
if files.is_empty() {
|
||
println!(" {} No files to lint", colored("Lint", BOLD_GREEN));
|
||
return 0;
|
||
}
|
||
|
||
println!(
|
||
" {} {} files {}",
|
||
colored("Linting", BOLD_GREEN),
|
||
files.len(),
|
||
if do_fix { "(with --fix)" } else { "" }
|
||
);
|
||
|
||
let mut total_issues = 0u32;
|
||
let mut failed = 0u32;
|
||
|
||
for file in &files {
|
||
let mut cmd = Command::new("clang-tidy");
|
||
if do_fix {
|
||
cmd.arg("--fix");
|
||
}
|
||
cmd.arg("--quiet");
|
||
cmd.arg(file);
|
||
|
||
let output = match cmd.output() {
|
||
Ok(o) => o,
|
||
Err(e) => {
|
||
error(&format!("Failed to execute clang-tidy: {}", e));
|
||
return 1;
|
||
}
|
||
};
|
||
|
||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||
let stdout = String::from_utf8_lossy(&output.stdout);
|
||
let combined = format!("{}{}", stdout, stderr);
|
||
|
||
let issues: Vec<&str> = combined
|
||
.lines()
|
||
.filter(|l| l.contains("warning:") || l.contains("error:"))
|
||
.collect();
|
||
let issue_count = issues.len();
|
||
|
||
if issue_count > 0 {
|
||
total_issues += issue_count as u32;
|
||
failed += 1;
|
||
print!(" {} ", colored("×", BOLD_YELLOW));
|
||
println!("{} ({} issues)", file, issue_count);
|
||
for issue in &issues {
|
||
println!(" {}", issue);
|
||
}
|
||
} else {
|
||
print!(" {} ", colored("✓", BOLD_GREEN));
|
||
println!("{}", file);
|
||
}
|
||
}
|
||
|
||
let total = files.len();
|
||
let passed = total - failed as usize;
|
||
|
||
if failed == 0 {
|
||
println!(
|
||
" {} {} files, no issues",
|
||
colored("Lint", BOLD_GREEN),
|
||
total
|
||
);
|
||
} else {
|
||
println!(
|
||
" {} {}/{} files passed, {} issues found",
|
||
colored("Lint", BOLD_YELLOW),
|
||
passed,
|
||
total,
|
||
total_issues
|
||
);
|
||
}
|
||
|
||
0
|
||
}
|