#![allow(clippy::doc_markdown)]
use std::io::{self, Write};
use crate::output::offenders::{OffenderRecord, TOOL_ID, warn_non_utf8_path};
const DEFAULT_COL: u32 = 1;
pub fn write_clang_warning<W: Write>(
offenders: &[OffenderRecord],
mut writer: W,
) -> io::Result<()> {
for record in offenders {
let Some(path) = warn_non_utf8_path("clang-warning", &record.path) else {
continue;
};
let line = record.start_line.max(1);
let col = record.start_col.unwrap_or(DEFAULT_COL).max(1);
writeln!(
writer,
"{path}:{line}:{col}: {severity}: {message} [{prefix}-{metric}]",
severity = record.severity.as_str(),
message = record.default_message(),
prefix = TOOL_ID,
metric = record.metric,
)?;
}
Ok(())
}
pub fn write_msvc_warning<W: Write>(offenders: &[OffenderRecord], mut writer: W) -> io::Result<()> {
for record in offenders {
let Some(raw_path) = warn_non_utf8_path("msvc-warning", &record.path) else {
continue;
};
let path = msvc_path(raw_path);
let line = record.start_line.max(1);
let col = record.start_col.unwrap_or(DEFAULT_COL).max(1);
writeln!(
writer,
"{path}({line},{col}): {severity} : {message}",
severity = record.severity.as_str(),
message = record.default_message(),
)?;
}
Ok(())
}
#[cfg(windows)]
fn msvc_path(raw: &str) -> std::borrow::Cow<'_, str> {
if raw.contains('/') {
std::borrow::Cow::Owned(raw.replace('/', "\\"))
} else {
std::borrow::Cow::Borrowed(raw)
}
}
#[cfg(not(windows))]
fn msvc_path(raw: &str) -> &str {
raw
}
#[cfg(test)]
#[allow(
clippy::float_cmp,
clippy::cast_precision_loss,
clippy::cast_possible_truncation,
clippy::cast_sign_loss,
clippy::similar_names,
clippy::doc_markdown,
clippy::needless_raw_string_hashes,
clippy::too_many_lines
)]
mod tests {
use super::*;
use crate::output::offenders::Severity;
use std::path::PathBuf;
fn rec(path: &str, metric: &str, value: f64, limit: f64) -> OffenderRecord {
OffenderRecord {
path: PathBuf::from(path),
function: Some("f".into()),
start_line: 42,
end_line: 50,
start_col: Some(5),
metric: metric.into(),
value,
limit,
severity: Severity::Warning,
}
}
fn render_clang(offenders: &[OffenderRecord]) -> String {
let mut buf = Vec::new();
write_clang_warning(offenders, &mut buf).expect("writing to Vec is infallible");
String::from_utf8(buf).expect("output is UTF-8")
}
fn render_msvc(offenders: &[OffenderRecord]) -> String {
let mut buf = Vec::new();
write_msvc_warning(offenders, &mut buf).expect("writing to Vec is infallible");
String::from_utf8(buf).expect("output is UTF-8")
}
#[test]
fn clang_empty_writes_nothing() {
assert_eq!(render_clang(&[]), "");
}
#[test]
fn msvc_empty_writes_nothing() {
assert_eq!(render_msvc(&[]), "");
}
#[test]
fn clang_single_offender() {
let out = render_clang(&[rec("src/foo.rs", "cyclomatic", 17.0, 15.0)]);
assert_eq!(
out,
"src/foo.rs:42:5: warning: cyclomatic 17 exceeds limit 15 [big-code-analysis-cyclomatic]\n"
);
}
#[test]
fn msvc_single_offender() {
let out = render_msvc(&[rec("src/foo.rs", "cyclomatic", 17.0, 15.0)]);
#[cfg(not(windows))]
assert_eq!(
out,
"src/foo.rs(42,5): warning : cyclomatic 17 exceeds limit 15\n"
);
#[cfg(windows)]
assert_eq!(
out,
"src\\foo.rs(42,5): warning : cyclomatic 17 exceeds limit 15\n"
);
}
#[test]
fn clang_missing_column_defaults_to_one() {
let mut r = rec("a.rs", "cognitive", 30.0, 15.0);
r.start_col = None;
let out = render_clang(&[r]);
assert!(out.starts_with("a.rs:42:1: warning: "), "{out}");
}
#[test]
fn msvc_missing_column_defaults_to_one() {
let mut r = rec("a.rs", "cognitive", 30.0, 15.0);
r.start_col = None;
let out = render_msvc(&[r]);
assert!(out.starts_with("a.rs(42,1): warning : "), "{out}");
}
#[test]
fn clang_error_severity_renders_error_token() {
let mut r = rec("a.rs", "cyclomatic", 99.0, 15.0);
r.severity = Severity::Error;
let out = render_clang(&[r]);
assert!(out.contains(": error: "), "{out}");
}
#[test]
fn msvc_error_severity_renders_error_token() {
let mut r = rec("a.rs", "cyclomatic", 99.0, 15.0);
r.severity = Severity::Error;
let out = render_msvc(&[r]);
assert!(out.contains("): error : "), "{out}");
}
#[test]
fn clang_integer_value_has_no_decimal_point() {
let out = render_clang(&[rec("a.rs", "cyclomatic", 17.0, 15.0)]);
assert!(out.contains("cyclomatic 17 exceeds limit 15"), "{out}");
assert!(!out.contains("17.0"), "{out}");
assert!(!out.contains("15.0"), "{out}");
}
#[test]
fn clang_fractional_value_renders_decimals() {
let out = render_clang(&[rec("a.rs", "halstead.volume", 12.5, 10.0)]);
assert!(
out.contains("halstead.volume 12.5 exceeds limit 10"),
"{out}"
);
}
#[test]
fn clang_zero_start_line_clamps_to_one() {
let mut r = rec("a.rs", "cyclomatic", 17.0, 15.0);
r.start_line = 0;
let out = render_clang(&[r]);
assert!(out.starts_with("a.rs:1:5: "), "{out}");
}
#[test]
fn msvc_zero_start_line_clamps_to_one() {
let mut r = rec("a.rs", "cyclomatic", 17.0, 15.0);
r.start_line = 0;
let out = render_msvc(&[r]);
assert!(out.starts_with("a.rs(1,5): "), "{out}");
}
#[test]
fn clang_multi_offender_one_line_each() {
let offenders = vec![
rec("src/alpha.rs", "cyclomatic", 17.0, 15.0),
rec("src/alpha.rs", "loc.lloc", 250.0, 100.0),
rec("src/zeta.rs", "cognitive", 30.0, 15.0),
];
let out = render_clang(&offenders);
assert_eq!(out.lines().count(), 3);
assert!(out.ends_with('\n'));
}
#[test]
fn clang_function_name_does_not_appear_in_line() {
let r = rec("a.rs", "cyclomatic", 17.0, 15.0);
let out = render_clang(&[r]);
assert_eq!(
out,
"a.rs:42:5: warning: cyclomatic 17 exceeds limit 15 [big-code-analysis-cyclomatic]\n"
);
}
#[test]
fn clang_empty_snapshot() {
insta::assert_snapshot!("clang_warning_empty", render_clang(&[]));
}
#[test]
fn clang_multi_snapshot() {
let mut err = rec("src/zeta.rs", "cognitive", 30.0, 15.0);
err.severity = Severity::Error;
err.start_col = None;
err.function = None;
let offenders = vec![
rec("src/alpha.rs", "cyclomatic", 17.0, 15.0),
rec("src/alpha.rs", "loc.lloc", 250.0, 100.0),
err,
];
insta::assert_snapshot!("clang_warning_multi", render_clang(&offenders));
}
#[test]
fn msvc_empty_snapshot() {
insta::assert_snapshot!("msvc_warning_empty", render_msvc(&[]));
}
#[cfg(not(windows))]
#[test]
fn msvc_multi_snapshot() {
let mut err = rec("src/zeta.rs", "cognitive", 30.0, 15.0);
err.severity = Severity::Error;
err.start_col = None;
err.function = None;
let offenders = vec![
rec("src/alpha.rs", "cyclomatic", 17.0, 15.0),
rec("src/alpha.rs", "loc.lloc", 250.0, 100.0),
err,
];
insta::assert_snapshot!("msvc_warning_multi", render_msvc(&offenders));
}
#[cfg(windows)]
#[test]
fn msvc_path_uses_backslashes_on_windows() {
let out = render_msvc(&[rec("src/foo/bar.rs", "cyclomatic", 17.0, 15.0)]);
assert!(out.starts_with("src\\foo\\bar.rs("), "{out}");
}
#[cfg(not(windows))]
#[test]
fn msvc_path_keeps_forward_slashes_off_windows() {
let out = render_msvc(&[rec("src/foo/bar.rs", "cyclomatic", 17.0, 15.0)]);
assert!(out.starts_with("src/foo/bar.rs("), "{out}");
}
}