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)]
mod tests {
use super::*;
use crate::PackageRecord;
use crate::PublishState;
#[test]
fn package_snapshot_file_lookup_finds_matching_paths() {
let snapshot = PackageSnapshot {
label: "after".to_string(),
files: vec![PackageSnapshotFile {
path: PathBuf::from("src/lib.rs"),
contents: "pub fn greet() {}".to_string(),
}],
};
let file = snapshot
.file(Path::new("src/lib.rs"))
.unwrap_or_else(|| panic!("expected file in snapshot"));
assert_eq!(file.contents, "pub fn greet() {}");
}
#[test]
fn package_analysis_context_exposes_package_root() {
let package = PackageRecord::new(
Ecosystem::Cargo,
"core",
PathBuf::from("/repo/crates/core/Cargo.toml"),
PathBuf::from("/repo"),
None,
PublishState::Public,
);
let context = PackageAnalysisContext {
repo_root: Path::new("/repo"),
package: &package,
detection_level: DetectionLevel::Signature,
changed_files: &[],
before_snapshot: None,
after_snapshot: None,
};
assert_eq!(context.package_root(), Path::new("/repo/crates/core"));
}
}