use std::collections::HashMap;
use serde::{Deserialize, Serialize};
use super::barrels::BarrelAnalysis;
use super::crowd::types::Crowd;
use super::dead_parrots::DeadExport;
use super::dist::DistResult;
use crate::refactor_plan::{Move, PlanStats, RefactorPhase, RefactorPlan, Shim};
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub enum Confidence {
Certain,
High,
Smell,
}
impl Confidence {
pub fn indicator(&self) -> &'static str {
match self {
Confidence::Certain => "[!!]",
Confidence::High => "[!]",
Confidence::Smell => "[?]",
}
}
}
impl std::fmt::Display for Confidence {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Confidence::Certain => write!(f, "CERTAIN"),
Confidence::High => write!(f, "HIGH"),
Confidence::Smell => write!(f, "SMELL"),
}
}
}
#[derive(Clone, Debug, Serialize)]
pub struct StringLiteralMatch {
pub file: String,
pub line: usize,
pub context: String, }
#[derive(Clone, Serialize)]
pub struct CommandGap {
pub name: String,
pub implementation_name: Option<String>,
pub locations: Vec<(String, usize)>,
#[serde(skip_serializing_if = "Option::is_none")]
pub confidence: Option<Confidence>,
#[serde(skip_serializing_if = "Vec::is_empty", default)]
pub string_literal_matches: Vec<StringLiteralMatch>,
}
#[derive(Clone, Serialize)]
pub struct AiInsight {
pub title: String,
pub severity: String,
pub message: String,
}
pub use report_leptos::types::{GraphComponent, GraphData, GraphNode};
#[derive(Clone, Serialize)]
pub struct DupLocation {
pub file: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub line: Option<usize>,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum DupSeverity {
CrossLangExpected = 0,
ReExportOrGeneric = 1,
#[default]
SamePackage = 2,
CrossModule = 3,
CrossCrate = 4,
}
#[derive(Clone, Serialize)]
pub struct RankedDup {
pub name: String,
pub files: Vec<String>,
#[serde(skip_serializing_if = "Vec::is_empty", default)]
pub locations: Vec<DupLocation>,
pub score: usize,
pub prod_count: usize,
pub dev_count: usize,
pub canonical: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub canonical_line: Option<usize>,
pub refactors: Vec<String>,
#[serde(default)]
pub severity: DupSeverity,
#[serde(default)]
pub is_cross_lang: bool,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub packages: Vec<String>,
#[serde(default, skip_serializing_if = "String::is_empty")]
pub reason: String,
}
#[derive(Clone, Serialize)]
pub struct CommandBridge {
pub name: String,
pub fe_locations: Vec<(String, usize)>,
pub be_location: Option<(String, usize, String)>,
pub status: String,
pub language: String,
#[serde(default)]
pub comm_type: String,
#[serde(default)]
pub emits_events: Vec<String>,
}
#[derive(Clone, Serialize)]
pub struct PriorityTask {
pub priority: u8,
pub kind: String,
pub target: String,
pub location: String,
pub why: String,
pub risk: String,
pub fix_hint: String,
pub verify_cmd: String,
}
#[derive(Clone, Serialize)]
pub struct HubFile {
pub path: String,
pub loc: usize,
pub imports_count: usize,
pub exports_count: usize,
pub importers_count: usize,
pub commands_count: usize,
pub slice_cmd: String,
}
#[derive(Clone, Default, Serialize)]
pub struct TreeNode {
pub path: String,
pub loc: usize,
#[serde(default)]
pub children: Vec<TreeNode>,
}
#[derive(Serialize)]
pub struct ReportSection {
pub root: String,
pub files_analyzed: usize,
pub total_loc: usize,
pub reexport_files_count: usize,
pub dynamic_imports_count: usize,
pub ranked_dups: Vec<RankedDup>,
pub cascades: Vec<(String, String)>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub circular_imports: Vec<Vec<String>>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub lazy_circular_imports: Vec<Vec<String>>,
pub dynamic: Vec<(String, Vec<String>)>,
pub analyze_limit: usize,
#[serde(skip_serializing_if = "Option::is_none")]
pub generated_at: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub schema_name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub schema_version: Option<String>,
pub missing_handlers: Vec<CommandGap>,
pub unregistered_handlers: Vec<CommandGap>,
pub unused_handlers: Vec<CommandGap>,
pub command_counts: (usize, usize),
pub command_bridges: Vec<CommandBridge>,
pub open_base: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub tree: Option<Vec<TreeNode>>,
pub graph: Option<GraphData>,
pub graph_warning: Option<String>,
pub insights: Vec<AiInsight>,
pub git_branch: Option<String>,
pub git_commit: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub priority_tasks: Vec<PriorityTask>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub hub_files: Vec<HubFile>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub crowds: Vec<Crowd>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub dead_exports: Vec<DeadExport>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub dist: Option<DistResult>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub twins_data: Option<TwinsData>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub coverage_gaps: Vec<super::coverage_gaps::CoverageGap>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub health_score: Option<u8>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub refactor_plan: Option<RefactorPlanForReport>,
}
#[derive(Clone, Serialize)]
pub struct TwinsData {
pub dead_parrots: Vec<super::twins::SymbolEntry>,
pub exact_twins: Vec<super::twins::ExactTwin>,
pub barrel_chaos: BarrelAnalysis,
}
#[derive(Clone, Default, Serialize)]
pub struct RefactorMoveForReport {
pub source: String,
pub target: String,
pub current_layer: String,
pub target_layer: String,
pub risk: String,
pub loc: usize,
pub direct_consumers: usize,
pub reason: String,
pub verify_cmd: String,
}
#[derive(Clone, Default, Serialize)]
pub struct RefactorShimForReport {
pub old_path: String,
pub new_path: String,
pub importer_count: usize,
pub code: String,
}
#[derive(Clone, Default, Serialize)]
pub struct RefactorPhaseForReport {
pub name: String,
pub risk: String,
pub moves: Vec<RefactorMoveForReport>,
pub git_script: String,
}
#[derive(Clone, Default, Serialize)]
pub struct RefactorStatsForReport {
pub total_files: usize,
pub files_to_move: usize,
pub shims_needed: usize,
pub layer_before: HashMap<String, usize>,
pub layer_after: HashMap<String, usize>,
pub by_risk: HashMap<String, usize>,
}
#[derive(Clone, Default, Serialize)]
pub struct RefactorPlanForReport {
pub target: String,
pub phases: Vec<RefactorPhaseForReport>,
pub shims: Vec<RefactorShimForReport>,
pub cyclic_groups: Vec<Vec<String>>,
pub stats: RefactorStatsForReport,
}
impl From<&RefactorPlan> for RefactorPlanForReport {
fn from(plan: &RefactorPlan) -> Self {
Self {
target: plan.target.clone(),
phases: plan
.phases
.iter()
.map(RefactorPhaseForReport::from)
.collect(),
shims: plan.shims.iter().map(RefactorShimForReport::from).collect(),
cyclic_groups: plan.cyclic_groups.clone(),
stats: RefactorStatsForReport::from(&plan.stats),
}
}
}
impl From<&RefactorPhase> for RefactorPhaseForReport {
fn from(phase: &RefactorPhase) -> Self {
Self {
name: phase.name.clone(),
risk: phase.risk.label().to_lowercase(),
moves: phase
.moves
.iter()
.map(RefactorMoveForReport::from)
.collect(),
git_script: phase.git_script.clone(),
}
}
}
impl From<&Move> for RefactorMoveForReport {
fn from(mv: &Move) -> Self {
Self {
source: mv.source.clone(),
target: mv.target.clone(),
current_layer: mv.current_layer.display_name().to_string(),
target_layer: mv.target_layer.display_name().to_string(),
risk: mv.risk.label().to_lowercase(),
loc: mv.loc,
direct_consumers: mv.direct_consumers,
reason: mv.reason.clone(),
verify_cmd: mv.verify_cmd.clone(),
}
}
}
impl From<&Shim> for RefactorShimForReport {
fn from(shim: &Shim) -> Self {
Self {
old_path: shim.old_path.clone(),
new_path: shim.new_path.clone(),
importer_count: shim.importer_count,
code: shim.code.clone(),
}
}
}
impl From<&PlanStats> for RefactorStatsForReport {
fn from(stats: &PlanStats) -> Self {
Self {
total_files: stats.total_files,
files_to_move: stats.files_to_move,
shims_needed: stats.shims_needed,
layer_before: stats.layer_before.clone(),
layer_after: stats.layer_after.clone(),
by_risk: stats.by_risk.clone(),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::snapshot::CommandBridge;
#[test]
fn confidence_display_certain() {
assert_eq!(format!("{}", Confidence::Certain), "CERTAIN");
}
#[test]
fn confidence_display_high() {
assert_eq!(format!("{}", Confidence::High), "HIGH");
}
#[test]
fn confidence_display_smell() {
assert_eq!(format!("{}", Confidence::Smell), "SMELL");
}
#[test]
fn confidence_equality() {
assert_eq!(Confidence::Certain, Confidence::Certain);
assert_eq!(Confidence::High, Confidence::High);
assert_eq!(Confidence::Smell, Confidence::Smell);
assert_ne!(Confidence::High, Confidence::Smell);
}
#[test]
fn confidence_indicator() {
assert_eq!(Confidence::Certain.indicator(), "[!!]");
assert_eq!(Confidence::High.indicator(), "[!]");
assert_eq!(Confidence::Smell.indicator(), "[?]");
}
#[test]
fn string_literal_match_creation() {
let m = StringLiteralMatch {
file: "test.ts".to_string(),
line: 42,
context: "allowlist".to_string(),
};
assert_eq!(m.file, "test.ts");
assert_eq!(m.line, 42);
assert_eq!(m.context, "allowlist");
}
#[test]
fn command_gap_creation() {
let gap = CommandGap {
name: "test_cmd".to_string(),
implementation_name: Some("testCmd".to_string()),
locations: vec![("test.ts".to_string(), 10)],
confidence: Some(Confidence::High),
string_literal_matches: vec![],
};
assert_eq!(gap.name, "test_cmd");
assert_eq!(gap.implementation_name, Some("testCmd".to_string()));
assert_eq!(gap.locations.len(), 1);
assert_eq!(gap.confidence, Some(Confidence::High));
}
#[test]
fn ai_insight_creation() {
let insight = AiInsight {
title: "Test Insight".to_string(),
severity: "warning".to_string(),
message: "Some message".to_string(),
};
assert_eq!(insight.title, "Test Insight");
assert_eq!(insight.severity, "warning");
}
#[test]
fn graph_node_creation() {
let node = GraphNode {
id: "src/main.ts".to_string(),
label: "main.ts".to_string(),
loc: 100,
x: 0.5,
y: 0.5,
component: 0,
degree: 3,
detached: false,
};
assert_eq!(node.id, "src/main.ts");
assert_eq!(node.loc, 100);
assert!(!node.detached);
}
#[test]
fn command_bridge_creation() {
let bridge = CommandBridge {
name: "get_user".to_string(),
frontend_calls: vec![("src/app.ts".to_string(), 10)],
backend_handler: Some(("src-tauri/src/lib.rs".to_string(), 20)),
has_handler: true,
is_called: true,
};
assert_eq!(bridge.name, "get_user");
assert!(bridge.has_handler);
assert!(bridge.backend_handler.is_some());
}
}