use crate::finding::{Finding, Severity};
use crate::instance::Outcome;
use crate::scope::{DocumentCheck, DocumentView};
use crate::shape::Shape;
use headwater_doc::body::BlockKind;
pub const RULE: &str = "language.source_form.not_met";
const ONE_LINE_PER_BLOCK: &str = "one_line_per_block";
pub struct SourceForm {
bound: Vec<Bound>,
}
struct Bound {
kind: String,
regime: String,
}
impl SourceForm {
pub fn over(shape: &Shape) -> Self {
let mut bound = Vec::new();
for kind in &shape.kinds {
let Some(regime) = shape.language_of(&kind.name) else {
continue;
};
if regime.source_form.as_deref() != Some(ONE_LINE_PER_BLOCK) {
continue;
}
bound.push(Bound {
kind: kind.name.clone(),
regime: regime.name.clone(),
});
}
SourceForm { bound }
}
fn bound_to(&self, kind: &str) -> Option<&Bound> {
self.bound.iter().find(|bound| bound.kind == kind)
}
}
impl DocumentCheck for SourceForm {
const RULE: &'static str = self::RULE;
const VERSION: u32 = 1;
const NEEDS_BODY: bool = true;
fn instantiates(&self, kind: &str) -> bool {
self.bound_to(kind).is_some()
}
fn evaluate(&self, view: &DocumentView<'_>) -> Outcome {
let Some(bound) = self.bound_to(view.kind()) else {
return Outcome::Passed;
};
let Some(body) = view.body() else {
return Outcome::Passed;
};
let mut findings = Vec::new();
for block in &body.blocks {
if matches!(block.kind, BlockKind::Code | BlockKind::Html) {
continue;
}
for at in &block.soft_breaks {
findings.push(Finding {
rule: self::RULE,
severity: Severity::Error,
obligation: None,
path: view.path().to_string(),
line: at.end.line,
column: at.end.col,
message: format!(
"`{}` writes one {} on one line, and this line continues the {} above",
bound.regime,
block.kind.name(),
block.kind.name()
),
remediation: format!(
"join this line to the line above, or end the {} there",
block.kind.name()
),
patch: None,
});
}
}
findings.sort_by_key(|finding| (finding.line, finding.column));
Outcome::failed(findings)
}
}