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