use rowan::ast::AstNode as _;
use rowan::{TextRange, TextSize};
use smol_str::SmolStr;
use crate::ast::AssignmentExpr;
use crate::linter::diagnostic::{Diagnostic, ViolationData};
use crate::linter::rules::{Example, Rule, RuleContext};
use crate::semantic::{Binding, BindingId, BindingKind};
use crate::syntax::{SyntaxKind, SyntaxNode};
use crate::text::LineIndex;
const EXAMPLES: &[Example] = &[
Example {
caption: "The first `calc` is replaced before it is ever called:",
source: "calc <- function(x) {\n x + 1\n}\n\ncalc <- function(x) {\n x * 2\n}\n\ncalc(3)\n",
},
Example {
caption: "The same mistake inside a function body:",
source: "process <- function(data) {\n clean <- function(x) x[!is.na(x)]\n clean <- function(x) x[x > 0]\n clean(data)\n}\n",
},
];
pub struct DuplicatedFunctionDefinition;
impl Rule for DuplicatedFunctionDefinition {
fn id(&self) -> &'static str {
"duplicated-function-definition"
}
fn description(&self) -> &'static str {
"Flag a function name defined twice among the same run of statements, \
where the earlier definition is replaced before it is ever used. R \
evaluates both assignments, so every call reaches the second one and \
the first body is dead code — nearly always a copy-paste or merge \
artifact.\n\nOnly definitions that are siblings in one statement list \
are paired, so definition-by-condition (`if (x) f <- function() 1 \
else f <- function() 2`) is not flagged: only one branch runs. A \
redefinition that follows a genuine use of the earlier definition is \
not flagged either — that is a deliberate rewrite, not a duplicate. \
No fix is offered: which definition to keep is a judgement call."
}
fn examples(&self) -> &'static [Example] {
EXAMPLES
}
fn check_file(&self, ctx: &RuleContext<'_>, sink: &mut Vec<Diagnostic>) {
let mut sites: Vec<DefSite> = ctx
.model
.bindings()
.iter()
.enumerate()
.filter(|(_, b)| b.kind == BindingKind::Local)
.filter_map(|(idx, b)| DefSite::of(ctx.root, BindingId::from_index(idx), b))
.collect();
sites.sort_by_key(|site| site.def_range.start());
let mut lines: Option<LineIndex> = None;
for (i, cur) in sites.iter().enumerate() {
let Some(prev) = sites[..i]
.iter()
.rev()
.find(|prev| prev.list == cur.list && prev.name == cur.name)
else {
continue;
};
if prev.is_used_before(ctx, cur.def_range.start()) {
continue;
}
let index = lines.get_or_insert_with(|| LineIndex::new(&ctx.root.text().to_string()));
let line = index.byte_to_lc(usize::from(prev.def_range.start())).line;
sink.push(Diagnostic {
rule: "duplicated-function-definition",
severity: Default::default(),
path: Default::default(),
range: cur.def_range,
message: ViolationData::new(
"duplicated-function-definition",
format!(
"function `{}` is redefined here; the definition on line {line} is \
never used",
cur.name
),
)
.with_suggestion(
"Remove the definition that is overwritten, or give one of them a \
different name.",
),
fix: None,
});
}
}
}
struct DefSite {
binding: BindingId,
name: SmolStr,
def_range: TextRange,
statement: TextRange,
list: TextRange,
}
impl DefSite {
fn of(root: &SyntaxNode, id: BindingId, binding: &Binding) -> Option<Self> {
let token = root.covering_element(binding.def_range).into_token()?;
let assign = token.parent().and_then(AssignmentExpr::cast)?;
if assign.target_name_token()?.text_range() != binding.def_range {
return None;
}
let value = assign.value_element()?.into_node()?;
if value.kind() != SyntaxKind::FUNCTION_EXPR {
return None;
}
let list = assign.syntax().parent()?;
if !matches!(list.kind(), SyntaxKind::ROOT | SyntaxKind::BLOCK_EXPR) {
return None;
}
Some(Self {
binding: id,
name: binding.name.clone(),
def_range: binding.def_range,
statement: assign.syntax().text_range(),
list: list.text_range(),
})
}
fn is_used_before(&self, ctx: &RuleContext<'_>, offset: TextSize) -> bool {
ctx.model
.read_sites(self.binding)
.any(|read| read.range.start() < offset && !self.statement.contains_range(read.range))
}
}