code-graph-cli 3.0.3

Code intelligence engine for TypeScript/JavaScript/Rust/Python/Go — query the dependency graph instead of reading source files.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
use petgraph::Direction;
use petgraph::visit::EdgeRef;

use crate::graph::{
    CodeGraph,
    edge::EdgeKind,
    node::{GraphNode, SymbolKind},
};

/// Per-crate symbol breakdown (for workspace projects with multiple crates).
#[derive(Debug)]
pub struct CrateStats {
    /// Normalized crate name (hyphens → underscores).
    pub crate_name: String,
    /// Number of source files in this crate.
    pub file_count: usize,
    /// Total symbol count across all files in this crate.
    pub symbol_count: usize,
    // Symbol counts by kind
    pub fn_count: usize,
    pub struct_count: usize,
    pub enum_count: usize,
    pub trait_count: usize,
    pub impl_method_count: usize,
    pub type_alias_count: usize,
    pub const_count: usize,
    pub static_count: usize,
    pub macro_count: usize,
}

/// Aggregated project statistics derived from the code graph.
#[derive(Debug)]
pub struct ProjectStats {
    pub file_count: usize,
    pub symbol_count: usize,
    pub functions: usize,
    pub classes: usize,
    pub interfaces: usize,
    pub type_aliases: usize,
    pub enums: usize,
    pub variables: usize,
    pub components: usize,
    pub methods: usize,
    pub properties: usize,
    pub import_edges: usize,
    pub external_packages: usize,
    pub unresolved_imports: usize,
    // Rust-specific counts (Phase 8)
    pub rust_fns: usize,
    pub rust_structs: usize,
    pub rust_enums: usize,
    pub rust_traits: usize,
    pub rust_impl_methods: usize,
    pub rust_type_aliases: usize,
    pub rust_consts: usize,
    pub rust_statics: usize,
    pub rust_macros: usize,
    pub rust_imports: usize,
    pub rust_reexports: usize,
    // Phase 9 additions: per-crate breakdowns and dependency counts
    /// Per-crate symbol breakdowns (non-empty only for workspace projects).
    pub rust_crate_stats: Vec<CrateStats>,
    /// Number of distinct Builtin nodes (std, core, alloc).
    pub builtin_count: usize,
    /// Number of edges pointing to Builtin nodes (usage count).
    pub builtin_usage_count: usize,
    /// Number of edges pointing to ExternalPackage nodes (usage count).
    pub external_usage_count: usize,
    // Python-specific counts (Phase 17)
    /// Number of Python files in the graph.
    pub python_file_count: usize,
    /// Total Python symbols (functions + classes + type_aliases + variables + methods).
    pub python_symbol_count: usize,
    /// Python function count.
    pub python_fns: usize,
    /// Python class count.
    pub python_classes: usize,
    /// Python type alias count (PEP 695 `type X = ...`).
    pub python_type_aliases: usize,
    /// Python variable (module-level assignment) count.
    pub python_variables: usize,
    /// Python method count (class member methods).
    pub python_methods: usize,
    // Go-specific counts (Phase 18)
    /// Number of Go files in the graph.
    pub go_file_count: usize,
    /// Total Go symbols.
    pub go_symbol_count: usize,
    /// Go function count.
    pub go_fns: usize,
    /// Go struct count.
    pub go_structs: usize,
    /// Go interface count.
    pub go_interfaces: usize,
    /// Go method count (methods on receivers).
    pub go_methods: usize,
    /// Go const count.
    pub go_consts: usize,
    /// Go variable count.
    pub go_variables: usize,
    /// Go type alias/definition count.
    pub go_type_aliases: usize,
    // Phase 12: Non-parsed file counts
    /// Total number of non-parsed (non-source) files in the graph.
    pub non_parsed_files: usize,
    /// Count of doc files (FileKind::Doc).
    pub doc_files: usize,
    /// Count of config files (FileKind::Config).
    pub config_files: usize,
    /// Count of CI files (FileKind::Ci).
    pub ci_files: usize,
    /// Count of asset files (FileKind::Asset).
    pub asset_files: usize,
    /// Count of other non-parsed files (FileKind::Other).
    pub other_files: usize,
    /// Count of source files (FileKind::Source) -- for clarity in output.
    pub source_files: usize,
}

/// Compute project statistics from a built `CodeGraph`.
pub fn project_stats(graph: &CodeGraph) -> ProjectStats {
    let breakdown = graph.symbols_by_kind();

    let import_edges = graph
        .graph
        .edge_indices()
        .filter(|&e| matches!(graph.graph[e], EdgeKind::ResolvedImport { .. }))
        .count();

    let rust_imports = graph
        .graph
        .edge_indices()
        .filter(|&e| matches!(graph.graph[e], EdgeKind::RustImport { .. }))
        .count();

    let rust_reexports = graph
        .graph
        .edge_indices()
        .filter(|&e| matches!(graph.graph[e], EdgeKind::ReExport { .. }))
        .count();

    let mut external_packages = 0usize;
    let mut unresolved_imports = 0usize;
    let mut builtin_count = 0usize;

    for idx in graph.graph.node_indices() {
        match graph.graph[idx] {
            GraphNode::ExternalPackage(_) => external_packages += 1,
            GraphNode::UnresolvedImport { .. } => unresolved_imports += 1,
            GraphNode::Builtin { .. } => builtin_count += 1,
            _ => {}
        }
    }

    // Count edges pointing to Builtin and ExternalPackage nodes.
    let mut builtin_usage_count = 0usize;
    let mut external_usage_count = 0usize;
    for edge_idx in graph.graph.edge_indices() {
        if let EdgeKind::ResolvedImport { .. } = &graph.graph[edge_idx] {
            let (_src, tgt) = graph.graph.edge_endpoints(edge_idx).unwrap();
            match &graph.graph[tgt] {
                GraphNode::Builtin { .. } => builtin_usage_count += 1,
                GraphNode::ExternalPackage(_) => external_usage_count += 1,
                _ => {}
            }
        }
    }

    // Count Rust-specific symbols by checking parent file language
    let mut rust_fns = 0usize;
    let mut rust_structs = 0usize;
    let mut rust_enums = 0usize;
    let mut rust_traits = 0usize;
    let mut rust_impl_methods = 0usize;
    let mut rust_type_aliases = 0usize;
    let mut rust_consts = 0usize;
    let mut rust_statics = 0usize;
    let mut rust_macros = 0usize;

    for idx in graph.graph.node_indices() {
        if let GraphNode::Symbol(ref s) = graph.graph[idx] {
            let in_rust_file = graph
                .graph
                .edges_directed(idx, Direction::Incoming)
                .any(|e| {
                    if let EdgeKind::Contains = e.weight()
                        && let GraphNode::File(ref f) = graph.graph[e.source()]
                    {
                        return f.language == "rust";
                    }
                    false
                });
            let parent_in_rust = if !in_rust_file {
                graph
                    .graph
                    .edges_directed(idx, Direction::Outgoing)
                    .any(|e| {
                        if let EdgeKind::ChildOf = e.weight() {
                            let parent = e.target();
                            graph
                                .graph
                                .edges_directed(parent, Direction::Incoming)
                                .any(|pe| {
                                    if let EdgeKind::Contains = pe.weight()
                                        && let GraphNode::File(ref f) = graph.graph[pe.source()]
                                    {
                                        return f.language == "rust";
                                    }
                                    false
                                })
                        } else {
                            false
                        }
                    })
            } else {
                false
            };

            if !in_rust_file && !parent_in_rust {
                continue;
            }

            match s.kind {
                SymbolKind::Function => rust_fns += 1,
                SymbolKind::Struct => rust_structs += 1,
                SymbolKind::Enum => rust_enums += 1,
                SymbolKind::Trait => rust_traits += 1,
                SymbolKind::ImplMethod => rust_impl_methods += 1,
                SymbolKind::TypeAlias => rust_type_aliases += 1,
                SymbolKind::Const => rust_consts += 1,
                SymbolKind::Static => rust_statics += 1,
                SymbolKind::Macro => rust_macros += 1,
                _ => {}
            }
        }
    }

    // ---------------------------------------------------------------------------
    // Python symbol counts (Phase 17).
    // ---------------------------------------------------------------------------
    let mut python_symbol_count = 0usize;
    let mut python_fns = 0usize;
    let mut python_classes = 0usize;
    let mut python_type_aliases = 0usize;
    let mut python_variables = 0usize;

    // First pass: collect Python file node indices.
    let python_file_indices: Vec<petgraph::stable_graph::NodeIndex> = graph
        .graph
        .node_indices()
        .filter(|&idx| {
            if let GraphNode::File(ref fi) = graph.graph[idx] {
                fi.language == "python"
            } else {
                false
            }
        })
        .collect();
    let python_file_count = python_file_indices.len();

    // Second pass: count symbols contained in Python files.
    // This includes both top-level symbols (via Contains edges from file) and
    // class members (via ChildOf edges from child symbols whose parents are
    // in Python files).
    let mut python_methods = 0usize;
    for file_idx in &python_file_indices {
        for edge in graph.graph.edges(*file_idx) {
            if let EdgeKind::Contains = edge.weight()
                && let GraphNode::Symbol(ref s) = graph.graph[edge.target()]
            {
                python_symbol_count += 1;
                match s.kind {
                    SymbolKind::Function => python_fns += 1,
                    SymbolKind::Class => python_classes += 1,
                    SymbolKind::TypeAlias => python_type_aliases += 1,
                    SymbolKind::Variable => python_variables += 1,
                    _ => {}
                }

                // Also count class methods (ChildOf edges going OUT from this symbol).
                for child_edge in graph
                    .graph
                    .edges_directed(edge.target(), petgraph::Direction::Incoming)
                {
                    if let EdgeKind::ChildOf = child_edge.weight()
                        && let GraphNode::Symbol(ref cs) = graph.graph[child_edge.source()]
                    {
                        python_symbol_count += 1;
                        if matches!(cs.kind, SymbolKind::Method | SymbolKind::Function) {
                            python_methods += 1;
                        }
                    }
                }
            }
        }
    }

    // ---------------------------------------------------------------------------
    // Go symbol counts (Phase 18).
    // ---------------------------------------------------------------------------
    let go_file_indices: Vec<petgraph::stable_graph::NodeIndex> = graph
        .graph
        .node_indices()
        .filter(|&idx| {
            if let GraphNode::File(ref fi) = graph.graph[idx] {
                fi.language == "go"
            } else {
                false
            }
        })
        .collect();
    let go_file_count = go_file_indices.len();
    let mut go_symbol_count = 0usize;
    let mut go_fns = 0usize;
    let mut go_structs = 0usize;
    let mut go_interfaces = 0usize;
    let mut go_methods = 0usize;
    let mut go_consts = 0usize;
    let mut go_variables = 0usize;
    let mut go_type_aliases = 0usize;

    for file_idx in &go_file_indices {
        for edge in graph.graph.edges(*file_idx) {
            if let EdgeKind::Contains = edge.weight()
                && let GraphNode::Symbol(ref s) = graph.graph[edge.target()]
            {
                go_symbol_count += 1;
                match s.kind {
                    SymbolKind::Function => go_fns += 1,
                    SymbolKind::Struct => go_structs += 1,
                    SymbolKind::Interface => {
                        go_interfaces += 1;
                        // Interface methods are added as child symbols (ChildOf edges only,
                        // no Contains edge from file) — count them here to avoid missing them.
                        for child_edge in graph
                            .graph
                            .edges_directed(edge.target(), Direction::Incoming)
                        {
                            if let EdgeKind::ChildOf = child_edge.weight()
                                && let GraphNode::Symbol(ref cs) = graph.graph[child_edge.source()]
                                && matches!(cs.kind, SymbolKind::Method | SymbolKind::Function)
                            {
                                go_symbol_count += 1;
                                go_methods += 1;
                            }
                        }
                    }
                    SymbolKind::Const => go_consts += 1,
                    SymbolKind::Variable => go_variables += 1,
                    SymbolKind::TypeAlias => go_type_aliases += 1,
                    SymbolKind::Method => go_methods += 1,
                    _ => {}
                }
            }
        }
    }

    // ---------------------------------------------------------------------------
    // Per-crate breakdown (Phase 9).
    //
    // Group Rust file nodes by their crate_name field, then count symbols per crate.
    // Only populated when more than one crate is present (single-crate projects don't need it).
    // ---------------------------------------------------------------------------
    let rust_crate_stats = compute_crate_stats(graph);

    // Phase 12: Count files by FileKind
    let mut source_files = 0usize;
    let mut doc_files = 0usize;
    let mut config_files = 0usize;
    let mut ci_files = 0usize;
    let mut asset_files = 0usize;
    let mut other_files = 0usize;
    for idx in graph.graph.node_indices() {
        if let GraphNode::File(ref fi) = graph.graph[idx] {
            match fi.kind {
                crate::graph::node::FileKind::Source => source_files += 1,
                crate::graph::node::FileKind::Doc => doc_files += 1,
                crate::graph::node::FileKind::Config => config_files += 1,
                crate::graph::node::FileKind::Ci => ci_files += 1,
                crate::graph::node::FileKind::Asset => asset_files += 1,
                crate::graph::node::FileKind::Other => other_files += 1,
            }
        }
    }
    let non_parsed_files = doc_files + config_files + ci_files + asset_files + other_files;

    ProjectStats {
        file_count: graph.file_index.len(),
        symbol_count: graph.symbol_count(),
        functions: *breakdown.get(&SymbolKind::Function).unwrap_or(&0),
        classes: *breakdown.get(&SymbolKind::Class).unwrap_or(&0),
        interfaces: *breakdown.get(&SymbolKind::Interface).unwrap_or(&0),
        type_aliases: *breakdown.get(&SymbolKind::TypeAlias).unwrap_or(&0),
        enums: *breakdown.get(&SymbolKind::Enum).unwrap_or(&0),
        variables: *breakdown.get(&SymbolKind::Variable).unwrap_or(&0),
        components: *breakdown.get(&SymbolKind::Component).unwrap_or(&0),
        methods: *breakdown.get(&SymbolKind::Method).unwrap_or(&0),
        properties: *breakdown.get(&SymbolKind::Property).unwrap_or(&0),
        import_edges,
        external_packages,
        unresolved_imports,
        rust_fns,
        rust_structs,
        rust_enums,
        rust_traits,
        rust_impl_methods,
        rust_type_aliases,
        rust_consts,
        rust_statics,
        rust_macros,
        rust_imports,
        rust_reexports,
        rust_crate_stats,
        builtin_count,
        builtin_usage_count,
        external_usage_count,
        // Phase 17: Python counts
        python_file_count,
        python_symbol_count,
        python_fns,
        python_classes,
        python_type_aliases,
        python_variables,
        python_methods,
        // Phase 18: Go counts
        go_file_count,
        go_symbol_count,
        go_fns,
        go_structs,
        go_interfaces,
        go_methods,
        go_consts,
        go_variables,
        go_type_aliases,
        // Phase 12: Non-parsed file counts
        non_parsed_files,
        doc_files,
        config_files,
        ci_files,
        asset_files,
        other_files,
        source_files,
    }
}

// ---------------------------------------------------------------------------
// Per-crate breakdown computation
// ---------------------------------------------------------------------------

/// Build per-crate symbol stats by grouping files by their `crate_name` field.
///
/// Returns an empty `Vec` if there are no Rust files with `crate_name` set, or if all
/// files belong to a single unnamed crate (not worth showing a one-row breakdown).
fn compute_crate_stats(graph: &CodeGraph) -> Vec<CrateStats> {
    use std::collections::HashMap;

    // Collect (crate_name, file_idx) pairs from Rust files with crate_name set.
    let mut crate_files: HashMap<String, Vec<petgraph::stable_graph::NodeIndex>> = HashMap::new();

    for idx in graph.graph.node_indices() {
        if let GraphNode::File(ref fi) = graph.graph[idx]
            && fi.language == "rust"
            && let Some(ref cn) = fi.crate_name
        {
            crate_files.entry(cn.clone()).or_default().push(idx);
        }
    }

    // Only build per-crate breakdown if there are multiple crates.
    if crate_files.len() <= 1 {
        return Vec::new();
    }

    let mut result: Vec<CrateStats> = crate_files
        .into_iter()
        .map(|(crate_name, file_indices)| {
            let file_count = file_indices.len();
            let mut sym_count = 0usize;
            let mut fn_count = 0usize;
            let mut struct_count = 0usize;
            let mut enum_count = 0usize;
            let mut trait_count = 0usize;
            let mut impl_method_count = 0usize;
            let mut type_alias_count = 0usize;
            let mut const_count = 0usize;
            let mut static_count = 0usize;
            let mut macro_count = 0usize;

            // For each file in this crate, find all symbols via Contains edges.
            for file_idx in &file_indices {
                for edge in graph.graph.edges(*file_idx) {
                    if let EdgeKind::Contains = edge.weight()
                        && let GraphNode::Symbol(ref s) = graph.graph[edge.target()]
                    {
                        sym_count += 1;
                        match s.kind {
                            SymbolKind::Function => fn_count += 1,
                            SymbolKind::Struct => struct_count += 1,
                            SymbolKind::Enum => enum_count += 1,
                            SymbolKind::Trait => trait_count += 1,
                            SymbolKind::ImplMethod => impl_method_count += 1,
                            SymbolKind::TypeAlias => type_alias_count += 1,
                            SymbolKind::Const => const_count += 1,
                            SymbolKind::Static => static_count += 1,
                            SymbolKind::Macro => macro_count += 1,
                            _ => {}
                        }
                        // Also count child symbols (via ChildOf edges from children).
                        for child_edge in graph
                            .graph
                            .edges_directed(edge.target(), Direction::Incoming)
                        {
                            if let EdgeKind::ChildOf = child_edge.weight() {
                                sym_count += 1;
                                if let GraphNode::Symbol(ref cs) = graph.graph[child_edge.source()]
                                {
                                    match cs.kind {
                                        SymbolKind::ImplMethod => impl_method_count += 1,
                                        SymbolKind::Property => {} // don't double count
                                        _ => {}
                                    }
                                }
                            }
                        }
                    }
                }
            }

            CrateStats {
                crate_name,
                file_count,
                symbol_count: sym_count,
                fn_count,
                struct_count,
                enum_count,
                trait_count,
                impl_method_count,
                type_alias_count,
                const_count,
                static_count,
                macro_count,
            }
        })
        .collect();

    // Sort by crate name for deterministic output.
    result.sort_by(|a, b| a.crate_name.cmp(&b.crate_name));
    result
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::graph::CodeGraph;
    use crate::graph::node::FileKind;
    use std::path::PathBuf;

    #[test]
    fn test_project_stats_counts_non_parsed_files() {
        let mut graph = CodeGraph::new();

        // Add source files
        graph.add_file(PathBuf::from("src/main.rs"), "rust");
        graph.add_file(PathBuf::from("src/lib.ts"), "typescript");

        // Add non-parsed files
        graph.add_non_parsed_file(PathBuf::from("README.md"), FileKind::Doc);
        graph.add_non_parsed_file(PathBuf::from("Cargo.toml"), FileKind::Config);
        graph.add_non_parsed_file(PathBuf::from(".github/ci.yml"), FileKind::Ci);
        graph.add_non_parsed_file(PathBuf::from("logo.png"), FileKind::Asset);
        graph.add_non_parsed_file(PathBuf::from("LICENSE"), FileKind::Other);

        let stats = project_stats(&graph);

        assert_eq!(stats.file_count, 7, "total file count includes all files");
        assert_eq!(stats.source_files, 2, "source files only");
        assert_eq!(stats.non_parsed_files, 5, "non-parsed files only");
        assert_eq!(stats.doc_files, 1);
        assert_eq!(stats.config_files, 1);
        assert_eq!(stats.ci_files, 1);
        assert_eq!(stats.asset_files, 1);
        assert_eq!(stats.other_files, 1);
    }

    #[test]
    fn test_project_stats_zero_non_parsed() {
        let mut graph = CodeGraph::new();
        graph.add_file(PathBuf::from("src/main.rs"), "rust");

        let stats = project_stats(&graph);

        assert_eq!(stats.source_files, 1);
        assert_eq!(stats.non_parsed_files, 0);
    }
}