use super::names::{Binding, Level, Renderer};
use super::{Body, ComponentId, DocumentId, Resolved};
use crate::diagnostic::{Code, Diagnostic, Fix, Location, Stage};
use crate::nearest::nearest;
use crate::tree::{Code as CodeBlock, Node, Point};
use std::collections::HashSet;
pub(crate) struct Invocation<'a> {
pub(crate) name: &'a str,
pub(crate) start: Point,
}
pub(crate) fn invocations<'a>(nodes: &'a [Node], out: &mut Vec<Invocation<'a>>) {
for node in nodes {
let named = match node {
Node::ComponentContainer(container) => Some((&container.name, container.position)),
Node::ComponentLeaf(leaf) | Node::ComponentInline(leaf) => {
Some((&leaf.name, leaf.position))
}
_ => None,
};
if let Some((name, position)) = named {
out.push(Invocation {
name,
start: position.map(|position| position.start).unwrap_or_default(),
});
}
invocations(node.children(), out);
if let Node::ComponentContainer(container) = node {
for slot in &container.slots {
invocations(&slot.children, out);
}
}
}
}
pub(crate) fn code_blocks<'a>(nodes: &'a [Node], out: &mut Vec<&'a CodeBlock>) {
for node in nodes {
if let Node::Code(code) = node {
out.push(code);
}
code_blocks(node.children(), out);
if let Node::ComponentContainer(container) = node {
for slot in &container.slots {
code_blocks(&slot.children, out);
}
}
}
}
pub(super) fn report(resolved: &mut Resolved, report_ambient: bool) {
let main = resolved.main;
for id in resolved.ids() {
let root = &resolved.document(id).root;
if id == main {
let blocks = root
.children
.iter()
.map(|block| {
names_used(resolved, id, &[std::slice::from_ref(block)], report_ambient)
})
.collect();
resolved.diagnostics.blocks = blocks;
continue;
}
let bodies: Vec<&[Node]> = root
.children
.iter()
.filter(|node| matches!(node, Node::ComponentDefinition(_)))
.map(Node::children)
.collect();
let found = names_used(resolved, id, &bodies, report_ambient);
let list = if id < main {
&mut resolved.diagnostics.before
} else {
&mut resolved.diagnostics.after
};
list.extend(found);
}
let mut found = Vec::new();
resolved.cycles = recursion(resolved, &mut found);
resolved.diagnostics.after.extend(found);
}
pub(super) fn block(resolved: &Resolved, block: &Node, report_ambient: bool) -> Vec<Diagnostic> {
names_used(
resolved,
resolved.main,
&[std::slice::from_ref(block)],
report_ambient,
)
}
fn names_used(
resolved: &Resolved,
id: DocumentId,
bodies: &[&[Node]],
report_ambient: bool,
) -> Vec<Diagnostic> {
let document = resolved.document(id);
let lines = document.lines();
let file = resolved.file(id);
let mut used = Vec::new();
for body in bodies {
invocations(body, &mut used);
}
let mut found = Vec::new();
for invocation in used {
let at = lines.name_at(invocation.start);
let name = invocation.name;
let diagnostic = match document.names.get(name) {
None => {
let known = nearest(name, document.names.all());
let hint =
known.map_or_else(String::new, |known| format!("; did you mean `{known}`?"));
Diagnostic::new(Code::F002, at, format!("unknown component `{name}`{hint}"))
.with_fix(known.map(|known| Fix::replacing(at, name, known)))
}
Some(Binding::Ambiguous(sources)) => Diagnostic::new(
Code::F023,
at,
format!(
"`{name}` is provided by {}; list it in one import to choose",
sources.join(" and ")
),
),
Some(Binding::Component(_, Level::Ambient)) if report_ambient => Diagnostic::new(
Code::W113,
at,
format!(
"`{name}` comes from a pack this processor provides, so other processors show its fallback"
),
),
_ => continue,
};
found.push(diagnostic.in_stage(Stage::Resolve).in_file(file.clone()));
}
if report_ambient {
let mut blocks = Vec::new();
for body in bodies {
code_blocks(body, &mut blocks);
}
for code in blocks {
let renderer = code
.lang
.as_deref()
.and_then(|language| document.renderers.get(language));
let (Some(Renderer::Component(name, _)), Some(position)) = (renderer, code.position)
else {
continue;
};
if let Some(Binding::Component(_, Level::Ambient)) = document.names.get(name) {
found.push(
Diagnostic::new(
Code::W113,
lines.language_at(position.start),
format!("`{name}` draws this block and comes from a pack this processor provides, so other processors show the code"),
)
.in_stage(Stage::Resolve)
.in_file(file.clone()),
);
}
}
}
found
}
type Edge = (ComponentId, Location, Point);
fn recursion(resolved: &Resolved, found: &mut Vec<Diagnostic>) -> HashSet<(DocumentId, Point)> {
let edges: Vec<Vec<Edge>> = resolved
.components
.iter()
.map(|component| {
let Body::Compmd(body) = &component.body else {
return Vec::new();
};
let document = resolved.document(component.document);
let lines = document.lines();
let compmd = |name: &str| match document.names.get(name) {
Some(Binding::Component(callee, _))
if matches!(resolved.component(*callee).body, Body::Compmd(_)) =>
{
Some(*callee)
}
_ => None,
};
let mut used = Vec::new();
invocations(body, &mut used);
let invoked = used.into_iter().filter_map(|invocation| {
let callee = compmd(invocation.name)?;
Some((callee, lines.name_at(invocation.start), invocation.start))
});
let mut blocks = Vec::new();
code_blocks(body, &mut blocks);
let rendered = blocks.into_iter().filter_map(|code| {
code.info.as_ref()?;
let Renderer::Component(name, _) = document.renderers.get(code.lang.as_deref()?)?
else {
return None;
};
let start = code.position?.start;
Some((compmd(name)?, lines.language_at(start), start))
});
invoked.chain(rendered).collect()
})
.collect();
let mut search = Search {
resolved,
edges: &edges,
state: vec![State::New; edges.len()],
found,
cycles: HashSet::new(),
};
for component in 0..edges.len() {
if search.state[component] == State::New {
search.visit(ComponentId(component));
}
}
search.cycles
}
#[derive(Clone, Copy, PartialEq, Eq)]
enum State {
New,
Open,
Done,
}
struct Search<'a> {
resolved: &'a Resolved,
edges: &'a [Vec<Edge>],
state: Vec<State>,
found: &'a mut Vec<Diagnostic>,
cycles: HashSet<(DocumentId, Point)>,
}
impl Search<'_> {
fn visit(&mut self, component: ComponentId) {
self.state[component.0] = State::Open;
for &(callee, at, start) in &self.edges[component.0] {
match self.state[callee.0] {
State::Open => {
let caller = self.resolved.component(component);
self.cycles.insert((caller.document, start));
let name = &self.resolved.component(callee).name;
self.found.push(
Diagnostic::new(
Code::F019,
at,
format!("`{}` invokes `{name}`, which leads back to it: definitions can't recurse", caller.name),
)
.in_stage(Stage::Resolve)
.in_file(self.resolved.file(caller.document)),
);
}
State::New => self.visit(callee),
State::Done => {}
}
}
self.state[component.0] = State::Done;
}
}