use std::collections::HashMap;
use std::io::Write;
use std::path::PathBuf;
use std::sync::{Mutex, OnceLock};
use crate::graph::{Edge, HeuristicTier};
#[derive(Clone, Copy, PartialEq, Eq)]
pub(super) enum Step {
BaseMember,
QualifierMember,
QualifierType,
TypedReceiver,
PropertyHop,
Extension,
Scored,
}
impl Step {
fn name(self) -> &'static str {
match self {
Step::BaseMember => "base-member",
Step::QualifierMember => "qualifier-member",
Step::QualifierType => "qualifier-type",
Step::TypedReceiver => "typed-receiver",
Step::PropertyHop => "property-hop",
Step::Extension => "extension",
Step::Scored => "scored",
}
}
}
type Key = (String, usize, String, String, Option<String>);
fn key(e: &Edge) -> Option<Key> {
match e {
Edge::UsesMember {
from_file,
from_line,
to,
to_file,
member,
..
} => Some((
from_file.clone(),
*from_line,
to.clone(),
to_file.clone(),
member.clone(),
)),
_ => None,
}
}
fn out_path() -> Option<&'static PathBuf> {
static PATH: OnceLock<Option<PathBuf>> = OnceLock::new();
PATH.get_or_init(|| std::env::var_os("SCOUT_EDGE_PROVENANCE").map(PathBuf::from))
.as_ref()
}
fn rows() -> &'static Mutex<HashMap<Key, Step>> {
static ROWS: OnceLock<Mutex<HashMap<Key, Step>>> = OnceLock::new();
ROWS.get_or_init(|| Mutex::new(HashMap::new()))
}
pub(super) fn note(edges: &[Edge], step: Step) {
if out_path().is_none() {
return;
}
let Some(k) = edges.last().and_then(key) else {
return;
};
if let Ok(mut map) = rows().lock() {
map.entry(k).or_insert(step);
}
}
pub(super) fn flush(edges: &[Edge]) {
let Some(path) = out_path() else {
return;
};
let Ok(map) = rows().lock() else {
return;
};
let mut body = String::new();
for e in edges {
let Some(k) = key(e) else {
continue;
};
let step = match map.get(&k) {
Some(s) => format!("\"{}\"", s.name()),
None => "null".to_string(),
};
let tier = match e.tier() {
Some(HeuristicTier::Ext) => "\"ext\"",
Some(HeuristicTier::Guess) => "\"guess\"",
None => "\"precise\"",
};
let member = match &k.4 {
Some(m) => escape(m),
None => "null".to_string(),
};
body.push_str(&format!(
"{{\"file\":{},\"line\":{},\"to\":{},\"to_file\":{},\"member\":{},\"tier\":{},\"step\":{}}}\n",
escape(&k.0),
k.1,
escape(&k.2),
escape(&k.3),
member,
tier,
step
));
}
if let Ok(mut f) = std::fs::File::create(path) {
let _ = f.write_all(body.as_bytes());
}
}
fn escape(s: &str) -> String {
serde_json::to_string(s).expect("a &str always serializes to a JSON string")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn escape_turns_a_raw_control_character_into_a_valid_json_escape() {
let out = escape("line\none\ttwo");
assert_eq!(out, "\"line\\none\\ttwo\"");
let parsed: serde_json::Value =
serde_json::from_str(&out).expect("escape must produce valid JSON");
assert_eq!(parsed, "line\none\ttwo");
}
#[test]
fn escape_still_covers_quote_and_backslash() {
let out = escape("a\"b\\c");
assert_eq!(out, "\"a\\\"b\\\\c\"");
}
}