Skip to main content

icb_cli/
main.rs

1//! Command-line interface for the Infinite Code Blueprint (ICB).
2//!
3//! Provides four subcommands:
4//! - **analyze** – parse a project and print graph stats.
5//! - **query** – run analytical queries (functions, callers, callees,
6//!   unused code, cycles, dead code, complexity, DOT export).
7//! - **report** – generate a static HTML report with embedded graph.
8//! - **diff** – compare two project versions and produce an HTML diff.
9
10use clap::Parser;
11use std::fs;
12use std::path::{Path, PathBuf};
13
14use icb_common::Language;
15use icb_graph::builder::GraphBuilder;
16use icb_graph::{analysis, cache, query, visualizer};
17use icb_parser::manager::ParserManager;
18
19/// Top-level CLI structure.
20#[derive(Parser)]
21#[command(name = "icb")]
22#[command(about = "Infinite Code Blueprint CLI")]
23struct Cli {
24    #[command(subcommand)]
25    command: Command,
26}
27
28/// Available subcommands.
29#[derive(clap::Subcommand)]
30enum Command {
31    /// Parse and display graph size.
32    Analyze {
33        /// Path to source file or directory.
34        path: PathBuf,
35        /// Programming language (e.g. cpp, python).
36        #[arg(short, long)]
37        language: String,
38        /// Path to compile_commands.json (C/C++).
39        #[arg(long)]
40        compile_commands: Option<PathBuf>,
41        /// C++ standard version.
42        #[arg(long, default_value = "c++17")]
43        cpp_std: String,
44        /// Path to cache file.
45        #[arg(long)]
46        cache: Option<PathBuf>,
47        /// Exclude system headers.
48        #[arg(long)]
49        no_system_headers: bool,
50    },
51    /// Run queries on a project.
52    Query {
53        /// Project path.
54        project: PathBuf,
55        /// Language.
56        #[arg(short, long, default_value = "python")]
57        language: String,
58        /// compile_commands.json (C/C++).
59        #[arg(long)]
60        compile_commands: Option<PathBuf>,
61        /// C++ standard.
62        #[arg(long, default_value = "c++17")]
63        cpp_std: String,
64        /// List functions.
65        #[arg(long)]
66        functions: bool,
67        /// Show callers of a function.
68        #[arg(long)]
69        callers: Option<String>,
70        /// Show callees of a function.
71        #[arg(long)]
72        callees: Option<String>,
73        /// List unused functions.
74        #[arg(long)]
75        unused: bool,
76        /// Export DOT graph.
77        #[arg(long)]
78        dot: bool,
79        /// Detect call cycles.
80        #[arg(long)]
81        cycles: bool,
82        /// Detect dead code (requires --entries).
83        #[arg(long)]
84        dead_code: bool,
85        /// Entry points for dead code (comma separated).
86        #[arg(long, default_value = "main", requires = "dead_code")]
87        entries: String,
88        /// Show complex functions.
89        #[arg(long)]
90        complexity: bool,
91        /// Complexity threshold (AST nodes).
92        #[arg(long, default_value = "20", requires = "complexity")]
93        threshold: usize,
94        /// Cache file.
95        #[arg(long)]
96        cache: Option<PathBuf>,
97        /// Exclude system headers.
98        #[arg(long)]
99        no_system_headers: bool,
100    },
101    /// Generate static HTML report.
102    Report {
103        /// Project path.
104        project: PathBuf,
105        /// Language.
106        #[arg(short, long)]
107        language: String,
108        /// compile_commands.json.
109        #[arg(long)]
110        compile_commands: Option<PathBuf>,
111        /// C++ standard.
112        #[arg(long, default_value = "c++17")]
113        cpp_std: String,
114        /// Cache file.
115        #[arg(long)]
116        cache: Option<PathBuf>,
117        /// Exclude system headers.
118        #[arg(long)]
119        no_system_headers: bool,
120        /// Output file (default: report.html).
121        #[arg(short, long, default_value = "report.html")]
122        output: PathBuf,
123    },
124    /// Diff two project versions.
125    Diff {
126        /// Old project path.
127        old_project: PathBuf,
128        /// New project path.
129        new_project: PathBuf,
130        /// Language.
131        #[arg(short, long)]
132        language: String,
133        /// compile_commands.json.
134        #[arg(long)]
135        compile_commands: Option<PathBuf>,
136        /// C++ standard.
137        #[arg(long, default_value = "c++17")]
138        cpp_std: String,
139        /// Cache file.
140        #[arg(long)]
141        cache: Option<PathBuf>,
142        /// Exclude system headers.
143        #[arg(long)]
144        no_system_headers: bool,
145        /// Output file (default: diff.html).
146        #[arg(short, long, default_value = "diff.html")]
147        output: PathBuf,
148    },
149}
150
151/// Run the CLI.
152fn main() -> anyhow::Result<()> {
153    env_logger::init();
154    let cli = Cli::parse();
155    let manager = ParserManager::new();
156
157    match cli.command {
158        Command::Analyze {
159            path,
160            language,
161            compile_commands,
162            cpp_std,
163            cache: cache_path,
164            no_system_headers,
165        } => {
166            let lang = parse_language(&language)?;
167            let opts = BuildOptions {
168                manager: &manager,
169                lang,
170                path: &path,
171                compile_commands: compile_commands.as_deref(),
172                cpp_std: &cpp_std,
173                cache_path: cache_path.as_deref(),
174                show_progress: true,
175                no_system_headers,
176            };
177            let (cpg, _) = build_or_load_graph(opts)?;
178            println!(
179                "Graph: {} nodes, {} edges",
180                cpg.node_count(),
181                cpg.edge_count()
182            );
183        }
184        Command::Query {
185            project,
186            language,
187            compile_commands,
188            cpp_std,
189            functions,
190            callers,
191            callees,
192            unused,
193            dot,
194            cycles,
195            dead_code,
196            entries,
197            complexity,
198            threshold,
199            cache: cache_path,
200            no_system_headers,
201        } => {
202            let lang = parse_language(&language)?;
203            let opts = BuildOptions {
204                manager: &manager,
205                lang,
206                path: &project,
207                compile_commands: compile_commands.as_deref(),
208                cpp_std: &cpp_std,
209                cache_path: cache_path.as_deref(),
210                show_progress: false,
211                no_system_headers,
212            };
213            let (cpg, _) = build_or_load_graph(opts)?;
214            if functions {
215                print_functions(&cpg);
216            }
217            if let Some(target) = callers {
218                print_callers(&cpg, &target);
219            }
220            if let Some(target) = callees {
221                print_callees(&cpg, &target);
222            }
223            if unused {
224                print_unused(&cpg);
225            }
226            if dot {
227                println!("{}", visualizer::export_call_dot(&cpg));
228            }
229            if cycles {
230                print_cycles(&cpg);
231            }
232            if dead_code {
233                let entry_list: Vec<String> =
234                    entries.split(',').map(|s| s.trim().to_string()).collect();
235                print_dead_code(&cpg, &entry_list);
236            }
237            if complexity {
238                print_complexity(&cpg, threshold);
239            }
240        }
241        Command::Report {
242            project,
243            language,
244            compile_commands,
245            cpp_std,
246            cache: cache_path,
247            no_system_headers,
248            output,
249        } => {
250            let lang = parse_language(&language)?;
251            let opts = BuildOptions {
252                manager: &manager,
253                lang,
254                path: &project,
255                compile_commands: compile_commands.as_deref(),
256                cpp_std: &cpp_std,
257                cache_path: cache_path.as_deref(),
258                show_progress: true,
259                no_system_headers,
260            };
261            let (cpg, _) = build_or_load_graph(opts)?;
262            let html = icb_report::report::generate_report(&cpg, &project.display().to_string())?;
263            fs::write(&output, html)?;
264            println!("Report written to {:?}", output);
265        }
266        Command::Diff {
267            old_project,
268            new_project,
269            language,
270            compile_commands,
271            cpp_std,
272            cache: cache_path,
273            no_system_headers,
274            output,
275        } => {
276            let lang = parse_language(&language)?;
277            let opts_old = BuildOptions {
278                manager: &manager,
279                lang,
280                path: &old_project,
281                compile_commands: compile_commands.as_deref(),
282                cpp_std: &cpp_std,
283                cache_path: cache_path.as_deref(),
284                show_progress: true,
285                no_system_headers,
286            };
287            let (old_cpg, _) = build_or_load_graph(opts_old)?;
288            let opts_new = BuildOptions {
289                manager: &manager,
290                lang,
291                path: &new_project,
292                compile_commands: compile_commands.as_deref(),
293                cpp_std: &cpp_std,
294                cache_path: cache_path.as_deref(),
295                show_progress: true,
296                no_system_headers,
297            };
298            let (new_cpg, _) = build_or_load_graph(opts_new)?;
299            let html = icb_report::diff::generate_diff(&old_cpg, &new_cpg, "Project")?;
300            fs::write(&output, html)?;
301            println!("Diff written to {:?}", output);
302        }
303    }
304    Ok(())
305}
306
307/// Holds parameters for building or loading a graph.
308struct BuildOptions<'a> {
309    manager: &'a ParserManager,
310    lang: Language,
311    path: &'a Path,
312    compile_commands: Option<&'a Path>,
313    cpp_std: &'a str,
314    cache_path: Option<&'a Path>,
315    show_progress: bool,
316    no_system_headers: bool,
317}
318
319/// Convert a language string to [`Language`].
320fn parse_language(s: &str) -> anyhow::Result<Language> {
321    match s {
322        "python" => Ok(Language::Python),
323        "rust" => Ok(Language::Rust),
324        "javascript" => Ok(Language::JavaScript),
325        "cpp" | "c++" => Ok(Language::Cpp),
326        _ => anyhow::bail!("Unsupported language: {}", s),
327    }
328}
329
330/// Build a graph from source or load from cache.
331fn build_or_load_graph(
332    opts: BuildOptions,
333) -> anyhow::Result<(icb_graph::graph::CodePropertyGraph, usize)> {
334    if let Some(cache_file) = opts.cache_path {
335        if cache_file.exists() {
336            if let Ok(cpg) = cache::load_graph(cache_file) {
337                return Ok((cpg, 0));
338            }
339        }
340    }
341    let file_facts = build_file_facts(
342        opts.manager,
343        opts.lang,
344        opts.path,
345        opts.compile_commands,
346        opts.cpp_std,
347        opts.no_system_headers,
348    )?;
349    let count = file_facts.len();
350    if opts.show_progress {
351        println!("Parsed {} files", count);
352    }
353    let mut builder = GraphBuilder::new();
354    for (_, facts) in file_facts {
355        let mut local = GraphBuilder::new();
356        local.ingest_file_facts(&facts);
357        builder.merge(local);
358    }
359    builder.resolve_calls();
360    let cpg = builder.cpg;
361    if let Some(cache_file) = opts.cache_path {
362        if let Err(e) = cache::save_graph(&cpg, cache_file) {
363            log::warn!("Failed to save cache: {}", e);
364        }
365    }
366    Ok((cpg, count))
367}
368
369/// Build facts from a project path.
370fn build_file_facts(
371    manager: &ParserManager,
372    lang: Language,
373    path: &Path,
374    compile_commands: Option<&Path>,
375    cpp_std: &str,
376    no_system_headers: bool,
377) -> anyhow::Result<Vec<(String, Vec<icb_parser::facts::RawNode>)>> {
378    let allow_system = !no_system_headers;
379    if lang == Language::Cpp {
380        if let Some(cdb) = compile_commands {
381            let cdb = cdb.canonicalize()?;
382            let base_dir = cdb.parent().unwrap_or(Path::new("."));
383            Ok(icb_clang::project::parse_project(
384                &cdb,
385                base_dir,
386                true,
387                allow_system,
388            )?)
389        } else if path.is_file() {
390            let source = std::fs::read_to_string(path)?;
391            let args = vec![format!("-std={}", cpp_std)];
392            let facts = icb_clang::parser::parse_cpp_file(
393                &source,
394                &args,
395                Some(path.to_str().unwrap()),
396                allow_system,
397            )?;
398            Ok(vec![(
399                path.file_name()
400                    .unwrap_or_default()
401                    .to_string_lossy()
402                    .into_owned(),
403                facts,
404            )])
405        } else {
406            Ok(icb_clang::project::parse_directory(
407                path,
408                &[format!("-std={}", cpp_std)],
409                true,
410                None,
411                allow_system,
412            )?)
413        }
414    } else if path.is_dir() {
415        Ok(manager.parse_directory(lang, path)?)
416    } else {
417        let source = std::fs::read_to_string(path)?;
418        let facts = manager.parse_file(lang, &source)?;
419        Ok(vec![(
420            path.file_name()
421                .unwrap_or_default()
422                .to_string_lossy()
423                .into_owned(),
424            facts,
425        )])
426    }
427}
428
429fn print_functions(cpg: &icb_graph::graph::CodePropertyGraph) {
430    let funcs = query::find_by_kind(cpg, icb_common::NodeKind::Function);
431    println!("Functions ({})", funcs.len());
432    for f in &funcs {
433        println!(
434            "  {} (line {})",
435            f.name.as_deref().unwrap_or("?"),
436            f.start_line
437        );
438    }
439}
440fn print_callers(cpg: &icb_graph::graph::CodePropertyGraph, target: &str) {
441    let callers = query::callers_of(cpg, target);
442    println!("Callers of '{}' ({})", target, callers.len());
443    for (caller, _) in &callers {
444        println!(
445            "  {} (line {})",
446            caller.name.as_deref().unwrap_or("?"),
447            caller.start_line
448        );
449    }
450}
451fn print_callees(cpg: &icb_graph::graph::CodePropertyGraph, target: &str) {
452    let callees = query::callees_of(cpg, target);
453    println!("Callees of '{}' ({})", target, callees.len());
454    for (callee, _) in &callees {
455        println!(
456            "  {} (line {})",
457            callee.name.as_deref().unwrap_or("?"),
458            callee.start_line
459        );
460    }
461}
462fn print_unused(cpg: &icb_graph::graph::CodePropertyGraph) {
463    let unused = query::unused_functions(cpg);
464    println!("Unused functions ({})", unused.len());
465    for f in &unused {
466        println!(
467            "  {} (line {})",
468            f.name.as_deref().unwrap_or("?"),
469            f.start_line
470        );
471    }
472}
473fn print_cycles(cpg: &icb_graph::graph::CodePropertyGraph) {
474    let cycles = analysis::detect_call_cycles(cpg);
475    println!("Call cycles ({})", cycles.len());
476    for cycle in &cycles {
477        println!("  Length {}: {}", cycle.length, cycle.functions.join(", "));
478    }
479}
480fn print_dead_code(cpg: &icb_graph::graph::CodePropertyGraph, entries: &[String]) {
481    let dead = analysis::detect_dead_code(cpg, entries);
482    println!("Dead code from entries {:?} ({})", entries, dead.len());
483    for f in &dead {
484        println!(
485            "  {} (line {})",
486            f.name.as_deref().unwrap_or("?"),
487            f.start_line
488        );
489    }
490}
491fn print_complexity(cpg: &icb_graph::graph::CodePropertyGraph, threshold: usize) {
492    let complex = analysis::detect_complex_functions(cpg, threshold);
493    println!(
494        "Complex functions (threshold {}): {}",
495        threshold,
496        complex.len()
497    );
498    for report in &complex {
499        println!(
500            "  {} (AST nodes: {}, line {})",
501            report.function_name, report.ast_node_count, report.start_line
502        );
503    }
504}