1use std::cell::RefCell;
17use std::path::PathBuf;
18use std::sync::Arc;
19
20use parking_lot::RwLock;
21use rustc_hash::FxHashMap;
22
23pub use mir_issues::{Issue, Severity};
24pub use mir_types::Type;
25pub use mir_types;
28pub use php_ast;
29
30#[cfg(feature = "dylib")]
31pub mod dylib;
32#[cfg(feature = "psalm-bridge")]
33pub mod psalm;
34
35pub const MIR_PLUGIN_API_VERSION: u32 = 4;
38
39#[derive(Debug, Clone)]
46pub struct PluginIssue {
47 pub name: String,
50 pub message: String,
51 pub severity: Severity,
52 pub span: Option<php_ast::Span>,
54}
55
56impl PluginIssue {
57 pub fn new(name: impl Into<String>, message: impl Into<String>) -> Self {
58 Self {
59 name: name.into(),
60 message: message.into(),
61 severity: Severity::Error,
62 span: None,
63 }
64 }
65
66 pub fn with_severity(mut self, severity: Severity) -> Self {
67 self.severity = severity;
68 self
69 }
70
71 pub fn with_span(mut self, span: php_ast::Span) -> Self {
72 self.span = Some(span);
73 self
74 }
75}
76
77#[derive(Debug, Clone)]
86pub enum ProvidedType {
87 Union(Type),
88 Parse(String),
89}
90
91pub struct AfterExpressionAnalysisEvent<'a> {
97 pub expr: &'a php_ast::owned::Expr,
98 pub expr_type: &'a Type,
100 pub file: &'a str,
101 pub issues: Vec<PluginIssue>,
102}
103
104pub struct AfterStatementAnalysisEvent<'a> {
106 pub stmt: &'a php_ast::owned::Stmt,
107 pub file: &'a str,
108 pub issues: Vec<PluginIssue>,
109}
110
111pub struct AfterFunctionCallAnalysisEvent<'a> {
114 pub function_id: &'a str,
116 pub args: &'a [php_ast::owned::Arg],
117 pub arg_types: &'a [Type],
118 pub span: php_ast::Span,
119 pub file: &'a str,
120 pub return_type: &'a mut Type,
121 pub issues: Vec<PluginIssue>,
122}
123
124pub struct AfterMethodCallAnalysisEvent<'a> {
126 pub method_id: &'a str,
128 pub args: &'a [php_ast::owned::Arg],
129 pub arg_types: &'a [Type],
130 pub span: php_ast::Span,
131 pub file: &'a str,
132 pub return_type: &'a mut Type,
133 pub issues: Vec<PluginIssue>,
134}
135
136pub struct FunctionReturnTypeProviderEvent<'a> {
138 pub function_id: &'a str,
140 pub args: &'a [php_ast::owned::Arg],
141 pub arg_types: &'a [Type],
142 pub span: php_ast::Span,
143 pub file: &'a str,
144 pub call_snippet: Option<&'a str>,
148 pub calling_class: Option<&'a str>,
150 pub issues: RefCell<Vec<PluginIssue>>,
152}
153
154pub struct MethodReturnTypeProviderEvent<'a> {
156 pub fqcn: &'a str,
158 pub method_name: &'a str,
160 pub args: &'a [php_ast::owned::Arg],
161 pub arg_types: &'a [Type],
162 pub span: php_ast::Span,
163 pub file: &'a str,
164 pub call_snippet: Option<&'a str>,
165 pub calling_class: Option<&'a str>,
167 pub issues: RefCell<Vec<PluginIssue>>,
169}
170
171#[derive(Debug, Clone, PartialEq, Eq)]
173pub struct AttributeArgInfo {
174 pub name: Option<String>,
175 pub type_string: Option<String>,
178}
179
180#[derive(Debug, Clone, PartialEq, Eq)]
181pub struct AttributeInfo {
182 pub fq_class_name: String,
184 pub args: Vec<AttributeArgInfo>,
185 pub span: php_ast::Span,
186}
187
188#[derive(Debug, Clone, PartialEq, Eq)]
189pub struct FunctionLikeParamInfo {
190 pub name: String,
192 pub declared_type: Option<String>,
194 pub attributes: Vec<AttributeInfo>,
195}
196
197pub struct AfterFunctionLikeAnalysisEvent<'a> {
200 pub name: &'a str,
202 pub class: Option<&'a str>,
204 pub params: &'a [FunctionLikeParamInfo],
205 pub span: php_ast::Span,
207 pub snippet: Option<&'a str>,
209 pub file: &'a str,
210 pub issues: Vec<PluginIssue>,
211}
212
213pub struct AfterClassLikeAnalysisEvent<'a> {
216 pub fqcn: &'a str,
218 pub file: &'a str,
219 pub span: php_ast::Span,
221 pub issues: Vec<PluginIssue>,
222 pub suppressed_issues: Vec<String>,
225 pub used_classes: Vec<String>,
228 pub used_methods: Vec<(String, String)>,
230}
231
232pub struct AfterCodebasePopulatedEvent<'a> {
235 pub files: &'a [Arc<str>],
237}
238
239#[derive(Debug, Clone, PartialEq, Eq)]
243pub struct ArrayPropertyDefault {
244 pub property: String,
246 pub entries: Vec<(String, String)>,
250}
251
252pub struct ClassPropertyProviderEvent<'a> {
258 pub fqcn: &'a str,
260 pub property_name: &'a str,
262 pub array_property_defaults: &'a [ArrayPropertyDefault],
265 pub file: &'a str,
266}
267
268#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
276pub struct HookFlags {
277 pub after_expression_analysis: bool,
278 pub after_statement_analysis: bool,
279 pub after_function_call_analysis: bool,
280 pub after_method_call_analysis: bool,
281 pub after_function_like_analysis: bool,
282 pub after_class_like_analysis: bool,
283 pub before_add_issue: bool,
284 pub after_codebase_populated: bool,
285}
286
287pub trait MirPlugin: Send + Sync {
299 fn name(&self) -> &str;
300
301 fn hooks(&self) -> HookFlags {
302 HookFlags::default()
303 }
304
305 fn stub_files(&self) -> Vec<PathBuf> {
308 Vec::new()
309 }
310
311 fn function_return_type_ids(&self) -> Vec<String> {
314 Vec::new()
315 }
316
317 fn function_return_type(
320 &self,
321 _event: &FunctionReturnTypeProviderEvent<'_>,
322 ) -> Option<ProvidedType> {
323 None
324 }
325
326 fn method_return_type_classes(&self) -> Vec<String> {
329 Vec::new()
330 }
331
332 fn method_return_type(
333 &self,
334 _event: &MethodReturnTypeProviderEvent<'_>,
335 ) -> Option<ProvidedType> {
336 None
337 }
338
339 fn class_property_classes(&self) -> Vec<String> {
344 Vec::new()
345 }
346
347 fn class_property(&self, _event: &ClassPropertyProviderEvent<'_>) -> Option<ProvidedType> {
350 None
351 }
352
353 fn after_expression_analysis(&self, _event: &mut AfterExpressionAnalysisEvent<'_>) {}
354
355 fn after_statement_analysis(&self, _event: &mut AfterStatementAnalysisEvent<'_>) {}
356
357 fn after_function_call_analysis(&self, _event: &mut AfterFunctionCallAnalysisEvent<'_>) {}
358
359 fn after_method_call_analysis(&self, _event: &mut AfterMethodCallAnalysisEvent<'_>) {}
360
361 fn after_function_like_analysis(&self, _event: &mut AfterFunctionLikeAnalysisEvent<'_>) {}
362
363 fn after_class_like_analysis(&self, _event: &mut AfterClassLikeAnalysisEvent<'_>) {}
364
365 fn before_add_issue(&self, _issue: &Issue) -> Option<bool> {
369 None
370 }
371
372 fn after_codebase_populated(&self, _event: &mut AfterCodebasePopulatedEvent<'_>) {}
373}
374
375pub fn normalize_id(id: &str) -> String {
382 id.trim_start_matches('\\').to_ascii_lowercase()
383}
384
385#[derive(Default)]
386pub struct PluginRegistry {
387 plugins: Vec<Box<dyn MirPlugin>>,
388 combined_hooks: HookFlags,
389 function_providers: FxHashMap<String, Vec<usize>>,
391 method_providers: FxHashMap<String, Vec<usize>>,
393 class_property_providers: FxHashMap<String, Vec<usize>>,
396 after_expr: Vec<usize>,
399 after_stmt: Vec<usize>,
400 after_fn_call: Vec<usize>,
401 after_method_call: Vec<usize>,
402 after_function_like: Vec<usize>,
403 after_class_like: Vec<usize>,
404 before_issue: Vec<usize>,
405 after_codebase: Vec<usize>,
406}
407
408impl PluginRegistry {
409 pub fn new() -> Self {
410 Self::default()
411 }
412
413 pub fn register(&mut self, plugin: Box<dyn MirPlugin>) {
414 let idx = self.plugins.len();
415 let hooks = plugin.hooks();
416 macro_rules! subscribe {
417 ($flag:ident, $list:ident) => {
418 if hooks.$flag {
419 self.combined_hooks.$flag = true;
420 self.$list.push(idx);
421 }
422 };
423 }
424 subscribe!(after_expression_analysis, after_expr);
425 subscribe!(after_statement_analysis, after_stmt);
426 subscribe!(after_function_call_analysis, after_fn_call);
427 subscribe!(after_method_call_analysis, after_method_call);
428 subscribe!(after_function_like_analysis, after_function_like);
429 subscribe!(after_class_like_analysis, after_class_like);
430 subscribe!(before_add_issue, before_issue);
431 subscribe!(after_codebase_populated, after_codebase);
432
433 for id in plugin.function_return_type_ids() {
434 self.function_providers
435 .entry(normalize_id(&id))
436 .or_default()
437 .push(idx);
438 }
439 for fqcn in plugin.method_return_type_classes() {
440 self.method_providers
441 .entry(normalize_id(&fqcn))
442 .or_default()
443 .push(idx);
444 }
445 for fqcn in plugin.class_property_classes() {
446 self.class_property_providers
447 .entry(normalize_id(&fqcn))
448 .or_default()
449 .push(idx);
450 }
451 self.plugins.push(plugin);
452 }
453
454 pub fn is_empty(&self) -> bool {
455 self.plugins.is_empty()
456 }
457
458 pub fn len(&self) -> usize {
459 self.plugins.len()
460 }
461
462 pub fn plugin_names(&self) -> Vec<&str> {
463 self.plugins.iter().map(|p| p.name()).collect()
464 }
465
466 pub fn hooks(&self) -> HookFlags {
467 self.combined_hooks
468 }
469
470 pub fn stub_files(&self) -> Vec<PathBuf> {
472 self.plugins.iter().flat_map(|p| p.stub_files()).collect()
473 }
474
475 pub fn has_function_provider(&self, function_id: &str) -> bool {
478 self.function_providers.contains_key(function_id)
479 }
480
481 pub fn has_method_provider(&self, fqcn_normalized: &str) -> bool {
482 self.method_providers.contains_key(fqcn_normalized)
483 }
484
485 pub fn has_any_function_provider(&self) -> bool {
488 !self.function_providers.is_empty()
489 }
490
491 pub fn has_any_method_provider(&self) -> bool {
492 !self.method_providers.is_empty()
493 }
494
495 pub fn function_return_type(
499 &self,
500 event: &FunctionReturnTypeProviderEvent<'_>,
501 ) -> Option<ProvidedType> {
502 let indices = self.function_providers.get(event.function_id)?;
503 indices
504 .iter()
505 .find_map(|&i| self.plugins[i].function_return_type(event))
506 }
507
508 pub fn method_return_type(
509 &self,
510 fqcn_normalized: &str,
511 event: &MethodReturnTypeProviderEvent<'_>,
512 ) -> Option<ProvidedType> {
513 let indices = self.method_providers.get(fqcn_normalized)?;
514 indices
515 .iter()
516 .find_map(|&i| self.plugins[i].method_return_type(event))
517 }
518
519 pub fn has_any_class_property_provider(&self) -> bool {
520 !self.class_property_providers.is_empty()
521 }
522
523 pub fn has_class_property_marker(&self, marker_normalized: &str) -> bool {
527 self.class_property_providers
528 .contains_key(marker_normalized)
529 }
530
531 pub fn class_property(
532 &self,
533 marker_normalized: &str,
534 event: &ClassPropertyProviderEvent<'_>,
535 ) -> Option<ProvidedType> {
536 let indices = self.class_property_providers.get(marker_normalized)?;
537 indices
538 .iter()
539 .find_map(|&i| self.plugins[i].class_property(event))
540 }
541
542 pub fn after_expression_analysis(&self, event: &mut AfterExpressionAnalysisEvent<'_>) {
543 for &i in &self.after_expr {
544 self.plugins[i].after_expression_analysis(event);
545 }
546 }
547
548 pub fn after_statement_analysis(&self, event: &mut AfterStatementAnalysisEvent<'_>) {
549 for &i in &self.after_stmt {
550 self.plugins[i].after_statement_analysis(event);
551 }
552 }
553
554 pub fn after_function_call_analysis(&self, event: &mut AfterFunctionCallAnalysisEvent<'_>) {
555 for &i in &self.after_fn_call {
556 self.plugins[i].after_function_call_analysis(event);
557 }
558 }
559
560 pub fn after_method_call_analysis(&self, event: &mut AfterMethodCallAnalysisEvent<'_>) {
561 for &i in &self.after_method_call {
562 self.plugins[i].after_method_call_analysis(event);
563 }
564 }
565
566 pub fn after_function_like_analysis(&self, event: &mut AfterFunctionLikeAnalysisEvent<'_>) {
567 for &i in &self.after_function_like {
568 self.plugins[i].after_function_like_analysis(event);
569 }
570 }
571
572 pub fn after_class_like_analysis(&self, event: &mut AfterClassLikeAnalysisEvent<'_>) {
573 for &i in &self.after_class_like {
574 self.plugins[i].after_class_like_analysis(event);
575 }
576 }
577
578 pub fn before_add_issue(&self, issue: &Issue) -> bool {
580 for &i in &self.before_issue {
581 if let Some(keep) = self.plugins[i].before_add_issue(issue) {
582 return keep;
583 }
584 }
585 true
586 }
587
588 pub fn after_codebase_populated(&self, event: &mut AfterCodebasePopulatedEvent<'_>) {
589 for &i in &self.after_codebase {
590 self.plugins[i].after_codebase_populated(event);
591 }
592 }
593}
594
595static REGISTRY: RwLock<Option<Arc<PluginRegistry>>> = RwLock::new(None);
600
601pub fn install(registry: PluginRegistry) {
604 let shared = if registry.is_empty() {
605 None
606 } else {
607 Some(Arc::new(registry))
608 };
609 *REGISTRY.write() = shared;
610}
611
612pub fn snapshot() -> Option<Arc<PluginRegistry>> {
615 REGISTRY.read().clone()
616}
617
618#[doc(hidden)]
620pub fn uninstall() {
621 *REGISTRY.write() = None;
622}
623
624#[repr(C)]
633pub struct PluginDeclaration {
634 pub api_version: u32,
635 pub create: fn() -> Box<dyn MirPlugin>,
636}
637
638#[macro_export]
649macro_rules! export_plugin {
650 ($create:path) => {
651 #[no_mangle]
652 pub static MIR_PLUGIN_DECLARATION: $crate::PluginDeclaration = $crate::PluginDeclaration {
653 api_version: $crate::MIR_PLUGIN_API_VERSION,
654 create: $create,
655 };
656 };
657}
658
659#[cfg(test)]
660mod tests {
661 use super::*;
662
663 struct NoopPlugin;
664 impl MirPlugin for NoopPlugin {
665 fn name(&self) -> &str {
666 "noop"
667 }
668 }
669
670 struct ExprPlugin;
671 impl MirPlugin for ExprPlugin {
672 fn name(&self) -> &str {
673 "expr"
674 }
675 fn hooks(&self) -> HookFlags {
676 HookFlags {
677 after_expression_analysis: true,
678 ..Default::default()
679 }
680 }
681 fn function_return_type_ids(&self) -> Vec<String> {
682 vec!["\\App\\helper".to_string()]
683 }
684 fn function_return_type(
685 &self,
686 _event: &FunctionReturnTypeProviderEvent<'_>,
687 ) -> Option<ProvidedType> {
688 Some(ProvidedType::Parse("non-empty-string".to_string()))
689 }
690 }
691
692 #[test]
693 fn registry_indexes_hooks_and_providers() {
694 let mut reg = PluginRegistry::new();
695 reg.register(Box::new(NoopPlugin));
696 reg.register(Box::new(ExprPlugin));
697
698 assert_eq!(reg.len(), 2);
699 assert!(reg.hooks().after_expression_analysis);
700 assert!(!reg.hooks().after_statement_analysis);
701 assert!(reg.has_function_provider("app\\helper"));
702 assert!(!reg.has_function_provider("app\\other"));
703 assert!(reg.has_any_function_provider());
704 assert!(!reg.has_any_method_provider());
705 }
706
707 #[test]
708 fn normalize_id_strips_backslash_and_lowercases() {
709 assert_eq!(normalize_id("\\App\\Helper"), "app\\helper");
710 assert_eq!(normalize_id("strlen"), "strlen");
711 }
712}