use crate::finding::{Finding, Severity};
use crate::instance::Outcome;
use crate::scope::{DocumentCheck, DocumentView, OutsideCheck};
use crate::shape::{LanguageRegime, 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>,
outside: Vec<Bound>,
}
struct Bound {
kind: String,
regime: String,
}
impl SourceForm {
pub fn over(shape: &Shape) -> Self {
let bound = shape
.kinds
.iter()
.filter_map(|kind| bind(&kind.name, shape.language_of(&kind.name)?))
.collect();
let outside = shape
.language
.iter()
.filter(|regime| !regime.outside_root.is_empty())
.filter_map(|regime| bind("", regime))
.collect();
SourceForm { bound, outside }
}
fn bound_to(&self, kind: &str) -> Option<&Bound> {
self.bound.iter().find(|bound| bound.kind == kind)
}
fn bound_for(&self, view: &DocumentView<'_>) -> Option<&Bound> {
match view.outside_regime() {
Some(regime) => self.outside.iter().find(|bound| bound.regime == regime),
None => self.bound_to(view.kind()),
}
}
}
fn bind(kind: &str, regime: &LanguageRegime) -> Option<Bound> {
if regime.source_form.as_deref() != Some(ONE_LINE_PER_BLOCK) {
return None;
}
Some(Bound {
kind: kind.to_string(),
regime: regime.name.clone(),
})
}
impl OutsideCheck for SourceForm {
fn binds_regime(&self, regime: &str) -> bool {
self.outside.iter().any(|bound| bound.regime == regime)
}
}
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_for(view) 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)
}
}