scryer-engine 0.3.0

Tree-sitter AST indexing and reference resolution engine for Scryer code intelligence
//! What an indexing run found, kept with the project so tools can report on the health of the
//! index (`get_indexing_status`, `scryer doctor`) instead of leaving it to warnings in a log.

use schemars::JsonSchema;
use serde::{Deserialize, Serialize};

/// Statistics recorded by the last indexing run that changed a project.
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
pub struct IndexStats {
    /// References that name no symbol in the project or its dependencies: calls into the
    /// standard library and other external code, or genuinely unknown names. Counted over the
    /// whole project: the rows are kept (`unresolved_reference`), so a later edit that defines
    /// the name links them and takes them out of this count.
    #[serde(default)]
    pub unresolved_references: usize,
    /// Files the parser rejected outright; they are not in the index and are retried next run.
    #[serde(default)]
    pub parse_failures: ParseFailures,
    /// Files that parsed with syntax errors, in path order. They are indexed, but code around
    /// the errors may be missing from the index. Kept up to date across incremental runs.
    #[serde(default)]
    pub syntax_error_files: Vec<String>,
    /// How many files the last run re-indexed.
    #[serde(default)]
    pub last_run_files: usize,
    /// Files left out of the index on purpose.
    #[serde(default)]
    pub skipped: SkippedFiles,
    #[serde(default)]
    pub dependencies: DependencyState,
}

#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
pub struct ParseFailures {
    pub count: usize,
    /// The first failing file and why it failed.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub first: Option<ParseFailure>,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
pub struct ParseFailure {
    pub path: String,
    pub error: String,
}

#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
pub struct SkippedFiles {
    /// Source files over the size cap.
    pub too_large: usize,
    /// Files that could not be read.
    pub unreadable: usize,
}

/// Whether the project's Cargo dependencies are in the index.
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(tag = "state", rename_all = "snake_case")]
pub enum DependencyState {
    /// The project has no `Cargo.toml`, or it has not been indexed yet.
    #[default]
    NotApplicable,
    /// Every direct dependency's source was found and indexed.
    Indexed { packages: usize },
    /// Some direct dependencies have no source on disk (not downloaded).
    Partial {
        packages: usize,
        missing: Vec<String>,
    },
    /// `cargo metadata` failed, so no dependency was indexed.
    DiscoveryFailed { error: String },
}

impl DependencyState {
    /// One line for people, saying what to do when something is missing.
    pub fn describe(&self) -> String {
        match self {
            DependencyState::NotApplicable => "not applicable (no Cargo.toml)".to_string(),
            DependencyState::Indexed { packages } => format!("indexed ({packages} packages)"),
            DependencyState::Partial { packages, missing } => {
                let shown: Vec<&str> = missing.iter().take(5).map(String::as_str).collect();
                let more = missing.len().saturating_sub(shown.len());
                format!(
                    "partly indexed ({packages} packages; {} not downloaded: {}{}; run `cargo fetch`)",
                    missing.len(),
                    shown.join(", "),
                    if more > 0 {
                        format!(" and {more} more")
                    } else {
                        String::new()
                    }
                )
            }
            DependencyState::DiscoveryFailed { error } => format!(
                "not indexed (cargo metadata failed: {}; run `cargo fetch` or fix Cargo.toml)",
                error.lines().next().unwrap_or("unknown error").trim()
            ),
        }
    }

    /// Whether this deserves a warning in the health report.
    pub fn is_problem(&self) -> bool {
        matches!(
            self,
            DependencyState::Partial { .. } | DependencyState::DiscoveryFailed { .. }
        )
    }
}