mago_codex/scanner/
mod.rs

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