use alloc::vec::Vec;
use vize_l0::{Span, cstr};
use super::{Rigor, Violation, ViolationCode};
use crate::dump::{
Attribute as DumpAttribute, Binding as DumpBinding, If as DumpIf, Op as DumpOp, Page as L2Page,
};
type Owner = Option<(&'static str, Span)>;
pub(super) fn walk(folio: &L2Page, rigor: Rigor, out: &mut Vec<Violation>) {
for op in &folio.ops {
visit_op(op, None, rigor, out);
}
}
fn keyword(op: &DumpOp) -> &'static str {
match op {
DumpOp::Element(_) => "ui.element",
DumpOp::Component(_) => "ui.component",
DumpOp::Text(_) => "ui.text",
DumpOp::Interpolation(_) => "ui.interpolation",
DumpOp::Comment(_) => "ui.comment",
DumpOp::If(_) => "ui.if",
DumpOp::For(_) | DumpOp::OriginalFor(_) => "ui.for",
DumpOp::Slot(_) => "ui.slot",
}
}
fn op_span(op: &DumpOp) -> Span {
match op {
DumpOp::Element(element) => element.span,
DumpOp::Component(component) => component.span,
DumpOp::Text(text) => text.span,
DumpOp::Interpolation(interpolation) => interpolation.span,
DumpOp::Comment(comment) => comment.span,
DumpOp::If(if_op) => if_op.span,
DumpOp::For(for_op) => for_op.span,
DumpOp::OriginalFor(for_op) => for_op.span,
DumpOp::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: &DumpOp, 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 {
DumpOp::Element(element) => body(
kw,
span,
&element.attributes,
&element.bindings,
&element.children,
rigor,
out,
),
DumpOp::Component(component) => body(
kw,
span,
&component.attributes,
&component.bindings,
&component.children,
rigor,
out,
),
DumpOp::Text(_) | DumpOp::Interpolation(_) | DumpOp::Comment(_) => {}
DumpOp::If(if_op) => visit_if(if_op, rigor, out),
DumpOp::For(for_op) => {
for child in &for_op.ops {
visit_op(child, Some((kw, span)), rigor, out);
}
}
DumpOp::OriginalFor(for_op) => {
for child in &for_op.ops {
visit_op(child, Some((kw, span)), rigor, out);
}
}
DumpOp::Slot(slot) => body(
kw,
span,
&slot.attributes,
&slot.bindings,
&slot.fallback,
rigor,
out,
),
}
}
fn body(
owner_kw: &'static str,
owner_span: Span,
attributes: &[DumpAttribute],
bindings: &[DumpBinding],
children: &[DumpOp],
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 {
DumpBinding::Bind(bind) => {
line_checks("ui.bind", bind.span, owner, out);
}
DumpBinding::On(on) => {
line_checks("ui.on", on.span, owner, out);
}
DumpBinding::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);
}
}
DumpBinding::SlotContent(content) => {
line_checks("ui.slot-content", content.span, owner, out);
}
DumpBinding::SlotsObject(object) => {
line_checks("ui.slots-object", object.span, owner, out);
}
DumpBinding::VueDirective(directive) => {
line_checks("vue.directive", directive.span, owner, out);
}
DumpBinding::VueCssBind(bind) => {
line_checks("vue.css-bind", bind.span, owner, out);
}
DumpBinding::VueSync(sync) => {
line_checks("vue.sync", sync.span, owner, out);
}
DumpBinding::VueSlotScope(scope) => {
line_checks("vue.slot-scope", scope.span, owner, out);
}
DumpBinding::VueOnce(once) => {
line_checks("vue.once", once.span, owner, out);
}
DumpBinding::VueMemo(memo) => {
line_checks("vue.memo", memo.span, owner, out);
}
DumpBinding::VueShow(show) => {
line_checks("vue.show", show.span, owner, out);
}
DumpBinding::VueHtml(html) => {
line_checks("vue.html", html.span, owner, out);
}
DumpBinding::VueText(text) => {
line_checks("vue.text", text.span, owner, out);
}
DumpBinding::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: &DumpIf, 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);
}
}
}