Skip to main content

mago_analyzer/external/
mod.rs

1//! Worker-backed analyzer plugins.
2
3use std::borrow::Cow;
4use std::collections::HashMap;
5use std::collections::HashSet;
6use std::sync::Arc;
7use std::sync::Mutex;
8use std::sync::OnceLock;
9use std::sync::atomic::AtomicU64;
10use std::sync::atomic::Ordering;
11use std::thread::JoinHandle;
12use std::time::Duration;
13use std::time::Instant;
14
15use crate::plugin::provider::assertion::InvocationAssertions;
16use mago_codex::metadata::CodebaseMetadata;
17use mago_codex::metadata::class_like::ClassLikeMetadata;
18use mago_codex::reference::SymbolReferences;
19use mago_codex::ttype::union::TUnion;
20use mago_database::file::File;
21use mago_database::file::FileId;
22use mago_database::file::FileType;
23use mago_extension::Frame;
24use mago_extension::WorkerError;
25use mago_extension::WorkerPool;
26use mago_extension::WorkerRequestHandler;
27use mago_names::ResolvedNames;
28use mago_php_version::PHPVersion;
29use mago_reporting::IssueCollection;
30use mago_span::Span;
31use mago_syntax::cst::NodeKind;
32use mago_syntax::cst::Program;
33use mago_word::Word;
34use mago_word::WordMap;
35use mago_word::WordSet;
36use mago_word::ascii_lowercase_word;
37use mago_word::concat_word;
38use mago_word::starts_with_ignore_case;
39
40use crate::artifacts::AnalysisArtifacts;
41use crate::invocation::EffectiveCallableSignature;
42use crate::invocation::Invocation;
43use crate::plugin::available_plugins;
44
45pub use error::ExternalAnalyzerError;
46pub use lifecycle::AFTER_FILE_ANALYSIS_BATCH_SIZE;
47pub use lifecycle::FileAnalysisSnapshot;
48use protocol::Registration;
49
50mod error;
51mod lifecycle;
52mod metadata;
53pub mod protocol;
54mod scan;
55
56pub use scan::CodebaseScanFile;
57pub use scan::CodebaseScanPlan;
58
59const SLOW_PROVIDER_THRESHOLD: Duration = Duration::from_millis(5);
60const SLOW_LIFECYCLE_THRESHOLD: Duration = Duration::from_millis(5);
61const MAXIMUM_PROVIDER_RESPONSE_CACHE_ENTRIES: usize = 0x0001_0000;
62const PROVIDER_CALLABLE_SIGNATURE: u8 = 1;
63const PROVIDER_OVERRIDES_DECLARED_SIGNATURE: u8 = 1 << 1;
64const PROVIDER_UNDECLARED_RETURN_TYPE_ONLY: u8 = 1 << 2;
65const PROVIDER_MEMOIZED: u8 = 1 << 3;
66static NEXT_ANALYSIS_GENERATION: AtomicU64 = AtomicU64::new(1);
67
68type ExternalAnalyzerResult<T> = Result<T, Arc<ExternalAnalyzerError>>;
69
70#[derive(Debug, Default)]
71pub struct BeforeAnalysisResult {
72    pub issues: IssueCollection,
73    pub references: SymbolReferences,
74}
75
76#[derive(Debug, Default)]
77pub struct AfterFileAnalysisResult {
78    pub issues: IssueCollection,
79    pub references_by_file: foldhash::HashMap<FileId, SymbolReferences>,
80}
81
82fn extend_references_by_file(
83    target: &mut foldhash::HashMap<FileId, SymbolReferences>,
84    source: impl IntoIterator<Item = (FileId, SymbolReferences)>,
85) {
86    for (file_id, references) in source {
87        target.entry(file_id).or_default().extend(references);
88    }
89}
90
91#[derive(Debug, Clone, Copy, PartialEq, Eq)]
92pub(crate) enum PropertyAccessKind {
93    Read,
94    Write,
95}
96
97#[derive(Debug, Clone, PartialEq, Eq)]
98pub(crate) struct EffectivePropertyType {
99    pub read_type: Option<TUnion>,
100    pub write_type: Option<TUnion>,
101}
102
103/// Immutable request context shared by every external hook in one analysis run.
104///
105/// A new session is created for every frozen codebase generation. Keeping the
106/// generation on the request, instead of on the worker pool, makes PHP-side
107/// metadata caches safe when a pool is reused by watch mode or by concurrent
108/// analysis services.
109#[derive(Debug)]
110pub struct ExternalAnalysisSession {
111    generation: u64,
112    sources: foldhash::HashMap<FileId, Arc<File>>,
113}
114
115impl ExternalAnalysisSession {
116    #[must_use]
117    pub fn from_files(files: impl IntoIterator<Item = Arc<File>>) -> Self {
118        let generation = NEXT_ANALYSIS_GENERATION.fetch_add(1, Ordering::Relaxed);
119        let sources = files.into_iter().map(|file| (file.id, file)).collect();
120
121        Self { generation, sources }
122    }
123
124    #[inline]
125    #[must_use]
126    pub(crate) const fn generation(&self) -> u64 {
127        self.generation
128    }
129
130    #[inline]
131    #[must_use]
132    pub(crate) fn source_name(&self, file_id: FileId) -> Option<&[u8]> {
133        self.sources.get(&file_id).map(|file| file.name.as_ref())
134    }
135
136    fn source(&self, name: &[u8]) -> Option<(FileId, u32)> {
137        self.sources.iter().find_map(|(file_id, file)| (file.name.as_ref() == name).then_some((*file_id, file.size)))
138    }
139
140    fn source_file(&self, file_id: FileId) -> Option<&File> {
141        self.sources.get(&file_id).map(AsRef::as_ref)
142    }
143}
144
145#[derive(Debug, Default)]
146struct ExternalAnalyzerTelemetry {
147    function_lookups: AtomicU64,
148    method_lookups: AtomicU64,
149    property_lookups: AtomicU64,
150    property_initialization_lookups: AtomicU64,
151    class_initializer_lookups: AtomicU64,
152    issue_filter_batches: AtomicU64,
153    issue_filter_candidates: AtomicU64,
154    issue_filter_removed: AtomicU64,
155    issue_filter_errors: AtomicU64,
156    issue_filter_request_bytes: AtomicU64,
157    issue_filter_response_bytes: AtomicU64,
158    signature_lookups: AtomicU64,
159    assertion_lookups: AtomicU64,
160    backend_checks: AtomicU64,
161    candidate_providers: AtomicU64,
162    matched_providers: AtomicU64,
163    unmatched_lookups: AtomicU64,
164    requests: AtomicU64,
165    ipc_requests: AtomicU64,
166    provider_cache_hits: AtomicU64,
167    signature_requests: AtomicU64,
168    assertion_requests: AtomicU64,
169    provided_types: AtomicU64,
170    initialized_properties: AtomicU64,
171    provided_class_initializers: AtomicU64,
172    provided_signatures: AtomicU64,
173    provided_assertions: AtomicU64,
174    declined_requests: AtomicU64,
175    errors: AtomicU64,
176    snapshotted_types: AtomicU64,
177    arguments: AtomicU64,
178    typed_arguments: AtomicU64,
179    request_bytes: AtomicU64,
180    response_bytes: AtomicU64,
181    nested_requests: AtomicU64,
182    nested_errors: AtomicU64,
183    nested_request_bytes: AtomicU64,
184    nested_response_bytes: AtomicU64,
185    comparison_batches: AtomicU64,
186    comparisons: AtomicU64,
187    metadata_queries: AtomicU64,
188    analysis_queries: AtomicU64,
189    symbol_reference_queries: AtomicU64,
190    before_analysis_requests: AtomicU64,
191    after_file_analysis_requests: AtomicU64,
192    after_file_analysis_files: AtomicU64,
193    after_analysis_requests: AtomicU64,
194    lifecycle_plugins: AtomicU64,
195    lifecycle_issues: AtomicU64,
196    lifecycle_errors: AtomicU64,
197    lifecycle_request_bytes: AtomicU64,
198    lifecycle_response_bytes: AtomicU64,
199    matching_ns: AtomicU64,
200    encode_ns: AtomicU64,
201    type_snapshot_ns: AtomicU64,
202    ipc_ns: AtomicU64,
203    comparison_ns: AtomicU64,
204    metadata_query_ns: AtomicU64,
205    analysis_query_ns: AtomicU64,
206    symbol_reference_query_ns: AtomicU64,
207    lifecycle_encode_ns: AtomicU64,
208    lifecycle_ipc_ns: AtomicU64,
209    lifecycle_decode_ns: AtomicU64,
210    lifecycle_ns: AtomicU64,
211    issue_filter_encode_ns: AtomicU64,
212    issue_filter_ipc_ns: AtomicU64,
213    issue_filter_decode_ns: AtomicU64,
214    issue_filter_ns: AtomicU64,
215    nested_ns: AtomicU64,
216    decode_ns: AtomicU64,
217    lookup_ns: AtomicU64,
218}
219
220impl ExternalAnalyzerTelemetry {
221    fn record_nested_request(
222        &self,
223        request_bytes: usize,
224        elapsed: Duration,
225        result: &Result<(protocol::NestedRequestKind, Vec<u8>), ExternalAnalyzerError>,
226    ) {
227        self.nested_requests.fetch_add(1, Ordering::Relaxed);
228        self.nested_request_bytes.fetch_add(request_bytes as u64, Ordering::Relaxed);
229        self.nested_ns.fetch_add(duration_nanos(elapsed), Ordering::Relaxed);
230
231        let Ok((kind, response)) = result else {
232            self.nested_errors.fetch_add(1, Ordering::Relaxed);
233            self.errors.fetch_add(1, Ordering::Relaxed);
234            return;
235        };
236
237        self.nested_response_bytes.fetch_add(response.len() as u64, Ordering::Relaxed);
238        match kind {
239            protocol::NestedRequestKind::TypeComparison => {
240                self.comparisons.fetch_add(1, Ordering::Relaxed);
241                self.comparison_ns.fetch_add(duration_nanos(elapsed), Ordering::Relaxed);
242            }
243            protocol::NestedRequestKind::TypeComparisonBatch(count) => {
244                self.comparison_batches.fetch_add(1, Ordering::Relaxed);
245                self.comparisons.fetch_add(*count as u64, Ordering::Relaxed);
246                self.comparison_ns.fetch_add(duration_nanos(elapsed), Ordering::Relaxed);
247            }
248            protocol::NestedRequestKind::CodebaseQuery => {
249                self.metadata_queries.fetch_add(1, Ordering::Relaxed);
250                self.metadata_query_ns.fetch_add(duration_nanos(elapsed), Ordering::Relaxed);
251            }
252            protocol::NestedRequestKind::AnalysisQuery => {
253                self.analysis_queries.fetch_add(1, Ordering::Relaxed);
254                self.analysis_query_ns.fetch_add(duration_nanos(elapsed), Ordering::Relaxed);
255            }
256            protocol::NestedRequestKind::SymbolReferenceQuery => {
257                self.symbol_reference_queries.fetch_add(1, Ordering::Relaxed);
258                self.symbol_reference_query_ns.fetch_add(duration_nanos(elapsed), Ordering::Relaxed);
259            }
260        }
261    }
262}
263
264#[derive(Clone, Copy)]
265enum LifecyclePhase {
266    Before,
267    AfterFile,
268    AfterFileBatch,
269    After,
270}
271
272impl LifecyclePhase {
273    const fn request_kind(self) -> u16 {
274        match self {
275            Self::Before => lifecycle::BEFORE_ANALYSIS_REQUEST,
276            Self::AfterFile => lifecycle::AFTER_FILE_ANALYSIS_REQUEST,
277            Self::AfterFileBatch => lifecycle::AFTER_FILE_ANALYSIS_BATCH_REQUEST,
278            Self::After => lifecycle::AFTER_ANALYSIS_REQUEST,
279        }
280    }
281
282    const fn name(self) -> &'static str {
283        match self {
284            Self::Before => "before-analysis",
285            Self::AfterFile | Self::AfterFileBatch => "after-file-analysis",
286            Self::After => "after-analysis",
287        }
288    }
289}
290
291struct LookupTrace<'telemetry> {
292    telemetry: &'telemetry ExternalAnalyzerTelemetry,
293    started_at: Instant,
294}
295
296impl Drop for LookupTrace<'_> {
297    fn drop(&mut self) {
298        self.telemetry.lookup_ns.fetch_add(duration_nanos(self.started_at.elapsed()), Ordering::Relaxed);
299    }
300}
301
302#[derive(Debug, Clone, PartialEq, Eq)]
303pub struct ExternalExtension {
304    pub identifier: String,
305    pub name: String,
306    pub version: String,
307    pub plugins: Vec<ExternalPlugin>,
308}
309
310#[derive(Debug, Clone, PartialEq, Eq)]
311pub struct ExternalPlugin {
312    pub index: u16,
313    pub extension: String,
314    pub identifier: String,
315    pub name: String,
316    pub description: String,
317    pub aliases: Vec<String>,
318    pub default_enabled: bool,
319    pub initialization: bool,
320    pub before_analysis: bool,
321    pub after_file_analysis: bool,
322    pub after_file_expression_types: bool,
323    pub node_analysis: bool,
324    pub after_analysis: bool,
325}
326
327#[derive(Debug, Clone, PartialEq, Eq)]
328struct ExternalStub {
329    name: Vec<u8>,
330    contents: Vec<u8>,
331}
332
333impl ExternalStub {
334    fn new(extension: &str, plugin: &str, filename: &[u8], contents: Vec<u8>) -> Self {
335        let mut name = Vec::with_capacity(18 + extension.len() + plugin.len() + filename.len());
336        name.extend_from_slice(b"@mago-extension/");
337        append_path_component(&mut name, extension.as_bytes());
338        name.push(b'/');
339        append_path_component(&mut name, plugin.as_bytes());
340        name.push(b'/');
341        name.extend_from_slice(filename);
342        Self { name, contents }
343    }
344
345    fn to_file(&self) -> File {
346        File::new(Cow::Owned(self.name.clone()), FileType::External, None, Cow::Owned(self.contents.clone()))
347    }
348}
349
350impl ExternalPlugin {
351    fn matches(&self, name: &str) -> bool {
352        self.identifier.eq_ignore_ascii_case(name) || self.aliases.iter().any(|alias| alias.eq_ignore_ascii_case(name))
353    }
354}
355
356impl Registration {
357    fn file_analysis_plugins(&self) -> Vec<u16> {
358        let mut plugins =
359            Vec::with_capacity(self.after_file_analysis_plugins.len().saturating_add(self.node_analysis_plugins.len()));
360        plugins.extend_from_slice(&self.after_file_analysis_plugins);
361        plugins.extend_from_slice(&self.node_analysis_plugins);
362        plugins.sort_unstable();
363        plugins.dedup();
364        plugins
365    }
366
367    fn file_analysis_requires_expression_types(&self, plugins: &[u16]) -> bool {
368        self.plugins.iter().any(|plugin| plugins.contains(&plugin.index) && plugin.after_file_expression_types)
369            || self.node_analysis_hooks.iter().any(|hook| {
370                plugins.contains(&hook.plugin) && hook.requirements & NODE_REQUIREMENT_EXPRESSION_TYPES != 0
371            })
372            || self.method_call_analysis_hooks.iter().any(|hook| {
373                plugins.contains(&hook.plugin) && hook.requirements & NODE_REQUIREMENT_EXPRESSION_TYPES != 0
374            })
375            || self.class_like_analysis_hooks.iter().any(|hook| {
376                plugins.contains(&hook.plugin) && hook.requirements & NODE_REQUIREMENT_EXPRESSION_TYPES != 0
377            })
378    }
379
380    pub(crate) fn node_analysis_requirements(&self) -> Option<NodeAnalysisRequirements> {
381        let mut targets = [false; u8::MAX as usize + 1];
382        let mut requirements = [0; u8::MAX as usize + 1];
383        let mut any = false;
384        for hook in &self.node_analysis_hooks {
385            for target in &hook.targets {
386                targets[*target as usize] = true;
387                requirements[*target as usize] |= hook.requirements;
388                any = true;
389            }
390        }
391
392        let method_call_hooks: Arc<[MethodCallAnalysisHookRegistration]> =
393            Arc::from(self.method_call_analysis_hooks.clone().into_boxed_slice());
394        any |= !method_call_hooks.is_empty();
395
396        let class_like_hooks: Arc<[ClassLikeAnalysisHookRegistration]> =
397            Arc::from(self.class_like_analysis_hooks.clone().into_boxed_slice());
398        any |= !class_like_hooks.is_empty();
399
400        any.then_some(NodeAnalysisRequirements { targets, requirements, method_call_hooks, class_like_hooks })
401    }
402}
403
404#[derive(Debug, Clone, PartialEq, Eq)]
405enum FunctionTarget {
406    Exact(Vec<u8>),
407    Prefix(Vec<u8>),
408}
409
410impl FunctionTarget {
411    fn matches(&self, name: &[u8]) -> bool {
412        match self {
413            Self::Exact(target) => name.eq_ignore_ascii_case(target),
414            Self::Prefix(prefix) => starts_with_ignore_case(name, prefix),
415        }
416    }
417}
418
419#[derive(Debug, Clone, PartialEq, Eq, Hash)]
420struct MethodTarget {
421    class: Vec<u8>,
422    method: Vec<u8>,
423}
424
425impl MethodTarget {
426    fn matches(&self, codebase: &CodebaseMetadata, class: &[u8], method: &[u8]) -> bool {
427        class_pattern_matches(codebase, &self.class, class) && pattern_matches(&self.method, method)
428    }
429}
430
431#[derive(Clone, Copy)]
432enum CallableTarget<'target> {
433    Function(&'target [u8]),
434    Method { class: &'target [u8], method: &'target [u8] },
435}
436
437impl<'target> CallableTarget<'target> {
438    fn affinity(self) -> &'target [u8] {
439        match self {
440            Self::Function(function) => function,
441            Self::Method { class, .. } => class,
442        }
443    }
444}
445
446impl std::fmt::Display for CallableTarget<'_> {
447    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
448        match self {
449            Self::Function(function) => mago_bytes::BytesDisplay(function).fmt(formatter),
450            Self::Method { class, method } => {
451                write!(formatter, "{}::{}", mago_bytes::BytesDisplay(class), mago_bytes::BytesDisplay(method))
452            }
453        }
454    }
455}
456
457#[derive(Debug, Clone, PartialEq, Eq)]
458struct ProviderRegistration<T> {
459    plugin: u16,
460    index: u16,
461    capabilities: u8,
462    targets: Vec<T>,
463}
464
465type FunctionProvider = ProviderRegistration<FunctionTarget>;
466type MethodProvider = ProviderRegistration<MethodTarget>;
467type PropertyProvider = ProviderRegistration<PropertyTarget>;
468type ClassInitializerProvider = ProviderRegistration<Vec<u8>>;
469type IssueFilterHookRegistration = ProviderRegistration<String>;
470type CodebaseScanHookRegistration = ProviderRegistration<String>;
471
472#[derive(Debug, Clone, PartialEq, Eq)]
473struct PropertyTarget {
474    class: Vec<u8>,
475    property: Vec<u8>,
476}
477
478impl PropertyTarget {
479    fn matches(&self, codebase: &CodebaseMetadata, class: &[u8], property: &[u8]) -> bool {
480        class_pattern_matches(codebase, &self.class, class) && property_pattern_matches(&self.property, property)
481    }
482}
483
484#[derive(Debug, Clone, PartialEq, Eq)]
485struct EntryPoint {
486    plugin: u16,
487    source: Word,
488    target: MethodTarget,
489}
490
491#[derive(Debug, Clone, PartialEq, Eq)]
492struct AttributedEntryPoint {
493    plugin: u16,
494    source: Word,
495    class: Vec<u8>,
496    attribute: Vec<u8>,
497}
498
499#[derive(Debug, Clone, PartialEq, Eq)]
500struct AnalysisHookRegistration<T> {
501    plugin: u16,
502    index: u16,
503    requirements: u8,
504    targets: Vec<T>,
505    route: u32,
506}
507
508type NodeAnalysisHookRegistration = AnalysisHookRegistration<NodeKind>;
509type MethodCallAnalysisHookRegistration = AnalysisHookRegistration<MethodTarget>;
510type ClassLikeAnalysisHookRegistration = AnalysisHookRegistration<Word>;
511
512pub(super) const NODE_REQUIREMENT_EXPRESSION_TYPES: u8 = 1;
513pub(super) const NODE_REQUIREMENT_TARGET_EXPRESSION_TYPES: u8 = 1 << 1;
514pub(super) const NODE_REQUIREMENT_RECEIVER_TYPE: u8 = 1 << 2;
515pub(super) const NODE_REQUIREMENT_ARGUMENT_TYPES: u8 = 1 << 3;
516pub(super) const NODE_REQUIREMENT_TARGET_SUBTREE: u8 = 1 << 4;
517pub(super) const NODE_REQUIREMENT_SOURCE_TEXT: u8 = 1 << 5;
518pub(super) const NODE_REQUIREMENTS_ALL: u8 = NODE_REQUIREMENT_EXPRESSION_TYPES
519    | NODE_REQUIREMENT_TARGET_EXPRESSION_TYPES
520    | NODE_REQUIREMENT_RECEIVER_TYPE
521    | NODE_REQUIREMENT_ARGUMENT_TYPES
522    | NODE_REQUIREMENT_TARGET_SUBTREE
523    | NODE_REQUIREMENT_SOURCE_TEXT;
524
525/// Syntax targets and embedded data requested by external node-analysis hooks.
526#[derive(Debug, Clone)]
527pub struct NodeAnalysisRequirements {
528    targets: [bool; u8::MAX as usize + 1],
529    requirements: [u8; u8::MAX as usize + 1],
530    method_call_hooks: Arc<[MethodCallAnalysisHookRegistration]>,
531    class_like_hooks: Arc<[ClassLikeAnalysisHookRegistration]>,
532}
533
534impl NodeAnalysisRequirements {
535    #[inline]
536    #[must_use]
537    pub(crate) const fn targets(&self) -> &[bool; u8::MAX as usize + 1] {
538        &self.targets
539    }
540
541    #[inline]
542    #[must_use]
543    pub(crate) const fn requirements(&self, kind: NodeKind) -> u8 {
544        self.requirements[kind as usize]
545    }
546
547    #[inline]
548    #[must_use]
549    pub(crate) fn includes_source_text(&self) -> bool {
550        self.requirements.iter().any(|requirements| requirements & NODE_REQUIREMENT_SOURCE_TEXT != 0)
551            || self.method_call_hooks.iter().any(|hook| hook.requirements & NODE_REQUIREMENT_SOURCE_TEXT != 0)
552            || self.class_like_hooks.iter().any(|hook| hook.requirements & NODE_REQUIREMENT_SOURCE_TEXT != 0)
553    }
554}
555
556type ProviderExactIndex = WordMap<Vec<u16>>;
557type ProviderWildcardIndex<T> = Vec<(u16, Vec<T>)>;
558
559enum ProviderIndices {
560    One(u16),
561    Multiple(Vec<u16>),
562}
563
564impl ProviderIndices {
565    #[inline]
566    fn from_iter(mut indices: impl Iterator<Item = u16>) -> Option<Self> {
567        let first = indices.next()?;
568        let Some(second) = indices.next() else {
569            return Some(Self::One(first));
570        };
571
572        let mut matched = Vec::with_capacity(4);
573        matched.extend([first, second]);
574        matched.extend(indices);
575        Some(Self::Multiple(matched))
576    }
577
578    #[inline]
579    fn as_slice(&self) -> &[u16] {
580        match self {
581            Self::One(index) => std::slice::from_ref(index),
582            Self::Multiple(indices) => indices,
583        }
584    }
585
586    fn insert_sorted(&mut self, index: u16) {
587        match self {
588            Self::One(existing) if *existing == index => {}
589            Self::One(existing) => {
590                let (first, second) = if *existing < index { (*existing, index) } else { (index, *existing) };
591                *self = Self::Multiple(vec![first, second]);
592            }
593            Self::Multiple(indices) => {
594                if let Err(position) = indices.binary_search(&index) {
595                    indices.insert(position, index);
596                }
597            }
598        }
599    }
600
601    fn from_exact_and_wildcards(exact: &[u16], wildcards: impl IntoIterator<Item = u16>) -> Option<ProviderIndices> {
602        Self::add_wildcards(Self::from_iter(exact.iter().copied()), wildcards)
603    }
604
605    fn add_wildcards(mut indices: Option<Self>, wildcards: impl IntoIterator<Item = u16>) -> Option<ProviderIndices> {
606        for index in wildcards {
607            match &mut indices {
608                Some(indices) => indices.insert_sorted(index),
609                None => indices = Some(Self::One(index)),
610            }
611        }
612
613        indices
614    }
615}
616
617/// Request transport used by worker-backed analyzer providers.
618pub trait AnalyzerTransport: std::fmt::Debug + Send + Sync {
619    /// Starts preparing additional transport capacity for a sustained parallel workload.
620    fn prepare_capacity(self: &Arc<Self>) {}
621
622    /// Sends initialization data to every worker process.
623    ///
624    /// # Errors
625    ///
626    /// Returns an error if a worker cannot process the request.
627    fn broadcast(&self, payload: &[u8]) -> Result<Vec<Vec<u8>>, WorkerError>;
628
629    /// Returns the largest payload accepted by this transport.
630    fn maximum_payload_size(&self) -> usize {
631        usize::MAX
632    }
633
634    /// Sends an ordered, replayable state-replacement sequence to every worker.
635    ///
636    /// # Errors
637    ///
638    /// Returns an error if a worker cannot process the complete sequence.
639    fn broadcast_sequence(&self, _group: u64, payloads: &[Vec<u8>]) -> Result<Vec<Vec<Vec<u8>>>, WorkerError> {
640        payloads.iter().map(|payload| self.broadcast(payload)).collect()
641    }
642
643    /// Sends one provider request to an available worker.
644    ///
645    /// # Errors
646    ///
647    /// Returns an error if no worker can process the request.
648    fn request(&self, payload: Vec<u8>) -> Result<Vec<u8>, WorkerError>;
649
650    /// Sends one provider request while servicing nested analyzer queries.
651    ///
652    /// # Errors
653    ///
654    /// Returns an error if the worker or nested query handler fails.
655    fn request_with_handler<H>(&self, payload: Vec<u8>, handler: &mut H) -> Result<Vec<u8>, WorkerError>
656    where
657        H: WorkerRequestHandler;
658
659    /// Sends a provider request with a stable process-local cache key.
660    ///
661    /// The default implementation preserves compatibility with transports
662    /// that do not expose worker affinity.
663    ///
664    /// # Errors
665    ///
666    /// Returns an error if the worker or nested query handler fails.
667    fn request_with_handler_affinity<H>(
668        &self,
669        payload: Vec<u8>,
670        _affinity: &[u8],
671        handler: &mut H,
672    ) -> Result<Vec<u8>, WorkerError>
673    where
674        H: WorkerRequestHandler,
675    {
676        self.request_with_handler(payload, handler)
677    }
678}
679
680impl AnalyzerTransport for WorkerPool {
681    fn prepare_capacity(self: &Arc<Self>) {
682        Self::prepare_capacity(self);
683    }
684
685    fn broadcast(&self, payload: &[u8]) -> Result<Vec<Vec<u8>>, WorkerError> {
686        Self::broadcast(self, payload)
687    }
688
689    fn maximum_payload_size(&self) -> usize {
690        Self::maximum_payload_size(self)
691    }
692
693    fn broadcast_sequence(&self, group: u64, payloads: &[Vec<u8>]) -> Result<Vec<Vec<Vec<u8>>>, WorkerError> {
694        Self::broadcast_sequence(self, group, payloads)
695    }
696
697    fn request(&self, payload: Vec<u8>) -> Result<Vec<u8>, WorkerError> {
698        Self::request(self, payload)
699    }
700
701    fn request_with_handler<H>(&self, payload: Vec<u8>, handler: &mut H) -> Result<Vec<u8>, WorkerError>
702    where
703        H: WorkerRequestHandler,
704    {
705        Self::request_with_handler(self, payload, handler)
706    }
707
708    fn request_with_handler_affinity<H>(
709        &self,
710        payload: Vec<u8>,
711        affinity: &[u8],
712        handler: &mut H,
713    ) -> Result<Vec<u8>, WorkerError>
714    where
715        H: WorkerRequestHandler,
716    {
717        Self::request_with_handler_affinity(self, payload, affinity, handler)
718    }
719}
720
721#[derive(Debug)]
722struct Backend<T> {
723    transport: Arc<T>,
724    registration: Registration,
725    provider_response_cache: Mutex<ProviderResponseCache>,
726    function_capabilities: Box<[u8]>,
727    method_capabilities: Box<[u8]>,
728    function_assertion_capabilities: Box<[u8]>,
729    method_assertion_capabilities: Box<[u8]>,
730    function_exact: WordMap<Vec<u16>>,
731    function_wildcard: Vec<(u16, Vec<FunctionTarget>)>,
732    function_signature_exact: WordMap<Vec<u16>>,
733    function_signature_wildcard: Vec<(u16, Vec<FunctionTarget>)>,
734    function_assertion_exact: WordMap<Vec<u16>>,
735    function_assertion_wildcard: Vec<(u16, Vec<FunctionTarget>)>,
736    method_exact: WordMap<Vec<u16>>,
737    method_wildcard: Vec<(u16, Vec<MethodTarget>)>,
738    method_signature_exact: WordMap<Vec<u16>>,
739    method_signature_wildcard: Vec<(u16, Vec<MethodTarget>)>,
740    method_assertion_exact: WordMap<Vec<u16>>,
741    method_assertion_wildcard: Vec<(u16, Vec<MethodTarget>)>,
742    property_exact: ProviderExactIndex,
743    property_wildcard: ProviderWildcardIndex<PropertyTarget>,
744    property_initialization_exact: ProviderExactIndex,
745    property_initialization_wildcard: ProviderWildcardIndex<PropertyTarget>,
746    class_initializer_exact: ProviderExactIndex,
747    class_initializer_wildcard: ProviderWildcardIndex<Vec<u8>>,
748    issue_filter_indices: Box<[u16]>,
749    issue_filter_codes: foldhash::HashSet<String>,
750}
751
752#[derive(Debug, Default)]
753struct ProviderResponseCache {
754    generation: u64,
755    responses: foldhash::HashMap<Vec<u8>, Vec<u8>>,
756}
757
758impl ProviderResponseCache {
759    fn get(&mut self, generation: u64, request: &[u8]) -> Option<Vec<u8>> {
760        self.select_generation(generation);
761        self.responses.get(request).cloned()
762    }
763
764    fn insert(&mut self, generation: u64, request: Vec<u8>, response: Vec<u8>) {
765        self.select_generation(generation);
766        if self.responses.len() < MAXIMUM_PROVIDER_RESPONSE_CACHE_ENTRIES {
767            self.responses.insert(request, response);
768        }
769    }
770
771    fn select_generation(&mut self, generation: u64) {
772        if self.generation != generation {
773            self.generation = generation;
774            self.responses.clear();
775        }
776    }
777}
778
779fn provider_capabilities<T>(providers: &[ProviderRegistration<T>]) -> Box<[u8]> {
780    let Some(maximum) = providers.iter().map(|provider| provider.index).max() else {
781        return Box::new([]);
782    };
783
784    let mut capabilities = vec![0; usize::from(maximum) + 1];
785    for provider in providers {
786        capabilities[usize::from(provider.index)] = provider.capabilities;
787    }
788
789    capabilities.into_boxed_slice()
790}
791
792impl<T> Backend<T> {
793    fn new(transport: Arc<T>, registration: Registration) -> Self {
794        let (function_exact, function_wildcard) = index_function_providers(&registration.function_providers);
795        let (function_signature_exact, function_signature_wildcard) = index_function_providers(
796            registration
797                .function_providers
798                .iter()
799                .filter(|provider| provider.capabilities & PROVIDER_CALLABLE_SIGNATURE != 0),
800        );
801        let (function_assertion_exact, function_assertion_wildcard) =
802            index_function_providers(&registration.function_assertion_providers);
803        let (method_exact, method_wildcard) = index_method_providers(&registration.method_providers);
804        let (method_signature_exact, method_signature_wildcard) = index_method_providers(
805            registration
806                .method_providers
807                .iter()
808                .filter(|provider| provider.capabilities & PROVIDER_CALLABLE_SIGNATURE != 0),
809        );
810        let (method_assertion_exact, method_assertion_wildcard) =
811            index_method_providers(&registration.method_assertion_providers);
812
813        let (property_exact, property_wildcard) = index_property_providers(&registration.property_providers);
814        let (property_initialization_exact, property_initialization_wildcard) =
815            index_property_providers(&registration.property_initialization_providers);
816        let (class_initializer_exact, class_initializer_wildcard) =
817            index_class_providers(&registration.class_initializer_providers);
818        let issue_filter_indices = registration.issue_filter_hooks.iter().map(|hook| hook.index).collect::<Box<[_]>>();
819        let issue_filter_codes =
820            registration.issue_filter_hooks.iter().flat_map(|hook| hook.targets.iter().cloned()).collect();
821        let function_capabilities = provider_capabilities(&registration.function_providers);
822        let method_capabilities = provider_capabilities(&registration.method_providers);
823        let function_assertion_capabilities = provider_capabilities(&registration.function_assertion_providers);
824        let method_assertion_capabilities = provider_capabilities(&registration.method_assertion_providers);
825
826        Self {
827            transport,
828            registration,
829            provider_response_cache: Mutex::new(ProviderResponseCache::default()),
830            function_capabilities,
831            method_capabilities,
832            function_assertion_capabilities,
833            method_assertion_capabilities,
834            function_exact,
835            function_wildcard,
836            function_signature_exact,
837            function_signature_wildcard,
838            function_assertion_exact,
839            function_assertion_wildcard,
840            method_exact,
841            method_wildcard,
842            method_signature_exact,
843            method_signature_wildcard,
844            method_assertion_exact,
845            method_assertion_wildcard,
846            property_exact,
847            property_wildcard,
848            property_initialization_exact,
849            property_initialization_wildcard,
850            class_initializer_exact,
851            class_initializer_wildcard,
852            issue_filter_indices,
853            issue_filter_codes,
854        }
855    }
856
857    fn matching_function_providers(&self, function: &[u8], declared: bool) -> (Option<ProviderIndices>, usize) {
858        let (indices, candidates) =
859            matching_function_provider_indices(&self.function_exact, &self.function_wildcard, function);
860        let indices = filter_declared_provider_indices(
861            indices,
862            &self.function_capabilities,
863            declared,
864            PROVIDER_UNDECLARED_RETURN_TYPE_ONLY,
865            false,
866        );
867        (indices, candidates)
868    }
869
870    fn memoizes_function_providers(&self, indices: &[u16]) -> bool {
871        providers_have_capability(&self.function_capabilities, indices, PROVIDER_MEMOIZED)
872    }
873
874    fn memoizes_method_providers(&self, indices: &[u16]) -> bool {
875        providers_have_capability(&self.method_capabilities, indices, PROVIDER_MEMOIZED)
876    }
877
878    fn matching_method_providers(
879        &self,
880        codebase: &CodebaseMetadata,
881        class: &[u8],
882        method: &[u8],
883        declared: bool,
884    ) -> (Option<ProviderIndices>, usize) {
885        let (indices, candidates) =
886            matching_method_provider_indices(&self.method_exact, &self.method_wildcard, codebase, class, method);
887        let indices = filter_declared_provider_indices(
888            indices,
889            &self.method_capabilities,
890            declared,
891            PROVIDER_UNDECLARED_RETURN_TYPE_ONLY,
892            false,
893        );
894        (indices, candidates)
895    }
896
897    fn matching_function_signature_providers(
898        &self,
899        function: &[u8],
900        declared: bool,
901    ) -> (Option<ProviderIndices>, usize) {
902        let (indices, candidates) = matching_function_provider_indices(
903            &self.function_signature_exact,
904            &self.function_signature_wildcard,
905            function,
906        );
907        let indices = filter_declared_provider_indices(
908            indices,
909            &self.function_capabilities,
910            declared,
911            PROVIDER_OVERRIDES_DECLARED_SIGNATURE,
912            true,
913        );
914        (indices, candidates)
915    }
916
917    fn matching_method_signature_providers(
918        &self,
919        codebase: &CodebaseMetadata,
920        class: &[u8],
921        method: &[u8],
922        declared: bool,
923    ) -> (Option<ProviderIndices>, usize) {
924        let (indices, candidates) = matching_method_provider_indices(
925            &self.method_signature_exact,
926            &self.method_signature_wildcard,
927            codebase,
928            class,
929            method,
930        );
931        let indices = filter_declared_provider_indices(
932            indices,
933            &self.method_capabilities,
934            declared,
935            PROVIDER_OVERRIDES_DECLARED_SIGNATURE,
936            true,
937        );
938        (indices, candidates)
939    }
940
941    fn matching_function_assertion_providers(&self, function: &[u8]) -> (Option<ProviderIndices>, usize) {
942        matching_function_provider_indices(&self.function_assertion_exact, &self.function_assertion_wildcard, function)
943    }
944
945    fn matching_method_assertion_providers(
946        &self,
947        codebase: &CodebaseMetadata,
948        class: &[u8],
949        method: &[u8],
950    ) -> (Option<ProviderIndices>, usize) {
951        matching_method_provider_indices(
952            &self.method_assertion_exact,
953            &self.method_assertion_wildcard,
954            codebase,
955            class,
956            method,
957        )
958    }
959
960    fn memoizes_function_assertion_providers(&self, indices: &[u16]) -> bool {
961        providers_have_capability(&self.function_assertion_capabilities, indices, PROVIDER_MEMOIZED)
962    }
963
964    fn memoizes_method_assertion_providers(&self, indices: &[u16]) -> bool {
965        providers_have_capability(&self.method_assertion_capabilities, indices, PROVIDER_MEMOIZED)
966    }
967
968    fn matching_property_providers(
969        &self,
970        codebase: &CodebaseMetadata,
971        class: &[u8],
972        property: &[u8],
973    ) -> (Option<ProviderIndices>, usize) {
974        matching_property_provider_indices(&self.property_exact, &self.property_wildcard, codebase, class, property)
975    }
976
977    fn matching_property_initialization_providers(
978        &self,
979        codebase: &CodebaseMetadata,
980        class: &[u8],
981        property: &[u8],
982    ) -> (Option<ProviderIndices>, usize) {
983        matching_property_provider_indices(
984            &self.property_initialization_exact,
985            &self.property_initialization_wildcard,
986            codebase,
987            class,
988            property,
989        )
990    }
991
992    fn matching_class_initializer_providers(
993        &self,
994        codebase: &CodebaseMetadata,
995        class: &[u8],
996    ) -> (Option<ProviderIndices>, usize) {
997        let (exact, exact_candidates) = matching_exact_class_providers(&self.class_initializer_exact, codebase, class);
998        let candidates = exact_candidates + self.class_initializer_wildcard.len();
999        let wildcards = self
1000            .class_initializer_wildcard
1001            .iter()
1002            .filter(|(_, targets)| targets.iter().any(|target| class_pattern_matches(codebase, target, class)))
1003            .map(|(index, _)| *index);
1004
1005        (ProviderIndices::add_wildcards(exact, wildcards), candidates)
1006    }
1007
1008    fn contribute_entry_point_references(
1009        &self,
1010        codebase: &CodebaseMetadata,
1011        references: &mut SymbolReferences,
1012    ) -> usize {
1013        if self.registration.entry_points.is_empty() && self.registration.attributed_entry_points.is_empty() {
1014            return 0;
1015        }
1016
1017        let mut matches = 0;
1018        for class in codebase.class_likes.values() {
1019            for entry_point in &self.registration.entry_points {
1020                if !class_pattern_matches(codebase, &entry_point.target.class, class.name.as_bytes()) {
1021                    continue;
1022                }
1023
1024                let mut matched_class = false;
1025                for (method, declaring_method) in &class.declaring_method_ids {
1026                    if !pattern_matches(&entry_point.target.method, method.as_bytes()) {
1027                        continue;
1028                    }
1029
1030                    references.add_symbol_reference_to_class_member(
1031                        entry_point.source,
1032                        (declaring_method.get_class_name(), declaring_method.get_method_name()),
1033                        false,
1034                    );
1035                    matched_class = true;
1036                    matches += 1;
1037                }
1038                if matched_class {
1039                    references.add_symbol_reference_to_symbol(entry_point.source, class.name, false);
1040                }
1041            }
1042
1043            for entry_point in &self.registration.attributed_entry_points {
1044                if !class_pattern_matches(codebase, &entry_point.class, class.name.as_bytes()) {
1045                    continue;
1046                }
1047
1048                let mut matched_class = false;
1049                for declaring_method in class.declaring_method_ids.values() {
1050                    let Some(method) = codebase.get_method_by_id(declaring_method) else {
1051                        continue;
1052                    };
1053                    if !method
1054                        .attributes
1055                        .iter()
1056                        .any(|attribute| attribute.name.as_bytes().eq_ignore_ascii_case(&entry_point.attribute))
1057                    {
1058                        continue;
1059                    }
1060
1061                    references.add_symbol_reference_to_class_member(
1062                        entry_point.source,
1063                        (declaring_method.get_class_name(), declaring_method.get_method_name()),
1064                        false,
1065                    );
1066                    matched_class = true;
1067                    matches += 1;
1068                }
1069                if matched_class {
1070                    references.add_symbol_reference_to_symbol(entry_point.source, class.name, false);
1071                }
1072            }
1073        }
1074
1075        matches
1076    }
1077}
1078
1079fn index_class_providers(
1080    providers: &[ClassInitializerProvider],
1081) -> (ProviderExactIndex, ProviderWildcardIndex<Vec<u8>>) {
1082    let mut exact = WordMap::default();
1083    let mut wildcard = Vec::new();
1084    for provider in providers {
1085        let mut wildcard_targets = Vec::new();
1086        for target in &provider.targets {
1087            if target.contains(&b'*') {
1088                wildcard_targets.push(target.clone());
1089            } else {
1090                let indices = exact.entry(ascii_lowercase_word(target)).or_insert_with(Vec::new);
1091                if indices.last() != Some(&provider.index) {
1092                    indices.push(provider.index);
1093                }
1094            }
1095        }
1096
1097        if !wildcard_targets.is_empty() {
1098            wildcard.push((provider.index, wildcard_targets));
1099        }
1100    }
1101
1102    (exact, wildcard)
1103}
1104
1105fn index_function_providers<'provider>(
1106    providers: impl IntoIterator<Item = &'provider ProviderRegistration<FunctionTarget>>,
1107) -> (ProviderExactIndex, ProviderWildcardIndex<FunctionTarget>) {
1108    let mut exact = WordMap::default();
1109    let mut wildcard = Vec::new();
1110    for provider in providers {
1111        let mut wildcard_targets = Vec::new();
1112        for target in &provider.targets {
1113            match target {
1114                FunctionTarget::Exact(name) => push_provider_index(
1115                    exact.entry(ascii_lowercase_word(name)).or_insert_with(Vec::new),
1116                    provider.index,
1117                ),
1118                FunctionTarget::Prefix(_) => wildcard_targets.push(target.clone()),
1119            }
1120        }
1121        if !wildcard_targets.is_empty() {
1122            wildcard.push((provider.index, wildcard_targets));
1123        }
1124    }
1125
1126    (exact, wildcard)
1127}
1128
1129fn index_method_providers<'provider>(
1130    providers: impl IntoIterator<Item = &'provider ProviderRegistration<MethodTarget>>,
1131) -> (ProviderExactIndex, ProviderWildcardIndex<MethodTarget>) {
1132    let mut exact = WordMap::default();
1133    let mut wildcard = Vec::new();
1134    for provider in providers {
1135        let mut wildcard_targets = Vec::new();
1136        for target in &provider.targets {
1137            if target.class.contains(&b'*') || target.method.contains(&b'*') {
1138                wildcard_targets.push(target.clone());
1139            } else {
1140                let class = ascii_lowercase_word(&target.class);
1141                let method = ascii_lowercase_word(&target.method);
1142                push_provider_index(
1143                    exact.entry(concat_word!(class, b"::", method)).or_insert_with(Vec::new),
1144                    provider.index,
1145                );
1146            }
1147        }
1148        if !wildcard_targets.is_empty() {
1149            wildcard.push((provider.index, wildcard_targets));
1150        }
1151    }
1152
1153    (exact, wildcard)
1154}
1155
1156fn push_provider_index(indices: &mut Vec<u16>, index: u16) {
1157    if indices.last() != Some(&index) {
1158        indices.push(index);
1159    }
1160}
1161
1162fn matching_function_provider_indices(
1163    exact_index: &ProviderExactIndex,
1164    wildcard_index: &ProviderWildcardIndex<FunctionTarget>,
1165    function: &[u8],
1166) -> (Option<ProviderIndices>, usize) {
1167    let function = ascii_lowercase_word(function);
1168    let exact = exact_index.get(&function).map_or(&[][..], Vec::as_slice);
1169    let candidates = exact.len() + wildcard_index.len();
1170    let wildcards = wildcard_index
1171        .iter()
1172        .filter(|(_, targets)| targets.iter().any(|target| target.matches(function.as_bytes())))
1173        .map(|(index, _)| *index);
1174
1175    (ProviderIndices::from_exact_and_wildcards(exact, wildcards), candidates)
1176}
1177
1178fn matching_method_provider_indices(
1179    exact_index: &ProviderExactIndex,
1180    wildcard_index: &ProviderWildcardIndex<MethodTarget>,
1181    codebase: &CodebaseMetadata,
1182    class: &[u8],
1183    method: &[u8],
1184) -> (Option<ProviderIndices>, usize) {
1185    let (exact, exact_candidates) =
1186        matching_exact_member_providers(exact_index, codebase, class, ascii_lowercase_word(method).as_bytes());
1187    let candidates = exact_candidates + wildcard_index.len();
1188    let wildcards = wildcard_index
1189        .iter()
1190        .filter(|(_, targets)| targets.iter().any(|target| target.matches(codebase, class, method)))
1191        .map(|(index, _)| *index);
1192
1193    (ProviderIndices::add_wildcards(exact, wildcards), candidates)
1194}
1195
1196fn filter_provider_indices(
1197    indices: Option<ProviderIndices>,
1198    predicate: impl Fn(u16) -> bool,
1199) -> Option<ProviderIndices> {
1200    indices.and_then(|indices| {
1201        ProviderIndices::from_iter(indices.as_slice().iter().copied().filter(|&index| predicate(index)))
1202    })
1203}
1204
1205fn filter_declared_provider_indices(
1206    indices: Option<ProviderIndices>,
1207    capabilities: &[u8],
1208    declared: bool,
1209    capability: u8,
1210    required: bool,
1211) -> Option<ProviderIndices> {
1212    if !declared {
1213        return indices;
1214    }
1215
1216    filter_provider_indices(indices, |index| {
1217        capabilities.get(usize::from(index)).is_some_and(|flags| (flags & capability != 0) == required)
1218    })
1219}
1220
1221fn providers_have_capability(capabilities: &[u8], indices: &[u16], capability: u8) -> bool {
1222    indices.iter().all(|index| capabilities.get(usize::from(*index)).is_some_and(|flags| flags & capability != 0))
1223}
1224
1225fn matching_exact_class_providers(
1226    index: &ProviderExactIndex,
1227    codebase: &CodebaseMetadata,
1228    class: &[u8],
1229) -> (Option<ProviderIndices>, usize) {
1230    let mut providers: Option<ProviderIndices> = None;
1231    let mut candidates = 0;
1232    for candidate in class_hierarchy(codebase, class) {
1233        let Some(indices) = index.get(&candidate) else {
1234            continue;
1235        };
1236        candidates += indices.len();
1237        for provider in indices {
1238            match &mut providers {
1239                Some(providers) => providers.insert_sorted(*provider),
1240                None => providers = Some(ProviderIndices::One(*provider)),
1241            }
1242        }
1243    }
1244
1245    (providers, candidates)
1246}
1247
1248fn index_property_providers(
1249    providers: &[PropertyProvider],
1250) -> (ProviderExactIndex, ProviderWildcardIndex<PropertyTarget>) {
1251    let mut exact = WordMap::default();
1252    let mut wildcard = Vec::new();
1253    for provider in providers {
1254        let mut wildcard_targets = Vec::new();
1255        for target in &provider.targets {
1256            if target.class.contains(&b'*') || target.property.contains(&b'*') {
1257                wildcard_targets.push(target.clone());
1258            } else {
1259                let class = ascii_lowercase_word(&target.class);
1260                let indices =
1261                    exact.entry(concat_word!(class, b"::", target.property.as_slice())).or_insert_with(Vec::new);
1262                if indices.last() != Some(&provider.index) {
1263                    indices.push(provider.index);
1264                }
1265            }
1266        }
1267
1268        if !wildcard_targets.is_empty() {
1269            wildcard.push((provider.index, wildcard_targets));
1270        }
1271    }
1272
1273    (exact, wildcard)
1274}
1275
1276fn matching_property_provider_indices(
1277    exact_index: &ProviderExactIndex,
1278    wildcard_index: &ProviderWildcardIndex<PropertyTarget>,
1279    codebase: &CodebaseMetadata,
1280    class: &[u8],
1281    property: &[u8],
1282) -> (Option<ProviderIndices>, usize) {
1283    let (exact, exact_candidates) = matching_exact_member_providers(exact_index, codebase, class, property);
1284    let candidates = exact_candidates + wildcard_index.len();
1285    let wildcards = wildcard_index
1286        .iter()
1287        .filter(|(_, targets)| targets.iter().any(|target| target.matches(codebase, class, property)))
1288        .map(|(index, _)| *index);
1289
1290    (ProviderIndices::add_wildcards(exact, wildcards), candidates)
1291}
1292
1293fn matching_exact_member_providers(
1294    index: &WordMap<Vec<u16>>,
1295    codebase: &CodebaseMetadata,
1296    class: &[u8],
1297    member: &[u8],
1298) -> (Option<ProviderIndices>, usize) {
1299    let mut providers: Option<ProviderIndices> = None;
1300    let mut candidates = 0;
1301    for candidate in class_hierarchy(codebase, class) {
1302        let key = concat_word!(candidate, b"::", member);
1303        let Some(indices) = index.get(&key) else {
1304            continue;
1305        };
1306        candidates += indices.len();
1307        for provider in indices {
1308            match &mut providers {
1309                Some(providers) => providers.insert_sorted(*provider),
1310                None => providers = Some(ProviderIndices::One(*provider)),
1311            }
1312        }
1313    }
1314
1315    (providers, candidates)
1316}
1317
1318fn class_hierarchy<'codebase>(
1319    codebase: &'codebase CodebaseMetadata,
1320    class: &[u8],
1321) -> impl Iterator<Item = Word> + 'codebase {
1322    let class = ascii_lowercase_word(class);
1323    std::iter::once(class).chain(codebase.get_class_like(class.as_bytes()).into_iter().flat_map(|metadata| {
1324        metadata
1325            .all_parent_classes
1326            .iter()
1327            .chain(&metadata.all_parent_interfaces)
1328            .chain(&metadata.used_traits)
1329            .chain(&metadata.require_extends)
1330            .chain(&metadata.require_implements)
1331            .copied()
1332    }))
1333}
1334
1335#[derive(Debug)]
1336pub struct ExternalAnalyzer<T = WorkerPool> {
1337    backends: Box<[Backend<T>]>,
1338    extensions: Box<[ExternalExtension]>,
1339    initialization_stubs: Box<[ExternalStub]>,
1340    trace_enabled: bool,
1341    telemetry: ExternalAnalyzerTelemetry,
1342    started_at: Option<Instant>,
1343}
1344
1345#[derive(Debug, Clone, Copy)]
1346pub(crate) struct ExternalAnalyzerCapabilities {
1347    pub function_return_types: bool,
1348    pub method_return_types: bool,
1349    pub function_signatures: bool,
1350    pub method_signatures: bool,
1351    pub function_assertions: bool,
1352    pub method_assertions: bool,
1353    pub property_types: bool,
1354    pub property_initialization: bool,
1355    pub class_initializers: bool,
1356    pub issue_filters: bool,
1357    pub method_call_analysis: bool,
1358    pub after_file_analysis: bool,
1359    pub after_analysis: bool,
1360}
1361
1362/// An analyzer initialized concurrently with the codebase pipeline.
1363#[derive(Debug)]
1364pub struct ExternalAnalyzerHandle {
1365    analyzer: OnceLock<ExternalAnalyzerResult<ExternalAnalyzer>>,
1366    initializer: Mutex<Option<JoinHandle<Result<ExternalAnalyzer, ExternalAnalyzerError>>>>,
1367    trace_enabled: bool,
1368    started_at: Option<Instant>,
1369    prepare_calls: AtomicU64,
1370    initialization_wait_ns: AtomicU64,
1371}
1372
1373impl ExternalAnalyzerHandle {
1374    /// Wraps an analyzer that has already completed initialization.
1375    #[must_use]
1376    pub fn ready(analyzer: ExternalAnalyzer) -> Self {
1377        let trace_enabled = tracing::enabled!(tracing::Level::TRACE);
1378        let cell = OnceLock::new();
1379        let _result = cell.set(Ok(analyzer));
1380        tracing::trace!("Created ready external analyzer handle.");
1381        Self {
1382            analyzer: cell,
1383            initializer: Mutex::new(None),
1384            trace_enabled,
1385            started_at: trace_enabled.then(Instant::now),
1386            prepare_calls: AtomicU64::new(0),
1387            initialization_wait_ns: AtomicU64::new(0),
1388        }
1389    }
1390
1391    /// Wraps an analyzer initialization thread without waiting for it.
1392    #[must_use]
1393    pub fn pending(initializer: JoinHandle<Result<ExternalAnalyzer, ExternalAnalyzerError>>) -> Self {
1394        let trace_enabled = tracing::enabled!(tracing::Level::TRACE);
1395        tracing::trace!("Created pending external analyzer handle.");
1396        Self {
1397            analyzer: OnceLock::new(),
1398            initializer: Mutex::new(Some(initializer)),
1399            trace_enabled,
1400            started_at: trace_enabled.then(Instant::now),
1401            prepare_calls: AtomicU64::new(0),
1402            initialization_wait_ns: AtomicU64::new(0),
1403        }
1404    }
1405
1406    pub(crate) fn prepare(&self) -> ExternalAnalyzerResult<()> {
1407        if self.trace_enabled {
1408            self.prepare_calls.fetch_add(1, Ordering::Relaxed);
1409        }
1410        self.get().map(|_| ())
1411    }
1412
1413    pub(crate) fn initialization_files(&self) -> ExternalAnalyzerResult<Vec<File>> {
1414        Ok(self.get()?.initialization_stubs.iter().map(ExternalStub::to_file).collect())
1415    }
1416
1417    pub(crate) fn with<R>(
1418        &self,
1419        operation: impl FnOnce(&ExternalAnalyzer) -> Result<R, ExternalAnalyzerError>,
1420    ) -> ExternalAnalyzerResult<R> {
1421        operation(self.get()?).map_err(Arc::new)
1422    }
1423
1424    pub(crate) fn read<R>(&self, operation: impl FnOnce(&ExternalAnalyzer) -> R) -> ExternalAnalyzerResult<R> {
1425        Ok(operation(self.get()?))
1426    }
1427
1428    fn get(&self) -> ExternalAnalyzerResult<&ExternalAnalyzer> {
1429        self.analyzer
1430            .get_or_init(|| {
1431                let wait_start = self.trace_enabled.then(Instant::now);
1432                tracing::trace!("Waiting for external analyzer initialization thread.");
1433                let initializer = self
1434                    .initializer
1435                    .lock()
1436                    .unwrap_or_else(std::sync::PoisonError::into_inner)
1437                    .take()
1438                    .ok_or_else(|| Arc::new(ExternalAnalyzerError::InitializationUnavailable))?;
1439                let result = initializer
1440                    .join()
1441                    .map_err(|_| Arc::new(ExternalAnalyzerError::InitializationPanicked))?
1442                    .map_err(Arc::new);
1443
1444                if let Some(start) = wait_start {
1445                    self.initialization_wait_ns.fetch_add(duration_nanos(start.elapsed()), Ordering::Relaxed);
1446                    tracing::trace!(
1447                        elapsed = ?start.elapsed(),
1448                        success = result.is_ok(),
1449                        "External analyzer initialization thread joined."
1450                    );
1451                }
1452
1453                result
1454            })
1455            .as_ref()
1456            .map_err(Clone::clone)
1457    }
1458}
1459
1460impl Drop for ExternalAnalyzerHandle {
1461    fn drop(&mut self) {
1462        if self.analyzer.get().is_none() {
1463            let initializer = self.initializer.get_mut().unwrap_or_else(std::sync::PoisonError::into_inner).take();
1464            if let Some(initializer) = initializer {
1465                tracing::trace!("Joining unused external analyzer initialization thread during shutdown.");
1466                let _result = initializer.join();
1467            }
1468        }
1469
1470        if self.trace_enabled {
1471            tracing::trace!(
1472                initialized = self.analyzer.get().is_some(),
1473                prepare_calls = self.prepare_calls.load(Ordering::Relaxed),
1474                initialization_wait = ?Duration::from_nanos(self.initialization_wait_ns.load(Ordering::Relaxed)),
1475                lifetime = ?self.started_at.map(|start| start.elapsed()).unwrap_or_default(),
1476                "External analyzer handle dropped."
1477            );
1478        }
1479    }
1480}
1481
1482impl ExternalAnalyzer<WorkerPool> {
1483    /// Discovers and validates the analyzer plugins exposed by worker pools.
1484    ///
1485    /// # Errors
1486    ///
1487    /// Returns an error when a worker fails, sends malformed metadata, disagrees
1488    /// with another process in its pool, or advertises duplicate identifiers.
1489    pub fn initialize(
1490        pools: impl IntoIterator<Item = Arc<WorkerPool>>,
1491        php_version: PHPVersion,
1492        enabled_plugins: &[String],
1493        disable_defaults: bool,
1494    ) -> Result<Self, ExternalAnalyzerError> {
1495        let analyzer = Self::initialize_transports(pools, php_version, enabled_plugins, disable_defaults)?;
1496        for backend in &analyzer.backends {
1497            if backend.registration.has_worker_reducer {
1498                backend.transport.enable_worker_reduction();
1499            }
1500        }
1501
1502        Ok(analyzer)
1503    }
1504}
1505
1506impl<T> ExternalAnalyzer<T> {
1507    fn record_error(&self) {
1508        if self.trace_enabled {
1509            self.telemetry.errors.fetch_add(1, Ordering::Relaxed);
1510        }
1511    }
1512
1513    fn record_lifecycle_error(&self) {
1514        if self.trace_enabled {
1515            self.telemetry.lifecycle_errors.fetch_add(1, Ordering::Relaxed);
1516            self.telemetry.errors.fetch_add(1, Ordering::Relaxed);
1517        }
1518    }
1519
1520    fn record_issue_filter_error(&self) {
1521        if self.trace_enabled {
1522            self.telemetry.issue_filter_errors.fetch_add(1, Ordering::Relaxed);
1523            self.telemetry.errors.fetch_add(1, Ordering::Relaxed);
1524        }
1525    }
1526
1527    #[must_use]
1528    pub fn extensions(&self) -> &[ExternalExtension] {
1529        &self.extensions
1530    }
1531
1532    #[must_use]
1533    pub fn initialization_files(&self) -> Vec<File> {
1534        self.initialization_stubs.iter().map(ExternalStub::to_file).collect()
1535    }
1536
1537    pub(crate) fn capabilities(&self) -> ExternalAnalyzerCapabilities {
1538        let any = |predicate: fn(&Backend<T>) -> bool| self.backends.iter().any(predicate);
1539        ExternalAnalyzerCapabilities {
1540            function_return_types: any(|backend| !backend.registration.function_providers.is_empty()),
1541            method_return_types: any(|backend| !backend.registration.method_providers.is_empty()),
1542            function_signatures: any(|backend| {
1543                backend
1544                    .registration
1545                    .function_providers
1546                    .iter()
1547                    .any(|provider| provider.capabilities & PROVIDER_CALLABLE_SIGNATURE != 0)
1548            }),
1549            method_signatures: any(|backend| {
1550                backend
1551                    .registration
1552                    .method_providers
1553                    .iter()
1554                    .any(|provider| provider.capabilities & PROVIDER_CALLABLE_SIGNATURE != 0)
1555            }),
1556            function_assertions: any(|backend| !backend.registration.function_assertion_providers.is_empty()),
1557            method_assertions: any(|backend| !backend.registration.method_assertion_providers.is_empty()),
1558            property_types: any(|backend| !backend.registration.property_providers.is_empty()),
1559            property_initialization: any(|backend| !backend.registration.property_initialization_providers.is_empty()),
1560            class_initializers: any(|backend| !backend.registration.class_initializer_providers.is_empty()),
1561            issue_filters: any(|backend| !backend.issue_filter_indices.is_empty()),
1562            method_call_analysis: any(|backend| !backend.registration.method_call_analysis_hooks.is_empty()),
1563            after_file_analysis: any(|backend| {
1564                !backend.registration.after_file_analysis_plugins.is_empty()
1565                    || !backend.registration.node_analysis_plugins.is_empty()
1566            }),
1567            after_analysis: any(|backend| !backend.registration.after_analysis_plugins.is_empty()),
1568        }
1569    }
1570
1571    pub(crate) fn node_analysis_requirements(&self) -> Option<NodeAnalysisRequirements> {
1572        let mut targets = [false; u8::MAX as usize + 1];
1573        let mut requirements = [0; u8::MAX as usize + 1];
1574        let mut any = false;
1575        for backend in &self.backends {
1576            for hook in &backend.registration.node_analysis_hooks {
1577                for target in &hook.targets {
1578                    targets[*target as usize] = true;
1579                    requirements[*target as usize] |= hook.requirements;
1580                    any = true;
1581                }
1582            }
1583        }
1584
1585        let method_call_hooks: Arc<[MethodCallAnalysisHookRegistration]> = self
1586            .backends
1587            .iter()
1588            .flat_map(|backend| backend.registration.method_call_analysis_hooks.iter().cloned())
1589            .collect::<Vec<_>>()
1590            .into();
1591        any |= !method_call_hooks.is_empty();
1592
1593        let class_like_hooks: Arc<[ClassLikeAnalysisHookRegistration]> = self
1594            .backends
1595            .iter()
1596            .flat_map(|backend| backend.registration.class_like_analysis_hooks.iter().cloned())
1597            .collect::<Vec<_>>()
1598            .into();
1599        any |= !class_like_hooks.is_empty();
1600
1601        any.then_some(NodeAnalysisRequirements { targets, requirements, method_call_hooks, class_like_hooks })
1602    }
1603}
1604
1605impl<T> ExternalAnalyzer<T>
1606where
1607    T: AnalyzerTransport,
1608{
1609    pub(crate) fn codebase_scan_plan(&self) -> Result<Option<CodebaseScanPlan>, ExternalAnalyzerError> {
1610        CodebaseScanPlan::compile(&self.backends)
1611    }
1612
1613    pub(crate) fn run_codebase_scan(&self, files: Vec<CodebaseScanFile>) -> Result<(), ExternalAnalyzerError> {
1614        scan::dispatch(&self.backends, files)
1615    }
1616
1617    pub(crate) fn filter_issues(
1618        &self,
1619        file: &File,
1620        mut issues: IssueCollection,
1621        codebase: &CodebaseMetadata,
1622        session: &ExternalAnalysisSession,
1623    ) -> Result<IssueCollection, ExternalAnalyzerError> {
1624        for backend in &self.backends {
1625            let hooks = &backend.issue_filter_indices;
1626            if hooks.is_empty() || issues.is_empty() {
1627                continue;
1628            }
1629
1630            let mut candidate_indices = Vec::new();
1631            let mut candidates = Vec::new();
1632            for (index, issue) in issues.iter().enumerate() {
1633                if issue.code.as_ref().is_some_and(|code| backend.issue_filter_codes.contains(code)) {
1634                    candidate_indices.push(index);
1635                    candidates.push(issue);
1636                }
1637            }
1638            if candidates.is_empty() {
1639                continue;
1640            }
1641
1642            let started_at = self.trace_enabled.then(Instant::now);
1643            let encode_start = self.trace_enabled.then(Instant::now);
1644            let issue_count = candidates.len();
1645            let request =
1646                protocol::encode_issue_filter_request(hooks, file, &candidates, session.generation(), session)
1647                    .inspect_err(|_| self.record_issue_filter_error())?;
1648            if let Some(start) = encode_start {
1649                self.telemetry.issue_filter_encode_ns.fetch_add(duration_nanos(start.elapsed()), Ordering::Relaxed);
1650                self.telemetry.issue_filter_batches.fetch_add(1, Ordering::Relaxed);
1651                self.telemetry.issue_filter_candidates.fetch_add(issue_count as u64, Ordering::Relaxed);
1652                self.telemetry.issue_filter_request_bytes.fetch_add(request.len() as u64, Ordering::Relaxed);
1653            }
1654
1655            let nested_telemetry = &self.telemetry;
1656            let trace_enabled = self.trace_enabled;
1657            let mut handler = |frame: &Frame| {
1658                let nested_start = trace_enabled.then(Instant::now);
1659                let result = protocol::handle_nested_request(&frame.payload, codebase, session, |_| None);
1660                if let Some(start) = nested_start {
1661                    nested_telemetry.record_nested_request(frame.payload.len(), start.elapsed(), &result);
1662                }
1663
1664                result.map(|(_, response)| response).map_err(|error| error.to_string().into_bytes())
1665            };
1666
1667            let ipc_start = self.trace_enabled.then(Instant::now);
1668            let response = backend
1669                .transport
1670                .request_with_handler(request, &mut handler)
1671                .inspect_err(|_| self.record_issue_filter_error())?;
1672
1673            if let Some(start) = ipc_start {
1674                self.telemetry.issue_filter_ipc_ns.fetch_add(duration_nanos(start.elapsed()), Ordering::Relaxed);
1675                self.telemetry.issue_filter_response_bytes.fetch_add(response.len() as u64, Ordering::Relaxed);
1676            }
1677
1678            let decode_start = self.trace_enabled.then(Instant::now);
1679            let removed = protocol::decode_issue_filter_response(&response, issue_count)
1680                .inspect_err(|_| self.record_issue_filter_error())?;
1681            let removed = removed.into_iter().map(|index| candidate_indices[index]).collect::<Vec<_>>();
1682
1683            if let Some(start) = decode_start {
1684                self.telemetry.issue_filter_decode_ns.fetch_add(duration_nanos(start.elapsed()), Ordering::Relaxed);
1685                self.telemetry.issue_filter_removed.fetch_add(removed.len() as u64, Ordering::Relaxed);
1686            }
1687
1688            let removed_count = removed.len();
1689            if removed_count != 0 {
1690                let mut removed = removed.into_iter();
1691                let mut next_removed = removed.next();
1692                issues = IssueCollection::from(issues.into_iter().enumerate().filter_map(|(index, issue)| {
1693                    if next_removed == Some(index) {
1694                        next_removed = removed.next();
1695                        None
1696                    } else {
1697                        Some(issue)
1698                    }
1699                }));
1700            }
1701
1702            if let Some(start) = started_at {
1703                let elapsed = start.elapsed();
1704                self.telemetry.issue_filter_ns.fetch_add(duration_nanos(elapsed), Ordering::Relaxed);
1705                if elapsed >= SLOW_LIFECYCLE_THRESHOLD {
1706                    tracing::trace!(
1707                        file = %String::from_utf8_lossy(&file.name),
1708                        hooks = hooks.len(),
1709                        candidates = issue_count,
1710                        removed = removed_count,
1711                        retained = issues.len(),
1712                        response_bytes = response.len(),
1713                        elapsed = ?elapsed,
1714                        "Slow external analyzer issue-filter batch completed."
1715                    );
1716                }
1717            }
1718        }
1719
1720        Ok(issues)
1721    }
1722
1723    fn dispatch_lifecycle_request<H>(
1724        &self,
1725        backend: &Backend<T>,
1726        phase: LifecyclePhase,
1727        plugins: &[u16],
1728        logical_callbacks: usize,
1729        request: Vec<u8>,
1730        handler: &mut H,
1731        session: &ExternalAnalysisSession,
1732        default_file: Option<&File>,
1733        codebase: &CodebaseMetadata,
1734        started_at: Option<Instant>,
1735    ) -> Result<lifecycle::LifecycleEffects, ExternalAnalyzerError>
1736    where
1737        H: WorkerRequestHandler,
1738    {
1739        if self.trace_enabled {
1740            match phase {
1741                LifecyclePhase::Before => {
1742                    self.telemetry.before_analysis_requests.fetch_add(1, Ordering::Relaxed);
1743                }
1744                LifecyclePhase::AfterFile | LifecyclePhase::AfterFileBatch => {
1745                    self.telemetry.after_file_analysis_requests.fetch_add(1, Ordering::Relaxed);
1746                }
1747                LifecyclePhase::After => {
1748                    self.telemetry.after_analysis_requests.fetch_add(1, Ordering::Relaxed);
1749                }
1750            }
1751
1752            self.telemetry.lifecycle_plugins.fetch_add(logical_callbacks as u64, Ordering::Relaxed);
1753            self.telemetry.lifecycle_request_bytes.fetch_add(request.len() as u64, Ordering::Relaxed);
1754        }
1755
1756        let ipc_start = self.trace_enabled.then(Instant::now);
1757        let response =
1758            backend.transport.request_with_handler(request, handler).inspect_err(|_| self.record_lifecycle_error())?;
1759
1760        if let Some(start) = ipc_start {
1761            self.telemetry.lifecycle_ipc_ns.fetch_add(duration_nanos(start.elapsed()), Ordering::Relaxed);
1762            self.telemetry.lifecycle_response_bytes.fetch_add(response.len() as u64, Ordering::Relaxed);
1763        }
1764
1765        let decode_start = self.trace_enabled.then(Instant::now);
1766        let issues = lifecycle::decode_lifecycle_response(
1767            &response,
1768            phase.request_kind(),
1769            plugins,
1770            &backend.registration.plugins,
1771            session,
1772            default_file,
1773            codebase,
1774        )
1775        .inspect_err(|_| self.record_lifecycle_error())?;
1776
1777        if let Some(start) = decode_start {
1778            self.telemetry.lifecycle_decode_ns.fetch_add(duration_nanos(start.elapsed()), Ordering::Relaxed);
1779            self.telemetry.lifecycle_issues.fetch_add(issues.issues.len() as u64, Ordering::Relaxed);
1780        }
1781
1782        if let Some(start) = started_at {
1783            let elapsed = start.elapsed();
1784            self.telemetry.lifecycle_ns.fetch_add(duration_nanos(elapsed), Ordering::Relaxed);
1785            if elapsed >= SLOW_LIFECYCLE_THRESHOLD {
1786                let file = default_file
1787                    .map_or_else(|| "<project>".into(), |file| String::from_utf8_lossy(&file.name).into_owned());
1788                tracing::trace!(
1789                    phase = phase.name(),
1790                    plugins = plugins.len(),
1791                    file,
1792                    response_bytes = response.len(),
1793                    issues = issues.issues.len(),
1794                    elapsed = ?elapsed,
1795                    "Slow external analyzer lifecycle request completed."
1796                );
1797            }
1798        }
1799
1800        Ok(issues)
1801    }
1802
1803    pub(crate) fn run_before_analysis_hooks(
1804        &self,
1805        codebase: &CodebaseMetadata,
1806        session: &ExternalAnalysisSession,
1807    ) -> Result<BeforeAnalysisResult, ExternalAnalyzerError> {
1808        let mut result = BeforeAnalysisResult::default();
1809        for backend in &self.backends {
1810            let entry_point_start = self.trace_enabled.then(Instant::now);
1811            let entry_point_matches = backend.contribute_entry_point_references(codebase, &mut result.references);
1812            if let Some(start) = entry_point_start
1813                && (!backend.registration.entry_points.is_empty()
1814                    || !backend.registration.attributed_entry_points.is_empty())
1815            {
1816                tracing::trace!(
1817                    entry_points = backend.registration.entry_points.len(),
1818                    attributed_entry_points = backend.registration.attributed_entry_points.len(),
1819                    matches = entry_point_matches,
1820                    elapsed = ?start.elapsed(),
1821                    "Declarative external entry points resolved."
1822                );
1823            }
1824
1825            let plugins = &backend.registration.before_analysis_plugins;
1826            if plugins.is_empty() {
1827                continue;
1828            }
1829            let lifecycle_start = self.trace_enabled.then(Instant::now);
1830            let encode_start = self.trace_enabled.then(Instant::now);
1831            let request = lifecycle::encode_before_analysis_request(session.generation(), plugins)
1832                .inspect_err(|_| self.record_lifecycle_error())?;
1833
1834            if let Some(start) = encode_start {
1835                self.telemetry.lifecycle_encode_ns.fetch_add(duration_nanos(start.elapsed()), Ordering::Relaxed);
1836            }
1837
1838            let nested_telemetry = &self.telemetry;
1839            let trace_enabled = self.trace_enabled;
1840            let mut handler = |frame: &Frame| {
1841                let nested_start = trace_enabled.then(Instant::now);
1842                let result = protocol::handle_nested_request(&frame.payload, codebase, session, |_| None);
1843                if let Some(start) = nested_start {
1844                    nested_telemetry.record_nested_request(frame.payload.len(), start.elapsed(), &result);
1845                }
1846
1847                result.map(|(_, response)| response).map_err(|error| error.to_string().into_bytes())
1848            };
1849
1850            let effects = self.dispatch_lifecycle_request(
1851                backend,
1852                LifecyclePhase::Before,
1853                plugins,
1854                plugins.len(),
1855                request,
1856                &mut handler,
1857                session,
1858                None,
1859                codebase,
1860                lifecycle_start,
1861            )?;
1862            result.issues.extend(effects.issues);
1863            result.references.extend(effects.references);
1864        }
1865
1866        Ok(result)
1867    }
1868
1869    pub(crate) fn run_after_file_analysis_hooks(
1870        &self,
1871        file: &File,
1872        program: &Program<'_>,
1873        resolved_names: &ResolvedNames<'_>,
1874        artifacts: &AnalysisArtifacts,
1875        codebase: &CodebaseMetadata,
1876        session: &ExternalAnalysisSession,
1877    ) -> Result<AfterFileAnalysisResult, ExternalAnalyzerError> {
1878        let mut result = AfterFileAnalysisResult::default();
1879        let node_analysis_requirements = self.node_analysis_requirements();
1880        let store = lifecycle::AnalysisStore::File {
1881            file,
1882            program,
1883            resolved_names,
1884            artifacts,
1885            node_analysis_targets: node_analysis_requirements.as_ref().map(NodeAnalysisRequirements::targets),
1886        };
1887        for (backend_index, backend) in self.backends.iter().enumerate() {
1888            let plugins = backend.registration.file_analysis_plugins();
1889            if plugins.is_empty() {
1890                continue;
1891            }
1892            let include_expression_types = backend.registration.file_analysis_requires_expression_types(&plugins);
1893            let backend_node_requirements = backend.registration.node_analysis_requirements().filter(|requirements| {
1894                lifecycle::has_node_analysis_target(program, artifacts, resolved_names, codebase, requirements)
1895            });
1896
1897            let lifecycle_start = self.trace_enabled.then(Instant::now);
1898            let encode_start = self.trace_enabled.then(Instant::now);
1899            let request = lifecycle::encode_after_file_analysis_request(
1900                session.generation(),
1901                &plugins,
1902                file,
1903                program,
1904                resolved_names,
1905                artifacts,
1906                codebase,
1907                include_expression_types,
1908                backend_node_requirements.as_ref(),
1909                u16::try_from(backend_index)
1910                    .map_err(|_| error::protocol("external analyzer backend exceeds u16::MAX"))?,
1911            )
1912            .inspect_err(|_| self.record_lifecycle_error())?;
1913
1914            if let Some(start) = encode_start {
1915                self.telemetry.lifecycle_encode_ns.fetch_add(duration_nanos(start.elapsed()), Ordering::Relaxed);
1916            }
1917
1918            let nested_telemetry = &self.telemetry;
1919            let trace_enabled = self.trace_enabled;
1920            let mut handler = |frame: &Frame| {
1921                let nested_start = trace_enabled.then(Instant::now);
1922                let result =
1923                    if protocol::message_kind(&frame.payload).map_err(|error| error.to_string().into_bytes())? == 8 {
1924                        lifecycle::handle_analysis_query(&frame.payload, session, &store)
1925                            .map(|response| (protocol::NestedRequestKind::AnalysisQuery, response))
1926                    } else {
1927                        protocol::handle_nested_request(&frame.payload, codebase, session, |_| None)
1928                    };
1929                if let Some(start) = nested_start {
1930                    nested_telemetry.record_nested_request(frame.payload.len(), start.elapsed(), &result);
1931                }
1932
1933                result.map(|(_, response)| response).map_err(|error| error.to_string().into_bytes())
1934            };
1935
1936            let effects = self.dispatch_lifecycle_request(
1937                backend,
1938                LifecyclePhase::AfterFile,
1939                &plugins,
1940                plugins.len(),
1941                request,
1942                &mut handler,
1943                session,
1944                Some(file),
1945                codebase,
1946                lifecycle_start,
1947            )?;
1948            result.issues.extend(effects.issues);
1949            extend_references_by_file(&mut result.references_by_file, effects.references_by_file);
1950        }
1951
1952        Ok(result)
1953    }
1954
1955    pub(crate) fn run_after_file_analysis_batch_hooks(
1956        &self,
1957        files: &[Arc<FileAnalysisSnapshot>],
1958        codebase: &CodebaseMetadata,
1959        session: &ExternalAnalysisSession,
1960    ) -> Result<AfterFileAnalysisResult, ExternalAnalyzerError> {
1961        if files.is_empty() {
1962            return Ok(AfterFileAnalysisResult::default());
1963        }
1964
1965        let mut result = AfterFileAnalysisResult::default();
1966        for (backend_index, backend) in self.backends.iter().enumerate() {
1967            let plugins = backend.registration.file_analysis_plugins();
1968            if plugins.is_empty() {
1969                continue;
1970            }
1971
1972            let targeted_files;
1973            let files = if backend.registration.after_file_analysis_plugins.is_empty() {
1974                targeted_files =
1975                    files.iter().filter(|file| file.has_node_analysis_targets()).cloned().collect::<Vec<_>>();
1976                targeted_files.as_slice()
1977            } else {
1978                files
1979            };
1980            if files.is_empty() {
1981                continue;
1982            }
1983            let store = lifecycle::AnalysisStore::Project(files);
1984            let include_expression_types = backend.registration.file_analysis_requires_expression_types(&plugins);
1985            let backend_node_requirements = backend.registration.node_analysis_requirements();
1986
1987            let lifecycle_start = self.trace_enabled.then(Instant::now);
1988            let encode_start = self.trace_enabled.then(Instant::now);
1989            let request = lifecycle::encode_after_file_analysis_batch_request(
1990                session.generation(),
1991                &plugins,
1992                files,
1993                include_expression_types,
1994                backend_node_requirements.as_ref(),
1995                session,
1996                u16::try_from(backend_index)
1997                    .map_err(|_| error::protocol("external analyzer backend exceeds u16::MAX"))?,
1998            )
1999            .inspect_err(|_| self.record_lifecycle_error())?;
2000            if let Some(start) = encode_start {
2001                self.telemetry.lifecycle_encode_ns.fetch_add(duration_nanos(start.elapsed()), Ordering::Relaxed);
2002                self.telemetry.after_file_analysis_files.fetch_add(files.len() as u64, Ordering::Relaxed);
2003            }
2004
2005            let nested_telemetry = &self.telemetry;
2006            let trace_enabled = self.trace_enabled;
2007            let mut handler = |frame: &Frame| {
2008                let nested_start = trace_enabled.then(Instant::now);
2009                let result =
2010                    if protocol::message_kind(&frame.payload).map_err(|error| error.to_string().into_bytes())? == 8 {
2011                        lifecycle::handle_analysis_query(&frame.payload, session, &store)
2012                            .map(|response| (protocol::NestedRequestKind::AnalysisQuery, response))
2013                    } else {
2014                        protocol::handle_nested_request(&frame.payload, codebase, session, |_| None)
2015                    };
2016                if let Some(start) = nested_start {
2017                    nested_telemetry.record_nested_request(frame.payload.len(), start.elapsed(), &result);
2018                }
2019
2020                result.map(|(_, response)| response).map_err(|error| error.to_string().into_bytes())
2021            };
2022
2023            let effects = self.dispatch_lifecycle_request(
2024                backend,
2025                LifecyclePhase::AfterFileBatch,
2026                &plugins,
2027                plugins.len().saturating_mul(files.len()),
2028                request,
2029                &mut handler,
2030                session,
2031                None,
2032                codebase,
2033                lifecycle_start,
2034            )?;
2035            result.issues.extend(effects.issues);
2036            extend_references_by_file(&mut result.references_by_file, effects.references_by_file);
2037        }
2038
2039        Ok(result)
2040    }
2041
2042    pub(crate) fn run_after_analysis_hooks(
2043        &self,
2044        analysis_result: &crate::analysis_result::AnalysisResult,
2045        files: &[Arc<FileAnalysisSnapshot>],
2046        codebase: &CodebaseMetadata,
2047        session: &ExternalAnalysisSession,
2048    ) -> Result<IssueCollection, ExternalAnalyzerError> {
2049        let mut issues = IssueCollection::new();
2050        let store = lifecycle::AnalysisStore::Project(files);
2051        let reference_store = lifecycle::SymbolReferenceStore::new(&analysis_result.symbol_references);
2052        for backend in &self.backends {
2053            let plugins = &backend.registration.after_analysis_plugins;
2054            if plugins.is_empty() {
2055                continue;
2056            }
2057
2058            let lifecycle_start = self.trace_enabled.then(Instant::now);
2059            let encode_start = self.trace_enabled.then(Instant::now);
2060            let request =
2061                lifecycle::encode_after_analysis_request(session.generation(), plugins, analysis_result, files)
2062                    .inspect_err(|_| self.record_lifecycle_error())?;
2063            if let Some(start) = encode_start {
2064                self.telemetry.lifecycle_encode_ns.fetch_add(duration_nanos(start.elapsed()), Ordering::Relaxed);
2065            }
2066
2067            let nested_telemetry = &self.telemetry;
2068            let trace_enabled = self.trace_enabled;
2069            let mut handler = |frame: &Frame| {
2070                let nested_start = trace_enabled.then(Instant::now);
2071                let kind = protocol::message_kind(&frame.payload).map_err(|error| error.to_string().into_bytes())?;
2072                let result = if kind == 8 {
2073                    lifecycle::handle_analysis_query(&frame.payload, session, &store)
2074                        .map(|response| (protocol::NestedRequestKind::AnalysisQuery, response))
2075                } else if lifecycle::is_symbol_reference_query(&frame.payload)
2076                    .map_err(|error| error.to_string().into_bytes())?
2077                {
2078                    lifecycle::handle_symbol_reference_query(&frame.payload, session, codebase, &reference_store)
2079                        .map(|response| (protocol::NestedRequestKind::SymbolReferenceQuery, response))
2080                } else {
2081                    protocol::handle_nested_request(&frame.payload, codebase, session, |_| None)
2082                };
2083                if let Some(start) = nested_start {
2084                    nested_telemetry.record_nested_request(frame.payload.len(), start.elapsed(), &result);
2085                }
2086
2087                result.map(|(_, response)| response).map_err(|error| error.to_string().into_bytes())
2088            };
2089
2090            let effects = self.dispatch_lifecycle_request(
2091                backend,
2092                LifecyclePhase::After,
2093                plugins,
2094                plugins.len(),
2095                request,
2096                &mut handler,
2097                session,
2098                None,
2099                codebase,
2100                lifecycle_start,
2101            )?;
2102            issues.extend(effects.issues);
2103        }
2104
2105        Ok(issues)
2106    }
2107
2108    fn exchange_provider_request(
2109        &self,
2110        backend: &Backend<T>,
2111        request: &mut protocol::ReturnTypeRequest<'_>,
2112        affinity: &[u8],
2113        codebase: &CodebaseMetadata,
2114        session: &ExternalAnalysisSession,
2115    ) -> Result<Vec<u8>, ExternalAnalyzerError> {
2116        if self.trace_enabled {
2117            self.telemetry.requests.fetch_add(1, Ordering::Relaxed);
2118            self.telemetry.snapshotted_types.fetch_add(request.snapshotted_types as u64, Ordering::Relaxed);
2119            self.telemetry.arguments.fetch_add(request.arguments as u64, Ordering::Relaxed);
2120            self.telemetry.typed_arguments.fetch_add(request.typed_arguments as u64, Ordering::Relaxed);
2121            self.telemetry
2122                .type_snapshot_ns
2123                .fetch_add(duration_nanos(request.type_snapshot_duration), Ordering::Relaxed);
2124            self.telemetry.request_bytes.fetch_add(request.payload.len() as u64, Ordering::Relaxed);
2125        }
2126
2127        let generation = session.generation();
2128        let cache_key = request.memoize.then(|| request.payload.clone());
2129        let cached = cache_key.as_deref().and_then(|cache_key| {
2130            backend
2131                .provider_response_cache
2132                .lock()
2133                .unwrap_or_else(std::sync::PoisonError::into_inner)
2134                .get(generation, cache_key)
2135        });
2136        let response = if let Some(response) = cached {
2137            if self.trace_enabled {
2138                self.telemetry.provider_cache_hits.fetch_add(1, Ordering::Relaxed);
2139            }
2140            response
2141        } else {
2142            let payload = std::mem::take(&mut request.payload);
2143            let response = self.exchange_provider_payload(backend, payload, affinity, codebase, session, |handle| {
2144                protocol::resolve_type_handle(&request.types, handle)
2145            })?;
2146            if let Some(cache_key) = cache_key {
2147                backend.provider_response_cache.lock().unwrap_or_else(std::sync::PoisonError::into_inner).insert(
2148                    generation,
2149                    cache_key,
2150                    response.clone(),
2151                );
2152            }
2153            response
2154        };
2155
2156        Ok(response)
2157    }
2158
2159    fn decode_provider_response<R>(
2160        &self,
2161        decode: impl FnOnce() -> Result<R, ExternalAnalyzerError>,
2162    ) -> Result<R, ExternalAnalyzerError> {
2163        let start = self.trace_enabled.then(Instant::now);
2164        let result = decode().inspect_err(|_| self.record_error());
2165        if let Some(start) = start {
2166            self.telemetry.decode_ns.fetch_add(duration_nanos(start.elapsed()), Ordering::Relaxed);
2167        }
2168        result
2169    }
2170
2171    fn exchange_provider_payload<'type_info>(
2172        &self,
2173        backend: &Backend<T>,
2174        request: Vec<u8>,
2175        affinity: &[u8],
2176        codebase: &CodebaseMetadata,
2177        session: &ExternalAnalysisSession,
2178        resolve_type: impl Fn(usize) -> Option<&'type_info TUnion>,
2179    ) -> Result<Vec<u8>, ExternalAnalyzerError> {
2180        let nested_telemetry = &self.telemetry;
2181        let trace_enabled = self.trace_enabled;
2182        let mut handler = |frame: &Frame| {
2183            let nested_start = trace_enabled.then(Instant::now);
2184            let result = protocol::handle_nested_request(&frame.payload, codebase, session, &resolve_type);
2185            if let Some(start) = nested_start {
2186                nested_telemetry.record_nested_request(frame.payload.len(), start.elapsed(), &result);
2187            }
2188
2189            result.map(|(_, response)| response).map_err(|error| error.to_string().into_bytes())
2190        };
2191
2192        let ipc_start = self.trace_enabled.then(Instant::now);
2193        if self.trace_enabled {
2194            self.telemetry.ipc_requests.fetch_add(1, Ordering::Relaxed);
2195        }
2196        let response = backend
2197            .transport
2198            .request_with_handler_affinity(request, affinity, &mut handler)
2199            .inspect_err(|_| self.record_error())?;
2200        if let Some(start) = ipc_start {
2201            self.telemetry.ipc_ns.fetch_add(duration_nanos(start.elapsed()), Ordering::Relaxed);
2202            self.telemetry.response_bytes.fetch_add(response.len() as u64, Ordering::Relaxed);
2203        }
2204
2205        Ok(response)
2206    }
2207
2208    fn dispatch_assertion_request(
2209        &self,
2210        backend: &Backend<T>,
2211        mut request: protocol::ReturnTypeRequest<'_>,
2212        affinity: &[u8],
2213        codebase: &CodebaseMetadata,
2214        session: &ExternalAnalysisSession,
2215    ) -> Result<Option<InvocationAssertions>, ExternalAnalyzerError> {
2216        if self.trace_enabled {
2217            self.telemetry.assertion_requests.fetch_add(1, Ordering::Relaxed);
2218        }
2219        let response = self.exchange_provider_request(backend, &mut request, affinity, codebase, session)?;
2220        let result = self.decode_provider_response(|| {
2221            protocol::decode_assertion_response(&response, |handle| {
2222                protocol::resolve_type_handle(&request.types, handle)
2223            })
2224        })?;
2225        if self.trace_enabled {
2226            if result.is_some() {
2227                self.telemetry.provided_assertions.fetch_add(1, Ordering::Relaxed);
2228            } else {
2229                self.telemetry.declined_requests.fetch_add(1, Ordering::Relaxed);
2230            }
2231        }
2232        Ok(result)
2233    }
2234
2235    pub(crate) fn get_function_assertions(
2236        &self,
2237        function: &[u8],
2238        invocation: &Invocation<'_, '_, '_>,
2239        artifacts: &AnalysisArtifacts,
2240        source_file: &File,
2241        codebase: &CodebaseMetadata,
2242        session: &ExternalAnalysisSession,
2243    ) -> Result<Option<InvocationAssertions>, ExternalAnalyzerError> {
2244        self.get_assertions(CallableTarget::Function(function), invocation, artifacts, source_file, codebase, session)
2245    }
2246
2247    pub(crate) fn get_method_assertions(
2248        &self,
2249        class: &[u8],
2250        method: &[u8],
2251        invocation: &Invocation<'_, '_, '_>,
2252        artifacts: &AnalysisArtifacts,
2253        source_file: &File,
2254        codebase: &CodebaseMetadata,
2255        session: &ExternalAnalysisSession,
2256    ) -> Result<Option<InvocationAssertions>, ExternalAnalyzerError> {
2257        self.get_assertions(
2258            CallableTarget::Method { class, method },
2259            invocation,
2260            artifacts,
2261            source_file,
2262            codebase,
2263            session,
2264        )
2265    }
2266
2267    fn get_assertions(
2268        &self,
2269        target: CallableTarget<'_>,
2270        invocation: &Invocation<'_, '_, '_>,
2271        artifacts: &AnalysisArtifacts,
2272        source_file: &File,
2273        codebase: &CodebaseMetadata,
2274        session: &ExternalAnalysisSession,
2275    ) -> Result<Option<InvocationAssertions>, ExternalAnalyzerError> {
2276        let _lookup_trace =
2277            self.trace_enabled.then(|| LookupTrace { telemetry: &self.telemetry, started_at: Instant::now() });
2278        if self.trace_enabled {
2279            self.telemetry.assertion_lookups.fetch_add(1, Ordering::Relaxed);
2280        }
2281
2282        for backend in &self.backends {
2283            let matching_start = self.trace_enabled.then(Instant::now);
2284            let (indices, candidates) = match target {
2285                CallableTarget::Function(function) => backend.matching_function_assertion_providers(function),
2286                CallableTarget::Method { class, method } => {
2287                    backend.matching_method_assertion_providers(codebase, class, method)
2288                }
2289            };
2290            if self.trace_enabled {
2291                self.telemetry.backend_checks.fetch_add(1, Ordering::Relaxed);
2292                self.telemetry.candidate_providers.fetch_add(candidates as u64, Ordering::Relaxed);
2293            }
2294            if let Some(start) = matching_start {
2295                self.telemetry.matching_ns.fetch_add(duration_nanos(start.elapsed()), Ordering::Relaxed);
2296            }
2297
2298            let Some(indices) = indices else {
2299                continue;
2300            };
2301            let memoize = match target {
2302                CallableTarget::Function(_) => backend.memoizes_function_assertion_providers(indices.as_slice()),
2303                CallableTarget::Method { .. } => backend.memoizes_method_assertion_providers(indices.as_slice()),
2304            };
2305            if self.trace_enabled {
2306                self.telemetry.matched_providers.fetch_add(indices.as_slice().len() as u64, Ordering::Relaxed);
2307            }
2308
2309            let encode_start = self.trace_enabled.then(Instant::now);
2310            let provider_start = self.trace_enabled.then(Instant::now);
2311            let protocol_target = match target {
2312                CallableTarget::Function(function) => protocol::ProviderTarget::Function(function),
2313                CallableTarget::Method { class, method } => protocol::ProviderTarget::Method {
2314                    class: codebase.get_class_like(class).map_or(class, |metadata| metadata.original_name.as_bytes()),
2315                    method,
2316                },
2317            };
2318            let request = protocol::encode_provider_request(
2319                protocol::ProviderRequestKind::Assertion,
2320                indices.as_slice(),
2321                protocol_target,
2322                invocation,
2323                artifacts,
2324                source_file,
2325                session.generation(),
2326                memoize,
2327                self.trace_enabled,
2328            )?;
2329            if let Some(start) = encode_start {
2330                self.telemetry.encode_ns.fetch_add(duration_nanos(start.elapsed()), Ordering::Relaxed);
2331            }
2332
2333            let assertions = self.dispatch_assertion_request(backend, request, target.affinity(), codebase, session)?;
2334            if let Some(start) = provider_start
2335                && start.elapsed() >= SLOW_PROVIDER_THRESHOLD
2336            {
2337                tracing::trace!(
2338                    target = %target,
2339                    providers = indices.as_slice().len(),
2340                    elapsed = ?start.elapsed(),
2341                    provided = assertions.is_some(),
2342                    "Slow external assertion provider request completed."
2343                );
2344            }
2345            if assertions.is_some() {
2346                return Ok(assertions);
2347            }
2348        }
2349
2350        Ok(None)
2351    }
2352
2353    fn dispatch_callable_signature_request(
2354        &self,
2355        backend: &Backend<T>,
2356        mut request: protocol::ReturnTypeRequest<'_>,
2357        affinity: &[u8],
2358        codebase: &CodebaseMetadata,
2359        session: &ExternalAnalysisSession,
2360    ) -> Result<Option<EffectiveCallableSignature>, ExternalAnalyzerError> {
2361        if self.trace_enabled {
2362            self.telemetry.signature_requests.fetch_add(1, Ordering::Relaxed);
2363        }
2364        let response = self.exchange_provider_request(backend, &mut request, affinity, codebase, session)?;
2365        let result = self.decode_provider_response(|| {
2366            protocol::decode_callable_signature_response(&response, |handle| {
2367                protocol::resolve_type_handle(&request.types, handle)
2368            })
2369        })?;
2370
2371        if self.trace_enabled {
2372            if result.is_some() {
2373                self.telemetry.provided_signatures.fetch_add(1, Ordering::Relaxed);
2374            } else {
2375                self.telemetry.declined_requests.fetch_add(1, Ordering::Relaxed);
2376            }
2377        }
2378
2379        Ok(result)
2380    }
2381
2382    pub(crate) fn get_function_callable_signature(
2383        &self,
2384        function: &[u8],
2385        invocation: &Invocation<'_, '_, '_>,
2386        artifacts: &AnalysisArtifacts,
2387        source_file: &File,
2388        codebase: &CodebaseMetadata,
2389        session: &ExternalAnalysisSession,
2390    ) -> Result<Option<EffectiveCallableSignature>, ExternalAnalyzerError> {
2391        self.get_callable_signature(
2392            CallableTarget::Function(function),
2393            invocation,
2394            artifacts,
2395            source_file,
2396            codebase,
2397            session,
2398        )
2399    }
2400
2401    pub(crate) fn get_method_callable_signature(
2402        &self,
2403        class: &[u8],
2404        method: &[u8],
2405        invocation: &Invocation<'_, '_, '_>,
2406        artifacts: &AnalysisArtifacts,
2407        source_file: &File,
2408        codebase: &CodebaseMetadata,
2409        session: &ExternalAnalysisSession,
2410    ) -> Result<Option<EffectiveCallableSignature>, ExternalAnalyzerError> {
2411        self.get_callable_signature(
2412            CallableTarget::Method { class, method },
2413            invocation,
2414            artifacts,
2415            source_file,
2416            codebase,
2417            session,
2418        )
2419    }
2420
2421    fn get_callable_signature(
2422        &self,
2423        target: CallableTarget<'_>,
2424        invocation: &Invocation<'_, '_, '_>,
2425        artifacts: &AnalysisArtifacts,
2426        source_file: &File,
2427        codebase: &CodebaseMetadata,
2428        session: &ExternalAnalysisSession,
2429    ) -> Result<Option<EffectiveCallableSignature>, ExternalAnalyzerError> {
2430        let _lookup_trace =
2431            self.trace_enabled.then(|| LookupTrace { telemetry: &self.telemetry, started_at: Instant::now() });
2432        if self.trace_enabled {
2433            self.telemetry.signature_lookups.fetch_add(1, Ordering::Relaxed);
2434        }
2435
2436        for backend in &self.backends {
2437            let matching_start = self.trace_enabled.then(Instant::now);
2438            let (indices, candidates) = match target {
2439                CallableTarget::Function(function) => {
2440                    backend.matching_function_signature_providers(function, codebase.get_function(function).is_some())
2441                }
2442                CallableTarget::Method { class, method } => backend.matching_method_signature_providers(
2443                    codebase,
2444                    class,
2445                    method,
2446                    codebase.get_declaring_method(class, method).is_some(),
2447                ),
2448            };
2449            if self.trace_enabled {
2450                self.telemetry.backend_checks.fetch_add(1, Ordering::Relaxed);
2451                self.telemetry.candidate_providers.fetch_add(candidates as u64, Ordering::Relaxed);
2452            }
2453
2454            if let Some(start) = matching_start {
2455                self.telemetry.matching_ns.fetch_add(duration_nanos(start.elapsed()), Ordering::Relaxed);
2456            }
2457
2458            let Some(indices) = indices else {
2459                continue;
2460            };
2461            let memoize = match target {
2462                CallableTarget::Function(_) => backend.memoizes_function_providers(indices.as_slice()),
2463                CallableTarget::Method { .. } => backend.memoizes_method_providers(indices.as_slice()),
2464            };
2465
2466            if self.trace_enabled {
2467                self.telemetry.matched_providers.fetch_add(indices.as_slice().len() as u64, Ordering::Relaxed);
2468            }
2469
2470            let encode_start = self.trace_enabled.then(Instant::now);
2471            let provider_start = self.trace_enabled.then(Instant::now);
2472            let protocol_target = match target {
2473                CallableTarget::Function(function) => protocol::ProviderTarget::Function(function),
2474                CallableTarget::Method { class, method } => protocol::ProviderTarget::Method {
2475                    class: codebase.get_class_like(class).map_or(class, |metadata| metadata.original_name.as_bytes()),
2476                    method,
2477                },
2478            };
2479            let request = protocol::encode_provider_request(
2480                protocol::ProviderRequestKind::CallableSignature,
2481                indices.as_slice(),
2482                protocol_target,
2483                invocation,
2484                artifacts,
2485                source_file,
2486                session.generation(),
2487                memoize,
2488                self.trace_enabled,
2489            )
2490            .inspect_err(|_| self.record_error())?;
2491
2492            if let Some(start) = encode_start {
2493                self.telemetry.encode_ns.fetch_add(duration_nanos(start.elapsed()), Ordering::Relaxed);
2494            }
2495
2496            let signature =
2497                self.dispatch_callable_signature_request(backend, request, target.affinity(), codebase, session)?;
2498            if let Some(start) = provider_start
2499                && start.elapsed() >= SLOW_PROVIDER_THRESHOLD
2500            {
2501                tracing::trace!(
2502                    target = %target,
2503                    providers = indices.as_slice().len(),
2504                    elapsed = ?start.elapsed(),
2505                    provided = signature.is_some(),
2506                    "Slow external callable-signature provider request completed."
2507                );
2508            }
2509
2510            if let Some(signature) = signature {
2511                return Ok(Some(signature));
2512            }
2513        }
2514
2515        Ok(None)
2516    }
2517
2518    pub(crate) fn get_function_return_type(
2519        &self,
2520        function: &[u8],
2521        invocation: &Invocation<'_, '_, '_>,
2522        artifacts: &AnalysisArtifacts,
2523        source_file: &File,
2524        codebase: &CodebaseMetadata,
2525        session: &ExternalAnalysisSession,
2526    ) -> Result<Option<TUnion>, ExternalAnalyzerError> {
2527        self.get_return_type(CallableTarget::Function(function), invocation, artifacts, source_file, codebase, session)
2528    }
2529
2530    pub(crate) fn get_method_return_type(
2531        &self,
2532        class: &[u8],
2533        method: &[u8],
2534        invocation: &Invocation<'_, '_, '_>,
2535        artifacts: &AnalysisArtifacts,
2536        source_file: &File,
2537        codebase: &CodebaseMetadata,
2538        session: &ExternalAnalysisSession,
2539    ) -> Result<Option<TUnion>, ExternalAnalyzerError> {
2540        self.get_return_type(
2541            CallableTarget::Method { class, method },
2542            invocation,
2543            artifacts,
2544            source_file,
2545            codebase,
2546            session,
2547        )
2548    }
2549
2550    fn get_return_type(
2551        &self,
2552        target: CallableTarget<'_>,
2553        invocation: &Invocation<'_, '_, '_>,
2554        artifacts: &AnalysisArtifacts,
2555        source_file: &File,
2556        codebase: &CodebaseMetadata,
2557        session: &ExternalAnalysisSession,
2558    ) -> Result<Option<TUnion>, ExternalAnalyzerError> {
2559        let _lookup_trace =
2560            self.trace_enabled.then(|| LookupTrace { telemetry: &self.telemetry, started_at: Instant::now() });
2561        if self.trace_enabled {
2562            match target {
2563                CallableTarget::Function(_) => self.telemetry.function_lookups.fetch_add(1, Ordering::Relaxed),
2564                CallableTarget::Method { .. } => self.telemetry.method_lookups.fetch_add(1, Ordering::Relaxed),
2565            };
2566        }
2567
2568        let declared = match target {
2569            CallableTarget::Function(function) => codebase.get_function(function).is_some(),
2570            CallableTarget::Method { class, method } => codebase.get_declaring_method(class, method).is_some(),
2571        };
2572        let mut dispatched = false;
2573        for backend in &self.backends {
2574            let matching_start = self.trace_enabled.then(Instant::now);
2575            let (indices, candidates) = match target {
2576                CallableTarget::Function(function) => backend.matching_function_providers(function, declared),
2577                CallableTarget::Method { class, method } => {
2578                    backend.matching_method_providers(codebase, class, method, declared)
2579                }
2580            };
2581            if self.trace_enabled {
2582                self.telemetry.backend_checks.fetch_add(1, Ordering::Relaxed);
2583                self.telemetry.candidate_providers.fetch_add(candidates as u64, Ordering::Relaxed);
2584            }
2585
2586            if let Some(start) = matching_start {
2587                self.telemetry.matching_ns.fetch_add(duration_nanos(start.elapsed()), Ordering::Relaxed);
2588            }
2589
2590            let Some(indices) = indices else {
2591                continue;
2592            };
2593            let memoize = match target {
2594                CallableTarget::Function(_) => backend.memoizes_function_providers(indices.as_slice()),
2595                CallableTarget::Method { .. } => backend.memoizes_method_providers(indices.as_slice()),
2596            };
2597            dispatched = true;
2598            let provider_start = self.trace_enabled.then(Instant::now);
2599            if self.trace_enabled {
2600                self.telemetry.matched_providers.fetch_add(indices.as_slice().len() as u64, Ordering::Relaxed);
2601            }
2602
2603            let encode_start = self.trace_enabled.then(Instant::now);
2604            let protocol_target = match target {
2605                CallableTarget::Function(function) => protocol::ProviderTarget::Function(function),
2606                CallableTarget::Method { class, method } => protocol::ProviderTarget::Method {
2607                    class: codebase.get_class_like(class).map_or(class, |metadata| metadata.original_name.as_bytes()),
2608                    method,
2609                },
2610            };
2611            let mut request = protocol::encode_provider_request(
2612                protocol::ProviderRequestKind::ReturnType,
2613                indices.as_slice(),
2614                protocol_target,
2615                invocation,
2616                artifacts,
2617                source_file,
2618                session.generation(),
2619                memoize,
2620                self.trace_enabled,
2621            )
2622            .inspect_err(|_| self.record_error())?;
2623
2624            if let Some(start) = encode_start {
2625                self.telemetry.encode_ns.fetch_add(duration_nanos(start.elapsed()), Ordering::Relaxed);
2626            }
2627            let response =
2628                self.exchange_provider_request(backend, &mut request, target.affinity(), codebase, session)?;
2629            let result = self.decode_provider_response(|| {
2630                protocol::decode_return_type_response(&response, |handle| {
2631                    protocol::resolve_type_handle(&request.types, handle)
2632                })
2633            })?;
2634
2635            if let Some(start) = provider_start
2636                && start.elapsed() >= SLOW_PROVIDER_THRESHOLD
2637            {
2638                tracing::trace!(
2639                    target = %target,
2640                    providers = indices.as_slice().len(),
2641                    arguments = request.arguments,
2642                    typed_arguments = request.typed_arguments,
2643                    argument_types = request.types.len().saturating_sub(request.receiver_type_count),
2644                    response_bytes = response.len(),
2645                    elapsed = ?start.elapsed(),
2646                    provided = result.is_some(),
2647                    "Slow external return-type provider request completed."
2648                );
2649            }
2650
2651            if let Some(result) = result {
2652                if self.trace_enabled {
2653                    self.telemetry.provided_types.fetch_add(1, Ordering::Relaxed);
2654                }
2655                return Ok(Some(result));
2656            }
2657
2658            if self.trace_enabled {
2659                self.telemetry.declined_requests.fetch_add(1, Ordering::Relaxed);
2660            }
2661        }
2662
2663        if self.trace_enabled && !dispatched {
2664            self.telemetry.unmatched_lookups.fetch_add(1, Ordering::Relaxed);
2665        }
2666
2667        Ok(None)
2668    }
2669
2670    pub(crate) fn get_property_type(
2671        &self,
2672        class: &[u8],
2673        property: &[u8],
2674        access: PropertyAccessKind,
2675        receiver_type: &TUnion,
2676        span: Span,
2677        codebase: &CodebaseMetadata,
2678        session: &ExternalAnalysisSession,
2679    ) -> Result<Option<EffectivePropertyType>, ExternalAnalyzerError> {
2680        let _lookup_trace =
2681            self.trace_enabled.then(|| LookupTrace { telemetry: &self.telemetry, started_at: Instant::now() });
2682        if self.trace_enabled {
2683            self.telemetry.property_lookups.fetch_add(1, Ordering::Relaxed);
2684        }
2685
2686        let mut dispatched = false;
2687        for backend in &self.backends {
2688            let matching_start = self.trace_enabled.then(Instant::now);
2689            let (indices, candidates) = backend.matching_property_providers(codebase, class, property);
2690            if self.trace_enabled {
2691                self.telemetry.backend_checks.fetch_add(1, Ordering::Relaxed);
2692                self.telemetry.candidate_providers.fetch_add(candidates as u64, Ordering::Relaxed);
2693            }
2694            if let Some(start) = matching_start {
2695                self.telemetry.matching_ns.fetch_add(duration_nanos(start.elapsed()), Ordering::Relaxed);
2696            }
2697
2698            let Some(indices) = indices else {
2699                continue;
2700            };
2701            dispatched = true;
2702            let provider_start = self.trace_enabled.then(Instant::now);
2703            if self.trace_enabled {
2704                self.telemetry.requests.fetch_add(1, Ordering::Relaxed);
2705                self.telemetry.matched_providers.fetch_add(indices.as_slice().len() as u64, Ordering::Relaxed);
2706            }
2707
2708            let original_class =
2709                codebase.get_class_like(class).map_or(class, |metadata| metadata.original_name.as_bytes());
2710            let encode_start = self.trace_enabled.then(Instant::now);
2711            let request = protocol::encode_property_type_request(
2712                indices.as_slice(),
2713                original_class,
2714                property,
2715                access,
2716                receiver_type,
2717                span,
2718                session.generation(),
2719                self.trace_enabled,
2720            )
2721            .inspect_err(|_| self.record_error())?;
2722            if let Some(start) = encode_start {
2723                self.telemetry.encode_ns.fetch_add(duration_nanos(start.elapsed()), Ordering::Relaxed);
2724                self.telemetry.snapshotted_types.fetch_add(request.snapshotted_types as u64, Ordering::Relaxed);
2725                self.telemetry
2726                    .type_snapshot_ns
2727                    .fetch_add(duration_nanos(request.type_snapshot_duration), Ordering::Relaxed);
2728                self.telemetry.request_bytes.fetch_add(request.payload.len() as u64, Ordering::Relaxed);
2729            }
2730
2731            let response =
2732                self.exchange_provider_payload(backend, request.payload, class, codebase, session, |handle| {
2733                    protocol::resolve_type_handle(&request.types, handle)
2734                })?;
2735
2736            let decode_start = self.trace_enabled.then(Instant::now);
2737            let result = protocol::decode_property_type_response(&response, |handle| {
2738                protocol::resolve_type_handle(&request.types, handle)
2739            })
2740            .inspect_err(|_| self.record_error())?;
2741            if let Some(start) = decode_start {
2742                self.telemetry.decode_ns.fetch_add(duration_nanos(start.elapsed()), Ordering::Relaxed);
2743            }
2744
2745            if let Some(start) = provider_start
2746                && start.elapsed() >= SLOW_PROVIDER_THRESHOLD
2747            {
2748                tracing::trace!(
2749                    class = %mago_bytes::BytesDisplay(class),
2750                    property = %mago_bytes::BytesDisplay(property),
2751                    access = ?access,
2752                    providers = indices.as_slice().len(),
2753                    receiver_types = request.types.len(),
2754                    response_bytes = response.len(),
2755                    elapsed = ?start.elapsed(),
2756                    provided = result.is_some(),
2757                    "Slow external property type provider request completed."
2758                );
2759            }
2760
2761            if let Some(result) = result {
2762                if self.trace_enabled {
2763                    self.telemetry.provided_types.fetch_add(1, Ordering::Relaxed);
2764                }
2765                return Ok(Some(result));
2766            }
2767            if self.trace_enabled {
2768                self.telemetry.declined_requests.fetch_add(1, Ordering::Relaxed);
2769            }
2770        }
2771
2772        if self.trace_enabled && !dispatched {
2773            self.telemetry.unmatched_lookups.fetch_add(1, Ordering::Relaxed);
2774        }
2775
2776        Ok(None)
2777    }
2778
2779    pub(crate) fn is_property_initialized(
2780        &self,
2781        declaring_class: &[u8],
2782        property: &mago_codex::metadata::property::PropertyMetadata,
2783        codebase: &CodebaseMetadata,
2784        session: &ExternalAnalysisSession,
2785    ) -> Result<bool, ExternalAnalyzerError> {
2786        let property_name = property.name.0.as_bytes().strip_prefix(b"$").unwrap_or(property.name.0.as_bytes());
2787        let _lookup_trace =
2788            self.trace_enabled.then(|| LookupTrace { telemetry: &self.telemetry, started_at: Instant::now() });
2789        if self.trace_enabled {
2790            self.telemetry.property_initialization_lookups.fetch_add(1, Ordering::Relaxed);
2791        }
2792
2793        let mut dispatched = false;
2794        for backend in &self.backends {
2795            let matching_start = self.trace_enabled.then(Instant::now);
2796            let (indices, candidates) =
2797                backend.matching_property_initialization_providers(codebase, declaring_class, property_name);
2798            if self.trace_enabled {
2799                self.telemetry.backend_checks.fetch_add(1, Ordering::Relaxed);
2800                self.telemetry.candidate_providers.fetch_add(candidates as u64, Ordering::Relaxed);
2801            }
2802            if let Some(start) = matching_start {
2803                self.telemetry.matching_ns.fetch_add(duration_nanos(start.elapsed()), Ordering::Relaxed);
2804            }
2805
2806            let Some(indices) = indices else {
2807                continue;
2808            };
2809            dispatched = true;
2810            let provider_start = self.trace_enabled.then(Instant::now);
2811            if self.trace_enabled {
2812                self.telemetry.requests.fetch_add(1, Ordering::Relaxed);
2813                self.telemetry.matched_providers.fetch_add(indices.as_slice().len() as u64, Ordering::Relaxed);
2814            }
2815
2816            let original_class = codebase
2817                .get_class_like(declaring_class)
2818                .map_or(declaring_class, |metadata| metadata.original_name.as_bytes());
2819            let encode_start = self.trace_enabled.then(Instant::now);
2820            let request = protocol::encode_property_initialization_request(
2821                indices.as_slice(),
2822                original_class,
2823                property,
2824                session.generation(),
2825                session,
2826            )
2827            .inspect_err(|_| self.record_error())?;
2828
2829            if let Some(start) = encode_start {
2830                self.telemetry.encode_ns.fetch_add(duration_nanos(start.elapsed()), Ordering::Relaxed);
2831                self.telemetry.request_bytes.fetch_add(request.len() as u64, Ordering::Relaxed);
2832            }
2833
2834            let response =
2835                self.exchange_provider_payload(backend, request, declaring_class, codebase, session, |_| None)?;
2836
2837            let decode_start = self.trace_enabled.then(Instant::now);
2838            let initialized =
2839                protocol::decode_property_initialization_response(&response).inspect_err(|_| self.record_error())?;
2840
2841            if let Some(start) = decode_start {
2842                self.telemetry.decode_ns.fetch_add(duration_nanos(start.elapsed()), Ordering::Relaxed);
2843            }
2844
2845            if let Some(start) = provider_start
2846                && start.elapsed() >= SLOW_PROVIDER_THRESHOLD
2847            {
2848                tracing::trace!(
2849                    class = %mago_bytes::BytesDisplay(declaring_class),
2850                    property = %mago_bytes::BytesDisplay(property_name),
2851                    providers = indices.as_slice().len(),
2852                    response_bytes = response.len(),
2853                    elapsed = ?start.elapsed(),
2854                    initialized,
2855                    "Slow external property initialization provider request completed."
2856                );
2857            }
2858
2859            if initialized {
2860                if self.trace_enabled {
2861                    self.telemetry.initialized_properties.fetch_add(1, Ordering::Relaxed);
2862                }
2863
2864                return Ok(true);
2865            }
2866
2867            if self.trace_enabled {
2868                self.telemetry.declined_requests.fetch_add(1, Ordering::Relaxed);
2869            }
2870        }
2871
2872        if self.trace_enabled && !dispatched {
2873            self.telemetry.unmatched_lookups.fetch_add(1, Ordering::Relaxed);
2874        }
2875
2876        Ok(false)
2877    }
2878
2879    pub(crate) fn get_class_initializers(
2880        &self,
2881        class: &ClassLikeMetadata,
2882        codebase: &CodebaseMetadata,
2883        session: &ExternalAnalysisSession,
2884    ) -> Result<WordSet, ExternalAnalyzerError> {
2885        let _lookup_trace =
2886            self.trace_enabled.then(|| LookupTrace { telemetry: &self.telemetry, started_at: Instant::now() });
2887        if self.trace_enabled {
2888            self.telemetry.class_initializer_lookups.fetch_add(1, Ordering::Relaxed);
2889        }
2890
2891        let mut initializers = WordSet::default();
2892        let mut dispatched = false;
2893        for backend in &self.backends {
2894            let matching_start = self.trace_enabled.then(Instant::now);
2895            let (indices, candidates) = backend.matching_class_initializer_providers(codebase, class.name.as_bytes());
2896            if self.trace_enabled {
2897                self.telemetry.backend_checks.fetch_add(1, Ordering::Relaxed);
2898                self.telemetry.candidate_providers.fetch_add(candidates as u64, Ordering::Relaxed);
2899            }
2900            if let Some(start) = matching_start {
2901                self.telemetry.matching_ns.fetch_add(duration_nanos(start.elapsed()), Ordering::Relaxed);
2902            }
2903
2904            let Some(indices) = indices else {
2905                continue;
2906            };
2907            dispatched = true;
2908            let provider_start = self.trace_enabled.then(Instant::now);
2909            if self.trace_enabled {
2910                self.telemetry.requests.fetch_add(1, Ordering::Relaxed);
2911                self.telemetry.matched_providers.fetch_add(indices.as_slice().len() as u64, Ordering::Relaxed);
2912            }
2913
2914            let encode_start = self.trace_enabled.then(Instant::now);
2915            let request =
2916                protocol::encode_class_initializer_request(indices.as_slice(), class, session.generation(), session)
2917                    .inspect_err(|_| self.record_error())?;
2918            if let Some(start) = encode_start {
2919                self.telemetry.encode_ns.fetch_add(duration_nanos(start.elapsed()), Ordering::Relaxed);
2920                self.telemetry.request_bytes.fetch_add(request.len() as u64, Ordering::Relaxed);
2921            }
2922
2923            let response =
2924                self.exchange_provider_payload(backend, request, class.name.as_bytes(), codebase, session, |_| None)?;
2925
2926            let decode_start = self.trace_enabled.then(Instant::now);
2927            let provided =
2928                protocol::decode_class_initializer_response(&response).inspect_err(|_| self.record_error())?;
2929            if let Some(start) = decode_start {
2930                self.telemetry.decode_ns.fetch_add(duration_nanos(start.elapsed()), Ordering::Relaxed);
2931            }
2932            if self.trace_enabled {
2933                self.telemetry.provided_class_initializers.fetch_add(provided.len() as u64, Ordering::Relaxed);
2934                if provided.is_empty() {
2935                    self.telemetry.declined_requests.fetch_add(1, Ordering::Relaxed);
2936                }
2937            }
2938
2939            if let Some(start) = provider_start
2940                && start.elapsed() >= SLOW_PROVIDER_THRESHOLD
2941            {
2942                tracing::trace!(
2943                    class = %class.original_name,
2944                    providers = indices.as_slice().len(),
2945                    initializers = provided.len(),
2946                    response_bytes = response.len(),
2947                    elapsed = ?start.elapsed(),
2948                    "Slow external class initializer provider request completed."
2949                );
2950            }
2951
2952            initializers.extend(provided);
2953        }
2954
2955        if self.trace_enabled && !dispatched {
2956            self.telemetry.unmatched_lookups.fetch_add(1, Ordering::Relaxed);
2957        }
2958
2959        Ok(initializers)
2960    }
2961
2962    fn initialize_transports(
2963        transports: impl IntoIterator<Item = Arc<T>>,
2964        php_version: PHPVersion,
2965        enabled_plugins: &[String],
2966        disable_defaults: bool,
2967    ) -> Result<Self, ExternalAnalyzerError> {
2968        let trace_enabled = tracing::enabled!(tracing::Level::TRACE);
2969        let started_at = trace_enabled.then(Instant::now);
2970        tracing::trace!(
2971            php_version = %php_version,
2972            explicitly_enabled_plugins = enabled_plugins.len(),
2973            disable_defaults,
2974            "Initializing external analyzer registrations."
2975        );
2976
2977        let describe = protocol::encode_describe_request(php_version);
2978        let mut backends = Vec::new();
2979        let mut extensions = Vec::new();
2980        let mut initialization_stubs = Vec::new();
2981        let mut extension_identifiers = HashSet::new();
2982        let mut plugin_selectors = HashMap::new();
2983        for plugin in available_plugins() {
2984            for selector in std::iter::once(plugin.id).chain(plugin.aliases.iter().copied()) {
2985                let selector = selector.to_ascii_lowercase();
2986                if let Some(first) = plugin_selectors.insert(selector.clone(), plugin.id.to_string()) {
2987                    return Err(ExternalAnalyzerError::DuplicatePluginSelector {
2988                        selector,
2989                        first,
2990                        second: plugin.id.to_string(),
2991                    });
2992                }
2993            }
2994        }
2995
2996        for (backend_index, transport) in transports.into_iter().enumerate() {
2997            let backend_start = trace_enabled.then(Instant::now);
2998            tracing::trace!(
2999                backend = backend_index,
3000                request_bytes = describe.len(),
3001                "Describing external analyzer backend."
3002            );
3003
3004            let responses = transport.broadcast(&describe)?;
3005            let response_bytes = responses.iter().map(Vec::len).sum::<usize>();
3006            let mut decoded = responses.iter().map(|response| protocol::decode_registration(response));
3007            let Some(first) = decoded.next() else {
3008                return Err(error::protocol("worker pool returned no analyzer registration responses"));
3009            };
3010
3011            let mut registration = first?;
3012            for response in decoded {
3013                if response? != registration {
3014                    return Err(ExternalAnalyzerError::InconsistentRegistration);
3015                }
3016            }
3017
3018            for extension in &registration.extensions {
3019                if !extension_identifiers.insert(extension.identifier.to_ascii_lowercase()) {
3020                    return Err(ExternalAnalyzerError::DuplicateExtension(extension.identifier.clone()));
3021                }
3022            }
3023
3024            for plugin in &registration.plugins {
3025                for selector in
3026                    std::iter::once(plugin.identifier.as_str()).chain(plugin.aliases.iter().map(String::as_str))
3027                {
3028                    let selector = selector.to_ascii_lowercase();
3029                    if let Some(first) = plugin_selectors.insert(selector.clone(), plugin.identifier.clone()) {
3030                        return Err(ExternalAnalyzerError::DuplicatePluginSelector {
3031                            selector,
3032                            first,
3033                            second: plugin.identifier.clone(),
3034                        });
3035                    }
3036                }
3037            }
3038
3039            let enabled = registration
3040                .plugins
3041                .iter()
3042                .filter(|plugin| {
3043                    enabled_plugins.iter().any(|name| plugin.matches(name))
3044                        || (!disable_defaults && plugin.default_enabled)
3045                })
3046                .map(|plugin| plugin.index)
3047                .collect::<HashSet<_>>();
3048
3049            let advertised_function_providers = registration.function_providers.len();
3050            let advertised_method_providers = registration.method_providers.len();
3051            let advertised_function_assertion_providers = registration.function_assertion_providers.len();
3052            let advertised_method_assertion_providers = registration.method_assertion_providers.len();
3053            let advertised_property_providers = registration.property_providers.len();
3054            let advertised_property_initialization_providers = registration.property_initialization_providers.len();
3055            let advertised_class_initializer_providers = registration.class_initializer_providers.len();
3056            let advertised_entry_points = registration.entry_points.len();
3057            let advertised_attributed_entry_points = registration.attributed_entry_points.len();
3058            let advertised_issue_filter_hooks = registration.issue_filter_hooks.len();
3059            let advertised_node_analysis_hooks = registration.node_analysis_hooks.len();
3060            let advertised_method_call_analysis_hooks = registration.method_call_analysis_hooks.len();
3061            let advertised_class_like_analysis_hooks = registration.class_like_analysis_hooks.len();
3062            let advertised_codebase_scan_hooks = registration.codebase_scan_hooks.len();
3063            registration.function_providers.retain(|provider| enabled.contains(&provider.plugin));
3064            registration.method_providers.retain(|provider| enabled.contains(&provider.plugin));
3065            registration.function_assertion_providers.retain(|provider| enabled.contains(&provider.plugin));
3066            registration.method_assertion_providers.retain(|provider| enabled.contains(&provider.plugin));
3067            registration.property_providers.retain(|provider| enabled.contains(&provider.plugin));
3068            registration.property_initialization_providers.retain(|provider| enabled.contains(&provider.plugin));
3069            registration.class_initializer_providers.retain(|provider| enabled.contains(&provider.plugin));
3070            registration.entry_points.retain(|entry_point| enabled.contains(&entry_point.plugin));
3071            registration.attributed_entry_points.retain(|entry_point| enabled.contains(&entry_point.plugin));
3072            registration.issue_filter_hooks.retain(|hook| enabled.contains(&hook.plugin));
3073            registration.node_analysis_hooks.retain(|hook| enabled.contains(&hook.plugin));
3074            registration.method_call_analysis_hooks.retain(|hook| enabled.contains(&hook.plugin));
3075            registration.class_like_analysis_hooks.retain(|hook| enabled.contains(&hook.plugin));
3076            registration.codebase_scan_hooks.retain(|hook| enabled.contains(&hook.plugin));
3077            let backend_route = u16::try_from(backend_index)
3078                .map_err(|_| error::protocol("more than 65,536 external analyzer backends were configured"))?;
3079            for hook in &mut registration.method_call_analysis_hooks {
3080                hook.route = (u32::from(backend_route) << 16) | u32::from(hook.index);
3081            }
3082            for hook in &mut registration.class_like_analysis_hooks {
3083                hook.route = (u32::from(backend_route) << 16) | u32::from(hook.index);
3084            }
3085            registration.initialization_plugins.retain(|index| enabled.contains(index));
3086            registration.before_analysis_plugins.retain(|index| enabled.contains(index));
3087            registration.after_file_analysis_plugins.retain(|index| enabled.contains(index));
3088            registration.node_analysis_plugins.retain(|index| enabled.contains(index));
3089            registration.after_analysis_plugins.retain(|index| enabled.contains(index));
3090
3091            if !registration.initialization_plugins.is_empty() {
3092                let initialize = protocol::encode_initialization_request(&registration.initialization_plugins)?;
3093                let responses = transport.broadcast(&initialize)?;
3094                let mut decoded = responses.iter().map(|response| {
3095                    protocol::decode_initialization_response(response, &registration.initialization_plugins)
3096                });
3097
3098                let Some(first) = decoded.next() else {
3099                    return Err(error::protocol("worker pool returned no analyzer initialization responses"));
3100                };
3101
3102                let initialized = first?;
3103                for response in decoded {
3104                    if response? != initialized {
3105                        return Err(ExternalAnalyzerError::InconsistentInitialization);
3106                    }
3107                }
3108
3109                for stub in initialized {
3110                    let plugin = registration.plugins.get(usize::from(stub.plugin)).ok_or_else(|| {
3111                        error::protocol(format!("initialization references unknown plugin index {}", stub.plugin))
3112                    })?;
3113
3114                    initialization_stubs.push(ExternalStub::new(
3115                        &plugin.extension,
3116                        &plugin.identifier,
3117                        &stub.filename,
3118                        stub.contents,
3119                    ));
3120                }
3121            }
3122
3123            extensions.extend(registration.extensions.iter().cloned());
3124            if let Some(start) = backend_start {
3125                tracing::trace!(
3126                    backend = backend_index,
3127                    workers = responses.len(),
3128                    response_bytes,
3129                    extensions = registration.extensions.len(),
3130                    plugins = registration.plugins.len(),
3131                    initialization_plugins = registration.initialization_plugins.len(),
3132                    enabled_plugins = enabled.len(),
3133                    function_providers = registration.function_providers.len(),
3134                    disabled_function_providers = advertised_function_providers - registration.function_providers.len(),
3135                    method_providers = registration.method_providers.len(),
3136                    disabled_method_providers = advertised_method_providers - registration.method_providers.len(),
3137                    function_assertion_providers = registration.function_assertion_providers.len(),
3138                    disabled_function_assertion_providers = advertised_function_assertion_providers
3139                        - registration.function_assertion_providers.len(),
3140                    method_assertion_providers = registration.method_assertion_providers.len(),
3141                    disabled_method_assertion_providers = advertised_method_assertion_providers
3142                        - registration.method_assertion_providers.len(),
3143                    property_providers = registration.property_providers.len(),
3144                    disabled_property_providers = advertised_property_providers - registration.property_providers.len(),
3145                    property_initialization_providers = registration.property_initialization_providers.len(),
3146                    disabled_property_initialization_providers = advertised_property_initialization_providers
3147                        - registration.property_initialization_providers.len(),
3148                    class_initializer_providers = registration.class_initializer_providers.len(),
3149                    disabled_class_initializer_providers = advertised_class_initializer_providers
3150                        - registration.class_initializer_providers.len(),
3151                    entry_points = registration.entry_points.len(),
3152                    disabled_entry_points = advertised_entry_points - registration.entry_points.len(),
3153                    attributed_entry_points = registration.attributed_entry_points.len(),
3154                    disabled_attributed_entry_points = advertised_attributed_entry_points
3155                        - registration.attributed_entry_points.len(),
3156                    issue_filter_hooks = registration.issue_filter_hooks.len(),
3157                    disabled_issue_filter_hooks = advertised_issue_filter_hooks - registration.issue_filter_hooks.len(),
3158                    node_analysis_hooks = registration.node_analysis_hooks.len(),
3159                    disabled_node_analysis_hooks = advertised_node_analysis_hooks - registration.node_analysis_hooks.len(),
3160                    method_call_analysis_hooks = registration.method_call_analysis_hooks.len(),
3161                    disabled_method_call_analysis_hooks = advertised_method_call_analysis_hooks
3162                        - registration.method_call_analysis_hooks.len(),
3163                    class_like_analysis_hooks = registration.class_like_analysis_hooks.len(),
3164                    disabled_class_like_analysis_hooks = advertised_class_like_analysis_hooks
3165                        - registration.class_like_analysis_hooks.len(),
3166                    codebase_scan_hooks = registration.codebase_scan_hooks.len(),
3167                    disabled_codebase_scan_hooks = advertised_codebase_scan_hooks
3168                        - registration.codebase_scan_hooks.len(),
3169                    before_analysis_plugins = registration.before_analysis_plugins.len(),
3170                    after_file_analysis_plugins = registration.after_file_analysis_plugins.len(),
3171                    after_analysis_plugins = registration.after_analysis_plugins.len(),
3172                    elapsed = ?start.elapsed(),
3173                    "External analyzer backend registered."
3174                );
3175            }
3176
3177            if !registration.after_file_analysis_plugins.is_empty() || !registration.node_analysis_plugins.is_empty() {
3178                transport.prepare_capacity();
3179            }
3180
3181            backends.push(Backend::new(transport, registration));
3182        }
3183
3184        let analyzer = Self {
3185            backends: backends.into_boxed_slice(),
3186            extensions: extensions.into_boxed_slice(),
3187            initialization_stubs: initialization_stubs.into_boxed_slice(),
3188            trace_enabled,
3189            telemetry: ExternalAnalyzerTelemetry::default(),
3190            started_at,
3191        };
3192
3193        if let Some(start) = started_at {
3194            let total = |count: fn(&Registration) -> usize| {
3195                analyzer.backends.iter().map(|backend| count(&backend.registration)).sum::<usize>()
3196            };
3197
3198            tracing::trace!(
3199                backends = analyzer.backends.len(),
3200                extensions = analyzer.extensions.len(),
3201                plugins = analyzer.extensions.iter().map(|extension| extension.plugins.len()).sum::<usize>(),
3202                function_providers = total(|registration| registration.function_providers.len()),
3203                method_providers = total(|registration| registration.method_providers.len()),
3204                property_providers = total(|registration| registration.property_providers.len()),
3205                property_initialization_providers =
3206                    total(|registration| registration.property_initialization_providers.len()),
3207                class_initializer_providers = total(|registration| registration.class_initializer_providers.len()),
3208                entry_points = total(|registration| registration.entry_points.len()),
3209                attributed_entry_points = total(|registration| registration.attributed_entry_points.len()),
3210                issue_filter_hooks = total(|registration| registration.issue_filter_hooks.len()),
3211                node_analysis_hooks = total(|registration| registration.node_analysis_hooks.len()),
3212                method_call_analysis_hooks = total(|registration| registration.method_call_analysis_hooks.len()),
3213                class_like_analysis_hooks = total(|registration| registration.class_like_analysis_hooks.len()),
3214                before_analysis_plugins = total(|registration| registration.before_analysis_plugins.len()),
3215                after_file_analysis_plugins = total(|registration| registration.after_file_analysis_plugins.len()),
3216                after_analysis_plugins = total(|registration| registration.after_analysis_plugins.len()),
3217                elapsed = ?start.elapsed(),
3218                "External analyzer initialized."
3219            );
3220        }
3221
3222        Ok(analyzer)
3223    }
3224}
3225
3226impl<T> Drop for ExternalAnalyzer<T> {
3227    fn drop(&mut self) {
3228        if !self.trace_enabled {
3229            return;
3230        }
3231
3232        let function_lookups = self.telemetry.function_lookups.load(Ordering::Relaxed);
3233        let method_lookups = self.telemetry.method_lookups.load(Ordering::Relaxed);
3234        let property_lookups = self.telemetry.property_lookups.load(Ordering::Relaxed);
3235        let property_initialization_lookups = self.telemetry.property_initialization_lookups.load(Ordering::Relaxed);
3236        let class_initializer_lookups = self.telemetry.class_initializer_lookups.load(Ordering::Relaxed);
3237        let signature_lookups = self.telemetry.signature_lookups.load(Ordering::Relaxed);
3238        let assertion_lookups = self.telemetry.assertion_lookups.load(Ordering::Relaxed);
3239        let lookups = function_lookups
3240            .saturating_add(method_lookups)
3241            .saturating_add(property_lookups)
3242            .saturating_add(property_initialization_lookups)
3243            .saturating_add(class_initializer_lookups)
3244            .saturating_add(signature_lookups)
3245            .saturating_add(assertion_lookups);
3246        let requests = self.telemetry.requests.load(Ordering::Relaxed);
3247        let ipc_requests = self.telemetry.ipc_requests.load(Ordering::Relaxed);
3248        let nested_requests = self.telemetry.nested_requests.load(Ordering::Relaxed);
3249        let comparison_batches = self.telemetry.comparison_batches.load(Ordering::Relaxed);
3250        let comparisons = self.telemetry.comparisons.load(Ordering::Relaxed);
3251        let metadata_queries = self.telemetry.metadata_queries.load(Ordering::Relaxed);
3252        let analysis_queries = self.telemetry.analysis_queries.load(Ordering::Relaxed);
3253        let symbol_reference_queries = self.telemetry.symbol_reference_queries.load(Ordering::Relaxed);
3254        let lifecycle_requests = self
3255            .telemetry
3256            .before_analysis_requests
3257            .load(Ordering::Relaxed)
3258            .saturating_add(self.telemetry.after_file_analysis_requests.load(Ordering::Relaxed))
3259            .saturating_add(self.telemetry.after_analysis_requests.load(Ordering::Relaxed));
3260        tracing::trace!(
3261            function_lookups,
3262            method_lookups,
3263            property_lookups,
3264            property_initialization_lookups,
3265            class_initializer_lookups,
3266            signature_lookups,
3267            assertion_lookups,
3268            backend_checks = self.telemetry.backend_checks.load(Ordering::Relaxed),
3269            candidate_providers = self.telemetry.candidate_providers.load(Ordering::Relaxed),
3270            matched_providers = self.telemetry.matched_providers.load(Ordering::Relaxed),
3271            unmatched_lookups = self.telemetry.unmatched_lookups.load(Ordering::Relaxed),
3272            "External analyzer matching summary."
3273        );
3274        tracing::trace!(
3275            requests,
3276            ipc_requests,
3277            provider_cache_hits = self.telemetry.provider_cache_hits.load(Ordering::Relaxed),
3278            signature_requests = self.telemetry.signature_requests.load(Ordering::Relaxed),
3279            assertion_requests = self.telemetry.assertion_requests.load(Ordering::Relaxed),
3280            provided_types = self.telemetry.provided_types.load(Ordering::Relaxed),
3281            initialized_properties = self.telemetry.initialized_properties.load(Ordering::Relaxed),
3282            provided_class_initializers = self.telemetry.provided_class_initializers.load(Ordering::Relaxed),
3283            provided_signatures = self.telemetry.provided_signatures.load(Ordering::Relaxed),
3284            provided_assertions = self.telemetry.provided_assertions.load(Ordering::Relaxed),
3285            declined_requests = self.telemetry.declined_requests.load(Ordering::Relaxed),
3286            errors = self.telemetry.errors.load(Ordering::Relaxed),
3287            arguments = self.telemetry.arguments.load(Ordering::Relaxed),
3288            typed_arguments = self.telemetry.typed_arguments.load(Ordering::Relaxed),
3289            snapshotted_types = self.telemetry.snapshotted_types.load(Ordering::Relaxed),
3290            request_bytes = self.telemetry.request_bytes.load(Ordering::Relaxed),
3291            response_bytes = self.telemetry.response_bytes.load(Ordering::Relaxed),
3292            "External analyzer provider summary."
3293        );
3294
3295        tracing::trace!(
3296            nested_requests,
3297            nested_errors = self.telemetry.nested_errors.load(Ordering::Relaxed),
3298            nested_request_bytes = self.telemetry.nested_request_bytes.load(Ordering::Relaxed),
3299            nested_response_bytes = self.telemetry.nested_response_bytes.load(Ordering::Relaxed),
3300            comparison_batches,
3301            comparisons,
3302            metadata_queries,
3303            analysis_queries,
3304            symbol_reference_queries,
3305            "External analyzer nested-query summary."
3306        );
3307
3308        tracing::trace!(
3309            before_analysis_requests = self.telemetry.before_analysis_requests.load(Ordering::Relaxed),
3310            after_file_analysis_requests = self.telemetry.after_file_analysis_requests.load(Ordering::Relaxed),
3311            after_file_analysis_files = self.telemetry.after_file_analysis_files.load(Ordering::Relaxed),
3312            after_analysis_requests = self.telemetry.after_analysis_requests.load(Ordering::Relaxed),
3313            lifecycle_plugins = self.telemetry.lifecycle_plugins.load(Ordering::Relaxed),
3314            lifecycle_issues = self.telemetry.lifecycle_issues.load(Ordering::Relaxed),
3315            lifecycle_errors = self.telemetry.lifecycle_errors.load(Ordering::Relaxed),
3316            lifecycle_request_bytes = self.telemetry.lifecycle_request_bytes.load(Ordering::Relaxed),
3317            lifecycle_response_bytes = self.telemetry.lifecycle_response_bytes.load(Ordering::Relaxed),
3318            "External analyzer lifecycle summary."
3319        );
3320
3321        let issue_filter_batches = self.telemetry.issue_filter_batches.load(Ordering::Relaxed);
3322        tracing::trace!(
3323            issue_filter_batches,
3324            candidates = self.telemetry.issue_filter_candidates.load(Ordering::Relaxed),
3325            removed = self.telemetry.issue_filter_removed.load(Ordering::Relaxed),
3326            errors = self.telemetry.issue_filter_errors.load(Ordering::Relaxed),
3327            request_bytes = self.telemetry.issue_filter_request_bytes.load(Ordering::Relaxed),
3328            response_bytes = self.telemetry.issue_filter_response_bytes.load(Ordering::Relaxed),
3329            "External analyzer issue-filter summary."
3330        );
3331
3332        tracing::trace!(
3333            matching_ms = nanos_millis(self.telemetry.matching_ns.load(Ordering::Relaxed)),
3334            encode_ms = nanos_millis(self.telemetry.encode_ns.load(Ordering::Relaxed)),
3335            type_snapshot_ms = nanos_millis(self.telemetry.type_snapshot_ns.load(Ordering::Relaxed)),
3336            ipc_ms = nanos_millis(self.telemetry.ipc_ns.load(Ordering::Relaxed)),
3337            comparison_ms = nanos_millis(self.telemetry.comparison_ns.load(Ordering::Relaxed)),
3338            metadata_query_ms = nanos_millis(self.telemetry.metadata_query_ns.load(Ordering::Relaxed)),
3339            analysis_query_ms = nanos_millis(self.telemetry.analysis_query_ns.load(Ordering::Relaxed)),
3340            symbol_reference_query_ms = nanos_millis(
3341                self.telemetry.symbol_reference_query_ns.load(Ordering::Relaxed),
3342            ),
3343            nested_query_ms = nanos_millis(self.telemetry.nested_ns.load(Ordering::Relaxed)),
3344            decode_ms = nanos_millis(self.telemetry.decode_ns.load(Ordering::Relaxed)),
3345            total_worker_cpu_ms = nanos_millis(self.telemetry.lookup_ns.load(Ordering::Relaxed)),
3346            average_lookup_micros = average_micros(self.telemetry.lookup_ns.load(Ordering::Relaxed), lookups),
3347            average_request_micros = average_micros(self.telemetry.ipc_ns.load(Ordering::Relaxed), ipc_requests),
3348            average_comparison_micros = average_micros(
3349                self.telemetry.comparison_ns.load(Ordering::Relaxed),
3350                comparisons,
3351            ),
3352            average_metadata_query_micros = average_micros(
3353                self.telemetry.metadata_query_ns.load(Ordering::Relaxed),
3354                metadata_queries,
3355            ),
3356            average_analysis_query_micros = average_micros(
3357                self.telemetry.analysis_query_ns.load(Ordering::Relaxed),
3358                analysis_queries,
3359            ),
3360            average_symbol_reference_query_micros = average_micros(
3361                self.telemetry.symbol_reference_query_ns.load(Ordering::Relaxed),
3362                symbol_reference_queries,
3363            ),
3364            average_nested_query_micros = average_micros(
3365                self.telemetry.nested_ns.load(Ordering::Relaxed),
3366                nested_requests,
3367            ),
3368            lifetime = ?self.started_at.map(|start| start.elapsed()).unwrap_or_default(),
3369            "External analyzer timing summary."
3370        );
3371
3372        tracing::trace!(
3373            lifecycle_encode_ms = nanos_millis(self.telemetry.lifecycle_encode_ns.load(Ordering::Relaxed)),
3374            lifecycle_ipc_ms = nanos_millis(self.telemetry.lifecycle_ipc_ns.load(Ordering::Relaxed)),
3375            lifecycle_decode_ms = nanos_millis(self.telemetry.lifecycle_decode_ns.load(Ordering::Relaxed)),
3376            lifecycle_worker_cpu_ms = nanos_millis(self.telemetry.lifecycle_ns.load(Ordering::Relaxed)),
3377            average_lifecycle_request_micros =
3378                average_micros(self.telemetry.lifecycle_ns.load(Ordering::Relaxed), lifecycle_requests,),
3379            "External analyzer lifecycle timing summary."
3380        );
3381
3382        tracing::trace!(
3383            issue_filter_encode_ms = nanos_millis(self.telemetry.issue_filter_encode_ns.load(Ordering::Relaxed)),
3384            issue_filter_ipc_ms = nanos_millis(self.telemetry.issue_filter_ipc_ns.load(Ordering::Relaxed)),
3385            issue_filter_decode_ms = nanos_millis(self.telemetry.issue_filter_decode_ns.load(Ordering::Relaxed)),
3386            issue_filter_worker_cpu_ms = nanos_millis(self.telemetry.issue_filter_ns.load(Ordering::Relaxed)),
3387            average_issue_filter_batch_micros =
3388                average_micros(self.telemetry.issue_filter_ns.load(Ordering::Relaxed), issue_filter_batches),
3389            "External analyzer issue-filter timing summary."
3390        );
3391    }
3392}
3393
3394fn pattern_matches(pattern: &[u8], value: &[u8]) -> bool {
3395    if pattern == b"*" {
3396        return true;
3397    }
3398
3399    if let Some(prefix) = pattern.strip_suffix(b"*") {
3400        starts_with_ignore_case(value, prefix)
3401    } else {
3402        value.eq_ignore_ascii_case(pattern)
3403    }
3404}
3405
3406fn class_pattern_matches(codebase: &CodebaseMetadata, pattern: &[u8], class: &[u8]) -> bool {
3407    pattern_matches(pattern, class) || (!pattern.contains(&b'*') && codebase.is_instance_of(class, pattern))
3408}
3409
3410fn property_pattern_matches(pattern: &[u8], property: &[u8]) -> bool {
3411    if pattern == b"*" {
3412        return true;
3413    }
3414
3415    if let Some(prefix) = pattern.strip_suffix(b"*") { property.starts_with(prefix) } else { property == pattern }
3416}
3417
3418fn append_path_component(target: &mut Vec<u8>, component: &[u8]) {
3419    const HEX: &[u8; 16] = b"0123456789ABCDEF";
3420
3421    for byte in component {
3422        if byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'_' | b'.') {
3423            target.push(*byte);
3424        } else {
3425            target.push(b'%');
3426            target.push(HEX[usize::from(byte >> 4)]);
3427            target.push(HEX[usize::from(byte & 0x0f)]);
3428        }
3429    }
3430}
3431
3432fn duration_nanos(duration: Duration) -> u64 {
3433    u64::try_from(duration.as_nanos()).unwrap_or(u64::MAX)
3434}
3435
3436#[allow(clippy::cast_precision_loss, clippy::float_arithmetic)]
3437fn nanos_millis(nanos: u64) -> f64 {
3438    nanos as f64 / 1_000_000.0
3439}
3440
3441fn average_micros(nanos: u64, count: u64) -> u64 {
3442    nanos.checked_div(count).unwrap_or(0) / 1_000
3443}
3444
3445#[cfg(test)]
3446#[allow(clippy::expect_used, clippy::unwrap_in_result, clippy::unwrap_used)]
3447mod tests {
3448    use std::sync::Mutex;
3449
3450    use mago_codex::ttype::TType;
3451    use mago_codex::ttype::atomic::TAtomic;
3452    use mago_codex::ttype::atomic::array::TArray;
3453    use mago_extension::WorkerError;
3454
3455    use super::*;
3456
3457    #[derive(Debug)]
3458    struct TestTransport {
3459        requests: Mutex<Vec<Vec<u8>>>,
3460    }
3461
3462    #[derive(Debug)]
3463    struct InitializationTransport {
3464        broadcasts: Mutex<usize>,
3465        initialization_responses: Vec<Vec<u8>>,
3466    }
3467
3468    #[derive(Debug)]
3469    struct RegistrationTransport(Vec<u8>);
3470
3471    impl AnalyzerTransport for TestTransport {
3472        fn broadcast(&self, _payload: &[u8]) -> Result<Vec<Vec<u8>>, WorkerError> {
3473            Ok(vec![protocol::testing::registration_response()])
3474        }
3475
3476        fn request(&self, payload: Vec<u8>) -> Result<Vec<u8>, WorkerError> {
3477            self.requests.lock().unwrap().push(payload);
3478            Ok(protocol::testing::named_object_response("Demo\\Service"))
3479        }
3480
3481        fn request_with_handler<H>(&self, payload: Vec<u8>, _handler: &mut H) -> Result<Vec<u8>, WorkerError>
3482        where
3483            H: WorkerRequestHandler,
3484        {
3485            self.request(payload)
3486        }
3487    }
3488
3489    impl AnalyzerTransport for InitializationTransport {
3490        fn broadcast(&self, _payload: &[u8]) -> Result<Vec<Vec<u8>>, WorkerError> {
3491            let mut broadcasts = self.broadcasts.lock().unwrap();
3492            let responses = if *broadcasts == 0 {
3493                vec![protocol::testing::registration_response_with_initialization()]
3494            } else {
3495                self.initialization_responses.clone()
3496            };
3497
3498            *broadcasts += 1;
3499            Ok(responses)
3500        }
3501
3502        fn request(&self, _payload: Vec<u8>) -> Result<Vec<u8>, WorkerError> {
3503            unreachable!("initialization tests do not issue routed requests")
3504        }
3505
3506        fn request_with_handler<H>(&self, _payload: Vec<u8>, _handler: &mut H) -> Result<Vec<u8>, WorkerError>
3507        where
3508            H: WorkerRequestHandler,
3509        {
3510            unreachable!("initialization tests do not issue routed requests")
3511        }
3512    }
3513
3514    impl AnalyzerTransport for RegistrationTransport {
3515        fn broadcast(&self, _payload: &[u8]) -> Result<Vec<Vec<u8>>, WorkerError> {
3516            Ok(vec![self.0.clone()])
3517        }
3518
3519        fn request(&self, _payload: Vec<u8>) -> Result<Vec<u8>, WorkerError> {
3520            unreachable!("registration tests do not issue routed requests")
3521        }
3522
3523        fn request_with_handler<H>(&self, _payload: Vec<u8>, _handler: &mut H) -> Result<Vec<u8>, WorkerError>
3524        where
3525            H: WorkerRequestHandler,
3526        {
3527            unreachable!("registration tests do not issue routed requests")
3528        }
3529    }
3530
3531    #[test]
3532    fn registration_is_decoded_and_filtered() {
3533        let transport = Arc::new(TestTransport { requests: Mutex::new(Vec::new()) });
3534        let analyzer = ExternalAnalyzer::initialize_transports([transport], PHPVersion::PHP85, &[], false)
3535            .expect("registration should succeed");
3536
3537        assert_eq!(analyzer.extensions().len(), 1);
3538        assert_eq!(analyzer.extensions()[0].plugins[0].identifier, "demo");
3539        assert_eq!(analyzer.backends[0].registration.function_providers.len(), 1);
3540        assert!(analyzer.backends[0].matching_function_providers(b"demo_service", false).0.is_some());
3541        assert!(analyzer.backends[0].matching_function_signature_providers(b"demo_service", false).0.is_none());
3542    }
3543
3544    #[test]
3545    fn exact_and_wildcard_provider_indices_preserve_registration_order() {
3546        let indices =
3547            ProviderIndices::from_exact_and_wildcards(&[1, 3], [0, 3, 4]).expect("at least one provider should match");
3548
3549        assert_eq!(indices.as_slice(), &[0, 1, 3, 4]);
3550        assert!(ProviderIndices::from_exact_and_wildcards(&[], []).is_none());
3551    }
3552
3553    #[test]
3554    fn provider_response_cache_is_scoped_to_one_analysis_generation() {
3555        let mut cache = ProviderResponseCache::default();
3556        cache.insert(1, b"request".to_vec(), b"first".to_vec());
3557
3558        assert_eq!(cache.get(1, b"request"), Some(b"first".to_vec()));
3559        assert_eq!(cache.get(2, b"request"), None);
3560
3561        cache.insert(2, b"request".to_vec(), b"second".to_vec());
3562        assert_eq!(cache.get(2, b"request"), Some(b"second".to_vec()));
3563        assert_eq!(cache.get(1, b"request"), None);
3564    }
3565
3566    #[test]
3567    fn default_plugins_can_be_disabled_and_reenabled_by_alias() {
3568        let disabled_transport = Arc::new(TestTransport { requests: Mutex::new(Vec::new()) });
3569        let disabled = ExternalAnalyzer::initialize_transports([disabled_transport], PHPVersion::PHP85, &[], true)
3570            .expect("registration should succeed");
3571        assert!(disabled.backends[0].registration.function_providers.is_empty());
3572
3573        let enabled_transport = Arc::new(TestTransport { requests: Mutex::new(Vec::new()) });
3574        let enabled = ExternalAnalyzer::initialize_transports(
3575            [enabled_transport],
3576            PHPVersion::PHP85,
3577            &["EXAMPLE".to_string()],
3578            true,
3579        )
3580        .expect("registration should succeed");
3581        assert_eq!(enabled.backends[0].registration.function_providers.len(), 1);
3582    }
3583
3584    #[test]
3585    fn plugin_selectors_cannot_shadow_native_plugins() {
3586        let transport = Arc::new(RegistrationTransport(protocol::testing::registration_response_with_plugin(
3587            "demo/extension",
3588            "demo",
3589            &["StD"],
3590        )));
3591
3592        let result = ExternalAnalyzer::initialize_transports([transport], PHPVersion::PHP85, &[], false);
3593
3594        assert!(matches!(
3595            result,
3596            Err(ExternalAnalyzerError::DuplicatePluginSelector { selector, first, second })
3597                if selector == "std" && first == "stdlib" && second == "demo"
3598        ));
3599    }
3600
3601    #[test]
3602    fn plugin_aliases_must_be_unique_across_extension_hosts() {
3603        let first = Arc::new(RegistrationTransport(protocol::testing::registration_response_with_plugin(
3604            "demo/first",
3605            "first",
3606            &["shared"],
3607        )));
3608        let second = Arc::new(RegistrationTransport(protocol::testing::registration_response_with_plugin(
3609            "demo/second",
3610            "second",
3611            &["SHARED"],
3612        )));
3613
3614        let result = ExternalAnalyzer::initialize_transports([first, second], PHPVersion::PHP85, &[], false);
3615
3616        assert!(matches!(
3617            result,
3618            Err(ExternalAnalyzerError::DuplicatePluginSelector { selector, first, second })
3619                if selector == "shared" && first == "first" && second == "second"
3620        ));
3621    }
3622
3623    #[test]
3624    fn initialization_stubs_are_scoped_and_exposed_as_external_files() {
3625        let response = protocol::testing::initialization_response(b"framework.php", b"<?php class FrameworkStub {}");
3626        let transport = Arc::new(InitializationTransport {
3627            broadcasts: Mutex::new(0),
3628            initialization_responses: vec![response.clone(), response],
3629        });
3630
3631        let analyzer = ExternalAnalyzer::initialize_transports([Arc::clone(&transport)], PHPVersion::PHP85, &[], false)
3632            .expect("initialization should succeed");
3633
3634        let files = analyzer.initialization_files();
3635        assert_eq!(*transport.broadcasts.lock().unwrap(), 2);
3636        assert_eq!(files.len(), 1);
3637        assert_eq!(files[0].name.as_ref(), b"@mago-extension/demo%2Fextension/demo/framework.php");
3638        assert_eq!(files[0].contents.as_ref(), b"<?php class FrameworkStub {}");
3639        assert!(files[0].file_type.is_external());
3640        assert!(files[0].path.is_none());
3641    }
3642
3643    #[test]
3644    fn initialization_requires_identical_stubs_from_every_worker() {
3645        let transport = Arc::new(InitializationTransport {
3646            broadcasts: Mutex::new(0),
3647            initialization_responses: vec![
3648                protocol::testing::initialization_response(b"framework.php", b"<?php class First {}"),
3649                protocol::testing::initialization_response(b"framework.php", b"<?php class Second {}"),
3650            ],
3651        });
3652
3653        let result = ExternalAnalyzer::initialize_transports([transport], PHPVersion::PHP85, &[], false);
3654
3655        assert!(matches!(result, Err(ExternalAnalyzerError::InconsistentInitialization)));
3656    }
3657
3658    #[test]
3659    fn disabled_plugins_are_not_initialized() {
3660        let transport =
3661            Arc::new(InitializationTransport { broadcasts: Mutex::new(0), initialization_responses: Vec::new() });
3662        let analyzer = ExternalAnalyzer::initialize_transports([Arc::clone(&transport)], PHPVersion::PHP85, &[], true)
3663            .expect("registration should succeed");
3664
3665        assert_eq!(*transport.broadcasts.lock().unwrap(), 1);
3666        assert!(analyzer.initialization_files().is_empty());
3667    }
3668
3669    #[test]
3670    fn decodes_constructed_and_lossless_reference_types() {
3671        let named =
3672            protocol::decode_return_type_response(&protocol::testing::named_object_response("Demo\\Service"), |_| None)
3673                .expect("response should decode")
3674                .expect("response should be handled");
3675        assert_eq!(
3676            named.get_single_named_object().expect("type should be a named object").get_name().as_bytes(),
3677            b"Demo\\Service"
3678        );
3679
3680        let original = mago_codex::ttype::get_literal_string(mago_word::word(b"hello"));
3681        let referenced = protocol::decode_return_type_response(&protocol::testing::reference_response(0), |slot| {
3682            (slot == 0).then_some(&original)
3683        })
3684        .expect("response should decode")
3685        .expect("response should be handled");
3686        assert_eq!(referenced.get_id(), original.get_id());
3687
3688        let non_negative =
3689            protocol::decode_return_type_response(&protocol::testing::non_negative_int_response(), |_| None)
3690                .expect("response should decode")
3691                .expect("response should be handled");
3692        assert!(non_negative.get_single_int().expect("type should be an integer").is_non_negative());
3693
3694        let non_empty =
3695            protocol::decode_return_type_response(&protocol::testing::non_empty_string_response(), |_| None)
3696                .expect("response should decode")
3697                .expect("response should be handled");
3698        assert!(non_empty.is_non_empty_string());
3699
3700        let complete = protocol::decode_return_type_response(
3701            &protocol::testing::complete_non_empty_string_list_response(),
3702            |_| None,
3703        )
3704        .expect("complete response should decode")
3705        .expect("complete response should be handled");
3706        let TAtomic::Array(TArray::List(list)) = complete.get_single() else {
3707            panic!("complete type should be a list");
3708        };
3709
3710        assert!(list.non_empty);
3711        assert!(list.element_type.is_string());
3712    }
3713}