1use std::path::PathBuf;
17use std::sync::Arc;
18
19use parking_lot::RwLock;
20use rustc_hash::FxHashMap;
21
22pub use mir_issues::{Issue, Severity};
23pub use mir_types::Type;
24pub use mir_types;
27pub use php_ast;
28
29#[cfg(feature = "dylib")]
30pub mod dylib;
31#[cfg(feature = "psalm-bridge")]
32pub mod psalm;
33
34pub const MIR_PLUGIN_API_VERSION: u32 = 2;
37
38#[derive(Debug, Clone)]
45pub struct PluginIssue {
46 pub name: String,
49 pub message: String,
50 pub severity: Severity,
51 pub span: Option<php_ast::Span>,
53}
54
55impl PluginIssue {
56 pub fn new(name: impl Into<String>, message: impl Into<String>) -> Self {
57 Self {
58 name: name.into(),
59 message: message.into(),
60 severity: Severity::Error,
61 span: None,
62 }
63 }
64
65 pub fn with_severity(mut self, severity: Severity) -> Self {
66 self.severity = severity;
67 self
68 }
69
70 pub fn with_span(mut self, span: php_ast::Span) -> Self {
71 self.span = Some(span);
72 self
73 }
74}
75
76#[derive(Debug, Clone)]
85pub enum ProvidedType {
86 Union(Type),
87 Parse(String),
88}
89
90pub struct AfterExpressionAnalysisEvent<'a> {
96 pub expr: &'a php_ast::owned::Expr,
97 pub expr_type: &'a Type,
99 pub file: &'a str,
100 pub issues: Vec<PluginIssue>,
101}
102
103pub struct AfterStatementAnalysisEvent<'a> {
105 pub stmt: &'a php_ast::owned::Stmt,
106 pub file: &'a str,
107 pub issues: Vec<PluginIssue>,
108}
109
110pub struct AfterFunctionCallAnalysisEvent<'a> {
113 pub function_id: &'a str,
115 pub args: &'a [php_ast::owned::Arg],
116 pub arg_types: &'a [Type],
117 pub span: php_ast::Span,
118 pub file: &'a str,
119 pub return_type: &'a mut Type,
120 pub issues: Vec<PluginIssue>,
121}
122
123pub struct AfterMethodCallAnalysisEvent<'a> {
125 pub method_id: &'a str,
127 pub args: &'a [php_ast::owned::Arg],
128 pub arg_types: &'a [Type],
129 pub span: php_ast::Span,
130 pub file: &'a str,
131 pub return_type: &'a mut Type,
132 pub issues: Vec<PluginIssue>,
133}
134
135pub struct FunctionReturnTypeProviderEvent<'a> {
137 pub function_id: &'a str,
139 pub args: &'a [php_ast::owned::Arg],
140 pub arg_types: &'a [Type],
141 pub span: php_ast::Span,
142 pub file: &'a str,
143 pub call_snippet: Option<&'a str>,
147}
148
149pub struct MethodReturnTypeProviderEvent<'a> {
151 pub fqcn: &'a str,
153 pub method_name: &'a str,
155 pub args: &'a [php_ast::owned::Arg],
156 pub arg_types: &'a [Type],
157 pub span: php_ast::Span,
158 pub file: &'a str,
159 pub call_snippet: Option<&'a str>,
160}
161
162pub struct AfterCodebasePopulatedEvent<'a> {
165 pub files: &'a [Arc<str>],
167}
168
169#[derive(Debug, Clone, PartialEq, Eq)]
173pub struct ArrayPropertyDefault {
174 pub property: String,
176 pub entries: Vec<(String, String)>,
180}
181
182pub struct ClassPropertyProviderEvent<'a> {
188 pub fqcn: &'a str,
190 pub property_name: &'a str,
192 pub array_property_defaults: &'a [ArrayPropertyDefault],
195 pub file: &'a str,
196}
197
198#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
206pub struct HookFlags {
207 pub after_expression_analysis: bool,
208 pub after_statement_analysis: bool,
209 pub after_function_call_analysis: bool,
210 pub after_method_call_analysis: bool,
211 pub before_add_issue: bool,
212 pub after_codebase_populated: bool,
213}
214
215pub trait MirPlugin: Send + Sync {
227 fn name(&self) -> &str;
228
229 fn hooks(&self) -> HookFlags {
230 HookFlags::default()
231 }
232
233 fn stub_files(&self) -> Vec<PathBuf> {
236 Vec::new()
237 }
238
239 fn function_return_type_ids(&self) -> Vec<String> {
242 Vec::new()
243 }
244
245 fn function_return_type(
248 &self,
249 _event: &FunctionReturnTypeProviderEvent<'_>,
250 ) -> Option<ProvidedType> {
251 None
252 }
253
254 fn method_return_type_classes(&self) -> Vec<String> {
257 Vec::new()
258 }
259
260 fn method_return_type(
261 &self,
262 _event: &MethodReturnTypeProviderEvent<'_>,
263 ) -> Option<ProvidedType> {
264 None
265 }
266
267 fn class_property_classes(&self) -> Vec<String> {
272 Vec::new()
273 }
274
275 fn class_property(&self, _event: &ClassPropertyProviderEvent<'_>) -> Option<ProvidedType> {
278 None
279 }
280
281 fn after_expression_analysis(&self, _event: &mut AfterExpressionAnalysisEvent<'_>) {}
282
283 fn after_statement_analysis(&self, _event: &mut AfterStatementAnalysisEvent<'_>) {}
284
285 fn after_function_call_analysis(&self, _event: &mut AfterFunctionCallAnalysisEvent<'_>) {}
286
287 fn after_method_call_analysis(&self, _event: &mut AfterMethodCallAnalysisEvent<'_>) {}
288
289 fn before_add_issue(&self, _issue: &Issue) -> Option<bool> {
293 None
294 }
295
296 fn after_codebase_populated(&self, _event: &mut AfterCodebasePopulatedEvent<'_>) {}
297}
298
299pub fn normalize_id(id: &str) -> String {
306 id.trim_start_matches('\\').to_ascii_lowercase()
307}
308
309#[derive(Default)]
310pub struct PluginRegistry {
311 plugins: Vec<Box<dyn MirPlugin>>,
312 combined_hooks: HookFlags,
313 function_providers: FxHashMap<String, Vec<usize>>,
315 method_providers: FxHashMap<String, Vec<usize>>,
317 class_property_providers: FxHashMap<String, Vec<usize>>,
320 after_expr: Vec<usize>,
323 after_stmt: Vec<usize>,
324 after_fn_call: Vec<usize>,
325 after_method_call: Vec<usize>,
326 before_issue: Vec<usize>,
327 after_codebase: Vec<usize>,
328}
329
330impl PluginRegistry {
331 pub fn new() -> Self {
332 Self::default()
333 }
334
335 pub fn register(&mut self, plugin: Box<dyn MirPlugin>) {
336 let idx = self.plugins.len();
337 let hooks = plugin.hooks();
338 macro_rules! subscribe {
339 ($flag:ident, $list:ident) => {
340 if hooks.$flag {
341 self.combined_hooks.$flag = true;
342 self.$list.push(idx);
343 }
344 };
345 }
346 subscribe!(after_expression_analysis, after_expr);
347 subscribe!(after_statement_analysis, after_stmt);
348 subscribe!(after_function_call_analysis, after_fn_call);
349 subscribe!(after_method_call_analysis, after_method_call);
350 subscribe!(before_add_issue, before_issue);
351 subscribe!(after_codebase_populated, after_codebase);
352
353 for id in plugin.function_return_type_ids() {
354 self.function_providers
355 .entry(normalize_id(&id))
356 .or_default()
357 .push(idx);
358 }
359 for fqcn in plugin.method_return_type_classes() {
360 self.method_providers
361 .entry(normalize_id(&fqcn))
362 .or_default()
363 .push(idx);
364 }
365 for fqcn in plugin.class_property_classes() {
366 self.class_property_providers
367 .entry(normalize_id(&fqcn))
368 .or_default()
369 .push(idx);
370 }
371 self.plugins.push(plugin);
372 }
373
374 pub fn is_empty(&self) -> bool {
375 self.plugins.is_empty()
376 }
377
378 pub fn len(&self) -> usize {
379 self.plugins.len()
380 }
381
382 pub fn plugin_names(&self) -> Vec<&str> {
383 self.plugins.iter().map(|p| p.name()).collect()
384 }
385
386 pub fn hooks(&self) -> HookFlags {
387 self.combined_hooks
388 }
389
390 pub fn stub_files(&self) -> Vec<PathBuf> {
392 self.plugins.iter().flat_map(|p| p.stub_files()).collect()
393 }
394
395 pub fn has_function_provider(&self, function_id: &str) -> bool {
398 self.function_providers.contains_key(function_id)
399 }
400
401 pub fn has_method_provider(&self, fqcn_normalized: &str) -> bool {
402 self.method_providers.contains_key(fqcn_normalized)
403 }
404
405 pub fn has_any_function_provider(&self) -> bool {
408 !self.function_providers.is_empty()
409 }
410
411 pub fn has_any_method_provider(&self) -> bool {
412 !self.method_providers.is_empty()
413 }
414
415 pub fn function_return_type(
419 &self,
420 event: &FunctionReturnTypeProviderEvent<'_>,
421 ) -> Option<ProvidedType> {
422 let indices = self.function_providers.get(event.function_id)?;
423 indices
424 .iter()
425 .find_map(|&i| self.plugins[i].function_return_type(event))
426 }
427
428 pub fn method_return_type(
429 &self,
430 fqcn_normalized: &str,
431 event: &MethodReturnTypeProviderEvent<'_>,
432 ) -> Option<ProvidedType> {
433 let indices = self.method_providers.get(fqcn_normalized)?;
434 indices
435 .iter()
436 .find_map(|&i| self.plugins[i].method_return_type(event))
437 }
438
439 pub fn has_any_class_property_provider(&self) -> bool {
440 !self.class_property_providers.is_empty()
441 }
442
443 pub fn has_class_property_marker(&self, marker_normalized: &str) -> bool {
447 self.class_property_providers
448 .contains_key(marker_normalized)
449 }
450
451 pub fn class_property(
452 &self,
453 marker_normalized: &str,
454 event: &ClassPropertyProviderEvent<'_>,
455 ) -> Option<ProvidedType> {
456 let indices = self.class_property_providers.get(marker_normalized)?;
457 indices
458 .iter()
459 .find_map(|&i| self.plugins[i].class_property(event))
460 }
461
462 pub fn after_expression_analysis(&self, event: &mut AfterExpressionAnalysisEvent<'_>) {
463 for &i in &self.after_expr {
464 self.plugins[i].after_expression_analysis(event);
465 }
466 }
467
468 pub fn after_statement_analysis(&self, event: &mut AfterStatementAnalysisEvent<'_>) {
469 for &i in &self.after_stmt {
470 self.plugins[i].after_statement_analysis(event);
471 }
472 }
473
474 pub fn after_function_call_analysis(&self, event: &mut AfterFunctionCallAnalysisEvent<'_>) {
475 for &i in &self.after_fn_call {
476 self.plugins[i].after_function_call_analysis(event);
477 }
478 }
479
480 pub fn after_method_call_analysis(&self, event: &mut AfterMethodCallAnalysisEvent<'_>) {
481 for &i in &self.after_method_call {
482 self.plugins[i].after_method_call_analysis(event);
483 }
484 }
485
486 pub fn before_add_issue(&self, issue: &Issue) -> bool {
488 for &i in &self.before_issue {
489 if let Some(keep) = self.plugins[i].before_add_issue(issue) {
490 return keep;
491 }
492 }
493 true
494 }
495
496 pub fn after_codebase_populated(&self, event: &mut AfterCodebasePopulatedEvent<'_>) {
497 for &i in &self.after_codebase {
498 self.plugins[i].after_codebase_populated(event);
499 }
500 }
501}
502
503static REGISTRY: RwLock<Option<Arc<PluginRegistry>>> = RwLock::new(None);
508
509pub fn install(registry: PluginRegistry) {
512 let shared = if registry.is_empty() {
513 None
514 } else {
515 Some(Arc::new(registry))
516 };
517 *REGISTRY.write() = shared;
518}
519
520pub fn snapshot() -> Option<Arc<PluginRegistry>> {
523 REGISTRY.read().clone()
524}
525
526#[doc(hidden)]
528pub fn uninstall() {
529 *REGISTRY.write() = None;
530}
531
532#[repr(C)]
541pub struct PluginDeclaration {
542 pub api_version: u32,
543 pub create: fn() -> Box<dyn MirPlugin>,
544}
545
546#[macro_export]
557macro_rules! export_plugin {
558 ($create:path) => {
559 #[no_mangle]
560 pub static MIR_PLUGIN_DECLARATION: $crate::PluginDeclaration = $crate::PluginDeclaration {
561 api_version: $crate::MIR_PLUGIN_API_VERSION,
562 create: $create,
563 };
564 };
565}
566
567#[cfg(test)]
568mod tests {
569 use super::*;
570
571 struct NoopPlugin;
572 impl MirPlugin for NoopPlugin {
573 fn name(&self) -> &str {
574 "noop"
575 }
576 }
577
578 struct ExprPlugin;
579 impl MirPlugin for ExprPlugin {
580 fn name(&self) -> &str {
581 "expr"
582 }
583 fn hooks(&self) -> HookFlags {
584 HookFlags {
585 after_expression_analysis: true,
586 ..Default::default()
587 }
588 }
589 fn function_return_type_ids(&self) -> Vec<String> {
590 vec!["\\App\\helper".to_string()]
591 }
592 fn function_return_type(
593 &self,
594 _event: &FunctionReturnTypeProviderEvent<'_>,
595 ) -> Option<ProvidedType> {
596 Some(ProvidedType::Parse("non-empty-string".to_string()))
597 }
598 }
599
600 #[test]
601 fn registry_indexes_hooks_and_providers() {
602 let mut reg = PluginRegistry::new();
603 reg.register(Box::new(NoopPlugin));
604 reg.register(Box::new(ExprPlugin));
605
606 assert_eq!(reg.len(), 2);
607 assert!(reg.hooks().after_expression_analysis);
608 assert!(!reg.hooks().after_statement_analysis);
609 assert!(reg.has_function_provider("app\\helper"));
610 assert!(!reg.has_function_provider("app\\other"));
611 assert!(reg.has_any_function_provider());
612 assert!(!reg.has_any_method_provider());
613 }
614
615 #[test]
616 fn normalize_id_strips_backslash_and_lowercases() {
617 assert_eq!(normalize_id("\\App\\Helper"), "app\\helper");
618 assert_eq!(normalize_id("strlen"), "strlen");
619 }
620}