1use 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#[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#[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
617pub trait AnalyzerTransport: std::fmt::Debug + Send + Sync {
619 fn prepare_capacity(self: &Arc<Self>) {}
621
622 fn broadcast(&self, payload: &[u8]) -> Result<Vec<Vec<u8>>, WorkerError>;
628
629 fn maximum_payload_size(&self) -> usize {
631 usize::MAX
632 }
633
634 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 fn request(&self, payload: Vec<u8>) -> Result<Vec<u8>, WorkerError>;
649
650 fn request_with_handler<H>(&self, payload: Vec<u8>, handler: &mut H) -> Result<Vec<u8>, WorkerError>
656 where
657 H: WorkerRequestHandler;
658
659 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(®istration.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(®istration.function_assertion_providers);
803 let (method_exact, method_wildcard) = index_method_providers(®istration.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(®istration.method_assertion_providers);
812
813 let (property_exact, property_wildcard) = index_property_providers(®istration.property_providers);
814 let (property_initialization_exact, property_initialization_wildcard) =
815 index_property_providers(®istration.property_initialization_providers);
816 let (class_initializer_exact, class_initializer_wildcard) =
817 index_class_providers(®istration.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(®istration.function_providers);
822 let method_capabilities = provider_capabilities(®istration.method_providers);
823 let function_assertion_capabilities = provider_capabilities(®istration.function_assertion_providers);
824 let method_assertion_capabilities = provider_capabilities(®istration.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#[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 #[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 #[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 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 ®istration.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 ®istration.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(®istration.initialization_plugins)?;
3093 let responses = transport.broadcast(&initialize)?;
3094 let mut decoded = responses.iter().map(|response| {
3095 protocol::decode_initialization_response(response, ®istration.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}