Skip to main content

mago_analyzer/external/
lifecycle.rs

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
156/// Number of completed files sent through one external after-file request.
157pub 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    /// Builds a compact, thread-safe snapshot of one file's lazy analysis data.
305    ///
306    /// # Errors
307    ///
308    /// Returns an error when the source snapshot or an inferred type cannot be represented by the extension protocol.
309    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        // FIXME(azjezz): Should we forbid missing primary annotations? Rust-side can create issues without primary annotations,
1125        // but it messes up the baseline stuff.
1126        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}