1use std::ops::Range;
2use std::sync::Arc;
3use std::sync::OnceLock;
4
5use foldhash::HashMap;
6use foldhash::HashSet;
7use mago_codex::metadata::CodebaseMetadata;
8use mago_codex::reference::ReferenceOrigin;
9use mago_codex::reference::SymbolReferenceKind;
10use mago_codex::reference::SymbolReferences;
11use mago_codex::symbol::SymbolIdentifier;
12use mago_codex::ttype::union::TUnion;
13use mago_database::file::File;
14use mago_database::file::FileId;
15use mago_extension::PayloadReader;
16use mago_extension::PayloadWriter;
17use mago_extension::source::SourceSnapshot;
18use mago_names::ResolvedNames;
19use mago_reporting::Annotation;
20use mago_reporting::AnnotationKind;
21use mago_reporting::Issue;
22use mago_reporting::IssueCollection;
23use mago_reporting::Level;
24use mago_span::HasSpan;
25use mago_span::Position;
26use mago_span::Span;
27use mago_syntax::cst::Node;
28use mago_syntax::cst::Program;
29use mago_text_edit::Safety;
30use mago_text_edit::TextEdit;
31use mago_text_edit::TextRange;
32use mago_word::ascii_lowercase_constant_name_word;
33use mago_word::ascii_lowercase_word;
34use mago_word::empty_word;
35use mago_word::word;
36
37use crate::analysis_result::AnalysisResult;
38use crate::artifacts::AnalysisArtifacts;
39use crate::artifacts::ResolvedMethodCall;
40
41use super::ExternalAnalysisSession;
42use super::ExternalPlugin;
43use super::NODE_REQUIREMENT_ARGUMENT_TYPES;
44use super::NODE_REQUIREMENT_RECEIVER_TYPE;
45use super::NODE_REQUIREMENT_TARGET_EXPRESSION_TYPES;
46use super::NodeAnalysisRequirements;
47use super::error::ExternalAnalyzerError;
48use super::error::protocol;
49use super::protocol;
50
51pub(super) const BEFORE_ANALYSIS_REQUEST: u16 = 5;
52pub(super) const AFTER_FILE_ANALYSIS_REQUEST: u16 = 6;
53pub(super) const AFTER_ANALYSIS_REQUEST: u16 = 7;
54const ANALYSIS_QUERY_REQUEST: u16 = 8;
55pub(super) const AFTER_FILE_ANALYSIS_BATCH_REQUEST: u16 = 9;
56const SYMBOL_REFERENCE_QUERY_REQUEST: u16 = 10;
57const BEFORE_ANALYSIS_RESPONSE: u16 = 0x8005;
58const AFTER_FILE_ANALYSIS_RESPONSE: u16 = 0x8006;
59const AFTER_ANALYSIS_RESPONSE: u16 = 0x8007;
60const ANALYSIS_QUERY_RESPONSE: u16 = 0x8008;
61const AFTER_FILE_ANALYSIS_BATCH_RESPONSE: u16 = 0x8009;
62const SYMBOL_REFERENCE_QUERY_RESPONSE: u16 = 0x800A;
63
64const GET_EXPRESSION_TYPES: u8 = 1;
65const GET_ALL_EXPRESSION_TYPES: u8 = 2;
66const GET_INFERRED_RETURN_TYPES: u8 = 3;
67const GET_INFERRED_YIELD_KEY_TYPES: u8 = 4;
68const GET_INFERRED_YIELD_VALUE_TYPES: u8 = 5;
69const GET_SOURCE_FILE: u8 = 6;
70const GET_REFERENCES_TO: u8 = 1;
71const GET_REFERENCES_FROM: u8 = 2;
72const MAXIMUM_ISSUES: usize = 1_000_000;
73const MAXIMUM_ANNOTATIONS: usize = 0x0001_0000;
74const MAXIMUM_EDITS: usize = 0x0001_0000;
75const MAXIMUM_NOTES: usize = 0x0001_0000;
76const MAXIMUM_TYPE_QUERIES: usize = 1_000_000;
77const MAXIMUM_REFERENCE_QUERIES: usize = 1_000_000;
78const MAXIMUM_REFERENCES: usize = 10_000_000;
79const EXPRESSION_TYPE_PREFETCH: usize = 16;
80
81#[derive(Debug, Default)]
82pub(super) struct LifecycleEffects {
83 pub issues: IssueCollection,
84 pub references: SymbolReferences,
85 pub references_by_file: HashMap<FileId, SymbolReferences>,
86}
87
88pub(super) struct SymbolReferenceStore<'analysis> {
89 references: &'analysis SymbolReferences,
90 by_target: OnceLock<HashMap<SymbolIdentifier, Vec<RecordedReference>>>,
91 by_source: OnceLock<HashMap<ReferenceOrigin, Vec<RecordedReference>>>,
92}
93
94impl<'analysis> SymbolReferenceStore<'analysis> {
95 pub fn new(references: &'analysis SymbolReferences) -> Self {
96 Self { references, by_target: OnceLock::new(), by_source: OnceLock::new() }
97 }
98
99 fn references_to(&self, target: SymbolIdentifier) -> &[RecordedReference] {
100 self.by_target
101 .get_or_init(|| {
102 let mut index = HashMap::<SymbolIdentifier, Vec<RecordedReference>>::default();
103 self.references.for_each_reference(|source, target, kind| {
104 index.entry(target).or_default().push(RecordedReference { source, target, kind });
105 });
106 for references in index.values_mut() {
107 references.sort_unstable();
108 }
109 index
110 })
111 .get(&target)
112 .map_or(&[], Vec::as_slice)
113 }
114
115 fn references_from(&self, source: ReferenceOrigin) -> &[RecordedReference] {
116 self.by_source
117 .get_or_init(|| {
118 let mut index = HashMap::<ReferenceOrigin, Vec<RecordedReference>>::default();
119 self.references.for_each_reference(|source, target, kind| {
120 index.entry(source).or_default().push(RecordedReference { source, target, kind });
121 });
122 for references in index.values_mut() {
123 references.sort_unstable();
124 }
125 index
126 })
127 .get(&source)
128 .map_or(&[], Vec::as_slice)
129 }
130}
131
132#[derive(Debug)]
133pub struct FileAnalysisSnapshot {
134 file_id: mago_database::file::FileId,
135 name: Arc<[u8]>,
136 size: u32,
137 encoded_source: Box<[u8]>,
138 encoded_target_source: Box<[u8]>,
139 encoded_target_analysis: Box<[u8]>,
140 encoded_types: Box<[u8]>,
141 expression_types: HashMap<(u32, u32), Range<usize>>,
142 inferred_return_types: Vec<Range<usize>>,
143 inferred_yield_key_types: Vec<Range<usize>>,
144 inferred_yield_value_types: Vec<Range<usize>>,
145 references: ReferenceSummary,
146 node_analysis_targets: usize,
147}
148
149#[derive(Debug, Clone, Copy)]
150struct ReferenceSummary {
151 body: u64,
152 signature: u64,
153 maps: u64,
154}
155
156pub const AFTER_FILE_ANALYSIS_BATCH_SIZE: usize = 32;
158
159type NodeTargetKey = (u8, u32, u32);
160
161struct NodeAnalysisTarget<'ast, 'arena> {
162 node: Node<'ast, 'arena>,
163 requirements: u8,
164 targeted_hook_routes: Vec<u32>,
165}
166
167struct NodeAnalysisPlan<'ast, 'arena> {
168 targets: Vec<NodeAnalysisTarget<'ast, 'arena>>,
169 by_node: HashMap<NodeTargetKey, usize>,
170}
171
172impl<'ast, 'arena> NodeAnalysisPlan<'ast, 'arena> {
173 fn configuration(&self, node: Node<'ast, 'arena>) -> Option<bool> {
174 let span = node.span();
175 self.by_node
176 .get(&(node.kind() as u8, span.start.offset, span.end.offset))
177 .map(|index| self.targets[*index].requirements & super::NODE_REQUIREMENT_TARGET_SUBTREE != 0)
178 }
179}
180
181fn build_node_analysis_plan<'ast, 'arena>(
182 program: &'ast Program<'arena>,
183 artifacts: &AnalysisArtifacts,
184 resolved_names: &ResolvedNames<'arena>,
185 codebase: &CodebaseMetadata,
186 requirements: &NodeAnalysisRequirements,
187) -> NodeAnalysisPlan<'ast, 'arena> {
188 let (method_calls, method_call_targets) = if requirements.method_call_hooks.is_empty() {
189 (None, None)
190 } else {
191 let mut targets = artifacts.resolved_method_calls.iter().collect::<Vec<_>>();
192 targets.sort_unstable_by_key(|target| target.span);
193 let mut calls = HashMap::<(u32, u32), Range<usize>>::default();
194 for (index, call) in targets.iter().enumerate() {
195 calls.entry(call.span).and_modify(|range| range.end = index + 1).or_insert(index..index + 1);
196 }
197 (Some(calls), Some(targets))
198 };
199 let mut targets = Vec::new();
200 let mut by_node = HashMap::default();
201 let mut stack = Vec::with_capacity(64);
202 stack.push(Node::Program(program));
203 while let Some(node) = stack.pop() {
204 let kind = node.kind();
205 let span = node.span();
206 let mut requested = requirements.requirements(kind);
207 let mut targeted_hook_routes = Vec::new();
208 if is_method_call_kind(kind)
209 && let Some(call_range) =
210 method_calls.as_ref().and_then(|calls| calls.get(&(span.start.offset, span.end.offset)))
211 {
212 let calls = &method_call_targets.as_deref().unwrap_or_default()[call_range.clone()];
213 for hook in requirements.method_call_hooks.iter() {
214 if method_call_matches_hook(calls, hook, codebase) {
215 requested |= hook.requirements;
216 targeted_hook_routes.push(hook.route);
217 }
218 }
219 }
220
221 if !requirements.class_like_hooks.is_empty()
222 && let Some(class_like) = class_like_name(node, resolved_names)
223 {
224 for hook in requirements.class_like_hooks.iter() {
225 if class_like_matches_hook(class_like, hook, codebase) {
226 requested |= hook.requirements;
227 targeted_hook_routes.push(hook.route);
228 }
229 }
230 }
231
232 if requirements.targets()[kind as usize] || !targeted_hook_routes.is_empty() {
233 let index = targets.len();
234 by_node.insert((kind as u8, span.start.offset, span.end.offset), index);
235 targets.push(NodeAnalysisTarget { node, requirements: requested, targeted_hook_routes });
236 }
237
238 let start = stack.len();
239 node.visit_children(|child| stack.push(child));
240 stack[start..].reverse();
241 }
242
243 NodeAnalysisPlan { targets, by_node }
244}
245
246fn is_method_call_kind(kind: mago_syntax::cst::NodeKind) -> bool {
247 matches!(
248 kind,
249 mago_syntax::cst::NodeKind::MethodCall
250 | mago_syntax::cst::NodeKind::NullSafeMethodCall
251 | mago_syntax::cst::NodeKind::StaticMethodCall
252 )
253}
254
255fn method_call_matches_hook(
256 calls: &[&ResolvedMethodCall],
257 hook: &super::MethodCallAnalysisHookRegistration,
258 codebase: &CodebaseMetadata,
259) -> bool {
260 calls.iter().any(|call_target| {
261 hook.targets
262 .iter()
263 .any(|target| target.matches(codebase, call_target.class.as_bytes(), call_target.method.as_bytes()))
264 })
265}
266
267fn class_like_name<'arena>(node: Node<'_, 'arena>, resolved_names: &ResolvedNames<'arena>) -> Option<&'arena [u8]> {
268 match node {
269 Node::Class(class) => resolved_names.resolve(&class.name),
270 Node::Enum(r#enum) => resolved_names.resolve(&r#enum.name),
271 Node::Interface(interface) => resolved_names.resolve(&interface.name),
272 Node::Trait(r#trait) => resolved_names.resolve(&r#trait.name),
273 _ => None,
274 }
275}
276
277fn class_like_matches_hook(
278 class_like: &[u8],
279 hook: &super::ClassLikeAnalysisHookRegistration,
280 codebase: &CodebaseMetadata,
281) -> bool {
282 let class_like = ascii_lowercase_word(class_like);
283 let Some(metadata) = codebase.class_likes.get(&class_like) else {
284 return false;
285 };
286
287 hook.targets.iter().any(|ancestor| {
288 class_like != *ancestor
289 && (metadata.all_parent_classes.contains(ancestor) || metadata.all_parent_interfaces.contains(ancestor))
290 })
291}
292
293pub(super) fn has_node_analysis_target(
294 program: &Program<'_>,
295 artifacts: &AnalysisArtifacts,
296 resolved_names: &ResolvedNames<'_>,
297 codebase: &CodebaseMetadata,
298 requirements: &NodeAnalysisRequirements,
299) -> bool {
300 !build_node_analysis_plan(program, artifacts, resolved_names, codebase, requirements).targets.is_empty()
301}
302
303impl FileAnalysisSnapshot {
304 pub fn new(
310 file: &File,
311 program: &Program<'_>,
312 resolved_names: &ResolvedNames<'_>,
313 artifacts: &AnalysisArtifacts,
314 codebase: &CodebaseMetadata,
315 node_analysis_requirements: Option<&NodeAnalysisRequirements>,
316 ) -> Result<Self, ExternalAnalyzerError> {
317 let node_analysis_plan = node_analysis_requirements
318 .map(|requirements| build_node_analysis_plan(program, artifacts, resolved_names, codebase, requirements));
319 let source = SourceSnapshot::complete_with_target_filter(program, resolved_names, |node| {
320 node_analysis_plan.as_ref().is_some_and(|plan| plan.configuration(node).is_some())
321 })
322 .map_err(|error| {
323 protocol(format!("failed to retain syntax for `{}`: {error}", String::from_utf8_lossy(&file.name)))
324 })?;
325 let matched_target_count = source.target_count();
326
327 let mut source_writer = PayloadWriter::with_capacity(source.encoded_len());
328 source.write_to(&mut source_writer).map_err(|error| {
329 protocol(format!("failed to encode retained syntax for `{}`: {error}", String::from_utf8_lossy(&file.name)))
330 })?;
331
332 let (encoded_target_source, encoded_target_analysis, node_analysis_targets) = if let Some(plan) =
333 node_analysis_plan.as_ref().filter(|_| matched_target_count != 0)
334 {
335 let target_source = SourceSnapshot::targeted_with_filter(
336 program,
337 resolved_names,
338 |node| plan.configuration(node),
339 node_analysis_requirements.is_some_and(NodeAnalysisRequirements::includes_source_text),
340 )
341 .map_err(|error| {
342 protocol(format!(
343 "failed to retain targeted syntax for `{}`: {error}",
344 String::from_utf8_lossy(&file.name)
345 ))
346 })?;
347 if let Some(target_source) = target_source {
348 let target_count = target_source.target_count();
349 if target_count != matched_target_count {
350 return Err(protocol(format!(
351 "complete syntax contains {matched_target_count} targets, but targeted syntax contains {target_count}"
352 )));
353 }
354 let mut target_source_writer = PayloadWriter::with_capacity(target_source.encoded_len());
355 target_source.write_to(&mut target_source_writer).map_err(|error| {
356 protocol(format!(
357 "failed to encode targeted syntax for `{}`: {error}",
358 String::from_utf8_lossy(&file.name)
359 ))
360 })?;
361 let mut target_analysis_writer = PayloadWriter::new();
362 write_target_analysis(&mut target_analysis_writer, artifacts, plan, target_count)?;
363 (
364 target_source_writer.finish().into_boxed_slice(),
365 target_analysis_writer.finish().into_boxed_slice(),
366 target_count,
367 )
368 } else {
369 (Box::default(), Box::default(), 0)
370 }
371 } else {
372 (Box::default(), Box::default(), 0)
373 };
374
375 let mut writer = PayloadWriter::new();
376 let mut type_handles = Vec::new();
377 let mut expression_types =
378 HashMap::with_capacity_and_hasher(artifacts.expression_types.len(), foldhash::fast::RandomState::default());
379 for (span, union) in &artifacts.expression_types {
380 expression_types.insert(
381 *span,
382 encode_snapshot_type(&mut writer, union, &mut type_handles).map_err(|error| {
383 protocol(format!(
384 "failed to retain expression type at {}:{} in `{}`: {error}",
385 span.0,
386 span.1,
387 String::from_utf8_lossy(&file.name)
388 ))
389 })?,
390 );
391 }
392
393 let inferred_return_types = encode_snapshot_types(
394 &mut writer,
395 artifacts.inferred_return_types.iter().map(AsRef::as_ref),
396 &mut type_handles,
397 )?;
398 let inferred_yield_key_types =
399 encode_snapshot_types(&mut writer, &artifacts.inferred_yield_key_types, &mut type_handles)?;
400 let inferred_yield_value_types =
401 encode_snapshot_types(&mut writer, &artifacts.inferred_yield_value_types, &mut type_handles)?;
402
403 Ok(Self {
404 file_id: file.id,
405 name: Arc::from(file.name.as_ref()),
406 size: file.size,
407 encoded_source: source_writer.finish().into_boxed_slice(),
408 encoded_target_source,
409 encoded_target_analysis,
410 encoded_types: writer.finish().into_boxed_slice(),
411 expression_types,
412 inferred_return_types,
413 inferred_yield_key_types,
414 inferred_yield_value_types,
415 references: ReferenceSummary::from(&artifacts.symbol_references),
416 node_analysis_targets,
417 })
418 }
419
420 #[must_use]
421 pub const fn file_id(&self) -> mago_database::file::FileId {
422 self.file_id
423 }
424
425 #[must_use]
426 pub const fn has_node_analysis_targets(&self) -> bool {
427 self.node_analysis_targets != 0
428 }
429}
430
431impl From<&SymbolReferences> for ReferenceSummary {
432 fn from(references: &SymbolReferences) -> Self {
433 Self {
434 body: references.count_body_references() as u64,
435 signature: references.count_signature_references() as u64,
436 maps: references.total_map_entries() as u64,
437 }
438 }
439}
440
441impl ReferenceSummary {
442 fn write_to(self, writer: &mut PayloadWriter) {
443 writer.write_u64(self.body);
444 writer.write_u64(self.signature);
445 writer.write_u64(self.maps);
446 }
447}
448
449fn encode_snapshot_types<'type_info>(
450 writer: &mut PayloadWriter,
451 types: impl IntoIterator<Item = &'type_info TUnion>,
452 handles: &mut Vec<&'type_info TUnion>,
453) -> Result<Vec<Range<usize>>, ExternalAnalyzerError> {
454 types.into_iter().map(|ty| encode_snapshot_type(writer, ty, handles)).collect()
455}
456
457fn encode_snapshot_type<'type_info>(
458 writer: &mut PayloadWriter,
459 ty: &'type_info TUnion,
460 handles: &mut Vec<&'type_info TUnion>,
461) -> Result<Range<usize>, ExternalAnalyzerError> {
462 handles.clear();
463 let start = writer.len();
464 protocol::encode_union_snapshot(writer, ty, handles, 0)?;
465 Ok(start..writer.len())
466}
467
468fn write_target_analysis(
469 writer: &mut PayloadWriter,
470 artifacts: &AnalysisArtifacts,
471 plan: &NodeAnalysisPlan<'_, '_>,
472 expected_count: usize,
473) -> Result<(), ExternalAnalyzerError> {
474 if plan.targets.len() != expected_count {
475 return Err(protocol(format!(
476 "targeted syntax contains {expected_count} targets, but the analysis plan contains {}",
477 plan.targets.len()
478 )));
479 }
480 writer.write_u32(u32::try_from(expected_count).map_err(|_| protocol("too many node-analysis targets"))?);
481 for target in &plan.targets {
482 let requested = target.requirements;
483 writer.write_u8(requested);
484 if requested & NODE_REQUIREMENT_TARGET_EXPRESSION_TYPES != 0 {
485 write_optional_expression_type(writer, artifacts, Some(target.node.span()))?;
486 }
487 if requested & NODE_REQUIREMENT_RECEIVER_TYPE != 0 {
488 write_optional_expression_type(writer, artifacts, receiver_span(target.node))?;
489 }
490 if requested & NODE_REQUIREMENT_ARGUMENT_TYPES != 0 {
491 write_argument_types(writer, artifacts, target.node)?;
492 }
493 writer.write_u32(
494 u32::try_from(target.targeted_hook_routes.len())
495 .map_err(|_| protocol("too many targeted analysis hook routes matched one node"))?,
496 );
497 for route in &target.targeted_hook_routes {
498 writer.write_u32(*route);
499 }
500 }
501
502 Ok(())
503}
504
505fn write_optional_expression_type(
506 writer: &mut PayloadWriter,
507 artifacts: &AnalysisArtifacts,
508 span: Option<Span>,
509) -> Result<(), ExternalAnalyzerError> {
510 let ty = span.and_then(|span| artifacts.expression_types.get(&(span.start.offset, span.end.offset)));
511 writer.write_bool(ty.is_some());
512 if let Some(ty) = ty {
513 write_type(writer, ty)?;
514 }
515 Ok(())
516}
517
518fn receiver_span(node: Node<'_, '_>) -> Option<Span> {
519 match node {
520 Node::MethodCall(call) => Some(call.object.span()),
521 Node::NullSafeMethodCall(call) => Some(call.object.span()),
522 Node::StaticMethodCall(call) => Some(call.class.span()),
523 _ => None,
524 }
525}
526
527fn write_argument_types(
528 writer: &mut PayloadWriter,
529 artifacts: &AnalysisArtifacts,
530 node: Node<'_, '_>,
531) -> Result<(), ExternalAnalyzerError> {
532 let arguments = match node {
533 Node::FunctionCall(call) => Some(&call.argument_list.arguments),
534 Node::MethodCall(call) => Some(&call.argument_list.arguments),
535 Node::NullSafeMethodCall(call) => Some(&call.argument_list.arguments),
536 Node::StaticMethodCall(call) => Some(&call.argument_list.arguments),
537 _ => None,
538 };
539 let Some(arguments) = arguments else {
540 writer.write_u32(0);
541 return Ok(());
542 };
543
544 writer.write_u32(u32::try_from(arguments.len()).map_err(|_| protocol("too many call arguments"))?);
545 for argument in arguments.iter() {
546 write_optional_expression_type(writer, artifacts, Some(argument.value().span()))?;
547 }
548 Ok(())
549}
550
551pub(super) enum AnalysisStore<'analysis> {
552 File {
553 file: &'analysis File,
554 program: &'analysis Program<'analysis>,
555 resolved_names: &'analysis ResolvedNames<'analysis>,
556 artifacts: &'analysis AnalysisArtifacts,
557 node_analysis_targets: Option<&'analysis [bool; u8::MAX as usize + 1]>,
558 },
559 Project(&'analysis [Arc<FileAnalysisSnapshot>]),
560}
561
562impl AnalysisStore<'_> {
563 fn file(&self, name: &[u8]) -> Option<FileView<'_>> {
564 match self {
565 Self::File { file, program, resolved_names, artifacts, node_analysis_targets }
566 if file.name.as_ref() == name =>
567 {
568 Some(FileView::Artifacts(file, program, resolved_names, artifacts, *node_analysis_targets))
569 }
570 Self::File { .. } => None,
571 Self::Project(files) => {
572 files.iter().find(|file| file.name.as_ref() == name).map(|file| FileView::Snapshot(file.as_ref()))
573 }
574 }
575 }
576}
577
578enum FileView<'analysis> {
579 Artifacts(
580 &'analysis File,
581 &'analysis Program<'analysis>,
582 &'analysis ResolvedNames<'analysis>,
583 &'analysis AnalysisArtifacts,
584 Option<&'analysis [bool; u8::MAX as usize + 1]>,
585 ),
586 Snapshot(&'analysis FileAnalysisSnapshot),
587}
588
589enum TypeView<'analysis> {
590 Union(&'analysis TUnion),
591 Encoded(&'analysis [u8]),
592}
593
594impl TypeView<'_> {
595 fn write_to(self, writer: &mut PayloadWriter) -> Result<(), ExternalAnalyzerError> {
596 match self {
597 Self::Union(ty) => write_type(writer, ty),
598 Self::Encoded(bytes) => {
599 writer.write_raw(bytes);
600 Ok(())
601 }
602 }
603 }
604}
605
606impl FileView<'_> {
607 fn name(&self) -> &[u8] {
608 match self {
609 Self::Artifacts(file, ..) => &file.name,
610 Self::Snapshot(file) => &file.name,
611 }
612 }
613
614 fn size(&self) -> u32 {
615 match self {
616 Self::Artifacts(file, ..) => file.size,
617 Self::Snapshot(file) => file.size,
618 }
619 }
620
621 fn expression_count(&self) -> usize {
622 match self {
623 Self::Artifacts(_, _, _, artifacts, _) => artifacts.expression_types.len(),
624 Self::Snapshot(file) => file.expression_types.len(),
625 }
626 }
627
628 fn expression_type(&self, span: &(u32, u32)) -> Option<TypeView<'_>> {
629 match self {
630 Self::Artifacts(_, _, _, artifacts, _) => {
631 artifacts.expression_types.get(span).map(AsRef::as_ref).map(TypeView::Union)
632 }
633 Self::Snapshot(file) => file
634 .expression_types
635 .get(span)
636 .and_then(|range| file.encoded_types.get(range.clone()))
637 .map(TypeView::Encoded),
638 }
639 }
640
641 fn nearby_expression_spans(&self, requested: &[(u32, u32)]) -> Vec<(u32, u32)> {
642 let mut spans = match self {
643 Self::Artifacts(_, _, _, artifacts, _) => artifacts.expression_types.keys().copied().collect::<Vec<_>>(),
644 Self::Snapshot(file) => file.expression_types.keys().copied().collect::<Vec<_>>(),
645 };
646 spans.sort_unstable();
647
648 let requested = requested.iter().copied().collect::<HashSet<_>>();
649 let mut selected = HashSet::default();
650 let mut nearby = Vec::with_capacity(EXPRESSION_TYPE_PREFETCH.min(spans.len()));
651 for span in &requested {
652 let index = spans.binary_search(span).unwrap_or_else(|index| index);
653 let start = index.saturating_sub(2);
654 let end = (start + EXPRESSION_TYPE_PREFETCH).min(spans.len());
655 for candidate in &spans[start..end] {
656 if !requested.contains(candidate) && selected.insert(*candidate) {
657 nearby.push(*candidate);
658 }
659 }
660 }
661
662 nearby
663 }
664
665 fn inferred_return_count(&self) -> usize {
666 match self {
667 Self::Artifacts(_, _, _, artifacts, _) => artifacts.inferred_return_types.len(),
668 Self::Snapshot(file) => file.inferred_return_types.len(),
669 }
670 }
671
672 fn write_expression_types(&self, writer: &mut PayloadWriter) -> Result<(), ExternalAnalyzerError> {
673 let mut spans = match self {
674 Self::Artifacts(_, _, _, artifacts, _) => artifacts.expression_types.keys().copied().collect::<Vec<_>>(),
675 Self::Snapshot(file) => file.expression_types.keys().copied().collect::<Vec<_>>(),
676 };
677
678 spans.sort_unstable();
679 writer.write_u32(u32::try_from(spans.len()).map_err(|_| protocol("too many expression types"))?);
680 for span in spans {
681 writer.write_u32(span.0);
682 writer.write_u32(span.1);
683 let ty = self
684 .expression_type(&span)
685 .ok_or_else(|| protocol("expression type disappeared while encoding analysis artifacts"))?;
686 ty.write_to(writer).map_err(|error| {
687 protocol(format!(
688 "failed to encode expression type at {}:{} in `{}`: {error}",
689 span.0,
690 span.1,
691 String::from_utf8_lossy(self.name())
692 ))
693 })?;
694 }
695
696 Ok(())
697 }
698
699 fn write_expression_type_snapshot(&self, writer: &mut PayloadWriter) -> Result<(), ExternalAnalyzerError> {
700 match self {
701 Self::Artifacts(_, _, _, artifacts, _) => {
702 let mut spans = artifacts.expression_types.keys().copied().collect::<Vec<_>>();
703 spans.sort_unstable();
704 let mut encoded = PayloadWriter::new();
705 let mut handles = Vec::new();
706 let mut records = Vec::with_capacity(spans.len());
707 for span in spans {
708 let ty = artifacts
709 .expression_types
710 .get(&span)
711 .ok_or_else(|| protocol("expression type disappeared while snapshotting analysis artifacts"))?;
712 let range = encode_snapshot_type(&mut encoded, ty, &mut handles)?;
713 records.push((span, range));
714 }
715 write_expression_type_records(writer, &records, &encoded.finish())
716 }
717 Self::Snapshot(file) => {
718 let mut records =
719 file.expression_types.iter().map(|(span, range)| (*span, range.clone())).collect::<Vec<_>>();
720 records.sort_unstable_by_key(|(span, _)| *span);
721 let length = records.iter().map(|(_, range)| range.end).max().unwrap_or(0);
722 let encoded = file
723 .encoded_types
724 .get(..length)
725 .ok_or_else(|| protocol("expression type snapshot lies outside retained analysis artifacts"))?;
726 write_expression_type_records(writer, &records, encoded)
727 }
728 }
729 }
730
731 fn write_inferred_return_types(&self, writer: &mut PayloadWriter) -> Result<(), ExternalAnalyzerError> {
732 match self {
733 Self::Artifacts(_, _, _, artifacts, _) => {
734 writer.write_u32(
735 u32::try_from(artifacts.inferred_return_types.len())
736 .map_err(|_| protocol("too many inferred types"))?,
737 );
738 for ty in &artifacts.inferred_return_types {
739 write_type(writer, ty)?;
740 }
741 }
742 Self::Snapshot(file) => {
743 write_encoded_types(writer, &file.encoded_types, &file.inferred_return_types)?;
744 }
745 }
746 Ok(())
747 }
748
749 fn inferred_yield_key_count(&self) -> usize {
750 match self {
751 Self::Artifacts(_, _, _, artifacts, _) => artifacts.inferred_yield_key_types.len(),
752 Self::Snapshot(file) => file.inferred_yield_key_types.len(),
753 }
754 }
755
756 fn inferred_yield_value_count(&self) -> usize {
757 match self {
758 Self::Artifacts(_, _, _, artifacts, _) => artifacts.inferred_yield_value_types.len(),
759 Self::Snapshot(file) => file.inferred_yield_value_types.len(),
760 }
761 }
762
763 fn write_inferred_yield_key_types(&self, writer: &mut PayloadWriter) -> Result<(), ExternalAnalyzerError> {
764 match self {
765 Self::Artifacts(_, _, _, artifacts, _) => write_types(writer, &artifacts.inferred_yield_key_types),
766 Self::Snapshot(file) => write_encoded_types(writer, &file.encoded_types, &file.inferred_yield_key_types),
767 }
768 }
769
770 fn write_inferred_yield_value_types(&self, writer: &mut PayloadWriter) -> Result<(), ExternalAnalyzerError> {
771 match self {
772 Self::Artifacts(_, _, _, artifacts, _) => write_types(writer, &artifacts.inferred_yield_value_types),
773 Self::Snapshot(file) => write_encoded_types(writer, &file.encoded_types, &file.inferred_yield_value_types),
774 }
775 }
776
777 fn write_reference_summary(&self, writer: &mut PayloadWriter) {
778 match self {
779 Self::Artifacts(_, _, _, artifacts, _) => write_reference_summary(writer, &artifacts.symbol_references),
780 Self::Snapshot(file) => file.references.write_to(writer),
781 }
782 }
783
784 fn file_id(&self) -> mago_database::file::FileId {
785 match self {
786 Self::Artifacts(file, ..) => file.id,
787 Self::Snapshot(file) => file.file_id,
788 }
789 }
790
791 fn write_source_snapshot(&self, writer: &mut PayloadWriter) -> Result<(), ExternalAnalyzerError> {
792 match self {
793 Self::Artifacts(file, program, resolved_names, _, targets) => {
794 let snapshot =
795 SourceSnapshot::complete_with_targets(program, resolved_names, *targets).map_err(|error| {
796 protocol(format!(
797 "failed to snapshot syntax for `{}`: {error}",
798 String::from_utf8_lossy(&file.name)
799 ))
800 })?;
801
802 snapshot.write_to(writer).map_err(|error| {
803 protocol(format!("failed to encode syntax for `{}`: {error}", String::from_utf8_lossy(&file.name)))
804 })
805 }
806 Self::Snapshot(file) => {
807 writer.write_raw(&file.encoded_source);
808 Ok(())
809 }
810 }
811 }
812
813 fn write_target_source_snapshot(
814 &self,
815 writer: &mut PayloadWriter,
816 plan: Option<&NodeAnalysisPlan<'_, '_>>,
817 include_trivia: bool,
818 ) -> Result<(), ExternalAnalyzerError> {
819 match self {
820 Self::Artifacts(file, program, resolved_names, artifacts, _) => {
821 let plan = plan.ok_or_else(|| protocol("targeted file summary is missing its node-analysis plan"))?;
822 let snapshot = SourceSnapshot::targeted_with_filter(
823 program,
824 resolved_names,
825 |node| plan.configuration(node),
826 include_trivia,
827 )
828 .map_err(|error| {
829 protocol(format!(
830 "failed to snapshot targeted syntax for `{}`: {error}",
831 String::from_utf8_lossy(&file.name)
832 ))
833 })?
834 .ok_or_else(|| protocol("targeted file summary contains no matching syntax nodes"))?;
835 snapshot.write_to(writer).map_err(|error| {
836 protocol(format!(
837 "failed to encode targeted syntax for `{}`: {error}",
838 String::from_utf8_lossy(&file.name)
839 ))
840 })?;
841 write_target_analysis(writer, artifacts, plan, snapshot.target_count())
842 }
843 Self::Snapshot(file) => {
844 writer.write_raw(&file.encoded_target_source);
845 writer.write_raw(&file.encoded_target_analysis);
846 Ok(())
847 }
848 }
849 }
850}
851
852fn write_expression_type_records(
853 writer: &mut PayloadWriter,
854 records: &[((u32, u32), Range<usize>)],
855 encoded: &[u8],
856) -> Result<(), ExternalAnalyzerError> {
857 writer.write_u32(u32::try_from(records.len()).map_err(|_| protocol("too many expression types"))?);
858 for (span, range) in records {
859 writer.write_u32(span.0);
860 writer.write_u32(span.1);
861 writer.write_u32(u32::try_from(range.start).map_err(|_| protocol("expression type offset exceeds u32::MAX"))?);
862 writer.write_u32(u32::try_from(range.len()).map_err(|_| protocol("expression type length exceeds u32::MAX"))?);
863 }
864 writer.write_bytes(encoded)?;
865 Ok(())
866}
867
868pub(super) fn encode_before_analysis_request(
869 generation: u64,
870 plugins: &[u16],
871) -> Result<Vec<u8>, ExternalAnalyzerError> {
872 let writer = lifecycle_writer(BEFORE_ANALYSIS_REQUEST, generation, plugins)?;
873 Ok(writer.finish())
874}
875
876pub(super) fn encode_after_file_analysis_request(
877 generation: u64,
878 plugins: &[u16],
879 file: &File,
880 program: &Program<'_>,
881 resolved_names: &ResolvedNames<'_>,
882 artifacts: &AnalysisArtifacts,
883 codebase: &CodebaseMetadata,
884 include_expression_types: bool,
885 node_analysis_requirements: Option<&NodeAnalysisRequirements>,
886 backend: u16,
887) -> Result<Vec<u8>, ExternalAnalyzerError> {
888 let mut writer = lifecycle_writer(AFTER_FILE_ANALYSIS_REQUEST, generation, plugins)?;
889 let node_analysis_plan = node_analysis_requirements
890 .map(|requirements| build_node_analysis_plan(program, artifacts, resolved_names, codebase, requirements));
891 write_file_summary(
892 &mut writer,
893 FileView::Artifacts(
894 file,
895 program,
896 resolved_names,
897 artifacts,
898 node_analysis_requirements.map(NodeAnalysisRequirements::targets),
899 ),
900 include_expression_types,
901 node_analysis_requirements,
902 node_analysis_plan.as_ref(),
903 backend,
904 node_analysis_requirements
905 .is_some_and(NodeAnalysisRequirements::includes_source_text)
906 .then_some(file.contents.as_ref()),
907 )?;
908 Ok(writer.finish())
909}
910
911pub(super) fn encode_after_file_analysis_batch_request(
912 generation: u64,
913 plugins: &[u16],
914 files: &[Arc<FileAnalysisSnapshot>],
915 include_expression_types: bool,
916 node_analysis_requirements: Option<&NodeAnalysisRequirements>,
917 session: &ExternalAnalysisSession,
918 backend: u16,
919) -> Result<Vec<u8>, ExternalAnalyzerError> {
920 let mut writer = lifecycle_writer(AFTER_FILE_ANALYSIS_BATCH_REQUEST, generation, plugins)?;
921 writer.write_u32(u32::try_from(files.len()).map_err(|_| protocol("after-file batch exceeds u32::MAX files"))?);
922 for file in files {
923 let file_node_analysis_requirements = node_analysis_requirements.filter(|_| file.has_node_analysis_targets());
924 let contents = if file_node_analysis_requirements.is_some_and(NodeAnalysisRequirements::includes_source_text) {
925 Some(
926 session
927 .source_file(file.file_id())
928 .filter(|source| source.name.as_ref() == file.name.as_ref() && source.size == file.size)
929 .ok_or_else(|| {
930 protocol(format!(
931 "analysis source `{}` is unavailable in generation {}",
932 String::from_utf8_lossy(&file.name),
933 session.generation()
934 ))
935 })?
936 .contents
937 .as_ref(),
938 )
939 } else {
940 None
941 };
942 write_file_summary(
943 &mut writer,
944 FileView::Snapshot(file),
945 include_expression_types,
946 file_node_analysis_requirements,
947 None,
948 backend,
949 contents,
950 )?;
951 }
952
953 Ok(writer.finish())
954}
955
956pub(super) fn encode_after_analysis_request(
957 generation: u64,
958 plugins: &[u16],
959 result: &AnalysisResult,
960 files: &[Arc<FileAnalysisSnapshot>],
961) -> Result<Vec<u8>, ExternalAnalyzerError> {
962 let mut writer = lifecycle_writer(AFTER_ANALYSIS_REQUEST, generation, plugins)?;
963 writer
964 .write_u32(u32::try_from(result.issues.len()).map_err(|_| protocol("analysis has more than u32::MAX issues"))?);
965 write_reference_summary(&mut writer, &result.symbol_references);
966 writer.write_u32(u32::try_from(files.len()).map_err(|_| protocol("analysis has more than u32::MAX files"))?);
967 for file in files {
968 write_file_summary(&mut writer, FileView::Snapshot(file), false, None, None, 0, None)?;
969 }
970
971 Ok(writer.finish())
972}
973
974fn lifecycle_writer(kind: u16, generation: u64, plugins: &[u16]) -> Result<PayloadWriter, ExternalAnalyzerError> {
975 let mut writer = protocol::message_writer(kind);
976 writer.write_u64(generation);
977 writer
978 .write_u16(u16::try_from(plugins.len()).map_err(|_| protocol("more than u16::MAX lifecycle plugins matched"))?);
979 for plugin in plugins {
980 writer.write_u16(*plugin);
981 }
982
983 Ok(writer)
984}
985
986fn write_file_summary(
987 writer: &mut PayloadWriter,
988 file: FileView<'_>,
989 include_expression_types: bool,
990 node_analysis_requirements: Option<&NodeAnalysisRequirements>,
991 node_analysis_plan: Option<&NodeAnalysisPlan<'_, '_>>,
992 backend: u16,
993 source_contents: Option<&[u8]>,
994) -> Result<(), ExternalAnalyzerError> {
995 writer.write_bytes(file.name())?;
996 writer.write_u32(file.size());
997 writer.write_u32(u32::try_from(file.expression_count()).map_err(|_| protocol("too many expression types"))?);
998 writer.write_u32(u32::try_from(file.inferred_return_count()).map_err(|_| protocol("too many return types"))?);
999 writer.write_u32(u32::try_from(file.inferred_yield_key_count()).map_err(|_| protocol("too many yield key types"))?);
1000
1001 writer.write_u32(
1002 u32::try_from(file.inferred_yield_value_count()).map_err(|_| protocol("too many yield value types"))?,
1003 );
1004
1005 file.write_reference_summary(writer);
1006 writer.write_bool(include_expression_types);
1007 if include_expression_types {
1008 file.write_expression_type_snapshot(writer)?;
1009 }
1010 writer.write_bool(node_analysis_requirements.is_some());
1011 if node_analysis_requirements.is_some() {
1012 writer.write_u16(backend);
1013 writer.write_bool(source_contents.is_some());
1014 if let Some(contents) = source_contents {
1015 writer.write_bytes(contents)?;
1016 }
1017 file.write_target_source_snapshot(
1018 writer,
1019 node_analysis_plan,
1020 node_analysis_requirements.is_some_and(NodeAnalysisRequirements::includes_source_text),
1021 )?;
1022 }
1023 Ok(())
1024}
1025
1026fn write_reference_summary(writer: &mut PayloadWriter, references: &SymbolReferences) {
1027 writer.write_u64(references.count_body_references() as u64);
1028 writer.write_u64(references.count_signature_references() as u64);
1029 writer.write_u64(references.total_map_entries() as u64);
1030}
1031
1032pub(super) fn decode_lifecycle_response(
1033 payload: &[u8],
1034 request_kind: u16,
1035 active_plugins: &[u16],
1036 plugins: &[ExternalPlugin],
1037 session: &ExternalAnalysisSession,
1038 default_file: Option<&File>,
1039 codebase: &CodebaseMetadata,
1040) -> Result<LifecycleEffects, ExternalAnalyzerError> {
1041 let response_kind = match request_kind {
1042 BEFORE_ANALYSIS_REQUEST => BEFORE_ANALYSIS_RESPONSE,
1043 AFTER_FILE_ANALYSIS_REQUEST => AFTER_FILE_ANALYSIS_RESPONSE,
1044 AFTER_ANALYSIS_REQUEST => AFTER_ANALYSIS_RESPONSE,
1045 AFTER_FILE_ANALYSIS_BATCH_REQUEST => AFTER_FILE_ANALYSIS_BATCH_RESPONSE,
1046 _ => return Err(protocol(format!("unknown lifecycle request kind {request_kind}"))),
1047 };
1048
1049 let mut reader = protocol::message_reader(payload, response_kind)?;
1050 let count = reader.read_count("lifecycle issues", MAXIMUM_ISSUES)?;
1051 let mut issues = IssueCollection::new();
1052 issues.reserve(count);
1053 for _ in 0..count {
1054 let plugin_index = reader.read_u16("lifecycle issue plugin index")?;
1055 if !active_plugins.contains(&plugin_index) {
1056 return Err(protocol(format!("worker reported an issue for inactive plugin index {plugin_index}")));
1057 }
1058
1059 let plugin = plugins
1060 .get(plugin_index as usize)
1061 .ok_or_else(|| protocol(format!("worker reported unknown plugin index {plugin_index}")))?;
1062 let level = read_level(&mut reader)?;
1063 let local_code = reader.read_string("lifecycle issue code")?;
1064 if local_code.is_empty() {
1065 return Err(protocol(format!("plugin `{}` reported an empty issue code", plugin.identifier)));
1066 }
1067
1068 let message = reader.read_string("lifecycle issue message")?;
1069 if message.is_empty() {
1070 return Err(protocol(format!("plugin `{}` reported an empty issue message", plugin.identifier)));
1071 }
1072
1073 let note_count = reader.read_count("lifecycle issue notes", MAXIMUM_NOTES)?;
1074 let mut notes = Vec::with_capacity(note_count);
1075 for _ in 0..note_count {
1076 notes.push(reader.read_string("lifecycle issue note")?);
1077 }
1078
1079 let help = reader.read_optional_string("lifecycle issue help")?;
1080 let link = reader.read_optional_string("lifecycle issue link")?;
1081 let annotation_count = reader.read_count("lifecycle issue annotations", MAXIMUM_ANNOTATIONS)?;
1082 let mut annotations = Vec::with_capacity(annotation_count);
1083 let mut has_primary = false;
1084 for _ in 0..annotation_count {
1085 let kind = match reader.read_u8("lifecycle annotation kind")? {
1086 1 => AnnotationKind::Primary,
1087 2 => AnnotationKind::Secondary,
1088 value => return Err(protocol(format!("invalid lifecycle annotation kind {value}"))),
1089 };
1090
1091 let named_file = if reader.read_bool("lifecycle annotation file presence")? {
1092 Some(reader.read_bytes("lifecycle annotation file")?)
1093 } else {
1094 None
1095 };
1096
1097 let (file_id, size) = match named_file {
1098 Some(name) => session.source(name).ok_or_else(|| {
1099 protocol(format!("lifecycle annotation names unknown file `{}`", String::from_utf8_lossy(name)))
1100 })?,
1101 None => default_file
1102 .map(|file| (file.id, file.size))
1103 .ok_or_else(|| protocol("a project lifecycle annotation must name its source file"))?,
1104 };
1105
1106 let start = reader.read_u32("lifecycle annotation start")?;
1107 let end = reader.read_u32("lifecycle annotation end")?;
1108 if start > end || end > size {
1109 return Err(protocol(format!(
1110 "plugin `{}` reported invalid annotation span {start}..{end}",
1111 plugin.identifier
1112 )));
1113 }
1114
1115 has_primary |= kind == AnnotationKind::Primary;
1116 let mut annotation = Annotation::new(kind, Span::new(file_id, Position::new(start), Position::new(end)));
1117 if let Some(message) = reader.read_optional_string("lifecycle annotation message")? {
1118 annotation = annotation.with_message(message);
1119 }
1120
1121 annotations.push(annotation);
1122 }
1123
1124 if !has_primary {
1127 return Err(protocol(format!(
1128 "plugin `{}` reported an issue without a primary annotation",
1129 plugin.identifier
1130 )));
1131 }
1132
1133 let edit_count = reader.read_count("lifecycle issue edits", MAXIMUM_EDITS)?;
1134 let mut edits = HashMap::default();
1135 for _ in 0..edit_count {
1136 let named_file = if reader.read_bool("lifecycle edit file presence")? {
1137 Some(reader.read_bytes("lifecycle edit file")?)
1138 } else {
1139 None
1140 };
1141
1142 let (file_id, size) = match named_file {
1143 Some(name) => session.source(name).ok_or_else(|| {
1144 protocol(format!("lifecycle edit names unknown file `{}`", String::from_utf8_lossy(name)))
1145 })?,
1146 None => default_file
1147 .map(|file| (file.id, file.size))
1148 .ok_or_else(|| protocol("a project lifecycle edit must name its source file"))?,
1149 };
1150
1151 let start = reader.read_u32("lifecycle edit start")?;
1152 let end = reader.read_u32("lifecycle edit end")?;
1153 if start > end || end > size {
1154 return Err(protocol(format!(
1155 "plugin `{}` reported invalid edit range {start}..{end}",
1156 plugin.identifier
1157 )));
1158 }
1159
1160 let safety = read_safety(&mut reader)?;
1161 let new_text = reader.read_bytes("lifecycle edit replacement")?.to_vec();
1162 edits
1163 .entry(file_id)
1164 .or_insert_with(Vec::new)
1165 .push(TextEdit::replace(TextRange::new(start, end), new_text).with_safety(safety));
1166 }
1167
1168 let mut issue = Issue::new(level, message)
1169 .with_code(format!("{}/{}", plugin.identifier, local_code))
1170 .with_annotations(annotations);
1171 issue.notes = notes;
1172 issue.help = help;
1173 issue.link = link;
1174 issue.edits = edits;
1175 issues.push(issue);
1176 }
1177
1178 let reference_count = reader.read_count("lifecycle contributed references", MAXIMUM_REFERENCES)?;
1179 if request_kind == AFTER_ANALYSIS_REQUEST && reference_count != 0 {
1180 return Err(protocol("after-analysis hooks may not contribute symbol references"));
1181 }
1182
1183 let mut references = SymbolReferences::new();
1184 let mut references_by_file = HashMap::<FileId, SymbolReferences>::default();
1185 for _ in 0..reference_count {
1186 let plugin_index = reader.read_u16("reference contribution plugin index")?;
1187 if !active_plugins.contains(&plugin_index) {
1188 return Err(protocol(format!("worker contributed a reference for inactive plugin index {plugin_index}")));
1189 }
1190
1191 let plugin = plugins.get(plugin_index as usize).ok_or_else(|| {
1192 protocol(format!("worker contributed a reference for unknown plugin index {plugin_index}"))
1193 })?;
1194
1195 let discovery_file = if request_kind == BEFORE_ANALYSIS_REQUEST {
1196 None
1197 } else {
1198 let name = reader.read_bytes("reference contribution discovery file")?;
1199 Some(
1200 session
1201 .source(name)
1202 .ok_or_else(|| {
1203 protocol(format!(
1204 "reference contribution names unknown discovery file `{}`",
1205 String::from_utf8_lossy(name)
1206 ))
1207 })?
1208 .0,
1209 )
1210 };
1211
1212 let source = read_reference_origin(&mut reader, codebase)?;
1213 let target = read_symbol_identifier(&mut reader, codebase, true)?;
1214 let kind = read_reference_kind(&mut reader)?;
1215 validate_reference_target(codebase, target, kind).map_err(|reason| {
1216 protocol(format!("plugin `{}` contributed an invalid reference: {reason}", plugin.identifier))
1217 })?;
1218
1219 let references =
1220 discovery_file.map_or(&mut references, |file_id| references_by_file.entry(file_id).or_default());
1221 match kind {
1222 SymbolReferenceKind::Body => references.add_reference(source, target, false),
1223 SymbolReferenceKind::Signature => references.add_reference(source, target, true),
1224 SymbolReferenceKind::OverriddenMember => {
1225 let ReferenceOrigin::Symbol(source) = source else {
1226 return Err(protocol("an overridden-member reference must originate from a symbol"));
1227 };
1228 references.add_overridden_member_reference(source, target);
1229 }
1230 SymbolReferenceKind::FunctionLikeReturn => {
1231 let ReferenceOrigin::Symbol(source) = source else {
1232 return Err(protocol("a function-like-return reference must originate from a symbol"));
1233 };
1234 references.add_functionlike_return_reference(source, target);
1235 }
1236 SymbolReferenceKind::PropertyRead => {
1237 references.add_property_read_reference(source, target);
1238 }
1239 SymbolReferenceKind::PropertyWrite => {
1240 references.add_property_write_reference(source, target);
1241 }
1242 }
1243 }
1244
1245 reader.finish()?;
1246 Ok(LifecycleEffects { issues, references, references_by_file })
1247}
1248
1249fn read_safety(reader: &mut PayloadReader<'_>) -> Result<Safety, ExternalAnalyzerError> {
1250 match reader.read_u8("lifecycle edit safety")? {
1251 1 => Ok(Safety::Safe),
1252 2 => Ok(Safety::PotentiallyUnsafe),
1253 3 => Ok(Safety::Unsafe),
1254 value => Err(protocol(format!("invalid lifecycle text edit safety {value}"))),
1255 }
1256}
1257
1258pub(super) fn handle_analysis_query(
1259 payload: &[u8],
1260 session: &ExternalAnalysisSession,
1261 store: &AnalysisStore<'_>,
1262) -> Result<Vec<u8>, ExternalAnalyzerError> {
1263 let mut reader = protocol::message_reader(payload, ANALYSIS_QUERY_REQUEST)?;
1264 let generation = reader.read_u64("analysis query generation")?;
1265 if generation != session.generation() {
1266 return Err(protocol(format!(
1267 "analysis query generation {generation} does not match {}",
1268 session.generation()
1269 )));
1270 }
1271
1272 let operation = reader.read_u8("analysis query operation")?;
1273 let file_name = reader.read_bytes("analysis query file")?;
1274 let file = store.file(file_name).ok_or_else(|| {
1275 protocol(format!("analysis query names unavailable file `{}`", String::from_utf8_lossy(file_name)))
1276 })?;
1277
1278 let mut writer = protocol::message_writer(ANALYSIS_QUERY_RESPONSE);
1279 writer.write_u64(generation);
1280 writer.write_u8(operation);
1281 writer.write_bytes(file_name)?;
1282 match operation {
1283 GET_EXPRESSION_TYPES => {
1284 let count = reader.read_count("expression type queries", MAXIMUM_TYPE_QUERIES)?;
1285 let mut spans = Vec::with_capacity(count);
1286 for _ in 0..count {
1287 spans.push((reader.read_u32("expression start")?, reader.read_u32("expression end")?));
1288 }
1289 writer.write_u32(count as u32);
1290 for span in &spans {
1291 let ty = file.expression_type(span);
1292 writer.write_bool(ty.is_some());
1293 if let Some(ty) = ty {
1294 ty.write_to(&mut writer)?;
1295 }
1296 }
1297
1298 let nearby = file.nearby_expression_spans(&spans);
1299 writer.write_u32(nearby.len() as u32);
1300 for span in nearby {
1301 writer.write_u32(span.0);
1302 writer.write_u32(span.1);
1303 file.expression_type(&span)
1304 .ok_or_else(|| {
1305 protocol("prefetched expression type disappeared while encoding analysis artifacts")
1306 })?
1307 .write_to(&mut writer)?;
1308 }
1309 }
1310 GET_ALL_EXPRESSION_TYPES => file.write_expression_types(&mut writer)?,
1311 GET_INFERRED_RETURN_TYPES => file.write_inferred_return_types(&mut writer)?,
1312 GET_INFERRED_YIELD_KEY_TYPES => file.write_inferred_yield_key_types(&mut writer)?,
1313 GET_INFERRED_YIELD_VALUE_TYPES => file.write_inferred_yield_value_types(&mut writer)?,
1314 GET_SOURCE_FILE => {
1315 let source = session
1316 .source_file(file.file_id())
1317 .filter(|source| source.name.as_ref() == file.name() && source.size == file.size())
1318 .ok_or_else(|| {
1319 protocol(format!(
1320 "analysis source `{}` is unavailable in generation {generation}",
1321 String::from_utf8_lossy(file.name())
1322 ))
1323 })?;
1324
1325 writer.write_bytes(&source.contents)?;
1326 file.write_source_snapshot(&mut writer)?;
1327 }
1328 value => return Err(protocol(format!("unknown analysis query operation {value}"))),
1329 }
1330
1331 reader.finish()?;
1332 Ok(writer.finish())
1333}
1334
1335pub(super) fn is_symbol_reference_query(payload: &[u8]) -> Result<bool, ExternalAnalyzerError> {
1336 Ok(protocol::message_kind(payload)? == SYMBOL_REFERENCE_QUERY_REQUEST)
1337}
1338
1339pub(super) fn handle_symbol_reference_query(
1340 payload: &[u8],
1341 session: &ExternalAnalysisSession,
1342 codebase: &CodebaseMetadata,
1343 store: &SymbolReferenceStore<'_>,
1344) -> Result<Vec<u8>, ExternalAnalyzerError> {
1345 let mut reader = protocol::message_reader(payload, SYMBOL_REFERENCE_QUERY_REQUEST)?;
1346 let generation = reader.read_u64("symbol-reference query generation")?;
1347 if generation != session.generation() {
1348 return Err(protocol(format!(
1349 "symbol-reference query generation {generation} does not match {}",
1350 session.generation()
1351 )));
1352 }
1353
1354 let operation = reader.read_u8("symbol-reference query operation")?;
1355 let query_count = reader.read_count("symbol-reference queries", MAXIMUM_REFERENCE_QUERIES)?;
1356 let mut writer = protocol::message_writer(SYMBOL_REFERENCE_QUERY_RESPONSE);
1357 writer.write_u64(generation);
1358 writer.write_u8(operation);
1359 writer.write_u32(query_count as u32);
1360
1361 match operation {
1362 GET_REFERENCES_TO => {
1363 let mut queries = Vec::with_capacity(query_count);
1364 for _ in 0..query_count {
1365 let target = read_symbol_identifier(&mut reader, codebase, false)?;
1366 queries.push(target);
1367 }
1368
1369 for query in queries {
1370 write_recorded_references(&mut writer, store.references_to(query))?;
1371 }
1372 }
1373 GET_REFERENCES_FROM => {
1374 let mut queries = Vec::with_capacity(query_count);
1375 for _ in 0..query_count {
1376 let source = read_reference_origin(&mut reader, codebase)?;
1377 queries.push(source);
1378 }
1379
1380 for query in queries {
1381 write_recorded_references(&mut writer, store.references_from(query))?;
1382 }
1383 }
1384 unknown => return Err(protocol(format!("unknown symbol-reference query operation {unknown}"))),
1385 }
1386
1387 reader.finish()?;
1388 Ok(writer.finish())
1389}
1390
1391#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
1392struct RecordedReference {
1393 source: ReferenceOrigin,
1394 target: SymbolIdentifier,
1395 kind: SymbolReferenceKind,
1396}
1397
1398fn write_recorded_references(
1399 writer: &mut PayloadWriter,
1400 references: &[RecordedReference],
1401) -> Result<(), ExternalAnalyzerError> {
1402 writer.write_u32(u32::try_from(references.len()).map_err(|_| protocol("too many symbol references"))?);
1403 for reference in references {
1404 write_reference_origin(writer, reference.source)?;
1405 write_symbol_identifier(writer, reference.target)?;
1406 writer.write_u8(match reference.kind {
1407 SymbolReferenceKind::Body => 1,
1408 SymbolReferenceKind::Signature => 2,
1409 SymbolReferenceKind::OverriddenMember => 3,
1410 SymbolReferenceKind::FunctionLikeReturn => 4,
1411 SymbolReferenceKind::PropertyRead => 5,
1412 SymbolReferenceKind::PropertyWrite => 6,
1413 });
1414 }
1415
1416 Ok(())
1417}
1418
1419fn read_reference_origin(
1420 reader: &mut PayloadReader<'_>,
1421 codebase: &CodebaseMetadata,
1422) -> Result<ReferenceOrigin, ExternalAnalyzerError> {
1423 let endpoint = reader.read_u8("reference origin kind")?;
1424 if endpoint == 3 {
1425 let file = reader.read_bytes("reference origin file")?;
1426 if file.is_empty() {
1427 return Err(protocol("reference origin file cannot be empty"));
1428 }
1429
1430 return Ok(ReferenceOrigin::File(word(file)));
1431 }
1432
1433 read_symbol_identifier_with_kind(reader, codebase, false, endpoint).map(ReferenceOrigin::Symbol)
1434}
1435
1436fn write_reference_origin(writer: &mut PayloadWriter, source: ReferenceOrigin) -> Result<(), ExternalAnalyzerError> {
1437 match source {
1438 ReferenceOrigin::Symbol(symbol) => write_symbol_identifier(writer, symbol),
1439 ReferenceOrigin::File(file) => {
1440 if file.is_empty() {
1441 return Err(protocol("reference origin file cannot be empty"));
1442 }
1443 writer.write_u8(3);
1444 writer.write_bytes(file.as_bytes())?;
1445 Ok(())
1446 }
1447 }
1448}
1449
1450fn read_symbol_identifier(
1451 reader: &mut PayloadReader<'_>,
1452 codebase: &CodebaseMetadata,
1453 target: bool,
1454) -> Result<SymbolIdentifier, ExternalAnalyzerError> {
1455 let kind = reader.read_u8("symbol reference endpoint kind")?;
1456 read_symbol_identifier_with_kind(reader, codebase, target, kind)
1457}
1458
1459fn read_symbol_identifier_with_kind(
1460 reader: &mut PayloadReader<'_>,
1461 codebase: &CodebaseMetadata,
1462 target: bool,
1463 kind: u8,
1464) -> Result<SymbolIdentifier, ExternalAnalyzerError> {
1465 let symbol = reader.read_bytes("symbol reference name")?;
1466 if symbol.is_empty() {
1467 return Err(protocol("symbol reference name cannot be empty"));
1468 }
1469 let symbol = normalize_symbol(codebase, symbol, target);
1470
1471 match kind {
1472 1 => Ok((symbol, empty_word())),
1473 2 => {
1474 let member = reader.read_bytes("symbol reference member")?;
1475 if member.is_empty() {
1476 return Err(protocol("symbol reference member cannot be empty"));
1477 }
1478 let raw_member = word(member);
1479 let lowercase_member = ascii_lowercase_word(member);
1480 let member = if codebase.function_likes.contains_key(&(symbol, lowercase_member)) {
1481 lowercase_member
1482 } else {
1483 raw_member
1484 };
1485 Ok((symbol, member))
1486 }
1487 unknown => Err(protocol(format!("unknown symbol reference endpoint kind {unknown}"))),
1488 }
1489}
1490
1491fn normalize_symbol(codebase: &CodebaseMetadata, bytes: &[u8], target: bool) -> mago_word::Word {
1492 let lowercase = ascii_lowercase_word(bytes);
1493 if codebase.class_likes.contains_key(&lowercase) || codebase.function_likes.contains_key(&(empty_word(), lowercase))
1494 {
1495 return lowercase;
1496 }
1497
1498 let constant = ascii_lowercase_constant_name_word(bytes);
1499 if codebase.constants.contains_key(&constant) {
1500 return constant;
1501 }
1502
1503 if target { word(bytes) } else { lowercase }
1504}
1505
1506fn write_symbol_identifier(writer: &mut PayloadWriter, symbol: SymbolIdentifier) -> Result<(), ExternalAnalyzerError> {
1507 if symbol.0.is_empty() {
1508 return Err(protocol("symbol reference name cannot be empty"));
1509 }
1510
1511 if symbol.1.is_empty() {
1512 writer.write_u8(1);
1513 writer.write_bytes(symbol.0.as_bytes())?;
1514 } else {
1515 writer.write_u8(2);
1516 writer.write_bytes(symbol.0.as_bytes())?;
1517 writer.write_bytes(symbol.1.as_bytes())?;
1518 }
1519
1520 Ok(())
1521}
1522
1523fn read_reference_kind(reader: &mut PayloadReader<'_>) -> Result<SymbolReferenceKind, ExternalAnalyzerError> {
1524 match reader.read_u8("symbol reference kind")? {
1525 1 => Ok(SymbolReferenceKind::Body),
1526 2 => Ok(SymbolReferenceKind::Signature),
1527 3 => Ok(SymbolReferenceKind::OverriddenMember),
1528 4 => Ok(SymbolReferenceKind::FunctionLikeReturn),
1529 5 => Ok(SymbolReferenceKind::PropertyRead),
1530 6 => Ok(SymbolReferenceKind::PropertyWrite),
1531 unknown => Err(protocol(format!("unknown symbol reference kind {unknown}"))),
1532 }
1533}
1534
1535fn validate_reference_target(
1536 codebase: &CodebaseMetadata,
1537 target: SymbolIdentifier,
1538 kind: SymbolReferenceKind,
1539) -> Result<(), String> {
1540 if target.1.is_empty() {
1541 let exists = if kind == SymbolReferenceKind::FunctionLikeReturn {
1542 codebase.function_likes.contains_key(&(empty_word(), target.0))
1543 } else {
1544 codebase.class_likes.contains_key(&target.0)
1545 || codebase.function_likes.contains_key(&(empty_word(), target.0))
1546 || codebase.constants.contains_key(&target.0)
1547 };
1548 return exists.then_some(()).ok_or_else(|| format!("target `{}` does not exist", target.0));
1549 }
1550
1551 let Some(class_like) = codebase.class_likes.get(&target.0) else {
1552 return Err(format!("target class-like `{}` does not exist", target.0));
1553 };
1554 let exists = match kind {
1555 SymbolReferenceKind::PropertyRead | SymbolReferenceKind::PropertyWrite => {
1556 class_like.properties.contains_key(&target.1)
1557 }
1558 SymbolReferenceKind::FunctionLikeReturn => codebase.function_likes.contains_key(&target),
1559 SymbolReferenceKind::OverriddenMember => class_like.methods.contains(&target.1),
1560 SymbolReferenceKind::Body | SymbolReferenceKind::Signature => {
1561 class_like.methods.contains(&target.1)
1562 || class_like.properties.contains_key(&target.1)
1563 || class_like.constants.contains_key(&target.1)
1564 || class_like.enum_cases.contains_key(&target.1)
1565 }
1566 };
1567
1568 exists.then_some(()).ok_or_else(|| format!("target member `{}::{}` does not exist", target.0, target.1))
1569}
1570
1571fn write_types(writer: &mut PayloadWriter, types: &[TUnion]) -> Result<(), ExternalAnalyzerError> {
1572 writer.write_u32(u32::try_from(types.len()).map_err(|_| protocol("too many inferred types"))?);
1573 for ty in types {
1574 write_type(writer, ty)?;
1575 }
1576
1577 Ok(())
1578}
1579
1580fn write_encoded_types(
1581 writer: &mut PayloadWriter,
1582 payload: &[u8],
1583 ranges: &[Range<usize>],
1584) -> Result<(), ExternalAnalyzerError> {
1585 writer.write_u32(u32::try_from(ranges.len()).map_err(|_| protocol("too many inferred types"))?);
1586 for range in ranges {
1587 let bytes = payload
1588 .get(range.clone())
1589 .ok_or_else(|| protocol("retained analysis type points outside its encoded payload"))?;
1590 writer.write_raw(bytes);
1591 }
1592
1593 Ok(())
1594}
1595
1596fn write_type(writer: &mut PayloadWriter, ty: &TUnion) -> Result<(), ExternalAnalyzerError> {
1597 protocol::encode_union_snapshot(writer, ty, &mut Vec::new(), 0)
1598}
1599
1600fn read_level(reader: &mut PayloadReader<'_>) -> Result<Level, ExternalAnalyzerError> {
1601 match reader.read_u8("lifecycle issue level")? {
1602 1 => Ok(Level::Note),
1603 2 => Ok(Level::Help),
1604 3 => Ok(Level::Warning),
1605 4 => Ok(Level::Error),
1606 value => Err(protocol(format!("invalid lifecycle issue level {value}"))),
1607 }
1608}
1609
1610#[cfg(test)]
1611mod tests {
1612 use mago_codex::metadata::CodebaseMetadata;
1613 use mago_codex::metadata::class_like::ClassLikeMetadata;
1614 use mago_codex::metadata::flags::MetadataFlags;
1615 use mago_codex::reference::ReferenceOrigin;
1616 use mago_extension::PayloadWriter;
1617 use mago_span::Span;
1618 use mago_word::empty_word;
1619 use mago_word::word;
1620
1621 use super::class_like_matches_hook;
1622 use super::write_reference_origin;
1623 use super::write_symbol_identifier;
1624 use crate::external::ClassLikeAnalysisHookRegistration;
1625
1626 #[test]
1627 fn descendant_target_excludes_the_ancestor_declaration() {
1628 let ancestor = word(b"frameworktestcase");
1629 let child = word(b"applicationtest");
1630 let mut codebase = CodebaseMetadata::new();
1631 codebase.class_likes.insert(
1632 ancestor,
1633 ClassLikeMetadata::new(
1634 ancestor,
1635 word(b"FrameworkTestCase"),
1636 Span::dummy(0, 10),
1637 None,
1638 MetadataFlags::empty(),
1639 ),
1640 );
1641 let mut child_metadata =
1642 ClassLikeMetadata::new(child, word(b"ApplicationTest"), Span::dummy(11, 20), None, MetadataFlags::empty());
1643 child_metadata.all_parent_classes.insert(ancestor);
1644 codebase.class_likes.insert(child, child_metadata);
1645
1646 let hook = ClassLikeAnalysisHookRegistration {
1647 plugin: 0,
1648 index: 0,
1649 requirements: 0,
1650 targets: vec![ancestor],
1651 route: 0,
1652 };
1653
1654 assert!(!class_like_matches_hook(b"FrameworkTestCase", &hook, &codebase));
1655 assert!(class_like_matches_hook(b"ApplicationTest", &hook, &codebase));
1656 }
1657
1658 #[test]
1659 fn outbound_reference_endpoints_reject_empty_names() {
1660 let mut writer = PayloadWriter::new();
1661 assert!(write_symbol_identifier(&mut writer, (empty_word(), empty_word())).is_err());
1662 assert!(write_reference_origin(&mut writer, ReferenceOrigin::File(empty_word())).is_err());
1663 }
1664}