use crate::root_envelopes::{RootEnvelopeMode, attach_telemetry_meta, serialize_named_json_output};
use serde::Serialize;
#[derive(Debug, Clone, Serialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[cfg_attr(
feature = "schema",
schemars(title = "fallow explain <issue-type> --format json")
)]
pub struct ExplainOutput {
pub id: String,
pub name: String,
pub summary: String,
pub rationale: String,
pub example: String,
pub how_to_fix: String,
pub docs: String,
}
pub fn serialize_explain_json_output(
output: ExplainOutput,
mode: RootEnvelopeMode,
analysis_run_id: Option<&str>,
) -> Result<serde_json::Value, serde_json::Error> {
let mut value = serialize_named_json_output(output, "explain", mode)?;
attach_telemetry_meta(&mut value, analysis_run_id);
Ok(value)
}
#[derive(Debug, Clone, Serialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[cfg_attr(feature = "schema", schemars(title = "fallow inspect --format json"))]
pub struct InspectOutput {
pub target: InspectTargetDescriptor,
pub identity: InspectIdentity,
pub evidence: InspectEvidence,
pub warnings: Vec<String>,
}
#[derive(Debug, Clone, Serialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum InspectTargetDescriptor {
File { file: String },
Symbol { file: String, export_name: String },
}
#[derive(Debug, Clone, Serialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(untagged)]
pub enum InspectIdentity {
File(InspectFileIdentity),
Symbol(InspectSymbolIdentity),
}
#[derive(Debug, Clone, Serialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub struct InspectFileIdentity {
pub file: String,
pub is_reachable: Option<serde_json::Value>,
pub is_entry_point: Option<serde_json::Value>,
pub export_count: Option<usize>,
pub import_count: Option<usize>,
pub imported_by_count: Option<usize>,
}
#[derive(Debug, Clone, Serialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub struct InspectSymbolIdentity {
pub file: String,
pub export_name: String,
pub file_reachable: Option<serde_json::Value>,
pub is_entry_point: Option<serde_json::Value>,
pub is_used: Option<serde_json::Value>,
pub reason: Option<serde_json::Value>,
}
#[derive(Debug, Clone, Serialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub struct InspectEvidence {
pub trace_file: InspectEvidenceSection,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub trace_export: Option<InspectEvidenceSection>,
pub dead_code: InspectEvidenceSection,
pub duplication: InspectEvidenceSection,
pub complexity: InspectEvidenceSection,
pub security: InspectEvidenceSection,
pub impact_closure: InspectEvidenceSection,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub symbol_chain: Option<InspectEvidenceSection>,
}
#[derive(Debug, Clone, Serialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub struct InspectEvidenceSection {
pub status: InspectSectionStatus,
pub scope: InspectEvidenceScope,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub message: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub data: Option<serde_json::Value>,
}
impl InspectEvidenceSection {
#[must_use]
pub fn ok(scope: InspectEvidenceScope, data: serde_json::Value) -> Self {
Self {
status: InspectSectionStatus::Ok,
scope,
message: None,
data: Some(data),
}
}
#[must_use]
pub fn error(scope: InspectEvidenceScope, message: String) -> Self {
Self {
status: InspectSectionStatus::Error,
scope,
message: Some(message),
data: None,
}
}
}
pub fn serialize_inspect_json_output(
output: InspectOutput,
mode: RootEnvelopeMode,
analysis_run_id: Option<&str>,
) -> Result<serde_json::Value, serde_json::Error> {
let mut value = serialize_named_json_output(output, "inspect_target", mode)?;
attach_telemetry_meta(&mut value, analysis_run_id);
Ok(value)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(rename_all = "snake_case")]
pub enum InspectSectionStatus {
Ok,
Error,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(rename_all = "snake_case")]
pub enum InspectEvidenceScope {
Symbol,
File,
ProjectFilteredToFile,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn explain_json_output_uses_output_owned_root_contract() {
let output = ExplainOutput {
id: "unused-export".to_string(),
name: "Unused export".to_string(),
summary: "summary".to_string(),
rationale: "rationale".to_string(),
example: "example".to_string(),
how_to_fix: "fix".to_string(),
docs: "https://example.test".to_string(),
};
let value =
serialize_explain_json_output(output, RootEnvelopeMode::Tagged, Some("run-explain"))
.expect("explain output should serialize");
assert_eq!(value["kind"], "explain");
assert_eq!(
value["_meta"]["telemetry"]["analysis_run_id"],
"run-explain"
);
}
#[test]
fn inspect_json_output_uses_output_owned_root_contract() {
let output = InspectOutput {
target: InspectTargetDescriptor::File {
file: "src/app.ts".to_string(),
},
identity: InspectIdentity::File(InspectFileIdentity {
file: "src/app.ts".to_string(),
is_reachable: None,
is_entry_point: None,
export_count: Some(0),
import_count: Some(0),
imported_by_count: Some(0),
}),
evidence: InspectEvidence {
trace_file: InspectEvidenceSection::ok(
InspectEvidenceScope::File,
serde_json::json!({}),
),
trace_export: None,
dead_code: InspectEvidenceSection::error(
InspectEvidenceScope::File,
"not run".to_string(),
),
duplication: InspectEvidenceSection::error(
InspectEvidenceScope::ProjectFilteredToFile,
"not run".to_string(),
),
complexity: InspectEvidenceSection::error(
InspectEvidenceScope::ProjectFilteredToFile,
"not run".to_string(),
),
security: InspectEvidenceSection::error(
InspectEvidenceScope::File,
"not run".to_string(),
),
impact_closure: InspectEvidenceSection::error(
InspectEvidenceScope::ProjectFilteredToFile,
"not run".to_string(),
),
symbol_chain: None,
},
warnings: Vec::new(),
};
let value =
serialize_inspect_json_output(output, RootEnvelopeMode::Tagged, Some("run-inspect"))
.expect("inspect output should serialize");
assert_eq!(value["kind"], "inspect_target");
assert_eq!(
value["_meta"]["telemetry"]["analysis_run_id"],
"run-inspect"
);
}
}