go_brrr/
lib.rs

1#![feature(portable_simd)]
2
3//! llm-brrr - Token-efficient code analysis for LLMs.
4//!
5//! This library provides tools for extracting and analyzing code structure
6//! using tree-sitter parsers for multiple languages. It enables 95% token
7//! savings when analyzing codebases by providing structured summaries instead
8//! of raw source code.
9//!
10//! # Architecture
11//!
12//! The library is organized into several layers:
13//!
14//! - **AST Layer** ([`ast`]): File tree generation, code structure extraction, and AST parsing
15//! - **CFG Layer** ([`cfg`]): Control flow graph extraction with cyclomatic complexity
16//! - **DFG Layer** ([`dfg`]): Data flow graph extraction and basic program slicing
17//! - **PDG Layer** ([`pdg`]): Program Dependence Graph combining CFG + DFG for accurate slicing
18//! - **Call Graph Layer** ([`callgraph`]): Cross-file call graph analysis and impact detection
19//! - **Semantic Layer** ([`semantic`]): Semantic pattern detection, embedding unit extraction, and code enrichment
20//! - **Language Layer** ([`lang`]): Multi-language support via tree-sitter
21//!
22//! # Quick Start
23//!
24//! ```no_run
25//! use go_brrr::{get_tree, get_tree_default, get_structure, extract_file, get_cfg, get_slice, get_backward_slice};
26//!
27//! // Get file tree for a project (convenience wrapper with default options)
28//! let tree = get_tree_default("./src", Some(".py"))?;
29//!
30//! // Or use full API with explicit options
31//! let tree_full = get_tree("./src", Some(".py"), true, true)?;
32//!
33//! // Get code structure (functions, classes) summary
34//! let structure = get_structure("./src", Some("python"), 100, true)?;
35//!
36//! // Extract full AST from a single file (None = no base path validation)
37//! let module = extract_file("./src/main.py", None)?;
38//!
39//! // Get control flow graph for a function
40//! let cfg = get_cfg("./src/main.py", "process_data")?;
41//!
42//! // Get program slice (what affects line 42?) - using convenience wrapper
43//! let affected_lines = get_backward_slice("./src/main.py", "process_data", 42)?;
44//!
45//! // Or use full API with direction, variable, and language options
46//! let forward = get_slice("./src/main.py", "process_data", 10, Some("forward"), None, None)?;
47//! let var_slice = get_slice("./src/main.py", "process_data", 42, None, Some("x"), None)?;
48//! # Ok::<(), go_brrr::BrrrError>(())
49//! ```
50//!
51//! # Call Graph Analysis
52//!
53//! ```no_run
54//! use go_brrr::{build_callgraph, get_impact, find_dead_code, get_context};
55//!
56//! // Build project-wide call graph
57//! let graph = build_callgraph("./src")?;
58//!
59//! // Find all callers of a function (impact analysis)
60//! let callers = get_impact("./src", "critical_function", 3)?;
61//!
62//! // Find unreachable/dead code
63//! let dead = find_dead_code("./src")?;
64//!
65//! // Get LLM-ready context for an entry point
66//! let context = get_context("./src", "main", 2)?;
67//! # Ok::<(), go_brrr::BrrrError>(())
68//! ```
69//!
70//! # Project Scanning
71//!
72//! ```no_run
73//! use go_brrr::{scan_project_files, scan_extensions, get_project_metadata, ScanConfig, scan_with_config};
74//!
75//! // Scan all source files (respects .gitignore and .brrrignore)
76//! let result = scan_project_files("./project", None, true)?;
77//! println!("Found {} files", result.files.len());
78//!
79//! // Scan only Python files
80//! let py_result = scan_project_files("./project", Some("python"), true)?;
81//!
82//! // Scan by file extension
83//! let rs_files = scan_extensions("./project", &[".rs", ".toml"])?;
84//!
85//! // Get file metadata (size, modification time, language)
86//! let metadata = get_project_metadata("./project", None)?;
87//! for meta in &metadata {
88//!     println!("{}: {} bytes", meta.path.display(), meta.size);
89//! }
90//!
91//! // Advanced: custom scan configuration
92//! let config = ScanConfig::for_language("python")
93//!     .with_excludes(&["**/test/**"])
94//!     .with_metadata();
95//! let result = scan_with_config("./project", &config)?;
96//! # Ok::<(), go_brrr::BrrrError>(())
97//! ```
98//!
99//! # Semantic Pattern Detection
100//!
101//! Automatically detect semantic patterns in code for enriched embeddings:
102//!
103//! ```
104//! use go_brrr::{detect_semantic_patterns, SemanticPattern, SEMANTIC_PATTERNS};
105//!
106//! // Detect patterns in code
107//! let code = "def validate_user(user): assert user is not None";
108//! let patterns = detect_semantic_patterns(code);
109//! assert!(patterns.contains(&"validation".to_string()));
110//!
111//! // Access all available patterns
112//! for pattern in SEMANTIC_PATTERNS {
113//!     println!("Pattern: {} - {}", pattern.name, pattern.pattern);
114//! }
115//! ```
116//!
117//! Detected patterns include: `crud`, `validation`, `transform`, `error_handling`,
118//! `async_ops`, `iteration`, `api_endpoint`, `database`, `auth`, `cache`, `test`,
119//! `logging`, and `config`.
120
121// =============================================================================
122// Module Declarations
123// =============================================================================
124
125pub mod ast;
126pub mod callgraph;
127pub mod cfg;
128pub mod dfg;
129pub mod embedding;
130pub mod error;
131pub mod metrics;
132pub mod patterns;
133pub mod pdg;
134pub mod quality;
135pub mod security;
136pub mod semantic;
137pub mod simd;
138pub mod util;
139
140/// Language support module with implementations for all supported languages.
141pub mod lang {
142    pub mod c;
143    pub mod cpp;
144    pub mod go;
145    pub mod java;
146    pub mod python;
147    pub mod registry;
148    pub mod rust_lang;
149    pub mod traits;
150    pub mod typescript;
151
152    pub use registry::LanguageRegistry;
153    pub use traits::{BoxedLanguage, Language};
154}
155
156// =============================================================================
157// Public Type Re-exports
158// =============================================================================
159
160// Error types - most important for users
161pub use error::{Result, BrrrError};
162
163// AST types and utilities
164pub use ast::{
165    CallGraphInfo, ClassInfo, ClassSummary, CodeStructure, FieldInfo, FileTreeEntry, FunctionInfo,
166    FunctionSummary, ImportInfo, ModuleInfo,
167};
168
169// AST extractor for direct access to parsing functionality
170pub use ast::AstExtractor;
171
172// AST cache management - allows consumers to free memory
173pub use ast::{clear_parser_cache, clear_query_cache};
174
175// Import extraction utility
176pub use ast::extract_imports;
177
178// CFG types
179pub use cfg::{BlockId, BlockType, CFGBlock, CFGEdge, CFGError, CFGInfo, EdgeType};
180
181// CFG rendering functions
182pub use cfg::{
183    to_ascii as cfg_to_ascii, to_dot as cfg_to_dot, to_json as cfg_to_json,
184    to_json_compact as cfg_to_json_compact, to_mermaid as cfg_to_mermaid,
185};
186
187// DFG types
188pub use dfg::{DFGInfo, DataflowEdge, DataflowKind};
189
190// PDG types (combines CFG + DFG for accurate slicing)
191pub use pdg::{
192    BranchType, ControlDependence, PDGInfo, SliceCriteria, SliceMetrics, SliceResult,
193};
194// PDG slicing functions for advanced use cases (when you want to build PDG once and slice multiple times)
195pub use pdg::{backward_slice as pdg_backward_slice, forward_slice as pdg_forward_slice};
196
197// Metrics types
198pub use metrics::{
199    analyze_complexity, analyze_file_complexity, ComplexityAnalysis, ComplexityStats,
200    CyclomaticComplexity, FunctionComplexity, RiskLevel,
201};
202
203// Nesting depth metrics
204pub use metrics::{
205    analyze_nesting, analyze_file_nesting, NestingMetrics, NestingAnalysis,
206    NestingStats, FunctionNesting, NestingDepthLevel, NestingConstruct,
207    DeepNesting, NestingAnalysisError,
208};
209
210// Call graph types
211pub use callgraph::{CallEdge, CallGraph, FunctionRef};
212
213// Function index types for fast lookups
214pub use callgraph::{FunctionDef, FunctionIndex, IndexStats};
215
216// Scanner types for project file discovery
217pub use callgraph::scanner::{
218    ErrorHandling, FileMetadata, ProjectScanner, ScanConfig, ScanError, ScanErrorKind, ScanResult,
219};
220
221// Dead code analysis types
222pub use callgraph::{
223    analyze_dead_code, analyze_dead_code_with_config, DeadCodeConfig, DeadCodeResult,
224    DeadCodeStats, DeadFunction, DeadReason,
225};
226
227// Entry point detection
228pub use callgraph::{classify_entry_point, detect_entry_points_with_config, EntryPointKind};
229
230// Impact analysis types (reverse call graph)
231pub use callgraph::{analyze_impact, CallerInfo, ImpactConfig, ImpactResult};
232
233// Architecture analysis types
234pub use callgraph::{analyze_architecture, ArchAnalysis, ArchStats, CycleDependency};
235
236// Call graph cache utilities
237pub use callgraph::{
238    get_cache_dir, get_cache_file, get_or_build_graph_with_config,
239    invalidate_cache, warm_cache_with_config, CachedCallGraph, CachedEdge,
240};
241
242// Language types
243pub use lang::{BoxedLanguage, Language, LanguageRegistry};
244
245// Semantic types
246pub use semantic::{
247    ChunkInfo, CodeComplexity, CodeLocation, ContentHashedIndex, EmbeddingUnit, SearchResult,
248    SemanticPattern, UnitKind, CHUNK_OVERLAP_TOKENS, MAX_CODE_PREVIEW_TOKENS, MAX_EMBEDDING_TOKENS,
249    SEMANTIC_PATTERNS,
250};
251
252// Embedding types
253pub use embedding::{
254    distances_to_scores, distances_to_scores_for_metric, is_normalized, normalize_vector,
255    IndexConfig, Metric, Quantization, VectorIndex,
256};
257
258// Security analysis types - Command Injection
259pub use security::injection::command::{
260    CommandInjectionFinding, CommandSink, Confidence, InjectionKind,
261    Severity as CommandSeverity, SourceLocation, TaintSource, TaintSourceKind,
262    scan_command_injection, scan_file_command_injection,
263};
264
265// Security analysis types - SQL Injection
266pub use security::injection::sql::{
267    Location as SqlLocation, SQLInjectionFinding, ScanResult as SqlScanResult,
268    Severity as SqlSeverity, SqlInjectionDetector, SqlSinkType, UnsafePattern,
269};
270
271// Security analysis types - Path Traversal
272pub use security::injection::path_traversal::{
273    Confidence as PathTraversalConfidence, FileOperationType, FileSink,
274    PathTraversalFinding, ScanResult as PathTraversalScanResult,
275    Severity as PathTraversalSeverity, SourceLocation as PathTraversalLocation,
276    VulnerablePattern as PathTraversalPattern,
277    scan_path_traversal, scan_file_path_traversal, get_file_sinks,
278};
279
280// Security analysis types - Weak Cryptography
281pub use security::crypto::{
282    Algorithm as CryptoAlgorithm, Confidence as CryptoConfidence,
283    Location as CryptoLocation, ScanResult as CryptoScanResult,
284    Severity as CryptoSeverity, UsageContext as CryptoUsageContext,
285    WeakCryptoDetector, WeakCryptoFinding, WeakCryptoIssue,
286    scan_weak_crypto, scan_file_weak_crypto,
287};
288
289// Unified Security API (runs all analyzers in parallel)
290pub use security::{
291    scan_security, Confidence as UnifiedConfidence, InjectionType,
292    Location as UnifiedLocation, ScanSummary, SecurityCategory, SecurityConfig,
293    SecurityFinding, SecurityReport, Severity as UnifiedSeverity,
294    check_suppression, is_suppressed,
295};
296
297// SARIF output format for CI/CD integration
298pub use security::sarif::SarifLog;
299
300// Code quality types - Clone Detection
301pub use quality::clones::{
302    detect_clones, format_clone_summary, Clone, CloneAnalysis, CloneConfig, CloneError,
303    CloneInstance, CloneStats, CloneType, TextualCloneDetector,
304};
305
306// Design pattern detection types
307pub use patterns::{
308    detect_patterns, format_pattern_summary, DesignPattern, Location as PatternLocation,
309    PatternAnalysis, PatternCategory, PatternConfig, PatternDetector, PatternError,
310    PatternMatch, PatternStats,
311};
312
313// =============================================================================
314// Context Types for LLM API Parity
315// =============================================================================
316
317use serde::{Deserialize, Serialize};
318use std::path::Path;
319
320/// Context about a function including its code and metadata.
321///
322/// This struct provides a complete context for a single function, suitable for
323/// LLM consumption. It matches Python's `FunctionContext` dataclass for API parity.
324///
325/// # Examples
326///
327/// ```no_run
328/// use go_brrr::{FunctionContext, extract_file};
329///
330/// // Create from extracted function info
331/// let module = extract_file("./src/main.py", None)?;
332/// if let Some(func) = module.functions.first() {
333///     let source = std::fs::read_to_string("./src/main.py")?;
334///     let ctx = FunctionContext::from_function_info(func, "./src/main.py", &source, "python");
335///     println!("Function: {} at lines {}-{}", ctx.name, ctx.start_line, ctx.end_line);
336/// }
337/// # Ok::<(), Box<dyn std::error::Error>>(())
338/// ```
339#[derive(Debug, Clone, Serialize, Deserialize)]
340pub struct FunctionContext {
341    /// Function name
342    pub name: String,
343    /// File path containing the function
344    pub file: String,
345    /// Start line number (1-indexed)
346    pub start_line: usize,
347    /// End line number (1-indexed)
348    pub end_line: usize,
349    /// Function signature (e.g., "def foo(x: int) -> str")
350    pub signature: Option<String>,
351    /// Docstring or documentation comment
352    pub docstring: Option<String>,
353    /// Full source code of the function
354    pub source: String,
355    /// Programming language (e.g., "python", "rust", "go")
356    pub language: String,
357}
358
359impl FunctionContext {
360    /// Create a `FunctionContext` from a `FunctionInfo` and source file content.
361    ///
362    /// Extracts the function source code from the full file content using
363    /// the line number information in `FunctionInfo`.
364    ///
365    /// # Arguments
366    ///
367    /// * `info` - The function info from AST extraction
368    /// * `file_path` - Path to the source file
369    /// * `source` - Full source content of the file
370    /// * `language` - Programming language identifier
371    ///
372    /// # Examples
373    ///
374    /// ```no_run
375    /// use go_brrr::{FunctionContext, extract_file, FunctionInfo};
376    ///
377    /// let module = extract_file("./src/lib.rs", None)?;
378    /// let source = std::fs::read_to_string("./src/lib.rs")?;
379    ///
380    /// for func in &module.functions {
381    ///     let ctx = FunctionContext::from_function_info(func, "./src/lib.rs", &source, "rust");
382    ///     println!("{}: {} lines", ctx.name, ctx.end_line - ctx.start_line + 1);
383    /// }
384    /// # Ok::<(), Box<dyn std::error::Error>>(())
385    /// ```
386    pub fn from_function_info(
387        info: &FunctionInfo,
388        file_path: &str,
389        source: &str,
390        language: &str,
391    ) -> Self {
392        let start = info.line_number.saturating_sub(1);
393        let end = info.end_line_number.unwrap_or(info.line_number);
394        let lines: Vec<&str> = source.lines().collect();
395        let func_source = lines
396            .get(start..end)
397            .map(|ls| ls.join("\n"))
398            .unwrap_or_default();
399
400        Self {
401            name: info.name.clone(),
402            file: file_path.to_string(),
403            start_line: info.line_number,
404            end_line: end,
405            signature: Some(info.signature()),
406            docstring: info.docstring.clone(),
407            source: func_source,
408            language: language.to_string(),
409        }
410    }
411
412    /// Create a minimal context with just name and file location.
413    ///
414    /// Useful when you have limited information about a function reference.
415    pub fn minimal(name: &str, file: &str, line: usize, language: &str) -> Self {
416        Self {
417            name: name.to_string(),
418            file: file.to_string(),
419            start_line: line,
420            end_line: line,
421            signature: None,
422            docstring: None,
423            source: String::new(),
424            language: language.to_string(),
425        }
426    }
427}
428
429/// Relevant context for LLM consumption with call graph information.
430///
431/// This struct aggregates function context for an entry point along with
432/// its direct callees and callers, providing a complete picture for LLM analysis.
433/// Matches Python's `RelevantContext` dataclass for API parity.
434///
435/// # Examples
436///
437/// ```no_run
438/// use go_brrr::{RelevantContext, FunctionContext};
439///
440/// // Build context programmatically
441/// let entry = FunctionContext::minimal("main", "src/main.rs", 10, "rust");
442/// let callee = FunctionContext::minimal("helper", "src/utils.rs", 25, "rust");
443///
444/// let context = RelevantContext {
445///     entry,
446///     callees: vec![callee],
447///     callers: vec![],
448///     token_count: 500,
449///     depth: 1,
450/// };
451///
452/// println!("Entry: {} calls {} functions", context.entry.name, context.callees.len());
453/// ```
454#[derive(Debug, Clone, Serialize, Deserialize)]
455pub struct RelevantContext {
456    /// The entry point function being analyzed
457    pub entry: FunctionContext,
458    /// Functions called by the entry point (direct dependencies)
459    pub callees: Vec<FunctionContext>,
460    /// Functions that call the entry point (reverse dependencies)
461    pub callers: Vec<FunctionContext>,
462    /// Estimated token count for LLM context window budgeting
463    pub token_count: usize,
464    /// Depth of call graph traversal used
465    pub depth: usize,
466}
467
468impl RelevantContext {
469    /// Create a new RelevantContext with just an entry point.
470    ///
471    /// Callees and callers are initialized empty; use builder pattern to add them.
472    pub fn new(entry: FunctionContext, depth: usize) -> Self {
473        Self {
474            entry,
475            callees: Vec::new(),
476            callers: Vec::new(),
477            token_count: 0,
478            depth,
479        }
480    }
481
482    /// Add a callee (function called by entry point).
483    pub fn with_callee(mut self, callee: FunctionContext) -> Self {
484        self.callees.push(callee);
485        self
486    }
487
488    /// Add a caller (function that calls the entry point).
489    pub fn with_caller(mut self, caller: FunctionContext) -> Self {
490        self.callers.push(caller);
491        self
492    }
493
494    /// Set the token count estimate.
495    pub fn with_token_count(mut self, count: usize) -> Self {
496        self.token_count = count;
497        self
498    }
499
500    /// Calculate approximate token count based on source code length.
501    ///
502    /// Uses a rough estimate of 4 characters per token.
503    pub fn estimate_tokens(&mut self) {
504        let mut total_chars = self.entry.source.len();
505        for callee in &self.callees {
506            total_chars += callee.source.len();
507        }
508        for caller in &self.callers {
509            total_chars += caller.source.len();
510        }
511        self.token_count = total_chars / 4;
512    }
513
514    /// Total number of functions in this context.
515    pub fn function_count(&self) -> usize {
516        1 + self.callees.len() + self.callers.len()
517    }
518
519    /// Format context as a string suitable for LLM prompts.
520    ///
521    /// Produces a structured representation showing the entry point
522    /// and its relationships to other functions.
523    pub fn to_llm_string(&self) -> String {
524        let mut output = String::new();
525        output.push_str(&format!(
526            "## Code Context: {} (depth={})\n\n",
527            self.entry.name, self.depth
528        ));
529
530        // Entry point
531        let short_file = Path::new(&self.entry.file)
532            .file_name()
533            .map(|f| f.to_string_lossy().to_string())
534            .unwrap_or_else(|| self.entry.file.clone());
535        output.push_str(&format!(
536            "### Entry Point: {} ({}:{})\n",
537            self.entry.name, short_file, self.entry.start_line
538        ));
539        if let Some(sig) = &self.entry.signature {
540            output.push_str(&format!("```\n{}\n```\n", sig));
541        }
542        if let Some(doc) = &self.entry.docstring {
543            let first_line = doc.lines().next().unwrap_or("");
544            output.push_str(&format!("> {}\n", first_line));
545        }
546        output.push('\n');
547
548        // Callees
549        if !self.callees.is_empty() {
550            output.push_str("### Calls:\n");
551            for callee in &self.callees {
552                let short = Path::new(&callee.file)
553                    .file_name()
554                    .map(|f| f.to_string_lossy().to_string())
555                    .unwrap_or_else(|| callee.file.clone());
556                output.push_str(&format!("- {} ({}:{})\n", callee.name, short, callee.start_line));
557            }
558            output.push('\n');
559        }
560
561        // Callers
562        if !self.callers.is_empty() {
563            output.push_str("### Called By:\n");
564            for caller in &self.callers {
565                let short = Path::new(&caller.file)
566                    .file_name()
567                    .map(|f| f.to_string_lossy().to_string())
568                    .unwrap_or_else(|| caller.file.clone());
569                output.push_str(&format!("- {} ({}:{})\n", caller.name, short, caller.start_line));
570            }
571            output.push('\n');
572        }
573
574        output.push_str(&format!(
575            "Token estimate: {} | Functions: {}\n",
576            self.token_count,
577            self.function_count()
578        ));
579
580        output
581    }
582}
583
584// =============================================================================
585// Source Input Types
586// =============================================================================
587
588/// Input that can be either a file path or source code string.
589///
590/// This enum provides flexibility for analysis functions, allowing them to
591/// accept either a file path (which will be read and language-detected) or
592/// source code directly with an explicit language hint.
593///
594/// # Examples
595///
596/// ```no_run
597/// use go_brrr::SourceInput;
598///
599/// // From a file path
600/// let from_file = SourceInput::Path("./src/main.py");
601///
602/// // From source code with language hint
603/// let from_source = SourceInput::Source {
604///     code: "def hello(): return 'world'",
605///     language: "python",
606/// };
607/// ```
608#[derive(Debug, Clone)]
609pub enum SourceInput<'a> {
610    /// Path to a source file (language auto-detected from extension).
611    Path(&'a str),
612    /// Source code string with explicit language hint.
613    Source {
614        /// The source code as a string.
615        code: &'a str,
616        /// Language identifier (e.g., "python", "typescript", "rust").
617        language: &'a str,
618    },
619}
620
621impl<'a> SourceInput<'a> {
622    /// Resolve input to (source_bytes, language_trait, optional_path).
623    ///
624    /// For `Path` variant: reads file, detects language from extension.
625    /// For `Source` variant: uses provided code and language directly.
626    ///
627    /// # Returns
628    ///
629    /// A tuple of:
630    /// - `Vec<u8>`: The source code as bytes
631    /// - `&'static dyn Language`: The language implementation
632    /// - `Option<&'a str>`: The file path if `Path` variant was used
633    ///
634    /// # Errors
635    ///
636    /// - [`BrrrError::UnsupportedLanguage`] if language cannot be determined
637    /// - [`BrrrError::Io`] if file cannot be read (for `Path` variant)
638    pub fn resolve(&self) -> Result<(Vec<u8>, &'static dyn Language, Option<&'a str>)> {
639        let registry = LanguageRegistry::global();
640
641        match self {
642            SourceInput::Path(path) => {
643                let p = Path::new(path);
644                let lang = registry.detect_language(p).ok_or_else(|| {
645                    BrrrError::UnsupportedLanguage(
646                        p.extension()
647                            .map(|e| e.to_string_lossy().to_string())
648                            .unwrap_or_else(|| "unknown".to_string()),
649                    )
650                })?;
651                let source = std::fs::read(p)
652                    .map_err(|e| BrrrError::io_with_path(e, p))?;
653                Ok((source, lang, Some(*path)))
654            }
655            SourceInput::Source { code, language } => {
656                let lang = registry
657                    .get_by_name(language)
658                    .ok_or_else(|| BrrrError::UnsupportedLanguage((*language).to_string()))?;
659                Ok((code.as_bytes().to_vec(), lang, None))
660            }
661        }
662    }
663}
664
665// =============================================================================
666// High-Level Public API Functions
667// =============================================================================
668
669/// Get the file tree for a directory.
670///
671/// Generates a hierarchical tree structure suitable for JSON serialization.
672/// Skips common build directories (node_modules, __pycache__, .git, etc.).
673///
674/// # Arguments
675///
676/// * `path` - Root directory path to scan
677/// * `ext_filter` - Optional extension filter (e.g., `Some(".py")` for Python files only)
678/// * `exclude_hidden` - If true, exclude hidden files/directories (those starting with '.')
679/// * `respect_ignore` - If true, respect .gitignore and .brrrignore patterns
680///
681/// # Returns
682///
683/// A [`FileTreeEntry`] representing the root directory with nested children.
684///
685/// # Examples
686///
687/// ```no_run
688/// use go_brrr::get_tree;
689///
690/// // Get all files with default settings (exclude hidden, respect ignore)
691/// let tree = get_tree("./project", None, true, true)?;
692/// println!("Root: {}", tree.name);
693/// for child in &tree.children {
694///     println!("  {} (type: {})", child.name, child.entry_type);
695/// }
696///
697/// // Get only Python files, including hidden files
698/// let py_tree = get_tree("./project", Some(".py"), false, true)?;
699///
700/// // Get all files, ignoring .gitignore patterns
701/// let all_tree = get_tree("./project", None, true, false)?;
702/// # Ok::<(), go_brrr::BrrrError>(())
703/// ```
704///
705/// # Errors
706///
707/// Returns [`BrrrError::Io`] if the path cannot be read or does not exist.
708pub fn get_tree(
709    path: &str,
710    ext_filter: Option<&str>,
711    exclude_hidden: bool,
712    respect_ignore: bool,
713) -> Result<FileTreeEntry> {
714    let ext_vec: Vec<String> = ext_filter
715        .map(|e| vec![e.to_string()])
716        .unwrap_or_default();
717    // Map parameters to ast::file_tree conventions:
718    // - show_hidden = !exclude_hidden (show_hidden=false means exclude hidden files)
719    // - no_ignore = !respect_ignore (no_ignore=false means respect ignore patterns)
720    let show_hidden = !exclude_hidden;
721    let no_ignore = !respect_ignore;
722    ast::file_tree(path, &ext_vec, show_hidden, no_ignore, None)
723}
724
725/// Get file tree with default options (convenience wrapper).
726///
727/// Equivalent to calling `get_tree(path, ext_filter, true, true)`:
728/// - Excludes hidden files/directories
729/// - Respects .gitignore and .brrrignore patterns
730///
731/// # Arguments
732///
733/// * `path` - Root directory path to scan
734/// * `ext_filter` - Optional extension filter (e.g., `Some(".py")` for Python files only)
735///
736/// # Examples
737///
738/// ```no_run
739/// use go_brrr::get_tree_default;
740///
741/// let tree = get_tree_default("./project", None)?;
742/// let py_tree = get_tree_default("./project", Some(".py"))?;
743/// # Ok::<(), go_brrr::BrrrError>(())
744/// ```
745#[inline]
746pub fn get_tree_default(path: &str, ext_filter: Option<&str>) -> Result<FileTreeEntry> {
747    get_tree(path, ext_filter, true, true)
748}
749
750/// Get code structure summary for a project.
751///
752/// Scans the directory for source files matching the language filter,
753/// extracts function and class information using parallel processing,
754/// and returns a summary suitable for LLM consumption.
755///
756/// # Arguments
757///
758/// * `path` - Root directory to scan
759/// * `lang_filter` - Optional language filter (e.g., `Some("python")`, `Some("typescript")`)
760/// * `max_results` - Maximum number of functions/classes to return (0 = unlimited)
761/// * `respect_ignore` - If true, respect .gitignore and .brrrignore patterns
762///
763/// # Returns
764///
765/// A [`CodeStructure`] containing:
766/// - `path`: The analyzed path
767/// - `functions`: List of function summaries with file, line, and signature
768/// - `classes`: List of class summaries with file, line, and method count
769/// - `total_files`: Total number of files analyzed
770///
771/// # Examples
772///
773/// ```no_run
774/// use go_brrr::get_structure;
775///
776/// // Get all functions and classes (any language), respecting ignore files
777/// let structure = get_structure("./src", None, 0, true)?;
778/// println!("Found {} functions in {} files", structure.functions.len(), structure.total_files);
779///
780/// // Get only Python code, limited to 50 results, ignoring .gitignore
781/// let py_structure = get_structure("./src", Some("python"), 50, false)?;
782/// for func in &py_structure.functions {
783///     println!("{}:{} - {}", func.file, func.line, func.signature);
784/// }
785/// # Ok::<(), go_brrr::BrrrError>(())
786/// ```
787///
788/// # Errors
789///
790/// Returns [`BrrrError::Io`] if the path does not exist or cannot be read.
791pub fn get_structure(
792    path: &str,
793    lang_filter: Option<&str>,
794    max_results: usize,
795    respect_ignore: bool,
796) -> Result<CodeStructure> {
797    // no_ignore is the inverse of respect_ignore
798    let no_ignore = !respect_ignore;
799    ast::code_structure(path, lang_filter, max_results, no_ignore)
800}
801
802/// Get code structure with default options.
803///
804/// Convenience wrapper that calls [`get_structure`] with `respect_ignore=true`.
805/// This maintains backward compatibility with the original API.
806///
807/// # Arguments
808///
809/// * `path` - Project root directory
810/// * `lang_filter` - Optional language filter
811/// * `max_results` - Maximum files to process
812#[inline]
813pub fn get_structure_default(
814    path: &str,
815    lang_filter: Option<&str>,
816    max_results: usize,
817) -> Result<CodeStructure> {
818    get_structure(path, lang_filter, max_results, true)
819}
820
821/// Extract complete AST information from a source file with optional path containment validation.
822///
823/// Parses a single file and returns detailed information about all
824/// functions, classes, and imports. This is the most detailed extraction
825/// API, suitable for deep analysis of individual files.
826///
827/// # Arguments
828///
829/// * `file_path` - Path to the source file
830/// * `base_path` - Optional base directory for security validation.
831///   If provided, `file_path` must resolve to a location within `base_path`.
832///
833/// # Returns
834///
835/// A [`ModuleInfo`] containing:
836/// - `path`: The file path
837/// - `language`: Detected language (e.g., "python", "typescript")
838/// - `functions`: All top-level functions with full details
839/// - `classes`: All classes/structs with methods
840/// - `imports`: All import statements
841///
842/// # Examples
843///
844/// ```no_run
845/// use go_brrr::extract_file;
846///
847/// // Without path containment validation
848/// let module = extract_file("./src/main.py", None)?;
849/// println!("Language: {}", module.language);
850///
851/// // With path containment validation - prevents directory traversal
852/// let module = extract_file("./src/main.py", Some("./src"))?;
853///
854/// for func in &module.functions {
855///     println!("Function: {} at line {}", func.name, func.line_number);
856///     println!("  Signature: {}", func.signature());
857///     if let Some(doc) = &func.docstring {
858///         println!("  Docstring: {}", doc);
859///     }
860/// }
861///
862/// for class in &module.classes {
863///     println!("Class: {} with {} methods", class.name, class.methods.len());
864/// }
865/// # Ok::<(), go_brrr::BrrrError>(())
866/// ```
867///
868/// # Security
869///
870/// When `base_path` is provided, this function validates that `file_path`
871/// does not escape the base directory via:
872/// - Directory traversal (../..)
873/// - Symlink resolution
874/// - Absolute paths outside base
875///
876/// # Errors
877///
878/// - [`BrrrError::Io`] if the file cannot be read
879/// - [`BrrrError::UnsupportedLanguage`] if the file type is not recognized
880/// - [`BrrrError::Parse`] if the file cannot be parsed
881/// - [`BrrrError::PathTraversal`] if the path escapes base_path or contains dangerous patterns
882pub fn extract_file(file_path: &str, base_path: Option<&str>) -> Result<ModuleInfo> {
883    ast::extract_file(file_path, base_path)
884}
885
886/// Extract complete AST information from a source file without path validation.
887///
888/// This is a convenience function equivalent to `extract_file(file_path, None)`.
889/// Basic input validation is still performed for obviously dangerous paths.
890///
891/// # Security Warning
892///
893/// This function does not validate path containment. Only use when:
894/// - The file path comes from a trusted source
895/// - Path validation is performed by the caller
896/// - You explicitly want to allow any valid file path
897#[inline]
898pub fn extract_file_unchecked(file_path: &str) -> Result<ModuleInfo> {
899    ast::extract_file_unchecked(file_path)
900}
901
902/// Extract file information from a source code string.
903///
904/// Parses source code directly (without reading from a file) and returns
905/// detailed information about all functions, classes, and imports. This is
906/// useful when you have source code in memory or want to analyze code
907/// snippets without creating temporary files.
908///
909/// # Arguments
910///
911/// * `source` - Source code as a string
912/// * `language` - Language identifier (e.g., "python", "typescript", "rust", "go")
913///
914/// # Returns
915///
916/// A [`ModuleInfo`] containing:
917/// - `path`: Set to `"<string>"` since there is no file path
918/// - `language`: The language name provided
919/// - `functions`: All top-level functions with full details
920/// - `classes`: All classes/structs with methods
921/// - `imports`: All import statements
922///
923/// # Examples
924///
925/// ```
926/// use go_brrr::extract_from_source;
927///
928/// let source = r#"
929/// def greet(name: str) -> str:
930///     """Say hello to someone."""
931///     return f"Hello, {name}!"
932///
933/// class Greeter:
934///     def __init__(self, prefix: str):
935///         self.prefix = prefix
936/// "#;
937///
938/// let module = extract_from_source(source, "python")?;
939/// assert_eq!(module.language, "python");
940/// assert_eq!(module.functions.len(), 1);
941/// assert_eq!(module.classes.len(), 1);
942/// # Ok::<(), go_brrr::BrrrError>(())
943/// ```
944///
945/// # Errors
946///
947/// - [`BrrrError::UnsupportedLanguage`] if the language is not recognized
948/// - [`BrrrError::Parse`] if the source code cannot be parsed
949pub fn extract_from_source(source: &str, language: &str) -> Result<ModuleInfo> {
950    ast::AstExtractor::extract_from_source(source, language)
951}
952
953/// Extract import statements from a source file.
954///
955/// Parses a source file and extracts all import statements, returning
956/// detailed information about each import including module names, imported
957/// names, aliases, and line numbers.
958///
959/// # Arguments
960///
961/// * `file_path` - Path to the source file
962/// * `language` - Optional language override (auto-detected from file extension if None)
963///
964/// # Returns
965///
966/// A vector of [`ImportInfo`] containing:
967/// - `module`: The module being imported
968/// - `names`: Specific names imported (for `from X import Y` style)
969/// - `aliases`: Name to alias mappings
970/// - `is_from`: Whether this is a `from X import Y` style import
971/// - `level`: Relative import level (0 for absolute imports)
972/// - `line_number`: Line number of the import statement
973///
974/// # Examples
975///
976/// ```no_run
977/// use go_brrr::get_imports;
978///
979/// let imports = get_imports("./src/main.py", None)?;
980/// for imp in &imports {
981///     println!("Import at line {}: {}", imp.line_number, imp.statement());
982/// }
983///
984/// // With explicit language
985/// let ts_imports = get_imports("./src/index.ts", Some("typescript"))?;
986/// # Ok::<(), go_brrr::BrrrError>(())
987/// ```
988///
989/// # Errors
990///
991/// - [`BrrrError::Io`] if the file cannot be read
992/// - [`BrrrError::UnsupportedLanguage`] if the file type is not recognized
993/// - [`BrrrError::Parse`] if the file cannot be parsed
994pub fn get_imports(file_path: &str, language: Option<&str>) -> Result<Vec<ImportInfo>> {
995    use std::path::Path;
996
997    let path = Path::new(file_path);
998    let registry = LanguageRegistry::global();
999
1000    // Validate language if provided, but use file extension for parsing
1001    // This matches the behavior of extract_imports which auto-detects from path
1002    if let Some(lang_name) = language {
1003        if registry.get_by_name(lang_name).is_none() {
1004            return Err(BrrrError::UnsupportedLanguage(lang_name.to_string()));
1005        }
1006    }
1007
1008    // Use the existing extract_imports which handles language detection
1009    ast::extract_imports(path)
1010}
1011
1012/// Get LLM-ready context for a function entry point.
1013///
1014/// Builds a call graph and returns structured context about what
1015/// a function calls (and transitively, what those functions call).
1016/// This is ideal for providing focused context to an LLM about
1017/// a specific code path.
1018///
1019/// # Arguments
1020///
1021/// * `project` - Root project directory
1022/// * `entry_point` - Function name to start from (e.g., "main", "process_request")
1023/// * `depth` - How many levels of calls to traverse (typically 1-3)
1024///
1025/// # Returns
1026///
1027/// A JSON value containing:
1028/// - `entry_point`: The starting function
1029/// - `depth`: Traversal depth used
1030/// - `functions`: List of function names in the call chain
1031/// - `count`: Total number of functions found
1032///
1033/// # Examples
1034///
1035/// ```no_run
1036/// use go_brrr::get_context;
1037///
1038/// let context = get_context("./project", "handle_request", 2)?;
1039/// println!("{}", serde_json::to_string_pretty(&context)?);
1040/// // Output:
1041/// // {
1042/// //   "entry_point": "handle_request",
1043/// //   "depth": 2,
1044/// //   "functions": ["validate_input", "process_data", "send_response"],
1045/// //   "count": 3
1046/// // }
1047/// # Ok::<(), Box<dyn std::error::Error>>(())
1048/// ```
1049///
1050/// # Errors
1051///
1052/// Returns [`BrrrError`] if the project cannot be scanned or parsed.
1053pub fn get_context(project: &str, entry_point: &str, depth: usize) -> Result<serde_json::Value> {
1054    callgraph::get_context_with_lang(project, entry_point, depth, None)
1055}
1056
1057/// Query project for LLM-ready context string.
1058///
1059/// Returns a formatted markdown-style string suitable for direct consumption
1060/// by LLMs, including the entry point function and all its callees with
1061/// their complete source code.
1062///
1063/// This is a convenience wrapper around [`get_context`] that formats the
1064/// output as a human/LLM-readable string instead of JSON.
1065///
1066/// # Arguments
1067///
1068/// * `project` - Project root directory
1069/// * `entry_point` - Entry point function name (e.g., "main" or "Class.method")
1070/// * `depth` - Call graph traversal depth (typically 1-3)
1071/// * `language` - Optional language filter (e.g., "python", "rust")
1072///
1073/// # Returns
1074///
1075/// Formatted string with function source code:
1076///
1077/// ```text
1078/// # Entry: main (src/main.py:10-25)
1079/// def main():
1080///     process_data()
1081///
1082/// ## Callees:
1083///
1084/// ### process_data (src/utils.py:5-15)
1085/// def process_data():
1086///     ...
1087/// ```
1088///
1089/// # Examples
1090///
1091/// ```no_run
1092/// use go_brrr::query;
1093///
1094/// // Get context for main function
1095/// let context = query("./src", "main", 2, None)?;
1096/// println!("{}", context);
1097///
1098/// // Filter to Python files only
1099/// let py_context = query("./src", "handle_request", 3, Some("python"))?;
1100/// # Ok::<(), go_brrr::BrrrError>(())
1101/// ```
1102///
1103/// # Errors
1104///
1105/// Returns [`BrrrError`] if:
1106/// - The project cannot be scanned
1107/// - Source files cannot be parsed
1108/// - The entry point function cannot be found
1109pub fn query(
1110    project: &str,
1111    entry_point: &str,
1112    depth: usize,
1113    language: Option<&str>,
1114) -> Result<String> {
1115    // Get context JSON and extract the LLM-ready text format
1116    let result = callgraph::get_context_with_lang(project, entry_point, depth, language)?;
1117    Ok(result
1118        .get("llm_context")
1119        .and_then(|v| v.as_str())
1120        .unwrap_or("")
1121        .to_string())
1122}
1123
1124/// Get control flow graph for a function.
1125///
1126/// Extracts the CFG showing how control flows through a function,
1127/// including branches, loops, and exception handling. Useful for
1128/// understanding function complexity and control flow paths.
1129///
1130/// # Arguments
1131///
1132/// * `file` - Path to the source file
1133/// * `function` - Name of the function to analyze
1134///
1135/// # Returns
1136///
1137/// A [`CFGInfo`] containing:
1138/// - `function_name`: The analyzed function
1139/// - `blocks`: Map of basic blocks (each with statements and line ranges)
1140/// - `edges`: Control flow edges between blocks
1141/// - `entry`: Entry block ID
1142/// - `exits`: Exit block IDs
1143///
1144/// The CFG can be rendered to Mermaid format for visualization using
1145/// [`CFGInfo::to_mermaid`].
1146///
1147/// # Examples
1148///
1149/// ```no_run
1150/// use go_brrr::get_cfg;
1151///
1152/// let cfg = get_cfg("./src/main.py", "process_data")?;
1153/// println!("Function: {}", cfg.function_name);
1154/// println!("Blocks: {}", cfg.blocks.len());
1155/// println!("Cyclomatic complexity: {}", cfg.cyclomatic_complexity());
1156///
1157/// // Render to Mermaid diagram
1158/// println!("{}", cfg.to_mermaid());
1159/// # Ok::<(), go_brrr::BrrrError>(())
1160/// ```
1161///
1162/// # Errors
1163///
1164/// - [`BrrrError::Io`] if the file cannot be read
1165/// - [`BrrrError::FunctionNotFound`] if the function doesn't exist
1166/// - [`BrrrError::Parse`] if the file cannot be parsed
1167/// - [`BrrrError::UnsupportedLanguage`] if language is specified but not supported
1168pub fn get_cfg(file: &str, function: &str, language: Option<&str>) -> Result<CFGInfo> {
1169    cfg::extract_with_language(file, function, language)
1170}
1171
1172/// Get control flow graph with auto-detected language (convenience wrapper).
1173///
1174/// This is a convenience function for [`get_cfg`] with language auto-detection.
1175/// Equivalent to `get_cfg(file, function, None)`.
1176///
1177/// # Arguments
1178///
1179/// * `file` - Path to the source file
1180/// * `function` - Name of the function to analyze
1181///
1182/// # Example
1183///
1184/// ```no_run
1185/// use go_brrr::get_cfg_auto;
1186///
1187/// let cfg = get_cfg_auto("./src/main.py", "process_data")?;
1188/// # Ok::<(), go_brrr::BrrrError>(())
1189/// ```
1190#[inline]
1191pub fn get_cfg_auto(file: &str, function: &str) -> Result<CFGInfo> {
1192    get_cfg(file, function, None)
1193}
1194
1195/// Get control flow graph from a source code string.
1196///
1197/// Parses source code directly (without reading from a file) and extracts
1198/// the control flow graph for the specified function. This is useful when
1199/// you have source code in memory or want to analyze code snippets.
1200///
1201/// # Arguments
1202///
1203/// * `source` - Source code as a string
1204/// * `function` - Name of the function to analyze
1205/// * `language` - Language identifier (e.g., "python", "typescript", "rust")
1206///
1207/// # Returns
1208///
1209/// A [`CFGInfo`] containing the control flow graph for the function.
1210///
1211/// # Examples
1212///
1213/// ```
1214/// use go_brrr::get_cfg_from_source;
1215///
1216/// let source = r#"
1217/// def process(x):
1218///     if x > 0:
1219///         return x * 2
1220///     return 0
1221/// "#;
1222///
1223/// let cfg = get_cfg_from_source(source, "process", "python")?;
1224/// assert_eq!(cfg.function_name, "process");
1225/// assert!(cfg.cyclomatic_complexity() >= 2); // Has an if branch
1226/// # Ok::<(), go_brrr::BrrrError>(())
1227/// ```
1228///
1229/// # Errors
1230///
1231/// - [`BrrrError::UnsupportedLanguage`] if the language is not recognized
1232/// - [`BrrrError::FunctionNotFound`] if the function doesn't exist
1233/// - [`BrrrError::Parse`] if the source cannot be parsed
1234pub fn get_cfg_from_source(source: &str, function: &str, language: &str) -> Result<CFGInfo> {
1235    cfg::CfgBuilder::extract_from_source(source, language, function)
1236}
1237
1238/// Get CFG blocks for a function.
1239///
1240/// Convenience function that extracts the CFG and returns just the blocks.
1241/// This is equivalent to `get_cfg(file, function)?.blocks.into_values().collect()`.
1242///
1243/// # Arguments
1244///
1245/// * `file` - Path to the source file
1246/// * `function` - Name of the function to analyze
1247///
1248/// # Returns
1249///
1250/// A vector of [`CFGBlock`] containing block id, statements, and line range.
1251///
1252/// # Examples
1253///
1254/// ```no_run
1255/// use go_brrr::get_cfg_blocks;
1256///
1257/// let blocks = get_cfg_blocks("./src/main.py", "process")?;
1258/// for block in &blocks {
1259///     println!("Block {} (lines {}-{}): {}", block.id.0, block.start_line, block.end_line, block.label);
1260/// }
1261/// # Ok::<(), go_brrr::BrrrError>(())
1262/// ```
1263///
1264/// # Errors
1265///
1266/// - [`BrrrError::Io`] if the file cannot be read
1267/// - [`BrrrError::FunctionNotFound`] if the function doesn't exist
1268/// - [`BrrrError::Parse`] if the file cannot be parsed
1269pub fn get_cfg_blocks(file: &str, function: &str) -> Result<Vec<CFGBlock>> {
1270    let cfg = get_cfg(file, function, None)?;
1271    Ok(cfg.blocks.into_values().collect())
1272}
1273
1274/// Get CFG edges for a function.
1275///
1276/// Convenience function that extracts the CFG and returns just the edges.
1277/// This is equivalent to `get_cfg(file, function)?.edges`.
1278///
1279/// # Arguments
1280///
1281/// * `file` - Path to the source file
1282/// * `function` - Name of the function to analyze
1283///
1284/// # Returns
1285///
1286/// A vector of [`CFGEdge`] containing from/to block IDs and edge type.
1287///
1288/// # Examples
1289///
1290/// ```no_run
1291/// use go_brrr::get_cfg_edges;
1292///
1293/// let edges = get_cfg_edges("./src/main.py", "process")?;
1294/// for edge in &edges {
1295///     println!("Block {} -> Block {} ({})", edge.from.0, edge.to.0, edge.label());
1296/// }
1297/// # Ok::<(), go_brrr::BrrrError>(())
1298/// ```
1299///
1300/// # Errors
1301///
1302/// - [`BrrrError::Io`] if the file cannot be read
1303/// - [`BrrrError::FunctionNotFound`] if the function doesn't exist
1304/// - [`BrrrError::Parse`] if the file cannot be parsed
1305pub fn get_cfg_edges(file: &str, function: &str) -> Result<Vec<CFGEdge>> {
1306    let cfg = get_cfg(file, function, None)?;
1307    Ok(cfg.edges)
1308}
1309
1310/// Get CFG as Mermaid diagram string.
1311///
1312/// Convenience function that extracts the CFG and renders it as a Mermaid flowchart.
1313/// The output can be embedded in Markdown or rendered via mermaid.live.
1314///
1315/// # Arguments
1316///
1317/// * `file` - Path to the source file
1318/// * `function` - Name of the function to analyze
1319///
1320/// # Returns
1321///
1322/// A Mermaid flowchart string.
1323///
1324/// # Examples
1325///
1326/// ```no_run
1327/// use go_brrr::get_cfg_mermaid;
1328///
1329/// let mermaid = get_cfg_mermaid("./src/main.py", "process")?;
1330/// println!("{}", mermaid);
1331/// // Output:
1332/// // flowchart TD
1333/// //     B0["entry"]
1334/// //     B1["if x > 0"]
1335/// //     B0 --> B1
1336/// # Ok::<(), go_brrr::BrrrError>(())
1337/// ```
1338///
1339/// # Errors
1340///
1341/// - [`BrrrError::Io`] if the file cannot be read
1342/// - [`BrrrError::FunctionNotFound`] if the function doesn't exist
1343/// - [`BrrrError::Parse`] if the file cannot be parsed
1344pub fn get_cfg_mermaid(file: &str, function: &str) -> Result<String> {
1345    let cfg = get_cfg(file, function, None)?;
1346    Ok(cfg::to_mermaid(&cfg))
1347}
1348
1349/// Get CFG as DOT (Graphviz) string.
1350///
1351/// Convenience function that extracts the CFG and renders it in DOT format.
1352/// The output can be rendered using Graphviz tools (dot, neato, etc.).
1353///
1354/// # Arguments
1355///
1356/// * `file` - Path to the source file
1357/// * `function` - Name of the function to analyze
1358///
1359/// # Returns
1360///
1361/// A DOT graph string.
1362///
1363/// # Examples
1364///
1365/// ```no_run
1366/// use go_brrr::get_cfg_dot;
1367///
1368/// let dot = get_cfg_dot("./src/main.py", "process")?;
1369/// std::fs::write("cfg.dot", &dot)?;
1370/// // Then run: dot -Tpng cfg.dot -o cfg.png
1371/// # Ok::<(), Box<dyn std::error::Error>>(())
1372/// ```
1373///
1374/// # Errors
1375///
1376/// - [`BrrrError::Io`] if the file cannot be read
1377/// - [`BrrrError::FunctionNotFound`] if the function doesn't exist
1378/// - [`BrrrError::Parse`] if the file cannot be parsed
1379pub fn get_cfg_dot(file: &str, function: &str) -> Result<String> {
1380    let cfg = get_cfg(file, function, None)?;
1381    Ok(cfg::to_dot(&cfg))
1382}
1383
1384/// Get CFG as ASCII art string.
1385///
1386/// Convenience function that extracts the CFG and renders it as ASCII text.
1387/// Provides a terminal-friendly, human-readable view of the CFG structure.
1388///
1389/// # Arguments
1390///
1391/// * `file` - Path to the source file
1392/// * `function` - Name of the function to analyze
1393///
1394/// # Returns
1395///
1396/// An ASCII text representation of the CFG.
1397///
1398/// # Examples
1399///
1400/// ```no_run
1401/// use go_brrr::get_cfg_ascii;
1402///
1403/// let ascii = get_cfg_ascii("./src/main.py", "process")?;
1404/// println!("{}", ascii);
1405/// // Output:
1406/// // CFG: process
1407/// // ========================================
1408/// // Blocks: 5
1409/// // Edges: 6
1410/// // Complexity: 3
1411/// // ...
1412/// # Ok::<(), go_brrr::BrrrError>(())
1413/// ```
1414///
1415/// # Errors
1416///
1417/// - [`BrrrError::Io`] if the file cannot be read
1418/// - [`BrrrError::FunctionNotFound`] if the function doesn't exist
1419/// - [`BrrrError::Parse`] if the file cannot be parsed
1420pub fn get_cfg_ascii(file: &str, function: &str) -> Result<String> {
1421    let cfg = get_cfg(file, function, None)?;
1422    Ok(cfg::to_ascii(&cfg))
1423}
1424
1425/// Get CFG as JSON value.
1426///
1427/// Convenience function that extracts the CFG and converts it to a serde_json::Value.
1428/// Useful for serialization and integration with JSON-based tools.
1429///
1430/// # Arguments
1431///
1432/// * `file` - Path to the source file
1433/// * `function` - Name of the function to analyze
1434///
1435/// # Returns
1436///
1437/// A JSON value representing the CFG.
1438///
1439/// # Examples
1440///
1441/// ```no_run
1442/// use go_brrr::get_cfg_json;
1443///
1444/// let json = get_cfg_json("./src/main.py", "process")?;
1445/// println!("{}", serde_json::to_string_pretty(&json)?);
1446/// # Ok::<(), Box<dyn std::error::Error>>(())
1447/// ```
1448///
1449/// # Errors
1450///
1451/// - [`BrrrError::Io`] if the file cannot be read
1452/// - [`BrrrError::FunctionNotFound`] if the function doesn't exist
1453/// - [`BrrrError::Parse`] if the file cannot be parsed
1454pub fn get_cfg_json(file: &str, function: &str) -> Result<serde_json::Value> {
1455    let cfg = get_cfg(file, function, None)?;
1456    Ok(serde_json::to_value(&cfg)?)
1457}
1458
1459/// Get data flow graph for a function.
1460///
1461/// Extracts the DFG showing how data flows through a function,
1462/// tracking variable definitions, uses, and mutations. Essential
1463/// for understanding data dependencies and performing program slicing.
1464///
1465/// # Arguments
1466///
1467/// * `file` - Path to the source file
1468/// * `function` - Name of the function to analyze
1469///
1470/// # Returns
1471///
1472/// A [`DFGInfo`] containing:
1473/// - `function_name`: The analyzed function
1474/// - `edges`: Data flow edges (variable, from_line, to_line, kind)
1475/// - `definitions`: Map of variable -> definition lines
1476/// - `uses`: Map of variable -> use lines
1477///
1478/// # Examples
1479///
1480/// ```no_run
1481/// use go_brrr::get_dfg;
1482///
1483/// let dfg = get_dfg("./src/main.py", "calculate")?;
1484/// println!("Variables: {:?}", dfg.variables());
1485///
1486/// // Show where each variable is defined
1487/// for (var, lines) in &dfg.definitions {
1488///     println!("{} defined at lines {:?}", var, lines);
1489/// }
1490/// # Ok::<(), go_brrr::BrrrError>(())
1491/// ```
1492///
1493/// # Errors
1494///
1495/// - [`BrrrError::Io`] if the file cannot be read
1496/// - [`BrrrError::FunctionNotFound`] if the function doesn't exist
1497/// - [`BrrrError::Parse`] if the file cannot be parsed
1498/// - [`BrrrError::UnsupportedLanguage`] if language is specified but not supported
1499pub fn get_dfg(file: &str, function: &str, language: Option<&str>) -> Result<DFGInfo> {
1500    dfg::extract_with_language(file, function, language)
1501}
1502
1503/// Get data flow graph with auto-detected language (convenience wrapper).
1504///
1505/// This is a convenience function for [`get_dfg`] with language auto-detection.
1506/// Equivalent to `get_dfg(file, function, None)`.
1507///
1508/// # Arguments
1509///
1510/// * `file` - Path to the source file
1511/// * `function` - Name of the function to analyze
1512///
1513/// # Example
1514///
1515/// ```no_run
1516/// use go_brrr::get_dfg_auto;
1517///
1518/// let dfg = get_dfg_auto("./src/main.py", "calculate")?;
1519/// # Ok::<(), go_brrr::BrrrError>(())
1520/// ```
1521#[inline]
1522pub fn get_dfg_auto(file: &str, function: &str) -> Result<DFGInfo> {
1523    get_dfg(file, function, None)
1524}
1525
1526/// Get data flow graph from a source code string.
1527///
1528/// Parses source code directly (without reading from a file) and extracts
1529/// the data flow graph for the specified function. This is useful when
1530/// you have source code in memory or want to analyze code snippets.
1531///
1532/// # Arguments
1533///
1534/// * `source` - Source code as a string
1535/// * `function` - Name of the function to analyze
1536/// * `language` - Language identifier (e.g., "python", "typescript", "rust")
1537///
1538/// # Returns
1539///
1540/// A [`DFGInfo`] containing the data flow graph for the function.
1541///
1542/// # Examples
1543///
1544/// ```
1545/// use go_brrr::get_dfg_from_source;
1546///
1547/// let source = r#"
1548/// def compute(x, y):
1549///     z = x + y
1550///     result = z * 2
1551///     return result
1552/// "#;
1553///
1554/// let dfg = get_dfg_from_source(source, "compute", "python")?;
1555/// assert_eq!(dfg.function_name, "compute");
1556/// assert!(dfg.definitions.contains_key("z"));
1557/// assert!(dfg.definitions.contains_key("result"));
1558/// # Ok::<(), go_brrr::BrrrError>(())
1559/// ```
1560///
1561/// # Errors
1562///
1563/// - [`BrrrError::UnsupportedLanguage`] if the language is not recognized
1564/// - [`BrrrError::FunctionNotFound`] if the function doesn't exist
1565/// - [`BrrrError::Parse`] if the source cannot be parsed
1566pub fn get_dfg_from_source(source: &str, function: &str, language: &str) -> Result<DFGInfo> {
1567    dfg::DfgBuilder::extract_from_source(source, language, function)
1568}
1569
1570/// Get DFG edges for a function.
1571///
1572/// Convenience function that extracts the DFG and returns just the edges.
1573/// This is equivalent to `get_dfg(file, function)?.edges`.
1574///
1575/// # Arguments
1576///
1577/// * `file` - Path to the source file
1578/// * `function` - Name of the function to analyze
1579///
1580/// # Returns
1581///
1582/// A vector of [`DataflowEdge`] containing variable, from/to lines, and flow kind.
1583///
1584/// # Examples
1585///
1586/// ```no_run
1587/// use go_brrr::get_dfg_edges;
1588///
1589/// let edges = get_dfg_edges("./src/main.py", "calculate")?;
1590/// for edge in &edges {
1591///     println!("{}: line {} -> line {} ({:?})",
1592///         edge.variable, edge.from_line, edge.to_line, edge.kind);
1593/// }
1594/// # Ok::<(), go_brrr::BrrrError>(())
1595/// ```
1596///
1597/// # Errors
1598///
1599/// - [`BrrrError::Io`] if the file cannot be read
1600/// - [`BrrrError::FunctionNotFound`] if the function doesn't exist
1601/// - [`BrrrError::Parse`] if the file cannot be parsed
1602pub fn get_dfg_edges(file: &str, function: &str) -> Result<Vec<DataflowEdge>> {
1603    let dfg = get_dfg(file, function, None)?;
1604    Ok(dfg.edges)
1605}
1606
1607/// Get variables tracked in DFG.
1608///
1609/// Convenience function that extracts the DFG and returns the list of
1610/// variables that have definitions or uses tracked.
1611///
1612/// # Arguments
1613///
1614/// * `file` - Path to the source file
1615/// * `function` - Name of the function to analyze
1616///
1617/// # Returns
1618///
1619/// A vector of variable names tracked in the function.
1620///
1621/// # Examples
1622///
1623/// ```no_run
1624/// use go_brrr::get_dfg_variables;
1625///
1626/// let vars = get_dfg_variables("./src/main.py", "calculate")?;
1627/// println!("Variables tracked: {:?}", vars);
1628/// # Ok::<(), go_brrr::BrrrError>(())
1629/// ```
1630///
1631/// # Errors
1632///
1633/// - [`BrrrError::Io`] if the file cannot be read
1634/// - [`BrrrError::FunctionNotFound`] if the function doesn't exist
1635/// - [`BrrrError::Parse`] if the file cannot be parsed
1636pub fn get_dfg_variables(file: &str, function: &str) -> Result<Vec<String>> {
1637    let dfg = get_dfg(file, function, None)?;
1638    use std::collections::HashSet;
1639    let vars: HashSet<_> = dfg.edges.iter().map(|e| e.variable.clone()).collect();
1640    Ok(vars.into_iter().collect())
1641}
1642
1643/// Get def-use chains for a specific variable.
1644///
1645/// Extracts the data flow graph and filters edges for a specific variable,
1646/// returning pairs of (definition_line, use_line).
1647///
1648/// This is useful for understanding how a specific variable flows through
1649/// a function - where it's defined and where those definitions are used.
1650///
1651/// # Arguments
1652///
1653/// * `file` - Path to the source file
1654/// * `function` - Name of the function to analyze
1655/// * `variable` - Name of the variable to track
1656///
1657/// # Returns
1658///
1659/// A vector of (from_line, to_line) tuples representing def-use chains.
1660///
1661/// # Examples
1662///
1663/// ```no_run
1664/// use go_brrr::get_def_use_chains;
1665///
1666/// // Track how variable 'x' flows through the function
1667/// let chains = get_def_use_chains("./src/main.py", "calculate", "x")?;
1668/// for (def_line, use_line) in &chains {
1669///     println!("x: defined at line {} -> used at line {}", def_line, use_line);
1670/// }
1671/// # Ok::<(), go_brrr::BrrrError>(())
1672/// ```
1673///
1674/// # Errors
1675///
1676/// - [`BrrrError::Io`] if the file cannot be read
1677/// - [`BrrrError::FunctionNotFound`] if the function doesn't exist
1678/// - [`BrrrError::Parse`] if the file cannot be parsed
1679pub fn get_def_use_chains(file: &str, function: &str, variable: &str) -> Result<Vec<(usize, usize)>> {
1680    let dfg = get_dfg(file, function, None)?;
1681    let chains: Vec<_> = dfg
1682        .edges
1683        .iter()
1684        .filter(|e| e.variable == variable)
1685        .map(|e| (e.from_line, e.to_line))
1686        .collect();
1687    Ok(chains)
1688}
1689
1690/// Get program dependence graph (PDG) for a function.
1691///
1692/// A PDG combines control flow (CFG) and data flow (DFG) into a unified graph
1693/// that enables accurate program slicing. It includes:
1694/// - Control dependencies: which conditions determine if a statement executes
1695/// - Data dependencies: def-use chains for variables
1696///
1697/// # Arguments
1698///
1699/// * `file` - Path to the source file
1700/// * `function` - Name of the function to analyze
1701///
1702/// # Returns
1703///
1704/// A [`PDGInfo`] containing the combined CFG, DFG, and computed control dependencies.
1705///
1706/// # Examples
1707///
1708/// ```no_run
1709/// use go_brrr::get_pdg;
1710///
1711/// let pdg = get_pdg("./src/main.py", "process")?;
1712/// println!("Control dependencies: {}", pdg.control_dep_count());
1713/// println!("Data edges: {}", pdg.data_edge_count());
1714///
1715/// // Check if line 5 is control-dependent on line 3
1716/// if pdg.is_control_dependent(5, 3) {
1717///     println!("Line 5 depends on condition at line 3");
1718/// }
1719/// # Ok::<(), go_brrr::BrrrError>(())
1720/// ```
1721///
1722/// # Errors
1723///
1724/// - [`BrrrError::Io`] if the file cannot be read
1725/// - [`BrrrError::FunctionNotFound`] if the function doesn't exist
1726/// - [`BrrrError::Parse`] if the file cannot be parsed
1727/// - [`BrrrError::UnsupportedLanguage`] if language is specified but not supported
1728pub fn get_pdg(file: &str, function: &str, language: Option<&str>) -> Result<PDGInfo> {
1729    pdg::build_pdg_with_language(file, function, language)
1730}
1731
1732/// Get PDG with auto-detected language (convenience wrapper).
1733///
1734/// This is a convenience function for [`get_pdg`] with language auto-detection.
1735/// Equivalent to `get_pdg(file, function, None)`.
1736///
1737/// # Arguments
1738///
1739/// * `file` - Path to the source file
1740/// * `function` - Name of the function to analyze
1741///
1742/// # Example
1743///
1744/// ```no_run
1745/// use go_brrr::get_pdg_auto;
1746///
1747/// let pdg = get_pdg_auto("./src/main.py", "process")?;
1748/// # Ok::<(), go_brrr::BrrrError>(())
1749/// ```
1750#[inline]
1751pub fn get_pdg_auto(file: &str, function: &str) -> Result<PDGInfo> {
1752    get_pdg(file, function, None)
1753}
1754
1755/// Get program slice for a line of code.
1756///
1757/// Performs program slicing to find lines that affect (backward) or are affected by
1758/// (forward) the given target line. This is extremely useful for debugging
1759/// ("what could have caused this value?") and impact analysis ("what will this change affect?").
1760///
1761/// **NOTE**: This function uses PDG-based slicing which follows BOTH control
1762/// dependencies (conditions that determine if code executes) AND data dependencies
1763/// (variable def-use chains). This is more accurate than DFG-only slicing.
1764///
1765/// For example, given:
1766/// ```python
1767/// if x > 0:        # Line 2 - condition
1768///     result = x * 2  # Line 3
1769/// return result    # Line 4
1770/// ```
1771///
1772/// A backward slice from line 4 correctly includes line 2 (the condition) because
1773/// whether `result` is assigned depends on the condition's outcome.
1774///
1775/// # Arguments
1776///
1777/// * `file` - Path to the source file
1778/// * `function` - Name of the function containing the line
1779/// * `line` - Target line number (1-indexed)
1780/// * `direction` - Slice direction: "backward" (default) or "forward"
1781/// * `variable` - Optional variable name for variable-specific slicing
1782/// * `language` - Optional language override (auto-detected if None)
1783///
1784/// # Returns
1785///
1786/// A sorted vector of line numbers in the slice, including the target line itself.
1787///
1788/// # Examples
1789///
1790/// ```no_run
1791/// use go_brrr::get_slice;
1792///
1793/// // Backward slice from line 42 (default direction)
1794/// let affected_lines = get_slice("./src/main.py", "process", 42, None, None, None)?;
1795/// println!("Line 42 is affected by lines: {:?}", affected_lines);
1796///
1797/// // Forward slice to see what line 10 affects
1798/// let impacts = get_slice("./src/main.py", "process", 10, Some("forward"), None, None)?;
1799/// println!("Line 10 affects lines: {:?}", impacts);
1800///
1801/// // Variable-specific backward slice
1802/// let x_deps = get_slice("./src/main.py", "process", 42, None, Some("x"), None)?;
1803/// println!("Lines affecting 'x' at line 42: {:?}", x_deps);
1804///
1805/// // Forward slice for variable 'result'
1806/// let result_impacts = get_slice("./src/main.py", "process", 5, Some("forward"), Some("result"), None)?;
1807/// println!("Lines affected by 'result' from line 5: {:?}", result_impacts);
1808/// # Ok::<(), go_brrr::BrrrError>(())
1809/// ```
1810///
1811/// # Errors
1812///
1813/// - [`BrrrError::Io`] if the file cannot be read
1814/// - [`BrrrError::FunctionNotFound`] if the function doesn't exist
1815/// - [`BrrrError::Parse`] if the file cannot be parsed
1816/// - [`BrrrError::InvalidArgument`] if line is 0 or direction is not "backward" or "forward"
1817pub fn get_slice(
1818    file: &str,
1819    function: &str,
1820    line: usize,
1821    direction: Option<&str>,
1822    variable: Option<&str>,
1823    language: Option<&str>,
1824) -> Result<Vec<usize>> {
1825    // Validate line number is 1-indexed (lines start at 1, not 0)
1826    if line == 0 {
1827        return Err(BrrrError::InvalidArgument(
1828            "Line numbers are 1-indexed, got 0".to_string(),
1829        ));
1830    }
1831
1832    // Build the PDG for the function with optional language override
1833    let pdg_info = pdg::build_pdg_with_language(file, function, language)?;
1834
1835    // Create slicing criteria with optional variable filter
1836    let criteria = match variable {
1837        Some(var) => pdg::SliceCriteria::at_line_variable(line, var),
1838        None => pdg::SliceCriteria::at_line(line),
1839    };
1840
1841    // Route to appropriate slice direction
1842    let dir = direction.unwrap_or("backward");
1843    let result = match dir {
1844        "backward" => pdg::backward_slice(&pdg_info, &criteria),
1845        "forward" => pdg::forward_slice(&pdg_info, &criteria),
1846        _ => {
1847            return Err(BrrrError::InvalidArgument(format!(
1848                "Invalid direction '{}', expected 'backward' or 'forward'",
1849                dir
1850            )))
1851        }
1852    };
1853
1854    Ok(result.lines)
1855}
1856
1857/// Get program slice from a source code string.
1858///
1859/// Parses source code directly (without reading from a file) and performs
1860/// program slicing on the specified function. This uses DFG-based slicing
1861/// (data flow only) for source strings.
1862///
1863/// # Arguments
1864///
1865/// * `source` - Source code as a string
1866/// * `function` - Name of the function containing the line
1867/// * `line` - Target line number (1-indexed)
1868/// * `direction` - Slice direction: "backward" (default) or "forward"
1869/// * `variable` - Optional variable name for variable-specific slicing
1870/// * `language` - Language identifier (e.g., "python", "typescript", "rust")
1871///
1872/// # Returns
1873///
1874/// A sorted vector of line numbers in the slice.
1875///
1876/// # Examples
1877///
1878/// ```
1879/// use go_brrr::get_slice_from_source;
1880///
1881/// let source = r#"
1882/// def compute(x):
1883///     a = x + 1
1884///     b = a * 2
1885///     c = b + x
1886///     return c
1887/// "#;
1888///
1889/// // Backward slice from line 5 (c = b + x)
1890/// let slice = get_slice_from_source(source, "compute", 5, None, None, "python")?;
1891/// // Should include lines that affect line 5
1892/// assert!(!slice.is_empty());
1893/// # Ok::<(), go_brrr::BrrrError>(())
1894/// ```
1895///
1896/// # Errors
1897///
1898/// - [`BrrrError::UnsupportedLanguage`] if the language is not recognized
1899/// - [`BrrrError::FunctionNotFound`] if the function doesn't exist
1900/// - [`BrrrError::Parse`] if the source cannot be parsed
1901/// - [`BrrrError::InvalidArgument`] if line is 0 or direction is not "backward" or "forward"
1902pub fn get_slice_from_source(
1903    source: &str,
1904    function: &str,
1905    line: usize,
1906    direction: Option<&str>,
1907    variable: Option<&str>,
1908    language: &str,
1909) -> Result<Vec<usize>> {
1910    // Validate line number is 1-indexed (lines start at 1, not 0)
1911    if line == 0 {
1912        return Err(BrrrError::InvalidArgument(
1913            "Line numbers are 1-indexed, got 0".to_string(),
1914        ));
1915    }
1916
1917    // Build the DFG for the function from source
1918    let dfg_info = dfg::DfgBuilder::extract_from_source(source, language, function)?;
1919
1920    // Create slice criteria with optional variable filter
1921    let criteria = match variable {
1922        Some(var) => dfg::SliceCriteria::at_line_variable(line, var),
1923        None => dfg::SliceCriteria::at_line(line),
1924    };
1925
1926    let dir = direction.unwrap_or("backward");
1927    let result = match dir {
1928        "backward" => dfg::backward_slice(&dfg_info, &criteria).lines,
1929        "forward" => dfg::forward_slice(&dfg_info, &criteria).lines,
1930        _ => {
1931            return Err(BrrrError::InvalidArgument(format!(
1932                "Invalid direction '{}', expected 'backward' or 'forward'",
1933                dir
1934            )))
1935        }
1936    };
1937
1938    Ok(result)
1939}
1940
1941/// Get backward program slice (convenience wrapper).
1942///
1943/// This is a convenience function that calls `get_slice` with default direction.
1944/// For the full API with all parameters, use [`get_slice`].
1945///
1946/// # Arguments
1947///
1948/// * `file` - Path to the source file
1949/// * `function` - Name of the function containing the line
1950/// * `line` - Target line number (1-indexed)
1951///
1952/// # Returns
1953///
1954/// A sorted vector of line numbers that affect the target line.
1955///
1956/// # Examples
1957///
1958/// ```no_run
1959/// use go_brrr::get_backward_slice;
1960///
1961/// let affected_lines = get_backward_slice("./src/main.py", "process", 42)?;
1962/// # Ok::<(), go_brrr::BrrrError>(())
1963/// ```
1964pub fn get_backward_slice(file: &str, function: &str, line: usize) -> Result<Vec<usize>> {
1965    get_slice(file, function, line, Some("backward"), None, None)
1966}
1967
1968/// Get backward program slice using DFG-only (data flow only).
1969///
1970/// This is the legacy slicing function that only follows data dependencies,
1971/// NOT control dependencies. For most use cases, prefer [`get_slice`] which
1972/// uses PDG-based slicing for more accurate results.
1973///
1974/// Use this function only if you specifically want data-flow-only analysis
1975/// without control dependencies.
1976///
1977/// # Arguments
1978///
1979/// * `file` - Path to the source file
1980/// * `function` - Name of the function containing the line
1981/// * `line` - Target line number (1-indexed)
1982///
1983/// # Returns
1984///
1985/// A sorted vector of line numbers that affect the target line through
1986/// data dependencies only.
1987///
1988/// # Errors
1989///
1990/// - [`BrrrError::InvalidArgument`] if line is 0
1991pub fn get_slice_dfg_only(file: &str, function: &str, line: usize) -> Result<Vec<usize>> {
1992    // Validate line number is 1-indexed (lines start at 1, not 0)
1993    if line == 0 {
1994        return Err(BrrrError::InvalidArgument(
1995            "Line numbers are 1-indexed, got 0".to_string(),
1996        ));
1997    }
1998    dfg::get_slice(file, function, line)
1999}
2000
2001/// Get forward program slice for a line of code.
2002///
2003/// Performs forward slicing to find all lines affected by the given source line.
2004/// This is useful for impact analysis ("what will change if I modify this line?").
2005///
2006/// **NOTE**: This function uses PDG-based slicing which follows BOTH control
2007/// dependencies (conditions that determine if code executes) AND data dependencies
2008/// (variable def-use chains).
2009///
2010/// # Arguments
2011///
2012/// * `file` - Path to the source file
2013/// * `function` - Name of the function containing the line
2014/// * `line` - Source line number (1-indexed)
2015///
2016/// # Returns
2017///
2018/// A sorted vector of line numbers that are affected by the source line,
2019/// including the source line itself.
2020///
2021/// # Examples
2022///
2023/// ```no_run
2024/// use go_brrr::get_forward_slice;
2025///
2026/// // Find what line 10 affects
2027/// let affected_lines = get_forward_slice("./src/main.py", "process", 10)?;
2028/// println!("Line 10 affects lines: {:?}", affected_lines);
2029/// // Output: [10, 15, 23, 38, 42]
2030/// # Ok::<(), go_brrr::BrrrError>(())
2031/// ```
2032///
2033/// # Errors
2034///
2035/// - [`BrrrError::Io`] if the file cannot be read
2036/// - [`BrrrError::FunctionNotFound`] if the function doesn't exist
2037/// - [`BrrrError::Parse`] if the file cannot be parsed
2038/// - [`BrrrError::InvalidArgument`] if line is 0
2039pub fn get_forward_slice(file: &str, function: &str, line: usize) -> Result<Vec<usize>> {
2040    // Validate line number is 1-indexed (lines start at 1, not 0)
2041    if line == 0 {
2042        return Err(BrrrError::InvalidArgument(
2043            "Line numbers are 1-indexed, got 0".to_string(),
2044        ));
2045    }
2046    pdg::get_forward_slice(file, function, line)
2047}
2048
2049/// Get PDG-based slice with direction parameter.
2050///
2051/// Unified function for both backward and forward slicing using PDG.
2052/// For most cases, prefer [`get_slice`] (backward) or [`get_forward_slice`] (forward).
2053///
2054/// # Arguments
2055///
2056/// * `file` - Path to the source file
2057/// * `function` - Name of the function containing the line
2058/// * `line` - Target/source line number (1-indexed)
2059/// * `direction` - Either "backward" or "forward"
2060///
2061/// # Returns
2062///
2063/// A sorted vector of line numbers in the slice.
2064///
2065/// # Examples
2066///
2067/// ```no_run
2068/// use go_brrr::get_pdg_slice;
2069///
2070/// // Backward slice: what affects line 42?
2071/// let backward = get_pdg_slice("./src/main.py", "process", 42, "backward")?;
2072///
2073/// // Forward slice: what does line 10 affect?
2074/// let forward = get_pdg_slice("./src/main.py", "process", 10, "forward")?;
2075/// # Ok::<(), go_brrr::BrrrError>(())
2076/// ```
2077///
2078/// # Errors
2079///
2080/// - [`BrrrError::Io`] if the file cannot be read
2081/// - [`BrrrError::FunctionNotFound`] if the function doesn't exist
2082/// - [`BrrrError::Parse`] if the file cannot be parsed
2083/// - [`BrrrError::InvalidArgument`] if line is 0 or direction is not "backward" or "forward"
2084pub fn get_pdg_slice(file: &str, function: &str, line: usize, direction: &str) -> Result<Vec<usize>> {
2085    // Validate line number is 1-indexed (lines start at 1, not 0)
2086    if line == 0 {
2087        return Err(BrrrError::InvalidArgument(
2088            "Line numbers are 1-indexed, got 0".to_string(),
2089        ));
2090    }
2091    match direction {
2092        "backward" => pdg::get_slice(file, function, line),
2093        "forward" => pdg::get_forward_slice(file, function, line),
2094        _ => Err(BrrrError::InvalidArgument(format!(
2095            "Invalid direction '{}', expected 'backward' or 'forward'",
2096            direction
2097        ))),
2098    }
2099}
2100
2101/// Build the cross-file call graph for a project.
2102///
2103/// Scans all source files in the project, indexes function definitions,
2104/// and resolves call sites to build a complete call graph. This is the
2105/// foundation for impact analysis and dead code detection.
2106///
2107/// # Arguments
2108///
2109/// * `path` - Root project directory
2110///
2111/// # Returns
2112///
2113/// A [`CallGraph`] containing:
2114/// - `edges`: All call edges (caller -> callee with line numbers)
2115/// - `callers`: Index of callee -> set of callers (for impact analysis)
2116/// - `callees`: Index of caller -> set of callees (for forward traversal)
2117///
2118/// # Examples
2119///
2120/// ```no_run
2121/// use go_brrr::build_callgraph;
2122///
2123/// let graph = build_callgraph("./project")?;
2124/// println!("Found {} call edges", graph.edges.len());
2125/// println!("Unique functions: {}", graph.all_functions().len());
2126///
2127/// // Serialize for caching
2128/// let json = serde_json::to_string(&graph)?;
2129/// # Ok::<(), Box<dyn std::error::Error>>(())
2130/// ```
2131///
2132/// # Performance
2133///
2134/// Building a call graph requires parsing all source files, so it can be
2135/// expensive for large projects. Consider using [`warm_callgraph`] to
2136/// pre-build the cache.
2137///
2138/// # Errors
2139///
2140/// Returns [`BrrrError`] if files cannot be read or parsed.
2141pub fn build_callgraph(path: &str) -> Result<CallGraph> {
2142    callgraph::build(path)
2143}
2144
2145/// Find all callers of a function (impact analysis).
2146///
2147/// Performs reverse call graph traversal to find all functions that
2148/// directly or transitively call the target function. This is essential
2149/// for understanding the impact of changes to a function.
2150///
2151/// # Arguments
2152///
2153/// * `path` - Root project directory
2154/// * `function` - Target function name
2155/// * `depth` - Maximum traversal depth (typically 1-5)
2156///
2157/// # Returns
2158///
2159/// A vector of [`FunctionRef`] for all functions that call the target
2160/// (directly or transitively up to the specified depth).
2161///
2162/// # Examples
2163///
2164/// ```no_run
2165/// use go_brrr::get_impact;
2166///
2167/// // Who calls the database connection function?
2168/// let callers = get_impact("./project", "get_db_connection", 3)?;
2169/// println!("Functions affected by get_db_connection changes:");
2170/// for caller in &callers {
2171///     println!("  {}:{}", caller.file, caller.name);
2172/// }
2173/// # Ok::<(), go_brrr::BrrrError>(())
2174/// ```
2175///
2176/// # Use Cases
2177///
2178/// - Assess blast radius before refactoring
2179/// - Find all code paths through a critical function
2180/// - Identify tightly coupled components
2181///
2182/// # Errors
2183///
2184/// Returns [`BrrrError`] if the project cannot be scanned.
2185pub fn get_impact(path: &str, function: &str, depth: usize) -> Result<Vec<FunctionRef>> {
2186    use callgraph::{cache, analyze_impact, ImpactConfig};
2187
2188    let project = std::path::Path::new(path);
2189    let graph = cache::get_or_build_graph_with_config(project, None, false)?;
2190    let config = ImpactConfig::new().with_depth(depth);
2191    let result = analyze_impact(&graph, function, config);
2192
2193    // Convert CallerInfo to FunctionRef for backward compatibility
2194    Ok(result
2195        .callers
2196        .into_iter()
2197        .map(|c| FunctionRef {
2198            file: c.file,
2199            name: c.name,
2200            qualified_name: c.qualified_name,
2201        })
2202        .collect())
2203}
2204
2205/// Find dead (unreachable) code in a project.
2206///
2207/// Identifies functions that are not reachable from any entry point.
2208/// Entry points are detected heuristically (main functions, test functions,
2209/// exported modules, etc.).
2210///
2211/// # Arguments
2212///
2213/// * `path` - Root project directory
2214///
2215/// # Returns
2216///
2217/// A vector of [`FunctionRef`] for functions that appear to be unreachable.
2218///
2219/// # Examples
2220///
2221/// ```no_run
2222/// use go_brrr::find_dead_code;
2223///
2224/// let dead = find_dead_code("./project")?;
2225/// if dead.is_empty() {
2226///     println!("No dead code detected!");
2227/// } else {
2228///     println!("Potentially dead code ({} functions):", dead.len());
2229///     for func in &dead {
2230///         println!("  {}:{}", func.file, func.name);
2231///     }
2232/// }
2233/// # Ok::<(), go_brrr::BrrrError>(())
2234/// ```
2235///
2236/// # Caveats
2237///
2238/// - Dynamic dispatch (e.g., `getattr`, reflection) may cause false positives
2239/// - Decorator-wrapped functions may not be detected as entry points
2240/// - Test discovery patterns are language-specific
2241///
2242/// # Errors
2243///
2244/// Returns [`BrrrError`] if the project cannot be scanned.
2245pub fn find_dead_code(path: &str) -> Result<Vec<FunctionRef>> {
2246    use callgraph::{cache, dead, DeadCodeConfig};
2247
2248    let project = std::path::Path::new(path);
2249    let mut graph = cache::get_or_build_graph_with_config(project, None, false)?;
2250    graph.build_indexes();
2251    let result = dead::analyze_dead_code_with_config(&graph, &DeadCodeConfig::default());
2252
2253    // Convert DeadFunction to FunctionRef for backward compatibility
2254    Ok(result
2255        .dead_functions
2256        .into_iter()
2257        .map(|d| FunctionRef {
2258            file: d.file,
2259            name: d.name,
2260            qualified_name: d.qualified_name,
2261        })
2262        .collect())
2263}
2264
2265/// Warm the call graph cache for a project.
2266///
2267/// Pre-builds the call graph for a project so subsequent queries are fast.
2268/// This is useful for large projects where building the call graph is expensive.
2269///
2270/// # Arguments
2271///
2272/// * `path` - Root project directory
2273/// * `langs` - Optional language filter (e.g., `Some(&["python", "typescript"])`)
2274///
2275/// # Examples
2276///
2277/// ```no_run
2278/// use go_brrr::warm_callgraph;
2279///
2280/// // Pre-warm cache for all languages
2281/// warm_callgraph("./project", None)?;
2282///
2283/// // Pre-warm cache for specific languages
2284/// warm_callgraph("./project", Some(&["python".to_string(), "rust".to_string()]))?;
2285/// # Ok::<(), go_brrr::BrrrError>(())
2286/// ```
2287///
2288/// # Errors
2289///
2290/// Returns [`BrrrError`] if the project cannot be scanned.
2291pub fn warm_callgraph(path: &str, langs: Option<&[String]>) -> Result<()> {
2292    let project = std::path::Path::new(path);
2293    let lang = langs.and_then(|l| l.first().map(|s| s.as_str()));
2294    callgraph::warm_cache_with_config(project, lang, false)
2295}
2296
2297// =============================================================================
2298// Semantic Extraction API
2299// =============================================================================
2300
2301/// Extract semantic units from a project for embedding.
2302///
2303/// Scans the project for source files, extracts functions, classes, and methods,
2304/// calculates token counts, adds semantic tags, and handles chunking for
2305/// oversized units (>8K tokens).
2306///
2307/// # Arguments
2308///
2309/// * `path` - Root project directory
2310/// * `lang` - Programming language to filter by (e.g., "python", "typescript")
2311///
2312/// # Returns
2313///
2314/// Vector of [`EmbeddingUnit`] objects ready for embedding and indexing.
2315///
2316/// # Examples
2317///
2318/// ```no_run
2319/// use go_brrr::extract_semantic_units;
2320///
2321/// let units = extract_semantic_units("./my_project", "python")?;
2322/// for unit in &units {
2323///     println!("{}: {} ({} tokens, tags: {:?})",
2324///         unit.kind,
2325///         unit.name,
2326///         unit.token_count,
2327///         unit.semantic_tags
2328///     );
2329/// }
2330/// # Ok::<(), go_brrr::BrrrError>(())
2331/// ```
2332///
2333/// # Errors
2334///
2335/// Returns [`BrrrError::Io`] if the path does not exist or cannot be read.
2336pub fn extract_semantic_units(path: &str, lang: &str) -> Result<Vec<EmbeddingUnit>> {
2337    semantic::extract_units(path, lang)
2338}
2339
2340/// Extract semantic units with call graph information.
2341///
2342/// Similar to [`extract_semantic_units`], but also builds a call graph and
2343/// populates the `calls` and `called_by` fields for each unit. This provides
2344/// richer semantic context for embedding and retrieval.
2345///
2346/// # Arguments
2347///
2348/// * `path` - Root project directory
2349/// * `lang` - Programming language to filter by (e.g., "python", "typescript")
2350///
2351/// # Returns
2352///
2353/// Vector of [`EmbeddingUnit`] objects with populated call relationships.
2354///
2355/// # Examples
2356///
2357/// ```no_run
2358/// use go_brrr::extract_semantic_units_with_callgraph;
2359///
2360/// let units = extract_semantic_units_with_callgraph("./my_project", "python")?;
2361/// for unit in &units {
2362///     if !unit.calls.is_empty() {
2363///         println!("{} calls: {:?}", unit.name, unit.calls);
2364///     }
2365///     if !unit.called_by.is_empty() {
2366///         println!("{} called by: {:?}", unit.name, unit.called_by);
2367///     }
2368/// }
2369/// # Ok::<(), go_brrr::BrrrError>(())
2370/// ```
2371///
2372/// # Performance
2373///
2374/// This function builds a call graph in addition to extracting units, so it
2375/// is more expensive than [`extract_semantic_units`]. Use this when call
2376/// relationships are important for your use case.
2377///
2378/// # Errors
2379///
2380/// Returns [`BrrrError`] if the path does not exist, cannot be read, or
2381/// call graph building fails.
2382pub fn extract_semantic_units_with_callgraph(path: &str, lang: &str) -> Result<Vec<EmbeddingUnit>> {
2383    semantic::extract_units_with_callgraph(path, lang)
2384}
2385
2386/// Extract semantic units from a single file.
2387///
2388/// Convenience function for extracting units from a single source file
2389/// rather than an entire project.
2390///
2391/// # Arguments
2392///
2393/// * `file_path` - Path to the source file
2394///
2395/// # Returns
2396///
2397/// Vector of [`EmbeddingUnit`] objects from the file.
2398///
2399/// # Examples
2400///
2401/// ```no_run
2402/// use go_brrr::extract_file_units;
2403///
2404/// let units = extract_file_units("./src/main.py")?;
2405/// for unit in &units {
2406///     println!("  {} ({}): {} tokens", unit.name, unit.kind, unit.token_count);
2407/// }
2408/// # Ok::<(), go_brrr::BrrrError>(())
2409/// ```
2410///
2411/// # Errors
2412///
2413/// - [`BrrrError::Io`] if the file cannot be read
2414/// - [`BrrrError::UnsupportedLanguage`] if the file type is not recognized
2415pub fn extract_file_units(file_path: &str) -> Result<Vec<EmbeddingUnit>> {
2416    semantic::extract_units_from_file(file_path)
2417}
2418
2419/// Build embedding text from a semantic unit.
2420///
2421/// Creates a rich text representation suitable for embedding with a language model.
2422/// Includes natural language description, semantic tags, signature, complexity,
2423/// call relationships, and code preview.
2424///
2425/// # Arguments
2426///
2427/// * `unit` - The [`EmbeddingUnit`] to convert to embedding text
2428///
2429/// # Returns
2430///
2431/// A text string containing all relevant information for semantic embedding.
2432///
2433/// # Examples
2434///
2435/// ```no_run
2436/// use go_brrr::{extract_file_units, build_embedding_text};
2437///
2438/// let units = extract_file_units("./src/auth.py")?;
2439/// for unit in &units {
2440///     let text = build_embedding_text(&unit);
2441///     // Send text to embedding model...
2442/// }
2443/// # Ok::<(), go_brrr::BrrrError>(())
2444/// ```
2445pub fn build_embedding_text(unit: &EmbeddingUnit) -> String {
2446    semantic::build_embedding_text(unit)
2447}
2448
2449/// Detect semantic patterns in code.
2450///
2451/// Analyzes code to identify semantic categories like "crud", "validation",
2452/// "error_handling", "async_ops", etc. These patterns are used to enrich
2453/// embedding text for better semantic search retrieval.
2454///
2455/// # Arguments
2456///
2457/// * `code` - Source code to analyze for patterns
2458///
2459/// # Returns
2460///
2461/// Vector of detected pattern names (e.g., `["validation", "error_handling"]`).
2462///
2463/// # Examples
2464///
2465/// ```
2466/// use go_brrr::detect_semantic_patterns;
2467///
2468/// let code = "def validate_user(user): assert user is not None";
2469/// let patterns = detect_semantic_patterns(code);
2470/// assert!(patterns.contains(&"validation".to_string()));
2471/// ```
2472pub fn detect_semantic_patterns(code: &str) -> Vec<String> {
2473    semantic::detect_semantic_patterns(code)
2474}
2475
2476/// Count tokens in text using tiktoken (cl100k_base).
2477///
2478/// Uses the same tokenizer as GPT-4 and Claude for accurate token counting.
2479/// Falls back to character-based estimation if tokenizer is unavailable.
2480///
2481/// # Arguments
2482///
2483/// * `text` - The text to count tokens in
2484///
2485/// # Returns
2486///
2487/// Number of tokens in the text.
2488///
2489/// # Examples
2490///
2491/// ```
2492/// use go_brrr::count_tokens;
2493///
2494/// let tokens = count_tokens("Hello, world!");
2495/// assert!(tokens > 0);
2496/// ```
2497pub fn count_tokens(text: &str) -> usize {
2498    semantic::count_tokens(text)
2499}
2500
2501// =============================================================================
2502// Project Scanner API
2503// =============================================================================
2504
2505/// Scan project for source files of a specific language.
2506///
2507/// This is a high-level convenience function that wraps [`ProjectScanner`].
2508/// For more control over scanning options, use [`ProjectScanner`] directly
2509/// with a [`ScanConfig`].
2510///
2511/// # Arguments
2512///
2513/// * `root` - Project root directory path
2514/// * `language` - Optional language filter (e.g., `Some("python")`, `Some("rust")`)
2515/// * `respect_ignore` - Whether to respect `.gitignore` and `.brrrignore` patterns.
2516///                      Note: Currently always true. This parameter is reserved for
2517///                      future use when ignore bypass is needed.
2518///
2519/// # Returns
2520///
2521/// A [`ScanResult`] containing:
2522/// - `files`: Vector of matching file paths
2523/// - `errors`: Any errors encountered during scanning (permission denied, broken symlinks, etc.)
2524/// - `warnings`: Non-fatal warnings
2525///
2526/// # Examples
2527///
2528/// ```no_run
2529/// use go_brrr::scan_project_files;
2530///
2531/// // Scan all supported source files
2532/// let result = scan_project_files("./src", None, true)?;
2533/// println!("Found {} files", result.files.len());
2534///
2535/// // Scan only Python files
2536/// let py_result = scan_project_files("./src", Some("python"), true)?;
2537/// println!("Found {} Python files", py_result.files.len());
2538///
2539/// // Check for errors
2540/// if result.has_errors() {
2541///     eprintln!("Warning: {}", result.error_summary());
2542/// }
2543/// # Ok::<(), go_brrr::BrrrError>(())
2544/// ```
2545///
2546/// # Errors
2547///
2548/// - [`BrrrError::Io`] if the path does not exist or cannot be read
2549/// - [`BrrrError::UnsupportedLanguage`] if the language is not recognized
2550pub fn scan_project_files(
2551    root: &str,
2552    language: Option<&str>,
2553    _respect_ignore: bool, // Reserved for future use; currently always respects ignore
2554) -> Result<ScanResult> {
2555    let scanner = ProjectScanner::new(root)?;
2556
2557    match language {
2558        Some(lang) => scanner.scan_language_with_errors(lang),
2559        None => scanner.scan_files_with_errors(),
2560    }
2561}
2562
2563/// Scan project for files with specific extensions.
2564///
2565/// This is a high-level convenience function for extension-based filtering.
2566/// Respects `.gitignore` and `.brrrignore` patterns automatically.
2567///
2568/// # Arguments
2569///
2570/// * `root` - Project root directory path
2571/// * `extensions` - File extensions to include (e.g., `&[".py", ".pyi"]` or `&["py", "pyi"]`)
2572///                  Leading dots are optional; matching is case-insensitive.
2573///
2574/// # Returns
2575///
2576/// Vector of file paths matching the extensions.
2577///
2578/// # Examples
2579///
2580/// ```no_run
2581/// use go_brrr::scan_extensions;
2582///
2583/// // Find all Python files (including type stubs)
2584/// let py_files = scan_extensions("./src", &[".py", ".pyi"])?;
2585/// println!("Found {} Python files", py_files.len());
2586///
2587/// // Extensions without leading dots also work
2588/// let rs_files = scan_extensions("./src", &["rs"])?;
2589/// # Ok::<(), go_brrr::BrrrError>(())
2590/// ```
2591///
2592/// # Errors
2593///
2594/// Returns [`BrrrError::Io`] if the path does not exist or cannot be read.
2595pub fn scan_extensions(root: &str, extensions: &[&str]) -> Result<Vec<std::path::PathBuf>> {
2596    let scanner = ProjectScanner::new(root)?;
2597    scanner.scan_extensions(extensions)
2598}
2599
2600/// Get file metadata for a project directory.
2601///
2602/// Scans the project and returns detailed metadata for each source file,
2603/// including file size, modification time, and detected language.
2604///
2605/// # Arguments
2606///
2607/// * `root` - Project root directory path
2608/// * `language` - Optional language filter (e.g., `Some("python")`)
2609///
2610/// # Returns
2611///
2612/// Vector of [`FileMetadata`] containing:
2613/// - `path`: Absolute path to the file
2614/// - `size`: File size in bytes
2615/// - `modified`: Last modification time (if available)
2616/// - `language`: Detected programming language (if recognized)
2617///
2618/// # Examples
2619///
2620/// ```no_run
2621/// use go_brrr::get_project_metadata;
2622///
2623/// // Get metadata for all source files
2624/// let metadata = get_project_metadata("./src", None)?;
2625/// for meta in &metadata {
2626///     println!("{}: {} bytes, lang={:?}",
2627///         meta.path.display(),
2628///         meta.size,
2629///         meta.language
2630///     );
2631/// }
2632///
2633/// // Get metadata for only Rust files
2634/// let rust_meta = get_project_metadata("./src", Some("rust"))?;
2635/// let total_bytes: u64 = rust_meta.iter().map(|m| m.size).sum();
2636/// println!("Total Rust code: {} bytes", total_bytes);
2637/// # Ok::<(), go_brrr::BrrrError>(())
2638/// ```
2639///
2640/// # Errors
2641///
2642/// - [`BrrrError::Io`] if the path does not exist or cannot be read
2643/// - [`BrrrError::UnsupportedLanguage`] if the language is not recognized
2644pub fn get_project_metadata(root: &str, language: Option<&str>) -> Result<Vec<FileMetadata>> {
2645    let scanner = ProjectScanner::new(root)?;
2646
2647    match language {
2648        Some(lang) => scanner.scan_language_with_metadata(lang),
2649        None => scanner.scan_with_metadata(),
2650    }
2651}
2652
2653/// Scan project files with custom configuration.
2654///
2655/// This is the most flexible scanning function, supporting all configuration
2656/// options available in [`ScanConfig`]:
2657/// - Language and extension filtering
2658/// - Include/exclude glob patterns
2659/// - Metadata collection
2660/// - Parallel processing control
2661/// - Error handling strategy
2662///
2663/// # Arguments
2664///
2665/// * `root` - Project root directory path
2666/// * `config` - Scan configuration options
2667///
2668/// # Returns
2669///
2670/// A [`ScanResult`] containing files, metadata (if requested), and errors.
2671///
2672/// # Examples
2673///
2674/// ```no_run
2675/// use go_brrr::{scan_with_config, ScanConfig, ErrorHandling};
2676///
2677/// // Scan Python files, excluding tests, with metadata
2678/// let config = ScanConfig::for_language("python")
2679///     .with_excludes(&["**/test/**", "**/tests/**"])
2680///     .with_metadata()
2681///     .with_error_handling(ErrorHandling::CollectAndContinue);
2682///
2683/// let result = scan_with_config("./src", &config)?;
2684/// println!("Found {} files ({} bytes total)",
2685///     result.files.len(),
2686///     result.total_bytes
2687/// );
2688/// # Ok::<(), go_brrr::BrrrError>(())
2689/// ```
2690///
2691/// # Errors
2692///
2693/// Returns [`BrrrError`] if the path does not exist or scanning fails.
2694pub fn scan_with_config(root: &str, config: &ScanConfig) -> Result<ScanResult> {
2695    let scanner = ProjectScanner::new(root)?;
2696    scanner.scan_with_config(config)
2697}
2698
2699/// Estimate the number of source files in a project.
2700///
2701/// Performs a full directory traversal to count supported source files.
2702/// Useful for progress bars or deciding scan strategy before more expensive
2703/// operations.
2704///
2705/// # Arguments
2706///
2707/// * `root` - Project root directory path
2708///
2709/// # Returns
2710///
2711/// Count of supported source files (respecting `.gitignore` and `.brrrignore`).
2712///
2713/// # Examples
2714///
2715/// ```no_run
2716/// use go_brrr::estimate_file_count;
2717///
2718/// let count = estimate_file_count("./my_project")?;
2719/// println!("Project contains {} source files", count);
2720///
2721/// if count > 1000 {
2722///     println!("Large project - consider using parallel scanning");
2723/// }
2724/// # Ok::<(), go_brrr::BrrrError>(())
2725/// ```
2726///
2727/// # Errors
2728///
2729/// Returns [`BrrrError::Io`] if the path does not exist or cannot be read.
2730pub fn estimate_file_count(root: &str) -> Result<usize> {
2731    let scanner = ProjectScanner::new(root)?;
2732    scanner.estimate_file_count()
2733}
2734
2735// =============================================================================
2736// Function Index API
2737// =============================================================================
2738
2739/// Configuration for function indexing.
2740///
2741/// Controls how functions are discovered and indexed in a project.
2742/// Use the builder pattern to customize behavior.
2743///
2744/// # Examples
2745///
2746/// ```
2747/// use go_brrr::IndexingConfig;
2748///
2749/// // Index only Python files, excluding tests
2750/// let config = IndexingConfig::new()
2751///     .with_language("python")
2752///     .exclude_tests();
2753///
2754/// // Index all languages with parallel processing
2755/// let config = IndexingConfig::new()
2756///     .with_parallel(true);
2757/// ```
2758#[derive(Debug, Clone, Default)]
2759pub struct IndexingConfig {
2760    /// Optional language filter (e.g., "python", "typescript", "rust").
2761    /// If None, indexes all supported languages.
2762    pub language: Option<String>,
2763    /// Whether to respect .gitignore and .brrrignore patterns.
2764    /// Defaults to true.
2765    pub respect_ignore: bool,
2766    /// Whether to use parallel processing for indexing.
2767    /// Defaults to true.
2768    pub parallel: bool,
2769    /// Whether to include test files in the index.
2770    /// Defaults to true.
2771    pub include_tests: bool,
2772}
2773
2774impl IndexingConfig {
2775    /// Create a new indexing configuration with default settings.
2776    ///
2777    /// Defaults:
2778    /// - No language filter (indexes all languages)
2779    /// - Respects ignore patterns
2780    /// - Uses parallel processing
2781    /// - Includes test files
2782    pub fn new() -> Self {
2783        Self {
2784            language: None,
2785            respect_ignore: true,
2786            parallel: true,
2787            include_tests: true,
2788        }
2789    }
2790
2791    /// Set the language filter.
2792    ///
2793    /// Only files matching this language will be indexed.
2794    ///
2795    /// # Arguments
2796    ///
2797    /// * `lang` - Language name (e.g., "python", "typescript", "rust", "go")
2798    pub fn with_language(mut self, lang: &str) -> Self {
2799        self.language = Some(lang.to_string());
2800        self
2801    }
2802
2803    /// Set whether to respect ignore patterns.
2804    ///
2805    /// If true (default), respects .gitignore and .brrrignore.
2806    pub fn with_respect_ignore(mut self, respect: bool) -> Self {
2807        self.respect_ignore = respect;
2808        self
2809    }
2810
2811    /// Set whether to use parallel processing.
2812    ///
2813    /// If true (default), uses rayon for parallel file processing.
2814    pub fn with_parallel(mut self, parallel: bool) -> Self {
2815        self.parallel = parallel;
2816        self
2817    }
2818
2819    /// Exclude test files from the index.
2820    ///
2821    /// Filters out files matching common test patterns.
2822    pub fn exclude_tests(mut self) -> Self {
2823        self.include_tests = false;
2824        self
2825    }
2826
2827    /// Include test files in the index (default).
2828    pub fn include_tests(mut self) -> Self {
2829        self.include_tests = true;
2830        self
2831    }
2832}
2833
2834/// Build a function index for a project.
2835///
2836/// Creates an index of all functions in the project that can be used for
2837/// fast lookups by name, qualified name, file, or class+method. This is the
2838/// foundation for call graph analysis, impact detection, and code navigation.
2839///
2840/// # Arguments
2841///
2842/// * `root` - Project root directory path
2843/// * `language` - Optional language filter (indexes all languages if None)
2844///
2845/// # Returns
2846///
2847/// A [`FunctionIndex`] with multiple lookup strategies:
2848/// - `lookup(name)` - Find all functions with a simple name
2849/// - `lookup_qualified(qname)` - Find by fully qualified name (e.g., "module.Class.method")
2850/// - `lookup_in_file(file, name)` - Find a function in a specific file
2851/// - `lookup_method(class, method)` - Find a method in a specific class
2852/// - `lookup_pattern(pattern)` - Find by partial qualified name pattern
2853///
2854/// # Examples
2855///
2856/// ```no_run
2857/// use go_brrr::build_function_index;
2858///
2859/// // Index all source files in a project
2860/// let index = build_function_index("./src", None)?;
2861/// println!("Indexed {} functions", index.len());
2862///
2863/// // Lookup by simple name (returns all matches)
2864/// let funcs = index.lookup("process_data");
2865/// for f in funcs {
2866///     println!("  Found: {} in {}", f.name, f.file);
2867/// }
2868///
2869/// // Lookup by qualified name (exact match)
2870/// if let Some(func) = index.lookup_qualified("mymodule.MyClass.process_data") {
2871///     println!("Found specific function in {}", func.file);
2872/// }
2873///
2874/// // Lookup by class and method name
2875/// let methods = index.lookup_method("MyClass", "process_data");
2876/// for m in methods {
2877///     println!("  Method: {}", m.qualified_name.as_deref().unwrap_or(&m.name));
2878/// }
2879///
2880/// // Index only Python files
2881/// let py_index = build_function_index("./src", Some("python"))?;
2882/// # Ok::<(), go_brrr::BrrrError>(())
2883/// ```
2884///
2885/// # Performance
2886///
2887/// Building the index requires parsing all matching source files, so it can be
2888/// expensive for large projects. The index is typically built once and reused
2889/// for multiple lookups.
2890///
2891/// # Errors
2892///
2893/// - [`BrrrError::Io`] if the path does not exist or cannot be read
2894/// - [`BrrrError::UnsupportedLanguage`] if the language filter is not recognized
2895pub fn build_function_index(root: &str, language: Option<&str>) -> Result<FunctionIndex> {
2896    use std::path::Path;
2897
2898    let scanner = ProjectScanner::new(root)?;
2899    let project_root = Path::new(root);
2900
2901    let files = match language {
2902        Some(lang) => scanner.scan_language(lang)?,
2903        None => scanner.scan_files()?,
2904    };
2905
2906    FunctionIndex::build_with_root(&files, Some(project_root))
2907}
2908
2909/// Build a function index with custom configuration.
2910///
2911/// Provides more control over indexing behavior than [`build_function_index`].
2912///
2913/// # Arguments
2914///
2915/// * `root` - Project root directory path
2916/// * `config` - Indexing configuration
2917///
2918/// # Returns
2919///
2920/// A [`FunctionIndex`] with the configured settings.
2921///
2922/// # Examples
2923///
2924/// ```no_run
2925/// use go_brrr::{build_function_index_with_config, IndexingConfig};
2926///
2927/// // Index Python files, excluding tests
2928/// let config = IndexingConfig::new()
2929///     .with_language("python")
2930///     .exclude_tests();
2931///
2932/// let index = build_function_index_with_config("./src", &config)?;
2933/// println!("Indexed {} functions (excluding tests)", index.len());
2934/// # Ok::<(), go_brrr::BrrrError>(())
2935/// ```
2936///
2937/// # Errors
2938///
2939/// - [`BrrrError::Io`] if the path does not exist or cannot be read
2940/// - [`BrrrError::UnsupportedLanguage`] if the language filter is not recognized
2941pub fn build_function_index_with_config(root: &str, config: &IndexingConfig) -> Result<FunctionIndex> {
2942    use std::path::Path;
2943
2944    let scanner = ProjectScanner::new(root)?;
2945    let project_root = Path::new(root);
2946
2947    // Build scan config based on configuration options
2948    let mut scan_config = match &config.language {
2949        Some(lang) => ScanConfig::for_language(lang),
2950        None => ScanConfig::default(),
2951    };
2952
2953    // Add test exclusion patterns if needed
2954    if !config.include_tests {
2955        scan_config = scan_config.with_excludes(&[
2956            "**/test/**",
2957            "**/tests/**",
2958            "**/*_test.*",
2959            "**/*_spec.*",
2960            "**/test_*.*",
2961        ]);
2962    }
2963
2964    let result = scanner.scan_with_config(&scan_config)?;
2965    FunctionIndex::build_with_root(&result.files, Some(project_root))
2966}
2967
2968// =============================================================================
2969// Import Analysis API
2970// =============================================================================
2971
2972/// Result of an importer search - a file that imports a given module.
2973#[derive(Debug, Clone)]
2974pub struct ImporterInfo {
2975    /// Path to the file containing the import.
2976    pub file: std::path::PathBuf,
2977    /// The import statement details.
2978    pub import: ImportInfo,
2979}
2980
2981/// Find all files that import a given module.
2982///
2983/// Scans the project for source files and analyzes their import statements
2984/// to find files that import the specified module. This is the reverse of
2985/// `get_imports` - instead of listing what a file imports, it lists what
2986/// files import a specific module.
2987///
2988/// # Arguments
2989///
2990/// * `root` - Project root directory path
2991/// * `module` - Module name to search for importers
2992/// * `language` - Optional language filter (e.g., `Some("python")`)
2993///
2994/// # Matching Rules
2995///
2996/// A file is considered to import the module if any of these match:
2997/// 1. Exact module match: `import.module == module`
2998/// 2. Module is the last component: "json" matches "os.json"
2999/// 3. Module is the first component: "os" matches "os.path"
3000/// 4. Module is in the middle: "path" matches "os.path.join"
3001/// 5. Any of the imported names match: "Path" matches `from pathlib import Path`
3002///
3003/// # Returns
3004///
3005/// Vector of [`ImporterInfo`] containing the file path and import details
3006/// for each file that imports the specified module.
3007///
3008/// # Examples
3009///
3010/// ```no_run
3011/// use go_brrr::get_importers;
3012///
3013/// // Find all files that import the "json" module
3014/// let importers = get_importers("./src", "json", None)?;
3015/// println!("Found {} files importing 'json':", importers.len());
3016/// for importer in &importers {
3017///     println!("  {} at line {}", importer.file.display(), importer.import.line_number);
3018/// }
3019///
3020/// // Find Python files importing "flask"
3021/// let flask_users = get_importers("./src", "flask", Some("python"))?;
3022/// for importer in &flask_users {
3023///     println!("{}: {}", importer.file.display(), importer.import.statement());
3024/// }
3025/// # Ok::<(), go_brrr::BrrrError>(())
3026/// ```
3027///
3028/// # Errors
3029///
3030/// - [`BrrrError::Io`] if the path does not exist or cannot be read
3031/// - [`BrrrError::UnsupportedLanguage`] if the language is not recognized
3032pub fn get_importers(
3033    root: &str,
3034    module: &str,
3035    language: Option<&str>,
3036) -> Result<Vec<ImporterInfo>> {
3037    let scanner = ProjectScanner::new(root)?;
3038
3039    // Get files - optionally filtered by language
3040    let files = match language {
3041        Some(lang) => scanner.scan_language(lang)?,
3042        None => scanner.scan_files()?,
3043    };
3044
3045    let mut importers = Vec::new();
3046
3047    for file_path in files {
3048        // Extract imports from file
3049        let imports = match ast::extract_imports(&file_path) {
3050            Ok(i) => i,
3051            Err(_) => continue, // Skip files that fail to parse
3052        };
3053
3054        // Check if any import matches the module
3055        for import in imports {
3056            if import_matches_module(&import, module) {
3057                importers.push(ImporterInfo {
3058                    file: file_path.clone(),
3059                    import,
3060                });
3061            }
3062        }
3063    }
3064
3065    Ok(importers)
3066}
3067
3068/// Check if an import matches the target module name.
3069///
3070/// # Matching Rules
3071///
3072/// 1. Exact module match: `import.module == module`
3073/// 2. Module is the last component (e.g., "json" matches "os.json")
3074/// 3. Module is the first component (e.g., "os" matches "os.path")
3075/// 4. Module is in the middle (e.g., "path" matches "os.path.join")
3076/// 5. Any of the imported names match (e.g., "Path" matches `from pathlib import Path`)
3077fn import_matches_module(import: &ImportInfo, module: &str) -> bool {
3078    // Exact module match
3079    if import.module == module {
3080        return true;
3081    }
3082
3083    // Module ends with ".{module}" (module is the last component)
3084    if import.module.ends_with(&format!(".{}", module)) {
3085        return true;
3086    }
3087
3088    // Module starts with "{module}." (module is the first component)
3089    if import.module.starts_with(&format!("{}.", module)) {
3090        return true;
3091    }
3092
3093    // Module is in the middle (e.g., "path" matches "os.path.join")
3094    if import.module.contains(&format!(".{}.", module)) {
3095        return true;
3096    }
3097
3098    // Any of the imported names match exactly
3099    if import.names.iter().any(|name| name == module) {
3100        return true;
3101    }
3102
3103    false
3104}
3105
3106// =============================================================================
3107// Intra-File Call Graph API
3108// =============================================================================
3109
3110/// Detailed information about a single intra-file function call.
3111///
3112/// Represents a call from one function to another within the same source file,
3113/// including the exact location of the call site.
3114#[derive(Debug, Clone, serde::Serialize)]
3115pub struct IntraFileCall {
3116    /// Name of the calling function.
3117    pub caller: String,
3118    /// Name of the called function.
3119    pub callee: String,
3120    /// Line number of the call (1-indexed).
3121    pub line: usize,
3122    /// Column number of the call (0-indexed).
3123    pub column: usize,
3124}
3125
3126/// Get function call relationships within a single file.
3127///
3128/// Analyzes a source file and returns a mapping of which functions call which
3129/// other functions, considering only functions defined within the same file.
3130/// This is useful for understanding local code structure without needing to
3131/// build a full project call graph.
3132///
3133/// # Arguments
3134///
3135/// * `file_path` - Path to the source file to analyze
3136///
3137/// # Returns
3138///
3139/// A `HashMap` where keys are caller function names and values are vectors
3140/// of callee function names. Only includes calls to functions defined in the
3141/// same file (intra-file calls). Each caller has a deduplicated list of callees.
3142///
3143/// # Examples
3144///
3145/// ```no_run
3146/// use go_brrr::get_intra_file_calls;
3147///
3148/// let calls = get_intra_file_calls("src/main.py")?;
3149/// for (caller, callees) in &calls {
3150///     println!("{} calls: {:?}", caller, callees);
3151/// }
3152/// // Output:
3153/// // main calls: ["process_data", "cleanup"]
3154/// // process_data calls: ["validate", "transform"]
3155/// # Ok::<(), go_brrr::BrrrError>(())
3156/// ```
3157///
3158/// # Errors
3159///
3160/// - [`BrrrError::Io`] if the file cannot be read
3161/// - [`BrrrError::UnsupportedLanguage`] if the file type is not recognized
3162/// - [`BrrrError::Parse`] if the file cannot be parsed
3163pub fn get_intra_file_calls(file_path: &str) -> Result<std::collections::HashMap<String, Vec<String>>> {
3164    use std::collections::{HashMap, HashSet};
3165    use std::path::Path;
3166
3167    let path = Path::new(file_path);
3168    let registry = LanguageRegistry::global();
3169
3170    // Detect language from file extension
3171    let lang = registry.detect_language(path).ok_or_else(|| {
3172        BrrrError::UnsupportedLanguage(
3173            path.extension()
3174                .and_then(|e| e.to_str())
3175                .unwrap_or("unknown")
3176                .to_string(),
3177        )
3178    })?;
3179
3180    // Read and parse the file
3181    let source = std::fs::read(path)
3182        .map_err(|e| BrrrError::io_with_path(e, path))?;
3183    let mut parser = lang.parser_for_path(path)?;
3184    let tree = parser.parse(&source, None).ok_or_else(|| BrrrError::Parse {
3185        file: file_path.to_string(),
3186        message: "Failed to parse file".to_string(),
3187    })?;
3188
3189    // Extract all function definitions
3190    let module_info = ast::AstExtractor::extract_file(path)?;
3191
3192    // Build set of function names defined in this file (including methods)
3193    let mut defined_functions: HashSet<String> = HashSet::new();
3194    for func in &module_info.functions {
3195        defined_functions.insert(func.name.clone());
3196    }
3197    for class in &module_info.classes {
3198        for method in &class.methods {
3199            defined_functions.insert(method.name.clone());
3200        }
3201    }
3202
3203    // Build function line ranges for determining which function contains a call
3204    struct FuncRange {
3205        name: String,
3206        start_line: usize,
3207        end_line: usize,
3208    }
3209    let mut func_ranges: Vec<FuncRange> = Vec::new();
3210
3211    for func in &module_info.functions {
3212        func_ranges.push(FuncRange {
3213            name: func.name.clone(),
3214            start_line: func.line_number,
3215            end_line: func.end_line_number.unwrap_or(func.line_number),
3216        });
3217    }
3218    for class in &module_info.classes {
3219        for method in &class.methods {
3220            func_ranges.push(FuncRange {
3221                name: method.name.clone(),
3222                start_line: method.line_number,
3223                end_line: method.end_line_number.unwrap_or(method.line_number),
3224            });
3225        }
3226    }
3227
3228    // Sort by start line for efficient lookup
3229    func_ranges.sort_by_key(|r| r.start_line);
3230
3231    // Initialize call map with empty vectors for all defined functions
3232    let mut calls: HashMap<String, Vec<String>> = HashMap::new();
3233    for func_name in &defined_functions {
3234        calls.insert(func_name.clone(), Vec::new());
3235    }
3236
3237    // Use tree-sitter query to find call expressions
3238    let query_str = lang.call_query();
3239    let query = tree_sitter::Query::new(&tree.language(), query_str).map_err(|e| {
3240        BrrrError::TreeSitter(format!("Failed to compile call query: {}", e))
3241    })?;
3242
3243    let mut cursor = tree_sitter::QueryCursor::new();
3244    let mut matches = cursor.matches(&query, tree.root_node(), source.as_slice());
3245
3246    // Process all call matches
3247    use streaming_iterator::StreamingIterator;
3248    while let Some(match_) = matches.next() {
3249        // Find the callee capture (function name being called)
3250        let callee_capture = match_.captures.iter().find(|c| {
3251            let name = &query.capture_names()[c.index as usize];
3252            *name == "callee"
3253        });
3254
3255        if let Some(capture) = callee_capture {
3256            let callee_node = capture.node;
3257            let callee_name = std::str::from_utf8(
3258                &source[callee_node.start_byte()..callee_node.end_byte()],
3259            )
3260            .unwrap_or("")
3261            .to_string();
3262
3263            // Only consider calls to functions defined in this file
3264            if !defined_functions.contains(&callee_name) {
3265                continue;
3266            }
3267
3268            // Find which function contains this call (by line number)
3269            let call_line = callee_node.start_position().row + 1;
3270
3271            // Binary search would be more efficient, but linear is fine for typical file sizes
3272            for func_range in &func_ranges {
3273                if call_line >= func_range.start_line && call_line <= func_range.end_line {
3274                    // Don't add self-calls to the same function unless it's actually recursion
3275                    // (the call site line != function start line)
3276                    calls
3277                        .entry(func_range.name.clone())
3278                        .or_default()
3279                        .push(callee_name.clone());
3280                    break;
3281                }
3282            }
3283        }
3284    }
3285
3286    // Deduplicate callees for each caller
3287    for callees in calls.values_mut() {
3288        callees.sort();
3289        callees.dedup();
3290    }
3291
3292    Ok(calls)
3293}
3294
3295/// Get detailed intra-file call information with line and column numbers.
3296///
3297/// Similar to [`get_intra_file_calls`], but returns detailed information about
3298/// each call site including the exact line and column where the call occurs.
3299/// This is useful for precise navigation and analysis.
3300///
3301/// # Arguments
3302///
3303/// * `file_path` - Path to the source file to analyze
3304///
3305/// # Returns
3306///
3307/// A vector of [`IntraFileCall`] structs, each representing a single call from
3308/// one function to another within the file. Multiple calls from the same caller
3309/// to the same callee will result in multiple entries with different locations.
3310///
3311/// # Examples
3312///
3313/// ```no_run
3314/// use go_brrr::get_intra_file_calls_detailed;
3315///
3316/// let calls = get_intra_file_calls_detailed("src/main.py")?;
3317/// for call in &calls {
3318///     println!("{}:{} - {} calls {}",
3319///         call.line, call.column, call.caller, call.callee);
3320/// }
3321/// // Output:
3322/// // 15:4 - main calls process_data
3323/// // 16:4 - main calls cleanup
3324/// // 25:8 - process_data calls validate
3325/// # Ok::<(), go_brrr::BrrrError>(())
3326/// ```
3327///
3328/// # Errors
3329///
3330/// - [`BrrrError::Io`] if the file cannot be read
3331/// - [`BrrrError::UnsupportedLanguage`] if the file type is not recognized
3332/// - [`BrrrError::Parse`] if the file cannot be parsed
3333pub fn get_intra_file_calls_detailed(file_path: &str) -> Result<Vec<IntraFileCall>> {
3334    use std::collections::HashSet;
3335    use std::path::Path;
3336
3337    let path = Path::new(file_path);
3338    let registry = LanguageRegistry::global();
3339
3340    // Detect language from file extension
3341    let lang = registry.detect_language(path).ok_or_else(|| {
3342        BrrrError::UnsupportedLanguage(
3343            path.extension()
3344                .and_then(|e| e.to_str())
3345                .unwrap_or("unknown")
3346                .to_string(),
3347        )
3348    })?;
3349
3350    // Read and parse the file
3351    let source = std::fs::read(path)
3352        .map_err(|e| BrrrError::io_with_path(e, path))?;
3353    let mut parser = lang.parser_for_path(path)?;
3354    let tree = parser.parse(&source, None).ok_or_else(|| BrrrError::Parse {
3355        file: file_path.to_string(),
3356        message: "Failed to parse file".to_string(),
3357    })?;
3358
3359    // Extract all function definitions
3360    let module_info = ast::AstExtractor::extract_file(path)?;
3361
3362    // Build set of function names defined in this file (including methods)
3363    let mut defined_functions: HashSet<String> = HashSet::new();
3364    for func in &module_info.functions {
3365        defined_functions.insert(func.name.clone());
3366    }
3367    for class in &module_info.classes {
3368        for method in &class.methods {
3369            defined_functions.insert(method.name.clone());
3370        }
3371    }
3372
3373    // Build function line ranges for determining which function contains a call
3374    struct FuncRange {
3375        name: String,
3376        start_line: usize,
3377        end_line: usize,
3378    }
3379    let mut func_ranges: Vec<FuncRange> = Vec::new();
3380
3381    for func in &module_info.functions {
3382        func_ranges.push(FuncRange {
3383            name: func.name.clone(),
3384            start_line: func.line_number,
3385            end_line: func.end_line_number.unwrap_or(func.line_number),
3386        });
3387    }
3388    for class in &module_info.classes {
3389        for method in &class.methods {
3390            func_ranges.push(FuncRange {
3391                name: method.name.clone(),
3392                start_line: method.line_number,
3393                end_line: method.end_line_number.unwrap_or(method.line_number),
3394            });
3395        }
3396    }
3397
3398    // Sort by start line for efficient lookup
3399    func_ranges.sort_by_key(|r| r.start_line);
3400
3401    let mut detailed_calls: Vec<IntraFileCall> = Vec::new();
3402
3403    // Use tree-sitter query to find call expressions
3404    let query_str = lang.call_query();
3405    let query = tree_sitter::Query::new(&tree.language(), query_str).map_err(|e| {
3406        BrrrError::TreeSitter(format!("Failed to compile call query: {}", e))
3407    })?;
3408
3409    let mut cursor = tree_sitter::QueryCursor::new();
3410    let mut matches = cursor.matches(&query, tree.root_node(), source.as_slice());
3411
3412    // Process all call matches
3413    use streaming_iterator::StreamingIterator;
3414    while let Some(match_) = matches.next() {
3415        // Find the callee capture (function name being called)
3416        let callee_capture = match_.captures.iter().find(|c| {
3417            let name = &query.capture_names()[c.index as usize];
3418            *name == "callee"
3419        });
3420
3421        if let Some(capture) = callee_capture {
3422            let callee_node = capture.node;
3423            let callee_name = std::str::from_utf8(
3424                &source[callee_node.start_byte()..callee_node.end_byte()],
3425            )
3426            .unwrap_or("")
3427            .to_string();
3428
3429            // Only consider calls to functions defined in this file
3430            if !defined_functions.contains(&callee_name) {
3431                continue;
3432            }
3433
3434            // Get call location
3435            let position = callee_node.start_position();
3436            let call_line = position.row + 1;
3437            let call_column = position.column;
3438
3439            // Find which function contains this call (by line number)
3440            for func_range in &func_ranges {
3441                if call_line >= func_range.start_line && call_line <= func_range.end_line {
3442                    detailed_calls.push(IntraFileCall {
3443                        caller: func_range.name.clone(),
3444                        callee: callee_name.clone(),
3445                        line: call_line,
3446                        column: call_column,
3447                    });
3448                    break;
3449                }
3450            }
3451        }
3452    }
3453
3454    // Sort by line number for consistent output
3455    detailed_calls.sort_by(|a, b| {
3456        a.line.cmp(&b.line).then_with(|| a.column.cmp(&b.column))
3457    });
3458
3459    Ok(detailed_calls)
3460}
3461
3462// =============================================================================
3463// Tests
3464// =============================================================================
3465
3466#[cfg(test)]
3467mod tests {
3468    use super::*;
3469
3470    #[test]
3471    fn test_public_api_types_exported() {
3472        // Verify that all public types are accessible
3473        fn _assert_types() {
3474            let _: Option<FileTreeEntry> = None;
3475            let _: Option<CodeStructure> = None;
3476            let _: Option<ModuleInfo> = None;
3477            let _: Option<SourceInput> = None;
3478            let _: Option<FunctionInfo> = None;
3479            let _: Option<ClassInfo> = None;
3480            let _: Option<ImportInfo> = None;
3481            let _: Option<CFGInfo> = None;
3482            let _: Option<CFGBlock> = None;
3483            let _: Option<CFGEdge> = None;
3484            let _: Option<BlockId> = None;
3485            let _: Option<DFGInfo> = None;
3486            let _: Option<DataflowEdge> = None;
3487            let _: Option<DataflowKind> = None;
3488            // PDG types
3489            let _: Option<PDGInfo> = None;
3490            let _: Option<ControlDependence> = None;
3491            let _: Option<BranchType> = None;
3492            let _: Option<SliceCriteria> = None;
3493            let _: Option<SliceResult> = None;
3494            let _: Option<SliceMetrics> = None;
3495            let _: Option<CallGraph> = None;
3496            let _: Option<CallEdge> = None;
3497            let _: Option<FunctionRef> = None;
3498            let _: Option<BrrrError> = None;
3499            // Function index types
3500            let _: Option<FunctionIndex> = None;
3501            let _: Option<FunctionDef> = None;
3502            let _: Option<IndexStats> = None;
3503            let _: Option<IndexingConfig> = None;
3504            // Semantic types
3505            let _: Option<EmbeddingUnit> = None;
3506            let _: Option<SearchResult> = None;
3507            let _: Option<UnitKind> = None;
3508            let _: Option<CodeComplexity> = None;
3509            let _: Option<ChunkInfo> = None;
3510            // Scanner types
3511            let _: Option<ProjectScanner> = None;
3512            let _: Option<ScanConfig> = None;
3513            let _: Option<ScanResult> = None;
3514            let _: Option<ScanError> = None;
3515            let _: Option<ScanErrorKind> = None;
3516            let _: Option<FileMetadata> = None;
3517            let _: Option<ErrorHandling> = None;
3518            // Dead code analysis types
3519            let _: Option<DeadCodeConfig> = None;
3520            let _: Option<DeadCodeResult> = None;
3521            let _: Option<DeadCodeStats> = None;
3522            let _: Option<DeadFunction> = None;
3523            let _: Option<DeadReason> = None;
3524            let _: Option<EntryPointKind> = None;
3525            // Impact analysis types
3526            let _: Option<ImpactConfig> = None;
3527            let _: Option<ImpactResult> = None;
3528            let _: Option<CallerInfo> = None;
3529            // Architecture analysis types
3530            let _: Option<ArchAnalysis> = None;
3531            let _: Option<ArchStats> = None;
3532            let _: Option<CycleDependency> = None;
3533            // Call graph cache types
3534            let _: Option<CachedCallGraph> = None;
3535            let _: Option<CachedEdge> = None;
3536            // Import analysis types
3537            let _: Option<ImporterInfo> = None;
3538            // Intra-file call graph types
3539            let _: Option<IntraFileCall> = None;
3540            // LLM context types
3541            let _: Option<FunctionContext> = None;
3542            let _: Option<RelevantContext> = None;
3543        }
3544    }
3545
3546    #[test]
3547    fn test_public_api_functions_exist() {
3548        // Verify that all public API functions are callable
3549        // (compilation test - doesn't actually run them)
3550        fn _assert_functions() {
3551            let _ = get_tree as fn(&str, Option<&str>, bool, bool) -> Result<FileTreeEntry>;
3552            let _ = get_structure as fn(&str, Option<&str>, usize, bool) -> Result<CodeStructure>;
3553            let _ = extract_file as fn(&str, Option<&str>) -> Result<ModuleInfo>;
3554            let _ = extract_file_unchecked as fn(&str) -> Result<ModuleInfo>;
3555            let _ = extract_from_source as fn(&str, &str) -> Result<ModuleInfo>;
3556            let _ = get_imports as fn(&str, Option<&str>) -> Result<Vec<ImportInfo>>;
3557            let _ = get_context as fn(&str, &str, usize) -> Result<serde_json::Value>;
3558            let _ = get_cfg as fn(&str, &str, Option<&str>) -> Result<CFGInfo>;
3559            let _ = get_cfg_auto as fn(&str, &str) -> Result<CFGInfo>;
3560            let _ = get_cfg_from_source as fn(&str, &str, &str) -> Result<CFGInfo>;
3561            let _ = get_dfg as fn(&str, &str, Option<&str>) -> Result<DFGInfo>;
3562            let _ = get_dfg_auto as fn(&str, &str) -> Result<DFGInfo>;
3563            let _ = get_dfg_from_source as fn(&str, &str, &str) -> Result<DFGInfo>;
3564            let _ = get_pdg as fn(&str, &str, Option<&str>) -> Result<PDGInfo>;
3565            let _ = get_pdg_auto as fn(&str, &str) -> Result<PDGInfo>;
3566            let _ = get_slice as fn(&str, &str, usize, Option<&str>, Option<&str>, Option<&str>) -> Result<Vec<usize>>;
3567            let _ = get_slice_from_source as fn(&str, &str, usize, Option<&str>, Option<&str>, &str) -> Result<Vec<usize>>;
3568            let _ = get_backward_slice as fn(&str, &str, usize) -> Result<Vec<usize>>;
3569            let _ = get_slice_dfg_only as fn(&str, &str, usize) -> Result<Vec<usize>>;
3570            let _ = get_forward_slice as fn(&str, &str, usize) -> Result<Vec<usize>>;
3571            let _ = get_pdg_slice as fn(&str, &str, usize, &str) -> Result<Vec<usize>>;
3572            // PDG slicing functions (lower-level API)
3573            let _ = pdg_backward_slice as fn(&PDGInfo, &SliceCriteria) -> SliceResult;
3574            let _ = pdg_forward_slice as fn(&PDGInfo, &SliceCriteria) -> SliceResult;
3575            let _ = build_callgraph as fn(&str) -> Result<CallGraph>;
3576            let _ = get_impact as fn(&str, &str, usize) -> Result<Vec<FunctionRef>>;
3577            let _ = find_dead_code as fn(&str) -> Result<Vec<FunctionRef>>;
3578            let _ = warm_callgraph as fn(&str, Option<&[String]>) -> Result<()>;
3579            // Semantic API functions
3580            let _ = extract_semantic_units as fn(&str, &str) -> Result<Vec<EmbeddingUnit>>;
3581            let _ = extract_semantic_units_with_callgraph as fn(&str, &str) -> Result<Vec<EmbeddingUnit>>;
3582            let _ = extract_file_units as fn(&str) -> Result<Vec<EmbeddingUnit>>;
3583            let _ = build_embedding_text as fn(&EmbeddingUnit) -> String;
3584            let _ = count_tokens as fn(&str) -> usize;
3585            // Scanner API functions
3586            let _ = scan_project_files as fn(&str, Option<&str>, bool) -> Result<ScanResult>;
3587            let _ = scan_extensions as fn(&str, &[&str]) -> Result<Vec<std::path::PathBuf>>;
3588            let _ = get_project_metadata as fn(&str, Option<&str>) -> Result<Vec<FileMetadata>>;
3589            let _ = scan_with_config as fn(&str, &ScanConfig) -> Result<ScanResult>;
3590            let _ = estimate_file_count as fn(&str) -> Result<usize>;
3591            // Function index API functions
3592            let _ = build_function_index as fn(&str, Option<&str>) -> Result<FunctionIndex>;
3593            let _ = build_function_index_with_config as fn(&str, &IndexingConfig) -> Result<FunctionIndex>;
3594            // Import analysis API functions
3595            let _ = get_importers as fn(&str, &str, Option<&str>) -> Result<Vec<ImporterInfo>>;
3596            // Intra-file call graph API functions
3597            let _ = get_intra_file_calls as fn(&str) -> Result<std::collections::HashMap<String, Vec<String>>>;
3598            let _ = get_intra_file_calls_detailed as fn(&str) -> Result<Vec<IntraFileCall>>;
3599        }
3600    }
3601
3602    #[test]
3603    fn test_semantic_constants_exported() {
3604        // Verify semantic constants are accessible
3605        assert!(MAX_EMBEDDING_TOKENS > 0);
3606        assert!(MAX_CODE_PREVIEW_TOKENS > 0);
3607        assert!(CHUNK_OVERLAP_TOKENS > 0);
3608    }
3609
3610    #[test]
3611    fn test_semantic_pattern_exports() {
3612        // Verify SemanticPattern type is accessible
3613        let pattern = &SEMANTIC_PATTERNS[0];
3614        let _: &SemanticPattern = pattern;
3615        assert!(!pattern.name.is_empty());
3616        assert!(!pattern.pattern.is_empty());
3617
3618        // Verify detect_semantic_patterns function works
3619        let patterns = detect_semantic_patterns("def validate_user(): assert x");
3620        assert!(patterns.contains(&"validation".to_string()));
3621
3622        // Verify SEMANTIC_PATTERNS constant is accessible and non-empty
3623        assert!(!SEMANTIC_PATTERNS.is_empty());
3624    }
3625
3626    #[test]
3627    fn test_callgraph_advanced_type_exports() {
3628        // Verify dead code analysis types and defaults
3629        let dead_config = DeadCodeConfig::default();
3630        assert!(!dead_config.include_public_api_patterns);
3631        assert!(dead_config.min_confidence > 0.0);
3632        let _: DeadCodeResult;
3633        let _: DeadCodeStats;
3634        let _: DeadFunction;
3635        let _: DeadReason;
3636
3637        // Verify entry point detection types
3638        let kind = classify_entry_point("main");
3639        assert_eq!(kind, Some(EntryPointKind::Main));
3640        let test_kind = classify_entry_point("test_something");
3641        assert_eq!(test_kind, Some(EntryPointKind::Test));
3642
3643        // Verify impact analysis types and defaults
3644        let impact_config = ImpactConfig::default();
3645        assert_eq!(impact_config.max_depth, 0);
3646        assert!(!impact_config.exclude_tests);
3647        let _: ImpactResult;
3648        let _: CallerInfo;
3649
3650        // Verify architecture analysis types
3651        let _: ArchAnalysis;
3652        let _: ArchStats;
3653        let _: CycleDependency;
3654
3655        // Verify cache types
3656        let _: CachedCallGraph;
3657        let _: CachedEdge;
3658
3659        // Verify analysis functions are accessible
3660        let _ = analyze_dead_code as fn(&CallGraph) -> DeadCodeResult;
3661        let _ = analyze_dead_code_with_config as fn(&CallGraph, &DeadCodeConfig) -> DeadCodeResult;
3662        let _ = analyze_impact as fn(&CallGraph, &str, ImpactConfig) -> ImpactResult;
3663        let _ = detect_entry_points_with_config as fn(&CallGraph, &DeadCodeConfig) -> Vec<FunctionRef>;
3664        let _ = analyze_architecture as fn(&CallGraph) -> ArchAnalysis;
3665    }
3666
3667    #[test]
3668    fn test_extract_from_source() {
3669        let source = r#"
3670def greet(name: str) -> str:
3671    """Say hello."""
3672    return f"Hello, {name}!"
3673
3674class Greeter:
3675    def __init__(self, prefix: str):
3676        self.prefix = prefix
3677"#;
3678        let module = extract_from_source(source, "python").unwrap();
3679        assert_eq!(module.language, "python");
3680        assert_eq!(module.path, "<string>");
3681        assert_eq!(module.functions.len(), 1);
3682        assert_eq!(module.functions[0].name, "greet");
3683        assert_eq!(module.classes.len(), 1);
3684        assert_eq!(module.classes[0].name, "Greeter");
3685    }
3686
3687    #[test]
3688    fn test_get_cfg_from_source() {
3689        let source = r#"
3690def process(x):
3691    if x > 0:
3692        return x * 2
3693    return 0
3694"#;
3695        let cfg = get_cfg_from_source(source, "process", "python").unwrap();
3696        assert_eq!(cfg.function_name, "process");
3697        // Should have at least entry, if-branch, else-branch, and exit
3698        assert!(cfg.blocks.len() >= 2, "CFG should have multiple blocks");
3699    }
3700
3701    #[test]
3702    fn test_get_dfg_from_source() {
3703        let source = r#"
3704def compute(x, y):
3705    z = x + y
3706    result = z * 2
3707    return result
3708"#;
3709        let dfg = get_dfg_from_source(source, "compute", "python").unwrap();
3710        assert_eq!(dfg.function_name, "compute");
3711        assert!(dfg.definitions.contains_key("z"), "Should track 'z' definition");
3712        assert!(dfg.definitions.contains_key("result"), "Should track 'result' definition");
3713        assert!(dfg.uses.contains_key("x"), "Should track 'x' use");
3714        assert!(dfg.uses.contains_key("y"), "Should track 'y' use");
3715    }
3716
3717    #[test]
3718    fn test_get_slice_from_source() {
3719        let source = r#"
3720def compute(x):
3721    a = x + 1
3722    b = a * 2
3723    c = b + x
3724    return c
3725"#;
3726        // Backward slice from line 5 (c = b + x)
3727        let slice = get_slice_from_source(source, "compute", 5, None, None, "python").unwrap();
3728        // Should include lines affecting line 5 (definitions of b, a, x)
3729        assert!(!slice.is_empty(), "Backward slice should not be empty");
3730
3731        // Forward slice from line 3 (a = x + 1)
3732        let fwd_slice = get_slice_from_source(source, "compute", 3, Some("forward"), None, "python").unwrap();
3733        // Should include lines affected by line 3
3734        assert!(!fwd_slice.is_empty(), "Forward slice should not be empty");
3735    }
3736
3737    #[test]
3738    fn test_source_input_enum() {
3739        // Test SourceInput::Path variant
3740        let path_input: SourceInput = SourceInput::Path("./test.py");
3741        match &path_input {
3742            SourceInput::Path(p) => assert_eq!(*p, "./test.py"),
3743            _ => panic!("Expected Path variant"),
3744        }
3745
3746        // Test SourceInput::Source variant
3747        let source_input: SourceInput = SourceInput::Source {
3748            code: "def foo(): pass",
3749            language: "python",
3750        };
3751        match &source_input {
3752            SourceInput::Source { code, language } => {
3753                assert_eq!(*code, "def foo(): pass");
3754                assert_eq!(*language, "python");
3755            }
3756            _ => panic!("Expected Source variant"),
3757        }
3758
3759        // Test resolve for Source variant
3760        let (bytes, lang, path) = source_input.resolve().unwrap();
3761        assert_eq!(bytes, b"def foo(): pass");
3762        assert_eq!(lang.name(), "python");
3763        assert!(path.is_none());
3764    }
3765
3766    #[test]
3767    fn test_get_intra_file_calls_python() {
3768        use std::io::Write;
3769        use tempfile::NamedTempFile;
3770
3771        let source = r#"
3772def helper():
3773    return 42
3774
3775def process(x):
3776    result = helper()
3777    return result * 2
3778
3779def main():
3780    value = process(10)
3781    helper()
3782    return value
3783"#;
3784        let mut file = tempfile::Builder::new()
3785            .suffix(".py")
3786            .tempfile()
3787            .unwrap();
3788        file.write_all(source.as_bytes()).unwrap();
3789
3790        let calls = get_intra_file_calls(file.path().to_str().unwrap()).unwrap();
3791
3792        // Check that main calls both process and helper
3793        assert!(calls.contains_key("main"), "Should have main in call map");
3794        let main_calls = calls.get("main").unwrap();
3795        assert!(main_calls.contains(&"process".to_string()), "main should call process");
3796        assert!(main_calls.contains(&"helper".to_string()), "main should call helper");
3797
3798        // Check that process calls helper
3799        assert!(calls.contains_key("process"), "Should have process in call map");
3800        let process_calls = calls.get("process").unwrap();
3801        assert!(process_calls.contains(&"helper".to_string()), "process should call helper");
3802
3803        // Check that helper has no intra-file calls
3804        assert!(calls.contains_key("helper"), "Should have helper in call map");
3805        let helper_calls = calls.get("helper").unwrap();
3806        assert!(helper_calls.is_empty(), "helper should not call any local functions");
3807    }
3808
3809    #[test]
3810    fn test_get_intra_file_calls_detailed_python() {
3811        use std::io::Write;
3812        use tempfile::NamedTempFile;
3813
3814        let source = r#"def helper():
3815    return 42
3816
3817def process(x):
3818    result = helper()
3819    return result * 2
3820
3821def main():
3822    value = process(10)
3823    helper()
3824    return value
3825"#;
3826        let mut file = tempfile::Builder::new()
3827            .suffix(".py")
3828            .tempfile()
3829            .unwrap();
3830        file.write_all(source.as_bytes()).unwrap();
3831
3832        let calls = get_intra_file_calls_detailed(file.path().to_str().unwrap()).unwrap();
3833
3834        // Should have at least 3 intra-file calls
3835        assert!(calls.len() >= 3, "Should have at least 3 intra-file calls, got {}", calls.len());
3836
3837        // Check that calls are sorted by line number
3838        for window in calls.windows(2) {
3839            assert!(
3840                window[0].line <= window[1].line,
3841                "Calls should be sorted by line number"
3842            );
3843        }
3844
3845        // Check that all calls have valid caller and callee names
3846        for call in &calls {
3847            assert!(!call.caller.is_empty(), "Caller name should not be empty");
3848            assert!(!call.callee.is_empty(), "Callee name should not be empty");
3849            assert!(call.line > 0, "Line number should be positive");
3850        }
3851    }
3852
3853    #[test]
3854    fn test_get_intra_file_calls_typescript() {
3855        use std::io::Write;
3856        use tempfile::NamedTempFile;
3857
3858        let source = r#"
3859function helper(): number {
3860    return 42;
3861}
3862
3863function process(x: number): number {
3864    const result = helper();
3865    return result * 2;
3866}
3867
3868function main(): number {
3869    const value = process(10);
3870    helper();
3871    return value;
3872}
3873"#;
3874        let mut file = tempfile::Builder::new()
3875            .suffix(".ts")
3876            .tempfile()
3877            .unwrap();
3878        file.write_all(source.as_bytes()).unwrap();
3879
3880        let calls = get_intra_file_calls(file.path().to_str().unwrap()).unwrap();
3881
3882        // Check that main calls both process and helper
3883        assert!(calls.contains_key("main"), "Should have main in call map");
3884        let main_calls = calls.get("main").unwrap();
3885        assert!(main_calls.contains(&"process".to_string()), "main should call process");
3886        assert!(main_calls.contains(&"helper".to_string()), "main should call helper");
3887    }
3888
3889    #[test]
3890    fn test_get_intra_file_calls_with_class_methods() {
3891        use std::io::Write;
3892        use tempfile::NamedTempFile;
3893
3894        let source = r#"
3895def standalone():
3896    return 1
3897
3898class Calculator:
3899    def add(self, a, b):
3900        return a + b
3901
3902    def compute(self, x):
3903        result = self.add(x, 1)
3904        standalone()
3905        return result
3906"#;
3907        let mut file = tempfile::Builder::new()
3908            .suffix(".py")
3909            .tempfile()
3910            .unwrap();
3911        file.write_all(source.as_bytes()).unwrap();
3912
3913        let calls = get_intra_file_calls(file.path().to_str().unwrap()).unwrap();
3914
3915        // Methods should be tracked
3916        assert!(calls.contains_key("add"), "Should have add method in call map");
3917        assert!(calls.contains_key("compute"), "Should have compute method in call map");
3918        assert!(calls.contains_key("standalone"), "Should have standalone in call map");
3919
3920        // compute should call standalone (direct function call)
3921        let compute_calls = calls.get("compute").unwrap();
3922        assert!(
3923            compute_calls.contains(&"standalone".to_string()),
3924            "compute should call standalone"
3925        );
3926    }
3927
3928    #[test]
3929    fn test_get_intra_file_calls_no_external_calls() {
3930        use std::io::Write;
3931        use tempfile::NamedTempFile;
3932
3933        let source = r#"
3934import os
3935
3936def local_func():
3937    return 42
3938
3939def main():
3940    # This calls os.path.join which is external
3941    path = os.path.join("a", "b")
3942    # This calls local_func which is internal
3943    value = local_func()
3944    # This calls print which is builtin/external
3945    print(value)
3946    return value
3947"#;
3948        let mut file = tempfile::Builder::new()
3949            .suffix(".py")
3950            .tempfile()
3951            .unwrap();
3952        file.write_all(source.as_bytes()).unwrap();
3953
3954        let calls = get_intra_file_calls(file.path().to_str().unwrap()).unwrap();
3955
3956        let main_calls = calls.get("main").unwrap();
3957        // Should only contain local_func, not os, join, or print
3958        assert_eq!(
3959            main_calls.len(), 1,
3960            "main should only call one local function, got {:?}", main_calls
3961        );
3962        assert!(
3963            main_calls.contains(&"local_func".to_string()),
3964            "main should call local_func"
3965        );
3966    }
3967
3968    #[test]
3969    fn test_get_intra_file_calls_empty_file() {
3970        use std::io::Write;
3971        use tempfile::NamedTempFile;
3972
3973        let source = "# Empty Python file with just a comment\n";
3974        let mut file = tempfile::Builder::new()
3975            .suffix(".py")
3976            .tempfile()
3977            .unwrap();
3978        file.write_all(source.as_bytes()).unwrap();
3979
3980        let calls = get_intra_file_calls(file.path().to_str().unwrap()).unwrap();
3981
3982        // Should be empty since there are no functions
3983        assert!(calls.is_empty(), "Empty file should have no calls");
3984    }
3985
3986    #[test]
3987    fn test_get_intra_file_calls_unsupported_language() {
3988        use std::io::Write;
3989        use tempfile::NamedTempFile;
3990
3991        let source = "Some random content";
3992        let mut file = tempfile::Builder::new()
3993            .suffix(".xyz")
3994            .tempfile()
3995            .unwrap();
3996        file.write_all(source.as_bytes()).unwrap();
3997
3998        let result = get_intra_file_calls(file.path().to_str().unwrap());
3999        assert!(result.is_err(), "Should return error for unsupported language");
4000        assert!(matches!(result, Err(BrrrError::UnsupportedLanguage(_))));
4001    }
4002
4003    // =========================================================================
4004    // FunctionContext and RelevantContext Tests
4005    // =========================================================================
4006
4007    #[test]
4008    fn test_function_context_minimal() {
4009        let ctx = FunctionContext::minimal("test_func", "src/main.rs", 10, "rust");
4010        assert_eq!(ctx.name, "test_func");
4011        assert_eq!(ctx.file, "src/main.rs");
4012        assert_eq!(ctx.start_line, 10);
4013        assert_eq!(ctx.end_line, 10);
4014        assert_eq!(ctx.language, "rust");
4015        assert!(ctx.signature.is_none());
4016        assert!(ctx.docstring.is_none());
4017        assert!(ctx.source.is_empty());
4018    }
4019
4020    #[test]
4021    fn test_function_context_from_function_info() {
4022        let info = FunctionInfo {
4023            name: "process".to_string(),
4024            params: vec!["x: int".to_string(), "y: int".to_string()],
4025            return_type: Some("int".to_string()),
4026            docstring: Some("Process two numbers.".to_string()),
4027            is_method: false,
4028            is_async: false,
4029            decorators: vec![],
4030            line_number: 2,
4031            end_line_number: Some(5),
4032            language: "python".to_string(),
4033        };
4034
4035        let source = "# Header\ndef process(x: int, y: int) -> int:\n    \"\"\"Process two numbers.\"\"\"\n    return x + y\n# Footer";
4036        let ctx = FunctionContext::from_function_info(&info, "src/main.py", source, "python");
4037
4038        assert_eq!(ctx.name, "process");
4039        assert_eq!(ctx.file, "src/main.py");
4040        assert_eq!(ctx.start_line, 2);
4041        assert_eq!(ctx.end_line, 5);
4042        assert_eq!(ctx.language, "python");
4043        assert!(ctx.signature.is_some());
4044        assert!(ctx.signature.as_ref().unwrap().contains("process"));
4045        assert_eq!(ctx.docstring, Some("Process two numbers.".to_string()));
4046        // Source should contain lines 2-4 (0-indexed: 1-4)
4047        assert!(ctx.source.contains("def process"));
4048    }
4049
4050    #[test]
4051    fn test_function_context_serialization() {
4052        let ctx = FunctionContext::minimal("test", "test.py", 1, "python");
4053        let json = serde_json::to_string(&ctx).unwrap();
4054        assert!(json.contains("\"name\":\"test\""));
4055        assert!(json.contains("\"file\":\"test.py\""));
4056        assert!(json.contains("\"language\":\"python\""));
4057
4058        // Deserialize back
4059        let deserialized: FunctionContext = serde_json::from_str(&json).unwrap();
4060        assert_eq!(deserialized.name, ctx.name);
4061        assert_eq!(deserialized.file, ctx.file);
4062        assert_eq!(deserialized.language, ctx.language);
4063    }
4064
4065    #[test]
4066    fn test_relevant_context_new() {
4067        let entry = FunctionContext::minimal("main", "src/main.rs", 1, "rust");
4068        let ctx = RelevantContext::new(entry, 2);
4069
4070        assert_eq!(ctx.entry.name, "main");
4071        assert_eq!(ctx.depth, 2);
4072        assert!(ctx.callees.is_empty());
4073        assert!(ctx.callers.is_empty());
4074        assert_eq!(ctx.token_count, 0);
4075    }
4076
4077    #[test]
4078    fn test_relevant_context_builder_pattern() {
4079        let entry = FunctionContext::minimal("main", "src/main.rs", 1, "rust");
4080        let callee = FunctionContext::minimal("helper", "src/utils.rs", 10, "rust");
4081        let caller = FunctionContext::minimal("test_main", "tests/test.rs", 5, "rust");
4082
4083        let ctx = RelevantContext::new(entry, 2)
4084            .with_callee(callee)
4085            .with_caller(caller)
4086            .with_token_count(500);
4087
4088        assert_eq!(ctx.callees.len(), 1);
4089        assert_eq!(ctx.callees[0].name, "helper");
4090        assert_eq!(ctx.callers.len(), 1);
4091        assert_eq!(ctx.callers[0].name, "test_main");
4092        assert_eq!(ctx.token_count, 500);
4093        assert_eq!(ctx.function_count(), 3);
4094    }
4095
4096    #[test]
4097    fn test_relevant_context_estimate_tokens() {
4098        let mut entry = FunctionContext::minimal("main", "src/main.rs", 1, "rust");
4099        entry.source = "fn main() { println!(\"hello\"); }".to_string(); // 33 chars
4100
4101        let mut callee = FunctionContext::minimal("helper", "src/utils.rs", 10, "rust");
4102        callee.source = "fn helper() {}".to_string(); // 14 chars
4103
4104        let mut ctx = RelevantContext::new(entry, 1).with_callee(callee);
4105        ctx.estimate_tokens();
4106
4107        // Total: 47 chars / 4 = 11 tokens (integer division)
4108        assert_eq!(ctx.token_count, 11);
4109    }
4110
4111    #[test]
4112    fn test_relevant_context_to_llm_string() {
4113        let mut entry = FunctionContext::minimal("process", "src/main.py", 10, "python");
4114        entry.signature = Some("def process(x: int) -> int".to_string());
4115        entry.docstring = Some("Process a number.".to_string());
4116
4117        let callee = FunctionContext::minimal("helper", "src/utils.py", 25, "python");
4118        let caller = FunctionContext::minimal("main", "src/app.py", 5, "python");
4119
4120        let ctx = RelevantContext::new(entry, 2)
4121            .with_callee(callee)
4122            .with_caller(caller)
4123            .with_token_count(100);
4124
4125        let output = ctx.to_llm_string();
4126
4127        // Verify structure
4128        assert!(output.contains("## Code Context: process (depth=2)"));
4129        assert!(output.contains("### Entry Point: process (main.py:10)"));
4130        assert!(output.contains("def process(x: int) -> int"));
4131        assert!(output.contains("> Process a number."));
4132        assert!(output.contains("### Calls:"));
4133        assert!(output.contains("- helper (utils.py:25)"));
4134        assert!(output.contains("### Called By:"));
4135        assert!(output.contains("- main (app.py:5)"));
4136        assert!(output.contains("Token estimate: 100"));
4137        assert!(output.contains("Functions: 3"));
4138    }
4139
4140    #[test]
4141    fn test_relevant_context_serialization() {
4142        let entry = FunctionContext::minimal("test", "test.py", 1, "python");
4143        let callee = FunctionContext::minimal("helper", "helper.py", 5, "python");
4144
4145        let ctx = RelevantContext::new(entry, 1)
4146            .with_callee(callee)
4147            .with_token_count(50);
4148
4149        let json = serde_json::to_string(&ctx).unwrap();
4150        assert!(json.contains("\"depth\":1"));
4151        assert!(json.contains("\"token_count\":50"));
4152
4153        // Deserialize back
4154        let deserialized: RelevantContext = serde_json::from_str(&json).unwrap();
4155        assert_eq!(deserialized.entry.name, "test");
4156        assert_eq!(deserialized.callees.len(), 1);
4157        assert_eq!(deserialized.depth, 1);
4158        assert_eq!(deserialized.token_count, 50);
4159    }
4160
4161    #[test]
4162    fn test_context_types_exported() {
4163        // Verify FunctionContext and RelevantContext are accessible
4164        fn _assert_context_types() {
4165            let _: Option<FunctionContext> = None;
4166            let _: Option<RelevantContext> = None;
4167        }
4168    }
4169}