use crate::{ast, Wrap}; #[derive(Debug, PartialEq, Eq)] pub struct Analysis { diagnostics: Vec, } impl Analysis { pub fn contains_error(&self) -> bool { self.diagnostics.iter().any(|d| d.level == Level::Error) } } impl std::fmt::Display for Analysis { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { for Diagnostic { level, kind } in &self.diagnostics { writeln!(f, "{level} {kind}")?; } Ok(()) } } #[derive(Debug, PartialEq, Eq)] pub struct Diagnostic { level: Level, kind: Kind, } #[derive(Debug, PartialEq, Eq)] pub enum Level { Warning, Error, } impl std::fmt::Display for Level { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { Level::Warning => write!(f, "[warning]"), Level::Error => write!(f, "[error]"), } } } #[derive(Debug, PartialEq, Eq)] pub enum Kind { InvalidPattern { invalid_node_kind: String, full_pattern: ast::Pattern, }, SimplifiablePattern { pattern: ast::TreePattern, simplified: ast::TreePattern, }, } impl std::fmt::Display for Kind { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { Self::InvalidPattern { invalid_node_kind, full_pattern, } => { write!( f, "invalid node kind `{invalid_node_kind}` in pattern `{full_pattern}`" ) } Self::SimplifiablePattern { pattern, simplified, } => { write!( f, "the pattern `{pattern}` can be reduced to just `{simplified}`" ) } } } } pub fn run(ctx: &super::Context) -> Analysis { [validate_patterns(ctx), redundant_list_pattern(ctx)] .into_iter() .flatten() .collect::>() .wrap(|diagnostics| Analysis { diagnostics }) } fn validate_patterns(ctx: &super::Context) -> Vec { fn check(pattern: &ast::TreePattern, language: &tree_sitter::Language) -> Option { match pattern { // base case ast::TreePattern::Atom(a) => { if language.id_for_node_kind(a, true) == 0 { Some(a.to_owned()) } else { None } } // recursive case ast::TreePattern::List(items) => { for item in items { check(item, language)?; } None } } } ctx.program .stanzas .iter() .flat_map(|s| match &s.pattern { ast::Pattern::Begin | ast::Pattern::End => None, ast::Pattern::Tree { matcher, .. } => { check(matcher, &ctx.language).map(|invalid_node_kind| Kind::InvalidPattern { invalid_node_kind, full_pattern: s.pattern.clone(), }) } }) .map(|kind| Diagnostic { level: Level::Error, kind, }) .collect() } fn redundant_list_pattern(ctx: &super::Context) -> Vec { fn simplify(pattern: &ast::TreePattern) -> ast::TreePattern { match pattern { ast::TreePattern::Atom(a) => ast::TreePattern::Atom(a.to_owned()), ast::TreePattern::List(l) => match l.as_slice() { [a] => simplify(a), items => ast::TreePattern::List(items.iter().map(simplify).collect::>()), }, } } ctx.program .stanzas .iter() .flat_map(|s| match &s.pattern { ast::Pattern::Begin | ast::Pattern::End => None, ast::Pattern::Tree { matcher, .. } => { let simplified = simplify(matcher); if &simplified != matcher { Some(Kind::SimplifiablePattern { pattern: matcher.clone(), simplified, }) } else { None } } }) .map(|kind| Diagnostic { level: Level::Warning, kind, }) .collect() }