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