188 lines
5.3 KiB
Rust
188 lines
5.3 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_let;
|
|
pub mod redundant_parameter_forwarding;
|
|
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_let_diagnostics =
|
|
redundant_let::check(&file.root)
|
|
.into_iter()
|
|
.map(|finding| Finding {
|
|
rule_id: "redundant_let",
|
|
range: finding.range,
|
|
message: "Inline this returned let binding",
|
|
binding: None,
|
|
expected_order: None,
|
|
});
|
|
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,
|
|
});
|
|
node_diagnostics
|
|
.chain(token_diagnostics)
|
|
.chain(let_order_diagnostics)
|
|
.chain(redundant_let_diagnostics)
|
|
.chain(redundant_parameter_forwarding_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();
|
|
line_suppresses(line, rule_id) || preceding_line_suppresses(source, range, rule_id)
|
|
}
|
|
|
|
fn preceding_line_suppresses(source: &str, range: ast::SourceRange, rule_id: &str) -> bool {
|
|
let Some((preceding_source, _current_line)) = source[..range.start].rsplit_once('\n') else {
|
|
return false;
|
|
};
|
|
let preceding_line = preceding_source.lines().next_back().unwrap_or_default();
|
|
line_suppresses(preceding_line, rule_id)
|
|
}
|
|
|
|
fn line_suppresses(line: &str, rule_id: &str) -> bool {
|
|
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,
|
|
})
|
|
}
|