use std::path::Path;
use rowan::ast::AstNode as _;
use crate::ast::{BinaryExpr, CallExpr};
use crate::project::{ExternalResolution, FileScope};
use crate::rindex::provider::CompositeProvider;
use crate::semantic::{PackageOrigin, SemanticModel, SymbolProvider};
use crate::syntax::{SyntaxElement, SyntaxKind, SyntaxNode};
use super::diagnostic::{Diagnostic, Severity};
pub mod correctness;
pub mod matchers;
pub mod suspicious;
pub fn all_rules() -> Vec<Box<dyn Rule>> {
vec![
Box::new(correctness::UndefinedSymbol),
Box::new(correctness::UnusedBinding),
Box::new(correctness::DuplicateFormal),
Box::new(correctness::DuplicatedArguments),
Box::new(correctness::EqualsNa),
Box::new(suspicious::AssignmentInCondition),
Box::new(suspicious::ShadowedBuiltin),
Box::new(suspicious::RedundantEquals),
Box::new(suspicious::RedundantIfelse),
]
}
pub fn all_rule_ids() -> Vec<&'static str> {
all_rules().iter().map(|r| r.id()).collect()
}
pub trait Rule: Send + Sync {
fn id(&self) -> &'static str;
fn default_severity(&self) -> Severity {
Severity::Warning
}
fn default_enabled(&self) -> bool {
true
}
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);
}
}
pub struct RuleContext<'a> {
pub path: &'a Path,
pub root: &'a SyntaxNode,
pub model: &'a SemanticModel,
pub symbols: &'a dyn SymbolProvider,
pub project: Option<&'a FileScope<'a>>,
pub resolution: Option<&'a ExternalResolution>,
}
impl RuleContext<'_> {
pub fn resolves_to_base(&self, call: &CallExpr) -> bool {
let Some(name) = matchers::callee_name(call) else {
return false;
};
if !self.symbols.is_base(&name) {
return false;
}
if is_namespace_qualified(call) {
return false;
}
if let Some(callee) = call.callee_token() {
let range = callee.text_range();
let shadowed = self
.model
.idents()
.iter()
.any(|i| i.range == range && self.model.resolve_local(i).is_some());
if shadowed {
return false;
}
}
origin_is_default(self.symbols.origin(&name, self.model.loaded_packages()))
}
}
fn is_namespace_qualified(call: &CallExpr) -> bool {
let Some(callee) = call.callee_token() else {
return false;
};
call.syntax()
.parent()
.and_then(BinaryExpr::cast)
.and_then(|bin| bin.namespace_access())
.is_some_and(|ns| ns.name_token.text_range() == callee.text_range())
}
fn origin_is_default(origin: PackageOrigin) -> bool {
let pkg = match &origin {
PackageOrigin::Resolved(pkg) => Some(pkg.as_str()),
PackageOrigin::Ambiguous(pkgs) => pkgs.last().map(|p| p.as_str()),
PackageOrigin::Unknown => None,
};
pkg.is_some_and(|p| crate::semantic::symbols::default_packages().contains(&p))
}
pub struct ResolvedRules {
pub rules: Vec<Box<dyn Rule>>,
}
impl ResolvedRules {
pub fn resolve(select: Option<&[String]>, ignore: &[String]) -> (Self, Vec<String>) {
let known = all_rule_ids();
let mut unknown = Vec::new();
for id in select.iter().flat_map(|v| v.iter()).chain(ignore.iter()) {
if !known.contains(&id.as_str()) {
unknown.push(id.clone());
}
}
let mut chosen: Vec<Box<dyn Rule>> = match select {
Some(picks) => all_rules()
.into_iter()
.filter(|r| picks.iter().any(|p| p == r.id()))
.collect(),
None => all_rules()
.into_iter()
.filter(|r| r.default_enabled())
.collect(),
};
chosen.retain(|r| !ignore.iter().any(|i| i == r.id()));
(Self { rules: chosen }, unknown)
}
pub fn default_set() -> Self {
let (set, _) = Self::resolve(None, &[]);
set
}
}
pub fn run_rules(
rules: &[Box<dyn Rule>],
path: &Path,
root: &SyntaxNode,
model: &SemanticModel,
symbols: &dyn SymbolProvider,
project: Option<&FileScope<'_>>,
resolution: Option<&ExternalResolution>,
) -> Vec<Diagnostic> {
let ctx = RuleContext {
path,
root,
model,
symbols,
project,
resolution,
};
let mut all = Vec::new();
let mut by_kind: Vec<Vec<usize>> = vec![Vec::new(); SyntaxKind::COUNT];
let mut any_node_rules = false;
for (i, rule) in rules.iter().enumerate() {
for kind in rule.interests() {
by_kind[*kind as usize].push(i);
any_node_rules = true;
}
}
if any_node_rules {
for el in root.descendants_with_tokens() {
for &i in &by_kind[el.kind() as usize] {
rules[i].check(&el, &ctx, &mut all);
}
}
}
for rule in rules {
rule.check_file(&ctx, &mut all);
}
all.sort_by(|a, b| {
(u32::from(a.range.start()), u32::from(a.range.end()), a.rule).cmp(&(
u32::from(b.range.start()),
u32::from(b.range.end()),
b.rule,
))
});
all
}
pub fn default_symbol_provider() -> CompositeProvider {
CompositeProvider::base_only()
}
#[cfg(test)]
mod tests {
use super::*;
fn resolves(src: &str) -> bool {
let root = crate::parser::parse(src).cst;
let model = SemanticModel::build(&root);
let symbols = crate::semantic::StaticBaseR::new();
let ctx = RuleContext {
path: Path::new("test.R"),
root: &root,
model: &model,
symbols: &symbols,
project: None,
resolution: None,
};
let call = root
.descendants()
.find_map(CallExpr::cast)
.expect("a call in the source");
ctx.resolves_to_base(&call)
}
#[test]
fn confirms_unshadowed_base_call() {
assert!(resolves("c(1, 2)"));
assert!(resolves("f <- function() sum(a)"));
}
#[test]
fn rejects_local_value_shadow() {
assert!(!resolves("c <- 1\nc(2, 3)"));
}
#[test]
fn rejects_function_redefinition() {
assert!(!resolves("any <- function(x) x\nany(z)"));
}
#[test]
fn rejects_nested_scope_shadow() {
assert!(!resolves("f <- function() {\n sum <- 1\n sum(a)\n}"));
}
#[test]
fn rejects_non_base_name() {
assert!(!resolves("frobnicate(1)"));
}
#[test]
fn rejects_qualified_callee() {
assert!(!resolves("dplyr::filter(x)"));
}
#[test]
fn rejects_computed_callee() {
assert!(!resolves("(g())(1)"));
}
}