Skip to main content

mago_analyzer/plugin/
registry.rs

1//! Plugin registry for managing and dispatching to providers and hooks.
2
3use std::sync::Arc;
4use std::sync::OnceLock;
5
6use mago_codex::identifier::function_like::FunctionLikeIdentifier;
7use mago_codex::metadata::CodebaseMetadata;
8use mago_codex::metadata::class_like::ClassLikeMetadata;
9use mago_codex::metadata::function_like::FunctionLikeMetadata;
10use mago_codex::metadata::property::PropertyMetadata;
11use mago_codex::ttype::union::TUnion;
12use mago_database::file::File;
13use mago_names::ResolvedNames;
14use mago_reporting::IssueCollection;
15use mago_span::Span;
16use mago_syntax::cst::Class;
17use mago_syntax::cst::Enum;
18use mago_syntax::cst::Expression;
19use mago_syntax::cst::Function;
20use mago_syntax::cst::FunctionCall;
21use mago_syntax::cst::Interface;
22use mago_syntax::cst::MethodCall;
23use mago_syntax::cst::NullSafeMethodCall;
24use mago_syntax::cst::Program;
25use mago_syntax::cst::Statement;
26use mago_syntax::cst::StaticMethodCall;
27use mago_syntax::cst::Trait;
28use mago_word::Word;
29use mago_word::WordMap;
30use mago_word::WordSet;
31use mago_word::ascii_lowercase_word;
32use mago_word::concat_word;
33
34use crate::artifacts::AnalysisArtifacts;
35use crate::context::block::BlockContext;
36use crate::external::AfterFileAnalysisResult;
37use crate::external::BeforeAnalysisResult;
38use crate::external::CodebaseScanFile;
39use crate::external::CodebaseScanPlan;
40use crate::external::EffectivePropertyType;
41use crate::external::ExternalAnalysisSession;
42use crate::external::ExternalAnalyzer;
43use crate::external::ExternalAnalyzerCapabilities;
44use crate::external::ExternalAnalyzerError;
45use crate::external::ExternalAnalyzerHandle;
46use crate::external::FileAnalysisSnapshot;
47use crate::external::NodeAnalysisRequirements;
48use crate::external::PropertyAccessKind;
49use crate::invocation::EffectiveCallableSignature;
50use crate::invocation::Invocation;
51use crate::plugin::PluginError;
52use crate::plugin::context::HookContext;
53use crate::plugin::context::InvocationInfo;
54use crate::plugin::context::ProviderContext;
55use crate::plugin::context::ReportedIssue;
56use crate::plugin::error::PluginResult;
57use crate::plugin::hook::ClassDeclarationHook;
58use crate::plugin::hook::EnumDeclarationHook;
59use crate::plugin::hook::ExpressionHook;
60use crate::plugin::hook::ExpressionHookResult;
61use crate::plugin::hook::FunctionCallHook;
62use crate::plugin::hook::FunctionDeclarationHook;
63use crate::plugin::hook::HookAction;
64use crate::plugin::hook::InterfaceDeclarationHook;
65use crate::plugin::hook::IssueFilterDecision;
66use crate::plugin::hook::IssueFilterHook;
67use crate::plugin::hook::MethodCallHook;
68use crate::plugin::hook::NullSafeMethodCallHook;
69use crate::plugin::hook::ProgramHook;
70use crate::plugin::hook::StatementHook;
71use crate::plugin::hook::StaticMethodCallHook;
72use crate::plugin::hook::TraitDeclarationHook;
73use crate::plugin::provider::assertion::FunctionAssertionProvider;
74use crate::plugin::provider::assertion::InvocationAssertions;
75use crate::plugin::provider::assertion::MethodAssertionProvider;
76use crate::plugin::provider::function::FunctionReturnTypeProvider;
77use crate::plugin::provider::function::FunctionTarget;
78use crate::plugin::provider::method::MethodReturnTypeProvider;
79use crate::plugin::provider::method::MethodTarget;
80use crate::plugin::provider::property::PropertyInitializationProvider;
81use crate::plugin::provider::throw::ExpressionThrowTypeProvider;
82use crate::plugin::provider::throw::FunctionThrowTypeProvider;
83use crate::plugin::provider::throw::MethodThrowTypeProvider;
84
85pub struct ProviderResult {
86    pub return_type: Option<TUnion>,
87    pub issues: Vec<ReportedIssue>,
88}
89
90fn optional_external_hint<T>(operation: &'static str, result: Result<T, Arc<ExternalAnalyzerError>>) -> T
91where
92    T: Default,
93{
94    match result {
95        Ok(value) => value,
96        Err(error) => {
97            tracing::warn!(operation, error = %error, "External analyzer provider failed; using native analysis fallback.");
98            T::default()
99        }
100    }
101}
102
103#[derive(Default)]
104pub struct PluginRegistry {
105    external_analyzer: Option<Arc<ExternalAnalyzerHandle>>,
106    external_capabilities: OnceLock<ExternalAnalyzerCapabilities>,
107    function_exact: WordMap<Vec<usize>>,
108    function_prefix: Vec<(Word, usize)>,
109    function_namespace: Vec<(Word, usize)>,
110    function_providers: Vec<Box<dyn FunctionReturnTypeProvider>>,
111    method_exact: WordMap<Vec<usize>>,
112    method_wildcard: Vec<(Vec<MethodTarget>, usize)>,
113    method_providers: Vec<Box<dyn MethodReturnTypeProvider>>,
114    program_hooks: Vec<Box<dyn ProgramHook>>,
115    statement_hooks: Vec<Box<dyn StatementHook>>,
116    expression_hooks: Vec<Box<dyn ExpressionHook>>,
117    function_call_hooks: Vec<Box<dyn FunctionCallHook>>,
118    method_call_hooks: Vec<Box<dyn MethodCallHook>>,
119    static_method_call_hooks: Vec<Box<dyn StaticMethodCallHook>>,
120    nullsafe_method_call_hooks: Vec<Box<dyn NullSafeMethodCallHook>>,
121    class_hooks: Vec<Box<dyn ClassDeclarationHook>>,
122    interface_hooks: Vec<Box<dyn InterfaceDeclarationHook>>,
123    trait_hooks: Vec<Box<dyn TraitDeclarationHook>>,
124    enum_hooks: Vec<Box<dyn EnumDeclarationHook>>,
125    function_decl_hooks: Vec<Box<dyn FunctionDeclarationHook>>,
126    property_initialization_providers: Vec<Box<dyn PropertyInitializationProvider>>,
127    issue_filter_hooks: Vec<Box<dyn IssueFilterHook>>,
128    function_assertion_exact: WordMap<Vec<usize>>,
129    function_assertion_prefix: Vec<(Word, usize)>,
130    function_assertion_namespace: Vec<(Word, usize)>,
131    function_assertion_providers: Vec<Box<dyn FunctionAssertionProvider>>,
132    method_assertion_exact: WordMap<Vec<usize>>,
133    method_assertion_wildcard: Vec<(Vec<MethodTarget>, usize)>,
134    method_assertion_providers: Vec<Box<dyn MethodAssertionProvider>>,
135    expression_throw_providers: Vec<Box<dyn ExpressionThrowTypeProvider>>,
136    function_throw_exact: WordMap<Vec<usize>>,
137    function_throw_prefix: Vec<(Word, usize)>,
138    function_throw_namespace: Vec<(Word, usize)>,
139    function_throw_providers: Vec<Box<dyn FunctionThrowTypeProvider>>,
140    method_throw_exact: WordMap<Vec<usize>>,
141    method_throw_wildcard: Vec<(Vec<MethodTarget>, usize)>,
142    method_throw_providers: Vec<Box<dyn MethodThrowTypeProvider>>,
143}
144
145#[allow(clippy::missing_fields_in_debug)]
146impl std::fmt::Debug for PluginRegistry {
147    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
148        f.debug_struct("PluginRegistry")
149            .field("external_analyzer", &self.external_analyzer.is_some())
150            .field("external_capabilities", &self.external_capabilities.get())
151            .field("function_providers", &self.function_providers.len())
152            .field("method_providers", &self.method_providers.len())
153            .field("program_hooks", &self.program_hooks.len())
154            .field("statement_hooks", &self.statement_hooks.len())
155            .field("expression_hooks", &self.expression_hooks.len())
156            .field("function_call_hooks", &self.function_call_hooks.len())
157            .field("method_call_hooks", &self.method_call_hooks.len())
158            .field("static_method_call_hooks", &self.static_method_call_hooks.len())
159            .field("nullsafe_method_call_hooks", &self.nullsafe_method_call_hooks.len())
160            .field("class_hooks", &self.class_hooks.len())
161            .field("interface_hooks", &self.interface_hooks.len())
162            .field("trait_hooks", &self.trait_hooks.len())
163            .field("enum_hooks", &self.enum_hooks.len())
164            .field("function_decl_hooks", &self.function_decl_hooks.len())
165            .field("property_initialization_providers", &self.property_initialization_providers.len())
166            .field("issue_filter_hooks", &self.issue_filter_hooks.len())
167            .field("function_assertion_providers", &self.function_assertion_providers.len())
168            .field("method_assertion_providers", &self.method_assertion_providers.len())
169            .field("expression_throw_providers", &self.expression_throw_providers.len())
170            .field("function_throw_providers", &self.function_throw_providers.len())
171            .field("method_throw_providers", &self.method_throw_providers.len())
172            .finish()
173    }
174}
175
176impl PluginRegistry {
177    /// Attaches worker-backed analyzer plugins to this registry.
178    pub fn set_external_analyzer(&mut self, analyzer: Arc<ExternalAnalyzerHandle>) {
179        self.external_analyzer = Some(analyzer);
180    }
181
182    /// Completes concurrent external analyzer initialization before file analysis.
183    ///
184    /// # Errors
185    ///
186    /// Returns an error when a worker fails to initialize or advertises invalid capabilities.
187    pub fn prepare_external_analyzer(&self) -> PluginResult<()> {
188        let Some(analyzer) = self.external_analyzer.as_deref() else {
189            return Ok(());
190        };
191
192        analyzer.prepare().map_err(PluginError::from)?;
193        let capabilities = analyzer.read(|analyzer| analyzer.capabilities()).map_err(PluginError::from)?;
194        let _capabilities = self.external_capabilities.set(capabilities);
195        Ok(())
196    }
197
198    #[inline]
199    fn has_external_capability(&self, capability: fn(&ExternalAnalyzerCapabilities) -> bool) -> bool {
200        self.external_analyzer.is_some() && self.external_capabilities.get().is_none_or(capability)
201    }
202
203    #[inline]
204    #[must_use]
205    pub(crate) fn has_external_method_call_analysis_hooks(&self) -> bool {
206        self.has_external_capability(|capabilities| capabilities.method_call_analysis)
207    }
208
209    /// Completes external initialization and returns its in-memory source stubs.
210    ///
211    /// # Errors
212    ///
213    /// Returns an error when a worker fails to initialize or returns invalid initialization data.
214    pub fn external_initialization_files(&self) -> PluginResult<Vec<File>> {
215        self.prepare_external_analyzer()?;
216        self.external_analyzer
217            .as_deref()
218            .map(ExternalAnalyzerHandle::initialization_files)
219            .transpose()
220            .map(Option::unwrap_or_default)
221            .map_err(PluginError::from)
222    }
223
224    /// Returns compiled host-file selectors for enabled external codebase-scan hooks.
225    ///
226    /// # Errors
227    ///
228    /// Returns an error when the external analyzer cannot initialize or a hook advertises an invalid path pattern.
229    pub fn external_codebase_scan_plan(&self) -> PluginResult<Option<CodebaseScanPlan>> {
230        self.prepare_external_analyzer()?;
231        self.external_analyzer
232            .as_deref()
233            .map(|analyzer| analyzer.with(ExternalAnalyzer::codebase_scan_plan))
234            .transpose()
235            .map(Option::flatten)
236            .map_err(PluginError::from)
237    }
238
239    /// Replaces each external worker's selected codebase-scan source state.
240    ///
241    /// # Errors
242    ///
243    /// Returns an error when a worker cannot accept or validate the snapshot sequence.
244    pub fn run_external_codebase_scan(&self, files: Vec<CodebaseScanFile>) -> PluginResult<()> {
245        self.external_analyzer
246            .as_deref()
247            .map(|analyzer| analyzer.with(|analyzer| analyzer.run_codebase_scan(files)))
248            .transpose()
249            .map(|result| result.unwrap_or_default())
250            .map_err(PluginError::from)
251    }
252
253    /// Creates the immutable external-plugin context for one frozen codebase generation.
254    #[must_use]
255    pub fn create_external_analysis_session(
256        &self,
257        files: impl IntoIterator<Item = Arc<File>>,
258    ) -> Option<ExternalAnalysisSession> {
259        self.external_analyzer.as_ref()?;
260        Some(ExternalAnalysisSession::from_files(files))
261    }
262
263    /// Returns whether any enabled external plugin subscribed to per-file completion.
264    ///
265    /// # Errors
266    ///
267    /// Returns an error when the external analyzer cannot be initialized.
268    pub fn has_external_after_file_analysis_hooks(&self) -> PluginResult<bool> {
269        self.external_analyzer
270            .as_deref()
271            .map(|analyzer| analyzer.read(|analyzer| analyzer.capabilities().after_file_analysis))
272            .transpose()
273            .map(Option::unwrap_or_default)
274            .map_err(PluginError::from)
275    }
276
277    /// Returns the syntax targets and embedded data requested by enabled external analyzer hooks.
278    ///
279    /// # Errors
280    ///
281    /// Returns an error when the external analyzer cannot be initialized.
282    pub fn external_node_analysis_requirements(&self) -> PluginResult<Option<NodeAnalysisRequirements>> {
283        self.external_analyzer
284            .as_deref()
285            .map(|analyzer| analyzer.read(ExternalAnalyzer::node_analysis_requirements))
286            .transpose()
287            .map(Option::flatten)
288            .map_err(PluginError::from)
289    }
290
291    /// Returns whether any enabled external plugin subscribed to whole-project completion.
292    ///
293    /// # Errors
294    ///
295    /// Returns an error when the external analyzer cannot be initialized.
296    pub fn has_external_after_analysis_hooks(&self) -> PluginResult<bool> {
297        self.external_analyzer
298            .as_deref()
299            .map(|analyzer| analyzer.read(|analyzer| analyzer.capabilities().after_analysis))
300            .transpose()
301            .map(Option::unwrap_or_default)
302            .map_err(PluginError::from)
303    }
304
305    /// Runs enabled external hooks after the codebase is frozen and before file analysis starts.
306    ///
307    /// # Errors
308    ///
309    /// Returns an error when an external hook cannot be dispatched or returns an invalid response.
310    pub fn run_external_before_analysis_hooks(
311        &self,
312        codebase: &CodebaseMetadata,
313        session: Option<&ExternalAnalysisSession>,
314    ) -> PluginResult<BeforeAnalysisResult> {
315        self.external_analyzer
316            .as_deref()
317            .zip(session)
318            .map(|(analyzer, session)| analyzer.with(|analyzer| analyzer.run_before_analysis_hooks(codebase, session)))
319            .transpose()
320            .map(Option::unwrap_or_default)
321            .map_err(PluginError::from)
322    }
323
324    /// Runs enabled external hooks for one completed file analysis.
325    ///
326    /// # Errors
327    ///
328    /// Returns an error when an external hook cannot be dispatched or returns an invalid response.
329    pub fn run_external_after_file_analysis_hooks(
330        &self,
331        file: &File,
332        program: &Program<'_>,
333        resolved_names: &ResolvedNames<'_>,
334        artifacts: &AnalysisArtifacts,
335        codebase: &CodebaseMetadata,
336        session: Option<&ExternalAnalysisSession>,
337    ) -> PluginResult<AfterFileAnalysisResult> {
338        self.external_analyzer
339            .as_deref()
340            .zip(session)
341            .map(|(analyzer, session)| {
342                analyzer.with(|analyzer| {
343                    analyzer.run_after_file_analysis_hooks(file, program, resolved_names, artifacts, codebase, session)
344                })
345            })
346            .transpose()
347            .map(Option::unwrap_or_default)
348            .map_err(PluginError::from)
349    }
350
351    /// Runs enabled external after-file hooks for a batch of completed analyses.
352    ///
353    /// # Errors
354    ///
355    /// Returns an error when an external hook cannot be dispatched or returns an invalid response.
356    pub fn run_external_after_file_analysis_batch_hooks(
357        &self,
358        files: &[Arc<FileAnalysisSnapshot>],
359        codebase: &CodebaseMetadata,
360        session: Option<&ExternalAnalysisSession>,
361    ) -> PluginResult<AfterFileAnalysisResult> {
362        self.external_analyzer
363            .as_deref()
364            .zip(session)
365            .map(|(analyzer, session)| {
366                analyzer.with(|analyzer| analyzer.run_after_file_analysis_batch_hooks(files, codebase, session))
367            })
368            .transpose()
369            .map(Option::unwrap_or_default)
370            .map_err(PluginError::from)
371    }
372
373    /// Runs enabled external hooks for the final merged analysis result.
374    ///
375    /// # Errors
376    ///
377    /// Returns an error when an external hook cannot be dispatched or returns an invalid response.
378    pub fn run_external_after_analysis_hooks(
379        &self,
380        result: &crate::analysis_result::AnalysisResult,
381        files: &[Arc<FileAnalysisSnapshot>],
382        codebase: &CodebaseMetadata,
383        session: Option<&ExternalAnalysisSession>,
384    ) -> PluginResult<IssueCollection> {
385        self.external_analyzer
386            .as_deref()
387            .zip(session)
388            .map(|(analyzer, session)| {
389                analyzer.with(|analyzer| analyzer.run_after_analysis_hooks(result, files, codebase, session))
390            })
391            .transpose()
392            .map(Option::unwrap_or_default)
393            .map_err(PluginError::from)
394    }
395
396    #[inline]
397    #[must_use]
398    pub fn new() -> Self {
399        Self::default()
400    }
401
402    #[must_use]
403    pub fn with_library_providers() -> Self {
404        crate::plugin::create_registry()
405    }
406
407    pub fn register_function_provider<P>(&mut self, provider: P)
408    where
409        P: FunctionReturnTypeProvider + 'static,
410    {
411        let index = self.function_providers.len();
412
413        match P::targets() {
414            FunctionTarget::Exact(name) => {
415                self.function_exact.entry(ascii_lowercase_word(name)).or_default().push(index);
416            }
417            FunctionTarget::ExactMultiple(names) => {
418                for name in names {
419                    self.function_exact.entry(ascii_lowercase_word(name)).or_default().push(index);
420                }
421            }
422            FunctionTarget::Prefix(prefix) => {
423                self.function_prefix.push((ascii_lowercase_word(prefix), index));
424            }
425            FunctionTarget::Namespace(ns) => {
426                let ns_lower = ascii_lowercase_word(ns);
427                let ns_pattern = if ns_lower.as_bytes().last() == Some(&b'\\') {
428                    ns_lower
429                } else {
430                    concat_word!(ns_lower.as_bytes(), b"\\")
431                };
432                self.function_namespace.push((ns_pattern, index));
433            }
434        }
435
436        self.function_providers.push(Box::new(provider));
437    }
438
439    pub fn register_method_provider<P>(&mut self, provider: P)
440    where
441        P: MethodReturnTypeProvider + 'static,
442    {
443        let index = self.method_providers.len();
444        let targets = P::targets();
445
446        let mut has_wildcards = false;
447        let mut wildcard_targets = Vec::new();
448
449        for target in targets {
450            if let Some(key) = target.index_key() {
451                self.method_exact.entry(key).or_default().push(index);
452            } else {
453                has_wildcards = true;
454                wildcard_targets.push(*target);
455            }
456        }
457
458        if has_wildcards {
459            self.method_wildcard.push((wildcard_targets, index));
460        }
461
462        self.method_providers.push(Box::new(provider));
463    }
464
465    pub fn register_program_hook<H>(&mut self, hook: H)
466    where
467        H: ProgramHook + 'static,
468    {
469        self.program_hooks.push(Box::new(hook));
470    }
471
472    pub fn register_statement_hook<H>(&mut self, hook: H)
473    where
474        H: StatementHook + 'static,
475    {
476        self.statement_hooks.push(Box::new(hook));
477    }
478
479    pub fn register_expression_hook<H>(&mut self, hook: H)
480    where
481        H: ExpressionHook + 'static,
482    {
483        self.expression_hooks.push(Box::new(hook));
484    }
485
486    pub fn register_function_call_hook<H>(&mut self, hook: H)
487    where
488        H: FunctionCallHook + 'static,
489    {
490        self.function_call_hooks.push(Box::new(hook));
491    }
492
493    pub fn register_method_call_hook<H>(&mut self, hook: H)
494    where
495        H: MethodCallHook + 'static,
496    {
497        self.method_call_hooks.push(Box::new(hook));
498    }
499
500    pub fn register_static_method_call_hook<H>(&mut self, hook: H)
501    where
502        H: StaticMethodCallHook + 'static,
503    {
504        self.static_method_call_hooks.push(Box::new(hook));
505    }
506
507    pub fn register_nullsafe_method_call_hook<H>(&mut self, hook: H)
508    where
509        H: NullSafeMethodCallHook + 'static,
510    {
511        self.nullsafe_method_call_hooks.push(Box::new(hook));
512    }
513
514    pub fn register_class_hook<H>(&mut self, hook: H)
515    where
516        H: ClassDeclarationHook + 'static,
517    {
518        self.class_hooks.push(Box::new(hook));
519    }
520
521    pub fn register_interface_hook<H>(&mut self, hook: H)
522    where
523        H: InterfaceDeclarationHook + 'static,
524    {
525        self.interface_hooks.push(Box::new(hook));
526    }
527
528    pub fn register_trait_hook<H>(&mut self, hook: H)
529    where
530        H: TraitDeclarationHook + 'static,
531    {
532        self.trait_hooks.push(Box::new(hook));
533    }
534
535    pub fn register_enum_hook<H>(&mut self, hook: H)
536    where
537        H: EnumDeclarationHook + 'static,
538    {
539        self.enum_hooks.push(Box::new(hook));
540    }
541
542    pub fn register_function_decl_hook<H>(&mut self, hook: H)
543    where
544        H: FunctionDeclarationHook + 'static,
545    {
546        self.function_decl_hooks.push(Box::new(hook));
547    }
548
549    pub fn register_property_initialization_provider<P>(&mut self, provider: P)
550    where
551        P: PropertyInitializationProvider + 'static,
552    {
553        self.property_initialization_providers.push(Box::new(provider));
554    }
555
556    pub fn register_issue_filter_hook<H>(&mut self, hook: H)
557    where
558        H: IssueFilterHook + 'static,
559    {
560        self.issue_filter_hooks.push(Box::new(hook));
561    }
562
563    pub fn register_function_assertion_provider<P>(&mut self, provider: P)
564    where
565        P: FunctionAssertionProvider + 'static,
566    {
567        let index = self.function_assertion_providers.len();
568
569        match P::targets() {
570            FunctionTarget::Exact(name) => {
571                self.function_assertion_exact.entry(ascii_lowercase_word(name)).or_default().push(index);
572            }
573            FunctionTarget::ExactMultiple(names) => {
574                for name in names {
575                    self.function_assertion_exact.entry(ascii_lowercase_word(name)).or_default().push(index);
576                }
577            }
578            FunctionTarget::Prefix(prefix) => {
579                self.function_assertion_prefix.push((ascii_lowercase_word(prefix), index));
580            }
581            FunctionTarget::Namespace(ns) => {
582                let ns_lower = ascii_lowercase_word(ns);
583                let ns_pattern = if ns_lower.as_bytes().last() == Some(&b'\\') {
584                    ns_lower
585                } else {
586                    concat_word!(ns_lower.as_bytes(), b"\\")
587                };
588                self.function_assertion_namespace.push((ns_pattern, index));
589            }
590        }
591
592        self.function_assertion_providers.push(Box::new(provider));
593    }
594
595    pub fn register_method_assertion_provider<P>(&mut self, provider: P)
596    where
597        P: MethodAssertionProvider + 'static,
598    {
599        let index = self.method_assertion_providers.len();
600        let targets = P::targets();
601
602        let mut has_wildcards = false;
603        let mut wildcard_targets = Vec::new();
604
605        for target in targets {
606            if let Some(key) = target.index_key() {
607                self.method_assertion_exact.entry(key).or_default().push(index);
608            } else {
609                has_wildcards = true;
610                wildcard_targets.push(*target);
611            }
612        }
613
614        if has_wildcards {
615            self.method_assertion_wildcard.push((wildcard_targets, index));
616        }
617
618        self.method_assertion_providers.push(Box::new(provider));
619    }
620
621    pub fn register_expression_throw_provider<P>(&mut self, provider: P)
622    where
623        P: ExpressionThrowTypeProvider + 'static,
624    {
625        self.expression_throw_providers.push(Box::new(provider));
626    }
627
628    pub fn register_function_throw_provider<P>(&mut self, provider: P)
629    where
630        P: FunctionThrowTypeProvider + 'static,
631    {
632        let index = self.function_throw_providers.len();
633
634        match P::targets() {
635            FunctionTarget::Exact(name) => {
636                self.function_throw_exact.entry(ascii_lowercase_word(name)).or_default().push(index);
637            }
638            FunctionTarget::ExactMultiple(names) => {
639                for name in names {
640                    self.function_throw_exact.entry(ascii_lowercase_word(name)).or_default().push(index);
641                }
642            }
643            FunctionTarget::Prefix(prefix) => {
644                self.function_throw_prefix.push((ascii_lowercase_word(prefix), index));
645            }
646            FunctionTarget::Namespace(ns) => {
647                let ns_lower = ascii_lowercase_word(ns);
648                let ns_pattern = if ns_lower.as_bytes().last() == Some(&b'\\') {
649                    ns_lower
650                } else {
651                    concat_word!(ns_lower.as_bytes(), b"\\")
652                };
653                self.function_throw_namespace.push((ns_pattern, index));
654            }
655        }
656
657        self.function_throw_providers.push(Box::new(provider));
658    }
659
660    pub fn register_method_throw_provider<P>(&mut self, provider: P)
661    where
662        P: MethodThrowTypeProvider + 'static,
663    {
664        let index = self.method_throw_providers.len();
665        let targets = P::targets();
666
667        let mut has_wildcards = false;
668        let mut wildcard_targets = Vec::new();
669
670        for target in targets {
671            if let Some(key) = target.index_key() {
672                self.method_throw_exact.entry(key).or_default().push(index);
673            } else {
674                has_wildcards = true;
675                wildcard_targets.push(*target);
676            }
677        }
678
679        if has_wildcards {
680            self.method_throw_wildcard.push((wildcard_targets, index));
681        }
682
683        self.method_throw_providers.push(Box::new(provider));
684    }
685
686    #[inline]
687    #[must_use]
688    pub fn has_program_hooks(&self) -> bool {
689        !self.program_hooks.is_empty()
690    }
691
692    #[inline]
693    #[must_use]
694    pub fn has_statement_hooks(&self) -> bool {
695        !self.statement_hooks.is_empty()
696    }
697
698    #[inline]
699    #[must_use]
700    pub fn has_expression_hooks(&self) -> bool {
701        !self.expression_hooks.is_empty()
702    }
703
704    #[inline]
705    #[must_use]
706    pub fn has_function_call_hooks(&self) -> bool {
707        !self.function_call_hooks.is_empty()
708    }
709
710    #[inline]
711    #[must_use]
712    pub fn has_method_call_hooks(&self) -> bool {
713        !self.method_call_hooks.is_empty()
714    }
715
716    #[inline]
717    #[must_use]
718    pub fn has_static_method_call_hooks(&self) -> bool {
719        !self.static_method_call_hooks.is_empty()
720    }
721
722    #[inline]
723    #[must_use]
724    pub fn has_nullsafe_method_call_hooks(&self) -> bool {
725        !self.nullsafe_method_call_hooks.is_empty()
726    }
727
728    #[inline]
729    #[must_use]
730    pub fn has_class_hooks(&self) -> bool {
731        !self.class_hooks.is_empty()
732    }
733
734    #[inline]
735    #[must_use]
736    pub fn has_interface_hooks(&self) -> bool {
737        !self.interface_hooks.is_empty()
738    }
739
740    #[inline]
741    #[must_use]
742    pub fn has_trait_hooks(&self) -> bool {
743        !self.trait_hooks.is_empty()
744    }
745
746    #[inline]
747    #[must_use]
748    pub fn has_enum_hooks(&self) -> bool {
749        !self.enum_hooks.is_empty()
750    }
751
752    #[inline]
753    #[must_use]
754    pub fn has_function_decl_hooks(&self) -> bool {
755        !self.function_decl_hooks.is_empty()
756    }
757
758    #[inline]
759    #[must_use]
760    pub fn has_property_initialization_providers(&self) -> bool {
761        !self.property_initialization_providers.is_empty()
762            || self.has_external_capability(|capabilities| capabilities.property_initialization)
763    }
764
765    #[inline]
766    #[must_use]
767    pub fn has_issue_filter_hooks(&self) -> bool {
768        !self.issue_filter_hooks.is_empty() || self.has_external_capability(|capabilities| capabilities.issue_filters)
769    }
770
771    #[inline]
772    #[must_use]
773    pub fn has_function_assertion_providers(&self) -> bool {
774        !self.function_assertion_providers.is_empty()
775    }
776
777    #[inline]
778    #[must_use]
779    pub fn has_method_assertion_providers(&self) -> bool {
780        !self.method_assertion_providers.is_empty()
781    }
782
783    #[inline]
784    #[must_use]
785    pub fn has_expression_throw_providers(&self) -> bool {
786        !self.expression_throw_providers.is_empty()
787    }
788
789    #[inline]
790    #[must_use]
791    pub fn has_function_throw_providers(&self) -> bool {
792        !self.function_throw_providers.is_empty()
793    }
794
795    #[inline]
796    #[must_use]
797    pub fn has_method_throw_providers(&self) -> bool {
798        !self.method_throw_providers.is_empty()
799    }
800
801    /// Run all registered program hooks before analysis.
802    ///
803    /// # Errors
804    ///
805    /// Returns [`PluginError`] if any registered hook propagates one.
806    pub fn before_program(
807        &self,
808        file: &File,
809        program: &Program<'_>,
810        context: &mut HookContext<'_, '_>,
811    ) -> PluginResult<HookAction> {
812        for hook in &self.program_hooks {
813            if hook.before_program(file, program, context)? == HookAction::Skip {
814                return Ok(HookAction::Skip);
815            }
816        }
817        Ok(HookAction::Continue)
818    }
819
820    /// Run all registered program hooks after analysis.
821    ///
822    /// # Errors
823    ///
824    /// Returns [`PluginError`] if any registered hook propagates one.
825    pub fn after_program(
826        &self,
827        file: &File,
828        program: &Program<'_>,
829        context: &mut HookContext<'_, '_>,
830    ) -> PluginResult<()> {
831        for hook in &self.program_hooks {
832            hook.after_program(file, program, context)?;
833        }
834        Ok(())
835    }
836
837    /// Run all registered statement hooks before analysis.
838    ///
839    /// # Errors
840    ///
841    /// Returns [`PluginError`] if any registered hook propagates one.
842    pub fn before_statement(
843        &self,
844        stmt: &Statement<'_>,
845        context: &mut HookContext<'_, '_>,
846    ) -> PluginResult<HookAction> {
847        for hook in &self.statement_hooks {
848            if hook.before_statement(stmt, context)? == HookAction::Skip {
849                return Ok(HookAction::Skip);
850            }
851        }
852        Ok(HookAction::Continue)
853    }
854
855    /// Run all registered statement hooks after analysis.
856    ///
857    /// # Errors
858    ///
859    /// Returns [`PluginError`] if any registered hook propagates one.
860    pub fn after_statement(&self, stmt: &Statement<'_>, context: &mut HookContext<'_, '_>) -> PluginResult<()> {
861        for hook in &self.statement_hooks {
862            hook.after_statement(stmt, context)?;
863        }
864        Ok(())
865    }
866
867    /// Run all registered expression hooks before analysis.
868    ///
869    /// # Errors
870    ///
871    /// Returns [`PluginError`] if any registered hook propagates one.
872    pub fn before_expression(
873        &self,
874        expr: &Expression<'_>,
875        context: &mut HookContext<'_, '_>,
876    ) -> PluginResult<ExpressionHookResult> {
877        for hook in &self.expression_hooks {
878            let result = hook.before_expression(expr, context)?;
879            if result.should_skip() {
880                return Ok(result);
881            }
882        }
883        Ok(ExpressionHookResult::Continue)
884    }
885
886    /// Run all registered expression hooks after analysis.
887    ///
888    /// # Errors
889    ///
890    /// Returns [`PluginError`] if any registered hook propagates one.
891    pub fn after_expression(&self, expr: &Expression<'_>, context: &mut HookContext<'_, '_>) -> PluginResult<()> {
892        for hook in &self.expression_hooks {
893            hook.after_expression(expr, context)?;
894        }
895        Ok(())
896    }
897
898    /// Run all registered function call hooks before analysis.
899    ///
900    /// # Errors
901    ///
902    /// Returns [`PluginError`] if any registered hook propagates one.
903    pub fn before_function_call(
904        &self,
905        call: &FunctionCall<'_>,
906        context: &mut HookContext<'_, '_>,
907    ) -> PluginResult<ExpressionHookResult> {
908        for hook in &self.function_call_hooks {
909            let result = hook.before_function_call(call, context)?;
910            if result.should_skip() {
911                return Ok(result);
912            }
913        }
914        Ok(ExpressionHookResult::Continue)
915    }
916
917    /// Run all registered function call hooks after analysis.
918    ///
919    /// # Errors
920    ///
921    /// Returns [`PluginError`] if any registered hook propagates one.
922    pub fn after_function_call(&self, call: &FunctionCall<'_>, context: &mut HookContext<'_, '_>) -> PluginResult<()> {
923        for hook in &self.function_call_hooks {
924            hook.after_function_call(call, context)?;
925        }
926        Ok(())
927    }
928
929    /// Run all registered method call hooks before analysis.
930    ///
931    /// # Errors
932    ///
933    /// Returns [`PluginError`] if any registered hook propagates one.
934    pub fn before_method_call(
935        &self,
936        call: &MethodCall<'_>,
937        context: &mut HookContext<'_, '_>,
938    ) -> PluginResult<ExpressionHookResult> {
939        for hook in &self.method_call_hooks {
940            let result = hook.before_method_call(call, context)?;
941            if result.should_skip() {
942                return Ok(result);
943            }
944        }
945        Ok(ExpressionHookResult::Continue)
946    }
947
948    /// Run all registered method call hooks after analysis.
949    ///
950    /// # Errors
951    ///
952    /// Returns [`PluginError`] if any registered hook propagates one.
953    pub fn after_method_call(&self, call: &MethodCall<'_>, context: &mut HookContext<'_, '_>) -> PluginResult<()> {
954        for hook in &self.method_call_hooks {
955            hook.after_method_call(call, context)?;
956        }
957        Ok(())
958    }
959
960    /// Run all registered static method call hooks before analysis.
961    ///
962    /// # Errors
963    ///
964    /// Returns [`PluginError`] if any registered hook propagates one.
965    pub fn before_static_method_call(
966        &self,
967        call: &StaticMethodCall<'_>,
968        context: &mut HookContext<'_, '_>,
969    ) -> PluginResult<ExpressionHookResult> {
970        for hook in &self.static_method_call_hooks {
971            let result = hook.before_static_method_call(call, context)?;
972            if result.should_skip() {
973                return Ok(result);
974            }
975        }
976        Ok(ExpressionHookResult::Continue)
977    }
978
979    /// Run all registered static method call hooks after analysis.
980    ///
981    /// # Errors
982    ///
983    /// Returns [`PluginError`] if any registered hook propagates one.
984    pub fn after_static_method_call(
985        &self,
986        call: &StaticMethodCall<'_>,
987        context: &mut HookContext<'_, '_>,
988    ) -> PluginResult<()> {
989        for hook in &self.static_method_call_hooks {
990            hook.after_static_method_call(call, context)?;
991        }
992        Ok(())
993    }
994
995    /// Run all registered nullsafe method call hooks before analysis.
996    ///
997    /// # Errors
998    ///
999    /// Returns [`PluginError`] if any registered hook propagates one.
1000    pub fn before_nullsafe_method_call(
1001        &self,
1002        call: &NullSafeMethodCall<'_>,
1003        context: &mut HookContext<'_, '_>,
1004    ) -> PluginResult<ExpressionHookResult> {
1005        for hook in &self.nullsafe_method_call_hooks {
1006            let result = hook.before_nullsafe_method_call(call, context)?;
1007            if result.should_skip() {
1008                return Ok(result);
1009            }
1010        }
1011        Ok(ExpressionHookResult::Continue)
1012    }
1013
1014    /// Run all registered nullsafe method call hooks after analysis.
1015    ///
1016    /// # Errors
1017    ///
1018    /// Returns [`PluginError`] if any registered hook propagates one.
1019    pub fn after_nullsafe_method_call(
1020        &self,
1021        call: &NullSafeMethodCall<'_>,
1022        context: &mut HookContext<'_, '_>,
1023    ) -> PluginResult<()> {
1024        for hook in &self.nullsafe_method_call_hooks {
1025            hook.after_nullsafe_method_call(call, context)?;
1026        }
1027        Ok(())
1028    }
1029
1030    /// Run all registered class declaration hooks on entry.
1031    ///
1032    /// # Errors
1033    ///
1034    /// Returns [`PluginError`] if any registered hook propagates one.
1035    pub fn on_enter_class(
1036        &self,
1037        class: &Class<'_>,
1038        metadata: &ClassLikeMetadata,
1039        context: &mut HookContext<'_, '_>,
1040    ) -> PluginResult<()> {
1041        for hook in &self.class_hooks {
1042            hook.on_enter_class(class, metadata, context)?;
1043        }
1044        Ok(())
1045    }
1046
1047    /// Run all registered class declaration hooks on exit.
1048    ///
1049    /// # Errors
1050    ///
1051    /// Returns [`PluginError`] if any registered hook propagates one.
1052    pub fn on_leave_class(
1053        &self,
1054        class: &Class<'_>,
1055        metadata: &ClassLikeMetadata,
1056        context: &mut HookContext<'_, '_>,
1057    ) -> PluginResult<()> {
1058        for hook in &self.class_hooks {
1059            hook.on_leave_class(class, metadata, context)?;
1060        }
1061        Ok(())
1062    }
1063
1064    /// Run all registered interface declaration hooks on entry.
1065    ///
1066    /// # Errors
1067    ///
1068    /// Returns [`PluginError`] if any registered hook propagates one.
1069    pub fn on_enter_interface(
1070        &self,
1071        interface: &Interface<'_>,
1072        metadata: &ClassLikeMetadata,
1073        context: &mut HookContext<'_, '_>,
1074    ) -> PluginResult<()> {
1075        for hook in &self.interface_hooks {
1076            hook.on_enter_interface(interface, metadata, context)?;
1077        }
1078        Ok(())
1079    }
1080
1081    /// Run all registered interface declaration hooks on exit.
1082    ///
1083    /// # Errors
1084    ///
1085    /// Returns [`PluginError`] if any registered hook propagates one.
1086    pub fn on_leave_interface(
1087        &self,
1088        interface: &Interface<'_>,
1089        metadata: &ClassLikeMetadata,
1090        context: &mut HookContext<'_, '_>,
1091    ) -> PluginResult<()> {
1092        for hook in &self.interface_hooks {
1093            hook.on_leave_interface(interface, metadata, context)?;
1094        }
1095        Ok(())
1096    }
1097
1098    /// Run all registered trait declaration hooks on entry.
1099    ///
1100    /// # Errors
1101    ///
1102    /// Returns [`PluginError`] if any registered hook propagates one.
1103    pub fn on_enter_trait(
1104        &self,
1105        trait_: &Trait<'_>,
1106        metadata: &ClassLikeMetadata,
1107        context: &mut HookContext<'_, '_>,
1108    ) -> PluginResult<()> {
1109        for hook in &self.trait_hooks {
1110            hook.on_enter_trait(trait_, metadata, context)?;
1111        }
1112        Ok(())
1113    }
1114
1115    /// Run all registered trait declaration hooks on exit.
1116    ///
1117    /// # Errors
1118    ///
1119    /// Returns [`PluginError`] if any registered hook propagates one.
1120    pub fn on_leave_trait(
1121        &self,
1122        trait_: &Trait<'_>,
1123        metadata: &ClassLikeMetadata,
1124        context: &mut HookContext<'_, '_>,
1125    ) -> PluginResult<()> {
1126        for hook in &self.trait_hooks {
1127            hook.on_leave_trait(trait_, metadata, context)?;
1128        }
1129        Ok(())
1130    }
1131
1132    /// Run all registered enum declaration hooks on entry.
1133    ///
1134    /// # Errors
1135    ///
1136    /// Returns [`PluginError`] if any registered hook propagates one.
1137    pub fn on_enter_enum(
1138        &self,
1139        enum_: &Enum<'_>,
1140        metadata: &ClassLikeMetadata,
1141        context: &mut HookContext<'_, '_>,
1142    ) -> PluginResult<()> {
1143        for hook in &self.enum_hooks {
1144            hook.on_enter_enum(enum_, metadata, context)?;
1145        }
1146        Ok(())
1147    }
1148
1149    /// Run all registered enum declaration hooks on exit.
1150    ///
1151    /// # Errors
1152    ///
1153    /// Returns [`PluginError`] if any registered hook propagates one.
1154    pub fn on_leave_enum(
1155        &self,
1156        enum_: &Enum<'_>,
1157        metadata: &ClassLikeMetadata,
1158        context: &mut HookContext<'_, '_>,
1159    ) -> PluginResult<()> {
1160        for hook in &self.enum_hooks {
1161            hook.on_leave_enum(enum_, metadata, context)?;
1162        }
1163        Ok(())
1164    }
1165
1166    /// Run all registered function declaration hooks on entry.
1167    ///
1168    /// # Errors
1169    ///
1170    /// Returns [`PluginError`] if any registered hook propagates one.
1171    pub fn on_enter_function(
1172        &self,
1173        function: &Function<'_>,
1174        metadata: &FunctionLikeMetadata,
1175        context: &mut HookContext<'_, '_>,
1176    ) -> PluginResult<()> {
1177        for hook in &self.function_decl_hooks {
1178            hook.on_enter_function(function, metadata, context)?;
1179        }
1180        Ok(())
1181    }
1182
1183    /// Run all registered function declaration hooks on exit.
1184    ///
1185    /// # Errors
1186    ///
1187    /// Returns [`PluginError`] if any registered hook propagates one.
1188    pub fn on_leave_function(
1189        &self,
1190        function: &Function<'_>,
1191        metadata: &FunctionLikeMetadata,
1192        context: &mut HookContext<'_, '_>,
1193    ) -> PluginResult<()> {
1194        for hook in &self.function_decl_hooks {
1195            hook.on_leave_function(function, metadata, context)?;
1196        }
1197        Ok(())
1198    }
1199
1200    fn get_function_provider_indices(&self, name: &[u8]) -> Vec<usize> {
1201        let lower_name = ascii_lowercase_word(name);
1202        let mut indices = Vec::new();
1203
1204        if let Some(idxs) = self.function_exact.get(&lower_name) {
1205            indices.extend(idxs.iter().copied());
1206        }
1207
1208        for (prefix, idx) in &self.function_prefix {
1209            if lower_name.as_bytes().starts_with(prefix.as_bytes()) && !indices.contains(idx) {
1210                indices.push(*idx);
1211            }
1212        }
1213
1214        for (ns, idx) in &self.function_namespace {
1215            if lower_name.as_bytes().starts_with(ns.as_bytes()) && !indices.contains(idx) {
1216                indices.push(*idx);
1217            }
1218        }
1219
1220        indices
1221    }
1222
1223    fn get_method_provider_indices(&self, class_name: &[u8], method_name: &[u8]) -> Vec<usize> {
1224        use mago_word::concat_word;
1225        let key = concat_word!(ascii_lowercase_word(class_name), b"::", ascii_lowercase_word(method_name));
1226        let mut indices = Vec::new();
1227
1228        if let Some(idxs) = self.method_exact.get(&key) {
1229            indices.extend(idxs.iter().copied());
1230        }
1231
1232        for (targets, idx) in &self.method_wildcard {
1233            if !indices.contains(idx) {
1234                for target in targets {
1235                    if target.matches(class_name, method_name) {
1236                        indices.push(*idx);
1237                        break;
1238                    }
1239                }
1240            }
1241        }
1242
1243        indices
1244    }
1245
1246    #[inline]
1247    #[must_use]
1248    pub(crate) fn may_have_callable_signature_provider(&self, function_like: &FunctionLikeIdentifier) -> bool {
1249        if self.external_analyzer.is_none() {
1250            return false;
1251        }
1252
1253        match function_like {
1254            FunctionLikeIdentifier::Function(_) => {
1255                self.has_external_capability(|capabilities| capabilities.function_signatures)
1256            }
1257            FunctionLikeIdentifier::Method(_, _) => {
1258                self.has_external_capability(|capabilities| capabilities.method_signatures)
1259            }
1260            FunctionLikeIdentifier::Closure(_) => false,
1261        }
1262    }
1263
1264    /// Requests an external provider's effective callable signature before argument analysis.
1265    ///
1266    /// Provider failures are logged and preserve the native callable signature.
1267    pub fn get_function_like_callable_signature<'ctx>(
1268        &self,
1269        codebase: &'ctx CodebaseMetadata,
1270        source_file: &'ctx File,
1271        artifacts: &AnalysisArtifacts,
1272        function_like: &FunctionLikeIdentifier,
1273        invocation: &Invocation<'ctx, '_, '_>,
1274        external_session: Option<&ExternalAnalysisSession>,
1275    ) -> Option<EffectiveCallableSignature> {
1276        let (analyzer, session) = self.external_analyzer.as_deref().zip(external_session)?;
1277
1278        match function_like {
1279            FunctionLikeIdentifier::Function(name)
1280                if self.has_external_capability(|capabilities| capabilities.function_signatures) =>
1281            {
1282                optional_external_hint(
1283                    "function callable-signature provider",
1284                    analyzer.with(|analyzer| {
1285                        analyzer.get_function_callable_signature(
1286                            name.as_bytes(),
1287                            invocation,
1288                            artifacts,
1289                            source_file,
1290                            codebase,
1291                            session,
1292                        )
1293                    }),
1294                )
1295            }
1296            FunctionLikeIdentifier::Method(class, method)
1297                if self.has_external_capability(|capabilities| capabilities.method_signatures) =>
1298            {
1299                optional_external_hint(
1300                    "method callable-signature provider",
1301                    analyzer.with(|analyzer| {
1302                        analyzer.get_method_callable_signature(
1303                            class.as_bytes(),
1304                            method.as_bytes(),
1305                            invocation,
1306                            artifacts,
1307                            source_file,
1308                            codebase,
1309                            session,
1310                        )
1311                    }),
1312                )
1313            }
1314            _ => None,
1315        }
1316    }
1317
1318    /// Returns a provider result for a function or method invocation.
1319    ///
1320    /// External provider failures are logged and preserve the native return type.
1321    pub fn get_function_like_return_type<'ctx>(
1322        &self,
1323        codebase: &'ctx CodebaseMetadata,
1324        source_file: &'ctx File,
1325        block_context: &BlockContext<'ctx>,
1326        artifacts: &AnalysisArtifacts,
1327        function_like: &FunctionLikeIdentifier,
1328        invocation: &Invocation<'ctx, '_, '_>,
1329        external_session: Option<&ExternalAnalysisSession>,
1330    ) -> Option<ProviderResult> {
1331        match function_like {
1332            FunctionLikeIdentifier::Function(name) => Some(self.get_function_return_type(
1333                codebase,
1334                source_file,
1335                block_context,
1336                artifacts,
1337                name.as_bytes(),
1338                invocation,
1339                external_session,
1340            )),
1341            FunctionLikeIdentifier::Method(class_name, method_name) => Some(self.get_method_return_type(
1342                codebase,
1343                source_file,
1344                block_context,
1345                artifacts,
1346                class_name.as_bytes(),
1347                method_name.as_bytes(),
1348                invocation,
1349                external_session,
1350            )),
1351            _ => None,
1352        }
1353    }
1354
1355    /// Returns the first applicable function return-type provider result.
1356    ///
1357    /// External provider failures are logged and preserve the native return type.
1358    pub fn get_function_return_type<'ctx>(
1359        &self,
1360        codebase: &'ctx CodebaseMetadata,
1361        source_file: &'ctx File,
1362        block_context: &BlockContext<'ctx>,
1363        artifacts: &AnalysisArtifacts,
1364        function_name: &[u8],
1365        invocation: &Invocation<'ctx, '_, '_>,
1366        external_session: Option<&ExternalAnalysisSession>,
1367    ) -> ProviderResult {
1368        let indices = self.get_function_provider_indices(function_name);
1369        let mut all_issues = Vec::new();
1370
1371        for idx in indices {
1372            let provider_context = ProviderContext::new(codebase, source_file, block_context, artifacts);
1373            let invocation_info = InvocationInfo::new(invocation);
1374
1375            if let Some(ty) = self.function_providers[idx].get_return_type(&provider_context, &invocation_info) {
1376                all_issues.extend(provider_context.take_issues());
1377                return ProviderResult { return_type: Some(ty), issues: all_issues };
1378            }
1379
1380            all_issues.extend(provider_context.take_issues());
1381        }
1382
1383        let return_type = if self.has_external_capability(|capabilities| capabilities.function_return_types) {
1384            self.external_analyzer.as_deref().zip(external_session).and_then(|(analyzer, session)| {
1385                optional_external_hint(
1386                    "function return-type provider",
1387                    analyzer.with(|analyzer| {
1388                        analyzer.get_function_return_type(
1389                            function_name,
1390                            invocation,
1391                            artifacts,
1392                            source_file,
1393                            codebase,
1394                            session,
1395                        )
1396                    }),
1397                )
1398            })
1399        } else {
1400            None
1401        };
1402
1403        ProviderResult { return_type, issues: all_issues }
1404    }
1405
1406    /// Returns the first applicable method return-type provider result.
1407    ///
1408    /// External provider failures are logged and preserve the native return type.
1409    pub fn get_method_return_type<'ctx>(
1410        &self,
1411        codebase: &'ctx CodebaseMetadata,
1412        source_file: &'ctx File,
1413        block_context: &BlockContext<'ctx>,
1414        artifacts: &AnalysisArtifacts,
1415        class_name: &[u8],
1416        method_name: &[u8],
1417        invocation: &Invocation<'ctx, '_, '_>,
1418        external_session: Option<&ExternalAnalysisSession>,
1419    ) -> ProviderResult {
1420        let indices = self.get_method_provider_indices(class_name, method_name);
1421        let mut all_issues = Vec::new();
1422
1423        for idx in indices {
1424            let provider_context = ProviderContext::new(codebase, source_file, block_context, artifacts);
1425            let invocation_info = InvocationInfo::new(invocation);
1426
1427            if let Some(ty) =
1428                self.method_providers[idx].get_return_type(&provider_context, class_name, method_name, &invocation_info)
1429            {
1430                all_issues.extend(provider_context.take_issues());
1431                return ProviderResult { return_type: Some(ty), issues: all_issues };
1432            }
1433
1434            all_issues.extend(provider_context.take_issues());
1435        }
1436
1437        let return_type = if self.has_external_capability(|capabilities| capabilities.method_return_types) {
1438            self.external_analyzer.as_deref().zip(external_session).and_then(|(analyzer, session)| {
1439                optional_external_hint(
1440                    "method return-type provider",
1441                    analyzer.with(|analyzer| {
1442                        analyzer.get_method_return_type(
1443                            class_name,
1444                            method_name,
1445                            invocation,
1446                            artifacts,
1447                            source_file,
1448                            codebase,
1449                            session,
1450                        )
1451                    }),
1452                )
1453            })
1454        } else {
1455            None
1456        };
1457
1458        ProviderResult { return_type, issues: all_issues }
1459    }
1460
1461    #[inline]
1462    #[must_use]
1463    pub(crate) fn may_have_property_type_provider(&self) -> bool {
1464        self.has_external_capability(|capabilities| capabilities.property_types)
1465    }
1466
1467    #[inline]
1468    #[must_use]
1469    pub(crate) fn may_have_class_initializer_provider(&self) -> bool {
1470        self.has_external_capability(|capabilities| capabilities.class_initializers)
1471    }
1472
1473    /// Requests an external provider's effective magic-property contract.
1474    ///
1475    /// Provider failures are logged and preserve native property resolution.
1476    pub(crate) fn get_property_type(
1477        &self,
1478        codebase: &CodebaseMetadata,
1479        class: &[u8],
1480        property: &[u8],
1481        access: PropertyAccessKind,
1482        receiver_type: &TUnion,
1483        span: Span,
1484        external_session: Option<&ExternalAnalysisSession>,
1485    ) -> Option<EffectivePropertyType> {
1486        if !self.has_external_capability(|capabilities| capabilities.property_types) {
1487            return None;
1488        }
1489
1490        self.external_analyzer.as_deref().zip(external_session).and_then(|(analyzer, session)| {
1491            optional_external_hint(
1492                "property type provider",
1493                analyzer.with(|analyzer| {
1494                    analyzer.get_property_type(class, property, access, receiver_type, span, codebase, session)
1495                }),
1496            )
1497        })
1498    }
1499
1500    #[inline]
1501    #[must_use]
1502    pub fn function_provider_count(&self) -> usize {
1503        self.function_providers.len()
1504    }
1505
1506    #[inline]
1507    #[must_use]
1508    pub fn method_provider_count(&self) -> usize {
1509        self.method_providers.len()
1510    }
1511
1512    /// Checks whether any registered provider considers a property initialized.
1513    ///
1514    /// External provider failures are logged and preserve native initialization analysis.
1515    pub fn is_property_initialized(
1516        &self,
1517        codebase: &CodebaseMetadata,
1518        class_metadata: &ClassLikeMetadata,
1519        property_metadata: &PropertyMetadata,
1520        external_session: Option<&ExternalAnalysisSession>,
1521    ) -> bool {
1522        for provider in &self.property_initialization_providers {
1523            if provider.is_property_initialized(class_metadata, property_metadata) {
1524                return true;
1525            }
1526        }
1527
1528        if !self.has_external_capability(|capabilities| capabilities.property_initialization) {
1529            return false;
1530        }
1531
1532        self.external_analyzer
1533            .as_deref()
1534            .zip(external_session)
1535            .map(|(analyzer, session)| {
1536                optional_external_hint(
1537                    "property initialization provider",
1538                    analyzer.with(|analyzer| {
1539                        analyzer.is_property_initialized(
1540                            class_metadata.name.as_bytes(),
1541                            property_metadata,
1542                            codebase,
1543                            session,
1544                        )
1545                    }),
1546                )
1547            })
1548            .unwrap_or(false)
1549    }
1550
1551    /// Returns framework lifecycle methods that initialize properties on `class_metadata`.
1552    ///
1553    /// External provider failures are logged and preserve native initialization analysis.
1554    pub fn get_class_initializers(
1555        &self,
1556        codebase: &CodebaseMetadata,
1557        class_metadata: &ClassLikeMetadata,
1558        external_session: Option<&ExternalAnalysisSession>,
1559    ) -> WordSet {
1560        if !self.has_external_capability(|capabilities| capabilities.class_initializers) {
1561            return WordSet::default();
1562        }
1563
1564        self.external_analyzer
1565            .as_deref()
1566            .zip(external_session)
1567            .map(|(analyzer, session)| {
1568                optional_external_hint(
1569                    "class initializer provider",
1570                    analyzer.with(|analyzer| analyzer.get_class_initializers(class_metadata, codebase, session)),
1571                )
1572            })
1573            .unwrap_or_default()
1574    }
1575
1576    fn get_function_assertion_provider_indices(&self, name: &[u8]) -> Vec<usize> {
1577        if self.function_assertion_exact.is_empty()
1578            && self.function_assertion_prefix.is_empty()
1579            && self.function_assertion_namespace.is_empty()
1580        {
1581            return Vec::new();
1582        }
1583
1584        let lower_name = ascii_lowercase_word(name);
1585        let mut indices = Vec::new();
1586
1587        if let Some(idxs) = self.function_assertion_exact.get(&lower_name) {
1588            indices.extend(idxs.iter().copied());
1589        }
1590
1591        for (prefix, idx) in &self.function_assertion_prefix {
1592            if lower_name.as_bytes().starts_with(prefix.as_bytes()) && !indices.contains(idx) {
1593                indices.push(*idx);
1594            }
1595        }
1596
1597        for (ns, idx) in &self.function_assertion_namespace {
1598            if lower_name.as_bytes().starts_with(ns.as_bytes()) && !indices.contains(idx) {
1599                indices.push(*idx);
1600            }
1601        }
1602
1603        indices
1604    }
1605
1606    fn get_method_assertion_provider_indices(&self, class_name: &[u8], method_name: &[u8]) -> Vec<usize> {
1607        if self.method_assertion_exact.is_empty() && self.method_assertion_wildcard.is_empty() {
1608            return Vec::new();
1609        }
1610
1611        use mago_word::concat_word;
1612        let key = concat_word!(ascii_lowercase_word(class_name), b"::", ascii_lowercase_word(method_name));
1613        let mut indices = Vec::new();
1614
1615        if let Some(idxs) = self.method_assertion_exact.get(&key) {
1616            indices.extend(idxs.iter().copied());
1617        }
1618
1619        for (targets, idx) in &self.method_assertion_wildcard {
1620            if !indices.contains(idx) {
1621                for target in targets {
1622                    if target.matches(class_name, method_name) {
1623                        indices.push(*idx);
1624                        break;
1625                    }
1626                }
1627            }
1628        }
1629
1630        indices
1631    }
1632
1633    /// Returns assertions for a function or method invocation.
1634    ///
1635    /// External provider failures are logged and preserve native assertion analysis.
1636    pub fn get_function_like_assertions<'ctx>(
1637        &self,
1638        codebase: &'ctx CodebaseMetadata,
1639        source_file: &'ctx File,
1640        block_context: &BlockContext<'ctx>,
1641        artifacts: &AnalysisArtifacts,
1642        function_like: &FunctionLikeIdentifier,
1643        invocation: &Invocation<'ctx, '_, '_>,
1644        external_session: Option<&ExternalAnalysisSession>,
1645    ) -> Option<InvocationAssertions> {
1646        match function_like {
1647            FunctionLikeIdentifier::Function(name) => self.get_function_assertions(
1648                codebase,
1649                source_file,
1650                block_context,
1651                artifacts,
1652                name.as_bytes(),
1653                invocation,
1654                external_session,
1655            ),
1656            FunctionLikeIdentifier::Method(class_name, method_name) => self.get_method_assertions(
1657                codebase,
1658                source_file,
1659                block_context,
1660                artifacts,
1661                class_name.as_bytes(),
1662                method_name.as_bytes(),
1663                invocation,
1664                external_session,
1665            ),
1666            _ => None,
1667        }
1668    }
1669
1670    /// Get assertions for a function invocation from registered providers.
1671    ///
1672    /// External provider failures are logged and preserve native assertion analysis.
1673    pub fn get_function_assertions<'ctx>(
1674        &self,
1675        codebase: &'ctx CodebaseMetadata,
1676        source_file: &'ctx File,
1677        block_context: &BlockContext<'ctx>,
1678        artifacts: &AnalysisArtifacts,
1679        function_name: &[u8],
1680        invocation: &Invocation<'ctx, '_, '_>,
1681        external_session: Option<&ExternalAnalysisSession>,
1682    ) -> Option<InvocationAssertions> {
1683        let may_have_external = self.has_external_capability(|capabilities| capabilities.function_assertions);
1684        if self.function_assertion_providers.is_empty() && !may_have_external {
1685            return None;
1686        }
1687
1688        let indices = self.get_function_assertion_provider_indices(function_name);
1689
1690        for idx in indices {
1691            let provider_context = ProviderContext::new(codebase, source_file, block_context, artifacts);
1692            let invocation_info = InvocationInfo::new(invocation);
1693
1694            if let Some(assertions) =
1695                self.function_assertion_providers[idx].get_assertions(&provider_context, &invocation_info)
1696                && !assertions.is_empty()
1697            {
1698                return Some(assertions);
1699            }
1700        }
1701
1702        if !may_have_external {
1703            return None;
1704        }
1705
1706        self.external_analyzer.as_deref().zip(external_session).and_then(|(analyzer, session)| {
1707            optional_external_hint(
1708                "function assertion provider",
1709                analyzer.with(|analyzer| {
1710                    analyzer.get_function_assertions(
1711                        function_name,
1712                        invocation,
1713                        artifacts,
1714                        source_file,
1715                        codebase,
1716                        session,
1717                    )
1718                }),
1719            )
1720        })
1721    }
1722
1723    /// Get assertions for a method invocation from registered providers.
1724    ///
1725    /// External provider failures are logged and preserve native assertion analysis.
1726    pub fn get_method_assertions<'ctx>(
1727        &self,
1728        codebase: &'ctx CodebaseMetadata,
1729        source_file: &'ctx File,
1730        block_context: &BlockContext<'ctx>,
1731        artifacts: &AnalysisArtifacts,
1732        class_name: &[u8],
1733        method_name: &[u8],
1734        invocation: &Invocation<'ctx, '_, '_>,
1735        external_session: Option<&ExternalAnalysisSession>,
1736    ) -> Option<InvocationAssertions> {
1737        let may_have_external = self.has_external_capability(|capabilities| capabilities.method_assertions);
1738        if self.method_assertion_providers.is_empty() && !may_have_external {
1739            return None;
1740        }
1741
1742        let indices = self.get_method_assertion_provider_indices(class_name, method_name);
1743
1744        for idx in indices {
1745            let provider_context = ProviderContext::new(codebase, source_file, block_context, artifacts);
1746            let invocation_info = InvocationInfo::new(invocation);
1747
1748            if let Some(assertions) = self.method_assertion_providers[idx].get_assertions(
1749                &provider_context,
1750                class_name,
1751                method_name,
1752                &invocation_info,
1753            ) && !assertions.is_empty()
1754            {
1755                return Some(assertions);
1756            }
1757        }
1758
1759        if !may_have_external {
1760            return None;
1761        }
1762
1763        self.external_analyzer.as_deref().zip(external_session).and_then(|(analyzer, session)| {
1764            optional_external_hint(
1765                "method assertion provider",
1766                analyzer.with(|analyzer| {
1767                    analyzer.get_method_assertions(
1768                        class_name,
1769                        method_name,
1770                        invocation,
1771                        artifacts,
1772                        source_file,
1773                        codebase,
1774                        session,
1775                    )
1776                }),
1777            )
1778        })
1779    }
1780
1781    fn get_function_throw_provider_indices(&self, name: &[u8]) -> Vec<usize> {
1782        if self.function_throw_exact.is_empty()
1783            && self.function_throw_prefix.is_empty()
1784            && self.function_throw_namespace.is_empty()
1785        {
1786            return Vec::new();
1787        }
1788
1789        let lower_name = ascii_lowercase_word(name);
1790        let mut indices = Vec::new();
1791
1792        if let Some(idxs) = self.function_throw_exact.get(&lower_name) {
1793            indices.extend(idxs.iter().copied());
1794        }
1795
1796        for (prefix, idx) in &self.function_throw_prefix {
1797            if lower_name.as_bytes().starts_with(prefix.as_bytes()) && !indices.contains(idx) {
1798                indices.push(*idx);
1799            }
1800        }
1801
1802        for (ns, idx) in &self.function_throw_namespace {
1803            if lower_name.as_bytes().starts_with(ns.as_bytes()) && !indices.contains(idx) {
1804                indices.push(*idx);
1805            }
1806        }
1807
1808        indices
1809    }
1810
1811    fn get_method_throw_provider_indices(&self, class_name: &[u8], method_name: &[u8]) -> Vec<usize> {
1812        if self.method_throw_providers.is_empty()
1813            && self.method_throw_exact.is_empty()
1814            && self.method_throw_wildcard.is_empty()
1815        {
1816            return Vec::new();
1817        }
1818
1819        use mago_word::concat_word;
1820        let key = concat_word!(ascii_lowercase_word(class_name), b"::", ascii_lowercase_word(method_name));
1821        let mut indices = Vec::new();
1822
1823        if let Some(idxs) = self.method_throw_exact.get(&key) {
1824            indices.extend(idxs.iter().copied());
1825        }
1826
1827        for (targets, idx) in &self.method_throw_wildcard {
1828            if !indices.contains(idx) {
1829                for target in targets {
1830                    if target.matches(class_name, method_name) {
1831                        indices.push(*idx);
1832                        break;
1833                    }
1834                }
1835            }
1836        }
1837
1838        indices
1839    }
1840
1841    /// Get thrown exception class names for an expression from registered providers.
1842    #[must_use]
1843    pub fn get_expression_thrown_exceptions<'ctx>(
1844        &self,
1845        codebase: &'ctx CodebaseMetadata,
1846        source_file: &'ctx File,
1847        block_context: &BlockContext<'ctx>,
1848        artifacts: &AnalysisArtifacts,
1849        expression: &mago_syntax::cst::Expression<'_>,
1850    ) -> WordSet {
1851        let mut exceptions = WordSet::default();
1852
1853        for provider in &self.expression_throw_providers {
1854            let provider_context = ProviderContext::new(codebase, source_file, block_context, artifacts);
1855            exceptions.extend(provider.get_thrown_exceptions(&provider_context, expression));
1856        }
1857
1858        exceptions
1859    }
1860
1861    /// Get thrown exception class names for a function invocation from registered providers.
1862    #[must_use]
1863    pub fn get_function_thrown_exceptions<'ctx>(
1864        &self,
1865        codebase: &'ctx CodebaseMetadata,
1866        source_file: &'ctx File,
1867        block_context: &BlockContext<'ctx>,
1868        artifacts: &AnalysisArtifacts,
1869        function_name: &[u8],
1870        invocation: &Invocation<'ctx, '_, '_>,
1871    ) -> WordSet {
1872        let mut exceptions = WordSet::default();
1873        let indices = self.get_function_throw_provider_indices(function_name);
1874
1875        for idx in indices {
1876            let provider_context = ProviderContext::new(codebase, source_file, block_context, artifacts);
1877            let invocation_info = InvocationInfo::new(invocation);
1878            exceptions
1879                .extend(self.function_throw_providers[idx].get_thrown_exceptions(&provider_context, &invocation_info));
1880        }
1881
1882        exceptions
1883    }
1884
1885    /// Get thrown exception class names for a method invocation from registered providers.
1886    #[must_use]
1887    pub fn get_method_thrown_exceptions<'ctx>(
1888        &self,
1889        codebase: &'ctx CodebaseMetadata,
1890        source_file: &'ctx File,
1891        block_context: &BlockContext<'ctx>,
1892        artifacts: &AnalysisArtifacts,
1893        class_name: &[u8],
1894        method_name: &[u8],
1895        invocation: &Invocation<'ctx, '_, '_>,
1896    ) -> WordSet {
1897        let mut exceptions = WordSet::default();
1898        let indices = self.get_method_throw_provider_indices(class_name, method_name);
1899
1900        for idx in indices {
1901            let provider_context = ProviderContext::new(codebase, source_file, block_context, artifacts);
1902            let invocation_info = InvocationInfo::new(invocation);
1903            exceptions.extend(self.method_throw_providers[idx].get_thrown_exceptions(
1904                &provider_context,
1905                class_name,
1906                method_name,
1907                &invocation_info,
1908            ));
1909        }
1910
1911        exceptions
1912    }
1913
1914    /// Filter issues through all registered issue filter hooks.
1915    ///
1916    /// Returns a new `IssueCollection` with filtered issues.
1917    ///
1918    /// # Errors
1919    ///
1920    /// Returns an error when a native hook fails or an external issue batch cannot be filtered.
1921    pub fn filter_issues(
1922        &self,
1923        file: &File,
1924        issues: IssueCollection,
1925        codebase: &CodebaseMetadata,
1926        session: Option<&ExternalAnalysisSession>,
1927    ) -> PluginResult<IssueCollection> {
1928        let mut filtered = IssueCollection::default();
1929        if self.issue_filter_hooks.is_empty() {
1930            filtered = issues;
1931        } else {
1932            filtered.reserve(issues.len());
1933            for issue in issues {
1934                let mut keep = true;
1935                for hook in &self.issue_filter_hooks {
1936                    if hook.filter_issue(file, &issue)? == IssueFilterDecision::Remove {
1937                        keep = false;
1938                        break;
1939                    }
1940                }
1941
1942                if keep {
1943                    filtered.push(issue);
1944                }
1945            }
1946        }
1947
1948        if filtered.is_empty() || !self.has_external_capability(|capabilities| capabilities.issue_filters) {
1949            return Ok(filtered);
1950        }
1951
1952        let Some((analyzer, session)) = self.external_analyzer.as_deref().zip(session) else {
1953            return Ok(filtered);
1954        };
1955
1956        analyzer.with(|analyzer| analyzer.filter_issues(file, filtered, codebase, session)).map_err(PluginError::from)
1957    }
1958}
1959
1960#[cfg(test)]
1961mod tests {
1962    use super::*;
1963    use crate::external::ExternalAnalyzerError;
1964    use crate::plugin::provider::Provider;
1965    use crate::plugin::provider::ProviderMeta;
1966
1967    static TEST_META: ProviderMeta = ProviderMeta::new("test::provider", "Test Provider", "A test provider");
1968
1969    struct TestFunctionProvider;
1970
1971    impl Provider for TestFunctionProvider {
1972        fn meta() -> &'static ProviderMeta {
1973            &TEST_META
1974        }
1975    }
1976
1977    impl FunctionReturnTypeProvider for TestFunctionProvider {
1978        fn targets() -> FunctionTarget {
1979            FunctionTarget::Exact(b"test_func")
1980        }
1981
1982        fn get_return_type(
1983            &self,
1984            _context: &ProviderContext<'_, '_, '_>,
1985            _invocation: &InvocationInfo<'_, '_, '_>,
1986        ) -> Option<TUnion> {
1987            None
1988        }
1989    }
1990
1991    #[test]
1992    fn test_register_function_provider() {
1993        let mut registry = PluginRegistry::new();
1994        registry.register_function_provider(TestFunctionProvider);
1995
1996        assert_eq!(registry.function_provider_count(), 1);
1997        let indices = registry.get_function_provider_indices(b"test_func");
1998        assert_eq!(indices.len(), 1);
1999    }
2000
2001    #[test]
2002    fn test_function_exact_match() {
2003        let mut registry = PluginRegistry::new();
2004        registry.register_function_provider(TestFunctionProvider);
2005
2006        let indices = registry.get_function_provider_indices(b"test_func");
2007        assert_eq!(indices.len(), 1);
2008
2009        let indices = registry.get_function_provider_indices(b"TEST_FUNC");
2010        assert_eq!(indices.len(), 1);
2011
2012        let indices = registry.get_function_provider_indices(b"other_func");
2013        assert!(indices.is_empty());
2014    }
2015
2016    #[test]
2017    fn external_analyzer_errors_remain_structured() {
2018        let handle = ExternalAnalyzerHandle::pending(std::thread::spawn(|| {
2019            Err(ExternalAnalyzerError::Protocol("broken response".to_string()))
2020        }));
2021        let mut registry = PluginRegistry::new();
2022        registry.set_external_analyzer(Arc::new(handle));
2023
2024        let result = registry.prepare_external_analyzer();
2025        assert!(matches!(
2026            result,
2027            Err(PluginError::External(source))
2028                if matches!(source.as_ref(), ExternalAnalyzerError::Protocol(message) if message == "broken response")
2029        ));
2030    }
2031
2032    #[test]
2033    fn optional_external_provider_failures_use_native_fallback() {
2034        let result: Option<TUnion> = optional_external_hint(
2035            "test provider",
2036            Err(Arc::new(ExternalAnalyzerError::Protocol("broken response".to_string()))),
2037        );
2038
2039        assert!(result.is_none());
2040    }
2041}