use rowan::ast::AstNode;
use crate::ast::CallExpr;
use crate::linter::diagnostic::{Diagnostic, Severity};
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 MissingIncludeFile;
impl Rule for MissingIncludeFile {
fn id(&self) -> &'static str {
"missing-include-file"
}
fn default_severity(&self) -> Severity {
Severity::Error
}
fn description(&self) -> &'static str {
"Flag a static `include(\"path\")` whose target does not exist on disk \
(relative paths resolve against the including file's directory). \
Running the file would throw a `SystemError`. Only statically \
resolvable includes are checked: dynamic (`include(f)`), interpolated \
(`include(\"$dir/a.jl\")`), qualified (`M.include(...)`), and \
two-argument forms cannot be resolved without running the code and \
are never flagged."
}
fn examples(&self) -> &'static [Example] {
&[Example {
caption: "Including a file that does not exist:",
source: "include(\"missing.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 missing = ctx
.includes
.iter()
.any(|problem| problem.kind == IncludeProblemKind::Missing && problem.raw == raw);
if missing {
sink.push(Diagnostic::new(
self.id(),
literal.syntax().text_range(),
format!("included file \"{raw}\" does not exist"),
));
}
}
}