use std::path::Path;
use std::time::Duration;
use fallow_output::{
CHECK_SCHEMA_VERSION, CheckEnvelope, CheckGroupedEntry, CheckGroupedOutput, CheckOutput,
CheckOutputInput, DUPES_SCHEMA_VERSION, DupesOutput, DupesOutputInput, GroupByMode,
apply_config_fixable_to_duplicate_exports, build_check_output, build_dupes_output,
harmonize_multi_kind_suppress_line_actions as harmonize_typed_suppress_line_actions,
strip_root_prefix,
};
use fallow_types::duplicates::DuplicationReport;
use fallow_types::envelope::{
BaselineDeltas, BaselineMatch, ElapsedMs, Meta, RegressionResult, SchemaVersion, ToolVersion,
};
use fallow_types::output::NextStep;
use fallow_types::results::AnalysisResults;
use fallow_types::workspace::WorkspaceDiagnostic;
use crate::{DupesReportPayload, DuplicationGroup, DuplicationGrouping, ResultGroup};
pub struct CheckJsonOutputInput<'a> {
pub envelope: CheckEnvelope,
pub results: &'a AnalysisResults,
pub root: &'a Path,
pub elapsed: Duration,
pub config_fixable: bool,
pub meta: Option<Meta>,
pub extras: CheckJsonExtraOutputs,
pub workspace_diagnostics: Vec<WorkspaceDiagnostic>,
pub next_steps: Vec<NextStep>,
pub telemetry_analysis_run_id: Option<&'a str>,
}
pub struct CheckJsonPayloadInput<'a> {
pub results: &'a AnalysisResults,
pub root: &'a Path,
pub elapsed: Duration,
pub config_fixable: bool,
pub extras: CheckJsonExtraOutputs,
pub workspace_diagnostics: Vec<WorkspaceDiagnostic>,
}
#[derive(Debug, Clone, Default)]
pub struct CheckJsonExtraOutputs {
pub package_baselines: Vec<fallow_output::PackageBaselineStatus>,
pub baseline_deltas: Option<BaselineDeltas>,
pub baseline: Option<BaselineMatch>,
pub baseline_staleness: Option<fallow_output::BaselineStaleness>,
pub regression: Option<RegressionResult>,
pub gate_outcomes: Option<fallow_output::GateOutcomes>,
pub request_outcomes: Option<fallow_output::RequestOutcomes>,
pub finding_id_query: Option<fallow_output::FindingIdQuery>,
}
struct CheckJsonEnvelopeInput<'a> {
schema_version: u32,
results: &'a AnalysisResults,
elapsed: Duration,
config_fixable: bool,
meta: Option<Meta>,
extras: CheckJsonExtraOutputs,
workspace_diagnostics: Vec<WorkspaceDiagnostic>,
next_steps: Vec<NextStep>,
}
pub struct GroupedCheckJsonOutputInput<'a> {
pub envelope: CheckEnvelope,
pub package_baselines: Vec<fallow_output::PackageBaselineStatus>,
pub baseline_staleness: Option<fallow_output::BaselineStaleness>,
pub gate_outcomes: Option<fallow_output::GateOutcomes>,
pub request_outcomes: Option<fallow_output::RequestOutcomes>,
pub finding_id_query: Option<fallow_output::FindingIdQuery>,
pub groups: &'a [ResultGroup],
pub original: &'a AnalysisResults,
pub root: &'a Path,
pub elapsed: Duration,
pub grouped_by: GroupByMode,
pub config_fixable: bool,
pub meta: Option<Meta>,
pub workspace_diagnostics: Vec<WorkspaceDiagnostic>,
pub next_steps: Vec<NextStep>,
pub telemetry_analysis_run_id: Option<&'a str>,
}
pub struct DuplicationJsonOutputInput<'a> {
pub package_baselines: Vec<fallow_output::PackageBaselineStatus>,
pub baseline_staleness: Option<fallow_output::BaselineStaleness>,
pub gate_outcomes: Option<fallow_output::GateOutcomes>,
pub request_outcomes: Option<fallow_output::RequestOutcomes>,
pub report: &'a DuplicationReport,
pub root: &'a Path,
pub elapsed: Duration,
pub include_fragments: bool,
pub meta: Option<Meta>,
pub workspace_diagnostics: Vec<WorkspaceDiagnostic>,
pub next_steps: Vec<NextStep>,
pub telemetry_analysis_run_id: Option<&'a str>,
}
pub struct GroupedDuplicationJsonOutputInput<'a> {
pub package_baselines: Vec<fallow_output::PackageBaselineStatus>,
pub baseline_staleness: Option<fallow_output::BaselineStaleness>,
pub gate_outcomes: Option<fallow_output::GateOutcomes>,
pub request_outcomes: Option<fallow_output::RequestOutcomes>,
pub report: &'a DuplicationReport,
pub grouping: &'a DuplicationGrouping,
pub root: &'a Path,
pub elapsed: Duration,
pub include_fragments: bool,
pub meta: Option<Meta>,
pub workspace_diagnostics: Vec<WorkspaceDiagnostic>,
pub next_steps: Vec<NextStep>,
pub telemetry_analysis_run_id: Option<&'a str>,
}
pub fn serialize_check_json(
input: CheckJsonOutputInput<'_>,
) -> Result<serde_json::Value, serde_json::Error> {
let envelope = build_check_json_envelope(CheckJsonEnvelopeInput {
schema_version: input.envelope.schema_version(),
results: input.results,
elapsed: input.elapsed,
config_fixable: input.config_fixable,
meta: input.meta,
extras: input.extras,
workspace_diagnostics: input.workspace_diagnostics,
next_steps: input.next_steps,
});
let run_id = input.telemetry_analysis_run_id;
let mut output = match input.envelope {
CheckEnvelope::DeadCode => fallow_output::serialize_check_json_output(envelope, run_id)?,
CheckEnvelope::Architecture => {
fallow_output::serialize_architecture_json_output(envelope, run_id)?
}
};
strip_json_root_prefix(&mut output, input.root);
Ok(output)
}
pub fn serialize_check_json_payload(
input: CheckJsonPayloadInput<'_>,
) -> Result<serde_json::Value, serde_json::Error> {
let envelope = build_check_json_envelope(CheckJsonEnvelopeInput {
schema_version: CHECK_SCHEMA_VERSION,
results: input.results,
elapsed: input.elapsed,
config_fixable: input.config_fixable,
meta: None,
extras: input.extras,
workspace_diagnostics: input.workspace_diagnostics,
next_steps: Vec::new(),
});
let mut output = serde_json::to_value(envelope)?;
strip_json_root_prefix(&mut output, input.root);
Ok(output)
}
pub fn serialize_grouped_check_json(
input: GroupedCheckJsonOutputInput<'_>,
) -> Result<serde_json::Value, serde_json::Error> {
let entries = input
.groups
.iter()
.map(|group| {
let mut results = group.results.clone();
apply_config_fixable_to_duplicate_exports(&mut results, input.config_fixable);
harmonize_typed_suppress_line_actions(&mut results);
CheckGroupedEntry {
key: group.key.clone(),
owners: group.owners.clone(),
total_issues: results.total_issues(),
results,
}
})
.collect();
let envelope = CheckGroupedOutput {
package_baselines: input.package_baselines,
request_outcomes: input.request_outcomes,
schema_version: SchemaVersion(input.envelope.schema_version()),
version: ToolVersion(env!("CARGO_PKG_VERSION").to_string()),
elapsed_ms: ElapsedMs(input.elapsed.as_millis() as u64),
grouped_by: input.grouped_by,
total_issues: input.original.total_issues(),
groups: entries,
unused_load_data_keys_global_abstain: input.original.unused_load_data_keys_global_abstain,
cascade_hidden: input.original.cascade_hidden,
baseline_staleness: input.baseline_staleness,
finding_id_query: input.finding_id_query,
gate_outcomes: input.gate_outcomes,
meta: input.meta,
workspace_diagnostics: input.workspace_diagnostics,
next_steps: input.next_steps,
};
let run_id = input.telemetry_analysis_run_id;
let mut output = match input.envelope {
CheckEnvelope::DeadCode => {
fallow_output::serialize_check_grouped_json_output(envelope, run_id)?
}
CheckEnvelope::Architecture => {
fallow_output::serialize_architecture_grouped_json_output(envelope, run_id)?
}
};
strip_json_root_prefix(&mut output, input.root);
Ok(output)
}
pub fn serialize_duplication_json(
input: DuplicationJsonOutputInput<'_>,
) -> Result<serde_json::Value, serde_json::Error> {
let payload =
DupesReportPayload::from_report_with_fragments(input.report, input.include_fragments);
let mut envelope: DupesOutput<DupesReportPayload, DuplicationGroup> =
build_dupes_output(DupesOutputInput {
gate_outcomes: input.gate_outcomes,
request_outcomes: input.request_outcomes,
schema_version: DUPES_SCHEMA_VERSION,
version: env!("CARGO_PKG_VERSION").to_string(),
elapsed: input.elapsed,
report: payload,
clone_groups_shown: input.report.clone_groups_shown(),
clone_groups_omitted: input.report.clone_groups_omitted(),
clone_families_shown: input.report.clone_families_shown(),
clone_families_omitted: input.report.clone_families_omitted(),
grouped_by: None,
total_issues: None,
groups: None,
baseline_staleness: input.baseline_staleness,
meta: input.meta,
workspace_diagnostics: input.workspace_diagnostics,
next_steps: input.next_steps,
});
envelope.package_baselines = input.package_baselines;
let mut output =
fallow_output::serialize_dupes_json_output(envelope, input.telemetry_analysis_run_id)?;
let root_prefix = format!("{}/", input.root.display());
strip_root_prefix(&mut output, &root_prefix);
Ok(output)
}
pub fn serialize_grouped_duplication_json(
input: GroupedDuplicationJsonOutputInput<'_>,
) -> Result<serde_json::Value, serde_json::Error> {
let root_prefix = format!("{}/", input.root.display());
let payload =
DupesReportPayload::from_report_with_fragments(input.report, input.include_fragments);
let mut envelope: DupesOutput<DupesReportPayload, DuplicationGroup> =
build_dupes_output(DupesOutputInput {
gate_outcomes: input.gate_outcomes,
request_outcomes: input.request_outcomes,
schema_version: DUPES_SCHEMA_VERSION,
version: env!("CARGO_PKG_VERSION").to_string(),
elapsed: input.elapsed,
report: payload,
clone_groups_shown: input.report.clone_groups_shown(),
clone_groups_omitted: input.report.clone_groups_omitted(),
clone_families_shown: input.report.clone_families_shown(),
clone_families_omitted: input.report.clone_families_omitted(),
grouped_by: Some(group_by_mode_from_label(input.grouping.mode)),
total_issues: Some(input.report.clone_groups.len()),
groups: None,
baseline_staleness: input.baseline_staleness,
meta: input.meta,
workspace_diagnostics: input.workspace_diagnostics,
next_steps: input.next_steps,
});
envelope.package_baselines = input.package_baselines;
let mut output =
fallow_output::serialize_dupes_json_output(envelope, input.telemetry_analysis_run_id)?;
strip_root_prefix(&mut output, &root_prefix);
let group_values = input
.grouping
.groups
.iter()
.map(|group| {
let mut value = if input.include_fragments {
serde_json::to_value(group)?
} else {
let mut stripped = group.clone();
stripped.strip_fragments();
serde_json::to_value(&stripped)?
};
strip_root_prefix(&mut value, &root_prefix);
Ok(value)
})
.collect::<Result<Vec<_>, serde_json::Error>>()?;
if let serde_json::Value::Object(ref mut map) = output {
map.insert("groups".to_string(), serde_json::Value::Array(group_values));
}
Ok(output)
}
fn build_check_json_envelope(input: CheckJsonEnvelopeInput<'_>) -> CheckOutput {
let mut output = build_check_output(CheckOutputInput {
schema_version: input.schema_version,
version: env!("CARGO_PKG_VERSION").to_string(),
elapsed: input.elapsed,
results: input.results.clone(),
config_fixable: input.config_fixable,
meta: input.meta,
workspace_diagnostics: input.workspace_diagnostics,
next_steps: input.next_steps,
});
output.baseline_deltas = input.extras.baseline_deltas;
output.baseline = input.extras.baseline;
output.baseline_staleness = input.extras.baseline_staleness;
output.regression = input.extras.regression;
output.gate_outcomes = input.extras.gate_outcomes;
output.request_outcomes = input.extras.request_outcomes;
output.package_baselines = input.extras.package_baselines;
output.finding_id_query = input.extras.finding_id_query;
output
}
fn strip_json_root_prefix(output: &mut serde_json::Value, root: &Path) {
let root_prefix = format!("{}/", root.display());
strip_root_prefix(output, &root_prefix);
}
fn group_by_mode_from_label(label: &str) -> GroupByMode {
match label {
"directory" => GroupByMode::Directory,
"package" => GroupByMode::Package,
"section" => GroupByMode::Section,
_ => GroupByMode::Owner,
}
}
#[cfg(test)]
mod tests {
use super::*;
use fallow_types::workspace::WorkspaceDiagnosticKind;
#[test]
fn grouped_check_json_carries_workspace_diagnostics_with_relative_paths() {
let root = Path::new("/project");
let output = serialize_grouped_check_json(GroupedCheckJsonOutputInput {
envelope: CheckEnvelope::DeadCode,
package_baselines: Vec::new(),
gate_outcomes: None,
request_outcomes: None,
finding_id_query: None,
baseline_staleness: None,
groups: &[],
original: &AnalysisResults::default(),
root,
elapsed: Duration::ZERO,
grouped_by: GroupByMode::Directory,
config_fixable: false,
meta: None,
workspace_diagnostics: vec![WorkspaceDiagnostic::new(
root,
root.join("src/unreadable.ts"),
WorkspaceDiagnosticKind::SourceReadFailure {
error: "permission denied".to_string(),
},
)],
next_steps: Vec::new(),
telemetry_analysis_run_id: None,
})
.expect("grouped check JSON serializes");
assert_eq!(
output["workspace_diagnostics"][0]["path"],
"src/unreadable.ts"
);
assert_eq!(
output["workspace_diagnostics"][0]["kind"],
"source-read-failure"
);
assert_eq!(output["kind"], "dead-code-grouped");
assert_eq!(output["schema_version"], CHECK_SCHEMA_VERSION);
}
#[test]
fn architecture_envelope_sets_kind_and_schema_version() {
let root = Path::new("/project");
let results = AnalysisResults::default();
let flat = serialize_check_json(CheckJsonOutputInput {
envelope: CheckEnvelope::Architecture,
results: &results,
root,
elapsed: Duration::ZERO,
config_fixable: false,
meta: None,
extras: CheckJsonExtraOutputs::default(),
workspace_diagnostics: Vec::new(),
next_steps: Vec::new(),
telemetry_analysis_run_id: None,
})
.expect("architecture JSON serializes");
assert_eq!(flat["kind"], "architecture");
assert_eq!(
flat["schema_version"],
fallow_output::ARCHITECTURE_SCHEMA_VERSION
);
let grouped = serialize_grouped_check_json(GroupedCheckJsonOutputInput {
envelope: CheckEnvelope::Architecture,
package_baselines: Vec::new(),
gate_outcomes: None,
request_outcomes: None,
finding_id_query: None,
baseline_staleness: None,
groups: &[],
original: &results,
root,
elapsed: Duration::ZERO,
grouped_by: GroupByMode::Directory,
config_fixable: false,
meta: None,
workspace_diagnostics: Vec::new(),
next_steps: Vec::new(),
telemetry_analysis_run_id: None,
})
.expect("grouped architecture JSON serializes");
assert_eq!(grouped["kind"], "architecture-grouped");
assert_eq!(
grouped["schema_version"],
fallow_output::ARCHITECTURE_SCHEMA_VERSION
);
let payload = serialize_check_json_payload(CheckJsonPayloadInput {
results: &results,
root,
elapsed: Duration::ZERO,
config_fixable: false,
extras: CheckJsonExtraOutputs::default(),
workspace_diagnostics: Vec::new(),
})
.expect("embedded payload serializes");
assert_eq!(payload["schema_version"], CHECK_SCHEMA_VERSION);
assert!(payload.get("kind").is_none());
}
}