mago_codex/scanner/
mod.rs

1use bumpalo::Bump;
2
3use mago_atom::Atom;
4use mago_atom::AtomMap;
5use mago_atom::AtomSet;
6use mago_atom::ascii_lowercase_atom;
7use mago_atom::atom;
8use mago_atom::empty_atom;
9use mago_atom::u32_atom;
10use mago_atom::u64_atom;
11use mago_database::file::File;
12use mago_names::ResolvedNames;
13use mago_names::scope::NamespaceScope;
14use mago_span::HasSpan;
15use mago_syntax::ast::*;
16use mago_syntax::comments::docblock::get_docblock_for_node;
17use mago_syntax::walker::MutWalker;
18use mago_syntax::walker::walk_anonymous_class_mut;
19use mago_syntax::walker::walk_class_mut;
20use mago_syntax::walker::walk_enum_mut;
21use mago_syntax::walker::walk_interface_mut;
22use mago_syntax::walker::walk_trait_mut;
23
24use crate::identifier::method::MethodIdentifier;
25use crate::metadata::CodebaseMetadata;
26use crate::metadata::flags::MetadataFlags;
27use crate::metadata::function_like::FunctionLikeKind;
28use crate::metadata::function_like::FunctionLikeMetadata;
29use crate::misc::GenericParent;
30use crate::scanner::class_like::*;
31use crate::scanner::constant::*;
32use crate::scanner::function_like::*;
33use crate::scanner::property::scan_promoted_property;
34use crate::ttype::resolution::TypeResolutionContext;
35use crate::ttype::union::TUnion;
36
37mod attribute;
38mod class_like;
39mod class_like_constant;
40mod constant;
41mod docblock;
42mod enum_case;
43mod function_like;
44mod inference;
45mod parameter;
46mod property;
47mod ttype;
48
49#[inline]
50pub fn scan_program<'arena, 'ctx>(
51    arena: &'arena Bump,
52    file: &'ctx File,
53    program: &'arena Program<'arena>,
54    resolved_names: &'ctx ResolvedNames<'arena>,
55) -> CodebaseMetadata {
56    let mut context = Context::new(arena, file, program, resolved_names);
57    let mut scanner = Scanner::new();
58
59    scanner.walk_program(program, &mut context);
60
61    scanner.codebase
62}
63
64#[derive(Clone, Debug)]
65struct Context<'ctx, 'arena> {
66    pub arena: &'arena Bump,
67    pub file: &'ctx File,
68    pub program: &'arena Program<'arena>,
69    pub resolved_names: &'arena ResolvedNames<'arena>,
70}
71
72impl<'ctx, 'arena> Context<'ctx, 'arena> {
73    pub fn new(
74        arena: &'arena Bump,
75        file: &'ctx File,
76        program: &'arena Program<'arena>,
77        resolved_names: &'arena ResolvedNames<'arena>,
78    ) -> Self {
79        Self { arena, file, program, resolved_names }
80    }
81
82    pub fn get_docblock(&self, node: impl HasSpan) -> Option<&'arena Trivia<'arena>> {
83        get_docblock_for_node(self.program, self.file, node)
84    }
85}
86
87type TemplateConstraint = (Atom, Vec<(GenericParent, TUnion)>);
88type TemplateConstraintList = Vec<TemplateConstraint>;
89
90#[derive(Debug, Default)]
91struct Scanner {
92    codebase: CodebaseMetadata,
93    stack: Vec<Atom>,
94    template_constraints: Vec<TemplateConstraintList>,
95    scope: NamespaceScope,
96    has_constructor: bool,
97    file_type_aliases: AtomSet,
98    file_imported_aliases: AtomMap<(Atom, Atom)>,
99}
100
101impl Scanner {
102    pub fn new() -> Self {
103        Self::default()
104    }
105
106    fn get_current_type_resolution_context(&self) -> TypeResolutionContext {
107        let mut context = TypeResolutionContext::new();
108        context = context.with_type_aliases(self.file_type_aliases.clone());
109
110        // Add imported aliases
111        for (local_name, (source_class, original_name)) in &self.file_imported_aliases {
112            context = context.with_imported_type_alias(*local_name, *source_class, *original_name);
113        }
114
115        for template_constraint_list in self.template_constraints.iter().rev() {
116            for (name, constraints) in template_constraint_list {
117                if !context.has_template_definition(name) {
118                    context = context.with_template_definition(*name, constraints.clone());
119                }
120            }
121        }
122
123        context
124    }
125}
126
127impl<'ctx, 'arena> MutWalker<'arena, 'arena, Context<'ctx, 'arena>> for Scanner {
128    #[inline]
129    fn walk_in_namespace(&mut self, namespace: &'arena Namespace<'arena>, _context: &mut Context<'ctx, 'arena>) {
130        self.scope = match &namespace.name {
131            Some(name) => NamespaceScope::for_namespace(name.value()),
132            None => NamespaceScope::global(),
133        };
134    }
135
136    #[inline]
137    fn walk_out_namespace(&mut self, _namespace: &'arena Namespace<'arena>, _context: &mut Context<'ctx, 'arena>) {
138        self.scope = NamespaceScope::global();
139    }
140
141    #[inline]
142    fn walk_in_use(&mut self, r#use: &'arena Use<'arena>, _context: &mut Context<'ctx, 'arena>) {
143        self.scope.populate_from_use(r#use);
144    }
145
146    #[inline]
147    fn walk_in_function(&mut self, function: &'arena Function<'arena>, context: &mut Context<'ctx, 'arena>) {
148        let type_context = self.get_current_type_resolution_context();
149
150        let name = ascii_lowercase_atom(context.resolved_names.get(&function.name));
151        let identifier = (empty_atom(), name);
152        let metadata =
153            scan_function(identifier, function, self.stack.last().copied(), context, &mut self.scope, type_context);
154
155        self.template_constraints.push({
156            let mut constraints: TemplateConstraintList = vec![];
157            for (template_name, template_constraints) in &metadata.template_types {
158                constraints.push((*template_name, template_constraints.to_vec()));
159            }
160
161            constraints
162        });
163
164        self.codebase.function_likes.insert(identifier, metadata);
165    }
166
167    #[inline]
168    fn walk_out_function(&mut self, _function: &'arena Function<'arena>, _context: &mut Context<'ctx, 'arena>) {
169        self.template_constraints.pop().expect("Expected template stack to be non-empty");
170    }
171
172    #[inline]
173    fn walk_in_closure(&mut self, closure: &'arena Closure<'arena>, context: &mut Context<'ctx, 'arena>) {
174        let span = closure.span();
175
176        let file_ref = u64_atom(span.file_id.as_u64());
177        let closure_ref = u32_atom(span.start.offset);
178        let identifier = (file_ref, closure_ref);
179
180        let type_resolution_context = self.get_current_type_resolution_context();
181        let metadata = scan_closure(
182            identifier,
183            closure,
184            self.stack.last().copied(),
185            context,
186            &mut self.scope,
187            type_resolution_context,
188        );
189
190        self.template_constraints.push({
191            let mut constraints: TemplateConstraintList = vec![];
192            for (template_name, template_constraints) in &metadata.template_types {
193                constraints.push((*template_name, template_constraints.to_vec()));
194            }
195
196            constraints
197        });
198
199        self.codebase.function_likes.insert(identifier, metadata);
200    }
201
202    #[inline]
203    fn walk_out_closure(&mut self, _closure: &'arena Closure<'arena>, _context: &mut Context<'ctx, 'arena>) {
204        self.template_constraints.pop().expect("Expected template stack to be non-empty");
205    }
206
207    #[inline]
208    fn walk_in_arrow_function(
209        &mut self,
210        arrow_function: &'arena ArrowFunction<'arena>,
211        context: &mut Context<'ctx, 'arena>,
212    ) {
213        let span = arrow_function.span();
214
215        let file_ref = u64_atom(span.file_id.as_u64());
216        let closure_ref = u32_atom(span.start.offset);
217        let identifier = (file_ref, closure_ref);
218
219        let type_resolution_context = self.get_current_type_resolution_context();
220
221        let metadata = scan_arrow_function(
222            identifier,
223            arrow_function,
224            self.stack.last().copied(),
225            context,
226            &mut self.scope,
227            type_resolution_context,
228        );
229
230        self.template_constraints.push({
231            let mut constraints: TemplateConstraintList = vec![];
232            for (template_name, template_constraints) in &metadata.template_types {
233                constraints.push((*template_name, template_constraints.to_vec()));
234            }
235
236            constraints
237        });
238        self.codebase.function_likes.insert(identifier, metadata);
239    }
240
241    #[inline]
242    fn walk_out_arrow_function(
243        &mut self,
244        _arrow_function: &'arena ArrowFunction<'arena>,
245        _context: &mut Context<'ctx, 'arena>,
246    ) {
247        self.template_constraints.pop().expect("Expected template stack to be non-empty");
248    }
249
250    #[inline]
251    fn walk_in_constant(&mut self, constant: &'arena Constant<'arena>, context: &mut Context<'ctx, 'arena>) {
252        let constants = scan_constant(constant, context, self.get_current_type_resolution_context(), &self.scope);
253
254        for constant_metadata in constants {
255            let constant_name = constant_metadata.name;
256            self.codebase.constants.insert(constant_name, constant_metadata);
257        }
258    }
259
260    #[inline]
261    fn walk_in_function_call(
262        &mut self,
263        function_call: &'arena FunctionCall<'arena>,
264        context: &mut Context<'ctx, 'arena>,
265    ) {
266        let Some(constant_metadata) =
267            scan_defined_constant(function_call, context, self.get_current_type_resolution_context(), &self.scope)
268        else {
269            return;
270        };
271
272        self.codebase.constants.insert(constant_metadata.name, constant_metadata);
273    }
274
275    #[inline]
276    fn walk_anonymous_class(
277        &mut self,
278        anonymous_class: &'arena AnonymousClass<'arena>,
279        context: &mut Context<'ctx, 'arena>,
280    ) {
281        if let Some((id, template_definition, type_aliases, imported_aliases)) =
282            register_anonymous_class(&mut self.codebase, anonymous_class, context, &mut self.scope)
283        {
284            self.file_type_aliases.extend(type_aliases);
285            self.file_imported_aliases.extend(imported_aliases);
286            self.stack.push(id);
287            self.template_constraints.push(template_definition);
288
289            walk_anonymous_class_mut(self, anonymous_class, context);
290        } else {
291            // We don't need to walk the anonymous class if it's already been registered
292        }
293    }
294
295    #[inline]
296    fn walk_class(&mut self, class: &'arena Class<'arena>, context: &mut Context<'ctx, 'arena>) {
297        if let Some((id, templates, type_aliases, imported_aliases)) =
298            register_class(&mut self.codebase, class, context, &mut self.scope)
299        {
300            self.file_type_aliases.extend(type_aliases);
301            self.file_imported_aliases.extend(imported_aliases);
302            self.stack.push(id);
303            self.template_constraints.push(templates);
304
305            walk_class_mut(self, class, context);
306        } else {
307            // We don't need to walk the class if it's already been registered
308        }
309    }
310
311    #[inline]
312    fn walk_trait(&mut self, r#trait: &'arena Trait<'arena>, context: &mut Context<'ctx, 'arena>) {
313        if let Some((id, templates, type_aliases, imported_aliases)) =
314            register_trait(&mut self.codebase, r#trait, context, &mut self.scope)
315        {
316            self.file_type_aliases.extend(type_aliases);
317            self.file_imported_aliases.extend(imported_aliases);
318            self.stack.push(id);
319            self.template_constraints.push(templates);
320
321            walk_trait_mut(self, r#trait, context);
322        } else {
323            // We don't need to walk the trait if it's already been registered
324        }
325    }
326
327    #[inline]
328    fn walk_enum(&mut self, r#enum: &'arena Enum<'arena>, context: &mut Context<'ctx, 'arena>) {
329        if let Some((id, templates, type_aliases, imported_aliases)) =
330            register_enum(&mut self.codebase, r#enum, context, &mut self.scope)
331        {
332            self.file_type_aliases.extend(type_aliases);
333            self.file_imported_aliases.extend(imported_aliases);
334            self.stack.push(id);
335            self.template_constraints.push(templates);
336
337            walk_enum_mut(self, r#enum, context);
338        } else {
339            // We don't need to walk the enum if it's already been registered
340        }
341    }
342
343    #[inline]
344    fn walk_interface(&mut self, interface: &'arena Interface<'arena>, context: &mut Context<'ctx, 'arena>) {
345        if let Some((id, templates, type_aliases, imported_aliases)) =
346            register_interface(&mut self.codebase, interface, context, &mut self.scope)
347        {
348            self.file_type_aliases.extend(type_aliases);
349            self.file_imported_aliases.extend(imported_aliases);
350            self.stack.push(id);
351            self.template_constraints.push(templates);
352
353            walk_interface_mut(self, interface, context);
354        }
355    }
356
357    #[inline]
358    fn walk_in_method(&mut self, method: &'arena Method<'arena>, context: &mut Context<'ctx, 'arena>) {
359        let current_class = self.stack.last().copied().expect("Expected class-like stack to be non-empty");
360        let mut class_like_metadata =
361            self.codebase.class_likes.remove(&current_class).expect("Expected class-like metadata to be present");
362
363        let name = ascii_lowercase_atom(method.name.value);
364
365        if class_like_metadata.methods.contains(&name) {
366            if class_like_metadata.pseudo_methods.contains(&name)
367                && let Some(existing_method) = self.codebase.function_likes.get_mut(&(class_like_metadata.name, name))
368            {
369                class_like_metadata.pseudo_methods.remove(&name);
370                existing_method.flags.remove(MetadataFlags::MAGIC_METHOD);
371            }
372
373            self.codebase.class_likes.insert(current_class, class_like_metadata);
374            self.template_constraints.push(vec![]);
375
376            return;
377        }
378
379        let method_id = (class_like_metadata.name, name);
380        let type_resolution = if method.is_static() {
381            if !class_like_metadata.type_aliases.is_empty() || !class_like_metadata.imported_type_aliases.is_empty() {
382                let mut context = TypeResolutionContext::new();
383
384                for alias_name in class_like_metadata.type_aliases.keys() {
385                    context = context.with_type_alias(*alias_name);
386                }
387
388                for (alias_name, (source_class, original_name, _span)) in &class_like_metadata.imported_type_aliases {
389                    context = context.with_imported_type_alias(*alias_name, *source_class, *original_name);
390                }
391
392                Some(context)
393            } else {
394                None
395            }
396        } else {
397            Some(self.get_current_type_resolution_context())
398        };
399
400        let mut function_like_metadata =
401            scan_method(method_id, method, &class_like_metadata, context, &mut self.scope, type_resolution);
402        let Some(method_metadata) = &function_like_metadata.method_metadata else {
403            unreachable!("Method info should be present for method.",);
404        };
405
406        let mut is_constructor = false;
407        let mut is_clone = false;
408        if method_metadata.is_constructor {
409            is_constructor = true;
410            self.has_constructor = true;
411
412            let type_context = self.get_current_type_resolution_context();
413            for (index, param) in method.parameter_list.parameters.iter().enumerate() {
414                if !param.is_promoted_property() {
415                    continue;
416                }
417
418                let Some(parameter_metadata) = function_like_metadata.parameters.get_mut(index) else {
419                    continue;
420                };
421
422                let property_metadata = scan_promoted_property(
423                    param,
424                    parameter_metadata,
425                    &mut class_like_metadata,
426                    current_class,
427                    &type_context,
428                    context,
429                    &self.scope,
430                );
431
432                class_like_metadata.add_property_metadata(property_metadata);
433            }
434        } else {
435            is_clone = name == atom("__clone");
436        }
437
438        class_like_metadata.methods.insert(name);
439        let method_identifier = MethodIdentifier::new(class_like_metadata.name, name);
440        class_like_metadata.add_declaring_method_id(name, method_identifier);
441        if !method_metadata.visibility.is_private() || is_constructor || is_clone || class_like_metadata.kind.is_trait()
442        {
443            class_like_metadata.inheritable_method_ids.insert(name, method_identifier);
444        }
445
446        if method_metadata.is_final && is_constructor {
447            class_like_metadata.flags |= MetadataFlags::CONSISTENT_CONSTRUCTOR;
448        }
449
450        self.template_constraints.push({
451            let mut constraints: TemplateConstraintList = vec![];
452            for (template_name, template_constraints) in &function_like_metadata.template_types {
453                constraints.push((*template_name, template_constraints.to_vec()));
454            }
455
456            constraints
457        });
458
459        self.codebase.class_likes.insert(current_class, class_like_metadata);
460        self.codebase.function_likes.insert(method_id, function_like_metadata);
461    }
462
463    #[inline]
464    fn walk_out_method(&mut self, _method: &'arena Method<'arena>, _context: &mut Context<'ctx, 'arena>) {
465        self.template_constraints.pop().expect("Expected template stack to be non-empty");
466    }
467
468    #[inline]
469    fn walk_out_anonymous_class(
470        &mut self,
471        _anonymous_class: &'arena AnonymousClass<'arena>,
472        _context: &mut Context<'ctx, 'arena>,
473    ) {
474        self.stack.pop().expect("Expected class stack to be non-empty");
475        self.template_constraints.pop().expect("Expected template stack to be non-empty");
476    }
477
478    #[inline]
479    fn walk_out_class(&mut self, _class: &'arena Class<'arena>, context: &mut Context<'ctx, 'arena>) {
480        finalize_class_like(self, context);
481    }
482
483    #[inline]
484    fn walk_out_trait(&mut self, _trait: &'arena Trait<'arena>, context: &mut Context<'ctx, 'arena>) {
485        finalize_class_like(self, context);
486    }
487
488    #[inline]
489    fn walk_out_enum(&mut self, _enum: &'arena Enum<'arena>, context: &mut Context<'ctx, 'arena>) {
490        finalize_class_like(self, context);
491    }
492
493    #[inline]
494    fn walk_out_interface(&mut self, _interface: &'arena Interface<'arena>, context: &mut Context<'ctx, 'arena>) {
495        finalize_class_like(self, context);
496    }
497}
498
499fn finalize_class_like<'ctx, 'arena>(scanner: &mut Scanner, context: &mut Context<'ctx, 'arena>) {
500    let has_constructor = scanner.has_constructor;
501    scanner.has_constructor = false;
502
503    let class_like_id = scanner.stack.pop().expect("Expected class stack to be non-empty");
504    scanner.template_constraints.pop().expect("Expected template stack to be non-empty");
505
506    if has_constructor {
507        return;
508    }
509
510    let Some(mut class_like_metadata) = scanner.codebase.class_likes.remove(&class_like_id) else {
511        return;
512    };
513
514    if class_like_metadata.flags.has_consistent_constructor() {
515        let constructor_name = atom("__construct");
516
517        class_like_metadata.methods.insert(constructor_name);
518        let constructor_method_id = MethodIdentifier::new(class_like_metadata.name, constructor_name);
519        class_like_metadata.add_declaring_method_id(constructor_name, constructor_method_id);
520        class_like_metadata.inheritable_method_ids.insert(constructor_name, constructor_method_id);
521
522        let mut flags = MetadataFlags::PURE;
523        if context.file.file_type.is_host() {
524            flags |= MetadataFlags::USER_DEFINED;
525        } else if context.file.file_type.is_builtin() {
526            flags |= MetadataFlags::BUILTIN;
527        }
528
529        scanner.codebase.function_likes.insert(
530            (class_like_metadata.name, constructor_name),
531            FunctionLikeMetadata::new(FunctionLikeKind::Method, class_like_metadata.span, flags),
532        );
533    }
534
535    scanner.codebase.class_likes.insert(class_like_id, class_like_metadata);
536}