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