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, ®istry)?;
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}