use crate::graph;
use crate::query;
use crate::render;
use super::why::{why_for_uses_member, Why};
pub(crate) enum J {
Str(String),
UInt(u64),
RawNum(String),
Bool(bool),
Arr(Vec<J>),
Obj(Vec<(&'static str, J)>),
}
impl J {
fn write(&self, out: &mut String) {
match self {
J::Str(s) => {
out.push_str(&serde_json::to_string(s).expect("string JSON encoding cannot fail"))
}
J::UInt(n) => out.push_str(&n.to_string()),
J::RawNum(s) => out.push_str(s),
J::Bool(b) => out.push_str(if *b { "true" } else { "false" }),
J::Arr(items) => {
out.push('[');
for (i, item) in items.iter().enumerate() {
if i > 0 {
out.push(',');
}
item.write(out);
}
out.push(']');
}
J::Obj(entries) => {
out.push('{');
for (i, (k, v)) in entries.iter().enumerate() {
if i > 0 {
out.push(',');
}
out.push_str(&serde_json::to_string(k).expect("key JSON encoding cannot fail"));
out.push(':');
v.write(out);
}
out.push('}');
}
}
}
pub(crate) fn to_json_string(&self) -> String {
let mut s = String::new();
self.write(&mut s);
s
}
}
pub(crate) fn js_float_string(v: f64) -> String {
if !v.is_finite() {
return "null".to_string();
}
if v == 0.0 {
return "0".to_string();
}
let s = serde_json::Number::from_f64(v)
.expect("finite, checked above")
.to_string();
if !s.contains('e') && s.ends_with(".0") {
s[..s.len() - 2].to_string()
} else {
s
}
}
fn j_table<R>(t: &query::Table<R>, row: impl Fn(&R) -> J) -> J {
J::Obj(vec![
("total", J::UInt(t.total as u64)),
("dropped", J::UInt(t.dropped as u64)),
("rows", J::Arr(t.rows.iter().map(row).collect())),
])
}
fn push_heuristic(
fields: &mut Vec<(&'static str, J)>,
heuristic: bool,
tier: Option<graph::HeuristicTier>,
) {
if !heuristic {
return;
}
fields.push(("heuristic", J::Bool(true)));
if let Some(tier) = tier {
let word = match tier {
graph::HeuristicTier::Ext => "ext",
graph::HeuristicTier::Guess => "guess",
};
fields.push(("tier", J::Str(word.to_string())));
}
}
#[derive(Debug, Clone, Copy)]
enum RowKind {
Inherits,
UsesType,
UsesMember,
Implements,
Overrides,
}
fn why_for_row(kind: RowKind, heuristic: bool, tier: Option<graph::HeuristicTier>) -> Why {
match kind {
RowKind::Inherits => Why::Inherits,
RowKind::UsesType => Why::UsesType,
RowKind::UsesMember => why_for_uses_member(heuristic, tier),
RowKind::Implements => Why::Implements,
RowKind::Overrides => Why::Overrides,
}
}
fn j_inbound_row(r: &query::InboundRow, kind: RowKind) -> J {
let mut fields = vec![
("file", J::Str(r.file.clone())),
("line", J::UInt(r.line as u64)),
];
push_heuristic(&mut fields, r.heuristic, r.tier);
if !r.source.is_empty() {
fields.push(("source", J::Str(r.source.clone())));
}
fields.push((
"why",
J::Str(why_for_row(kind, r.heuristic, r.tier).as_str().to_string()),
));
J::Obj(fields)
}
fn j_outbound_row(r: &query::OutboundRow, kind: RowKind) -> J {
let mut fields = vec![
("file", J::Str(r.file.clone())),
("line", J::UInt(r.line as u64)),
("toFile", J::Str(r.to_file.clone())),
("to", J::Str(r.to.clone())),
];
push_heuristic(&mut fields, r.heuristic, r.tier);
if !r.source.is_empty() {
fields.push(("source", J::Str(r.source.clone())));
}
fields.push((
"why",
J::Str(why_for_row(kind, r.heuristic, r.tier).as_str().to_string()),
));
J::Obj(fields)
}
fn j_import_row(r: &query::ImportRow) -> J {
let mut fields = vec![
("file", J::Str(r.file.clone())),
("line", J::UInt(r.line as u64)),
("target", J::Str(r.target.clone())),
];
if !r.source.is_empty() {
fields.push(("source", J::Str(r.source.clone())));
}
fields.push(("why", J::Str(Why::Imports.as_str().to_string())));
J::Obj(fields)
}
fn j_ambiguous_row(r: &query::AmbiguousRow) -> J {
J::Obj(vec![
("file", J::Str(r.file.clone())),
("line", J::UInt(r.line as u64)),
("origin", J::Str(r.origin.clone())),
("raw", J::Str(r.raw.clone())),
("candidateCount", J::UInt(r.candidate_count as u64)),
])
}
pub(crate) fn refs_model_to_json(model: &query::RefsModel) -> String {
refs_model_j(model, query::Outcome::Hit).to_json_string()
}
pub(crate) fn read_model_to_json(model: &query::ReadModel) -> String {
let mut fields = refs_model_fields(&model.refs, query::Outcome::Hit);
let tail = fields.split_off(5);
if let Some(sp) = &model.span {
fields.push((
"span",
J::Obj(vec![
("file", J::Str(sp.file.clone())),
("startLine", J::UInt(sp.start_line as u64)),
("endLine", J::UInt(sp.end_line as u64)),
("source", J::Str(sp.source.clone())),
("why", J::Str(Why::Declaration.as_str().to_string())),
]),
));
}
fields.extend(tail);
J::Obj(fields).to_json_string()
}
pub(crate) fn member_refs_to_json(
query_str: &str,
models: &[query::RefsModel],
outcome: query::Outcome,
) -> String {
J::Obj(vec![
("schema_version", J::UInt(query::SCHEMA_VERSION)),
("status", J::Str("members".to_string())),
("query", J::Str(query_str.to_string())),
(
"members",
J::Arr(models.iter().map(|m| refs_model_j(m, outcome)).collect()),
),
("outcome", J::Str(outcome.as_str().to_string())),
])
.to_json_string()
}
fn refs_model_j(model: &query::RefsModel, outcome: query::Outcome) -> J {
J::Obj(refs_model_fields(model, outcome))
}
fn dispatch_table_fields<R>(
implements: &query::Table<R>,
overrides: &query::Table<R>,
row: impl Fn(&R, RowKind) -> J + Copy,
) -> Vec<(&'static str, J)> {
let mut fields = Vec::new();
if implements.total != 0 {
fields.push((
"implements",
j_table(implements, |r| row(r, RowKind::Implements)),
));
}
if overrides.total != 0 {
fields.push((
"overrides",
j_table(overrides, |r| row(r, RowKind::Overrides)),
));
}
fields
}
fn j_inbound_tables(t: &query::InboundTables) -> J {
J::Obj(
vec![
(
"inherits",
j_table(&t.inherits, |r| j_inbound_row(r, RowKind::Inherits)),
),
(
"uses-type",
j_table(&t.uses_type, |r| j_inbound_row(r, RowKind::UsesType)),
),
(
"uses-member",
j_table(&t.uses_member, |r| j_inbound_row(r, RowKind::UsesMember)),
),
]
.into_iter()
.chain(dispatch_table_fields(
&t.implements,
&t.overrides,
j_inbound_row,
))
.collect::<Vec<_>>(),
)
}
fn j_outbound_tables(t: &query::OutboundTables) -> J {
J::Obj(
vec![
(
"inherits",
j_table(&t.inherits, |r| j_outbound_row(r, RowKind::Inherits)),
),
(
"uses-type",
j_table(&t.uses_type, |r| j_outbound_row(r, RowKind::UsesType)),
),
(
"uses-member",
j_table(&t.uses_member, |r| j_outbound_row(r, RowKind::UsesMember)),
),
]
.into_iter()
.chain(dispatch_table_fields(
&t.implements,
&t.overrides,
j_outbound_row,
))
.chain(std::iter::once((
"imports",
j_table(&t.imports, j_import_row),
)))
.collect::<Vec<_>>(),
)
}
fn refs_model_fields(model: &query::RefsModel, outcome: query::Outcome) -> Vec<(&'static str, J)> {
let mut fields = vec![
("schema_version", J::UInt(query::SCHEMA_VERSION)),
("status", J::Str("resolved".to_string())),
("query", J::Str(model.query.clone())),
("id", J::Str(model.id.clone())),
("kind", J::Str(model.kind.clone())),
(
"sites",
J::Arr(
model
.sites
.iter()
.map(|s| {
J::Obj(vec![
("file", J::Str(s.file.clone())),
("line", J::UInt(s.line as u64)),
])
})
.collect(),
),
),
("inbound", j_inbound_tables(&model.inbound)),
];
if let Some(ob) = &model.outbound {
fields.push(("outbound", j_outbound_tables(ob)));
}
fields.push((
"ambiguous",
J::Obj(vec![
(
"inbound",
j_table(&model.ambiguous.inbound, j_ambiguous_row),
),
(
"outbound",
j_table(&model.ambiguous.outbound, j_ambiguous_row),
),
]),
));
fields.push(("manifestGap", J::UInt(model.manifest_gap as u64)));
if let Some(m) = &model.member_refs {
fields.push((
"memberRefs",
J::Obj(vec![
("total", J::UInt(m.total as u64)),
("memberCount", J::UInt(m.member_count as u64)),
(
"members",
J::Arr(
m.members
.iter()
.map(|e| {
J::Obj(vec![
("name", J::Str(e.name.clone())),
("count", J::UInt(e.count as u64)),
])
})
.collect(),
),
),
("dropped", J::UInt(m.dropped as u64)),
]),
));
}
fields.push(("outcome", J::Str(outcome.as_str().to_string())));
fields
}
fn j_impact_row(r: &query::ImpactRow) -> J {
let mut fields = vec![
("file", J::Str(r.file.clone())),
("hop", J::UInt(r.hop as u64)),
("viaCount", J::UInt(r.via_count as u64)),
("ambiguousCount", J::UInt(r.ambiguous_count as u64)),
(
"topSymbols",
J::Arr(r.top_symbols.iter().map(|s| J::Str(s.clone())).collect()),
),
("topSymbolsMore", J::UInt(r.top_symbols_more as u64)),
("score", J::RawNum(js_float_string(r.score))),
];
if r.heuristic {
fields.push(("heuristicCount", J::UInt(r.heuristic_count as u64)));
}
push_heuristic(&mut fields, r.heuristic, r.tier);
if !r.iface_via.is_empty() {
fields.push((
"ifaceVia",
J::Arr(r.iface_via.iter().map(|s| J::Str(s.clone())).collect()),
));
}
if !r.from_lines.is_empty() {
fields.push((
"fromLines",
J::Obj(
r.from_lines
.iter()
.map(|(kind, line)| (*kind, J::UInt(*line as u64)))
.collect(),
),
));
}
if r.infra {
fields.push(("class", J::Str("infra".to_string())));
}
fields.push(("why", J::Str(r.why.as_str().to_string())));
J::Obj(fields)
}
fn impact_model_fields(query_str: &str, model: &query::ImpactModel) -> Vec<(&'static str, J)> {
vec![
("schema_version", J::UInt(query::SCHEMA_VERSION)),
("query", J::Str(query_str.to_string())),
("status", J::Str("resolved".to_string())),
(
"kind",
J::Str(render::seed_kind_str(model.kind).to_string()),
),
(
"seedFiles",
J::Arr(model.seed_files.iter().map(|f| J::Str(f.clone())).collect()),
),
("hops", J::UInt(model.hops as u64)),
("totalAffected", J::UInt(model.total_affected as u64)),
(
"rows",
J::Arr(model.rows.iter().map(j_impact_row).collect()),
),
("dropped", J::UInt(model.dropped as u64)),
("manifestGap", J::UInt(model.manifest_gap as u64)),
(
"heuristicAffected",
J::UInt(model.heuristic_affected as u64),
),
("testsAffected", J::UInt(model.tests_affected as u64)),
]
.into_iter()
.chain(
if model.braked.is_empty() && model.braked_files.is_empty() {
None
} else {
Some((
"braked",
J::Arr(
model
.braked
.iter()
.map(|b| {
J::Obj(vec![
("iface", J::Str(b.iface.clone())),
("fanin", J::UInt(b.fanin as u64)),
])
})
.chain(model.braked_files.iter().map(|b| {
J::Obj(vec![
("file", J::Str(b.file.clone())),
("indegree", J::UInt(b.indegree as u64)),
])
}))
.collect(),
),
))
},
)
.collect()
}
fn impact_outcome(model: &query::ImpactModel, imported_hit: bool) -> query::Outcome {
if model.rows.is_empty() && !imported_hit {
query::Outcome::ZeroHit
} else {
query::Outcome::Hit
}
}
pub(crate) fn impact_model_to_json(query_str: &str, model: &query::ImpactModel) -> String {
let mut fields = impact_model_fields(query_str, model);
let outcome = impact_outcome(model, false);
fields.push(("outcome", J::Str(outcome.as_str().to_string())));
J::Obj(fields).to_json_string()
}
fn j_imported_row(r: &query::ImportedRow) -> J {
J::Obj(vec![
("file", J::Str(r.file.clone())),
("hop", J::UInt(r.hop as u64)),
("repo", J::Str(r.repo.clone())),
("importedKind", J::Str(r.imported_kind.clone())),
("why", J::Str(r.why.as_str().to_string())),
])
}
pub(crate) fn impact_model_to_json_with_imports(
query_str: &str,
model: &query::ImpactModel,
imported: &query::ImportedSection,
provenance: &graph::Provenance,
) -> String {
let mut fields = impact_model_fields(query_str, model);
fields.push(("importedAffected", J::UInt(imported.affected as u64)));
fields.push(("importedDropped", J::UInt(imported.dropped as u64)));
fields.push((
"importedRows",
J::Arr(imported.rows.iter().map(j_imported_row).collect()),
));
fields.push((
"provenance",
J::Obj(vec![
("id", J::Str(provenance.id.clone())),
("producer", J::Str(provenance.producer.clone())),
("formatVersion", J::UInt(provenance.format_version)),
]),
));
let outcome = impact_outcome(model, !imported.rows.is_empty());
fields.push(("outcome", J::Str(outcome.as_str().to_string())));
J::Obj(fields).to_json_string()
}
pub(crate) fn tests_model_to_json(model: &query::TestsModel) -> String {
J::Obj(vec![
("schema_version", J::UInt(query::SCHEMA_VERSION)),
("status", J::Str("resolved".to_string())),
("query", J::Str(model.query.clone())),
("symbol", J::Str(model.symbol.clone())),
(
"defFiles",
J::Arr(model.def_files.iter().map(|f| J::Str(f.clone())).collect()),
),
(
"rows",
J::Arr(
model
.rows
.iter()
.map(|r| {
let mut fields = vec![
("file", J::Str(r.file.clone())),
(
"testDefs",
J::Arr(r.test_defs.iter().map(|d| J::Str(d.clone())).collect()),
),
(
"lines",
J::Arr(r.lines.iter().map(|l| J::UInt(*l as u64)).collect()),
),
("refCount", J::UInt(r.ref_count as u64)),
];
push_heuristic(&mut fields, r.heuristic, r.tier);
let why = if r.via == query::TestVia::Project {
fields.push(("via", J::Str("project".to_string())));
Why::TestProject
} else {
Why::TestAttribute
};
fields.push(("why", J::Str(why.as_str().to_string())));
J::Obj(fields)
})
.collect(),
),
),
("testFileCount", J::UInt(model.test_file_count as u64)),
("refCount", J::UInt(model.ref_count as u64)),
(
"heuristicFileCount",
J::UInt(model.heuristic_file_count as u64),
),
(
"heuristicRefCount",
J::UInt(model.heuristic_ref_count as u64),
),
("outcome", J::Str(query::Outcome::Hit.as_str().to_string())),
])
.to_json_string()
}