use crate::ast::AstToken;
use crate::config::DiscouragedFunctionConfig;
use crate::linter::diagnostic::Diagnostic;
use crate::linter::rules::{Example, Rule, RuleContext, matchers};
use crate::syntax::{SyntaxElement, SyntaxKind};
pub struct DiscouragedFunction;
impl Rule for DiscouragedFunction {
fn id(&self) -> &'static str {
"discouraged-function"
}
fn description(&self) -> &'static str {
"Flag a call to a function on a configurable deny-list. The built-in \
set covers Base functions with process-wide or memory-unsafe effects — \
`exit`, `cd`, `redirect_stdout`, `redirect_stderr`, and the \
`unsafe_*`/pointer conversions — each reported with the alternative to \
reach for.\n\n\
Configure it under `[lint.rules.discouraged-function]`: `functions` \
replaces the built-in set, `extend-functions` adds to it (an entry \
there also rewords a built-in), and `functions = {}` silences the rule \
without ignoring it. Both are tables mapping a function name to the \
suggestion shown in the diagnostic.\n\n\
A call carrying a `do` block is never reported, since for `cd` and the \
`redirect_*` functions that form is the recommended alternative. A \
qualified callee (`Base.exit`) is a different name and does not match. \
A built-in name is only reported once it is confirmed to be Base's, so \
a local of the same name — or a file whose imports cannot be resolved — \
reports nothing; a name the project configured is reported unless a \
definition in the same file shadows it. No fix is offered, since the \
rewrite is a judgment call."
}
fn examples(&self) -> &'static [Example] {
&[Example {
caption: "`exit` ends the process and `cd` leaves the working \
directory changed:",
source: "function cleanup()\n cd(\"/tmp\")\n exit(1)\nend\n",
}]
}
fn interests(&self) -> &'static [SyntaxKind] {
&[SyntaxKind::CALL_EXPR]
}
fn check(&self, el: &SyntaxElement, ctx: &RuleContext<'_>, sink: &mut Vec<Diagnostic>) {
let Some(node) = el.as_node() else {
return;
};
let Some(call) = matchers::call_expr(node) else {
return;
};
if matchers::has_do_block(&call) {
return;
}
let Some(callee) = call.callee_ident() else {
return;
};
let config = &ctx.config.discouraged_function;
let Some(suggestion) = config.lookup(callee.text()) else {
return;
};
let confirmed = if DiscouragedFunctionConfig::is_builtin(callee.text()) {
ctx.resolves_to_base(&call)
} else {
!ctx.read_is_shadowed_locally(callee.syntax())
};
if !confirmed {
return;
}
sink.push(Diagnostic::new(
self.id(),
callee.syntax().text_range(),
format!("`{}` is discouraged: {suggestion}", callee.text()),
));
}
}