use std::collections::HashSet;
use rowan::ast::AstNode;
use crate::ast::CallExpr;
use crate::linter::diagnostic::Diagnostic;
use crate::linter::include_graph::IncludeProblemKind;
use crate::linter::rules::{Example, Rule, RuleContext};
use crate::project::include_literal;
pub struct DuplicateInclude;
impl Rule for DuplicateInclude {
fn id(&self) -> &'static str {
"duplicate-include"
}
fn description(&self) -> &'static str {
"Flag a static `include(\"path\")` that pulls in a file an earlier \
`include` in the same file already did. Julia has no include guard, so \
the repeat evaluates the file a second time, redefining its methods \
and re-running its top-level code. Paths are compared after resolution, \
so `\"a.jl\"` and `\"./a.jl\"` count as the same file. Including one \
file into two different `module` blocks is not flagged — that runs its \
definitions into two separate namespaces — and neither is a file \
reached twice along different include chains."
}
fn examples(&self) -> &'static [Example] {
&[Example {
caption: "The same file included twice, so its definitions run again:",
source: "include(\"util.jl\")\ninclude(\"util.jl\")\n",
}]
}
fn check_file(&self, ctx: &RuleContext<'_>, sink: &mut Vec<Diagnostic>) {
let repeats: HashSet<usize> = ctx
.includes
.iter()
.filter(|problem| problem.kind == IncludeProblemKind::Duplicate)
.map(|problem| problem.edge)
.collect();
if repeats.is_empty() {
return;
}
let sites = ctx
.root
.descendants()
.filter_map(CallExpr::cast)
.filter_map(|call| include_literal(&call));
for (index, literal) in sites.enumerate() {
if !repeats.contains(&index) {
continue;
}
let raw: String = literal
.content_tokens()
.map(|token| token.text().to_string())
.collect();
sink.push(Diagnostic::new(
self.id(),
literal.syntax().text_range(),
format!("duplicate include: \"{raw}\" is already included earlier in this file"),
));
}
}
}