#[path = "score_output_commands.rs"]
mod score_output;
use score_output::*;
use crate::cli::args::{LintProfileArg, ScoreOutputFormat};
use crate::models::{Error, Result};
use std::fs;
use std::path::Path;
pub(crate) fn score_command(
input: &Path,
format: ScoreOutputFormat,
detailed: bool,
dockerfile: bool,
runtime: bool,
show_grade: bool,
profile: Option<LintProfileArg>,
) -> Result<()> {
if detailed && !matches!(format, ScoreOutputFormat::Human) {
eprintln!(
"Warning: --detailed has no effect with {:?} format (dimensions are always included).",
format
);
}
let source = fs::read_to_string(input)
.map_err(|e| Error::Internal(format!("Failed to read {}: {}", input.display(), e)))?;
let filename = input.file_name().and_then(|n| n.to_str()).unwrap_or("");
let is_dockerfile = dockerfile
|| filename.eq_ignore_ascii_case("dockerfile")
|| filename.to_lowercase().ends_with(".dockerfile");
if is_dockerfile {
use crate::bash_quality::dockerfile_scoring::score_dockerfile;
use crate::linter::docker_profiler::{estimate_size, is_docker_available, PlatformProfile};
let score = score_dockerfile(&source)
.map_err(|e| Error::Internal(format!("Failed to score Dockerfile: {}", e)))?;
let platform_profile = match profile {
Some(LintProfileArg::Coursera) => PlatformProfile::Coursera,
_ => PlatformProfile::Standard,
};
let runtime_score = if runtime {
let estimate = estimate_size(&source);
let docker_available = is_docker_available();
Some(RuntimeScore::new(
&estimate,
platform_profile,
docker_available,
))
} else {
None
};
match format {
ScoreOutputFormat::Human => {
print_human_dockerfile_score_results(&score, detailed);
if let Some(ref rt) = runtime_score {
print_human_runtime_score(rt, platform_profile);
}
if show_grade {
print_combined_grade(&score, runtime_score.as_ref());
}
}
ScoreOutputFormat::Json => {
print_json_dockerfile_score_with_runtime(&score, runtime_score.as_ref());
}
ScoreOutputFormat::Markdown => {
print_markdown_dockerfile_score_results(&score, input);
if let Some(ref rt) = runtime_score {
print_markdown_runtime_score(rt);
}
}
}
} else {
use crate::bash_quality::scoring::score_script_with_file_type;
let score = score_script_with_file_type(&source, Some(input))
.map_err(|e| Error::Internal(format!("Failed to score script: {}", e)))?;
match format {
ScoreOutputFormat::Human => {
print_human_score_results(&score, detailed);
if show_grade {
println!("\nGrade: {} ({:.1}/10.0)", score.grade, score.score);
}
}
ScoreOutputFormat::Json => {
print_json_score_results(&score);
}
ScoreOutputFormat::Markdown => {
print_markdown_score_results(&score, input);
}
}
}
Ok(())
}
#[derive(Debug)]
pub(crate) struct RuntimeScore {
pub(crate) score: f64,
pub(crate) estimated_size: u64,
pub(crate) size_score: f64,
pub(crate) layer_score: f64,
pub(crate) bloat_count: usize,
pub(crate) docker_available: bool,
pub(crate) suggestions: Vec<String>,
}
impl RuntimeScore {
pub(crate) fn new(
estimate: &crate::linter::docker_profiler::SizeEstimate,
profile: crate::linter::docker_profiler::PlatformProfile,
docker_available: bool,
) -> Self {
let max_size = profile.max_size_bytes();
let size_score = Self::calculate_size_score(estimate.total_estimated, max_size);
let layer_count = estimate.layer_estimates.len();
let bloat_count = estimate.bloat_patterns.len();
let layer_score = Self::calculate_layer_score(layer_count, bloat_count);
let suggestions = Self::build_suggestions(
&estimate.bloat_patterns,
layer_count,
estimate.total_estimated,
max_size,
);
let score = (size_score * 0.6 + layer_score * 0.4).clamp(0.0, 100.0);
Self {
score,
estimated_size: estimate.total_estimated,
size_score,
layer_score,
bloat_count,
docker_available,
suggestions,
}
}
fn calculate_size_score(total_estimated: u64, max_size: u64) -> f64 {
if max_size == u64::MAX {
let five_gb = 5_000_000_000u64;
if total_estimated < five_gb {
100.0
} else {
let ratio = total_estimated as f64 / five_gb as f64;
(100.0 / ratio).clamp(0.0, 100.0)
}
} else {
let ratio = total_estimated as f64 / max_size as f64;
if ratio > 1.0 {
0.0
} else if ratio > 0.8 {
(1.0 - ratio) * 500.0
} else {
100.0 - (ratio * 50.0)
}
}
}
fn calculate_layer_score(layer_count: usize, bloat_count: usize) -> f64 {
let base = if layer_count <= 5 {
100.0
} else if layer_count <= 10 {
80.0
} else {
60.0
};
(base - (bloat_count as f64 * 20.0)).max(0.0)
}
fn build_suggestions(
bloat_patterns: &[crate::linter::docker_profiler::BloatPattern],
layer_count: usize,
total_estimated: u64,
max_size: u64,
) -> Vec<String> {
let mut suggestions: Vec<String> = bloat_patterns
.iter()
.map(|p| format!("{}: {}", p.code, p.remediation))
.collect();
if layer_count > 10 {
suggestions.push("Consider combining RUN commands to reduce layer count".to_string());
}
if total_estimated > max_size {
suggestions.push(format!(
"Image size ({:.1}GB) exceeds limit ({:.1}GB) - use smaller base image or multi-stage build",
total_estimated as f64 / 1_000_000_000.0,
max_size as f64 / 1_000_000_000.0
));
}
suggestions
}
pub(crate) fn grade(&self) -> &'static str {
match self.score as u32 {
95..=100 => "A+",
90..=94 => "A",
85..=89 => "A-",
80..=84 => "B+",
75..=79 => "B",
70..=74 => "B-",
65..=69 => "C+",
60..=64 => "C",
55..=59 => "C-",
50..=54 => "D",
_ => "F",
}
}
}