2025-06-16 16:46:22 +02:00
|
|
|
use crate::utils::project::get_project_info;
|
2025-06-17 16:46:45 +02:00
|
|
|
use console::style;
|
2025-06-16 16:46:22 +02:00
|
|
|
use std::path::Path;
|
2025-06-17 20:45:33 +02:00
|
|
|
use std::sync::{Arc, Mutex};
|
|
|
|
|
use r2d2::Pool;
|
|
|
|
|
use r2d2_sqlite::SqliteConnectionManager;
|
2025-06-17 16:46:45 +02:00
|
|
|
use crate::database::index::{IssueRow, Indexer};
|
2025-06-17 11:35:23 +02:00
|
|
|
use crate::patterns::Severity;
|
2025-06-17 16:46:45 +02:00
|
|
|
use crate::utils::config::Config;
|
2025-06-16 23:47:50 +02:00
|
|
|
use crate::walk::spawn_senders;
|
2025-06-17 19:54:03 +02:00
|
|
|
use rayon::prelude::*;
|
2025-06-23 18:25:10 +02:00
|
|
|
use std::collections::BTreeMap;
|
2025-06-23 20:27:16 +02:00
|
|
|
use dashmap::DashMap;
|
|
|
|
|
use crate::errors::NyxResult;
|
|
|
|
|
pub(crate) use crate::file::run_rules_on_file;
|
2025-06-17 16:46:45 +02:00
|
|
|
|
2025-06-17 19:54:03 +02:00
|
|
|
type DynError = Box<dyn std::error::Error + Send + Sync>;
|
|
|
|
|
|
2025-06-17 16:46:45 +02:00
|
|
|
#[derive(Debug)]
|
|
|
|
|
pub struct Diag {
|
|
|
|
|
pub(crate) path: String,
|
|
|
|
|
pub(crate) line: usize,
|
|
|
|
|
pub(crate) col: usize,
|
|
|
|
|
pub(crate) severity: Severity,
|
|
|
|
|
pub(crate) id: String,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Entry point called by the CLI.
|
2025-06-16 16:46:22 +02:00
|
|
|
pub fn handle(
|
|
|
|
|
path: &str,
|
|
|
|
|
no_index: bool,
|
|
|
|
|
rebuild_index: bool,
|
2025-06-17 16:46:45 +02:00
|
|
|
format: String,
|
2025-06-16 16:46:22 +02:00
|
|
|
database_dir: &Path,
|
|
|
|
|
config: &Config,
|
|
|
|
|
) -> Result<(), Box<dyn std::error::Error>> {
|
|
|
|
|
let scan_path = Path::new(path).canonicalize()?;
|
|
|
|
|
let (project_name, db_path) = get_project_info(&scan_path, database_dir)?;
|
2025-06-23 20:27:16 +02:00
|
|
|
|
|
|
|
|
println!("{} {}...\n", style("Checking").green().bold(), &project_name);
|
2025-06-17 16:46:45 +02:00
|
|
|
|
2025-06-23 17:49:15 +02:00
|
|
|
let diags: Vec<Diag> = if no_index {
|
2025-06-23 18:25:10 +02:00
|
|
|
scan_filesystem(&scan_path, config)?
|
2025-06-16 16:46:22 +02:00
|
|
|
} else {
|
|
|
|
|
if rebuild_index || !db_path.exists() {
|
2025-06-17 17:42:41 +02:00
|
|
|
tracing::debug!("Scanning filesystem index filesystem");
|
|
|
|
|
crate::commands::index::build_index(&project_name,&scan_path, &db_path, config)?;
|
2025-06-16 16:46:22 +02:00
|
|
|
}
|
2025-06-17 17:52:22 +02:00
|
|
|
|
2025-06-17 20:45:33 +02:00
|
|
|
let pool = Indexer::init(&db_path)?;
|
2025-06-23 17:49:15 +02:00
|
|
|
scan_with_index_parallel(&project_name, pool, config)?
|
|
|
|
|
};
|
2025-06-16 16:46:22 +02:00
|
|
|
|
2025-06-23 17:45:54 +02:00
|
|
|
tracing::debug!("Found {:?} issues.", diags.len());
|
2025-06-23 18:25:10 +02:00
|
|
|
|
|
|
|
|
if format == "console"
|
|
|
|
|
|| (format.is_empty() && config.output.default_format == "console")
|
|
|
|
|
{
|
2025-06-23 17:45:54 +02:00
|
|
|
tracing::debug!("Printing to console");
|
2025-06-23 18:25:10 +02:00
|
|
|
let mut grouped: BTreeMap<&str, Vec<&Diag>> = BTreeMap::new();
|
2025-06-17 16:46:45 +02:00
|
|
|
for d in &diags {
|
2025-06-23 18:25:10 +02:00
|
|
|
grouped.entry(&d.path).or_default().push(d);
|
|
|
|
|
}
|
2025-06-23 20:27:16 +02:00
|
|
|
|
|
|
|
|
for (path, issues) in &grouped {
|
2025-06-23 18:25:10 +02:00
|
|
|
println!("{}", style(path).blue().underlined());
|
|
|
|
|
for d in issues {
|
2025-06-23 20:27:16 +02:00
|
|
|
println!(" {:>4}:{:<4} [{}] {}",
|
|
|
|
|
d.line, d.col, d.severity, style(&d.id).bold());
|
2025-06-23 18:25:10 +02:00
|
|
|
}
|
2025-06-23 20:27:16 +02:00
|
|
|
println!();
|
2025-06-17 16:46:45 +02:00
|
|
|
}
|
2025-06-23 18:25:10 +02:00
|
|
|
|
2025-06-23 20:27:16 +02:00
|
|
|
println!("{} '{}' generated {} issues.",
|
|
|
|
|
style("warning").yellow().bold(),
|
2025-06-23 18:25:10 +02:00
|
|
|
style(project_name).white().bold(),
|
|
|
|
|
style(diags.len()).bold());
|
|
|
|
|
println!("\t"); // TODO: Add individual counts for different warning levels
|
2025-06-16 16:46:22 +02:00
|
|
|
}
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
|
2025-06-17 16:46:45 +02:00
|
|
|
// --------------------------------------------------------------------------------------------
|
|
|
|
|
// Scanning helpers
|
|
|
|
|
// --------------------------------------------------------------------------------------------
|
2025-06-16 23:47:50 +02:00
|
|
|
|
2025-06-17 16:46:45 +02:00
|
|
|
fn scan_filesystem(
|
|
|
|
|
root: &Path,
|
|
|
|
|
cfg: &Config,
|
2025-06-23 18:25:10 +02:00
|
|
|
) ->Result<Vec<Diag>, Box<dyn std::error::Error>> {
|
2025-06-17 16:46:45 +02:00
|
|
|
let rx = spawn_senders(root, cfg);
|
2025-06-17 19:54:03 +02:00
|
|
|
let acc = Mutex::new(Vec::new());
|
2025-06-23 20:27:16 +02:00
|
|
|
|
2025-06-17 19:54:03 +02:00
|
|
|
rx.into_iter()
|
|
|
|
|
.flatten()
|
2025-06-23 17:45:54 +02:00
|
|
|
.par_bridge()
|
2025-06-23 20:27:16 +02:00
|
|
|
.try_for_each(|path| {
|
2025-06-23 17:45:54 +02:00
|
|
|
let mut local = run_rules_on_file(&path, cfg).unwrap();
|
2025-06-23 18:25:10 +02:00
|
|
|
acc.lock().unwrap().append(&mut local);
|
2025-06-23 17:45:54 +02:00
|
|
|
Ok::<(), DynError>(())
|
2025-06-23 18:25:10 +02:00
|
|
|
}).unwrap();
|
2025-06-23 17:45:54 +02:00
|
|
|
|
2025-06-23 18:25:10 +02:00
|
|
|
Ok(acc.into_inner()?)
|
2025-06-16 23:47:50 +02:00
|
|
|
}
|
|
|
|
|
|
2025-06-23 20:27:16 +02:00
|
|
|
pub fn scan_with_index_parallel(
|
2025-06-17 16:46:45 +02:00
|
|
|
project: &str,
|
2025-06-17 20:45:33 +02:00
|
|
|
pool: Arc<Pool<SqliteConnectionManager>>,
|
2025-06-17 16:46:45 +02:00
|
|
|
cfg: &Config,
|
2025-06-23 20:27:16 +02:00
|
|
|
) -> NyxResult<Vec<Diag>> {
|
|
|
|
|
|
2025-06-17 20:45:33 +02:00
|
|
|
let files = {
|
|
|
|
|
let idx = Indexer::from_pool(project, &pool)?;
|
|
|
|
|
idx.get_files(project)?
|
|
|
|
|
};
|
|
|
|
|
|
2025-06-23 20:27:16 +02:00
|
|
|
// ① Collect per-path Vec<Diag> without a global mutex
|
|
|
|
|
let diag_map: DashMap<String, Vec<Diag>> = DashMap::new();
|
2025-06-17 20:45:33 +02:00
|
|
|
|
|
|
|
|
files.into_par_iter()
|
2025-06-23 20:27:16 +02:00
|
|
|
.for_each_init(
|
|
|
|
|
// ② A single Indexer per Rayon worker thread
|
|
|
|
|
|| Indexer::from_pool(project, &pool).expect("db pool"),
|
|
|
|
|
|idx, path| {
|
|
|
|
|
let needs_scan = idx.should_scan(&path).unwrap_or(true);
|
|
|
|
|
|
|
|
|
|
let mut diags = if needs_scan {
|
|
|
|
|
let d = run_rules_on_file(&path, cfg).unwrap_or_default();
|
|
|
|
|
let file_id = idx.upsert_file(&path).unwrap();
|
|
|
|
|
idx.replace_issues(
|
|
|
|
|
file_id,
|
|
|
|
|
d.iter().map(|d| IssueRow {
|
|
|
|
|
rule_id: &d.id,
|
|
|
|
|
severity: d.severity.as_db_str(),
|
|
|
|
|
line: d.line as i64,
|
|
|
|
|
col: d.col as i64,
|
|
|
|
|
}),
|
|
|
|
|
).ok();
|
|
|
|
|
d
|
|
|
|
|
} else {
|
|
|
|
|
idx.get_issues_from_file(&path).unwrap_or_default()
|
|
|
|
|
};
|
|
|
|
|
if !diags.is_empty() {
|
|
|
|
|
diag_map.entry(path.to_string_lossy().to_string())
|
|
|
|
|
.or_default()
|
|
|
|
|
.append(&mut diags);
|
|
|
|
|
}
|
2025-06-17 20:45:33 +02:00
|
|
|
}
|
2025-06-23 20:27:16 +02:00
|
|
|
);
|
2025-06-17 20:45:33 +02:00
|
|
|
|
2025-06-23 20:27:16 +02:00
|
|
|
// Optional, heavy: only vacuum on --rebuild-index
|
|
|
|
|
// if rebuild { idx.vacuum()?; }
|
2025-06-17 21:00:24 +02:00
|
|
|
|
2025-06-23 20:27:16 +02:00
|
|
|
// Flatten
|
|
|
|
|
Ok(diag_map.into_iter().flat_map(|(_, v)| v).collect())
|
2025-06-16 16:46:22 +02:00
|
|
|
}
|