1use 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 pub fn set_external_analyzer(&mut self, analyzer: Arc<ExternalAnalyzerHandle>) {
179 self.external_analyzer = Some(analyzer);
180 }
181
182 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 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 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 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 #[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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 #[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 #[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 #[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 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}