use std::path::Path;
use std::path::PathBuf;
use serde::Deserialize;
use serde::Serialize;
use crate::Ecosystem;
use crate::MonochangeResult;
use crate::PackageRecord;
#[derive(Debug, Clone, Copy, Eq, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
#[non_exhaustive]
pub enum DetectionLevel {
Basic,
Signature,
Semantic,
}
#[derive(Debug, Clone, Copy, Eq, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
#[non_exhaustive]
pub enum FileChangeKind {
Added,
Modified,
Deleted,
}
#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct AnalyzedFileChange {
pub path: PathBuf,
pub package_path: PathBuf,
pub kind: FileChangeKind,
pub before_contents: Option<String>,
pub after_contents: Option<String>,
}
#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PackageSnapshotFile {
pub path: PathBuf,
pub contents: String,
}
#[derive(Debug, Clone, Default, Eq, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PackageSnapshot {
pub label: String,
pub files: Vec<PackageSnapshotFile>,
}
impl PackageSnapshot {
#[must_use]
pub fn file(&self, path: &Path) -> Option<&PackageSnapshotFile> {
self.files.iter().find(|file| file.path == path)
}
}
#[derive(Debug, Clone, Copy, Eq, Ord, PartialEq, PartialOrd, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
#[non_exhaustive]
pub enum SemanticChangeCategory {
PublicApi,
Export,
Dependency,
Metadata,
}
#[derive(Debug, Clone, Copy, Eq, Ord, PartialEq, PartialOrd, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
#[non_exhaustive]
pub enum SemanticChangeKind {
Added,
Removed,
Modified,
}
#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SemanticChange {
pub category: SemanticChangeCategory,
pub kind: SemanticChangeKind,
pub item_kind: String,
pub item_path: String,
pub summary: String,
pub file_path: PathBuf,
pub before_signature: Option<String>,
pub after_signature: Option<String>,
}
#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PackageAnalysisResult {
pub analyzer_id: String,
pub package_id: String,
pub ecosystem: Ecosystem,
pub changed_files: Vec<PathBuf>,
pub semantic_changes: Vec<SemanticChange>,
pub warnings: Vec<String>,
}
#[derive(Debug)]
pub struct PackageAnalysisContext<'a> {
pub repo_root: &'a Path,
pub package: &'a PackageRecord,
pub detection_level: DetectionLevel,
pub changed_files: &'a [AnalyzedFileChange],
pub before_snapshot: Option<&'a PackageSnapshot>,
pub after_snapshot: Option<&'a PackageSnapshot>,
}
impl PackageAnalysisContext<'_> {
#[must_use]
pub fn package_root(&self) -> &Path {
self.package
.manifest_path
.parent()
.unwrap_or(&self.package.workspace_root)
}
}
pub trait SemanticAnalyzer: Send + Sync {
fn analyzer_id(&self) -> &'static str;
fn applies_to(&self, package: &PackageRecord) -> bool;
fn analyze_package(
&self,
context: &PackageAnalysisContext<'_>,
) -> MonochangeResult<PackageAnalysisResult>;
}
#[cfg(test)]
#[path = "__tests__/analysis_tests.rs"]
mod tests;