1use 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#[derive(Parser)]
21#[command(name = "icb")]
22#[command(about = "Infinite Code Blueprint CLI")]
23struct Cli {
24 #[command(subcommand)]
25 command: Command,
26}
27
28#[derive(clap::Subcommand)]
30enum Command {
31 Analyze {
33 path: PathBuf,
35 #[arg(short, long)]
37 language: String,
38 #[arg(long)]
40 compile_commands: Option<PathBuf>,
41 #[arg(long, default_value = "c++17")]
43 cpp_std: String,
44 #[arg(long)]
46 cache: Option<PathBuf>,
47 #[arg(long)]
49 no_system_headers: bool,
50 },
51 Query {
53 project: PathBuf,
55 #[arg(short, long, default_value = "python")]
57 language: String,
58 #[arg(long)]
60 compile_commands: Option<PathBuf>,
61 #[arg(long, default_value = "c++17")]
63 cpp_std: String,
64 #[arg(long)]
66 functions: bool,
67 #[arg(long)]
69 callers: Option<String>,
70 #[arg(long)]
72 callees: Option<String>,
73 #[arg(long)]
75 unused: bool,
76 #[arg(long)]
78 dot: bool,
79 #[arg(long)]
81 cycles: bool,
82 #[arg(long)]
84 dead_code: bool,
85 #[arg(long, default_value = "main", requires = "dead_code")]
87 entries: String,
88 #[arg(long)]
90 complexity: bool,
91 #[arg(long, default_value = "20", requires = "complexity")]
93 threshold: usize,
94 #[arg(long)]
96 cache: Option<PathBuf>,
97 #[arg(long)]
99 no_system_headers: bool,
100 },
101 Report {
103 project: PathBuf,
105 #[arg(short, long)]
107 language: String,
108 #[arg(long)]
110 compile_commands: Option<PathBuf>,
111 #[arg(long, default_value = "c++17")]
113 cpp_std: String,
114 #[arg(long)]
116 cache: Option<PathBuf>,
117 #[arg(long)]
119 no_system_headers: bool,
120 #[arg(short, long, default_value = "report.html")]
122 output: PathBuf,
123 },
124 Diff {
126 old_project: PathBuf,
128 new_project: PathBuf,
130 #[arg(short, long)]
132 language: String,
133 #[arg(long)]
135 compile_commands: Option<PathBuf>,
136 #[arg(long, default_value = "c++17")]
138 cpp_std: String,
139 #[arg(long)]
141 cache: Option<PathBuf>,
142 #[arg(long)]
144 no_system_headers: bool,
145 #[arg(short, long, default_value = "diff.html")]
147 output: PathBuf,
148 },
149}
150
151fn 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
307struct 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
319fn 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
330fn 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
369fn 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}