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