use crate::core::stats::aggregation::AggregatedStats;
use crate::core::stats::basic::ExtensionStats;
use crate::core::types::FileStats;
use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct FilterOptions {
pub min_lines: Option<usize>,
pub max_lines: Option<usize>,
pub min_size_bytes: Option<u64>,
pub max_size_bytes: Option<u64>,
pub min_complexity: Option<f64>,
pub max_complexity: Option<f64>,
pub min_functions: Option<usize>,
pub max_functions: Option<usize>,
pub min_quality_score: Option<f64>,
pub max_quality_score: Option<f64>,
pub min_doc_ratio: Option<f64>,
pub max_doc_ratio: Option<f64>,
pub include_languages: Vec<String>,
pub exclude_languages: Vec<String>,
pub show_complexity: bool,
pub show_quality: bool,
pub show_ratios: bool,
pub show_size_info: bool,
pub compact_output: bool,
}
#[derive(Debug, Clone, Copy, Default, PartialEq)]
pub struct FileComplexity {
pub function_count: usize,
pub max_cyclomatic: f64,
pub average_cyclomatic: f64,
}
impl FileComplexity {
pub fn index(
details: &[crate::core::stats::complexity::FunctionComplexityDetail],
) -> BTreeMap<String, FileComplexity> {
let mut totals: BTreeMap<String, (usize, f64, f64)> = BTreeMap::new();
for detail in details {
let entry = totals
.entry(detail.file_path.clone())
.or_insert((0, 0.0, 0.0));
let complexity = detail.cyclomatic_complexity as f64;
entry.0 += 1;
entry.1 += complexity;
entry.2 = entry.2.max(complexity);
}
totals
.into_iter()
.map(|(path, (count, sum, max))| {
(
path,
FileComplexity {
function_count: count,
max_cyclomatic: max,
average_cyclomatic: if count == 0 { 0.0 } else { sum / count as f64 },
},
)
})
.collect()
}
}
pub struct FileFilter {
options: FilterOptions,
}
impl FileFilter {
pub fn new(options: FilterOptions) -> Self {
Self { options }
}
pub fn needs_per_file_stats(options: &FilterOptions) -> bool {
options.min_lines.is_some()
|| options.max_lines.is_some()
|| options.min_size_bytes.is_some()
|| options.max_size_bytes.is_some()
|| options.min_doc_ratio.is_some()
|| options.max_doc_ratio.is_some()
|| options.min_quality_score.is_some()
|| options.max_quality_score.is_some()
|| !options.include_languages.is_empty()
|| !options.exclude_languages.is_empty()
|| Self::needs_complexity(options)
}
pub fn needs_complexity(options: &FilterOptions) -> bool {
options.min_complexity.is_some()
|| options.max_complexity.is_some()
|| options.min_functions.is_some()
|| options.max_functions.is_some()
}
pub fn passes_complexity_filter(&self, complexity: Option<&FileComplexity>) -> bool {
let measured = complexity.copied().unwrap_or_default();
let within = |value: f64, min: Option<f64>, max: Option<f64>| {
min.is_none_or(|m| value >= m) && max.is_none_or(|m| value <= m)
};
within(
measured.max_cyclomatic,
self.options.min_complexity,
self.options.max_complexity,
) && self
.options
.min_functions
.is_none_or(|m| measured.function_count >= m)
&& self
.options
.max_functions
.is_none_or(|m| measured.function_count <= m)
}
pub fn passes_filter(&self, file_path: &str, file_stats: &FileStats) -> bool {
if let Some(min_lines) = self.options.min_lines {
if file_stats.total_lines < min_lines {
return false;
}
}
if let Some(max_lines) = self.options.max_lines {
if file_stats.total_lines > max_lines {
return false;
}
}
if let Some(min_size) = self.options.min_size_bytes {
if file_stats.file_size < min_size {
return false;
}
}
if let Some(max_size) = self.options.max_size_bytes {
if file_stats.file_size > max_size {
return false;
}
}
let extension = std::path::Path::new(file_path)
.extension()
.and_then(|ext| ext.to_str())
.unwrap_or("no_ext")
.to_lowercase();
if !self.options.include_languages.is_empty()
&& !self
.options
.include_languages
.iter()
.any(|lang| lang.to_lowercase() == extension)
{
return false;
}
if !self.options.exclude_languages.is_empty()
&& self
.options
.exclude_languages
.iter()
.any(|lang| lang.to_lowercase() == extension)
{
return false;
}
if let Some(min_doc_ratio) = self.options.min_doc_ratio {
let doc_ratio = if file_stats.total_lines > 0 {
file_stats.doc_lines as f64 / file_stats.total_lines as f64
} else {
0.0
};
if doc_ratio < min_doc_ratio {
return false;
}
}
if let Some(max_doc_ratio) = self.options.max_doc_ratio {
let doc_ratio = if file_stats.total_lines > 0 {
file_stats.doc_lines as f64 / file_stats.total_lines as f64
} else {
0.0
};
if doc_ratio > max_doc_ratio {
return false;
}
}
if self.options.min_quality_score.is_some() || self.options.max_quality_score.is_some() {
let score = Self::quality_score(file_stats);
if self
.options
.min_quality_score
.is_some_and(|min| score < min)
|| self
.options
.max_quality_score
.is_some_and(|max| score > max)
{
return false;
}
}
true
}
fn quality_score(file_stats: &FileStats) -> f64 {
crate::core::stats::ratios::RatioStatsCalculator::new()
.calculate_ratio_stats(file_stats)
.map(|ratios| ratios.quality_metrics.overall_quality_score)
.unwrap_or(0.0)
}
}
pub struct ProjectFilter {
options: FilterOptions,
}
impl ProjectFilter {
pub fn new(options: FilterOptions) -> Self {
Self { options }
}
pub fn filter_extensions(
&self,
stats_by_extension: &BTreeMap<String, ExtensionStats>,
) -> BTreeMap<String, ExtensionStats> {
let mut filtered = BTreeMap::new();
for (ext, stats) in stats_by_extension {
if !self.options.include_languages.is_empty()
&& !self
.options
.include_languages
.iter()
.any(|lang| lang.to_lowercase() == ext.to_lowercase())
{
continue;
}
if !self.options.exclude_languages.is_empty()
&& self
.options
.exclude_languages
.iter()
.any(|lang| lang.to_lowercase() == ext.to_lowercase())
{
continue;
}
if let Some(min_lines) = self.options.min_lines {
if stats.total_lines < min_lines {
continue;
}
}
if let Some(max_lines) = self.options.max_lines {
if stats.total_lines > max_lines {
continue;
}
}
if let Some(min_size) = self.options.min_size_bytes {
if stats.total_size < min_size {
continue;
}
}
if let Some(max_size) = self.options.max_size_bytes {
if stats.total_size > max_size {
continue;
}
}
filtered.insert(ext.clone(), stats.clone());
}
filtered
}
}
pub struct FilterParser;
impl FilterParser {
pub fn parse_size(size_str: &str) -> Option<u64> {
const UNITS: [(&str, u64); 4] = [
("KB", 1024),
("MB", 1024 * 1024),
("GB", 1024 * 1024 * 1024),
("B", 1),
];
let upper = size_str.trim().to_uppercase();
let (number, multiplier) = UNITS
.iter()
.find_map(|(suffix, factor)| Some((upper.strip_suffix(suffix)?, *factor)))
.unwrap_or((upper.as_str(), 1));
let value: f64 = number.trim().parse().ok()?;
(value.is_finite() && value >= 0.0).then_some((value * multiplier as f64) as u64)
}
pub fn parse_languages(lang_str: &str) -> Vec<String> {
lang_str
.split(',')
.map(|s| s.trim().to_lowercase())
.filter(|s| !s.is_empty())
.collect()
}
}
pub struct FilteredOutputFormatter;
impl FilteredOutputFormatter {
pub fn format_enhanced_cli_output(
aggregated_stats: &AggregatedStats,
options: &FilterOptions,
) -> String {
let mut output = String::new();
output.push_str(&format!(
"{} files, {} lines",
aggregated_stats.basic.total_files, aggregated_stats.basic.total_lines
));
if options.show_size_info {
let size_mb = aggregated_stats.basic.total_size as f64 / (1024.0 * 1024.0);
output.push_str(&format!(", {:.1} MB", size_mb));
}
if options.show_complexity && aggregated_stats.complexity.function_count > 0 {
output.push_str(&format!(
", {:.1} avg complexity",
aggregated_stats.complexity.cyclomatic_complexity
));
}
if options.show_quality {
output.push_str(&format!(
", {:.1}/100 quality",
aggregated_stats
.ratios
.quality_metrics
.overall_quality_score
));
}
if options.show_ratios {
output.push_str(&format!(
", {:.1}% code",
aggregated_stats.ratios.code_ratio * 100.0
));
if aggregated_stats.ratios.comment_ratio > 0.0 {
output.push_str(&format!(
", {:.1}% comments",
aggregated_stats.ratios.comment_ratio * 100.0
));
}
}
output
}
}