fatou 0.23.0

A language server, formatter, and linter for Julia
//! `missing-include-file`: a static `include("path")` whose resolved target is
//! missing or unreadable as source text. The finding defaults to an error.
//!
//! The check is driven by the include problems the lint driver precomputes
//! (see [`crate::linter::include_graph`]): the rule itself only matches each
//! problem back to its call site by the literal's decoded path
//! ([`literal_path`]), so it never touches the filesystem. Only statically
//! resolvable includes participate — dynamic, interpolated, qualified, and
//! two-argument forms cannot be resolved without evaluation and are never
//! flagged, and neither is a literal whose escapes denote no path at all. A
//! pathless document (stdin) has no base directory to resolve against and stays
//! silent, and the language server passes no problems (it publishes its own
//! include-graph diagnostics), so no false positive can come from a missing
//! context. A decoded NUL byte can never name a file, so StaticLint's
//! `IncludePathContainsNULL` falls out of the same check. No fix: the linter
//! cannot know the intended file.

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, literal_path};
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 is missing or unreadable \
         (relative paths resolve against the including file's directory). \
         Missing files throw a `SystemError` at runtime. 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 Some(path) = literal_path(&literal) else {
            return;
        };
        let problem = ctx.includes.iter().find(|problem| {
            matches!(
                problem.kind,
                IncludeProblemKind::Missing | IncludeProblemKind::Unreadable
            ) && problem.path == path
        });
        if let Some(problem) = problem {
            let reason = if problem.kind == IncludeProblemKind::Unreadable {
                "could not be read"
            } else {
                "does not exist"
            };
            sink.push(Diagnostic::new(
                self.id(),
                literal.syntax().text_range(),
                format!("included file \"{path}\" {reason}"),
            ));
        }
    }
}