mod body;
pub(crate) mod data;
pub(crate) mod params;
pub(crate) mod scope;
pub(crate) mod universal;
use crate::cel::{self, Program};
use crate::diagnostic::{Code, Diagnostic, Fix, Location, Stage};
use crate::lines::Lines;
use crate::parse::Spliced;
use crate::reserved;
use crate::resolve::{DocumentId, Resolved};
use crate::tree::{Node, Param, Point, Position};
use scope::Scope;
use std::collections::{HashMap, HashSet};
use std::rc::Rc;
#[derive(Clone, Debug, Default)]
pub(crate) struct Validated {
before: Vec<Diagnostic>,
blocks: Vec<Checks>,
imports: Checks,
after: Vec<Diagnostic>,
refused: HashSet<Refused>,
programs: Programs,
}
#[derive(Clone, Debug, Default)]
struct Checks {
diagnostics: Vec<Diagnostic>,
refused: Vec<Refused>,
bound_by_hosts: Vec<String>,
sources: Vec<String>,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
enum Refused {
Param(DocumentId, Point),
Data(DocumentId, usize),
Invocation(DocumentId, Point),
}
impl Refused {
fn document(self) -> DocumentId {
match self {
Self::Param(id, _) | Self::Data(id, _) | Self::Invocation(id, _) => id,
}
}
}
#[derive(Clone, Debug, Default)]
struct Programs(HashMap<String, (Rc<Program>, usize)>);
impl Programs {
fn parse(&mut self, source: &str) -> Result<Rc<Program>, cel::Error> {
if let Some((program, writers)) = self.0.get_mut(source) {
*writers += 1;
return Ok(Rc::clone(program));
}
let program = Rc::new(Program::parse(source)?);
self.0.insert(source.to_string(), (Rc::clone(&program), 1));
Ok(program)
}
fn release(&mut self, source: &str) {
if let Some((_, writers)) = self.0.get_mut(source) {
*writers -= 1;
if *writers == 0 {
self.0.remove(source);
}
}
}
}
impl Validated {
pub(crate) fn refuses_param(&self, id: DocumentId, param: &Param) -> bool {
param
.position
.is_some_and(|position| self.refused.contains(&Refused::Param(id, position.start)))
}
pub(crate) fn refuses_data(&self, id: DocumentId, index: usize) -> bool {
self.refused.contains(&Refused::Data(id, index))
}
pub(crate) fn program(&self, source: &str) -> Option<&Rc<Program>> {
self.programs.0.get(source).map(|(program, _)| program)
}
pub(crate) fn falls_back(&self, id: DocumentId, position: Option<Position>) -> bool {
position.is_some_and(|position| {
self.refused
.contains(&Refused::Invocation(id, position.start))
})
}
pub(crate) fn diagnostics(&self) -> impl Iterator<Item = &Diagnostic> {
self.before
.iter()
.chain(self.blocks.iter().flat_map(|block| &block.diagnostics))
.chain(&self.imports.diagnostics)
.chain(&self.after)
}
pub(crate) fn splice(
&mut self,
resolved: &Resolved,
spliced: &Spliced,
allowed_origins: &[String],
) -> bool {
let main = resolved.main;
let moved = spliced.moved;
for block in &mut self.blocks {
for diagnostic in &mut block.diagnostics {
moved.diagnostic(diagnostic);
}
for refused in &mut block.refused {
if let Refused::Param(_, start) | Refused::Invocation(_, start) = refused {
moved.point(start);
}
}
}
let start = spliced.blocks.start;
let blocks = &resolved.document(main).root.children[start..start + spliced.inserted];
let checked = self.blocks_of(resolved, blocks);
let removed: Vec<Checks> = self
.blocks
.splice(spliced.blocks.clone(), checked)
.collect();
for source in removed.iter().flat_map(|block| &block.sources) {
self.programs.release(source);
}
self.refused.retain(|refused| refused.document() != main);
let refused = self.blocks.iter().flat_map(|block| &block.refused);
self.refused.extend(refused.copied());
let data = std::mem::take(&mut self.imports.refused);
self.check_imports(resolved, allowed_origins);
data != self.imports.refused
}
fn blocks_of<'r>(&mut self, resolved: &'r Resolved, blocks: &'r [Node]) -> Vec<Checks> {
let main = resolved.main;
let scope = Scope::body(resolved, main);
let mut checker = Checker::new(resolved, main, &mut self.programs);
blocks
.iter()
.map(|block| {
checker.nodes(std::slice::from_ref(block), &scope);
std::mem::take(&mut checker.found)
})
.collect()
}
fn check_imports(&mut self, resolved: &Resolved, allowed_origins: &[String]) {
let bound = self
.blocks
.iter()
.flat_map(|block| block.bound_by_hosts.iter().cloned())
.collect();
let mut checker = Checker::new(resolved, resolved.main, &mut self.programs);
checker.bound_by_hosts = bound;
checker.inputs();
checker.data(allowed_origins);
self.imports = checker.found;
self.refused.extend(self.imports.refused.iter().copied());
}
}
pub(crate) fn validate(resolved: &Resolved, allowed_origins: &[String]) -> Validated {
let mut validated = Validated::default();
for id in resolved.ids() {
let document = resolved.document(id);
if id == resolved.main {
validated.blocks = validated.blocks_of(resolved, &document.root.children);
let refused = validated.blocks.iter().flat_map(|block| &block.refused);
validated.refused.extend(refused.copied());
validated.check_imports(resolved, allowed_origins);
continue;
}
let mut checker = Checker::new(resolved, id, &mut validated.programs);
for node in &document.root.children {
if let Node::ComponentDefinition(definition) = node {
checker.definition(definition);
}
}
checker.bound_by_hosts = checker.found.bound_by_hosts.iter().cloned().collect();
checker.data(allowed_origins);
let found = checker.found;
validated.refused.extend(found.refused);
let list = if id < resolved.main {
&mut validated.before
} else {
&mut validated.after
};
list.extend(found.diagnostics);
}
validated
}
struct Checker<'r, 'd> {
resolved: &'r Resolved,
id: DocumentId,
lines: Lines<'r>,
file: Option<String>,
bound_by_hosts: HashSet<String>,
areas: Option<Rc<[String]>>,
found: Checks,
programs: &'d mut Programs,
}
impl<'r, 'd> Checker<'r, 'd> {
fn new(resolved: &'r Resolved, id: DocumentId, programs: &'d mut Programs) -> Self {
Self {
resolved,
id,
lines: resolved.document(id).lines(),
file: resolved.file(id),
bound_by_hosts: HashSet::new(),
areas: None,
found: Checks::default(),
programs,
}
}
}
impl Checker<'_, '_> {
fn report(&mut self, code: Code, at: Location, message: impl Into<String>) {
self.report_fixed(code, at, message, None);
}
fn report_fixed(
&mut self,
code: Code,
at: Location,
message: impl Into<String>,
fix: Option<Fix>,
) {
self.found.diagnostics.push(
Diagnostic::new(code, at, message)
.in_stage(Stage::Validate)
.in_file(self.file.clone())
.with_fix(fix),
);
}
fn parse(&mut self, source: &str) -> Result<Rc<Program>, cel::Error> {
let program = self.programs.parse(source)?;
self.found.sources.push(source.to_string());
Ok(program)
}
fn refuse_param(&mut self, param: &Param) {
if let Some(position) = param.position {
self.found
.refused
.push(Refused::Param(self.id, position.start));
}
}
fn fall_back(&mut self, position: Option<Position>) {
if let Some(position) = position {
self.found
.refused
.push(Refused::Invocation(self.id, position.start));
}
}
fn refuse_data(&mut self, index: usize) {
self.found.refused.push(Refused::Data(self.id, index));
}
fn inputs(&mut self) {
let document = self.resolved.document(self.id);
for input in &document.settings.inputs {
let declaration = &input.declaration;
if let Some(why) = reserved::as_variable(&declaration.name) {
self.report(Code::F013, input.at, why);
}
if declaration.default.is_none() {
self.report(
Code::F010,
input.at,
format!("the input `{}` has no default, which every input needs so the document renders before a reader sets it; it starts empty", declaration.name),
);
}
}
}
}