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 }
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 }
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 }
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 }
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(¤t_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}