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