use rowan::TextRange;
use crate::linter::diagnostic::Diagnostic;
use crate::linter::rules::{Example, Rule, RuleContext};
use crate::semantic::{Access, BindingKind};
use crate::syntax::{SyntaxKind, SyntaxNode};
pub struct RedefinedConstant;
#[derive(PartialEq, Eq)]
enum WriteSite {
FunctionName,
MacroName,
TypeName,
ConstDecl,
Plain,
}
fn classify_write(root: &SyntaxNode, range: TextRange) -> WriteSite {
let token = match root.covering_element(range) {
rowan::NodeOrToken::Token(t) => t,
rowan::NodeOrToken::Node(_) => return WriteSite::Plain,
};
let Some(name) = token.parent().filter(|n| n.kind() == SyntaxKind::NAME) else {
return WriteSite::Plain;
};
let mut child = name;
let mut passed_call = false;
while let Some(parent) = child.parent() {
match parent.kind() {
SyntaxKind::CALL_EXPR
| SyntaxKind::CURLY_EXPR
| SyntaxKind::BINARY_EXPR
| SyntaxKind::COMPARISON_EXPR => {
if parent.children().next().as_ref() != Some(&child) {
return WriteSite::Plain;
}
passed_call |= parent.kind() == SyntaxKind::CALL_EXPR;
}
SyntaxKind::WHERE_EXPR
| SyntaxKind::TYPE_ANNOTATION
| SyntaxKind::PAREN_EXPR
| SyntaxKind::TUPLE_EXPR
| SyntaxKind::BARE_TUPLE_EXPR
| SyntaxKind::ARG
| SyntaxKind::SPLAT_EXPR => {}
SyntaxKind::SIGNATURE => {
return match parent.parent().map(|p| p.kind()) {
Some(SyntaxKind::FUNCTION_DEF) => WriteSite::FunctionName,
Some(SyntaxKind::MACRO_DEF) => WriteSite::MacroName,
_ => WriteSite::TypeName,
};
}
SyntaxKind::ASSIGNMENT_EXPR => {
if parent.children().next().as_ref() != Some(&child) {
return WriteSite::Plain;
}
if passed_call {
return WriteSite::FunctionName;
}
return match parent.parent().map(|p| p.kind()) {
Some(SyntaxKind::CONST_STMT) => WriteSite::ConstDecl,
_ => WriteSite::Plain,
};
}
SyntaxKind::CONST_STMT => return WriteSite::ConstDecl,
_ => return WriteSite::Plain,
}
child = parent;
}
WriteSite::Plain
}
fn branch_chain(root: &SyntaxNode, range: TextRange) -> Vec<(SyntaxNode, SyntaxNode)> {
let mut chain = Vec::new();
let mut node = match root.covering_element(range) {
rowan::NodeOrToken::Token(t) => t.parent(),
rowan::NodeOrToken::Node(n) => Some(n),
};
while let Some(current) = node {
let parent = current.parent();
if let Some(parent) = &parent
&& parent.kind() == SyntaxKind::IF_EXPR
&& matches!(
current.kind(),
SyntaxKind::BLOCK | SyntaxKind::ELSEIF_CLAUSE | SyntaxKind::ELSE_CLAUSE
)
{
chain.push((parent.clone(), current));
}
node = parent;
}
chain
}
fn in_disjoint_branches(root: &SyntaxNode, a: TextRange, b: TextRange) -> bool {
let chain_a = branch_chain(root, a);
if chain_a.is_empty() {
return false;
}
let chain_b = branch_chain(root, b);
chain_a.iter().any(|(if_a, branch_a)| {
chain_b
.iter()
.any(|(if_b, branch_b)| if_a == if_b && branch_a != branch_b)
})
}
fn holds_value(kind: BindingKind) -> bool {
matches!(
kind,
BindingKind::Global
| BindingKind::Local
| BindingKind::LetVar
| BindingKind::Param
| BindingKind::KeywordParam
| BindingKind::ForVar
| BindingKind::CatchParam
)
}
impl Rule for RedefinedConstant {
fn id(&self) -> &'static str {
"redefined-constant"
}
fn description(&self) -> &'static str {
"Flag a write that redefines a constant name, or defines over a name \
that already holds a value: reassigning a `const` binding, assigning \
to a global function, type, or module name (those bind implicit \
constants), defining a function over a plain value, or declaring a \
value `const` after the fact. All of these error at runtime when both \
sites execute. A definition and a write in disjoint branches of the \
same `if` are exempt — only one branch runs. Adding a method to a \
function and defining an outer constructor on a type are legal and \
stay silent. No fix: the rule cannot know which of the two \
definitions the author meant to keep."
}
fn examples(&self) -> &'static [Example] {
&[
Example {
caption: "Reassigning a `const` binding errors at runtime:",
source: "const threshold = 1.0\nthreshold = 2.0\n",
},
Example {
caption: "`count` already holds a value, so the method definition fails:",
source: "count = 0\ncount(xs) = length(xs)\n",
},
]
}
fn check_file(&self, ctx: &RuleContext<'_>, sink: &mut Vec<Diagnostic>) {
for ident in ctx.model.idents() {
if ident.access == Access::Read {
continue;
}
let Some(id) = ident.binding else { continue };
let binding = ctx.model.binding(id);
if ident.range == binding.def_range {
continue;
}
let global = ctx.model.scope(binding.scope).kind.is_global();
let site = classify_write(ctx.root, ident.range);
let message = match binding.kind {
BindingKind::Const => match site {
WriteSite::MacroName | WriteSite::TypeName => continue,
WriteSite::FunctionName => {
format!(
"cannot define function `{}`: it already has a value",
binding.name
)
}
_ => format!("reassignment of constant `{}`", binding.name),
},
BindingKind::Function | BindingKind::Type | BindingKind::Module
if global && matches!(site, WriteSite::Plain | WriteSite::ConstDecl) =>
{
format!("reassignment of constant `{}`", binding.name)
}
kind if holds_value(kind) && site == WriteSite::FunctionName => {
format!(
"cannot define function `{}`: it already has a value",
binding.name
)
}
BindingKind::Global if site == WriteSite::ConstDecl => {
format!(
"cannot declare `{}` constant: it already has a value",
binding.name
)
}
_ => continue,
};
if in_disjoint_branches(ctx.root, binding.def_range, ident.range) {
continue;
}
sink.push(Diagnostic::new(self.id(), ident.range, message));
}
}
}