1use crate::relations::{FileRelation, RelationAnalyzer, RelationType};
5use anyhow::Result;
6use std::collections::{HashMap, HashSet};
7use std::io::Write;
8use std::path::{Path, PathBuf};
9
10pub struct MermaidRelationFormatter;
12
13impl MermaidRelationFormatter {
14 pub fn format<W: Write>(
15 &self,
16 writer: &mut W,
17 analyzer: &RelationAnalyzer,
18 root_path: &Path,
19 ) -> Result<()> {
20 writeln!(writer, "```mermaid")?;
21 writeln!(writer, "graph TD")?;
22 writeln!(writer, " %% Smart Tree Relationship Map 🌟")?;
23 writeln!(writer, " %% Generated by st --relations --mode mermaid")?;
24 writeln!(writer)?;
25
26 let mut files = HashSet::new();
28 let mut file_ids = HashMap::new();
29 let mut id_counter = 0;
30
31 for rel in analyzer.get_relations() {
32 files.insert(&rel.source);
33 files.insert(&rel.target);
34 }
35
36 writeln!(writer, " %% File nodes")?;
38 for file in files {
39 let relative = file.strip_prefix(root_path).unwrap_or(file);
40 let display_name = relative.to_string_lossy();
41 let id = format!("F{}", id_counter);
42 file_ids.insert(file, id.clone());
43 id_counter += 1;
44
45 let style = if display_name.contains("test") {
47 "fill:#90EE90,stroke:#228B22,stroke-width:2px" } else if display_name.ends_with(".rs") {
49 "fill:#FFE4B5,stroke:#FF8C00,stroke-width:2px" } else if display_name.ends_with(".py") {
51 "fill:#87CEEB,stroke:#4682B4,stroke-width:2px" } else {
53 "fill:#F0F0F0,stroke:#696969,stroke-width:1px"
54 };
55
56 writeln!(
57 writer,
58 " {}[\"{}\"]\n style {} {}",
59 id, display_name, id, style
60 )?;
61 }
62
63 writeln!(writer)?;
64 writeln!(writer, " %% Relationships")?;
65
66 for rel in analyzer.get_relations() {
68 let source_id = &file_ids[&rel.source];
69 let target_id = &file_ids[&rel.target];
70
71 let (arrow, label) = match rel.relation_type {
72 RelationType::Imports => ("-->", "imports"),
73 RelationType::FunctionCall => ("-.->", "calls"),
74 RelationType::TypeUsage => ("-.->", "uses"),
75 RelationType::TestedBy => ("==>", "tested by"),
76 RelationType::Exports => ("<-->", "exports"),
77 RelationType::Coupled => ("<===>", "coupled!"),
78 };
79
80 if rel.items.is_empty() {
81 writeln!(
82 writer,
83 " {} {}|{}| {}",
84 source_id, arrow, label, target_id
85 )?;
86 } else {
87 let items = rel.items.join(", ");
88 writeln!(
89 writer,
90 " {} {}|{}: {}| {}",
91 source_id, arrow, label, items, target_id
92 )?;
93 }
94 }
95
96 writeln!(writer)?;
97 writeln!(writer, " %% Legend")?;
98 writeln!(writer, " subgraph Legend")?;
99 writeln!(writer, " L1[Imports] --> L2[Target]")?;
100 writeln!(writer, " L3[Caller] -.-> L4[Function]")?;
101 writeln!(writer, " L5[Source] ==> L6[Tests]")?;
102 writeln!(writer, " L7[Tightly] <==> L8[Coupled]")?;
103 writeln!(writer, " end")?;
104
105 writeln!(writer, "```")?;
106
107 Ok(())
108 }
109}
110
111pub struct DotRelationFormatter;
113
114impl DotRelationFormatter {
115 pub fn format<W: Write>(
116 &self,
117 writer: &mut W,
118 analyzer: &RelationAnalyzer,
119 root_path: &Path,
120 ) -> Result<()> {
121 writeln!(writer, "digraph CodeRelations {{")?;
122 writeln!(writer, " // Smart Tree Relationship Graph")?;
123 writeln!(writer, " rankdir=LR;")?;
124 writeln!(writer, " node [shape=box, style=filled];")?;
125 writeln!(writer)?;
126
127 let mut files = HashSet::new();
129 for rel in analyzer.get_relations() {
130 files.insert(&rel.source);
131 files.insert(&rel.target);
132 }
133
134 writeln!(writer, " // Nodes")?;
136 for file in &files {
137 let relative = file.strip_prefix(root_path).unwrap_or(file);
138 let display_name = relative.to_string_lossy();
139 let color = if display_name.contains("test") {
140 "lightgreen"
141 } else if display_name.ends_with(".rs") {
142 "lightyellow"
143 } else if display_name.ends_with(".py") {
144 "lightblue"
145 } else {
146 "lightgray"
147 };
148
149 writeln!(
150 writer,
151 " \"{}\" [fillcolor=\"{}\"];",
152 display_name, color
153 )?;
154 }
155
156 writeln!(writer)?;
157 writeln!(writer, " // Edges")?;
158
159 for rel in analyzer.get_relations() {
160 let source = rel.source.strip_prefix(root_path).unwrap_or(&rel.source);
161 let target = rel.target.strip_prefix(root_path).unwrap_or(&rel.target);
162
163 let style = match rel.relation_type {
164 RelationType::Imports => "solid",
165 RelationType::FunctionCall => "dashed",
166 RelationType::TypeUsage => "dotted",
167 RelationType::TestedBy => "bold",
168 RelationType::Exports => "solid",
169 RelationType::Coupled => "bold",
170 };
171
172 let color = match rel.relation_type {
173 RelationType::TestedBy => "green",
174 RelationType::Coupled => "red",
175 _ => "black",
176 };
177
178 writeln!(
179 writer,
180 " \"{}\" -> \"{}\" [style={}, color={}, label=\"{:?}\"];",
181 source.to_string_lossy(),
182 target.to_string_lossy(),
183 style,
184 color,
185 rel.relation_type
186 )?;
187 }
188
189 writeln!(writer, "}}")?;
190
191 Ok(())
192 }
193}
194
195pub struct CompressedRelationFormatter;
197
198impl CompressedRelationFormatter {
199 pub fn format<W: Write>(
200 &self,
201 writer: &mut W,
202 analyzer: &RelationAnalyzer,
203 root_path: &Path,
204 ) -> Result<()> {
205 writeln!(writer, "RELATIONS_V1:")?;
206
207 let mut files = HashSet::new();
209 for rel in analyzer.get_relations() {
210 files.insert(&rel.source);
211 files.insert(&rel.target);
212 }
213
214 let mut file_index: HashMap<&PathBuf, usize> = HashMap::new();
215 writeln!(writer, "FILES:")?;
216 for (idx, file) in files.iter().enumerate() {
217 let relative = file.strip_prefix(root_path).unwrap_or(file);
218 writeln!(writer, "{:x}:{}", idx, relative.to_string_lossy())?;
219 file_index.insert(file, idx);
220 }
221
222 writeln!(writer, "RELS:")?;
223 for rel in analyzer.get_relations() {
225 let source_idx = file_index[&rel.source];
226 let target_idx = file_index[&rel.target];
227 let type_code = match rel.relation_type {
228 RelationType::Imports => 'I',
229 RelationType::FunctionCall => 'F',
230 RelationType::TypeUsage => 'T',
231 RelationType::TestedBy => 'X',
232 RelationType::Exports => 'E',
233 RelationType::Coupled => 'C',
234 };
235
236 if rel.items.is_empty() {
237 writeln!(
238 writer,
239 "{:x},{:x},{},{}",
240 source_idx, target_idx, type_code, rel.strength
241 )?;
242 } else {
243 writeln!(
244 writer,
245 "{:x},{:x},{},{}:{}",
246 source_idx,
247 target_idx,
248 type_code,
249 rel.strength,
250 rel.items.join(",")
251 )?;
252 }
253 }
254
255 writeln!(writer, "END_RELATIONS")?;
256
257 Ok(())
258 }
259}
260
261pub struct TextRelationFormatter;
263
264impl TextRelationFormatter {
265 pub fn format<W: Write>(
266 &self,
267 writer: &mut W,
268 analyzer: &RelationAnalyzer,
269 root_path: &Path,
270 ) -> Result<()> {
271 writeln!(writer, "🔗 Code Relationship Analysis")?;
272 writeln!(writer, "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━")?;
273 writeln!(writer)?;
274
275 let mut file_relations: HashMap<&PathBuf, Vec<&FileRelation>> = HashMap::new();
277 for rel in analyzer.get_relations() {
278 file_relations.entry(&rel.source).or_default().push(rel);
279 }
280
281 let total_files = file_relations.len();
283
284 for (file, relations) in file_relations {
285 let relative = file.strip_prefix(root_path).unwrap_or(file);
286 writeln!(writer, "📄 {}", relative.to_string_lossy())?;
287
288 for rel_type in &[
290 RelationType::Imports,
291 RelationType::FunctionCall,
292 RelationType::TypeUsage,
293 RelationType::TestedBy,
294 RelationType::Exports,
295 RelationType::Coupled,
296 ] {
297 let typed_rels: Vec<_> = relations
298 .iter()
299 .filter(|r| &r.relation_type == rel_type)
300 .collect();
301
302 if !typed_rels.is_empty() {
303 let emoji = match rel_type {
304 RelationType::Imports => "├─→ imports:",
305 RelationType::FunctionCall => "├─→ calls:",
306 RelationType::TypeUsage => "├─→ uses types from:",
307 RelationType::TestedBy => "├─→ tested by:",
308 RelationType::Exports => "├─→ exports to:",
309 RelationType::Coupled => "├─⚠️ tightly coupled with:",
310 };
311
312 writeln!(writer, " {}", emoji)?;
313 for rel in typed_rels {
314 let target = rel.target.strip_prefix(root_path).unwrap_or(&rel.target);
315 if rel.items.is_empty() {
316 writeln!(writer, " • {}", target.to_string_lossy())?;
317 } else {
318 writeln!(
319 writer,
320 " • {} ({})",
321 target.to_string_lossy(),
322 rel.items.join(", ")
323 )?;
324 }
325 }
326 }
327 }
328 writeln!(writer)?;
329 }
330
331 writeln!(writer, "📊 Summary")?;
333 writeln!(writer, "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━")?;
334
335 let total_relations = analyzer.get_relations().len();
336 let coupled_count = analyzer
337 .get_relations()
338 .iter()
339 .filter(|r| r.relation_type == RelationType::Coupled)
340 .count();
341 let tested_count = analyzer
342 .get_relations()
343 .iter()
344 .filter(|r| r.relation_type == RelationType::TestedBy)
345 .count();
346
347 writeln!(writer, "Total files analyzed: {}", total_files)?;
348 writeln!(writer, "Total relationships: {}", total_relations)?;
349 writeln!(writer, "Tightly coupled pairs: {}", coupled_count)?;
350 writeln!(writer, "Files with tests: {}", tested_count)?;
351
352 Ok(())
353 }
354}