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        // surface-gaps-v1 (BUG-19): honor the global `--format dot` flag in
110        // addition to the legacy `-o dot` switch. Inheritance graphs are the
111        // canonical class-hierarchy DOT use case.
112        let inh_format = self.output.unwrap_or_else(|| {
113            if writer.is_text() {
114                InheritanceFormat::Text
115            } else if writer.is_dot() {
116                InheritanceFormat::Dot
117            } else {
118                InheritanceFormat::Json
119            }
120        });
121
122        // Output based on format
123        match inh_format {
124            InheritanceFormat::Json => {
125                writer.write(&report)?;
126            }
127            InheritanceFormat::Text => {
128                let text = format_text(&report);
129                writer.write_text(&text)?;
130            }
131            InheritanceFormat::Dot => {
132                let dot = format_dot(&report);
133                writer.write_text(&dot)?;
134            }
135        }
136
137        // determinism-and-stderr-hygiene-v1 (BUG-18): the summary
138        // ("Found N classes in Mms") and diamond-inheritance warning
139        // were unconditionally written to stderr, which contaminated
140        // the JSON-mode contract — `tldr inheritance <path> 2>/dev/null
141        // > out.json` produced a clean JSON file but a non-empty stderr
142        // stream, breaking shell pipelines that gate on stderr-empty.
143        // Gate on text format: text consumers still see the summary
144        // (now via a writer-aware path), JSON consumers get a clean
145        // stream. The information is already in the JSON
146        // (`report.count`, `report.scan_time_ms`, `report.diamonds`),
147        // so no data loss for downstream tooling.
148        if !quiet && writer.is_text() {
149            eprintln!(
150                "Found {} classes in {}ms",
151                report.count, report.scan_time_ms
152            );
153
154            if !report.diamonds.is_empty() {
155                eprintln!(
156                    "Warning: {} diamond inheritance pattern(s) detected",
157                    report.diamonds.len()
158                );
159            }
160        }
161
162        Ok(())
163    }
164}
165
166#[cfg(test)]
167mod tests {
168    use super::*;
169
170    #[test]
171    fn test_parse_inheritance_format() {
172        assert_eq!(
173            parse_inheritance_format("json").unwrap(),
174            InheritanceFormat::Json
175        );
176        assert_eq!(
177            parse_inheritance_format("text").unwrap(),
178            InheritanceFormat::Text
179        );
180        assert_eq!(
181            parse_inheritance_format("dot").unwrap(),
182            InheritanceFormat::Dot
183        );
184        assert_eq!(
185            parse_inheritance_format("graphviz").unwrap(),
186            InheritanceFormat::Dot
187        );
188        assert_eq!(
189            parse_inheritance_format("DOT").unwrap(),
190            InheritanceFormat::Dot
191        );
192        assert!(parse_inheritance_format("invalid").is_err());
193    }
194}