Skip to main content

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