fn format_output(
report: &DuplicateReport,
format: crate::cli::DuplicateOutputFormat,
) -> Result<String> {
match format {
crate::cli::DuplicateOutputFormat::Json => format_json_output(report),
crate::cli::DuplicateOutputFormat::Human => format_human_output(report),
crate::cli::DuplicateOutputFormat::Summary => {
format_text_output(report, DEFAULT_TOP_FILES, TextDetail::Summary)
}
crate::cli::DuplicateOutputFormat::Detailed => {
format_text_output(report, DEFAULT_TOP_FILES, TextDetail::Detailed)
}
crate::cli::DuplicateOutputFormat::Sarif => format_sarif_output(report),
crate::cli::DuplicateOutputFormat::Csv => format_csv_output(report),
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum TextDetail {
Summary,
Human,
Detailed,
}
const HUMAN_BLOCK_LIMIT: usize = 20;
fn count_of(report: &DuplicateReport, clone_type: CloneType) -> usize {
report
.duplicate_blocks
.iter()
.filter(|b| b.clone_type == clone_type)
.count()
}
fn format_json_output(report: &DuplicateReport) -> Result<String> {
let enhanced_json = serde_json::json!({
"total_duplicates": report.total_duplicates,
"duplicate_lines": report.duplicate_lines,
"total_lines": report.total_lines,
"duplication_percentage": report.duplication_percentage,
"duplicate_blocks": report.duplicate_blocks,
"file_statistics": report.file_statistics,
"exact_duplicates": count_of(report, CloneType::Exact),
"renamed_duplicates": count_of(report, CloneType::Renamed),
"structural_similarities": count_of(report, CloneType::NearMiss),
"metrics": {
"files_processed": report.file_statistics.len(),
"blocks_analyzed": report.duplicate_blocks.len()
}
});
Ok(serde_json::to_string_pretty(&enhanced_json)?)
}
#[provable_contracts_macros::contract("pmat-core.yaml", equation = "check_compliance")]
pub fn format_human_output(report: &DuplicateReport) -> Result<String> {
format_human_output_with_limit(report, DEFAULT_TOP_FILES)
}
const DEFAULT_TOP_FILES: usize = 10;
pub fn format_human_output_with_limit(
report: &DuplicateReport,
top_files: usize,
) -> Result<String> {
format_text_output(report, top_files, TextDetail::Human)
}
fn format_text_output(
report: &DuplicateReport,
top_files: usize,
detail: TextDetail,
) -> Result<String> {
let mut output = String::new();
write_header(&mut output)?;
write_summary(&mut output, report)?;
write_top_files_section(&mut output, report, top_files)?;
if detail != TextDetail::Summary {
write_duplicate_blocks_section(&mut output, report, detail)?;
}
Ok(output)
}
fn write_header(output: &mut String) -> Result<()> {
use crate::cli::colors as c;
use std::fmt::Write;
writeln!(output, "{}", c::header("Duplicate Code Analysis"))?;
writeln!(output)?;
Ok(())
}
fn write_summary(output: &mut String, report: &DuplicateReport) -> Result<()> {
use crate::cli::colors as c;
use std::fmt::Write;
writeln!(output, "{}", c::subheader("Summary"))?;
writeln!(
output,
" Total duplicate blocks: {}",
c::number(&report.total_duplicates.to_string())
)?;
writeln!(
output,
" Duplicate lines: {} / {}",
c::number(&report.duplicate_lines.to_string()),
c::number(&report.total_lines.to_string())
)?;
writeln!(
output,
" Duplication percentage: {}\n",
c::pct(report.duplication_percentage as f64, 5.0, 15.0)
)?;
Ok(())
}
fn write_top_files_section(
output: &mut String,
report: &DuplicateReport,
top_files: usize,
) -> Result<()> {
if report.file_statistics.is_empty() {
return Ok(());
}
use crate::cli::colors as c;
use std::fmt::Write;
writeln!(output, "{}\n", c::subheader("Top Files by Duplication"))?;
let sorted_files = get_sorted_file_stats(&report.file_statistics);
write_file_stats_list(output, &sorted_files, top_files)?;
Ok(())
}
fn get_sorted_file_stats(
file_stats: &std::collections::BTreeMap<String, FileStats>,
) -> Vec<(&String, &FileStats)> {
let mut sorted_files: Vec<_> = file_stats.iter().collect();
sorted_files.sort_by(|a, b| {
b.1.duplication_percentage
.partial_cmp(&a.1.duplication_percentage)
.unwrap_or(std::cmp::Ordering::Equal)
.then_with(|| a.0.cmp(b.0))
});
sorted_files
}
fn write_file_stats_list(
output: &mut String,
sorted_files: &[(&String, &FileStats)],
top_files: usize,
) -> Result<()> {
use crate::cli::colors as c;
use std::fmt::Write;
for (i, (file_path, stats)) in crate::cli::top_files_slice(sorted_files, top_files)
.iter()
.enumerate()
{
let filename = crate::cli::report_paths::report_path(file_path);
writeln!(
output,
" {}. {} - {} duplication ({} / {} lines)",
c::number(&(i + 1).to_string()),
c::path(filename),
c::pct(stats.duplication_percentage as f64, 5.0, 15.0),
c::number(&stats.duplicate_lines.to_string()),
c::number(&stats.total_lines.to_string()),
)?;
}
writeln!(output)?;
Ok(())
}
fn write_duplicate_blocks_section(
output: &mut String,
report: &DuplicateReport,
detail: TextDetail,
) -> Result<()> {
if report.duplicate_blocks.is_empty() {
return Ok(());
}
use crate::cli::colors as c;
use std::fmt::Write;
writeln!(output, "{}\n", c::subheader("Duplicate Blocks"))?;
let block_limit = match detail {
TextDetail::Detailed => usize::MAX,
_ => HUMAN_BLOCK_LIMIT,
};
write_block_details(output, &report.duplicate_blocks, block_limit)?;
if block_limit != usize::MAX {
write_remaining_blocks_count(output, report.duplicate_blocks.len())?;
}
Ok(())
}
fn write_block_details(
output: &mut String,
duplicate_blocks: &[DuplicateBlock],
limit: usize,
) -> Result<()> {
for (i, block) in duplicate_blocks.iter().enumerate().take(limit) {
write_block_header(output, i + 1, block)?;
write_block_locations(output, block)?;
write_block_preview(output, block)?;
}
Ok(())
}
fn write_block_header(output: &mut String, block_num: usize, block: &DuplicateBlock) -> Result<()> {
use crate::cli::colors as c;
use std::fmt::Write;
writeln!(
output,
" {}Block {}{} ({}, {} lines, {} locations)",
c::seq(c::BOLD),
block_num,
c::seq(c::RESET),
block.clone_type.label(),
c::number(&block.lines.to_string()),
c::number(&block.locations.len().to_string()),
)?;
Ok(())
}
fn write_block_locations(output: &mut String, block: &DuplicateBlock) -> Result<()> {
use crate::cli::colors as c;
use std::fmt::Write;
for loc in &block.locations {
writeln!(
output,
" {}{}{}:{}{}{}-{}{}{}",
c::seq(c::CYAN),
loc.file,
c::seq(c::RESET),
c::seq(c::BOLD_WHITE),
loc.start_line,
c::seq(c::RESET),
c::seq(c::BOLD_WHITE),
loc.end_line,
c::seq(c::RESET),
)?;
}
Ok(())
}
fn write_block_preview(output: &mut String, block: &DuplicateBlock) -> Result<()> {
use crate::cli::colors as c;
use std::fmt::Write;
writeln!(output, " {}Preview:{}", c::seq(c::DIM), c::seq(c::RESET))?;
writeln!(
output,
" {}{}{}",
c::seq(c::DIM),
block.locations[0].content_preview,
c::seq(c::RESET)
)?;
writeln!(output)?;
Ok(())
}
fn write_remaining_blocks_count(output: &mut String, total_blocks: usize) -> Result<()> {
if total_blocks > HUMAN_BLOCK_LIMIT {
use crate::cli::colors as c;
use std::fmt::Write;
writeln!(
output,
" {}... and {} more blocks{}",
c::seq(c::DIM),
total_blocks - HUMAN_BLOCK_LIMIT,
c::seq(c::RESET)
)?;
}
Ok(())
}
#[cfg_attr(coverage_nightly, coverage(off))]
#[cfg(test)]
mod top_files_is_a_row_limit_tests {
use super::*;
fn report_with_files(n: usize) -> DuplicateReport {
let mut file_statistics = BTreeMap::new();
for i in 0..n {
file_statistics.insert(
format!("src/f{i:02}.rs"),
FileStats {
duplicate_lines: n - i,
total_lines: 100,
duplication_percentage: (n - i) as f32,
},
);
}
DuplicateReport {
total_duplicates: 7,
duplicate_lines: 42,
total_lines: 1000,
duplication_percentage: 4.2,
duplicate_blocks: vec![],
file_statistics,
}
}
fn row_count(rendered: &str) -> usize {
rendered
.lines()
.filter(|l| l.contains(" duplication ("))
.count()
}
#[test]
fn top_files_limits_the_printed_rows() {
let report = report_with_files(25);
for requested in [1usize, 2, 3, 10, 17] {
let rendered = format_human_output_with_limit(&report, requested).unwrap();
assert_eq!(
row_count(&rendered),
requested,
"--top-files {requested} must print {requested} rows"
);
}
}
#[test]
fn top_files_zero_prints_every_file() {
let report = report_with_files(25);
let rendered = format_human_output_with_limit(&report, 0).unwrap();
assert_eq!(row_count(&rendered), 25);
}
#[test]
fn human_output_is_plain_text_when_colour_is_disabled() {
assert!(
!crate::cli::colors::colors_enabled(),
"cargo test captures stdout, so colour must resolve to off here"
);
let mut report = report_with_files(3);
report.duplicate_blocks = vec![DuplicateBlock {
hash: "deadbeef".to_string(),
lines: 12,
tokens: 40,
similarity: 1.0,
clone_type: CloneType::Exact,
locations: vec![
DuplicateLocation {
file: "src/a.rs".to_string(),
start_line: 1,
end_line: 12,
content_preview: "fn dup() {}".to_string(),
},
DuplicateLocation {
file: "src/b.rs".to_string(),
start_line: 40,
end_line: 51,
content_preview: "fn dup() {}".to_string(),
},
],
}];
let rendered = format_human_output(&report).unwrap();
assert!(
!rendered.contains('\u{1b}'),
"no ANSI escape may reach a redirected stdout: {:?}",
rendered
.lines()
.filter(|l| l.contains('\u{1b}'))
.collect::<Vec<_>>()
);
assert!(rendered.contains("Duplicate Code Analysis"));
assert!(rendered.contains("Block 1"));
assert!(rendered.contains("src/a.rs:1-12"));
assert!(rendered.contains("Preview:"));
}
fn report_with_blocks(n: usize) -> DuplicateReport {
let mut report = report_with_files(3);
report.duplicate_blocks = (0..n)
.map(|i| DuplicateBlock {
hash: format!("h{i:02}"),
lines: 6,
tokens: 20,
similarity: 1.0,
clone_type: CloneType::Exact,
locations: vec![
DuplicateLocation {
file: format!("src/a{i:02}.rs"),
start_line: 1,
end_line: 6,
content_preview: "fn dup() {}".to_string(),
},
DuplicateLocation {
file: format!("src/b{i:02}.rs"),
start_line: 1,
end_line: 6,
content_preview: "fn dup() {}".to_string(),
},
],
})
.collect();
report
}
fn block_count(rendered: &str) -> usize {
rendered
.lines()
.filter(|l| l.contains(" locations)"))
.count()
}
#[test]
fn summary_omits_the_per_block_listing() {
let report = report_with_blocks(3);
let summary = format_text_output(&report, DEFAULT_TOP_FILES, TextDetail::Summary).unwrap();
assert!(!summary.contains("Duplicate Blocks"), "{summary}");
assert_eq!(block_count(&summary), 0);
assert!(summary.contains("Total duplicate blocks:"));
assert!(summary.contains("Duplication percentage:"));
assert!(summary.contains("Top Files by Duplication"));
}
#[test]
fn the_three_text_formats_differ() {
let report = report_with_blocks(25);
let summary = format_text_output(&report, DEFAULT_TOP_FILES, TextDetail::Summary).unwrap();
let human = format_text_output(&report, DEFAULT_TOP_FILES, TextDetail::Human).unwrap();
let detailed =
format_text_output(&report, DEFAULT_TOP_FILES, TextDetail::Detailed).unwrap();
assert_eq!(block_count(&summary), 0);
assert_eq!(block_count(&human), HUMAN_BLOCK_LIMIT);
assert_eq!(block_count(&detailed), 25);
assert!(human.contains("... and 5 more blocks"));
assert!(
!detailed.contains("more blocks"),
"detailed lists every block, so nothing remains to summarise"
);
assert_ne!(summary, human);
assert_ne!(human, detailed);
}
#[test]
fn the_format_dispatcher_keeps_the_three_text_formats_apart() {
let report = report_with_blocks(25);
let of = |f| format_output(&report, f).unwrap();
let summary = of(crate::cli::DuplicateOutputFormat::Summary);
let human = of(crate::cli::DuplicateOutputFormat::Human);
let detailed = of(crate::cli::DuplicateOutputFormat::Detailed);
assert_ne!(summary, human);
assert_ne!(human, detailed);
assert_ne!(summary, detailed);
}
#[test]
fn sarif_reports_the_running_pmat_version() {
let report = report_with_blocks(1);
let sarif = format_sarif_output(&report).unwrap();
let parsed: serde_json::Value = serde_json::from_str(&sarif).unwrap();
let driver = &parsed["runs"][0]["tool"]["driver"];
assert_eq!(driver["version"], env!("CARGO_PKG_VERSION"));
assert_eq!(driver["semanticVersion"], env!("CARGO_PKG_VERSION"));
assert_ne!(driver["version"], "1.0.0");
assert_ne!(driver["semanticVersion"], "2.97.0");
}
#[test]
fn the_summary_is_identical_at_every_row_limit() {
let report = report_with_files(25);
let summary_of = |limit: usize| {
format_human_output_with_limit(&report, limit)
.unwrap()
.lines()
.filter(|l| l.contains("Duplication percentage") || l.contains("Duplicate lines"))
.collect::<Vec<_>>()
.join("\n")
};
let baseline = summary_of(1);
for limit in [2usize, 3, 10, 0] {
assert_eq!(
baseline,
summary_of(limit),
"limit {limit} changed the summary"
);
}
}
}
fn format_sarif_output(report: &DuplicateReport) -> Result<String> {
let sarif = serde_json::json!({
"$schema": "https://json.schemastore.org/sarif-2.1.0.json",
"version": "2.1.0",
"runs": [{
"tool": {
"driver": {
"name": "pmat-duplicates",
"version": env!("CARGO_PKG_VERSION"),
"informationUri": "https://github.com/paiml/paiml-mcp-agent-toolkit",
"semanticVersion": env!("CARGO_PKG_VERSION")
}
},
"results": report.duplicate_blocks.iter().map(|block| {
serde_json::json!({
"ruleId": "duplicate-code",
"level": "warning",
"message": {
"text": format!("Duplicate code block found ({} lines)", block.lines)
},
"locations": block.locations.iter().map(|loc| {
serde_json::json!({
"physicalLocation": {
"artifactLocation": {
"uri": loc.file
},
"region": {
"startLine": loc.start_line,
"endLine": loc.end_line
}
}
})
}).collect::<Vec<_>>()
})
}).collect::<Vec<_>>()
}]
});
Ok(serde_json::to_string_pretty(&sarif)?)
}
fn format_csv_output(report: &DuplicateReport) -> Result<String> {
let mut csv = String::new();
csv.push_str("Type,File1,Start1,End1,File2,Start2,End2\n");
for block in &report.duplicate_blocks {
let Some((first, rest)) = block.locations.split_first() else {
continue;
};
for other in rest {
csv.push_str(&format!(
"{},{},{},{},{},{},{}\n",
block.clone_type.label(),
first.file,
first.start_line,
first.end_line,
other.file,
other.start_line,
other.end_line
));
}
}
Ok(csv)
}
#[cfg_attr(coverage_nightly, coverage(off))]
#[cfg(test)]
mod clone_class_and_csv_completeness_tests {
use super::*;
use crate::cli::DuplicateType;
use std::path::Path;
const RENAMED_FOURFOLD: &str = "\
fn alpha(input: usize) -> usize {
let total = input + 1;
let doubled = total * 2;
doubled
}
fn beta(value: usize) -> usize {
let sum = value + 1;
let twice = sum * 2;
twice
}
fn gamma(arg: usize) -> usize {
let acc = arg + 1;
let scaled = acc * 2;
scaled
}
fn delta(operand: usize) -> usize {
let carry = operand + 1;
let bumped = carry * 2;
bumped
}
";
const IDENTICAL_TWICE: &str = "\
fn first_caller() -> usize {
let accumulator = compute_value();
let adjusted = accumulator + OFFSET;
let rounded = adjusted / DIVISOR;
rounded
}
fn second_caller() -> usize {
let accumulator = compute_value();
let adjusted = accumulator + OFFSET;
let rounded = adjusted / DIVISOR;
rounded
}
";
fn report_for(source: &str, kind: DuplicateType) -> DuplicateReport {
let lines: Vec<&str> = source.lines().collect();
let blocks = extract_blocks(&lines, Path::new("dup.rs"), 4, 1000, kind);
let duplicate_blocks = find_duplicate_blocks(blocks);
DuplicateReport {
total_duplicates: duplicate_blocks.len(),
duplicate_lines: 0,
total_lines: lines.len(),
duplication_percentage: 0.0,
duplicate_blocks,
file_statistics: BTreeMap::new(),
}
}
fn json_of(report: &DuplicateReport) -> serde_json::Value {
serde_json::from_str(&format_json_output(report).unwrap()).unwrap()
}
fn data_rows(csv: &str) -> Vec<&str> {
csv.lines().skip(1).filter(|l| !l.is_empty()).collect()
}
#[test]
fn renamed_clones_are_not_counted_as_exact() {
let report = report_for(RENAMED_FOURFOLD, DuplicateType::Renamed);
assert!(
report.total_duplicates > 0,
"fixture must produce renamed clone groups"
);
let json = json_of(&report);
assert_eq!(
json["exact_duplicates"], 0,
"no group here is byte identical: {json:#}"
);
assert_eq!(
json["renamed_duplicates"].as_u64().unwrap() as usize,
report.total_duplicates,
"every group here is Type-2: {json:#}"
);
for block in &json["duplicate_blocks"].as_array().unwrap().clone() {
assert_eq!(block["clone_type"], "renamed", "{block:#}");
assert!(
block["similarity"].as_f64().unwrap() < 1.0,
"a renamed clone is not a perfect match: {block:#}"
);
}
}
#[test]
fn identical_clones_are_still_counted_as_exact() {
let report = report_for(IDENTICAL_TWICE, DuplicateType::Exact);
assert!(report.total_duplicates > 0, "fixture must produce a group");
let json = json_of(&report);
assert_eq!(
json["exact_duplicates"].as_u64().unwrap() as usize,
report.total_duplicates,
"{json:#}"
);
assert_eq!(json["renamed_duplicates"], 0, "{json:#}");
for block in json["duplicate_blocks"].as_array().unwrap() {
assert_eq!(block["clone_type"], "exact", "{block:#}");
assert_eq!(block["similarity"].as_f64().unwrap(), 1.0, "{block:#}");
}
}
#[test]
fn the_csv_type_column_is_the_measured_clone_class() {
let renamed = report_for(RENAMED_FOURFOLD, DuplicateType::Renamed);
let csv = format_csv_output(&renamed).unwrap();
let rows = data_rows(&csv);
assert!(!rows.is_empty(), "fixture must produce CSV rows");
for row in &rows {
assert!(
row.starts_with("renamed,"),
"a Type-2 clone must not be exported as `exact`: {row}"
);
}
let exact = report_for(IDENTICAL_TWICE, DuplicateType::Exact);
let csv = format_csv_output(&exact).unwrap();
for row in data_rows(&csv) {
assert!(row.starts_with("exact,"), "{row}");
}
}
#[test]
fn the_csv_exports_every_located_site() {
let report = report_for(RENAMED_FOURFOLD, DuplicateType::Renamed);
assert!(
report
.duplicate_blocks
.iter()
.any(|b| b.locations.len() > 2),
"fixture must contain a family of more than two sites, else this \
test cannot see the defect"
);
let csv = format_csv_output(&report).unwrap();
let rows = data_rows(&csv);
let expected: usize = report
.duplicate_blocks
.iter()
.map(|b| b.locations.len().saturating_sub(1))
.sum();
assert_eq!(
rows.len(),
expected,
"one row per site past the first; got {} rows for {} sites",
rows.len(),
report
.duplicate_blocks
.iter()
.map(|b| b.locations.len())
.sum::<usize>()
);
for block in &report.duplicate_blocks {
for loc in &block.locations {
let needle = format!(",{},{}", loc.start_line, loc.end_line);
assert!(
rows.iter().any(|r| r.contains(&needle)),
"{}:{}-{} never reached the CSV",
loc.file,
loc.start_line,
loc.end_line
);
}
}
}
#[test]
fn the_csv_schema_is_unchanged() {
let report = report_for(RENAMED_FOURFOLD, DuplicateType::Renamed);
let csv = format_csv_output(&report).unwrap();
assert!(csv.starts_with("Type,File1,Start1,End1,File2,Start2,End2\n"));
for row in data_rows(&csv) {
assert_eq!(row.split(',').count(), 7, "{row}");
}
}
}