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