repotoire 0.8.1

Graph-powered code analysis CLI. 110 detectors for security, architecture, bus factor, and code quality.
Documentation
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
//! Graph store traits for detector compatibility

use super::CodeNode;
use crate::graph::interner::{StrKey, StringInterner};
use crate::graph::node_index::NodeIndex;
use crate::graph::store_models::ExtraProps;
use std::collections::{BTreeMap, HashMap};
use std::sync::Arc;

/// Core interface for graph stores.
///
/// Contains two categories of methods:
///
/// **Core methods** — identity, extra props, stats.
///
/// **NodeIndex-based methods** (`functions_idx()`, `callers_idx(idx)`, …) —
///   Return zero-copy `&[NodeIndex]` slices from pre-built indexes.
///   Implemented by `CodeGraph` (frozen, production path) and `GraphBuilder`
///   (via lazily-built `QuerySnapshot`).
///   Default implementations return empty results for backends that
///   don't support indexed access (e.g., lightweight test mocks).
///
/// String-based convenience methods (`get_functions()`, `get_callers()`, …)
/// and derived helpers (shared Arc wrappers, filtering, adjacency builders)
/// live on [`GraphQueryExt`], which is auto-implemented for every
/// `GraphQuery` implementor via a blanket impl.
#[allow(dead_code)] // Trait defines public API surface; not all methods called in binary
pub trait GraphQuery: Send + Sync {
    // ==================== Core identity ====================

    /// Access the pre-computed graph primitives (dominator trees, PageRank, etc.).
    fn primitives(&self) -> &crate::graph::primitives::GraphPrimitives;

    /// Access the string interner for resolving StrKey -> &str.
    fn interner(&self) -> &StringInterner;

    /// Get extra (cold) properties for a node by its qualified_name StrKey.
    fn extra_props(&self, qn: StrKey) -> Option<ExtraProps>;

    /// Extra properties for a node by qualified_name StrKey (returns reference).
    fn extra_props_ref(&self, _qn: StrKey) -> Option<&ExtraProps> {
        None
    }

    /// Get stats (BTreeMap for deterministic key order)
    fn stats(&self) -> BTreeMap<String, i64>;

    // ==================== NodeIndex-based API ====================
    //
    // Zero-copy, O(1) access using NodeIndex.
    // CodeGraph implements all of these via pre-built indexes.
    // GraphBuilder implements them via a lazily-built QuerySnapshot.
    // Default implementations return empty results for lightweight
    // test mocks that don't support indexed access.

    /// Get a node by its graph index.
    fn node_idx(&self, _idx: NodeIndex) -> Option<&CodeNode> {
        None
    }

    /// Look up a node by qualified name. Returns both index and reference.
    fn node_by_name_idx(&self, _qn: &str) -> Option<(NodeIndex, &CodeNode)> {
        None
    }

    /// All function NodeIndexes.
    fn functions_idx(&self) -> &[NodeIndex] {
        &[]
    }

    /// All class NodeIndexes.
    fn classes_idx(&self) -> &[NodeIndex] {
        &[]
    }

    /// All file NodeIndexes.
    fn files_idx(&self) -> &[NodeIndex] {
        &[]
    }

    /// Functions that call this node (incoming Calls edges).
    fn callers_idx(&self, _idx: NodeIndex) -> &[NodeIndex] {
        &[]
    }

    /// Functions this node calls (outgoing Calls edges).
    fn callees_idx(&self, _idx: NodeIndex) -> &[NodeIndex] {
        &[]
    }

    /// Modules/files that import this node (incoming Imports edges).
    fn importers_idx(&self, _idx: NodeIndex) -> &[NodeIndex] {
        &[]
    }

    /// Modules/files this node imports (outgoing Imports edges).
    fn importees_idx(&self, _idx: NodeIndex) -> &[NodeIndex] {
        &[]
    }

    /// Parent classes (outgoing Inherits edges).
    fn parent_classes_idx(&self, _idx: NodeIndex) -> &[NodeIndex] {
        &[]
    }

    /// Child classes (incoming Inherits edges).
    fn child_classes_idx(&self, _idx: NodeIndex) -> &[NodeIndex] {
        &[]
    }

    /// Number of callers (fan-in). O(1) on CodeGraph.
    fn call_fan_in_idx(&self, idx: NodeIndex) -> usize {
        self.callers_idx(idx).len()
    }

    /// Number of callees (fan-out). O(1) on CodeGraph.
    fn call_fan_out_idx(&self, idx: NodeIndex) -> usize {
        self.callees_idx(idx).len()
    }

    /// Functions in a file as NodeIndex slice.
    fn functions_in_file_idx(&self, _file_path: &str) -> &[NodeIndex] {
        &[]
    }

    /// Classes in a file as NodeIndex slice.
    fn classes_in_file_idx(&self, _file_path: &str) -> &[NodeIndex] {
        &[]
    }

    /// Find the function containing a line in a file (returns NodeIndex).
    fn function_at_idx(&self, _file_path: &str, _line: u32) -> Option<NodeIndex> {
        None
    }

    /// All call edges as (caller, callee) NodeIndex pairs.
    fn all_call_edges(&self) -> &[(NodeIndex, NodeIndex)] {
        &[]
    }

    /// All import edges as (importer, importee) NodeIndex pairs.
    fn all_import_edges(&self) -> &[(NodeIndex, NodeIndex)] {
        &[]
    }

    /// All inheritance edges as (child, parent) NodeIndex pairs.
    fn all_inheritance_edges(&self) -> &[(NodeIndex, NodeIndex)] {
        &[]
    }

    /// Import cycle groups. Each inner Vec contains NodeIndexes of nodes in the cycle.
    fn import_cycles_idx(&self) -> &[Vec<NodeIndex>] {
        &[]
    }

    /// Edge fingerprint for topology change detection.
    fn edge_fingerprint_idx(&self) -> u64 {
        0
    }
}

/// Convenience extension trait with derived methods.
///
/// Auto-implemented for every [`GraphQuery`] implementor via blanket impl.
/// Contains the 16 string-based convenience methods (delegating to `_idx`
/// methods), shared-Arc wrappers, filtering helpers, cycle checks,
/// fan-in/fan-out index methods, and adjacency builders.
#[allow(dead_code)] // Trait defines public API surface; not all methods called in binary
pub trait GraphQueryExt: GraphQuery {
    // ==================== String-based convenience methods ====================
    //
    // Default implementations delegate to the _idx methods on GraphQuery.

    /// Get all functions
    fn get_functions(&self) -> Vec<CodeNode> {
        self.functions_idx()
            .iter()
            .filter_map(|&idx| self.node_idx(idx).copied())
            .collect()
    }

    /// Get all classes
    fn get_classes(&self) -> Vec<CodeNode> {
        self.classes_idx()
            .iter()
            .filter_map(|&idx| self.node_idx(idx).copied())
            .collect()
    }

    /// Get all files
    fn get_files(&self) -> Vec<CodeNode> {
        self.files_idx()
            .iter()
            .filter_map(|&idx| self.node_idx(idx).copied())
            .collect()
    }

    /// Get functions in a specific file
    fn get_functions_in_file(&self, file_path: &str) -> Vec<CodeNode> {
        self.functions_in_file_idx(file_path)
            .iter()
            .filter_map(|&idx| self.node_idx(idx).copied())
            .collect()
    }

    /// Get classes in a specific file
    fn get_classes_in_file(&self, file_path: &str) -> Vec<CodeNode> {
        self.classes_in_file_idx(file_path)
            .iter()
            .filter_map(|&idx| self.node_idx(idx).copied())
            .collect()
    }

    /// Get node by qualified name
    fn get_node(&self, qn: &str) -> Option<CodeNode> {
        self.node_by_name_idx(qn).map(|(_, node)| *node)
    }

    /// Get functions that call this function
    fn get_callers(&self, qn: &str) -> Vec<CodeNode> {
        let Some((idx, _)) = self.node_by_name_idx(qn) else {
            return vec![];
        };
        self.callers_idx(idx)
            .iter()
            .filter_map(|&ci| self.node_idx(ci).copied())
            .collect()
    }

    /// Get functions this function calls
    fn get_callees(&self, qn: &str) -> Vec<CodeNode> {
        let Some((idx, _)) = self.node_by_name_idx(qn) else {
            return vec![];
        };
        self.callees_idx(idx)
            .iter()
            .filter_map(|&ci| self.node_idx(ci).copied())
            .collect()
    }

    /// Count of callers (fan-in)
    fn call_fan_in(&self, qn: &str) -> usize {
        self.node_by_name_idx(qn)
            .map(|(idx, _)| self.callers_idx(idx).len())
            .unwrap_or(0)
    }

    /// Count of callees (fan-out)
    fn call_fan_out(&self, qn: &str) -> usize {
        self.node_by_name_idx(qn)
            .map(|(idx, _)| self.callees_idx(idx).len())
            .unwrap_or(0)
    }

    /// Get all call edges
    fn get_calls(&self) -> Vec<(StrKey, StrKey)> {
        self.all_call_edges()
            .iter()
            .filter_map(|&(src, tgt)| {
                let s = self.node_idx(src)?;
                let t = self.node_idx(tgt)?;
                Some((s.qualified_name, t.qualified_name))
            })
            .collect()
    }

    /// Get all import edges
    fn get_imports(&self) -> Vec<(StrKey, StrKey)> {
        self.all_import_edges()
            .iter()
            .filter_map(|&(src, tgt)| {
                let s = self.node_idx(src)?;
                let t = self.node_idx(tgt)?;
                Some((s.qualified_name, t.qualified_name))
            })
            .collect()
    }

    /// Get inheritance edges
    fn get_inheritance(&self) -> Vec<(StrKey, StrKey)> {
        self.all_inheritance_edges()
            .iter()
            .filter_map(|&(src, tgt)| {
                let s = self.node_idx(src)?;
                let t = self.node_idx(tgt)?;
                Some((s.qualified_name, t.qualified_name))
            })
            .collect()
    }

    /// Get child classes
    fn get_child_classes(&self, qn: &str) -> Vec<CodeNode> {
        let Some((idx, _)) = self.node_by_name_idx(qn) else {
            return vec![];
        };
        self.child_classes_idx(idx)
            .iter()
            .filter_map(|&ci| self.node_idx(ci).copied())
            .collect()
    }

    /// Get files that import this file
    fn get_importers(&self, qn: &str) -> Vec<CodeNode> {
        let Some((idx, _)) = self.node_by_name_idx(qn) else {
            return vec![];
        };
        self.importers_idx(idx)
            .iter()
            .filter_map(|&ci| self.node_idx(ci).copied())
            .collect()
    }

    /// Find import cycles
    fn find_import_cycles(&self) -> Vec<Vec<String>> {
        let si = self.interner();
        self.import_cycles_idx()
            .iter()
            .map(|cycle| {
                let mut names: Vec<String> = cycle
                    .iter()
                    .filter_map(|&idx| self.node_idx(idx))
                    .map(|n| si.resolve(n.qualified_name).to_string())
                    .collect();
                names.sort();
                names
            })
            .collect()
    }

    // ==================== Derived convenience methods ====================

    /// Get all functions as shared Arc — Arc::clone is ~10ns vs Vec::clone ~50ms.
    fn get_functions_shared(&self) -> Arc<[CodeNode]> {
        Arc::from(self.get_functions())
    }

    /// Get all classes as shared Arc.
    fn get_classes_shared(&self) -> Arc<[CodeNode]> {
        Arc::from(self.get_classes())
    }

    /// Get all files as shared Arc.
    fn get_files_shared(&self) -> Arc<[CodeNode]> {
        Arc::from(self.get_files())
    }

    /// Get all call edges as shared Arc — avoids cloning 296K+ (StrKey, StrKey) pairs.
    fn get_calls_shared(&self) -> Arc<[(StrKey, StrKey)]> {
        Arc::from(self.get_calls())
    }

    /// Check if a file participates in any import cycle.
    fn is_in_import_cycle(&self, file_path: &str) -> bool {
        let cycles = self.find_import_cycles();
        cycles.iter().any(|cycle| {
            cycle
                .iter()
                .any(|qn| qn == file_path || file_path.contains(qn.as_str()))
        })
    }

    /// Find the function containing a specific line in a file.
    fn find_function_at(&self, file_path: &str, line: u32) -> Option<CodeNode> {
        // Try indexed path first (CodeGraph), fall back to string-based scan
        if let Some(idx) = self.function_at_idx(file_path, line) {
            return self.node_idx(idx).copied();
        }
        self.get_functions_in_file(file_path)
            .into_iter()
            .find(|f| f.line_start <= line && f.line_end >= line)
    }

    /// Get complex functions (complexity > threshold)
    fn get_complex_functions(&self, min_complexity: i64) -> Vec<CodeNode> {
        self.functions_idx()
            .iter()
            .filter_map(|&idx| {
                let node = self.node_idx(idx)?;
                if node.complexity_opt().is_some_and(|c| c >= min_complexity) {
                    Some(*node)
                } else {
                    None
                }
            })
            .collect()
    }

    /// Get long parameter functions
    fn get_long_param_functions(&self, min_params: i64) -> Vec<CodeNode> {
        self.functions_idx()
            .iter()
            .filter_map(|&idx| {
                let node = self.node_idx(idx)?;
                if node.param_count_opt().is_some_and(|p| p >= min_params) {
                    Some(*node)
                } else {
                    None
                }
            })
            .collect()
    }

    /// Count unique files of callers.
    fn caller_file_spread(&self, qn: &str) -> usize {
        let Some((idx, _)) = self.node_by_name_idx(qn) else {
            return 0;
        };
        let files: std::collections::HashSet<StrKey> = self
            .callers_idx(idx)
            .iter()
            .filter_map(|&ci| self.node_idx(ci))
            .map(|n| n.file_path)
            .collect();
        files.len()
    }

    /// Count callers of `qn` that are OUTSIDE a given class boundary.
    fn count_external_callers_of(
        &self,
        qn: &str,
        class_file: &str,
        class_start: u32,
        class_end: u32,
    ) -> usize {
        let si = self.interner();
        let Some((idx, _)) = self.node_by_name_idx(qn) else {
            return 0;
        };
        self.callers_idx(idx)
            .iter()
            .filter_map(|&ci| self.node_idx(ci))
            .filter(|c| {
                si.resolve(c.file_path) != class_file
                    || c.line_start < class_start
                    || c.line_end > class_end
            })
            .count()
    }

    /// Count unique modules (parent directories) of callers.
    fn caller_module_spread(&self, qn: &str) -> usize {
        let si = self.interner();
        let Some((idx, _)) = self.node_by_name_idx(qn) else {
            return 0;
        };
        let modules: std::collections::HashSet<&str> = self
            .callers_idx(idx)
            .iter()
            .filter_map(|&ci| self.node_idx(ci))
            .map(|n| {
                std::path::Path::new(si.resolve(n.file_path))
                    .parent()
                    .and_then(|p| p.to_str())
                    .unwrap_or("root")
            })
            .collect();
        modules.len()
    }

    /// Build call maps as (qn_to_idx, callers_idx, callees_idx).
    ///
    /// Returns index-based maps where indices correspond to positions in `get_functions()`.
    fn build_call_maps_raw(
        &self,
    ) -> (
        HashMap<StrKey, usize>,
        HashMap<usize, Vec<usize>>,
        HashMap<usize, Vec<usize>>,
    ) {
        let functions = self.get_functions();
        let calls = self.get_calls();
        let qn_to_idx: HashMap<StrKey, usize> = functions
            .iter()
            .enumerate()
            .map(|(i, f)| (f.qualified_name, i))
            .collect();
        let mut callers: HashMap<usize, Vec<usize>> = HashMap::new();
        let mut callees: HashMap<usize, Vec<usize>> = HashMap::new();
        for (caller, callee) in &calls {
            if let (Some(&from), Some(&to)) = (qn_to_idx.get(caller), qn_to_idx.get(callee)) {
                callers.entry(to).or_default().push(from);
                callees.entry(from).or_default().push(to);
            }
        }
        (qn_to_idx, callers, callees)
    }

    /// Build call adjacency lists: (forward_adj, reverse_adj, qn_to_idx).
    ///
    /// Indices correspond to positions in `get_functions()`.
    fn get_call_adjacency(&self) -> (Vec<Vec<usize>>, Vec<Vec<usize>>, HashMap<StrKey, usize>) {
        let functions = self.get_functions();
        let calls = self.get_calls();
        let qn_to_idx: HashMap<StrKey, usize> = functions
            .iter()
            .enumerate()
            .map(|(i, f)| (f.qualified_name, i))
            .collect();
        let n = functions.len();
        let mut adj = vec![vec![]; n];
        let mut rev_adj = vec![vec![]; n];
        for (caller, callee) in &calls {
            if let (Some(&from), Some(&to)) = (qn_to_idx.get(caller), qn_to_idx.get(callee)) {
                adj[from].push(to);
                rev_adj[to].push(from);
            }
        }
        (adj, rev_adj, qn_to_idx)
    }
}

// Blanket impl — every GraphQuery type automatically gets the extension methods.
// `?Sized` ensures this covers `dyn GraphQuery` as well as concrete types.
impl<T: GraphQuery + ?Sized> GraphQueryExt for T {}