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