Skip to main content

tldr_cli/commands/bugbot/l2/
mod.rs

1//! L2 deep-analysis engine framework for bugbot.
2//!
3//! This module provides the trait, types, and registry for L2 engines that
4//! perform deeper static analysis beyond diff-level heuristics. Each engine
5//! targets specific finding types (e.g., null-deref, use-after-move).
6//!
7//! # Architecture
8//!
9//! ```text
10//! l2_engine_registry() -> Vec<Box<dyn L2Engine>>
11//!       |
12//!       v
13//!   for engine in engines {
14//!       engine.analyze(&ctx) -> L2AnalyzerOutput
15//!   }
16//! ```
17
18pub mod composition;
19pub mod context;
20pub mod daemon_client;
21pub mod dedup;
22pub mod engines;
23pub mod findings;
24pub mod ir;
25pub mod types;
26
27pub use context::L2Context;
28pub use types::*;
29
30
31/// Trait for L2 deep-analysis engines.
32///
33/// Implementations must be object-safe so they can be stored as
34/// `Box<dyn L2Engine>` in the engine registry. Each engine declares:
35///
36/// - A unique name for logging and identification
37/// - The finding types it can produce (e.g., `["null-deref", "uninitialized-read"]`)
38/// - Which languages it supports (empty means language-agnostic)
39/// - The analysis entry point
40pub trait L2Engine: Send + Sync {
41    /// Unique human-readable name for this engine (used in logging and reports).
42    fn name(&self) -> &'static str;
43
44    /// The set of finding type identifiers this engine can produce.
45    fn finding_types(&self) -> &[&'static str];
46
47    /// Languages this engine supports. An empty slice means language-agnostic
48    /// (the engine handles all languages or performs language-independent analysis).
49    fn languages(&self) -> &[tldr_core::Language] {
50        &[]
51    }
52
53    /// Run analysis on the provided context and return findings.
54    fn analyze(&self, ctx: &context::L2Context) -> types::L2AnalyzerOutput;
55}
56
57/// Returns the set of all registered L2 engines.
58///
59/// Contains the TldrDifferentialEngine that invokes `tldr` CLI commands
60/// for differential analysis. The pipeline orchestrator invokes each
61/// engine's `analyze` method.
62pub fn l2_engine_registry() -> Vec<Box<dyn L2Engine>> {
63    vec![
64        Box::new(engines::tldr_differential::TldrDifferentialEngine::new()),
65    ]
66}
67
68#[cfg(test)]
69mod tests {
70    use super::*;
71
72    /// Registry must contain only TldrDifferentialEngine.
73    #[test]
74    fn test_l2_engine_registry_contains_engines() {
75        let engines = l2_engine_registry();
76        assert_eq!(engines.len(), 1, "Registry should contain exactly 1 engine (TldrDifferentialEngine)");
77        assert!(
78            engines.iter().any(|e| e.name() == "TldrDifferentialEngine"),
79            "Registry must contain TldrDifferentialEngine"
80        );
81    }
82
83    /// TldrDifferentialEngine should be registered and accessible via the registry.
84    #[test]
85    fn test_tldr_engine_registered() {
86        let engines = l2_engine_registry();
87        let engine = engines.iter().find(|e| e.name() == "TldrDifferentialEngine");
88        assert!(engine.is_some(), "TldrDifferentialEngine must be in registry");
89        let engine = engine.unwrap();
90        assert_eq!(engine.finding_types().len(), 11);
91    }
92
93    /// Verify L2Engine is object-safe by constructing a mock implementation,
94    /// storing it as `Box<dyn L2Engine>`, and calling every trait method.
95    #[test]
96    fn test_l2_engine_trait_object_safe() {
97        struct MockEngine;
98
99        impl L2Engine for MockEngine {
100            fn name(&self) -> &'static str {
101                "MockEngine"
102            }
103
104            fn finding_types(&self) -> &[&'static str] {
105                &["test-finding"]
106            }
107
108            fn analyze(&self, _ctx: &context::L2Context) -> types::L2AnalyzerOutput {
109                types::L2AnalyzerOutput {
110                    findings: vec![],
111                    status: types::AnalyzerStatus::Complete,
112                    duration_ms: 0,
113                    functions_analyzed: 0,
114                    functions_skipped: 0,
115                }
116            }
117        }
118
119        let engine: Box<dyn L2Engine> = Box::new(MockEngine);
120
121        assert_eq!(engine.name(), "MockEngine");
122        assert_eq!(engine.finding_types(), &["test-finding"]);
123        assert!(engine.languages().is_empty());
124
125        // Construct a minimal L2Context for the analyze call
126        let ctx = context::L2Context::new(
127            std::path::PathBuf::from("/tmp/test"),
128            tldr_core::Language::Rust,
129            vec![],
130            context::FunctionDiff {
131                changed: vec![],
132                inserted: vec![],
133                deleted: vec![],
134            },
135            std::collections::HashMap::new(),
136            std::collections::HashMap::new(),
137            std::collections::HashMap::new(),
138        );
139        let output = engine.analyze(&ctx);
140        assert!(output.findings.is_empty());
141        assert_eq!(output.status, types::AnalyzerStatus::Complete);
142        assert_eq!(output.duration_ms, 0);
143        assert_eq!(output.functions_analyzed, 0);
144        assert_eq!(output.functions_skipped, 0);
145    }
146}