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 = scan_function(
174            identifier,
175            function,
176            self.stack.last().copied(),
177            context,
178            &mut self.scope,
179            type_context,
180            Some(&self.codebase.constants),
181        );
182
183        self.template_constraints.push({
184            let mut constraints: TemplateConstraintList = vec![];
185            for (template_name, template_constraints) in &metadata.template_types {
186                constraints.push((*template_name, template_constraints.clone()));
187            }
188
189            constraints
190        });
191
192        self.codebase.function_likes.entry(identifier).or_insert(metadata);
193    }
194
195    #[inline]
196    fn walk_out_function(&mut self, _function: &'arena Function<'arena>, _context: &mut Context<'ctx, 'arena>) {
197        self.template_constraints.pop().expect("Expected template stack to be non-empty");
198    }
199
200    #[inline]
201    fn walk_in_closure(&mut self, closure: &'arena Closure<'arena>, context: &mut Context<'ctx, 'arena>) {
202        let span = closure.span();
203
204        let file_ref = u64_atom(span.file_id.as_u64());
205        let closure_ref = u32_atom(span.start.offset);
206        let identifier = (file_ref, closure_ref);
207
208        let type_resolution_context = self.get_current_type_resolution_context();
209        let metadata = scan_closure(
210            identifier,
211            closure,
212            self.stack.last().copied(),
213            context,
214            &mut self.scope,
215            type_resolution_context,
216        );
217
218        self.template_constraints.push({
219            let mut constraints: TemplateConstraintList = vec![];
220            for (template_name, template_constraints) in &metadata.template_types {
221                constraints.push((*template_name, template_constraints.clone()));
222            }
223
224            constraints
225        });
226
227        self.codebase.function_likes.entry(identifier).or_insert(metadata);
228    }
229
230    #[inline]
231    fn walk_out_closure(&mut self, _closure: &'arena Closure<'arena>, _context: &mut Context<'ctx, 'arena>) {
232        self.template_constraints.pop().expect("Expected template stack to be non-empty");
233    }
234
235    #[inline]
236    fn walk_in_arrow_function(
237        &mut self,
238        arrow_function: &'arena ArrowFunction<'arena>,
239        context: &mut Context<'ctx, 'arena>,
240    ) {
241        let span = arrow_function.span();
242
243        let file_ref = u64_atom(span.file_id.as_u64());
244        let closure_ref = u32_atom(span.start.offset);
245        let identifier = (file_ref, closure_ref);
246
247        let type_resolution_context = self.get_current_type_resolution_context();
248
249        let metadata = scan_arrow_function(
250            identifier,
251            arrow_function,
252            self.stack.last().copied(),
253            context,
254            &mut self.scope,
255            type_resolution_context,
256        );
257
258        self.template_constraints.push({
259            let mut constraints: TemplateConstraintList = vec![];
260            for (template_name, template_constraints) in &metadata.template_types {
261                constraints.push((*template_name, template_constraints.clone()));
262            }
263
264            constraints
265        });
266        self.codebase.function_likes.entry(identifier).or_insert(metadata);
267    }
268
269    #[inline]
270    fn walk_out_arrow_function(
271        &mut self,
272        _arrow_function: &'arena ArrowFunction<'arena>,
273        _context: &mut Context<'ctx, 'arena>,
274    ) {
275        self.template_constraints.pop().expect("Expected template stack to be non-empty");
276    }
277
278    #[inline]
279    fn walk_in_constant(&mut self, constant: &'arena Constant<'arena>, context: &mut Context<'ctx, 'arena>) {
280        let constants = scan_constant(constant, context, &self.get_current_type_resolution_context(), &self.scope);
281
282        for constant_metadata in constants {
283            let constant_name = constant_metadata.name;
284            self.codebase.constants.entry(constant_name).or_insert(constant_metadata);
285        }
286    }
287
288    #[inline]
289    fn walk_in_function_call(
290        &mut self,
291        function_call: &'arena FunctionCall<'arena>,
292        context: &mut Context<'ctx, 'arena>,
293    ) {
294        let Some(constant_metadata) =
295            scan_defined_constant(function_call, context, &self.get_current_type_resolution_context(), &self.scope)
296        else {
297            return;
298        };
299
300        self.codebase.constants.entry(constant_metadata.name).or_insert(constant_metadata);
301    }
302
303    #[inline]
304    fn walk_anonymous_class(
305        &mut self,
306        anonymous_class: &'arena AnonymousClass<'arena>,
307        context: &mut Context<'ctx, 'arena>,
308    ) {
309        if let Some((id, template_definition, type_aliases, imported_aliases)) =
310            register_anonymous_class(&mut self.codebase, anonymous_class, context, &mut self.scope)
311        {
312            self.file_type_aliases.extend(type_aliases);
313            self.file_imported_aliases.extend(imported_aliases);
314            self.stack.push(id);
315            self.template_constraints.push(template_definition);
316
317            walk_anonymous_class_mut(self, anonymous_class, context);
318        } else {
319            // We don't need to walk the anonymous class if it's already been registered
320        }
321    }
322
323    #[inline]
324    fn walk_class(&mut self, class: &'arena Class<'arena>, context: &mut Context<'ctx, 'arena>) {
325        if let Some((id, templates, type_aliases, imported_aliases)) =
326            register_class(&mut self.codebase, class, context, &mut self.scope)
327        {
328            self.file_type_aliases.extend(type_aliases);
329            self.file_imported_aliases.extend(imported_aliases);
330            self.stack.push(id);
331            self.template_constraints.push(templates);
332
333            walk_class_mut(self, class, context);
334        } else {
335            // We don't need to walk the class if it's already been registered
336        }
337    }
338
339    #[inline]
340    fn walk_trait(&mut self, r#trait: &'arena Trait<'arena>, context: &mut Context<'ctx, 'arena>) {
341        if let Some((id, templates, type_aliases, imported_aliases)) =
342            register_trait(&mut self.codebase, r#trait, context, &mut self.scope)
343        {
344            self.file_type_aliases.extend(type_aliases);
345            self.file_imported_aliases.extend(imported_aliases);
346            self.stack.push(id);
347            self.template_constraints.push(templates);
348
349            walk_trait_mut(self, r#trait, context);
350        } else {
351            // We don't need to walk the trait if it's already been registered
352        }
353    }
354
355    #[inline]
356    fn walk_enum(&mut self, r#enum: &'arena Enum<'arena>, context: &mut Context<'ctx, 'arena>) {
357        if let Some((id, templates, type_aliases, imported_aliases)) =
358            register_enum(&mut self.codebase, r#enum, context, &mut self.scope)
359        {
360            self.file_type_aliases.extend(type_aliases);
361            self.file_imported_aliases.extend(imported_aliases);
362            self.stack.push(id);
363            self.template_constraints.push(templates);
364
365            walk_enum_mut(self, r#enum, context);
366        } else {
367            // We don't need to walk the enum if it's already been registered
368        }
369    }
370
371    #[inline]
372    fn walk_interface(&mut self, interface: &'arena Interface<'arena>, context: &mut Context<'ctx, 'arena>) {
373        if let Some((id, templates, type_aliases, imported_aliases)) =
374            register_interface(&mut self.codebase, interface, context, &mut self.scope)
375        {
376            self.file_type_aliases.extend(type_aliases);
377            self.file_imported_aliases.extend(imported_aliases);
378            self.stack.push(id);
379            self.template_constraints.push(templates);
380
381            walk_interface_mut(self, interface, context);
382        }
383    }
384
385    #[inline]
386    fn walk_in_method(&mut self, method: &'arena Method<'arena>, context: &mut Context<'ctx, 'arena>) {
387        let current_class = self.stack.last().copied().expect("Expected class-like stack to be non-empty");
388        let mut class_like_metadata =
389            self.codebase.class_likes.remove(&current_class).expect("Expected class-like metadata to be present");
390
391        let name = ascii_lowercase_atom(method.name.value);
392
393        if class_like_metadata.methods.contains(&name) {
394            if class_like_metadata.pseudo_methods.contains(&name)
395                && let Some(existing_method) = self.codebase.function_likes.get_mut(&(class_like_metadata.name, name))
396            {
397                class_like_metadata.pseudo_methods.remove(&name);
398                existing_method.flags.remove(MetadataFlags::MAGIC_METHOD);
399            }
400
401            self.codebase.class_likes.insert(current_class, class_like_metadata);
402            self.template_constraints.push(vec![]);
403
404            return;
405        }
406
407        let method_id = (class_like_metadata.name, name);
408        let type_resolution_context = {
409            let mut context = self.get_current_type_resolution_context();
410
411            for alias_name in class_like_metadata.type_aliases.keys() {
412                context = context.with_type_alias(*alias_name);
413            }
414
415            for (alias_name, (source_class, original_name, _span)) in &class_like_metadata.imported_type_aliases {
416                context = context.with_imported_type_alias(*alias_name, *source_class, *original_name);
417            }
418
419            context
420        };
421
422        let mut function_like_metadata = scan_method(
423            method_id,
424            method,
425            &class_like_metadata,
426            context,
427            &mut self.scope,
428            Some(type_resolution_context),
429        );
430
431        let Some(method_metadata) = &function_like_metadata.method_metadata else {
432            unreachable!("Method info should be present for method.",);
433        };
434
435        let mut is_constructor = false;
436        let mut is_clone = false;
437        if method_metadata.is_constructor {
438            is_constructor = true;
439            self.has_constructor = true;
440
441            let type_context = self.get_current_type_resolution_context();
442            for (index, param) in method.parameter_list.parameters.iter().enumerate() {
443                if !param.is_promoted_property() {
444                    continue;
445                }
446
447                let Some(parameter_metadata) = function_like_metadata.parameters.get_mut(index) else {
448                    continue;
449                };
450
451                let property_metadata = scan_promoted_property(
452                    param,
453                    parameter_metadata,
454                    &mut class_like_metadata,
455                    current_class,
456                    &type_context,
457                    context,
458                    &self.scope,
459                );
460
461                class_like_metadata.add_property_metadata(property_metadata);
462            }
463        } else {
464            is_clone = name == atom("__clone");
465        }
466
467        class_like_metadata.methods.insert(name);
468        let method_identifier = MethodIdentifier::new(class_like_metadata.name, name);
469        class_like_metadata.add_declaring_method_id(name, method_identifier);
470        if !method_metadata.visibility.is_private() || is_constructor || is_clone || class_like_metadata.kind.is_trait()
471        {
472            class_like_metadata.inheritable_method_ids.insert(name, method_identifier);
473        }
474
475        if method_metadata.is_final && is_constructor {
476            class_like_metadata.flags |= MetadataFlags::CONSISTENT_CONSTRUCTOR;
477        }
478
479        self.template_constraints.push({
480            let mut constraints: TemplateConstraintList = vec![];
481            for (template_name, template_constraints) in &function_like_metadata.template_types {
482                constraints.push((*template_name, template_constraints.clone()));
483            }
484
485            constraints
486        });
487
488        self.codebase.class_likes.entry(current_class).or_insert(class_like_metadata);
489        self.codebase.function_likes.entry(method_id).or_insert(function_like_metadata);
490    }
491
492    #[inline]
493    fn walk_out_method(&mut self, _method: &'arena Method<'arena>, _context: &mut Context<'ctx, 'arena>) {
494        self.template_constraints.pop().expect("Expected template stack to be non-empty");
495    }
496
497    #[inline]
498    fn walk_out_anonymous_class(
499        &mut self,
500        _anonymous_class: &'arena AnonymousClass<'arena>,
501        _context: &mut Context<'ctx, 'arena>,
502    ) {
503        self.stack.pop().expect("Expected class stack to be non-empty");
504        self.template_constraints.pop().expect("Expected template stack to be non-empty");
505    }
506
507    #[inline]
508    fn walk_out_class(&mut self, _class: &'arena Class<'arena>, context: &mut Context<'ctx, 'arena>) {
509        finalize_class_like(self, context);
510    }
511
512    #[inline]
513    fn walk_out_trait(&mut self, _trait: &'arena Trait<'arena>, context: &mut Context<'ctx, 'arena>) {
514        finalize_class_like(self, context);
515    }
516
517    #[inline]
518    fn walk_out_enum(&mut self, _enum: &'arena Enum<'arena>, context: &mut Context<'ctx, 'arena>) {
519        finalize_class_like(self, context);
520    }
521
522    #[inline]
523    fn walk_out_interface(&mut self, _interface: &'arena Interface<'arena>, context: &mut Context<'ctx, 'arena>) {
524        finalize_class_like(self, context);
525    }
526}
527
528fn finalize_class_like(scanner: &mut Scanner, context: &mut Context<'_, '_>) {
529    let has_constructor = scanner.has_constructor;
530    scanner.has_constructor = false;
531
532    let class_like_id = scanner.stack.pop().expect("Expected class stack to be non-empty");
533    scanner.template_constraints.pop().expect("Expected template stack to be non-empty");
534
535    if has_constructor {
536        return;
537    }
538
539    let Some(mut class_like_metadata) = scanner.codebase.class_likes.remove(&class_like_id) else {
540        return;
541    };
542
543    if class_like_metadata.flags.has_consistent_constructor() {
544        let constructor_name = atom("__construct");
545
546        class_like_metadata.methods.insert(constructor_name);
547        let constructor_method_id = MethodIdentifier::new(class_like_metadata.name, constructor_name);
548        class_like_metadata.add_declaring_method_id(constructor_name, constructor_method_id);
549        class_like_metadata.inheritable_method_ids.insert(constructor_name, constructor_method_id);
550
551        let mut flags = MetadataFlags::PURE;
552        if context.file.file_type.is_host() {
553            flags |= MetadataFlags::USER_DEFINED;
554        } else if context.file.file_type.is_builtin() {
555            flags |= MetadataFlags::BUILTIN;
556        }
557
558        scanner.codebase.function_likes.insert(
559            (class_like_metadata.name, constructor_name),
560            FunctionLikeMetadata::new(FunctionLikeKind::Method, class_like_metadata.span, flags),
561        );
562    }
563
564    scanner.codebase.class_likes.insert(class_like_id, class_like_metadata);
565}