Skip to main content

mago_analyzer/plugin/
registry.rs

1//! Plugin registry for managing and dispatching to providers and hooks.
2
3use mago_atom::Atom;
4use mago_atom::AtomMap;
5use mago_atom::AtomSet;
6use mago_atom::ascii_lowercase_atom;
7use mago_codex::identifier::function_like::FunctionLikeIdentifier;
8use mago_codex::metadata::CodebaseMetadata;
9use mago_codex::metadata::class_like::ClassLikeMetadata;
10use mago_codex::metadata::function_like::FunctionLikeMetadata;
11use mago_codex::metadata::property::PropertyMetadata;
12use mago_codex::ttype::union::TUnion;
13use mago_database::file::File;
14use mago_syntax::ast::Class;
15use mago_syntax::ast::Enum;
16use mago_syntax::ast::Expression;
17use mago_syntax::ast::Function;
18use mago_syntax::ast::FunctionCall;
19use mago_syntax::ast::Interface;
20use mago_syntax::ast::MethodCall;
21use mago_syntax::ast::NullSafeMethodCall;
22use mago_syntax::ast::Program;
23use mago_syntax::ast::Statement;
24use mago_syntax::ast::StaticMethodCall;
25use mago_syntax::ast::Trait;
26
27use crate::artifacts::AnalysisArtifacts;
28use crate::context::block::BlockContext;
29use crate::invocation::Invocation;
30use crate::plugin::context::HookContext;
31use crate::plugin::context::InvocationInfo;
32use crate::plugin::context::ProviderContext;
33use crate::plugin::context::ReportedIssue;
34use crate::plugin::error::PluginResult;
35use crate::plugin::hook::ClassDeclarationHook;
36use crate::plugin::hook::EnumDeclarationHook;
37use crate::plugin::hook::ExpressionHook;
38use crate::plugin::hook::ExpressionHookResult;
39use crate::plugin::hook::FunctionCallHook;
40use crate::plugin::hook::FunctionDeclarationHook;
41use crate::plugin::hook::HookAction;
42use crate::plugin::hook::InterfaceDeclarationHook;
43use crate::plugin::hook::IssueFilterDecision;
44use crate::plugin::hook::IssueFilterHook;
45use crate::plugin::hook::MethodCallHook;
46use crate::plugin::hook::NullSafeMethodCallHook;
47use crate::plugin::hook::ProgramHook;
48use crate::plugin::hook::StatementHook;
49use crate::plugin::hook::StaticMethodCallHook;
50use crate::plugin::hook::TraitDeclarationHook;
51use crate::plugin::provider::assertion::FunctionAssertionProvider;
52use crate::plugin::provider::assertion::InvocationAssertions;
53use crate::plugin::provider::assertion::MethodAssertionProvider;
54use crate::plugin::provider::function::FunctionReturnTypeProvider;
55use crate::plugin::provider::function::FunctionTarget;
56use crate::plugin::provider::method::MethodReturnTypeProvider;
57use crate::plugin::provider::method::MethodTarget;
58use crate::plugin::provider::property::PropertyInitializationProvider;
59use crate::plugin::provider::throw::ExpressionThrowTypeProvider;
60use crate::plugin::provider::throw::FunctionThrowTypeProvider;
61use crate::plugin::provider::throw::MethodThrowTypeProvider;
62
63use mago_reporting::IssueCollection;
64
65pub struct ProviderResult {
66    pub return_type: Option<TUnion>,
67    pub issues: Vec<ReportedIssue>,
68}
69
70#[derive(Default)]
71pub struct PluginRegistry {
72    function_exact: AtomMap<Vec<usize>>,
73    function_prefix: Vec<(Atom, usize)>,
74    function_namespace: Vec<(Atom, usize)>,
75    function_providers: Vec<Box<dyn FunctionReturnTypeProvider>>,
76    method_exact: AtomMap<Vec<usize>>,
77    method_wildcard: Vec<(Vec<MethodTarget>, usize)>,
78    method_providers: Vec<Box<dyn MethodReturnTypeProvider>>,
79    program_hooks: Vec<Box<dyn ProgramHook>>,
80    statement_hooks: Vec<Box<dyn StatementHook>>,
81    expression_hooks: Vec<Box<dyn ExpressionHook>>,
82    function_call_hooks: Vec<Box<dyn FunctionCallHook>>,
83    method_call_hooks: Vec<Box<dyn MethodCallHook>>,
84    static_method_call_hooks: Vec<Box<dyn StaticMethodCallHook>>,
85    nullsafe_method_call_hooks: Vec<Box<dyn NullSafeMethodCallHook>>,
86    class_hooks: Vec<Box<dyn ClassDeclarationHook>>,
87    interface_hooks: Vec<Box<dyn InterfaceDeclarationHook>>,
88    trait_hooks: Vec<Box<dyn TraitDeclarationHook>>,
89    enum_hooks: Vec<Box<dyn EnumDeclarationHook>>,
90    function_decl_hooks: Vec<Box<dyn FunctionDeclarationHook>>,
91    property_initialization_providers: Vec<Box<dyn PropertyInitializationProvider>>,
92    issue_filter_hooks: Vec<Box<dyn IssueFilterHook>>,
93    function_assertion_exact: AtomMap<Vec<usize>>,
94    function_assertion_prefix: Vec<(Atom, usize)>,
95    function_assertion_namespace: Vec<(Atom, usize)>,
96    function_assertion_providers: Vec<Box<dyn FunctionAssertionProvider>>,
97    method_assertion_exact: AtomMap<Vec<usize>>,
98    method_assertion_wildcard: Vec<(Vec<MethodTarget>, usize)>,
99    method_assertion_providers: Vec<Box<dyn MethodAssertionProvider>>,
100    expression_throw_providers: Vec<Box<dyn ExpressionThrowTypeProvider>>,
101    function_throw_exact: AtomMap<Vec<usize>>,
102    function_throw_prefix: Vec<(Atom, usize)>,
103    function_throw_namespace: Vec<(Atom, usize)>,
104    function_throw_providers: Vec<Box<dyn FunctionThrowTypeProvider>>,
105    method_throw_exact: AtomMap<Vec<usize>>,
106    method_throw_wildcard: Vec<(Vec<MethodTarget>, usize)>,
107    method_throw_providers: Vec<Box<dyn MethodThrowTypeProvider>>,
108}
109
110impl std::fmt::Debug for PluginRegistry {
111    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
112        f.debug_struct("PluginRegistry")
113            .field("function_providers", &self.function_providers.len())
114            .field("method_providers", &self.method_providers.len())
115            .field("program_hooks", &self.program_hooks.len())
116            .field("statement_hooks", &self.statement_hooks.len())
117            .field("expression_hooks", &self.expression_hooks.len())
118            .field("function_call_hooks", &self.function_call_hooks.len())
119            .field("method_call_hooks", &self.method_call_hooks.len())
120            .field("static_method_call_hooks", &self.static_method_call_hooks.len())
121            .field("nullsafe_method_call_hooks", &self.nullsafe_method_call_hooks.len())
122            .field("class_hooks", &self.class_hooks.len())
123            .field("interface_hooks", &self.interface_hooks.len())
124            .field("trait_hooks", &self.trait_hooks.len())
125            .field("enum_hooks", &self.enum_hooks.len())
126            .field("function_decl_hooks", &self.function_decl_hooks.len())
127            .field("property_initialization_providers", &self.property_initialization_providers.len())
128            .field("issue_filter_hooks", &self.issue_filter_hooks.len())
129            .field("function_assertion_providers", &self.function_assertion_providers.len())
130            .field("method_assertion_providers", &self.method_assertion_providers.len())
131            .field("expression_throw_providers", &self.expression_throw_providers.len())
132            .field("function_throw_providers", &self.function_throw_providers.len())
133            .field("method_throw_providers", &self.method_throw_providers.len())
134            .finish()
135    }
136}
137
138impl PluginRegistry {
139    #[inline]
140    #[must_use]
141    pub fn new() -> Self {
142        Self::default()
143    }
144
145    #[must_use]
146    pub fn with_library_providers() -> Self {
147        crate::plugin::create_registry()
148    }
149
150    pub fn register_function_provider<P: FunctionReturnTypeProvider + 'static>(&mut self, provider: P) {
151        let index = self.function_providers.len();
152
153        match P::targets() {
154            FunctionTarget::Exact(name) => {
155                self.function_exact.entry(ascii_lowercase_atom(name)).or_default().push(index);
156            }
157            FunctionTarget::ExactMultiple(names) => {
158                for name in names {
159                    self.function_exact.entry(ascii_lowercase_atom(name)).or_default().push(index);
160                }
161            }
162            FunctionTarget::Prefix(prefix) => {
163                self.function_prefix.push((ascii_lowercase_atom(prefix), index));
164            }
165            FunctionTarget::Namespace(ns) => {
166                let ns_lower = ns.to_lowercase();
167                let ns_pattern = if ns_lower.ends_with('\\') { ns_lower } else { format!("{ns_lower}\\") };
168                self.function_namespace.push((ascii_lowercase_atom(&ns_pattern), index));
169            }
170        }
171
172        self.function_providers.push(Box::new(provider));
173    }
174
175    pub fn register_method_provider<P: MethodReturnTypeProvider + 'static>(&mut self, provider: P) {
176        let index = self.method_providers.len();
177        let targets = P::targets();
178
179        let mut has_wildcards = false;
180        let mut wildcard_targets = Vec::new();
181
182        for target in targets {
183            if let Some(key) = target.index_key() {
184                self.method_exact.entry(key).or_default().push(index);
185            } else {
186                has_wildcards = true;
187                wildcard_targets.push(*target);
188            }
189        }
190
191        if has_wildcards {
192            self.method_wildcard.push((wildcard_targets, index));
193        }
194
195        self.method_providers.push(Box::new(provider));
196    }
197
198    pub fn register_program_hook<H: ProgramHook + 'static>(&mut self, hook: H) {
199        self.program_hooks.push(Box::new(hook));
200    }
201
202    pub fn register_statement_hook<H: StatementHook + 'static>(&mut self, hook: H) {
203        self.statement_hooks.push(Box::new(hook));
204    }
205
206    pub fn register_expression_hook<H: ExpressionHook + 'static>(&mut self, hook: H) {
207        self.expression_hooks.push(Box::new(hook));
208    }
209
210    pub fn register_function_call_hook<H: FunctionCallHook + 'static>(&mut self, hook: H) {
211        self.function_call_hooks.push(Box::new(hook));
212    }
213
214    pub fn register_method_call_hook<H: MethodCallHook + 'static>(&mut self, hook: H) {
215        self.method_call_hooks.push(Box::new(hook));
216    }
217
218    pub fn register_static_method_call_hook<H: StaticMethodCallHook + 'static>(&mut self, hook: H) {
219        self.static_method_call_hooks.push(Box::new(hook));
220    }
221
222    pub fn register_nullsafe_method_call_hook<H: NullSafeMethodCallHook + 'static>(&mut self, hook: H) {
223        self.nullsafe_method_call_hooks.push(Box::new(hook));
224    }
225
226    pub fn register_class_hook<H: ClassDeclarationHook + 'static>(&mut self, hook: H) {
227        self.class_hooks.push(Box::new(hook));
228    }
229
230    pub fn register_interface_hook<H: InterfaceDeclarationHook + 'static>(&mut self, hook: H) {
231        self.interface_hooks.push(Box::new(hook));
232    }
233
234    pub fn register_trait_hook<H: TraitDeclarationHook + 'static>(&mut self, hook: H) {
235        self.trait_hooks.push(Box::new(hook));
236    }
237
238    pub fn register_enum_hook<H: EnumDeclarationHook + 'static>(&mut self, hook: H) {
239        self.enum_hooks.push(Box::new(hook));
240    }
241
242    pub fn register_function_decl_hook<H: FunctionDeclarationHook + 'static>(&mut self, hook: H) {
243        self.function_decl_hooks.push(Box::new(hook));
244    }
245
246    pub fn register_property_initialization_provider<P: PropertyInitializationProvider + 'static>(
247        &mut self,
248        provider: P,
249    ) {
250        self.property_initialization_providers.push(Box::new(provider));
251    }
252
253    pub fn register_issue_filter_hook<H: IssueFilterHook + 'static>(&mut self, hook: H) {
254        self.issue_filter_hooks.push(Box::new(hook));
255    }
256
257    pub fn register_function_assertion_provider<P: FunctionAssertionProvider + 'static>(&mut self, provider: P) {
258        let index = self.function_assertion_providers.len();
259
260        match P::targets() {
261            FunctionTarget::Exact(name) => {
262                self.function_assertion_exact.entry(ascii_lowercase_atom(name)).or_default().push(index);
263            }
264            FunctionTarget::ExactMultiple(names) => {
265                for name in names {
266                    self.function_assertion_exact.entry(ascii_lowercase_atom(name)).or_default().push(index);
267                }
268            }
269            FunctionTarget::Prefix(prefix) => {
270                self.function_assertion_prefix.push((ascii_lowercase_atom(prefix), index));
271            }
272            FunctionTarget::Namespace(ns) => {
273                let ns_lower = ns.to_lowercase();
274                let ns_pattern = if ns_lower.ends_with('\\') { ns_lower } else { format!("{ns_lower}\\") };
275                self.function_assertion_namespace.push((ascii_lowercase_atom(&ns_pattern), index));
276            }
277        }
278
279        self.function_assertion_providers.push(Box::new(provider));
280    }
281
282    pub fn register_method_assertion_provider<P: MethodAssertionProvider + 'static>(&mut self, provider: P) {
283        let index = self.method_assertion_providers.len();
284        let targets = P::targets();
285
286        let mut has_wildcards = false;
287        let mut wildcard_targets = Vec::new();
288
289        for target in targets {
290            if let Some(key) = target.index_key() {
291                self.method_assertion_exact.entry(key).or_default().push(index);
292            } else {
293                has_wildcards = true;
294                wildcard_targets.push(*target);
295            }
296        }
297
298        if has_wildcards {
299            self.method_assertion_wildcard.push((wildcard_targets, index));
300        }
301
302        self.method_assertion_providers.push(Box::new(provider));
303    }
304
305    pub fn register_expression_throw_provider<P: ExpressionThrowTypeProvider + 'static>(&mut self, provider: P) {
306        self.expression_throw_providers.push(Box::new(provider));
307    }
308
309    pub fn register_function_throw_provider<P: FunctionThrowTypeProvider + 'static>(&mut self, provider: P) {
310        let index = self.function_throw_providers.len();
311
312        match P::targets() {
313            FunctionTarget::Exact(name) => {
314                self.function_throw_exact.entry(ascii_lowercase_atom(name)).or_default().push(index);
315            }
316            FunctionTarget::ExactMultiple(names) => {
317                for name in names {
318                    self.function_throw_exact.entry(ascii_lowercase_atom(name)).or_default().push(index);
319                }
320            }
321            FunctionTarget::Prefix(prefix) => {
322                self.function_throw_prefix.push((ascii_lowercase_atom(prefix), index));
323            }
324            FunctionTarget::Namespace(ns) => {
325                let ns_lower = ns.to_lowercase();
326                let ns_pattern = if ns_lower.ends_with('\\') { ns_lower } else { format!("{ns_lower}\\") };
327                self.function_throw_namespace.push((ascii_lowercase_atom(&ns_pattern), index));
328            }
329        }
330
331        self.function_throw_providers.push(Box::new(provider));
332    }
333
334    pub fn register_method_throw_provider<P: MethodThrowTypeProvider + 'static>(&mut self, provider: P) {
335        let index = self.method_throw_providers.len();
336        let targets = P::targets();
337
338        let mut has_wildcards = false;
339        let mut wildcard_targets = Vec::new();
340
341        for target in targets {
342            if let Some(key) = target.index_key() {
343                self.method_throw_exact.entry(key).or_default().push(index);
344            } else {
345                has_wildcards = true;
346                wildcard_targets.push(*target);
347            }
348        }
349
350        if has_wildcards {
351            self.method_throw_wildcard.push((wildcard_targets, index));
352        }
353
354        self.method_throw_providers.push(Box::new(provider));
355    }
356
357    #[inline]
358    #[must_use]
359    pub fn has_program_hooks(&self) -> bool {
360        !self.program_hooks.is_empty()
361    }
362
363    #[inline]
364    #[must_use]
365    pub fn has_statement_hooks(&self) -> bool {
366        !self.statement_hooks.is_empty()
367    }
368
369    #[inline]
370    #[must_use]
371    pub fn has_expression_hooks(&self) -> bool {
372        !self.expression_hooks.is_empty()
373    }
374
375    #[inline]
376    #[must_use]
377    pub fn has_function_call_hooks(&self) -> bool {
378        !self.function_call_hooks.is_empty()
379    }
380
381    #[inline]
382    #[must_use]
383    pub fn has_method_call_hooks(&self) -> bool {
384        !self.method_call_hooks.is_empty()
385    }
386
387    #[inline]
388    #[must_use]
389    pub fn has_static_method_call_hooks(&self) -> bool {
390        !self.static_method_call_hooks.is_empty()
391    }
392
393    #[inline]
394    #[must_use]
395    pub fn has_nullsafe_method_call_hooks(&self) -> bool {
396        !self.nullsafe_method_call_hooks.is_empty()
397    }
398
399    #[inline]
400    #[must_use]
401    pub fn has_class_hooks(&self) -> bool {
402        !self.class_hooks.is_empty()
403    }
404
405    #[inline]
406    #[must_use]
407    pub fn has_interface_hooks(&self) -> bool {
408        !self.interface_hooks.is_empty()
409    }
410
411    #[inline]
412    #[must_use]
413    pub fn has_trait_hooks(&self) -> bool {
414        !self.trait_hooks.is_empty()
415    }
416
417    #[inline]
418    #[must_use]
419    pub fn has_enum_hooks(&self) -> bool {
420        !self.enum_hooks.is_empty()
421    }
422
423    #[inline]
424    #[must_use]
425    pub fn has_function_decl_hooks(&self) -> bool {
426        !self.function_decl_hooks.is_empty()
427    }
428
429    #[inline]
430    #[must_use]
431    pub fn has_property_initialization_providers(&self) -> bool {
432        !self.property_initialization_providers.is_empty()
433    }
434
435    #[inline]
436    #[must_use]
437    pub fn has_issue_filter_hooks(&self) -> bool {
438        !self.issue_filter_hooks.is_empty()
439    }
440
441    #[inline]
442    #[must_use]
443    pub fn has_function_assertion_providers(&self) -> bool {
444        !self.function_assertion_providers.is_empty()
445    }
446
447    #[inline]
448    #[must_use]
449    pub fn has_method_assertion_providers(&self) -> bool {
450        !self.method_assertion_providers.is_empty()
451    }
452
453    #[inline]
454    #[must_use]
455    pub fn has_expression_throw_providers(&self) -> bool {
456        !self.expression_throw_providers.is_empty()
457    }
458
459    #[inline]
460    #[must_use]
461    pub fn has_function_throw_providers(&self) -> bool {
462        !self.function_throw_providers.is_empty()
463    }
464
465    #[inline]
466    #[must_use]
467    pub fn has_method_throw_providers(&self) -> bool {
468        !self.method_throw_providers.is_empty()
469    }
470
471    pub fn before_program(
472        &self,
473        file: &File,
474        program: &Program<'_>,
475        context: &mut HookContext<'_, '_>,
476    ) -> PluginResult<HookAction> {
477        for hook in &self.program_hooks {
478            if let HookAction::Skip = hook.before_program(file, program, context)? {
479                return Ok(HookAction::Skip);
480            }
481        }
482        Ok(HookAction::Continue)
483    }
484
485    pub fn after_program(
486        &self,
487        file: &File,
488        program: &Program<'_>,
489        context: &mut HookContext<'_, '_>,
490    ) -> PluginResult<()> {
491        for hook in &self.program_hooks {
492            hook.after_program(file, program, context)?;
493        }
494        Ok(())
495    }
496
497    pub fn before_statement(
498        &self,
499        stmt: &Statement<'_>,
500        context: &mut HookContext<'_, '_>,
501    ) -> PluginResult<HookAction> {
502        for hook in &self.statement_hooks {
503            if let HookAction::Skip = hook.before_statement(stmt, context)? {
504                return Ok(HookAction::Skip);
505            }
506        }
507        Ok(HookAction::Continue)
508    }
509
510    pub fn after_statement(&self, stmt: &Statement<'_>, context: &mut HookContext<'_, '_>) -> PluginResult<()> {
511        for hook in &self.statement_hooks {
512            hook.after_statement(stmt, context)?;
513        }
514        Ok(())
515    }
516
517    pub fn before_expression(
518        &self,
519        expr: &Expression<'_>,
520        context: &mut HookContext<'_, '_>,
521    ) -> PluginResult<ExpressionHookResult> {
522        for hook in &self.expression_hooks {
523            let result = hook.before_expression(expr, context)?;
524            if result.should_skip() {
525                return Ok(result);
526            }
527        }
528        Ok(ExpressionHookResult::Continue)
529    }
530
531    pub fn after_expression(&self, expr: &Expression<'_>, context: &mut HookContext<'_, '_>) -> PluginResult<()> {
532        for hook in &self.expression_hooks {
533            hook.after_expression(expr, context)?;
534        }
535        Ok(())
536    }
537
538    pub fn before_function_call(
539        &self,
540        call: &FunctionCall<'_>,
541        context: &mut HookContext<'_, '_>,
542    ) -> PluginResult<ExpressionHookResult> {
543        for hook in &self.function_call_hooks {
544            let result = hook.before_function_call(call, context)?;
545            if result.should_skip() {
546                return Ok(result);
547            }
548        }
549        Ok(ExpressionHookResult::Continue)
550    }
551
552    pub fn after_function_call(&self, call: &FunctionCall<'_>, context: &mut HookContext<'_, '_>) -> PluginResult<()> {
553        for hook in &self.function_call_hooks {
554            hook.after_function_call(call, context)?;
555        }
556        Ok(())
557    }
558
559    pub fn before_method_call(
560        &self,
561        call: &MethodCall<'_>,
562        context: &mut HookContext<'_, '_>,
563    ) -> PluginResult<ExpressionHookResult> {
564        for hook in &self.method_call_hooks {
565            let result = hook.before_method_call(call, context)?;
566            if result.should_skip() {
567                return Ok(result);
568            }
569        }
570        Ok(ExpressionHookResult::Continue)
571    }
572
573    pub fn after_method_call(&self, call: &MethodCall<'_>, context: &mut HookContext<'_, '_>) -> PluginResult<()> {
574        for hook in &self.method_call_hooks {
575            hook.after_method_call(call, context)?;
576        }
577        Ok(())
578    }
579
580    pub fn before_static_method_call(
581        &self,
582        call: &StaticMethodCall<'_>,
583        context: &mut HookContext<'_, '_>,
584    ) -> PluginResult<ExpressionHookResult> {
585        for hook in &self.static_method_call_hooks {
586            let result = hook.before_static_method_call(call, context)?;
587            if result.should_skip() {
588                return Ok(result);
589            }
590        }
591        Ok(ExpressionHookResult::Continue)
592    }
593
594    pub fn after_static_method_call(
595        &self,
596        call: &StaticMethodCall<'_>,
597        context: &mut HookContext<'_, '_>,
598    ) -> PluginResult<()> {
599        for hook in &self.static_method_call_hooks {
600            hook.after_static_method_call(call, context)?;
601        }
602        Ok(())
603    }
604
605    pub fn before_nullsafe_method_call(
606        &self,
607        call: &NullSafeMethodCall<'_>,
608        context: &mut HookContext<'_, '_>,
609    ) -> PluginResult<ExpressionHookResult> {
610        for hook in &self.nullsafe_method_call_hooks {
611            let result = hook.before_nullsafe_method_call(call, context)?;
612            if result.should_skip() {
613                return Ok(result);
614            }
615        }
616        Ok(ExpressionHookResult::Continue)
617    }
618
619    pub fn after_nullsafe_method_call(
620        &self,
621        call: &NullSafeMethodCall<'_>,
622        context: &mut HookContext<'_, '_>,
623    ) -> PluginResult<()> {
624        for hook in &self.nullsafe_method_call_hooks {
625            hook.after_nullsafe_method_call(call, context)?;
626        }
627        Ok(())
628    }
629
630    pub fn on_enter_class(
631        &self,
632        class: &Class<'_>,
633        metadata: &ClassLikeMetadata,
634        context: &mut HookContext<'_, '_>,
635    ) -> PluginResult<()> {
636        for hook in &self.class_hooks {
637            hook.on_enter_class(class, metadata, context)?;
638        }
639        Ok(())
640    }
641
642    pub fn on_leave_class(
643        &self,
644        class: &Class<'_>,
645        metadata: &ClassLikeMetadata,
646        context: &mut HookContext<'_, '_>,
647    ) -> PluginResult<()> {
648        for hook in &self.class_hooks {
649            hook.on_leave_class(class, metadata, context)?;
650        }
651        Ok(())
652    }
653
654    pub fn on_enter_interface(
655        &self,
656        interface: &Interface<'_>,
657        metadata: &ClassLikeMetadata,
658        context: &mut HookContext<'_, '_>,
659    ) -> PluginResult<()> {
660        for hook in &self.interface_hooks {
661            hook.on_enter_interface(interface, metadata, context)?;
662        }
663        Ok(())
664    }
665
666    pub fn on_leave_interface(
667        &self,
668        interface: &Interface<'_>,
669        metadata: &ClassLikeMetadata,
670        context: &mut HookContext<'_, '_>,
671    ) -> PluginResult<()> {
672        for hook in &self.interface_hooks {
673            hook.on_leave_interface(interface, metadata, context)?;
674        }
675        Ok(())
676    }
677
678    pub fn on_enter_trait(
679        &self,
680        trait_: &Trait<'_>,
681        metadata: &ClassLikeMetadata,
682        context: &mut HookContext<'_, '_>,
683    ) -> PluginResult<()> {
684        for hook in &self.trait_hooks {
685            hook.on_enter_trait(trait_, metadata, context)?;
686        }
687        Ok(())
688    }
689
690    pub fn on_leave_trait(
691        &self,
692        trait_: &Trait<'_>,
693        metadata: &ClassLikeMetadata,
694        context: &mut HookContext<'_, '_>,
695    ) -> PluginResult<()> {
696        for hook in &self.trait_hooks {
697            hook.on_leave_trait(trait_, metadata, context)?;
698        }
699        Ok(())
700    }
701
702    pub fn on_enter_enum(
703        &self,
704        enum_: &Enum<'_>,
705        metadata: &ClassLikeMetadata,
706        context: &mut HookContext<'_, '_>,
707    ) -> PluginResult<()> {
708        for hook in &self.enum_hooks {
709            hook.on_enter_enum(enum_, metadata, context)?;
710        }
711        Ok(())
712    }
713
714    pub fn on_leave_enum(
715        &self,
716        enum_: &Enum<'_>,
717        metadata: &ClassLikeMetadata,
718        context: &mut HookContext<'_, '_>,
719    ) -> PluginResult<()> {
720        for hook in &self.enum_hooks {
721            hook.on_leave_enum(enum_, metadata, context)?;
722        }
723        Ok(())
724    }
725
726    pub fn on_enter_function(
727        &self,
728        function: &Function<'_>,
729        metadata: &FunctionLikeMetadata,
730        context: &mut HookContext<'_, '_>,
731    ) -> PluginResult<()> {
732        for hook in &self.function_decl_hooks {
733            hook.on_enter_function(function, metadata, context)?;
734        }
735        Ok(())
736    }
737
738    pub fn on_leave_function(
739        &self,
740        function: &Function<'_>,
741        metadata: &FunctionLikeMetadata,
742        context: &mut HookContext<'_, '_>,
743    ) -> PluginResult<()> {
744        for hook in &self.function_decl_hooks {
745            hook.on_leave_function(function, metadata, context)?;
746        }
747        Ok(())
748    }
749
750    fn get_function_provider_indices(&self, name: &str) -> Vec<usize> {
751        let lower_name = ascii_lowercase_atom(name);
752        let mut indices = Vec::new();
753
754        if let Some(idxs) = self.function_exact.get(&lower_name) {
755            indices.extend(idxs.iter().copied());
756        }
757
758        for (prefix, idx) in &self.function_prefix {
759            if lower_name.as_str().starts_with(prefix.as_str()) && !indices.contains(idx) {
760                indices.push(*idx);
761            }
762        }
763
764        for (ns, idx) in &self.function_namespace {
765            if lower_name.as_str().starts_with(ns.as_str()) && !indices.contains(idx) {
766                indices.push(*idx);
767            }
768        }
769
770        indices
771    }
772
773    fn get_method_provider_indices(&self, class_name: &str, method_name: &str) -> Vec<usize> {
774        use mago_atom::concat_atom;
775        let key =
776            concat_atom!(ascii_lowercase_atom(class_name).as_str(), "::", ascii_lowercase_atom(method_name).as_str());
777        let mut indices = Vec::new();
778
779        if let Some(idxs) = self.method_exact.get(&key) {
780            indices.extend(idxs.iter().copied());
781        }
782
783        for (targets, idx) in &self.method_wildcard {
784            if !indices.contains(idx) {
785                for target in targets {
786                    if target.matches(class_name, method_name) {
787                        indices.push(*idx);
788                        break;
789                    }
790                }
791            }
792        }
793
794        indices
795    }
796
797    #[must_use]
798    pub fn get_function_like_return_type<'ctx>(
799        &self,
800        codebase: &'ctx CodebaseMetadata,
801        block_context: &BlockContext<'ctx>,
802        artifacts: &AnalysisArtifacts,
803        function_like: &FunctionLikeIdentifier,
804        invocation: &Invocation<'ctx, '_, '_>,
805    ) -> Option<ProviderResult> {
806        match function_like {
807            FunctionLikeIdentifier::Function(name) => {
808                Some(self.get_function_return_type(codebase, block_context, artifacts, name, invocation))
809            }
810            FunctionLikeIdentifier::Method(class_name, method_name) => Some(self.get_method_return_type(
811                codebase,
812                block_context,
813                artifacts,
814                class_name,
815                method_name,
816                invocation,
817            )),
818            _ => None,
819        }
820    }
821
822    #[must_use]
823    pub fn get_function_return_type<'ctx>(
824        &self,
825        codebase: &'ctx CodebaseMetadata,
826        block_context: &BlockContext<'ctx>,
827        artifacts: &AnalysisArtifacts,
828        function_name: &str,
829        invocation: &Invocation<'ctx, '_, '_>,
830    ) -> ProviderResult {
831        let indices = self.get_function_provider_indices(function_name);
832        let mut all_issues = Vec::new();
833
834        for idx in indices {
835            let provider_context = ProviderContext::new(codebase, block_context, artifacts);
836            let invocation_info = InvocationInfo::new(invocation);
837
838            if let Some(ty) = self.function_providers[idx].get_return_type(&provider_context, &invocation_info) {
839                all_issues.extend(provider_context.take_issues());
840                return ProviderResult { return_type: Some(ty), issues: all_issues };
841            }
842
843            all_issues.extend(provider_context.take_issues());
844        }
845
846        ProviderResult { return_type: None, issues: all_issues }
847    }
848
849    #[must_use]
850    pub fn get_method_return_type<'ctx>(
851        &self,
852        codebase: &'ctx CodebaseMetadata,
853        block_context: &BlockContext<'ctx>,
854        artifacts: &AnalysisArtifacts,
855        class_name: &str,
856        method_name: &str,
857        invocation: &Invocation<'ctx, '_, '_>,
858    ) -> ProviderResult {
859        let indices = self.get_method_provider_indices(class_name, method_name);
860        let mut all_issues = Vec::new();
861
862        for idx in indices {
863            let provider_context = ProviderContext::new(codebase, block_context, artifacts);
864            let invocation_info = InvocationInfo::new(invocation);
865
866            if let Some(ty) =
867                self.method_providers[idx].get_return_type(&provider_context, class_name, method_name, &invocation_info)
868            {
869                all_issues.extend(provider_context.take_issues());
870                return ProviderResult { return_type: Some(ty), issues: all_issues };
871            }
872
873            all_issues.extend(provider_context.take_issues());
874        }
875
876        ProviderResult { return_type: None, issues: all_issues }
877    }
878
879    #[inline]
880    #[must_use]
881    pub fn function_provider_count(&self) -> usize {
882        self.function_providers.len()
883    }
884
885    #[inline]
886    #[must_use]
887    pub fn method_provider_count(&self) -> usize {
888        self.method_providers.len()
889    }
890
891    /// Check if a property should be considered initialized by any registered provider.
892    ///
893    /// Returns `true` if any provider considers the property initialized.
894    #[must_use]
895    pub fn is_property_initialized(
896        &self,
897        class_metadata: &ClassLikeMetadata,
898        property_metadata: &PropertyMetadata,
899    ) -> bool {
900        for provider in &self.property_initialization_providers {
901            if provider.is_property_initialized(class_metadata, property_metadata) {
902                return true;
903            }
904        }
905
906        false
907    }
908
909    fn get_function_assertion_provider_indices(&self, name: &str) -> Vec<usize> {
910        if self.function_assertion_exact.is_empty()
911            && self.function_assertion_prefix.is_empty()
912            && self.function_assertion_namespace.is_empty()
913        {
914            return Vec::new();
915        }
916
917        let lower_name = ascii_lowercase_atom(name);
918        let mut indices = Vec::new();
919
920        if let Some(idxs) = self.function_assertion_exact.get(&lower_name) {
921            indices.extend(idxs.iter().copied());
922        }
923
924        for (prefix, idx) in &self.function_assertion_prefix {
925            if lower_name.as_str().starts_with(prefix.as_str()) && !indices.contains(idx) {
926                indices.push(*idx);
927            }
928        }
929
930        for (ns, idx) in &self.function_assertion_namespace {
931            if lower_name.as_str().starts_with(ns.as_str()) && !indices.contains(idx) {
932                indices.push(*idx);
933            }
934        }
935
936        indices
937    }
938
939    fn get_method_assertion_provider_indices(&self, class_name: &str, method_name: &str) -> Vec<usize> {
940        if self.method_assertion_exact.is_empty() && self.method_assertion_wildcard.is_empty() {
941            return Vec::new();
942        }
943
944        use mago_atom::concat_atom;
945        let key =
946            concat_atom!(ascii_lowercase_atom(class_name).as_str(), "::", ascii_lowercase_atom(method_name).as_str());
947        let mut indices = Vec::new();
948
949        if let Some(idxs) = self.method_assertion_exact.get(&key) {
950            indices.extend(idxs.iter().copied());
951        }
952
953        for (targets, idx) in &self.method_assertion_wildcard {
954            if !indices.contains(idx) {
955                for target in targets {
956                    if target.matches(class_name, method_name) {
957                        indices.push(*idx);
958                        break;
959                    }
960                }
961            }
962        }
963
964        indices
965    }
966
967    #[must_use]
968    pub fn get_function_like_assertions<'ctx>(
969        &self,
970        codebase: &'ctx CodebaseMetadata,
971        block_context: &BlockContext<'ctx>,
972        artifacts: &AnalysisArtifacts,
973        function_like: &FunctionLikeIdentifier,
974        invocation: &Invocation<'ctx, '_, '_>,
975    ) -> Option<InvocationAssertions> {
976        match function_like {
977            FunctionLikeIdentifier::Function(name) => {
978                self.get_function_assertions(codebase, block_context, artifacts, name, invocation)
979            }
980            FunctionLikeIdentifier::Method(class_name, method_name) => {
981                self.get_method_assertions(codebase, block_context, artifacts, class_name, method_name, invocation)
982            }
983            _ => None,
984        }
985    }
986
987    /// Get assertions for a function invocation from registered providers.
988    #[must_use]
989    pub fn get_function_assertions<'ctx>(
990        &self,
991        codebase: &'ctx CodebaseMetadata,
992        block_context: &BlockContext<'ctx>,
993        artifacts: &AnalysisArtifacts,
994        function_name: &str,
995        invocation: &Invocation<'ctx, '_, '_>,
996    ) -> Option<InvocationAssertions> {
997        if self.function_assertion_providers.is_empty() {
998            return None;
999        }
1000
1001        let indices = self.get_function_assertion_provider_indices(function_name);
1002
1003        for idx in indices {
1004            let provider_context = ProviderContext::new(codebase, block_context, artifacts);
1005            let invocation_info = InvocationInfo::new(invocation);
1006
1007            if let Some(assertions) =
1008                self.function_assertion_providers[idx].get_assertions(&provider_context, &invocation_info)
1009                && !assertions.is_empty()
1010            {
1011                return Some(assertions);
1012            }
1013        }
1014
1015        None
1016    }
1017
1018    /// Get assertions for a method invocation from registered providers.
1019    #[must_use]
1020    pub fn get_method_assertions<'ctx>(
1021        &self,
1022        codebase: &'ctx CodebaseMetadata,
1023        block_context: &BlockContext<'ctx>,
1024        artifacts: &AnalysisArtifacts,
1025        class_name: &str,
1026        method_name: &str,
1027        invocation: &Invocation<'ctx, '_, '_>,
1028    ) -> Option<InvocationAssertions> {
1029        if self.method_assertion_providers.is_empty() {
1030            return None;
1031        }
1032
1033        let indices = self.get_method_assertion_provider_indices(class_name, method_name);
1034
1035        for idx in indices {
1036            let provider_context = ProviderContext::new(codebase, block_context, artifacts);
1037            let invocation_info = InvocationInfo::new(invocation);
1038
1039            if let Some(assertions) = self.method_assertion_providers[idx].get_assertions(
1040                &provider_context,
1041                class_name,
1042                method_name,
1043                &invocation_info,
1044            ) && !assertions.is_empty()
1045            {
1046                return Some(assertions);
1047            }
1048        }
1049
1050        None
1051    }
1052
1053    fn get_function_throw_provider_indices(&self, name: &str) -> Vec<usize> {
1054        if self.function_throw_exact.is_empty()
1055            && self.function_throw_prefix.is_empty()
1056            && self.function_throw_namespace.is_empty()
1057        {
1058            return Vec::new();
1059        }
1060
1061        let lower_name = ascii_lowercase_atom(name);
1062        let mut indices = Vec::new();
1063
1064        if let Some(idxs) = self.function_throw_exact.get(&lower_name) {
1065            indices.extend(idxs.iter().copied());
1066        }
1067
1068        for (prefix, idx) in &self.function_throw_prefix {
1069            if lower_name.as_str().starts_with(prefix.as_str()) && !indices.contains(idx) {
1070                indices.push(*idx);
1071            }
1072        }
1073
1074        for (ns, idx) in &self.function_throw_namespace {
1075            if lower_name.as_str().starts_with(ns.as_str()) && !indices.contains(idx) {
1076                indices.push(*idx);
1077            }
1078        }
1079
1080        indices
1081    }
1082
1083    fn get_method_throw_provider_indices(&self, class_name: &str, method_name: &str) -> Vec<usize> {
1084        if self.method_throw_providers.is_empty()
1085            && self.method_throw_exact.is_empty()
1086            && self.method_throw_wildcard.is_empty()
1087        {
1088            return Vec::new();
1089        }
1090
1091        use mago_atom::concat_atom;
1092        let key =
1093            concat_atom!(ascii_lowercase_atom(class_name).as_str(), "::", ascii_lowercase_atom(method_name).as_str());
1094        let mut indices = Vec::new();
1095
1096        if let Some(idxs) = self.method_throw_exact.get(&key) {
1097            indices.extend(idxs.iter().copied());
1098        }
1099
1100        for (targets, idx) in &self.method_throw_wildcard {
1101            if !indices.contains(idx) {
1102                for target in targets {
1103                    if target.matches(class_name, method_name) {
1104                        indices.push(*idx);
1105                        break;
1106                    }
1107                }
1108            }
1109        }
1110
1111        indices
1112    }
1113
1114    /// Get thrown exception class names for an expression from registered providers.
1115    #[must_use]
1116    pub fn get_expression_thrown_exceptions<'ctx>(
1117        &self,
1118        codebase: &'ctx CodebaseMetadata,
1119        block_context: &BlockContext<'ctx>,
1120        artifacts: &AnalysisArtifacts,
1121        expression: &mago_syntax::ast::Expression<'_>,
1122    ) -> AtomSet {
1123        let mut exceptions = AtomSet::default();
1124
1125        for provider in &self.expression_throw_providers {
1126            let provider_context = ProviderContext::new(codebase, block_context, artifacts);
1127            exceptions.extend(provider.get_thrown_exceptions(&provider_context, expression));
1128        }
1129
1130        exceptions
1131    }
1132
1133    /// Get thrown exception class names for a function invocation from registered providers.
1134    #[must_use]
1135    pub fn get_function_thrown_exceptions<'ctx>(
1136        &self,
1137        codebase: &'ctx CodebaseMetadata,
1138        block_context: &BlockContext<'ctx>,
1139        artifacts: &AnalysisArtifacts,
1140        function_name: &str,
1141        invocation: &Invocation<'ctx, '_, '_>,
1142    ) -> AtomSet {
1143        let mut exceptions = AtomSet::default();
1144        let indices = self.get_function_throw_provider_indices(function_name);
1145
1146        for idx in indices {
1147            let provider_context = ProviderContext::new(codebase, block_context, artifacts);
1148            let invocation_info = InvocationInfo::new(invocation);
1149            exceptions
1150                .extend(self.function_throw_providers[idx].get_thrown_exceptions(&provider_context, &invocation_info));
1151        }
1152
1153        exceptions
1154    }
1155
1156    /// Get thrown exception class names for a method invocation from registered providers.
1157    #[must_use]
1158    pub fn get_method_thrown_exceptions<'ctx>(
1159        &self,
1160        codebase: &'ctx CodebaseMetadata,
1161        block_context: &BlockContext<'ctx>,
1162        artifacts: &AnalysisArtifacts,
1163        class_name: &str,
1164        method_name: &str,
1165        invocation: &Invocation<'ctx, '_, '_>,
1166    ) -> AtomSet {
1167        let mut exceptions = AtomSet::default();
1168        let indices = self.get_method_throw_provider_indices(class_name, method_name);
1169
1170        for idx in indices {
1171            let provider_context = ProviderContext::new(codebase, block_context, artifacts);
1172            let invocation_info = InvocationInfo::new(invocation);
1173            exceptions.extend(self.method_throw_providers[idx].get_thrown_exceptions(
1174                &provider_context,
1175                class_name,
1176                method_name,
1177                &invocation_info,
1178            ));
1179        }
1180
1181        exceptions
1182    }
1183
1184    /// Filter issues through all registered issue filter hooks.
1185    ///
1186    /// Returns a new `IssueCollection` with filtered issues.
1187    #[must_use]
1188    pub fn filter_issues(&self, file: &File, issues: IssueCollection) -> IssueCollection {
1189        if self.issue_filter_hooks.is_empty() {
1190            return issues;
1191        }
1192
1193        let mut filtered = IssueCollection::default();
1194
1195        for issue in issues {
1196            let mut keep = true;
1197            for hook in &self.issue_filter_hooks {
1198                if let Ok(IssueFilterDecision::Remove) = hook.filter_issue(file, &issue) {
1199                    keep = false;
1200                    break;
1201                }
1202            }
1203
1204            if keep {
1205                filtered.push(issue);
1206            }
1207        }
1208
1209        filtered
1210    }
1211}
1212
1213#[cfg(test)]
1214mod tests {
1215    use super::*;
1216    use crate::plugin::provider::Provider;
1217    use crate::plugin::provider::ProviderMeta;
1218
1219    static TEST_META: ProviderMeta = ProviderMeta::new("test::provider", "Test Provider", "A test provider");
1220
1221    struct TestFunctionProvider;
1222
1223    impl Provider for TestFunctionProvider {
1224        fn meta() -> &'static ProviderMeta {
1225            &TEST_META
1226        }
1227    }
1228
1229    impl FunctionReturnTypeProvider for TestFunctionProvider {
1230        fn targets() -> FunctionTarget {
1231            FunctionTarget::Exact("test_func")
1232        }
1233
1234        fn get_return_type(
1235            &self,
1236            _context: &ProviderContext<'_, '_, '_>,
1237            _invocation: &InvocationInfo<'_, '_, '_>,
1238        ) -> Option<TUnion> {
1239            None
1240        }
1241    }
1242
1243    #[test]
1244    fn test_register_function_provider() {
1245        let mut registry = PluginRegistry::new();
1246        registry.register_function_provider(TestFunctionProvider);
1247
1248        assert_eq!(registry.function_provider_count(), 1);
1249        let indices = registry.get_function_provider_indices("test_func");
1250        assert_eq!(indices.len(), 1);
1251    }
1252
1253    #[test]
1254    fn test_function_exact_match() {
1255        let mut registry = PluginRegistry::new();
1256        registry.register_function_provider(TestFunctionProvider);
1257
1258        let indices = registry.get_function_provider_indices("test_func");
1259        assert_eq!(indices.len(), 1);
1260
1261        let indices = registry.get_function_provider_indices("TEST_FUNC");
1262        assert_eq!(indices.len(), 1);
1263
1264        let indices = registry.get_function_provider_indices("other_func");
1265        assert!(indices.is_empty());
1266    }
1267}