use rowan::TextRange;
use rowan::ast::AstNode as _;
use crate::ast::AssignmentExpr;
use crate::linter::diagnostic::{Diagnostic, Severity, ViolationData};
use crate::linter::rules::matchers::is_callee;
use crate::linter::rules::{Example, Rule, RuleContext};
use crate::semantic::BindingKind;
use crate::syntax::{SyntaxKind, SyntaxNode};
fn defining_assignment(root: &SyntaxNode, def_range: TextRange) -> Option<SyntaxNode> {
root.covering_element(def_range)
.into_token()
.and_then(|t| t.parent())
.filter(|p| p.kind() == SyntaxKind::ASSIGNMENT_EXPR)
}
fn binds_function_literal(assign: &SyntaxNode) -> bool {
AssignmentExpr::cast(assign.clone())
.and_then(|a| a.value_element())
.is_some_and(|value| value.kind() == SyntaxKind::FUNCTION_EXPR)
}
pub struct ShadowedBuiltin;
impl Rule for ShadowedBuiltin {
fn id(&self) -> &'static str {
"shadowed-builtin"
}
fn description(&self) -> &'static str {
"Flag a local binding to a function whose name is exported by a default R \
package when that name is later called in the same scope (`c <- \
function(...) ...; c(2, 3)`). A value binding (`names <- names(x)`) is \
exempt: R's call-position lookup skips non-function locals, so it is not \
a hazard."
}
fn examples(&self) -> &'static [Example] {
&[Example {
caption: "Binding a function over base `c()` and then calling it:",
source: "c <- function(x, y) x\nc(2, 3)\n",
}]
}
fn default_severity(&self) -> Severity {
Severity::Warning
}
fn check_file(&self, ctx: &RuleContext<'_>, sink: &mut Vec<Diagnostic>) {
for (binding_idx, binding) in ctx.model.bindings().iter().enumerate() {
if !matches!(binding.kind, BindingKind::Local) {
continue;
}
if !ctx.symbols.is_base(&binding.name) {
continue;
}
let Some(assign) = defining_assignment(ctx.root, binding.def_range) else {
continue;
};
if !binds_function_literal(&assign) {
continue;
}
let id = crate::semantic::BindingId(binding_idx as u32);
let def_end = assign.text_range().end();
let triggered = ctx.model.idents().iter().any(|ident| {
ident.name == binding.name
&& u32::from(ident.range.start()) >= u32::from(def_end)
&& is_callee(ctx.root, ident.range)
&& ctx.model.resolve_local(ident) == Some(id)
});
if !triggered {
continue;
}
sink.push(Diagnostic {
rule: "shadowed-builtin",
severity: Severity::Warning,
path: Default::default(),
range: binding.def_range,
message: ViolationData::new(
"shadowed-builtin",
format!(
"local binding `{}` shadows a base-R name later used in this scope",
binding.name
),
)
.with_suggestion(
"Rename the local, or fully qualify the base call (e.g. `base::c`).",
),
fix: None,
});
}
}
}