use bern_ir as ir;
use bern_target_api::Diagnostic;
use std::collections::{BTreeMap, BTreeSet};
pub fn check(
components: &[ir::ComponentDef],
root: Option<&ir::Element>,
diagnostics: &mut Vec<Diagnostic>,
) {
let bodies: BTreeMap<&str, &ir::Node> = components
.iter()
.map(|component| (component.name.as_str(), &component.body))
.collect();
let root = root.cloned().map(ir::Node::Element);
let trees: Vec<&ir::Node> = bodies.values().copied().chain(root.as_ref()).collect();
check_buttons(&trees, &bodies, diagnostics);
check_toolbar_searches(&trees, diagnostics);
check_toolbar_dividers(&trees, diagnostics);
check_rails(&trees, diagnostics);
check_inspectors(&trees, diagnostics);
}
fn check_buttons(
trees: &[&ir::Node],
bodies: &BTreeMap<&str, &ir::Node>,
diagnostics: &mut Vec<Diagnostic>,
) {
for body in trees {
body.walk(&mut |node| {
let ir::Node::Element(button) = node else {
return;
};
if button.kind != "Button" || button.children.is_empty() {
return;
}
let content: Vec<ir::Node> = button
.children
.iter()
.filter(|child| !matches!(child, ir::Node::Element(menu) if menu.kind == "ContextMenu"))
.cloned()
.collect();
if let Some(control) = control_inside(&content, bodies) {
diagnostics.push(
Diagnostic::error(format!(
"a `Button` cannot hold `{}`: one press would mean two things; put the `{}` beside the button, for example in a `Stack`",
control.kind, control.kind
))
.at(control.source.clone()),
);
}
});
}
}
fn check_toolbar_searches(trees: &[&ir::Node], diagnostics: &mut Vec<Diagnostic>) {
for body in trees {
body.walk(&mut |node| {
let ir::Node::Element(toolbar) = node else {
return;
};
if toolbar.kind != "Toolbar" {
return;
}
for child in &toolbar.children {
if matches!(child, ir::Node::Element(_)) {
continue;
}
child.walk(&mut |node| {
if let ir::Node::Element(search) = node
&& search.kind == "Search"
{
diagnostics.push(
Diagnostic::error(
"a `Search` stands directly in its `Toolbar`, not inside `if`, `match` or `ForEach`",
)
.at(search.source.clone()),
);
}
});
}
});
}
}
fn check_toolbar_dividers(trees: &[&ir::Node], diagnostics: &mut Vec<Diagnostic>) {
for body in trees {
body.walk(&mut |node| {
let ir::Node::Element(toolbar) = node else {
return;
};
if toolbar.kind != "Toolbar" {
return;
}
let mut nested = Vec::new();
for child in &toolbar.children {
if !matches!(child, ir::Node::Element(_)) {
toolbar_dividers(std::slice::from_ref(child), &mut nested);
}
}
for divider in nested {
diagnostics.push(
Diagnostic::error(
"a `Divider` parts the groups of a `Toolbar` and stands directly in it, not inside `if`, `match` or `ForEach`",
)
.at(divider.source.clone()),
);
}
let items: Vec<&ir::Node> = toolbar
.children
.iter()
.filter(|child| !matches!(child, ir::Node::Element(e) if e.kind == "Search"))
.collect();
for (index, child) in items.iter().enumerate() {
let ir::Node::Element(divider) = child else {
continue;
};
if divider.kind != "Divider" {
continue;
}
let is_divider = |node: Option<&&ir::Node>| {
matches!(node, Some(ir::Node::Element(e)) if e.kind == "Divider")
};
if index == 0 || index + 1 == items.len() || is_divider(items.get(index + 1)) {
diagnostics.push(
Diagnostic::error(
"a `Divider` in a `Toolbar` parts two groups of items; put it between items",
)
.at(divider.source.clone()),
);
}
}
});
}
}
fn check_rails(trees: &[&ir::Node], diagnostics: &mut Vec<Diagnostic>) {
for body in trees {
let mut items = Vec::new();
let mut components = Vec::new();
body.walk(&mut |node| {
if let ir::Node::Element(rail) = node
&& rail.kind == "Rail"
{
rail_items(&rail.children, &mut items, &mut components);
}
});
for component in components {
diagnostics.push(
Diagnostic::error(format!(
"a `Rail` places elements by their `at` or `from` and `to`; put `{}` inside one, such as a `Column` with `at:`",
component.component
))
.at(component.source.clone()),
);
}
body.walk(&mut |node| {
let ir::Node::Element(element) = node else {
return;
};
let on_rail = items.iter().any(|item| std::ptr::eq(*item, element));
let at = element.prop("at").is_some();
let from = element.prop("from").is_some();
let to = element.prop("to").is_some();
let message = if on_rail {
match (at, from, to) {
(true, false, false) | (false, true, true) => None,
(false, false, false) => Some(
"an element on a `Rail` needs `at:` (a fraction of the rail) or `from:` and `to:`",
),
(true, _, _) => Some("write either `at:` or `from:` and `to:`, not both"),
_ => Some("`from:` and `to:` go together"),
}
} else if at || from || to {
Some("`at`, `from` and `to` place an element on the `Rail` it stands in")
} else {
None
};
if let Some(message) = message {
diagnostics.push(Diagnostic::error(message).at(element.source.clone()));
}
if element.prop("stagger").is_some()
&& !bern_ir::spec::STAGGERING.contains(&element.kind.as_str())
{
diagnostics.push(
Diagnostic::error(format!(
"`stagger` spaces out the children of a container (`{}`); `{}` has none to space",
bern_ir::spec::STAGGERING.join("`, `"),
element.kind
))
.at(element.source.clone()),
);
}
});
}
}
fn check_inspectors(trees: &[&ir::Node], diagnostics: &mut Vec<Diagnostic>) {
for body in trees {
let mut misplaced = Vec::new();
inspectors(std::slice::from_ref(*body), true, &mut misplaced);
for (inspector, message) in misplaced {
diagnostics.push(Diagnostic::error(message).at(inspector.source.clone()));
}
}
}
fn inspectors<'a>(
nodes: &'a [ir::Node],
direct: bool,
out: &mut Vec<(&'a ir::Element, &'static str)>,
) {
for node in nodes {
match node {
ir::Node::Element(element) => {
if element.kind == "Inspector" && !direct {
out.push((
element,
"an `Inspector` stands directly in the container it attaches to, not inside `if`, `match` or `ForEach`; `presented` shows and hides it",
));
}
let panels = element
.children
.iter()
.filter(|child| matches!(child, ir::Node::Element(e) if e.kind == "Inspector"))
.collect::<Vec<_>>();
if let Some(ir::Node::Element(extra)) = panels.get(1) {
out.push((extra, "a container has at most one `Inspector`"));
}
inspectors(&element.children, element.kind != "ForEach", out);
}
ir::Node::If(node) => {
for (_, body) in &node.branches {
inspectors(body, false, out);
}
inspectors(&node.otherwise, false, out);
}
ir::Node::IfLet(node) => {
inspectors(&node.then, false, out);
inspectors(&node.otherwise, false, out);
}
ir::Node::Match(node) => {
for arm in &node.arms {
inspectors(&arm.body, false, out);
}
}
ir::Node::Component(_) => {}
}
}
}
fn rail_items<'a>(
nodes: &'a [ir::Node],
items: &mut Vec<&'a ir::Element>,
components: &mut Vec<&'a ir::ComponentRef>,
) {
for node in nodes {
match node {
ir::Node::Element(element) if element.kind == "ForEach" => {
rail_items(&element.children, items, components);
}
ir::Node::Element(element) => items.push(element),
ir::Node::If(node) => {
for (_, body) in &node.branches {
rail_items(body, items, components);
}
rail_items(&node.otherwise, items, components);
}
ir::Node::IfLet(node) => {
rail_items(&node.then, items, components);
rail_items(&node.otherwise, items, components);
}
ir::Node::Match(node) => {
for arm in &node.arms {
rail_items(&arm.body, items, components);
}
}
ir::Node::Component(component) => components.push(component),
}
}
}
fn passive_video(element: &ir::Element) -> bool {
element.kind == "Video" && element.keyword("controls") == Some("none")
}
fn control_inside<'a>(
nodes: &'a [ir::Node],
bodies: &BTreeMap<&str, &'a ir::Node>,
) -> Option<&'a ir::Element> {
let mut pending: Vec<&'a ir::Node> = nodes.iter().collect();
let mut seen = BTreeSet::new();
while let Some(node) = pending.pop() {
let mut found = None;
node.walk(&mut |node| match node {
ir::Node::Element(element)
if found.is_none()
&& ir::spec::component(&element.kind).is_some_and(|spec| spec.interactive)
&& !passive_video(element) =>
{
found = Some(element);
}
ir::Node::Component(reference) if seen.insert(reference.component.clone()) => {
if let Some(body) = bodies.get(reference.component.as_str()) {
pending.push(body);
}
}
_ => {}
});
if found.is_some() {
return found;
}
}
None
}
fn toolbar_dividers<'a>(nodes: &'a [ir::Node], found: &mut Vec<&'a ir::Element>) {
for node in nodes {
match node {
ir::Node::Element(element) if element.kind == "Divider" => found.push(element),
ir::Node::Element(element) if element.kind == "ForEach" => {
toolbar_dividers(&element.children, found);
}
ir::Node::Element(_) | ir::Node::Component(_) => {}
ir::Node::If(node) => {
for (_, body) in &node.branches {
toolbar_dividers(body, found);
}
toolbar_dividers(&node.otherwise, found);
}
ir::Node::IfLet(node) => {
toolbar_dividers(&node.then, found);
toolbar_dividers(&node.otherwise, found);
}
ir::Node::Match(node) => {
for arm in &node.arms {
toolbar_dividers(&arm.body, found);
}
}
}
}
}