use crate::config_schema::{MarkdownRule, markdown_rule_name};
use monochromatic_lint::{
diagnostic::{Diagnostic, Severity},
run_embedded::{EmbeddedFailure, EmbeddedRun},
};
pub const EXPLANATION_LIMIT: usize = 400;
pub const MAX_FIXED_BYTES: usize = 64 << 20;
pub const MAX_FINDINGS_BYTES: usize = 4 << 20;
pub const INCOMPLETE_STATUS: u8 = 2;
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct RemainingFinding {
pub rule: MarkdownRule,
pub line: u64,
pub column: u64,
pub message: String,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct LintRun {
pub fixed: Vec<u8>,
pub remaining: Vec<RemainingFinding>,
}
fn selected_rule(code: &str, rules: &[MarkdownRule]) -> Option<MarkdownRule> {
for rule in rules {
if code.strip_prefix("markdown/") == Some(markdown_rule_name(*rule)) {
return Some(*rule);
}
}
return None;
}
fn position(name: &str, value: usize) -> Result<u64, String> {
match u64::try_from(value) {
Ok(count) => return Ok(count),
Err(_) => return Err(format!("its {name} is not a position")),
}
}
fn remaining_finding(finding: &Diagnostic, rules: &[MarkdownRule]) -> Result<RemainingFinding, String> {
let Some(rule) = selected_rule(finding.code.as_str(), rules) else {
return Err(format!("its code {:?} is not a selected rule", finding.code));
};
if finding.severity != Severity::Warn {
return Err(String::from("its severity is not the configured \"warn\""));
}
let Some(label) = finding.labels.first() else {
return Err(String::from("it has no label with a position"));
};
return Ok(RemainingFinding {
rule,
line: position("line", label.span.line)?,
column: position("column", label.span.column)?,
message: finding.message.clone(),
});
}
pub fn explanation(text: &str) -> String {
if text.chars().count() <= EXPLANATION_LIMIT {
return String::from(text);
}
let kept: String = text.chars().take(EXPLANATION_LIMIT).collect();
return format!("{kept}...");
}
fn add_finding_bytes(total: usize, finding: &Diagnostic) -> usize {
return total
.saturating_add(finding.message.len())
.saturating_add(finding.code.len())
.saturating_add(finding.filename.len());
}
pub fn interpret_embedded_run(
input: &[u8],
rules: &[MarkdownRule],
outcome: Result<EmbeddedRun, EmbeddedFailure>,
) -> Result<LintRun, String> {
let run: EmbeddedRun = match outcome {
Ok(usable) => usable,
Err(failure) if failure.exit_status == INCOMPLETE_STATUS => {
return Err(format!(
"monochromatic-lint could not finish (exit status {INCOMPLETE_STATUS}): {}",
explanation(failure.explanation.as_str())
));
}
Err(failure) => {
return Err(format!(
"monochromatic-lint exited with status {}, which its one-rule configuration never produces: {}",
failure.exit_status,
explanation(failure.explanation.as_str())
));
}
};
if run.fixed.len() > MAX_FIXED_BYTES {
return Err(format!(
"monochromatic-lint produced a fixed source of {} bytes, over the {MAX_FIXED_BYTES} bytes cli-git accepts",
run.fixed.len()
));
}
let findings_bytes: usize = run.findings.iter().fold(0, add_finding_bytes);
if findings_bytes > MAX_FINDINGS_BYTES {
return Err(format!(
"monochromatic-lint produced findings of {findings_bytes} bytes, over the {MAX_FINDINGS_BYTES} bytes cli-git accepts"
));
}
if run.fixed.is_empty() && !input.is_empty() {
return Err(String::from(
"monochromatic-lint produced an empty fixed source for a file that is not empty",
));
}
let mut remaining: Vec<RemainingFinding> = Vec::new();
for (index, finding) in run.findings.iter().enumerate() {
match remaining_finding(finding, rules) {
Ok(found) => remaining.push(found),
Err(reason) => {
return Err(format!(
"monochromatic-lint returned finding {} that cli-git cannot use: {reason}",
index + 1
));
}
}
}
return Ok(LintRun {
fixed: run.fixed.into_bytes(),
remaining,
});
}
#[cfg(test)]
#[path = "markdown_linter_output_tests.rs"]
mod tests;