Skip to main content

tldr_cli/commands/
inheritance.rs

1//! Inheritance command - Extract and visualize class hierarchies
2//!
3//! Analyzes class inheritance relationships across a codebase:
4//! - Python: class inheritance, ABC, Protocol, metaclasses
5//! - TypeScript: class extends, implements, interfaces
6//! - Go: struct embedding (modeled as inheritance)
7//! - Rust: trait implementations
8//!
9//! # Output Formats
10//!
11//! - JSON: Full structured output (default)
12//! - DOT: Graphviz format for visualization
13//! - text: Human-readable tree format
14//!
15//! # Mitigations Addressed
16//!
17//! - A2: Diamond detection uses BFS + set intersection (O(|ancestors|))
18//! - A12: Python metaclass extraction
19//! - A14: Go struct embedding as Embeds relationships
20//! - A16: Rust trait impl blocks
21//! - A17: --depth requires --class validation
22//! - A19: DOT output properly escapes special characters
23
24use std::path::PathBuf;
25
26use anyhow::Result;
27use clap::Args;
28
29use tldr_core::inheritance::{extract_inheritance, format_dot, format_text, InheritanceOptions};
30use tldr_core::Language;
31
32use crate::output::{OutputFormat, OutputWriter};
33
34/// Extract class inheritance hierarchies
35#[derive(Debug, Args)]
36pub struct InheritanceArgs {
37    /// Path to file or directory to analyze (default: current directory)
38    #[arg(default_value = ".")]
39    pub path: PathBuf,
40
41    /// Programming language (auto-detect if not specified)
42    #[arg(long, short = 'l')]
43    pub lang: Option<Language>,
44
45    /// Focus on specific class (shows ancestors + descendants)
46    #[arg(long, short = 'c')]
47    pub class: Option<String>,
48
49    /// Limit traversal depth (requires --class)
50    #[arg(long, short = 'd')]
51    pub depth: Option<usize>,
52
53    /// Skip ABC/Protocol/mixin/diamond detection
54    #[arg(long)]
55    pub no_patterns: bool,
56
57    /// Skip external base resolution
58    #[arg(long)]
59    pub no_external: bool,
60
61    /// Output format override (backwards compatibility, prefer global --format/-f)
62    #[arg(long = "output", short = 'o', hide = true, value_parser = parse_inheritance_format)]
63    pub output: Option<InheritanceFormat>,
64}
65
66/// Inheritance-specific output formats (includes DOT)
67#[derive(Debug, Clone, Copy, PartialEq, Eq)]
68pub enum InheritanceFormat {
69    Json,
70    Text,
71    Dot,
72}
73
74fn parse_inheritance_format(s: &str) -> Result<InheritanceFormat, String> {
75    match s.to_lowercase().as_str() {
76        "json" => Ok(InheritanceFormat::Json),
77        "text" => Ok(InheritanceFormat::Text),
78        "dot" | "graphviz" => Ok(InheritanceFormat::Dot),
79        _ => Err(format!(
80            "Invalid format '{}'. Expected: json, text, or dot",
81            s
82        )),
83    }
84}
85
86impl InheritanceArgs {
87    /// Run the inheritance command
88    pub fn run(&self, format: OutputFormat, quiet: bool) -> Result<()> {
89        let writer = OutputWriter::new(format, quiet);
90
91        writer.progress(&format!(
92            "Analyzing inheritance in {}...",
93            self.path.display()
94        ));
95
96        // Build options
97        let options = InheritanceOptions {
98            class_filter: self.class.clone(),
99            depth: self.depth,
100            no_patterns: self.no_patterns,
101            no_external: self.no_external,
102            ..Default::default()
103        };
104
105        // Run analysis
106        let report = extract_inheritance(&self.path, self.lang, &options)?;
107
108        // Determine output format
109        let inh_format = self.output.unwrap_or_else(|| {
110            if writer.is_text() {
111                InheritanceFormat::Text
112            } else {
113                InheritanceFormat::Json
114            }
115        });
116
117        // Output based on format
118        match inh_format {
119            InheritanceFormat::Json => {
120                writer.write(&report)?;
121            }
122            InheritanceFormat::Text => {
123                let text = format_text(&report);
124                writer.write_text(&text)?;
125            }
126            InheritanceFormat::Dot => {
127                let dot = format_dot(&report);
128                writer.write_text(&dot)?;
129            }
130        }
131
132        // Summary if not quiet
133        if !quiet {
134            eprintln!(
135                "Found {} classes in {}ms",
136                report.count, report.scan_time_ms
137            );
138
139            if !report.diamonds.is_empty() {
140                eprintln!(
141                    "Warning: {} diamond inheritance pattern(s) detected",
142                    report.diamonds.len()
143                );
144            }
145        }
146
147        Ok(())
148    }
149}
150
151#[cfg(test)]
152mod tests {
153    use super::*;
154
155    #[test]
156    fn test_parse_inheritance_format() {
157        assert_eq!(
158            parse_inheritance_format("json").unwrap(),
159            InheritanceFormat::Json
160        );
161        assert_eq!(
162            parse_inheritance_format("text").unwrap(),
163            InheritanceFormat::Text
164        );
165        assert_eq!(
166            parse_inheritance_format("dot").unwrap(),
167            InheritanceFormat::Dot
168        );
169        assert_eq!(
170            parse_inheritance_format("graphviz").unwrap(),
171            InheritanceFormat::Dot
172        );
173        assert_eq!(
174            parse_inheritance_format("DOT").unwrap(),
175            InheritanceFormat::Dot
176        );
177        assert!(parse_inheritance_format("invalid").is_err());
178    }
179}