impactsense-parser 0.1.1

Multi-language static analysis: parse codebases into an in-memory dependency graph for impact analysis
Documentation
use serde::{Deserialize, Serialize};

use crate::schema;

/// Serialized, language-agnostic graph representation that can be written to
/// disk and later consumed by a separate "graph loader" runtime.
///
/// This sits between the Tree-Sitter based parsing layer (which works on
/// `ParsedFile` + CST in memory) and the Neo4j integration layer (which
/// creates nodes and relationships in the database).
#[derive(Debug, Default, Serialize, Deserialize)]
pub struct ProjectIr {
    pub files: Vec<FileIr>,
    pub modules: Vec<ModuleIr>,
    pub functions: Vec<FunctionIr>,
    pub api_endpoints: Vec<ApiEndpointIr>,
    pub external_apis: Vec<ExternalApiIr>,
    pub edges: Vec<EdgeIr>,
    /// C# / Java-style types (CRM-3595).
    #[serde(default)]
    pub classes: Vec<ClassIr>,
    /// C# properties (CRM-3595).
    #[serde(default)]
    pub properties: Vec<PropertyIr>,
    /// OTP/custom behaviour contracts (Erlang).
    #[serde(default)]
    pub behaviours: Vec<BehaviourIr>,
    /// Callback contracts declared by behaviours.
    #[serde(default)]
    pub callbacks: Vec<CallbackIr>,
}

impl ProjectIr {
    pub fn empty() -> Self {
        Self::default()
    }
}

/// File node IR, mirroring `schema::FileNode`.
#[derive(Debug, Serialize, Deserialize)]
pub struct FileIr {
    pub path: String,
    pub language: String,
    pub framework: Option<String>,
    pub project_name: Option<String>,
}

/// Module node IR, mirroring `schema::ModuleNode`.
#[derive(Debug, Serialize, Deserialize)]
pub struct ModuleIr {
    pub name: String,
    pub path: String,
    pub language: String,
    pub framework: Option<String>,
    pub project_name: Option<String>,
    /// Zstd-compressed source (RedCompressor wire format), when compression is enabled.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub code_bytes: Option<Vec<u8>>,
}

/// Class node IR for languages that map types to `Class` (CRM-3595).
#[derive(Debug, Serialize, Deserialize)]
pub struct ClassIr {
    pub fqn: String,
    pub name: String,
    pub path: String,
    pub language: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub project_name: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub kind: Option<String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub code_bytes: Option<Vec<u8>>,
}

/// Property node IR (C#; CRM-3595).
#[derive(Debug, Serialize, Deserialize)]
pub struct PropertyIr {
    pub fqn: String,
    pub name: String,
    pub class_fqn: String,
    pub path: String,
    pub language: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub project_name: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub declared_type: Option<String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub code_bytes: Option<Vec<u8>>,
}

/// Function node IR, mirroring `schema::FunctionNode`.
#[derive(Debug, Serialize, Deserialize)]
pub struct FunctionIr {
    pub name: String,
    pub fqn: String,
    pub path: String,
    pub language: String,
    pub framework: Option<String>,
    pub project_name: Option<String>,
    pub arity: Option<u32>,
    pub return_type: Option<String>,
    pub param_count: Option<u32>,
    pub param_types: Vec<String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub code_bytes: Option<Vec<u8>>,
}

/// ApiEndpoint node IR, mirroring `schema::ApiEndpointNode`.
#[derive(Debug, Serialize, Deserialize)]
pub struct ApiEndpointIr {
    pub methods: Vec<String>,
    pub path: String,
    pub protocol: Option<String>,
    pub framework: Option<String>,
    pub project_name: Option<String>,
}

/// ExternalApi node IR, mirroring `schema::ExternalApiNode`.
#[derive(Debug, Serialize, Deserialize)]
pub struct ExternalApiIr {
    pub name: String,
    pub base_url: Option<String>,
    pub protocol: Option<String>,
    pub provider: Option<String>,
    pub service: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub norm_path: Option<String>,
}

/// Behaviour node IR, mirroring `schema::BehaviourNode`.
#[derive(Debug, Serialize, Deserialize)]
pub struct BehaviourIr {
    pub name: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub path: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub language: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub project_name: Option<String>,
}

/// Callback node IR, mirroring `schema::CallbackNode`.
#[derive(Debug, Serialize, Deserialize)]
pub struct CallbackIr {
    pub name: String,
    pub fqn: String,
    pub arity: u32,
    pub optional: bool,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub language: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub project_name: Option<String>,
}

/// Edge kinds in the IR, aligned with `edge::RelType`.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum EdgeKind {
    DeclaresModule,
    DeclaresFunction,
    DeclaresClass,
    DeclaresProperty,
    DependsOnFile,
    CallsFunction,
    HandlesApi,
    CallsExternalApi,
    /// Function → Class type reference.
    UsesClass,
    /// Class → Class inheritance / interface (C# `base_list`).
    ClassUsesClass,
    SameApi,
    ImplementsBehaviour,
    DeclaresCallback,
    ImplementsCallback,
    DeclaresBehaviour,
    ExtendsBehaviour,
    OverridesCallback,
}

/// A relationship between two nodes in the IR.
///
/// We refer to nodes by a stable "key" string rather than by numeric IDs:
/// - File: `schema::NodeLabel::File` + `path`
/// - Module: `Module.name` + `Module.path`
/// - Function: `Function.fqn`
/// - Class: `Class.fqn`
/// - Property: `Property.fqn`
/// - ApiEndpoint: `"{methods.join(\",\")} {path}"`
/// - Behaviour: `Behaviour.name`
/// - Callback: `Callback.fqn`
/// - ExternalApi: `base_url` + `norm_path` or `ExternalApi.name`
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EdgeIr {
    pub kind: EdgeKind,
    /// Label of the source node (e.g. "File", "Module", "Function").
    pub from_label: String,
    /// Stable key identifying the source node (see comment above).
    pub from_key: String,
    /// Label of the target node.
    pub to_label: String,
    /// Stable key identifying the target node.
    pub to_key: String,
}

impl EdgeKind {
    /// Convert this IR edge kind into the runtime `RelType` used when writing
    /// to Neo4j. This is a small adapter so the loader can reuse the same
    /// relationship names as the in-process graph builder.
    pub fn to_rel_type(&self) -> crate::edge::RelType {
        use crate::edge::RelType;
        match self {
            EdgeKind::DeclaresModule => RelType::DeclaresModule,
            EdgeKind::DeclaresFunction => RelType::DeclaresFunction,
            EdgeKind::DeclaresClass => RelType::DeclaresClass,
            EdgeKind::DeclaresProperty => RelType::DeclaresProperty,
            EdgeKind::DependsOnFile => RelType::DependsOnFile,
            EdgeKind::CallsFunction => RelType::CallsFunction,
            EdgeKind::HandlesApi => RelType::HandlesApi,
            EdgeKind::CallsExternalApi => RelType::CallsExternalApi,
            EdgeKind::UsesClass => RelType::UsesClass,
            EdgeKind::ClassUsesClass => RelType::ClassUsesClass,
            EdgeKind::SameApi => RelType::SameApi,
            EdgeKind::ImplementsBehaviour => RelType::ImplementsBehaviour,
            EdgeKind::DeclaresCallback => RelType::DeclaresCallback,
            EdgeKind::ImplementsCallback => RelType::ImplementsCallback,
            EdgeKind::DeclaresBehaviour => RelType::DeclaresBehaviour,
            EdgeKind::ExtendsBehaviour => RelType::ExtendsBehaviour,
            EdgeKind::OverridesCallback => RelType::OverridesCallback,
        }
    }
}

/// Stable key for an ExternalApi node in the IR.
pub fn external_api_key(base_url: &str, norm_path: &str) -> String {
    format!("{base_url}|{norm_path}")
}

/// Stable key for an ApiEndpoint node in the IR.
pub fn api_endpoint_key(methods: &[String], path: &str) -> String {
    format!("{} {}", methods.join(","), path)
}

/// Stable key for a Module node in the IR.
pub fn module_key(name: &str, path: &str) -> String {
    format!("{name}@{path}")
}

impl From<schema::NodeLabel> for String {
    fn from(label: schema::NodeLabel) -> Self {
        label.to_string()
    }
}