Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 25 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ A Rust CLI tool and library for finding all subclasses (direct and transitive) o
- **Re-export support**: Tracks classes defined in one module but exported from another
- **Ambiguity detection**: Detects when a class name appears in multiple modules and provides clear guidance
- **Gitignore support**: Automatically respects `.gitignore` files using the `ignore` crate
- **Multiple output formats**: Text and JSON output formats
- **Multiple output formats**: Text, JSON, and Graphviz dot formats for visualization
- **Fast and efficient**: Written in Rust with parallel file traversal
- **Smart caching**: Caches parsed files with gzip compression for 2.5x speedup on repeated runs
- **Configurable logging**: Uses `env_logger` for flexible logging control
Expand Down Expand Up @@ -78,6 +78,28 @@ Get results in JSON format for scripting:
pysubclasses Animal --format json
```

### Graphviz Dot Format

Generate a graph visualization in Graphviz dot format:

```bash
# Output dot format
pysubclasses Animal --format dot

# Pipe to dot to generate an image
pysubclasses Animal --format dot | dot -Tpng > graph.png
pysubclasses Animal --format dot | dot -Tsvg > graph.svg

# Works with both modes
pysubclasses Animal --format dot --mode direct # Shows only direct relationships
pysubclasses Animal --format dot --mode all # Shows full inheritance tree
```

The dot format includes:
- The base class (highlighted in green)
- All subclasses (in blue)
- Arrows showing inheritance relationships

### Search Mode

Control whether to find only direct subclasses or all transitive subclasses:
Expand Down Expand Up @@ -231,12 +253,12 @@ $ pysubclasses Animal --format json
You can also use `pysubclasses` as a library in your Rust projects:

```rust
use pysubclasses::{SubclassFinder, SubclassMode};
use pysubclasses::{SubclassFinder, SearchMode};
use std::path::PathBuf;

fn main() -> Result<(), Box<dyn std::error::Error>> {
let finder = SubclassFinder::new(PathBuf::from("./src"))?;
let subclasses = finder.find_subclasses("BaseClass", None, SubclassMode::All)?;
let subclasses = finder.find_subclasses("BaseClass", None, SearchMode::All)?;

for class_ref in subclasses {
println!("{} ({})", class_ref.class_name, class_ref.module_path);
Expand Down
110 changes: 104 additions & 6 deletions src/graph.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,15 @@ use crate::registry::{ClassId, Registry};

/// An inheritance graph representing parent-child relationships between classes.
///
/// This structure maps each class to the set of classes that directly inherit from it.
/// The graph can be used to efficiently find all descendants of a given class using
/// breadth-first search.
/// This structure maps each class to the set of classes that directly inherit from it,
/// and also maintains the reverse mapping for efficient parent lookup.
/// The graph can be used to efficiently find all descendants or ancestors of a given
/// class using breadth-first search.
pub struct InheritanceGraph {
/// Maps parent classes to their direct children.
pub children: HashMap<ClassId, HashSet<ClassId>>,
/// Maps child classes to their direct parents.
parents: HashMap<ClassId, HashSet<ClassId>>,
}

impl InheritanceGraph {
Expand All @@ -41,22 +44,29 @@ impl InheritanceGraph {
/// 3. Skip any base classes that cannot be resolved (e.g., external dependencies)
pub fn build(registry: &Registry) -> Self {
let mut children: HashMap<ClassId, HashSet<ClassId>> = HashMap::new();
let mut parents: HashMap<ClassId, HashSet<ClassId>> = HashMap::new();

// Build parent → children edges by examining each class's bases
// Build parent → children and child → parents edges by examining each class's bases
for (child_id, metadata) in &registry.classes {
for base_name in &metadata.bases {
// Resolve the base class reference in this class's module context
if let Some(parent_id) = registry.resolve_class(&child_id.module, base_name) {
// Add this class as a child of its parent
children
.entry(parent_id)
.entry(parent_id.clone())
.or_default()
.insert(child_id.clone());

// Add the parent as a parent of this class
parents
.entry(child_id.clone())
.or_default()
.insert(parent_id);
}
}
}

Self { children }
Self { children, parents }
}

/// Finds only the direct subclasses of a given class.
Expand Down Expand Up @@ -149,4 +159,92 @@ impl InheritanceGraph {

result
}

/// Finds only the direct parent classes of a given class.
///
/// Returns classes that the specified class directly inherits from,
/// without traversing further up the inheritance hierarchy.
///
/// # Arguments
///
/// * `root` - The class to find direct parent classes for
///
/// # Returns
///
/// A vector containing only the direct parent classes.
///
/// # Examples
///
/// ```text
/// Given:
/// class Animal: pass
/// class Mammal(Animal): pass
/// class Dog(Mammal): pass
///
/// find_direct_parent_classes(Dog) → [Mammal]
/// find_direct_parent_classes(Mammal) → [Animal]
/// ```
pub fn find_direct_parent_classes(&self, root: &ClassId) -> Vec<ClassId> {
self.parents
.get(root)
.map(|parents| parents.iter().cloned().collect())
.unwrap_or_default()
}

/// Finds all transitive parent classes of a given class.
///
/// Performs a breadth-first search to discover all classes that the specified
/// class directly or indirectly inherits from. This includes:
/// - Direct parents (one level of inheritance)
/// - Grandparents (two levels)
/// - Great-grandparents, etc. (any depth)
///
/// # Arguments
///
/// * `root` - The class to find parent classes for
///
/// # Returns
///
/// A vector containing all transitive parent classes. The root class itself is not
/// included in the result. The order of classes in the vector is determined by
/// the BFS traversal order.
///
/// # Examples
///
/// ```text
/// Given:
/// class Animal: pass
/// class Mammal(Animal): pass
/// class Dog(Mammal): pass
///
/// find_all_parent_classes(Dog) → [Mammal, Animal]
/// ```
pub fn find_all_parent_classes(&self, root: &ClassId) -> Vec<ClassId> {
use std::collections::VecDeque;

let mut result = Vec::new();
let mut visited = HashSet::new();
let mut queue = VecDeque::new();

// Initialize BFS with the root class
queue.push_back(root.clone());
visited.insert((root.module.clone(), root.name.clone()));

// BFS traversal
while let Some(current) = queue.pop_front() {
// Examine all direct parents of the current class
if let Some(parents) = self.parents.get(&current) {
for parent in parents {
let key = (parent.module.clone(), parent.name.clone());
if !visited.contains(&key) {
visited.insert(key);
result.push(parent.clone());
queue.push_back(parent.clone());
}
}
}
}

result
}
}
Loading