mago_codex/scanner/
mod.rs

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