use crate::linter::diagnostic::{Diagnostic, Severity};
use crate::linter::rules::correctness::const_decl;
use crate::linter::rules::{Example, Rule, RuleContext, matchers};
use crate::syntax::{SyntaxElement, SyntaxKind, SyntaxNode};
pub struct ConstLocal;
fn opens_local_scope(node: &SyntaxNode, from: &SyntaxNode) -> bool {
match node.kind() {
SyntaxKind::FUNCTION_DEF | SyntaxKind::MACRO_DEF | SyntaxKind::ARROW_EXPR => true,
SyntaxKind::DO_EXPR => from.kind() == SyntaxKind::BLOCK,
SyntaxKind::LET_EXPR => from.kind() == SyntaxKind::BLOCK,
SyntaxKind::FOR_EXPR | SyntaxKind::WHILE_EXPR => from.kind() == SyntaxKind::BLOCK,
SyntaxKind::TRY_EXPR => true,
SyntaxKind::COMPREHENSION
| SyntaxKind::BRACES_COMPREHENSION
| SyntaxKind::TYPED_COMPREHENSION
| SyntaxKind::GENERATOR => from.kind() != SyntaxKind::FOR_BINDING,
SyntaxKind::ASSIGNMENT_EXPR => matchers::is_short_form_def(node),
_ => false,
}
}
impl Rule for ConstLocal {
fn id(&self) -> &'static str {
"const-local"
}
fn default_severity(&self) -> Severity {
Severity::Error
}
fn description(&self) -> &'static str {
"Flag a `const` declaration inside a local scope — a function or macro \
body, a `let`, a `for`/`while` body, a `try`, a closure, or a \
comprehension. `const` is only meaningful at global scope (the file \
top level and each `module` body); anywhere else the code parses but \
always fails at lowering with \"unsupported `const` declaration on \
local variable\". A `const` field of a mutable struct is a different \
construct and is left alone, as is a `const` inside quoted code or a \
macro argument, which may never be lowered as written."
}
fn examples(&self) -> &'static [Example] {
&[
Example {
caption: "`const` inside a function body:",
source: "function scale(x)\n const factor = 2\n factor * x\nend\n",
},
Example {
caption: "A `let` body is local too — the declaration belongs at top level:",
source: "let\n const limit = 10\n limit\nend\n",
},
]
}
fn interests(&self) -> &'static [SyntaxKind] {
&[SyntaxKind::CONST_STMT]
}
fn check(&self, el: &SyntaxElement, _ctx: &RuleContext<'_>, sink: &mut Vec<Diagnostic>) {
let Some(node) = el.as_node() else {
return;
};
if const_decl::scope_modifier(node).is_some() {
return;
}
let mut from = node.clone();
let mut local = None;
for ancestor in node.ancestors().skip(1) {
if const_decl::is_unlowered_context(&ancestor) {
return;
}
if local.is_none() {
if opens_local_scope(&ancestor, &from) {
local = Some(true);
} else if matches!(
ancestor.kind(),
SyntaxKind::MODULE_DEF | SyntaxKind::STRUCT_DEF
) {
local = Some(false);
}
}
from = ancestor;
}
if local == Some(true) {
sink.push(Diagnostic::new(
self.id(),
node.text_range(),
"`const` declaration on a local variable",
));
}
}
}