use std::path::PathBuf;
use std::sync::Arc;
use parking_lot::RwLock;
use rustc_hash::FxHashMap;
pub use mir_issues::{Issue, Severity};
pub use mir_types::Type;
pub use mir_types;
pub use php_ast;
#[cfg(feature = "dylib")]
pub mod dylib;
#[cfg(feature = "psalm-bridge")]
pub mod psalm;
pub const MIR_PLUGIN_API_VERSION: u32 = 2;
#[derive(Debug, Clone)]
pub struct PluginIssue {
pub name: String,
pub message: String,
pub severity: Severity,
pub span: Option<php_ast::Span>,
}
impl PluginIssue {
pub fn new(name: impl Into<String>, message: impl Into<String>) -> Self {
Self {
name: name.into(),
message: message.into(),
severity: Severity::Error,
span: None,
}
}
pub fn with_severity(mut self, severity: Severity) -> Self {
self.severity = severity;
self
}
pub fn with_span(mut self, span: php_ast::Span) -> Self {
self.span = Some(span);
self
}
}
#[derive(Debug, Clone)]
pub enum ProvidedType {
Union(Type),
Parse(String),
}
pub struct AfterExpressionAnalysisEvent<'a> {
pub expr: &'a php_ast::owned::Expr,
pub expr_type: &'a Type,
pub file: &'a str,
pub issues: Vec<PluginIssue>,
}
pub struct AfterStatementAnalysisEvent<'a> {
pub stmt: &'a php_ast::owned::Stmt,
pub file: &'a str,
pub issues: Vec<PluginIssue>,
}
pub struct AfterFunctionCallAnalysisEvent<'a> {
pub function_id: &'a str,
pub args: &'a [php_ast::owned::Arg],
pub arg_types: &'a [Type],
pub span: php_ast::Span,
pub file: &'a str,
pub return_type: &'a mut Type,
pub issues: Vec<PluginIssue>,
}
pub struct AfterMethodCallAnalysisEvent<'a> {
pub method_id: &'a str,
pub args: &'a [php_ast::owned::Arg],
pub arg_types: &'a [Type],
pub span: php_ast::Span,
pub file: &'a str,
pub return_type: &'a mut Type,
pub issues: Vec<PluginIssue>,
}
pub struct FunctionReturnTypeProviderEvent<'a> {
pub function_id: &'a str,
pub args: &'a [php_ast::owned::Arg],
pub arg_types: &'a [Type],
pub span: php_ast::Span,
pub file: &'a str,
pub call_snippet: Option<&'a str>,
}
pub struct MethodReturnTypeProviderEvent<'a> {
pub fqcn: &'a str,
pub method_name: &'a str,
pub args: &'a [php_ast::owned::Arg],
pub arg_types: &'a [Type],
pub span: php_ast::Span,
pub file: &'a str,
pub call_snippet: Option<&'a str>,
}
pub struct AfterCodebasePopulatedEvent<'a> {
pub files: &'a [Arc<str>],
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ArrayPropertyDefault {
pub property: String,
pub entries: Vec<(String, String)>,
}
pub struct ClassPropertyProviderEvent<'a> {
pub fqcn: &'a str,
pub property_name: &'a str,
pub array_property_defaults: &'a [ArrayPropertyDefault],
pub file: &'a str,
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct HookFlags {
pub after_expression_analysis: bool,
pub after_statement_analysis: bool,
pub after_function_call_analysis: bool,
pub after_method_call_analysis: bool,
pub before_add_issue: bool,
pub after_codebase_populated: bool,
}
pub trait MirPlugin: Send + Sync {
fn name(&self) -> &str;
fn hooks(&self) -> HookFlags {
HookFlags::default()
}
fn stub_files(&self) -> Vec<PathBuf> {
Vec::new()
}
fn function_return_type_ids(&self) -> Vec<String> {
Vec::new()
}
fn function_return_type(
&self,
_event: &FunctionReturnTypeProviderEvent<'_>,
) -> Option<ProvidedType> {
None
}
fn method_return_type_classes(&self) -> Vec<String> {
Vec::new()
}
fn method_return_type(
&self,
_event: &MethodReturnTypeProviderEvent<'_>,
) -> Option<ProvidedType> {
None
}
fn class_property_classes(&self) -> Vec<String> {
Vec::new()
}
fn class_property(&self, _event: &ClassPropertyProviderEvent<'_>) -> Option<ProvidedType> {
None
}
fn after_expression_analysis(&self, _event: &mut AfterExpressionAnalysisEvent<'_>) {}
fn after_statement_analysis(&self, _event: &mut AfterStatementAnalysisEvent<'_>) {}
fn after_function_call_analysis(&self, _event: &mut AfterFunctionCallAnalysisEvent<'_>) {}
fn after_method_call_analysis(&self, _event: &mut AfterMethodCallAnalysisEvent<'_>) {}
fn before_add_issue(&self, _issue: &Issue) -> Option<bool> {
None
}
fn after_codebase_populated(&self, _event: &mut AfterCodebasePopulatedEvent<'_>) {}
}
pub fn normalize_id(id: &str) -> String {
id.trim_start_matches('\\').to_ascii_lowercase()
}
#[derive(Default)]
pub struct PluginRegistry {
plugins: Vec<Box<dyn MirPlugin>>,
combined_hooks: HookFlags,
function_providers: FxHashMap<String, Vec<usize>>,
method_providers: FxHashMap<String, Vec<usize>>,
class_property_providers: FxHashMap<String, Vec<usize>>,
after_expr: Vec<usize>,
after_stmt: Vec<usize>,
after_fn_call: Vec<usize>,
after_method_call: Vec<usize>,
before_issue: Vec<usize>,
after_codebase: Vec<usize>,
}
impl PluginRegistry {
pub fn new() -> Self {
Self::default()
}
pub fn register(&mut self, plugin: Box<dyn MirPlugin>) {
let idx = self.plugins.len();
let hooks = plugin.hooks();
macro_rules! subscribe {
($flag:ident, $list:ident) => {
if hooks.$flag {
self.combined_hooks.$flag = true;
self.$list.push(idx);
}
};
}
subscribe!(after_expression_analysis, after_expr);
subscribe!(after_statement_analysis, after_stmt);
subscribe!(after_function_call_analysis, after_fn_call);
subscribe!(after_method_call_analysis, after_method_call);
subscribe!(before_add_issue, before_issue);
subscribe!(after_codebase_populated, after_codebase);
for id in plugin.function_return_type_ids() {
self.function_providers
.entry(normalize_id(&id))
.or_default()
.push(idx);
}
for fqcn in plugin.method_return_type_classes() {
self.method_providers
.entry(normalize_id(&fqcn))
.or_default()
.push(idx);
}
for fqcn in plugin.class_property_classes() {
self.class_property_providers
.entry(normalize_id(&fqcn))
.or_default()
.push(idx);
}
self.plugins.push(plugin);
}
pub fn is_empty(&self) -> bool {
self.plugins.is_empty()
}
pub fn len(&self) -> usize {
self.plugins.len()
}
pub fn plugin_names(&self) -> Vec<&str> {
self.plugins.iter().map(|p| p.name()).collect()
}
pub fn hooks(&self) -> HookFlags {
self.combined_hooks
}
pub fn stub_files(&self) -> Vec<PathBuf> {
self.plugins.iter().flat_map(|p| p.stub_files()).collect()
}
pub fn has_function_provider(&self, function_id: &str) -> bool {
self.function_providers.contains_key(function_id)
}
pub fn has_method_provider(&self, fqcn_normalized: &str) -> bool {
self.method_providers.contains_key(fqcn_normalized)
}
pub fn has_any_function_provider(&self) -> bool {
!self.function_providers.is_empty()
}
pub fn has_any_method_provider(&self) -> bool {
!self.method_providers.is_empty()
}
pub fn function_return_type(
&self,
event: &FunctionReturnTypeProviderEvent<'_>,
) -> Option<ProvidedType> {
let indices = self.function_providers.get(event.function_id)?;
indices
.iter()
.find_map(|&i| self.plugins[i].function_return_type(event))
}
pub fn method_return_type(
&self,
fqcn_normalized: &str,
event: &MethodReturnTypeProviderEvent<'_>,
) -> Option<ProvidedType> {
let indices = self.method_providers.get(fqcn_normalized)?;
indices
.iter()
.find_map(|&i| self.plugins[i].method_return_type(event))
}
pub fn has_any_class_property_provider(&self) -> bool {
!self.class_property_providers.is_empty()
}
pub fn has_class_property_marker(&self, marker_normalized: &str) -> bool {
self.class_property_providers
.contains_key(marker_normalized)
}
pub fn class_property(
&self,
marker_normalized: &str,
event: &ClassPropertyProviderEvent<'_>,
) -> Option<ProvidedType> {
let indices = self.class_property_providers.get(marker_normalized)?;
indices
.iter()
.find_map(|&i| self.plugins[i].class_property(event))
}
pub fn after_expression_analysis(&self, event: &mut AfterExpressionAnalysisEvent<'_>) {
for &i in &self.after_expr {
self.plugins[i].after_expression_analysis(event);
}
}
pub fn after_statement_analysis(&self, event: &mut AfterStatementAnalysisEvent<'_>) {
for &i in &self.after_stmt {
self.plugins[i].after_statement_analysis(event);
}
}
pub fn after_function_call_analysis(&self, event: &mut AfterFunctionCallAnalysisEvent<'_>) {
for &i in &self.after_fn_call {
self.plugins[i].after_function_call_analysis(event);
}
}
pub fn after_method_call_analysis(&self, event: &mut AfterMethodCallAnalysisEvent<'_>) {
for &i in &self.after_method_call {
self.plugins[i].after_method_call_analysis(event);
}
}
pub fn before_add_issue(&self, issue: &Issue) -> bool {
for &i in &self.before_issue {
if let Some(keep) = self.plugins[i].before_add_issue(issue) {
return keep;
}
}
true
}
pub fn after_codebase_populated(&self, event: &mut AfterCodebasePopulatedEvent<'_>) {
for &i in &self.after_codebase {
self.plugins[i].after_codebase_populated(event);
}
}
}
static REGISTRY: RwLock<Option<Arc<PluginRegistry>>> = RwLock::new(None);
pub fn install(registry: PluginRegistry) {
let shared = if registry.is_empty() {
None
} else {
Some(Arc::new(registry))
};
*REGISTRY.write() = shared;
}
pub fn snapshot() -> Option<Arc<PluginRegistry>> {
REGISTRY.read().clone()
}
#[doc(hidden)]
pub fn uninstall() {
*REGISTRY.write() = None;
}
#[repr(C)]
pub struct PluginDeclaration {
pub api_version: u32,
pub create: fn() -> Box<dyn MirPlugin>,
}
#[macro_export]
macro_rules! export_plugin {
($create:path) => {
#[no_mangle]
pub static MIR_PLUGIN_DECLARATION: $crate::PluginDeclaration = $crate::PluginDeclaration {
api_version: $crate::MIR_PLUGIN_API_VERSION,
create: $create,
};
};
}
#[cfg(test)]
mod tests {
use super::*;
struct NoopPlugin;
impl MirPlugin for NoopPlugin {
fn name(&self) -> &str {
"noop"
}
}
struct ExprPlugin;
impl MirPlugin for ExprPlugin {
fn name(&self) -> &str {
"expr"
}
fn hooks(&self) -> HookFlags {
HookFlags {
after_expression_analysis: true,
..Default::default()
}
}
fn function_return_type_ids(&self) -> Vec<String> {
vec!["\\App\\helper".to_string()]
}
fn function_return_type(
&self,
_event: &FunctionReturnTypeProviderEvent<'_>,
) -> Option<ProvidedType> {
Some(ProvidedType::Parse("non-empty-string".to_string()))
}
}
#[test]
fn registry_indexes_hooks_and_providers() {
let mut reg = PluginRegistry::new();
reg.register(Box::new(NoopPlugin));
reg.register(Box::new(ExprPlugin));
assert_eq!(reg.len(), 2);
assert!(reg.hooks().after_expression_analysis);
assert!(!reg.hooks().after_statement_analysis);
assert!(reg.has_function_provider("app\\helper"));
assert!(!reg.has_function_provider("app\\other"));
assert!(reg.has_any_function_provider());
assert!(!reg.has_any_method_provider());
}
#[test]
fn normalize_id_strips_backslash_and_lowercases() {
assert_eq!(normalize_id("\\App\\Helper"), "app\\helper");
assert_eq!(normalize_id("strlen"), "strlen");
}
}