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;
use crate::syntax::{SyntaxElement, SyntaxKind};
pub struct IncludeCycle;
impl Rule for IncludeCycle {
fn id(&self) -> &'static str {
"include-cycle"
}
fn description(&self) -> &'static str {
"Flag a static `include(\"path\")` whose target transitively `include`s \
this file again — including a file that includes itself. Running such \
a file recurses without end. Every linted file on the cycle is \
flagged at its own `include` call; a file merely included twice along \
different paths (a diamond) is not a cycle."
}
fn examples(&self) -> &'static [Example] {
&[Example {
caption: "A file (here `example.jl`) that includes itself:",
source: "include(\"example.jl\")\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().cloned().and_then(CallExpr::cast) else {
return;
};
let Some(literal) = include_literal(&call) else {
return;
};
let raw: String = literal
.content_tokens()
.map(|token| token.text().to_string())
.collect();
let cycles = ctx
.includes
.iter()
.any(|problem| problem.kind == IncludeProblemKind::Cycle && problem.raw == raw);
if cycles {
sink.push(Diagnostic::new(
self.id(),
literal.syntax().text_range(),
format!("include cycle: \"{raw}\" transitively includes this file"),
));
}
}
}