use alloc::vec::Vec;
use vize_l0::{Span, cstr};
use super::{Rigor, Violation, ViolationCode};
use crate::folio::{FolioAttribute, FolioBinding, FolioIf, FolioOp, L2Folio};
type Owner = Option<(&'static str, Span)>;
pub(super) fn walk(folio: &L2Folio, rigor: Rigor, out: &mut Vec<Violation>) {
for op in &folio.ops {
visit_op(op, None, rigor, out);
}
}
fn keyword(op: &FolioOp) -> &'static str {
match op {
FolioOp::Element(_) => "ui.element",
FolioOp::Component(_) => "ui.component",
FolioOp::Text(_) => "ui.text",
FolioOp::Interpolation(_) => "ui.interpolation",
FolioOp::Comment(_) => "ui.comment",
FolioOp::If(_) => "ui.if",
FolioOp::For(_) => "ui.for",
FolioOp::Slot(_) => "ui.slot",
}
}
fn op_span(op: &FolioOp) -> Span {
match op {
FolioOp::Element(element) => element.span,
FolioOp::Component(component) => component.span,
FolioOp::Text(text) => text.span,
FolioOp::Interpolation(interpolation) => interpolation.span,
FolioOp::Comment(comment) => comment.span,
FolioOp::If(if_op) => if_op.span,
FolioOp::For(for_op) => for_op.span,
FolioOp::Slot(slot) => slot.span,
}
}
fn line_checks(kw: &'static str, span: Span, owner: Owner, out: &mut Vec<Violation>) {
if span.start > span.end {
out.push(Violation {
code: ViolationCode::SpanOrder,
span,
message: cstr!("`{kw}` span runs backwards: {}:{}", span.start, span.end),
});
}
if let Some((owner_kw, owner_span)) = owner
&& (span.start < owner_span.start || span.end > owner_span.end)
{
out.push(Violation {
code: ViolationCode::RegionNesting,
span,
message: cstr!(
"`{kw}` at {}:{} escapes its `{owner_kw}` owner {}:{}",
span.start,
span.end,
owner_span.start,
owner_span.end
),
});
}
}
fn visit_op(op: &FolioOp, owner: Owner, rigor: Rigor, out: &mut Vec<Violation>) {
let kw = keyword(op);
let span = op_span(op);
line_checks(kw, span, owner, out);
match op {
FolioOp::Element(element) => body(
kw,
span,
&element.attributes,
&element.bindings,
&element.children,
rigor,
out,
),
FolioOp::Component(component) => body(
kw,
span,
&component.attributes,
&component.bindings,
&component.children,
rigor,
out,
),
FolioOp::Text(_) | FolioOp::Interpolation(_) | FolioOp::Comment(_) => {}
FolioOp::If(if_op) => visit_if(if_op, rigor, out),
FolioOp::For(for_op) => {
for child in &for_op.ops {
visit_op(child, Some((kw, span)), rigor, out);
}
}
FolioOp::Slot(slot) => body(
kw,
span,
&slot.attributes,
&slot.bindings,
&slot.fallback,
rigor,
out,
),
}
}
fn body(
owner_kw: &'static str,
owner_span: Span,
attributes: &[FolioAttribute],
bindings: &[FolioBinding],
children: &[FolioOp],
rigor: Rigor,
out: &mut Vec<Violation>,
) {
let owner = Some((owner_kw, owner_span));
for attribute in attributes {
line_checks("attr", attribute.span, owner, out);
}
for binding in bindings {
match binding {
FolioBinding::Bind(bind) => {
line_checks("ui.bind", bind.span, owner, out);
}
FolioBinding::On(on) => {
line_checks("ui.on", on.span, owner, out);
}
FolioBinding::Model(model) => {
line_checks("ui.model", model.span, owner, out);
for attribute in &model.attributes {
line_checks("attr", attribute.span, Some(("ui.model", model.span)), out);
}
}
FolioBinding::SlotContent(content) => {
line_checks("ui.slot-content", content.span, owner, out);
}
FolioBinding::VueDirective(directive) => {
line_checks("vue.directive", directive.span, owner, out);
}
FolioBinding::VueCssBind(bind) => {
line_checks("vue.css-bind", bind.span, owner, out);
}
FolioBinding::VueSync(sync) => {
line_checks("vue.sync", sync.span, owner, out);
}
FolioBinding::VueSlotScope(scope) => {
line_checks("vue.slot-scope", scope.span, owner, out);
}
FolioBinding::VueOnce(once) => {
line_checks("vue.once", once.span, owner, out);
}
FolioBinding::VueMemo(memo) => {
line_checks("vue.memo", memo.span, owner, out);
}
FolioBinding::VueShow(show) => {
line_checks("vue.show", show.span, owner, out);
}
FolioBinding::VueHtml(html) => {
line_checks("vue.html", html.span, owner, out);
}
FolioBinding::VueText(text) => {
line_checks("vue.text", text.span, owner, out);
}
FolioBinding::VueCloak(cloak) => {
line_checks("vue.cloak", cloak.span, owner, out);
}
}
}
for child in children {
visit_op(child, owner, rigor, out);
}
}
fn visit_if(if_op: &FolioIf, rigor: Rigor, out: &mut Vec<Violation>) {
let canonical = rigor == Rigor::Canonical;
if canonical && if_op.branches.is_empty() {
out.push(Violation {
code: ViolationCode::EmptyIf,
span: if_op.span,
message: cstr!("`ui.if` owns no branch"),
});
}
let owner = Some(("ui.if", if_op.span));
let last = if_op.branches.len().saturating_sub(1);
for (index, branch) in if_op.branches.iter().enumerate() {
line_checks("branch", branch.span, owner, out);
if canonical && branch.condition.is_none() {
if index == 0 {
out.push(Violation {
code: ViolationCode::LeadingElse,
span: branch.span,
message: cstr!("`ui.if` opens with an unconditional branch"),
});
} else if index != last {
out.push(Violation {
code: ViolationCode::BranchOrder,
span: branch.span,
message: cstr!("unconditional branch before the trailing branch"),
});
}
}
for child in &branch.ops {
visit_op(child, Some(("branch", branch.span)), rigor, out);
}
}
}