cargo_perf/
plugin.rs

1//! Plugin system for extending cargo-perf with custom rules.
2//!
3//! This module provides the infrastructure for creating custom performance rules
4//! that can be integrated with cargo-perf.
5//!
6//! # Creating a Custom Rule
7//!
8//! To create a custom rule, implement the [`Rule`] trait:
9//!
10//! ```rust,ignore
11//! use cargo_perf::rules::{Rule, Diagnostic, Severity};
12//! use cargo_perf::engine::AnalysisContext;
13//!
14//! pub struct MyCustomRule;
15//!
16//! impl Rule for MyCustomRule {
17//!     fn id(&self) -> &'static str { "my-custom-rule" }
18//!     fn name(&self) -> &'static str { "My Custom Rule" }
19//!     fn description(&self) -> &'static str { "Detects my custom anti-pattern" }
20//!     fn default_severity(&self) -> Severity { Severity::Warning }
21//!
22//!     fn check(&self, ctx: &AnalysisContext) -> Vec<Diagnostic> {
23//!         // Your detection logic here
24//!         Vec::new()
25//!     }
26//! }
27//! ```
28//!
29//! # Creating a Custom Binary
30//!
31//! To use custom rules, create a new binary that combines built-in and custom rules:
32//!
33//! ```rust,ignore
34//! use cargo_perf::plugin::{PluginRegistry, run_with_plugins};
35//!
36//! fn main() {
37//!     let mut registry = PluginRegistry::new();
38//!
39//!     // Add all built-in rules
40//!     registry.add_builtin_rules();
41//!
42//!     // Add your custom rules
43//!     registry.add_rule(Box::new(MyCustomRule));
44//!     registry.add_rule(Box::new(AnotherCustomRule));
45//!
46//!     // Run with the combined rule set
47//!     run_with_plugins(registry);
48//! }
49//! ```
50//!
51//! # Configuration
52//!
53//! Custom rules can be configured in `cargo-perf.toml` just like built-in rules:
54//!
55//! ```toml
56//! [rules]
57//! my-custom-rule = "warn"
58//! another-custom-rule = "deny"
59//! ```
60
61use crate::discovery::{discover_rust_files, DiscoveryOptions};
62use crate::engine::{analyze_file_with_rules, AnalysisContext};
63use crate::error::Error;
64use crate::rules::{Diagnostic, Rule};
65use crate::Config;
66use rayon::prelude::*;
67use std::collections::HashMap;
68use std::path::Path;
69use std::sync::Mutex;
70
71/// A registry for managing both built-in and custom rules.
72///
73/// This registry reuses the static rule registry for built-in rules,
74/// avoiding duplication. Custom rules are stored separately and can
75/// override built-in rules with the same ID.
76///
77/// # Example
78///
79/// ```rust,ignore
80/// use cargo_perf::plugin::PluginRegistry;
81///
82/// let mut registry = PluginRegistry::new();
83/// registry.add_builtin_rules();
84/// registry.add_rule(Box::new(MyCustomRule));
85/// ```
86pub struct PluginRegistry {
87    /// Whether built-in rules from the static registry are included.
88    include_builtins: bool,
89    /// Custom rules (may override built-in rules).
90    custom_rules: Vec<Box<dyn Rule>>,
91    /// Index for O(1) lookup of custom rules by ID.
92    custom_index: HashMap<String, usize>,
93}
94
95impl Default for PluginRegistry {
96    fn default() -> Self {
97        Self::new()
98    }
99}
100
101impl PluginRegistry {
102    /// Create an empty plugin registry.
103    pub fn new() -> Self {
104        Self {
105            include_builtins: false,
106            custom_rules: Vec::new(),
107            custom_index: HashMap::new(),
108        }
109    }
110
111    /// Add all built-in rules to the registry.
112    ///
113    /// This references the static rule registry rather than creating new instances,
114    /// avoiding memory duplication.
115    pub fn add_builtin_rules(&mut self) {
116        self.include_builtins = true;
117    }
118
119    /// Add a custom rule to the registry.
120    ///
121    /// # Panics
122    ///
123    /// Panics if a rule with the same ID already exists. Use [`try_add_rule`] for
124    /// a non-panicking version or [`add_or_replace_rule`] to replace existing rules.
125    ///
126    /// [`try_add_rule`]: Self::try_add_rule
127    /// [`add_or_replace_rule`]: Self::add_or_replace_rule
128    pub fn add_rule(&mut self, rule: Box<dyn Rule>) {
129        let id = rule.id().to_string();
130        if self.try_add_rule(rule).is_err() {
131            panic!("Rule with ID '{}' already exists", id);
132        }
133    }
134
135    /// Try to add a custom rule to the registry.
136    ///
137    /// Returns an error if a rule with the same ID already exists in custom rules.
138    /// Note: This allows overriding built-in rules with custom implementations.
139    /// Use [`add_or_replace_rule`] to replace existing rules without error.
140    ///
141    /// [`add_or_replace_rule`]: Self::add_or_replace_rule
142    ///
143    /// # Errors
144    ///
145    /// Returns `Err` with the rejected rule if a custom rule with the same ID already exists.
146    pub fn try_add_rule(&mut self, rule: Box<dyn Rule>) -> Result<(), Box<dyn Rule>> {
147        let id = rule.id().to_string();
148        if self.custom_index.contains_key(&id) {
149            return Err(rule);
150        }
151        let idx = self.custom_rules.len();
152        self.custom_index.insert(id, idx);
153        self.custom_rules.push(rule);
154        Ok(())
155    }
156
157    /// Add a custom rule, replacing any existing custom rule with the same ID.
158    pub fn add_or_replace_rule(&mut self, rule: Box<dyn Rule>) {
159        let id = rule.id().to_string();
160        if let Some(&idx) = self.custom_index.get(&id) {
161            self.custom_rules[idx] = rule;
162        } else {
163            let idx = self.custom_rules.len();
164            self.custom_index.insert(id, idx);
165            self.custom_rules.push(rule);
166        }
167    }
168
169    /// Get all registered rules as trait object references.
170    ///
171    /// Returns an iterator over all rules (built-in + custom).
172    /// Custom rules with the same ID as built-in rules will override them.
173    pub fn rules(&self) -> Vec<&dyn Rule> {
174        use crate::rules::registry;
175
176        // Pre-allocate capacity: max builtins + all custom rules
177        let builtin_count = if self.include_builtins {
178            registry::all_rules().len()
179        } else {
180            0
181        };
182        let mut rules: Vec<&dyn Rule> = Vec::with_capacity(builtin_count + self.custom_rules.len());
183
184        // Add built-in rules (if enabled), skipping those overridden by custom rules
185        if self.include_builtins {
186            for rule in registry::all_rules() {
187                if !self.custom_index.contains_key(rule.id()) {
188                    rules.push(rule.as_ref());
189                }
190            }
191        }
192
193        // Add custom rules
194        for rule in &self.custom_rules {
195            rules.push(rule.as_ref());
196        }
197
198        rules
199    }
200
201    /// Get a rule by its ID.
202    ///
203    /// Custom rules take precedence over built-in rules.
204    pub fn get_rule(&self, id: &str) -> Option<&dyn Rule> {
205        use crate::rules::registry;
206
207        // Check custom rules first (they override built-ins)
208        if let Some(&idx) = self.custom_index.get(id) {
209            return Some(self.custom_rules[idx].as_ref());
210        }
211
212        // Fall back to built-in rules
213        if self.include_builtins {
214            return registry::get_rule(id);
215        }
216
217        None
218    }
219
220    /// Check if a rule with the given ID exists.
221    pub fn has_rule(&self, id: &str) -> bool {
222        use crate::rules::registry;
223
224        self.custom_index.contains_key(id) || (self.include_builtins && registry::has_rule(id))
225    }
226
227    /// Get all rule IDs.
228    pub fn rule_ids(&self) -> Vec<&str> {
229        use crate::rules::registry;
230
231        let mut ids: Vec<&str> = Vec::new();
232
233        // Add built-in rule IDs (if enabled), skipping overridden ones
234        if self.include_builtins {
235            for id in registry::rule_ids() {
236                if !self.custom_index.contains_key(id) {
237                    ids.push(id);
238                }
239            }
240        }
241
242        // Add custom rule IDs
243        for id in self.custom_index.keys() {
244            ids.push(id.as_str());
245        }
246
247        ids
248    }
249
250    /// Run all rules on the given analysis context.
251    pub fn check_all(&self, ctx: &AnalysisContext) -> Vec<Diagnostic> {
252        let mut diagnostics = Vec::new();
253        for rule in self.rules() {
254            diagnostics.extend(rule.check(ctx));
255        }
256        diagnostics
257    }
258
259    /// Run specific rules on the given analysis context.
260    pub fn check_rules(&self, ctx: &AnalysisContext, rule_ids: &[&str]) -> Vec<Diagnostic> {
261        let mut diagnostics = Vec::new();
262        for id in rule_ids {
263            if let Some(rule) = self.get_rule(id) {
264                diagnostics.extend(rule.check(ctx));
265            }
266        }
267        diagnostics
268    }
269}
270
271/// Builder pattern for creating a plugin registry with a fluent API.
272///
273/// # Example
274///
275/// ```rust,ignore
276/// use cargo_perf::plugin::PluginRegistryBuilder;
277///
278/// let registry = PluginRegistryBuilder::new()
279///     .with_builtin_rules()
280///     .with_rule(Box::new(MyCustomRule))
281///     .with_rule(Box::new(AnotherRule))
282///     .build();
283/// ```
284pub struct PluginRegistryBuilder {
285    registry: PluginRegistry,
286}
287
288impl Default for PluginRegistryBuilder {
289    fn default() -> Self {
290        Self::new()
291    }
292}
293
294impl PluginRegistryBuilder {
295    /// Create a new builder.
296    pub fn new() -> Self {
297        Self {
298            registry: PluginRegistry::new(),
299        }
300    }
301
302    /// Add all built-in rules.
303    pub fn with_builtin_rules(mut self) -> Self {
304        self.registry.add_builtin_rules();
305        self
306    }
307
308    /// Add a custom rule.
309    pub fn with_rule(mut self, rule: Box<dyn Rule>) -> Self {
310        self.registry.add_rule(rule);
311        self
312    }
313
314    /// Build the registry.
315    pub fn build(self) -> PluginRegistry {
316        self.registry
317    }
318}
319
320/// Analyze a path using a custom plugin registry.
321///
322/// This is similar to [`crate::analyze`] but uses the provided registry
323/// instead of the built-in rules.
324///
325/// # Example
326///
327/// ```rust,ignore
328/// use cargo_perf::plugin::{PluginRegistry, analyze_with_plugins};
329/// use cargo_perf::Config;
330/// use std::path::Path;
331///
332/// let mut registry = PluginRegistry::new();
333/// registry.add_builtin_rules();
334/// registry.add_rule(Box::new(MyCustomRule));
335///
336/// let config = Config::default();
337/// let diagnostics = analyze_with_plugins(Path::new("."), &config, &registry)?;
338/// ```
339pub fn analyze_with_plugins(
340    path: &Path,
341    config: &Config,
342    registry: &PluginRegistry,
343) -> Result<Vec<Diagnostic>, Error> {
344    // Use secure discovery (same as Engine) to prevent symlink attacks
345    let files = discover_rust_files(path, &DiscoveryOptions::secure());
346
347    // Track errors but don't fail the entire analysis
348    let errors: Mutex<Vec<(std::path::PathBuf, Error)>> = Mutex::new(Vec::new());
349
350    // Analyze files in parallel using shared file analysis logic
351    let all_diagnostics: Vec<Diagnostic> = files
352        .par_iter()
353        .flat_map(|file_path| {
354            // Use shared analysis function with plugin registry rules
355            let rules = registry.rules().into_iter();
356            match analyze_file_with_rules(file_path, config, rules) {
357                Ok(diagnostics) => diagnostics,
358                Err(e) => {
359                    // Log errors but continue analyzing other files
360                    if let Ok(mut errs) = errors.lock() {
361                        errs.push((file_path.clone(), e));
362                    }
363                    Vec::new()
364                }
365            }
366        })
367        .collect();
368
369    // Report errors at the end (same as Engine)
370    if let Ok(errs) = errors.lock() {
371        for (path, error) in errs.iter() {
372            eprintln!("Warning: Failed to analyze {}: {}", path.display(), error);
373        }
374    }
375
376    Ok(all_diagnostics)
377}
378
379/// A helper macro for defining custom rules more concisely.
380///
381/// # Example
382///
383/// ```rust,ignore
384/// use cargo_perf::define_rule;
385///
386/// define_rule! {
387///     /// Detects usage of unwrap() in production code.
388///     pub struct NoUnwrapRule {
389///         id: "no-unwrap",
390///         name: "No Unwrap",
391///         description: "Detects .unwrap() calls that should use proper error handling",
392///         severity: Warning,
393///     }
394///
395///     fn check(&self, ctx: &AnalysisContext) -> Vec<Diagnostic> {
396///         // Implementation here
397///         Vec::new()
398///     }
399/// }
400/// ```
401#[macro_export]
402macro_rules! define_rule {
403    (
404        $(#[$meta:meta])*
405        pub struct $name:ident {
406            id: $id:literal,
407            name: $rule_name:literal,
408            description: $desc:literal,
409            severity: $severity:ident,
410        }
411
412        fn check(&$self:ident, $ctx:ident: &AnalysisContext) -> Vec<Diagnostic> $body:block
413    ) => {
414        $(#[$meta])*
415        pub struct $name;
416
417        impl $crate::rules::Rule for $name {
418            fn id(&self) -> &'static str {
419                $id
420            }
421
422            fn name(&self) -> &'static str {
423                $rule_name
424            }
425
426            fn description(&self) -> &'static str {
427                $desc
428            }
429
430            fn default_severity(&self) -> $crate::rules::Severity {
431                $crate::rules::Severity::$severity
432            }
433
434            fn check(&$self, $ctx: &$crate::engine::AnalysisContext) -> Vec<$crate::rules::Diagnostic> $body
435        }
436    };
437}
438
439#[cfg(test)]
440mod tests {
441    use super::*;
442    use crate::rules::Severity;
443
444    struct TestRule;
445
446    impl Rule for TestRule {
447        fn id(&self) -> &'static str {
448            "test-rule"
449        }
450
451        fn name(&self) -> &'static str {
452            "Test Rule"
453        }
454
455        fn description(&self) -> &'static str {
456            "A test rule"
457        }
458
459        fn default_severity(&self) -> Severity {
460            Severity::Warning
461        }
462
463        fn check(&self, _ctx: &AnalysisContext) -> Vec<Diagnostic> {
464            Vec::new()
465        }
466    }
467
468    #[test]
469    fn test_registry_add_rule() {
470        let mut registry = PluginRegistry::new();
471        registry.add_rule(Box::new(TestRule));
472
473        assert!(registry.has_rule("test-rule"));
474        assert!(!registry.has_rule("nonexistent"));
475    }
476
477    #[test]
478    fn test_registry_get_rule() {
479        let mut registry = PluginRegistry::new();
480        registry.add_rule(Box::new(TestRule));
481
482        let rule = registry.get_rule("test-rule");
483        assert!(rule.is_some());
484        assert_eq!(rule.unwrap().id(), "test-rule");
485    }
486
487    #[test]
488    #[should_panic(expected = "already exists")]
489    fn test_registry_duplicate_rule_panics() {
490        let mut registry = PluginRegistry::new();
491        registry.add_rule(Box::new(TestRule));
492        registry.add_rule(Box::new(TestRule)); // Should panic
493    }
494
495    #[test]
496    fn test_try_add_rule_returns_err_on_duplicate() {
497        let mut registry = PluginRegistry::new();
498        assert!(registry.try_add_rule(Box::new(TestRule)).is_ok());
499        assert!(registry.try_add_rule(Box::new(TestRule)).is_err());
500        // Original rule should still be there
501        assert!(registry.has_rule("test-rule"));
502    }
503
504    #[test]
505    fn test_registry_add_or_replace() {
506        let mut registry = PluginRegistry::new();
507        registry.add_rule(Box::new(TestRule));
508        registry.add_or_replace_rule(Box::new(TestRule)); // Should not panic
509
510        assert!(registry.has_rule("test-rule"));
511    }
512
513    #[test]
514    fn test_builder_pattern() {
515        let registry = PluginRegistryBuilder::new()
516            .with_rule(Box::new(TestRule))
517            .build();
518
519        assert!(registry.has_rule("test-rule"));
520    }
521
522    #[test]
523    fn test_rule_ids() {
524        let mut registry = PluginRegistry::new();
525        registry.add_rule(Box::new(TestRule));
526
527        let ids = registry.rule_ids();
528        assert!(ids.contains(&"test-rule"));
529    }
530}