Files
dev/src/natu/rules.rs

177 lines
4.8 KiB
Rust

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<String>,
pub expected_order: Option<Vec<String>>,
}
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<Diagnostic> {
let node_diagnostics = file
.nodes
.iter()
.flat_map(|node| {
[
run_node_rule::<ListToAttrsMap>(node),
run_node_rule::<ManualInherit>(node),
run_node_rule::<ManualInheritFrom>(node),
]
})
.flatten();
let token_diagnostics = file
.tokens
.iter()
.flat_map(|token| [run_token_rule::<UnquotedUri>(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<R>(node: &ast::Node) -> Option<Finding>
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<R>(token: &ast::Token) -> Option<Finding>
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,
})
}