use rowan::ast::AstNode as _;
use crate::ast::CallExpr;
use crate::linter::diagnostic::{Diagnostic, ViolationData};
use crate::linter::rules::matchers;
use crate::linter::rules::{Example, Rule, RuleContext};
use crate::syntax::{SyntaxElement, SyntaxKind};
pub struct UndesirableFunction;
const EXAMPLES: &[Example] = &[Example {
caption: "`attach()` is in the built-in set — it puts a data frame's columns \
on the search path, so later code silently depends on load order:",
source: "attach(mtcars)\nmean(mpg)\n",
}];
impl Rule for UndesirableFunction {
fn id(&self) -> &'static str {
"undesirable-function"
}
fn description(&self) -> &'static str {
"Flag a call to a function the project has banned, with the configured \
alternative as the suggestion.\n\nThe name -> suggestion map is set in \
`[lint.rules.undesirable-function]`: `functions` replaces the built-in \
set, `extend-functions` adds to it. The built-in set covers base-R \
functions that mutate global state (`attach`, `setwd`, `options`, \
`Sys.setenv`, ...) and the debugging entry points (`debug`, `trace`, \
...); `browser()` is left to the dedicated `browser` rule.\n\nOnly \
bare-name calls are flagged, and a locally redefined name is skipped. \
There is no autofix — the rule knows the call is unwanted, not what \
should replace it."
}
fn examples(&self) -> &'static [Example] {
EXAMPLES
}
fn default_enabled(&self) -> bool {
false
}
fn interests(&self) -> &'static [SyntaxKind] {
&[SyntaxKind::CALL_EXPR]
}
fn check(&self, el: &SyntaxElement, ctx: &RuleContext<'_>, sink: &mut Vec<Diagnostic>) {
let Some(call) = el.as_node().cloned().and_then(CallExpr::cast) else {
return;
};
let Some(name) = matchers::callee_name(&call) else {
return;
};
let Some(suggestion) = ctx.config.undesirable_function.lookup(&name) else {
return;
};
let confirmed = if ctx.symbols.is_base(&name) {
ctx.resolves_to_base(&call)
} else {
call.callee_token()
.is_some_and(|t| !ctx.is_locally_shadowed(t.text_range()))
};
if !confirmed {
return;
}
let range = call
.callee_token()
.map_or_else(|| call.syntax().text_range(), |t| t.text_range());
let message = ViolationData::new(
"undesirable-function",
format!("call to undesirable function `{name}`"),
);
let message = if suggestion.is_empty() {
message
} else {
message.with_suggestion(format!("Avoid `{name}()`: {suggestion}."))
};
sink.push(Diagnostic {
rule: "undesirable-function",
severity: Default::default(),
path: Default::default(),
range,
message,
fix: None,
});
}
}