use crate::ast::{AssignmentExpr, AstNode, AstToken, Expr};
use crate::linter::diagnostic::Diagnostic;
use crate::linter::rules::matchers::call_expr;
use crate::linter::rules::{Example, Rule, RuleContext};
use crate::resolve::{Namespace, Resolution};
use crate::semantic::{BindingId, BindingKind};
use crate::syntax::{SyntaxElement, SyntaxKind, SyntaxNode};
pub struct ShadowedBaseName;
impl Rule for ShadowedBaseName {
fn id(&self) -> &'static str {
"shadowed-base-name"
}
fn description(&self) -> &'static str {
"Flag a call to a name the file binds to a value while Base or Core \
exports it as a function. Julia has one namespace and no \
call-position fallback, so the binding masks the Base name \
everywhere: after `length = 3`, a later `length(xs)` raises a \
`MethodError` for calling an `Int`, and assigning the other way \
round fails too once the module has used the Base name. Both a \
binding and a call are required — the binding alone is ordinary \
Julia. Only bindings that plainly hold a value count. Method, macro, \
type, and module definitions of the name are exempt, as is `import \
Base: length`; so is a parameter, since passing a function in under \
a Base name (`request(stack::Base.Callable, …)`) is the \
higher-order idiom rather than an accident, though a `catch` \
variable — which holds the thrown exception — still counts. A \
binding the source visibly assigns something callable (a lambda, \
another builtin under a new name, or any qualified path such as \
`exit = t.__exit__`) is exempt too, as is anything defined or called \
inside quoted code or a macro call, where a DSL may spell an \
attribute `length = 32` without touching Base. No fix: renaming the \
binding means rewriting every reference to it."
}
fn examples(&self) -> &'static [Example] {
&[
Example {
caption: "The assignment masks Base's `length`, so the call fails:",
source: "length = 3\nn = length(xs)\n",
},
Example {
caption: "A `catch` variable can mask a builtin just as well:",
source: "try\n risky()\ncatch error\n error(\"failed\")\nend\n",
},
]
}
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().and_then(call_expr) else {
return;
};
let Some(callee) = call.callee_ident() else {
return;
};
let range = callee.syntax().text_range();
if ctx.file_scan().in_skipped(range) {
return;
}
let Some(resolver) = ctx.resolver() else {
return;
};
let Resolution::Binding(id) =
resolver.resolve(callee.text(), range.start(), Namespace::Value)
else {
return;
};
let binding = ctx.model.binding(id);
if !is_value_binding(binding.kind) || ctx.file_scan().in_skipped(binding.def_range) {
return;
}
let Some(module) = ctx.base_export_module(callee.text()) else {
return;
};
if assigns_a_function(ctx, id) {
return;
}
let name = callee.text();
sink.push(Diagnostic::new(
self.id(),
range,
format!("`{name}` here is this file's own binding, not {module}'s `{name}`"),
));
}
}
fn is_value_binding(kind: BindingKind) -> bool {
match kind {
BindingKind::Global
| BindingKind::Local
| BindingKind::Const
| BindingKind::ForVar
| BindingKind::LetVar
| BindingKind::CatchParam => true,
BindingKind::Param
| BindingKind::KeywordParam
| BindingKind::Function
| BindingKind::Macro
| BindingKind::Type
| BindingKind::Module
| BindingKind::Import
| BindingKind::Field
| BindingKind::TypeParam => false,
}
}
fn assigns_a_function(ctx: &RuleContext<'_>, id: BindingId) -> bool {
let binding = ctx.model.binding(id);
std::iter::once(binding.def_range)
.chain(ctx.model.occurrences(id).map(|occ| occ.range))
.filter_map(|range| assigned_value(ctx.root, range))
.any(|value| is_function_valued(ctx, &value))
}
fn assigned_value(root: &SyntaxNode, range: rowan::TextRange) -> Option<Expr> {
let name = root.covering_element(range).into_token()?.parent()?;
let assignment = AssignmentExpr::cast(name.parent()?)?;
if assignment.lhs()?.syntax() != &name {
return None;
}
assignment.rhs()
}
fn is_function_valued(ctx: &RuleContext<'_>, value: &Expr) -> bool {
match value {
Expr::ArrowExpr(_) | Expr::FunctionDef(_) => true,
Expr::Name(name) => name
.ident()
.is_some_and(|ident| ctx.base_export_module(ident.text()).is_some()),
Expr::BinaryExpr(path) => path
.op()
.is_some_and(|op| op.syntax().kind() == SyntaxKind::DOT),
_ => false,
}
}