use std::path::Path;
use std::sync::Arc;
use crate::config::LintConfig;
use crate::index::PackageIndex;
use crate::linter::diagnostic::{Diagnostic, Severity};
use crate::resolve::{ModulePath, PackageSource};
use crate::semantic::SemanticModel;
use crate::syntax::{SyntaxElement, SyntaxKind, SyntaxNode};
pub mod correctness;
pub mod suspicious;
pub struct Example {
pub caption: &'static str,
pub source: &'static str,
}
pub fn all_rules() -> Vec<Box<dyn Rule>> {
vec![
Box::new(correctness::UnusedBinding),
Box::new(correctness::UnusedImport),
Box::new(correctness::DuplicateArgument),
Box::new(correctness::UnusedArgument),
Box::new(correctness::UndefinedName),
Box::new(correctness::BreakOutsideLoop),
Box::new(correctness::NotEqDefinition),
Box::new(correctness::UnusedTypeParameter),
Box::new(suspicious::AssignmentInCondition),
Box::new(suspicious::NothingComparison),
Box::new(suspicious::ConstantCondition),
Box::new(suspicious::ModuleShadowsParent),
]
}
pub fn all_rule_ids() -> Vec<&'static str> {
all_rules().iter().map(|r| r.id()).collect()
}
pub struct RuleContext<'a> {
pub path: Option<&'a Path>,
pub root: &'a SyntaxNode,
pub model: &'a SemanticModel,
pub resolution: Option<ResolutionContext<'a>>,
}
pub struct ResolutionContext<'a> {
pub packages: &'a dyn PackageSource,
pub workspace: Option<(Arc<PackageIndex>, ModulePath)>,
}
pub trait Rule: Send + Sync {
fn id(&self) -> &'static str;
fn default_severity(&self) -> Severity {
Severity::Warning
}
fn default_enabled(&self) -> bool {
true
}
fn description(&self) -> &'static str {
""
}
fn examples(&self) -> &'static [Example] {
&[]
}
fn interests(&self) -> &'static [SyntaxKind] {
&[]
}
fn check(&self, el: &SyntaxElement, ctx: &RuleContext<'_>, sink: &mut Vec<Diagnostic>) {
let _ = (el, ctx, sink);
}
fn check_file(&self, ctx: &RuleContext<'_>, sink: &mut Vec<Diagnostic>) {
let _ = (ctx, sink);
}
}
struct ConfiguredRule {
rule: Box<dyn Rule>,
severity: Severity,
}
pub struct ResolvedRules {
rules: Vec<ConfiguredRule>,
by_kind: Vec<Vec<usize>>,
any_node_rules: bool,
}
impl ResolvedRules {
pub fn resolve(config: &LintConfig) -> (Self, Vec<String>) {
let all = all_rules();
let select = config.select.as_deref();
let mut unknown = Vec::new();
for id in select
.into_iter()
.flatten()
.chain(&config.ignore)
.chain(config.severity.keys())
{
let recognized = all.iter().any(|rule| rule.id() == id);
if !recognized && !unknown.contains(id) {
unknown.push(id.clone());
}
}
let rules: Vec<ConfiguredRule> = all
.into_iter()
.filter(|rule| {
let enabled = match select {
Some(selected) => selected.iter().any(|id| id == rule.id()),
None => rule.default_enabled(),
};
enabled && !config.ignore.iter().any(|id| id == rule.id())
})
.map(|rule| {
let severity = config
.severity
.get(rule.id())
.copied()
.unwrap_or(rule.default_severity());
ConfiguredRule { rule, severity }
})
.collect();
let mut by_kind: Vec<Vec<usize>> = vec![Vec::new(); SyntaxKind::COUNT];
let mut any_node_rules = false;
for (i, configured) in rules.iter().enumerate() {
for kind in configured.rule.interests() {
by_kind[*kind as usize].push(i);
any_node_rules = true;
}
}
(
Self {
rules,
by_kind,
any_node_rules,
},
unknown,
)
}
pub fn run(&self, ctx: &RuleContext<'_>) -> Vec<Diagnostic> {
let mut all = Vec::new();
if self.any_node_rules {
for el in ctx.root.descendants_with_tokens() {
for &i in &self.by_kind[el.kind() as usize] {
let before = all.len();
self.rules[i].rule.check(&el, ctx, &mut all);
stamp_severity(&mut all[before..], self.rules[i].severity);
}
}
}
for configured in &self.rules {
let before = all.len();
configured.rule.check_file(ctx, &mut all);
stamp_severity(&mut all[before..], configured.severity);
}
if let Some(path) = ctx.path {
for diag in &mut all {
diag.path = Some(path.to_path_buf());
}
}
all.sort_by_key(|d| (d.range.start(), d.range.end(), d.rule));
all
}
pub fn is_empty(&self) -> bool {
self.rules.is_empty()
}
}
fn stamp_severity(diags: &mut [Diagnostic], severity: Severity) {
for diag in diags {
diag.severity = severity;
}
}
#[cfg(test)]
mod tests {
use super::*;
fn ids(v: &[&str]) -> Vec<String> {
v.iter().map(|s| s.to_string()).collect()
}
#[test]
fn resolve_flags_unknown_select_and_ignore_ids() {
let config = LintConfig {
select: Some(ids(&["unused-binding", "made-up-rule"])),
ignore: ids(&["also-bogus"]),
..Default::default()
};
let (_rules, unknown) = ResolvedRules::resolve(&config);
assert_eq!(unknown, ids(&["made-up-rule", "also-bogus"]));
}
#[test]
fn resolve_reports_no_unknowns_for_valid_ids() {
let config = LintConfig {
ignore: ids(&["unused-import"]),
..Default::default()
};
let (_rules, unknown) = ResolvedRules::resolve(&config);
assert!(unknown.is_empty());
}
#[test]
fn resolve_dedupes_repeated_unknown_ids() {
let config = LintConfig {
select: Some(ids(&["typo", "typo"])),
ignore: ids(&["typo"]),
..Default::default()
};
let (_rules, unknown) = ResolvedRules::resolve(&config);
assert_eq!(unknown, ids(&["typo"]));
}
#[test]
fn resolve_flags_unknown_severity_keys() {
let config = LintConfig {
severity: [("no-such-rule".to_string(), Severity::Error)].into(),
..Default::default()
};
let (_rules, unknown) = ResolvedRules::resolve(&config);
assert_eq!(unknown, ids(&["no-such-rule"]));
}
#[test]
fn resolved_severity_is_override_or_rule_default() {
let config = LintConfig {
severity: [("unused-binding".to_string(), Severity::Error)].into(),
..Default::default()
};
let (rules, _) = ResolvedRules::resolve(&config);
let severity_of = |id: &str| {
rules
.rules
.iter()
.find(|c| c.rule.id() == id)
.map(|c| c.severity)
.unwrap()
};
assert_eq!(severity_of("unused-binding"), Severity::Error);
assert_eq!(severity_of("duplicate-argument"), Severity::Error);
assert_eq!(severity_of("unused-import"), Severity::Warning);
}
}