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<'ast, 'arena>(
48    arena: &'arena Bump,
49    file: &File,
50    program: &'ast Program<'arena>,
51    resolved_names: &'ast 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, 'ast, 'arena> {
62    pub arena: &'arena Bump,
63    pub file: &'ctx File,
64    pub program: &'ast Program<'arena>,
65    pub resolved_names: &'ast ResolvedNames<'arena>,
66}
67
68impl<'ctx, 'ast, 'arena> Context<'ctx, 'ast, 'arena> {
69    pub fn new(
70        arena: &'arena Bump,
71        file: &'ctx File,
72        program: &'ast Program<'arena>,
73        resolved_names: &'ast ResolvedNames<'arena>,
74    ) -> Self {
75        Self { arena, file, program, resolved_names }
76    }
77
78    pub fn get_docblock(&self, node: impl HasSpan) -> Option<&'ast 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, 'ast, 'arena> MutWalker<'ast, 'arena, Context<'ctx, 'ast, 'arena>> for Scanner {
115    #[inline]
116    fn walk_in_namespace(&mut self, namespace: &'ast Namespace<'arena>, _context: &mut Context<'ctx, 'ast, '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: &'ast Namespace<'arena>, _context: &mut Context<'ctx, 'ast, 'arena>) {
125        self.scope = NamespaceScope::global();
126    }
127
128    #[inline]
129    fn walk_in_use(&mut self, r#use: &'ast Use<'arena>, _context: &mut Context<'ctx, 'ast, 'arena>) {
130        self.scope.populate_from_use(r#use);
131    }
132
133    #[inline]
134    fn walk_in_function(&mut self, function: &'ast Function<'arena>, context: &mut Context<'ctx, 'ast, '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: &'ast Function<'arena>, _context: &mut Context<'ctx, 'ast, '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: &'ast Closure<'arena>, context: &mut Context<'ctx, 'ast, '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: &'ast Closure<'arena>, _context: &mut Context<'ctx, 'ast, '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: &'ast ArrowFunction<'arena>,
198        context: &mut Context<'ctx, 'ast, '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: &'ast ArrowFunction<'arena>,
232        _context: &mut Context<'ctx, 'ast, '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: &'ast Constant<'arena>, context: &mut Context<'ctx, 'ast, 'arena>) {
239        for metadata in scan_constant(constant, context) {
240            self.codebase.constants.insert(metadata.name, metadata);
241        }
242    }
243
244    #[inline]
245    fn walk_in_function_call(
246        &mut self,
247        function_call: &'ast FunctionCall<'arena>,
248        context: &mut Context<'ctx, 'ast, 'arena>,
249    ) {
250        if let Some(metadata) = scan_defined_constant(function_call, context) {
251            self.codebase.constants.insert(metadata.name, metadata);
252        }
253    }
254
255    #[inline]
256    fn walk_anonymous_class(
257        &mut self,
258        anonymous_class: &'ast AnonymousClass<'arena>,
259        context: &mut Context<'ctx, 'ast, 'arena>,
260    ) {
261        if let Some((id, template_definition)) =
262            register_anonymous_class(&mut self.codebase, anonymous_class, context, &mut self.scope)
263        {
264            self.stack.push(id);
265            self.template_constraints.push(template_definition);
266
267            walk_anonymous_class_mut(self, anonymous_class, context);
268        } else {
269            // We don't need to walk the anonymous class if it's already been registered
270        }
271    }
272
273    #[inline]
274    fn walk_class(&mut self, class: &'ast Class<'arena>, context: &mut Context<'ctx, 'ast, 'arena>) {
275        if let Some((id, templates)) = register_class(&mut self.codebase, class, context, &mut self.scope) {
276            self.stack.push(id);
277            self.template_constraints.push(templates);
278
279            walk_class_mut(self, class, context);
280        } else {
281            // We don't need to walk the class if it's already been registered
282        }
283    }
284
285    #[inline]
286    fn walk_trait(&mut self, r#trait: &'ast Trait<'arena>, context: &mut Context<'ctx, 'ast, 'arena>) {
287        if let Some((id, templates)) = register_trait(&mut self.codebase, r#trait, context, &mut self.scope) {
288            self.stack.push(id);
289            self.template_constraints.push(templates);
290
291            walk_trait_mut(self, r#trait, context);
292        } else {
293            // We don't need to walk the trait if it's already been registered
294        }
295    }
296
297    #[inline]
298    fn walk_enum(&mut self, r#enum: &'ast Enum<'arena>, context: &mut Context<'ctx, 'ast, 'arena>) {
299        if let Some((id, templates)) = register_enum(&mut self.codebase, r#enum, context, &mut self.scope) {
300            self.stack.push(id);
301            self.template_constraints.push(templates);
302
303            walk_enum_mut(self, r#enum, context);
304        } else {
305            // We don't need to walk the enum if it's already been registered
306        }
307    }
308
309    #[inline]
310    fn walk_interface(&mut self, interface: &'ast Interface<'arena>, context: &mut Context<'ctx, 'ast, 'arena>) {
311        if let Some((id, templates)) = register_interface(&mut self.codebase, interface, context, &mut self.scope) {
312            self.stack.push(id);
313            self.template_constraints.push(templates);
314
315            walk_interface_mut(self, interface, context);
316        }
317    }
318
319    #[inline]
320    fn walk_in_method(&mut self, method: &'ast Method<'arena>, context: &mut Context<'ctx, 'ast, 'arena>) {
321        let current_class = self.stack.last().copied().expect("Expected class-like stack to be non-empty");
322        let mut class_like_metadata =
323            self.codebase.class_likes.remove(&current_class).expect("Expected class-like metadata to be present");
324
325        let name = ascii_lowercase_atom(method.name.value);
326        if class_like_metadata.methods.contains(&name) {
327            self.codebase.class_likes.insert(current_class, class_like_metadata);
328            self.template_constraints.push(vec![]);
329
330            return;
331        }
332
333        let method_id = (class_like_metadata.name, name);
334        let type_resolution = if method.is_static() { None } else { Some(self.get_current_type_resolution_context()) };
335
336        let function_like_metadata =
337            scan_method(method_id, method, &class_like_metadata, context, &mut self.scope, type_resolution);
338        let Some(method_metadata) = &function_like_metadata.method_metadata else {
339            unreachable!("Method info should be present for method.",);
340        };
341
342        let mut is_constructor = false;
343        let mut is_clone = false;
344        if method_metadata.is_constructor {
345            is_constructor = true;
346            self.has_constructor = true;
347
348            for (index, param) in method.parameter_list.parameters.iter().enumerate() {
349                if !param.is_promoted_property() {
350                    continue;
351                }
352
353                let Some(parameter_info) = function_like_metadata.parameters.get(index) else {
354                    continue;
355                };
356
357                let property_metadata = scan_promoted_property(param, parameter_info, &class_like_metadata, context);
358
359                class_like_metadata.add_property_metadata(property_metadata);
360            }
361        } else {
362            is_clone = name == atom("__clone");
363        }
364
365        class_like_metadata.methods.insert(name);
366        class_like_metadata.add_declaring_method_id(name, class_like_metadata.name);
367        if !method_metadata.visibility.is_private() || is_constructor || is_clone || class_like_metadata.kind.is_trait()
368        {
369            class_like_metadata.inheritable_method_ids.insert(name, class_like_metadata.name);
370        }
371
372        if method_metadata.is_final && is_constructor {
373            class_like_metadata.flags |= MetadataFlags::CONSISTENT_CONSTRUCTOR;
374        }
375
376        self.template_constraints.push({
377            let mut constraints: TemplateConstraintList = vec![];
378            for (template_name, template_constraints) in &function_like_metadata.template_types {
379                constraints.push((*template_name, template_constraints.to_vec()));
380            }
381
382            constraints
383        });
384
385        self.codebase.class_likes.insert(current_class, class_like_metadata);
386        self.codebase.function_likes.insert(method_id, function_like_metadata);
387    }
388
389    #[inline]
390    fn walk_out_method(&mut self, _method: &'ast Method<'arena>, _context: &mut Context<'ctx, 'ast, 'arena>) {
391        self.template_constraints.pop().expect("Expected template stack to be non-empty");
392    }
393
394    #[inline]
395    fn walk_out_anonymous_class(
396        &mut self,
397        _anonymous_class: &'ast AnonymousClass<'arena>,
398        _context: &mut Context<'ctx, 'ast, 'arena>,
399    ) {
400        self.stack.pop().expect("Expected class stack to be non-empty");
401        self.template_constraints.pop().expect("Expected template stack to be non-empty");
402    }
403
404    #[inline]
405    fn walk_out_class(&mut self, _class: &'ast Class<'arena>, context: &mut Context<'ctx, 'ast, 'arena>) {
406        finalize_class_like(self, context);
407    }
408
409    #[inline]
410    fn walk_out_trait(&mut self, _trait: &'ast Trait<'arena>, context: &mut Context<'ctx, 'ast, 'arena>) {
411        finalize_class_like(self, context);
412    }
413
414    #[inline]
415    fn walk_out_enum(&mut self, _enum: &'ast Enum<'arena>, context: &mut Context<'ctx, 'ast, 'arena>) {
416        finalize_class_like(self, context);
417    }
418
419    #[inline]
420    fn walk_out_interface(&mut self, _interface: &'ast Interface<'arena>, context: &mut Context<'ctx, 'ast, 'arena>) {
421        finalize_class_like(self, context);
422    }
423}
424
425fn finalize_class_like<'ctx, 'ast, 'arena>(scanner: &mut Scanner, context: &mut Context<'ctx, 'ast, 'arena>) {
426    let has_constructor = scanner.has_constructor;
427    scanner.has_constructor = false;
428
429    let class_like_id = scanner.stack.pop().expect("Expected class stack to be non-empty");
430    scanner.template_constraints.pop().expect("Expected template stack to be non-empty");
431
432    if has_constructor {
433        return;
434    }
435
436    let Some(mut class_like_metadata) = scanner.codebase.class_likes.remove(&class_like_id) else {
437        return;
438    };
439
440    if class_like_metadata.flags.has_consistent_constructor() {
441        let constructor_name = atom("__construct");
442
443        class_like_metadata.methods.insert(constructor_name);
444        class_like_metadata.add_declaring_method_id(constructor_name, class_like_metadata.name);
445        class_like_metadata.inheritable_method_ids.insert(constructor_name, class_like_metadata.name);
446
447        let mut flags = MetadataFlags::PURE;
448        if context.file.file_type.is_host() {
449            flags |= MetadataFlags::USER_DEFINED;
450        } else if context.file.file_type.is_builtin() {
451            flags |= MetadataFlags::BUILTIN;
452        }
453
454        scanner.codebase.function_likes.insert(
455            (class_like_metadata.name, constructor_name),
456            FunctionLikeMetadata::new(FunctionLikeKind::Method, class_like_metadata.span, flags),
457        );
458    }
459
460    scanner.codebase.class_likes.insert(class_like_id, class_like_metadata);
461}