use crate::FileId; use crate::ast; use crate::rules::list_to_attrs_map::ListToAttrsMap; use crate::rules::manual_inherit::ManualInherit; use crate::rules::manual_inherit_from::ManualInheritFrom; use crate::rules::unquoted_uri::UnquotedUri; use crate::scope_graph::ScopeGraph; pub mod let_order; pub mod list_to_attrs_map; pub mod manual_inherit; pub mod manual_inherit_from; pub mod redundant_parameter_forwarding; pub mod single_use_binding; pub mod unquoted_uri; #[derive(Debug)] pub struct Diagnostic { pub file_id: FileId, pub finding: Finding, } #[derive(Debug)] pub struct Finding { pub rule_id: &'static str, pub range: ast::SourceRange, pub message: &'static str, pub binding: Option, pub expected_order: Option>, } impl std::fmt::Display for Diagnostic { fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!( formatter, "[{}] {}", self.finding.rule_id, self.finding.message )?; if let Some(binding) = &self.finding.binding { write!(formatter, ": {binding}")?; } Ok(()) } } pub trait NodeRule { type Node; const ID: &'static str; fn select(data: &ast::NodeData) -> Option<&Self::Node>; fn check(node: &Self::Node) -> Option<&'static str>; } pub trait TokenRule { type Token; const ID: &'static str; fn select(data: &ast::TokenData) -> Option<&Self::Token>; fn check(token: &Self::Token) -> Option<&'static str>; } pub fn run_rules(file: &ast::ParsedFile, graph: &ScopeGraph, source: &str) -> Vec { let node_diagnostics = file .nodes .iter() .flat_map(|node| { [ run_node_rule::(node), run_node_rule::(node), run_node_rule::(node), ] }) .flatten(); let token_diagnostics = file .tokens .iter() .flat_map(|token| [run_token_rule::(token)]) .flatten(); let let_order_diagnostics = let_order::check(graph, source) .into_iter() .map(|finding| Finding { rule_id: "let_order", range: finding.range, message: "Let bindings must follow dependency order", binding: None, expected_order: Some(finding.expected_order), }); let redundant_parameter_forwarding_diagnostics = redundant_parameter_forwarding::check(&file.root) .into_iter() .map(|finding| Finding { rule_id: "redundant_parameter_forwarding", range: finding.range, message: "Use the outer parameter instead of forwarding it", binding: None, expected_order: None, }); let single_use_binding_diagnostics = single_use_binding::check(graph) .into_iter() .map(|finding| Finding { rule_id: "single_use_binding", range: finding.range, message: "Inline this single-use let binding", binding: Some(finding.name), expected_order: None, }); node_diagnostics .chain(token_diagnostics) .chain(let_order_diagnostics) .chain(redundant_parameter_forwarding_diagnostics) .chain(single_use_binding_diagnostics) .filter(|finding| !is_suppressed(source, finding.range, finding.rule_id)) .map(|finding| Diagnostic { file_id: file.file_id, finding, }) .collect() } pub fn is_suppressed(source: &str, range: ast::SourceRange, rule_id: &str) -> bool { let line = source[range.start..].lines().next().unwrap_or_default(); let Some(comment) = line.split_once('#').map(|(_, comment)| comment.trim()) else { return false; }; if comment == "NOLINT" { return true; } comment .strip_prefix("NOLINT(") .and_then(|rules| rules.strip_suffix(')')) .is_some_and(|rules| { rules .split(',') .any(|candidate| candidate.trim() == rule_id) }) } fn run_node_rule(node: &ast::Node) -> Option where R: NodeRule, { let typed_node = R::select(&node.data)?; let message = R::check(typed_node)?; Some(Finding { rule_id: R::ID, range: node.range, message, binding: None, expected_order: None, }) } fn run_token_rule(token: &ast::Token) -> Option where R: TokenRule, { let typed_token = R::select(&token.data)?; let message = R::check(typed_token)?; Some(Finding { rule_id: R::ID, range: token.range, message, binding: None, expected_order: None, }) }