use rowan::TextRange;
use crate::index::harvest_tree;
use crate::index::model::{Method, ModuleIndex};
use crate::index::typeexpr::TypeExpr;
use crate::linter::diagnostic::Diagnostic;
use crate::linter::rules::{Example, Rule, RuleContext};
use crate::syntax::SyntaxKind;
pub struct DuplicateMethod;
#[derive(PartialEq, Eq)]
struct SignatureKey {
params: Vec<(Option<TypeExpr>, bool)>,
where_clauses: Vec<TypeExpr>,
}
impl SignatureKey {
fn of(method: &Method) -> Self {
Self {
params: method
.params
.iter()
.map(|param| (param.type_annotation.clone(), param.is_vararg))
.collect(),
where_clauses: method.where_clauses.clone(),
}
}
}
impl Rule for DuplicateMethod {
fn id(&self) -> &'static str {
"duplicate-method"
}
fn description(&self) -> &'static str {
"Flag a method definition whose dispatch signature an earlier \
definition in the same file already used. Julia holds one method per \
signature, so the later definition silently replaces the earlier one \
and the earlier body becomes dead. Signatures are compared on what \
dispatch actually sees: the positional argument types (an unannotated \
argument is `Any`) and the `where` specs. Argument names, default \
values, keyword arguments, and the declared return type are not part \
of a method's identity, so definitions differing only in those still \
collide. Definitions with different `where` bounds are separate \
methods and are not flagged, and neither are definitions inside a \
macro call or a conditional branch, where the written signature is \
not evidence of what gets defined."
}
fn examples(&self) -> &'static [Example] {
&[Example {
caption: "The second definition replaces the first, so `1` is unreachable:",
source: "f(x::Int) = 1\nf(y::Int) = 2\n",
}]
}
fn check_file(&self, ctx: &RuleContext<'_>, sink: &mut Vec<Diagnostic>) {
let macro_calls: Vec<TextRange> = ctx
.root
.descendants()
.filter(|node| node.kind() == SyntaxKind::MACRO_CALL)
.map(|node| node.text_range())
.collect();
let index = harvest_tree(ctx.root);
self.check_module(&index, ¯o_calls, sink);
}
}
impl DuplicateMethod {
fn check_module(
&self,
module: &ModuleIndex,
macro_calls: &[TextRange],
sink: &mut Vec<Diagnostic>,
) {
for group in &module.functions {
let mut seen: Vec<SignatureKey> = Vec::new();
for method in &group.methods {
if !method.has_body {
continue;
}
let range =
TextRange::new(method.loc.range.start.into(), method.loc.range.end.into());
if macro_calls.iter().any(|call| call.contains_range(range)) {
continue;
}
let key = SignatureKey::of(method);
if seen.contains(&key) {
sink.push(Diagnostic::new(
self.id(),
range,
format!(
"`{}` is already defined with this signature earlier in this \
file; the later definition replaces the earlier one",
display_name(group.owner.as_deref(), &group.name)
),
));
} else {
seen.push(key);
}
}
}
for submodule in &module.submodules {
self.check_module(submodule, macro_calls, sink);
}
}
}
fn display_name(owner: Option<&[String]>, name: &str) -> String {
match owner {
Some(path) => format!("{}.{name}", path.join(".")),
None => name.to_string(),
}
}