use crate::ast::{AstNode, AstToken, ModuleDef};
use crate::linter::diagnostic::Diagnostic;
use crate::linter::rules::{Example, Rule, RuleContext};
use crate::syntax::{SyntaxElement, SyntaxKind};
pub struct ModuleShadowsParent;
impl Rule for ModuleShadowsParent {
fn id(&self) -> &'static str {
"module-shadows-parent"
}
fn description(&self) -> &'static str {
"Flag a nested `module` with the same name as its direct parent module. \
A module binds its own name inside itself, so the child rebinds that \
self-reference: `A` in the parent body then refers to the child, and \
qualified names like `A.x` resolve against the wrong module. The usual \
cause is a file `include`d into the module it already defines. No fix: \
renaming a module means updating every reference to it."
}
fn examples(&self) -> &'static [Example] {
&[Example {
caption: "A submodule shadowing the module that contains it:",
source: "module A\n\nmodule A\nend\n\nend\n",
}]
}
fn interests(&self) -> &'static [SyntaxKind] {
&[SyntaxKind::MODULE_DEF]
}
fn check(&self, el: &SyntaxElement, ctx: &RuleContext<'_>, sink: &mut Vec<Diagnostic>) {
let Some(module) = el.as_node().cloned().and_then(ModuleDef::cast) else {
return;
};
let Some(ident) = module.name().and_then(|name| name.ident()) else {
return;
};
let in_quote_or_macro = module.syntax().ancestors().skip(1).any(|a| {
matches!(
a.kind(),
SyntaxKind::QUOTE_EXPR | SyntaxKind::QUOTE_SYM | SyntaxKind::MACRO_CALL
)
});
if in_quote_or_macro {
return;
}
let scope = ctx.model.scope_at(module.syntax().text_range().start());
let path = ctx.model.enclosing_module_path(scope);
if path.last().map(|parent| parent.as_str()) != Some(ident.text()) {
return;
}
sink.push(Diagnostic::new(
self.id(),
ident.syntax().text_range(),
format!(
"module `{}` has the same name as its parent module",
ident.text()
),
));
}
}