├── src ├── visualization │ ├── mod.rs │ ├── web_server.rs │ └── graph_renderer.rs ├── analyzer │ ├── circular_detector.rs │ ├── dependency_graph.rs │ ├── mod.rs │ ├── refactor_suggester.rs │ └── errors.rs ├── utils │ ├── mod.rs │ └── file_walker.rs ├── parsers │ ├── mod.rs │ ├── rust.rs │ ├── java.rs │ ├── python.rs │ ├── javascript.rs │ └── parser_registry.rs ├── graph │ └── mod.rs └── main.rs ├── test_sources ├── app.py ├── main.rs ├── index.js └── HelloWorld.java ├── .gitignore ├── Cargo.toml ├── LICENSE └── README.md /src/visualization/mod.rs: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/analyzer/circular_detector.rs: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/analyzer/dependency_graph.rs: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/visualization/web_server.rs: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/analyzer/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod errors; -------------------------------------------------------------------------------- /src/analyzer/refactor_suggester.rs: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/visualization/graph_renderer.rs: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/utils/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod file_walker; 2 | -------------------------------------------------------------------------------- /test_sources/app.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | -------------------------------------------------------------------------------- /test_sources/main.rs: -------------------------------------------------------------------------------- 1 | use std::fs; 2 | use crate::utils::file_walker; 3 | -------------------------------------------------------------------------------- /test_sources/index.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import path from 'path'; 3 | -------------------------------------------------------------------------------- /test_sources/HelloWorld.java: -------------------------------------------------------------------------------- 1 | import java.util.List; 2 | import java.io.File; 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | **/*.rs.bk 3 | **/*.swp 4 | **/*.swo 5 | Cargo.lock 6 | .DS_Store 7 | *.log 8 | *.tmp 9 | .idea/ 10 | .vscode/ 11 | output.* 12 | node_modules/ 13 | dist/ 14 | build/ 15 | .env 16 | -------------------------------------------------------------------------------- /src/analyzer/errors.rs: -------------------------------------------------------------------------------- 1 | 2 | 3 | #[derive(Debug)] 4 | 5 | pub enum ParseError { 6 | InvalidSyntax(String), 7 | IoError(std::io::Error), 8 | } 9 | 10 | impl From for ParseError { 11 | fn from(err: std::io::Error) -> Self { 12 | ParseError::IoError(err) 13 | } 14 | } -------------------------------------------------------------------------------- /src/parsers/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod rust; 2 | pub mod python; 3 | pub mod javascript; 4 | pub mod java; 5 | pub mod parser_registry; 6 | use crate::analyzer::errors::ParseError; 7 | 8 | pub trait LanguageParser { 9 | fn parse_file(&self, content: &str) -> Result, ParseError>; 10 | fn file_extension(&self) -> &[&str]; 11 | fn language_name(&self) -> &str; 12 | } -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "nexus" 3 | version = "0.1.0" 4 | edition = "2024" 5 | 6 | [dependencies] 7 | clap = { version = "4.0", features = ["derive"] } 8 | serde = { version = "1.0", features = ["derive"] } 9 | serde_json = "1.0" 10 | petgraph = "0.6" 11 | walkdir = "2.0" 12 | regex = "1.0" 13 | tokio = { version = "1.0", features = ["full"] } 14 | axum = "0.7" 15 | tower = "0.4" 16 | -------------------------------------------------------------------------------- /src/utils/file_walker.rs: -------------------------------------------------------------------------------- 1 | use std::path::{Path, PathBuf}; 2 | use walkdir::WalkDir; 3 | 4 | pub fn walk_source_dir(path: &str) -> Result, std::io::Error> { 5 | let mut files = Vec::new(); 6 | for entry in WalkDir::new(path).into_iter().filter_map(Result::ok) { 7 | let path = entry.path(); 8 | if path.is_file() { 9 | if let Some(ext) = path.extension() { 10 | if ext == "rs" || ext == "js" || ext == "py" || ext == "java" { 11 | files.push(path.to_path_buf()); 12 | } 13 | } 14 | } 15 | } 16 | Ok(files) 17 | } 18 | -------------------------------------------------------------------------------- /src/parsers/rust.rs: -------------------------------------------------------------------------------- 1 | use regex::Regex; 2 | use crate::parsers::LanguageParser; 3 | use crate::analyzer::errors::ParseError; 4 | 5 | pub struct RustParser; 6 | 7 | impl LanguageParser for RustParser { 8 | fn parse_file(&self, content: &str) -> Result, ParseError> { 9 | let mut deps = Vec::new(); 10 | let re = Regex::new(r"^\s*use\s+([a-zA-Z0-9_:]+)").unwrap(); 11 | 12 | for line in content.lines() { 13 | if let Some(cap) = re.captures(line) { 14 | if let Some(dep) = cap.get(1) { 15 | deps.push(dep.as_str().to_string()); 16 | } 17 | } 18 | } 19 | Ok(deps) 20 | } 21 | 22 | fn file_extension(&self) -> &[&str] { 23 | &["rs"] 24 | } 25 | fn language_name(&self) -> &str { 26 | "Rust" 27 | } 28 | 29 | } -------------------------------------------------------------------------------- /src/parsers/java.rs: -------------------------------------------------------------------------------- 1 | use regex::Regex; 2 | use crate::parsers::LanguageParser; 3 | use crate::analyzer::errors::ParseError; 4 | 5 | pub struct JavaParser; 6 | 7 | impl LanguageParser for JavaParser { 8 | fn parse_file(&self, content: &str) -> Result, ParseError> { 9 | let mut deps = Vec::new(); 10 | 11 | let import_re = Regex::new(r#"^\s*import\s+([a-zA-Z0-9_\.]+)"#) 12 | .map_err(|e| ParseError::InvalidSyntax(e.to_string()))?; 13 | 14 | for line in content.lines() { 15 | if let Some(cap) = import_re.captures(line) { 16 | deps.push(cap[1].to_string()); 17 | } 18 | } 19 | 20 | Ok(deps) 21 | } 22 | 23 | fn file_extension(&self) -> &[&str] { 24 | &["java"] 25 | } 26 | 27 | fn language_name(&self) -> &str { 28 | "Java" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Mitali 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/parsers/python.rs: -------------------------------------------------------------------------------- 1 | use regex::Regex; 2 | use crate::parsers::LanguageParser; 3 | use crate::analyzer::errors::ParseError; 4 | 5 | pub struct PythonParser; 6 | 7 | impl LanguageParser for PythonParser { 8 | fn parse_file(&self, content: &str) -> Result, ParseError> { 9 | let mut deps = Vec::new(); 10 | 11 | let import_re = Regex::new(r#"^\s*import\s+([a-zA-Z0-9_\.]+)"#) 12 | .map_err(|e| ParseError::InvalidSyntax(e.to_string()))?; 13 | let from_import_re = Regex::new(r#"^\s*from\s+([a-zA-Z0-9_\.]+)\s+import"#) 14 | .map_err(|e| ParseError::InvalidSyntax(e.to_string()))?; 15 | 16 | for line in content.lines() { 17 | if let Some(cap) = import_re.captures(line) { 18 | deps.push(cap[1].to_string()); 19 | } 20 | if let Some(cap) = from_import_re.captures(line) { 21 | deps.push(cap[1].to_string()); 22 | } 23 | } 24 | 25 | Ok(deps) 26 | } 27 | 28 | fn file_extension(&self) -> &[&str] { 29 | &["py"] 30 | } 31 | 32 | fn language_name(&self) -> &str { 33 | "Python" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/parsers/javascript.rs: -------------------------------------------------------------------------------- 1 | use regex::Regex; 2 | use crate::parsers::LanguageParser; 3 | use crate::analyzer::errors::ParseError; 4 | 5 | pub struct JavaScriptParser; 6 | 7 | impl LanguageParser for JavaScriptParser { 8 | fn parse_file(&self, content: &str) -> Result, ParseError> { 9 | let mut deps = Vec::new(); 10 | 11 | let import_re = Regex::new(r#"^\s*import\s+.*from\s+['"]([^'"]+)['"]"#) 12 | .map_err(|e| ParseError::InvalidSyntax(e.to_string()))?; 13 | let require_re = Regex::new(r#"require\(['"]([^'"]+)['"]\)"#) 14 | .map_err(|e| ParseError::InvalidSyntax(e.to_string()))?; 15 | 16 | for line in content.lines() { 17 | if let Some(cap) = import_re.captures(line) { 18 | deps.push(cap[1].to_string()); 19 | } 20 | if let Some(cap) = require_re.captures(line) { 21 | deps.push(cap[1].to_string()); 22 | } 23 | } 24 | 25 | Ok(deps) 26 | } 27 | 28 | fn file_extension(&self) -> &[&str] { 29 | &["js", "ts"] 30 | } 31 | 32 | fn language_name(&self) -> &str { 33 | "JavaScript/TypeScript" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/parsers/parser_registry.rs: -------------------------------------------------------------------------------- 1 | use crate::parsers::{ 2 | rust::RustParser, 3 | python::PythonParser, 4 | javascript::JavaScriptParser, 5 | java::JavaParser, 6 | LanguageParser, 7 | }; 8 | use std::fs; 9 | 10 | /// Returns all available language parsers. 11 | 12 | pub fn get_parsers() -> Vec> { 13 | vec![ 14 | Box::new(RustParser), 15 | Box::new(PythonParser), 16 | Box::new(JavaScriptParser), 17 | Box::new(JavaParser), 18 | ] 19 | } 20 | 21 | /// Match file extensions to language names. 22 | fn get_language_by_extension(path: &str) -> Option<&'static str> { 23 | match std::path::Path::new(path) 24 | .extension() 25 | .and_then(|ext| ext.to_str()) 26 | { 27 | Some("rs") => Some("Rust"), 28 | Some("py") => Some("Python"), 29 | Some("js") | Some("ts") => Some("JavaScript/TypeScript"), 30 | Some("java") => Some("Java"), 31 | _ => None, 32 | } 33 | } 34 | 35 | /// Try to find a matching parser for a given file. 36 | /// If a parser matches, it parses the file and returns dependencies. 37 | pub fn parse_file_with_matching_parser( 38 | path: &std::path::Path, 39 | parsers: &[Box], 40 | ) -> Result<(String, Vec), String> { 41 | let file_content = fs::read_to_string(path) 42 | .map_err(|e| format!("Failed to read file {}: {}", path.display(), e))?; 43 | 44 | if let Some(lang) = get_language_by_extension(path.to_str().unwrap_or_default()) { 45 | for parser in parsers { 46 | if parser.language_name() == lang { 47 | let deps = parser 48 | .parse_file(&file_content) 49 | .map_err(|e| format!("Parse error in {}: {:?}", path.display(), e))?; 50 | return Ok((path.display().to_string(), deps)); 51 | } 52 | } 53 | } 54 | 55 | Err(format!("No parser found for {}", path.display())) 56 | } 57 | 58 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Nexus 2 | 3 | **Nexus** is a Rust-based CLI tool for analyzing and visualizing code dependencies in source files. It builds a dependency graph from Rust code and supports circular dependency detection. 4 | 5 | ## Features 6 | 7 | - Analyze Rust project structure by parsing `mod` and `use` declarations 8 | - Generate dependency graph in DOT format 9 | - Detect circular dependencies 10 | - Traverse entire source directory 11 | - Modular parser system (currently supports Rust, Javascript, Python and Java) 12 | 13 | ## Project Structure 14 | 15 | ``` 16 | nexus/ 17 | ├── src/ 18 | │ ├── main.rs # CLI entrypoint 19 | │ ├── graph/ # Graph construction and cycle detection 20 | │ ├── parsers/ # Rust parser module 21 | │ ├── utils/ # File system walking logic 22 | │ └── analyzer/ # Central analyzer logic 23 | ``` 24 | 25 | ## Usage 26 | 27 | ### Build 28 | 29 | ```bash 30 | cargo build --release 31 | ```` 32 | 33 | ### Run 34 | 35 | ```bash 36 | cargo run -- --path ./src 37 | ``` 38 | 39 | #### Options 40 | 41 | | Flag | Description | Default | 42 | | ----------------- | ------------------------------------------- | ------------ | 43 | | `--path, -p` | Path to the source code directory | *(required)* | 44 | | `--output, -o` | Output file path | `output.dot` | 45 | | `--format` | Output format: `dot` or `json` | `dot` | 46 | | `--detect-cycles` | Enable or disable circular dependency check | `true` | 47 | 48 | Example: 49 | 50 | ```bash 51 | cargo run -- --path ./src --output graph.dot --format dot --detect-cycles false 52 | ``` 53 | 54 | ## Output 55 | 56 | The tool outputs a `.dot` file representing the dependency graph. You can render it using Graphviz: 57 | 58 | ```bash 59 | dot -Tpng output.dot -o graph.png 60 | ``` 61 | 62 | image 63 | image 64 | GyKXrqsXoAA0Wm- 65 | image 66 | 67 | -------------------------------------------------------------------------------- /src/graph/mod.rs: -------------------------------------------------------------------------------- 1 | use std::collections::{HashMap, HashSet}; 2 | use std::fs::File; 3 | use std::io::{self, Write as IoWrite}; 4 | use std::fmt::Write as FmtWrite; 5 | 6 | #[derive(Debug)] 7 | pub struct DepGraph { 8 | edges: HashMap>, 9 | } 10 | 11 | impl DepGraph { 12 | pub fn new() -> Self { 13 | Self { 14 | edges: HashMap::new(), 15 | } 16 | } 17 | 18 | pub fn add_file(&mut self, filename: String, deps: Vec) { 19 | self.edges.insert(filename, deps); 20 | } 21 | 22 | pub fn detect_cycles(&self) -> Vec> { 23 | let mut visited = HashSet::new(); 24 | let mut stack = Vec::new(); 25 | let mut all_cycles = Vec::new(); 26 | 27 | for node in self.edges.keys() { 28 | if !visited.contains(node) { 29 | self.dfs(node, &mut visited, &mut stack, &mut all_cycles); 30 | } 31 | } 32 | 33 | all_cycles 34 | } 35 | 36 | fn dfs( 37 | &self, 38 | node: &str, 39 | visited: &mut HashSet, 40 | stack: &mut Vec, 41 | all_cycles: &mut Vec>, 42 | ) { 43 | visited.insert(node.to_string()); 44 | stack.push(node.to_string()); 45 | 46 | if let Some(neighbors) = self.edges.get(node) { 47 | for neighbor in neighbors { 48 | if stack.contains(neighbor) { 49 | let start = stack.iter().position(|x| x == neighbor).unwrap(); 50 | let cycle = stack[start..].to_vec(); 51 | all_cycles.push(cycle); 52 | } else if !visited.contains(neighbor) { 53 | self.dfs(neighbor, visited, stack, all_cycles); 54 | } 55 | } 56 | } 57 | 58 | stack.pop(); 59 | } 60 | 61 | pub fn export_to_json(&self, path: &str) -> io::Result<()> { 62 | let json_map = &self.edges; 63 | let json = serde_json::to_string_pretty(json_map) 64 | .map_err(|e| io::Error::new(io::ErrorKind::Other, e))?; 65 | 66 | let mut file = File::create(path)?; 67 | file.write_all(json.as_bytes())?; 68 | 69 | Ok(()) 70 | } 71 | 72 | pub fn export_to_dot(&self, path: &str) -> io::Result<()> { 73 | let mut dot = String::from("digraph dependencies {\n"); 74 | 75 | for (file, deps) in &self.edges { 76 | for dep in deps { 77 | writeln!(dot, " \"{}\" -> \"{}\";", file, dep) 78 | .map_err(|e| io::Error::new(io::ErrorKind::Other, e))?; 79 | } 80 | } 81 | 82 | dot.push_str("}\n"); 83 | 84 | let mut file = File::create(path)?; 85 | file.write_all(dot.as_bytes())?; 86 | 87 | Ok(()) 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | use clap::Parser; 2 | 3 | mod utils; 4 | mod parsers; 5 | pub mod analyzer; 6 | mod graph; 7 | 8 | use graph::DepGraph; 9 | use parsers::parser_registry::{get_parsers, parse_file_with_matching_parser}; 10 | use utils::file_walker::walk_source_dir; 11 | 12 | #[derive(Parser, Debug)] 13 | #[command(name = "nexus")] 14 | struct Args { 15 | /// Path to the source code directory 16 | #[arg(short, long)] 17 | path: String, 18 | 19 | /// Output file path 20 | #[arg(short, long, default_value = "output.dot")] 21 | output: String, 22 | 23 | /// Output format: dot or json 24 | #[arg(long, default_value = "dot")] 25 | format: String, 26 | 27 | /// Whether to detect circular dependencies 28 | #[arg(long, default_value = "true", action = clap::ArgAction::Set)] 29 | detect_cycles: bool, 30 | 31 | 32 | } 33 | 34 | #[tokio::main] 35 | async fn main() { 36 | let args = Args::parse(); 37 | let parsers = get_parsers(); 38 | let mut graph = DepGraph::new(); 39 | 40 | println!("Scanning directory: {}", args.path); 41 | 42 | match walk_source_dir(&args.path) { 43 | Ok(files) => { 44 | println!("Found {} files", files.len()); 45 | 46 | for file in &files { 47 | match parse_file_with_matching_parser(file, &parsers) { 48 | Ok((filename, deps)) => { 49 | let file_str = file.to_string_lossy().to_string(); 50 | graph.add_file(file_str.clone(), deps.clone()); 51 | 52 | println!("File: {filename}"); 53 | for dep in &deps { 54 | println!(" depends on: {dep}"); 55 | } 56 | } 57 | Err(e) => { 58 | eprintln!("Failed to parse {}: {:?}", file.display(), e); 59 | } 60 | } 61 | } 62 | 63 | match args.format.as_str() { 64 | "dot" => { 65 | if let Err(e) = graph.export_to_dot(&args.output) { 66 | eprintln!("Failed to write DOT file: {}", e); 67 | } else { 68 | println!("DOT graph written to {}", args.output); 69 | } 70 | } 71 | "json" => { 72 | if let Err(e) = graph.export_to_json(&args.output) { 73 | eprintln!("Failed to write JSON file: {}", e); 74 | } else { 75 | println!("JSON graph written to {}", args.output); 76 | } 77 | } 78 | _ => { 79 | eprintln!("Unknown format: {}", args.format); 80 | } 81 | } 82 | 83 | if args.detect_cycles { 84 | let cycles = graph.detect_cycles(); 85 | if !cycles.is_empty() { 86 | println!("\nCircular Dependencies Found:"); 87 | for cycle in cycles { 88 | println!(" - {}", cycle.join(" -> ")); 89 | } 90 | } else { 91 | println!("\nNo circular dependencies found."); 92 | } 93 | } 94 | } 95 | Err(e) => eprintln!("Error reading directory: {}", e), 96 | } 97 | } 98 | --------------------------------------------------------------------------------