use super::sources::Texts;
use crate::operations::architecture::source_metrics::function_lines;
use crate::operations::health;
use crate::operations::history::diff::nodes_differ;
use blazingly_json::{Value, json};
use std::collections::BTreeMap;
use weavatrix_graph::{AttributeValue, Graph, Node, NodeKind};
pub(super) struct Symbol {
pub id: String,
pub label: String,
pub kind: String,
pub file: Option<String>,
pub line: Option<u32>,
pub change: &'static str,
}
pub(super) struct Changed {
pub symbols: Vec<Symbol>,
pub files: usize,
}
impl Changed {
pub(super) fn none() -> Self {
Self {
symbols: Vec::new(),
files: 0,
}
}
}
pub(super) fn changed(
baseline: &Graph,
target: &Graph,
texts: &Texts,
args: &Value,
scope: Option<&str>,
) -> Changed {
let before = declarations(baseline, texts, args, scope);
let after = declarations(target, texts, args, scope);
let mut symbols = Vec::new();
for (id, node) in &after {
let change = match before.get(id) {
None => "added",
Some(previous) if body_differs(previous, node, texts) => "modified",
Some(_) => continue,
};
symbols.push(symbol(id, node, change));
}
for (id, node) in &before {
if !after.contains_key(id) {
symbols.push(symbol(id, node, "removed"));
}
}
symbols.sort_by(|left, right| left.id.cmp(&right.id));
Changed {
symbols,
files: texts.len(),
}
}
pub(super) fn render(symbol: &Symbol) -> Value {
json!({
"id": symbol.id,
"label": symbol.label,
"kind": symbol.kind,
"file": symbol.file,
"line": symbol.line,
"change": symbol.change
})
}
fn declarations<'graph>(
graph: &'graph Graph,
texts: &Texts,
args: &Value,
scope: Option<&str>,
) -> BTreeMap<&'graph str, &'graph Node> {
graph
.nodes()
.iter()
.filter(|node| matches!(node.kind, NodeKind::Function | NodeKind::Method))
.filter(|node| {
node.span
.as_ref()
.is_some_and(|span| texts.contains_key(&span.file))
})
.filter(|node| {
health::paths::path_is_in_scope(
crate::operations::node_path(node).unwrap_or_default(),
scope,
)
})
.filter(|node| is_visible(node, args))
.map(|node| (node.id.as_str(), node))
.collect()
}
fn body_differs(before: &Node, after: &Node, texts: &Texts) -> bool {
if nodes_differ(before, after) {
return true;
}
let Some(span) = after.span.as_ref() else {
return true;
};
let Some((left, right)) = texts.get(&span.file) else {
return false;
};
let (Some(left), Some(right)) = (left.as_deref(), right.as_deref()) else {
return true;
};
extent(left, before) != extent(right, after)
}
fn extent(text: &str, node: &Node) -> String {
let Some(span) = node.span.as_ref() else {
return String::new();
};
let lines = function_lines(text, span.start.line, node.language.as_deref());
let skip = usize::try_from(span.start.line)
.unwrap_or(1)
.saturating_sub(1);
let take = usize::try_from(lines).unwrap_or(1).max(1);
text.lines()
.skip(skip)
.take(take)
.collect::<Vec<_>>()
.join("\n")
}
fn is_visible(node: &Node, args: &Value) -> bool {
if matches!(
node.attributes.get("test_only"),
Some(AttributeValue::Bool(true))
) {
return args.get("include_tests").and_then(Value::as_bool) == Some(true);
}
crate::operations::node_path(node).is_none_or(|path| health::path_is_visible(path, args))
}
fn symbol(id: &str, node: &Node, change: &'static str) -> Symbol {
Symbol {
id: id.to_owned(),
label: node.label.clone(),
kind: node.kind.as_str().to_owned(),
file: node.span.as_ref().map(|span| span.file.clone()),
line: node.span.as_ref().map(|span| span.start.line),
change,
}
}