use crate::graph::{Graph, NodeKind};
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use std::cmp::Reverse;
use std::collections::{HashMap, HashSet};
use std::path::Path;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct StageTime {
pub stage: String,
pub ms: u64,
#[serde(default, skip_serializing_if = "String::is_empty")]
pub detail: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Snapshot {
pub schema_version: String,
pub generated_at: DateTime<Utc>,
pub command: String,
pub workspace: String,
pub target: String,
pub plugin: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub config_file: Option<String>,
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
pub local_only: bool,
pub versions: HashMap<String, String>,
#[serde(default, skip_serializing_if = "HashMap::is_empty")]
pub roots: HashMap<String, String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub git: Option<GitInfo>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub timings: Vec<StageTime>,
pub graphs: PluginGraphs,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GitInfo {
pub branch: String,
pub commit: String,
pub dirty_files: u32,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct PluginGraphs {
pub modules: Graph,
#[serde(default, skip_serializing_if = "Graph::is_empty")]
pub files: Graph,
pub functions: Graph,
}
impl Snapshot {
#[allow(clippy::too_many_arguments)]
pub fn new(
command: String,
workspace: String,
target: String,
plugin: String,
config_file: Option<String>,
local_only: bool,
versions: HashMap<String, String>,
roots: HashMap<String, String>,
git: Option<GitInfo>,
timings: Vec<StageTime>,
graphs: PluginGraphs,
) -> Self {
Self {
schema_version: "1".to_string(),
generated_at: Utc::now(),
command,
workspace,
target,
plugin,
config_file,
local_only,
versions,
roots,
git,
timings,
graphs,
}
}
}
pub fn relativize_graphs(
graphs: &mut PluginGraphs,
target: &Path,
roots: &HashMap<String, String>,
) {
for graph in [
&mut graphs.modules,
&mut graphs.files,
&mut graphs.functions,
] {
for node in &mut graph.nodes {
node.path = relativize_path(&node.path, target, roots);
}
}
}
pub(crate) fn relativize_path(
path: &str,
target: &Path,
roots: &HashMap<String, String>,
) -> String {
if path.is_empty() {
return path.to_string();
}
let p = Path::new(path);
if let Ok(rel) = p.strip_prefix(target) {
return format!("{{target}}/{}", rel.to_string_lossy());
}
let mut sorted: Vec<_> = roots.iter().collect();
sorted.sort_by_key(|(_, root)| Reverse(root.len()));
for (name, root) in &sorted {
if let Ok(rel) = p.strip_prefix(root.as_str()) {
return format!("{{{name}}}/{}", rel.to_string_lossy());
}
}
path.to_string()
}
pub fn rewrite_ids(graphs: &mut PluginGraphs, target: &Path, roots: &HashMap<String, String>) {
let mut pkg_info: HashMap<String, (String, String)> = HashMap::new();
for node in graphs
.modules
.nodes
.iter()
.chain(graphs.files.nodes.iter())
.chain(graphs.functions.nodes.iter())
{
if node.kind == NodeKind::Crate
&& let Some(pkg_repr) = node.id.strip_prefix("crate:")
{
pkg_info
.entry(pkg_repr.to_string())
.or_insert_with(|| parse_pkg_repr(pkg_repr));
}
}
let mut name_versions: HashMap<String, HashSet<String>> = HashMap::new();
for (name, version) in pkg_info.values() {
name_versions
.entry(name.clone())
.or_default()
.insert(version.clone());
}
let crate_map: HashMap<String, String> = pkg_info
.iter()
.map(|(repr, (name, version))| {
let conflict = name_versions.get(name).is_some_and(|v| v.len() > 1);
let short = if conflict && !version.is_empty() {
format!("{name}@{version}")
} else {
name.clone()
};
(repr.clone(), short)
})
.collect();
let mut id_map: HashMap<String, String> = HashMap::new();
for node in graphs
.modules
.nodes
.iter()
.chain(graphs.files.nodes.iter())
.chain(graphs.functions.nodes.iter())
{
let new_id = rewrite_node_id(&node.id, &crate_map, target, roots);
if new_id != node.id {
id_map.insert(node.id.clone(), new_id);
}
}
for graph in [
&mut graphs.modules,
&mut graphs.files,
&mut graphs.functions,
] {
for node in &mut graph.nodes {
if let Some(new_id) = id_map.get(&node.id) {
node.id = new_id.clone();
}
if let Some(parent) = node.parent.as_mut() {
if let Some(new_parent) = id_map.get(parent.as_str()) {
*parent = new_parent.clone();
} else {
let rewritten = rewrite_node_id(parent, &crate_map, target, roots);
if rewritten != *parent {
*parent = rewritten;
}
}
}
}
for edge in &mut graph.edges {
if let Some(v) = id_map.get(&edge.from) {
edge.from = v.clone();
}
if let Some(v) = id_map.get(&edge.to) {
edge.to = v.clone();
}
}
}
}
fn rewrite_node_id(
id: &str,
crate_map: &HashMap<String, String>,
target: &Path,
roots: &HashMap<String, String>,
) -> String {
if let Some(pkg_repr) = id.strip_prefix("crate:") {
let short = crate_map
.get(pkg_repr)
.cloned()
.unwrap_or_else(|| parse_pkg_repr(pkg_repr).0);
return format!("crate:{short}");
}
for kind in ["mod", "trait", "fn", "method"] {
let prefix = format!("{kind}:");
if let Some(rest) = id.strip_prefix(&prefix)
&& let Some((pkg_repr, path_part)) = split_version_boundary(rest)
{
let short = crate_map
.get(&pkg_repr)
.cloned()
.unwrap_or_else(|| parse_pkg_repr(&pkg_repr).0);
let trimmed = path_part
.strip_prefix(&format!("{short}::"))
.unwrap_or(&path_part)
.to_string();
return format!("{kind}:{short}::{trimmed}");
}
}
if let Some(abs_path) = id.strip_prefix("file:") {
let rel = relativize_path(abs_path, target, roots);
return format!("file:{rel}");
}
id.to_string()
}
fn split_version_boundary(s: &str) -> Option<(String, String)> {
let hash_pos = s.find('#')?;
let after_hash = &s[hash_pos + 1..];
let colon_pos = after_hash.find("::")?;
let pkg_repr = s[..hash_pos + 1 + colon_pos].to_string();
let path_part = after_hash[colon_pos + 2..].to_string();
Some((pkg_repr, path_part))
}
fn parse_pkg_repr(repr: &str) -> (String, String) {
if let Some(hash_pos) = repr.rfind('#') {
let after = &repr[hash_pos + 1..];
if let Some((name, ver)) = after.split_once('@') {
return (name.to_string(), ver.to_string());
}
let version = after.to_string();
let before = &repr[..hash_pos];
let before = before.split('?').next().unwrap_or(before);
let name = before
.split('/')
.next_back()
.unwrap_or("unknown")
.to_string();
return (name, version);
}
(repr.to_string(), String::new())
}