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::Call;
18use mago_syntax::ast::Class;
19use mago_syntax::ast::Closure;
20use mago_syntax::ast::Constant;
21use mago_syntax::ast::Enum;
22use mago_syntax::ast::Expression;
23use mago_syntax::ast::Function;
24use mago_syntax::ast::FunctionCall;
25use mago_syntax::ast::If;
26use mago_syntax::ast::IfBody;
27use mago_syntax::ast::Interface;
28use mago_syntax::ast::Method;
29use mago_syntax::ast::Namespace;
30use mago_syntax::ast::Program;
31use mago_syntax::ast::Trait;
32use mago_syntax::ast::Trivia;
33use mago_syntax::ast::UnaryPrefix;
34use mago_syntax::ast::UnaryPrefixOperator;
35use mago_syntax::ast::Use;
36use mago_syntax::comments::docblock::get_docblock_for_node;
37use mago_syntax::walker::MutWalker;
38use mago_syntax::walker::walk_anonymous_class_mut;
39use mago_syntax::walker::walk_class_mut;
40use mago_syntax::walker::walk_enum_mut;
41use mago_syntax::walker::walk_interface_mut;
42use mago_syntax::walker::walk_trait_mut;
43
44use crate::identifier::method::MethodIdentifier;
45use crate::metadata::CodebaseMetadata;
46use crate::metadata::flags::MetadataFlags;
47use crate::metadata::function_like::FunctionLikeKind;
48use crate::metadata::function_like::FunctionLikeMetadata;
49use crate::scanner::class_like::register_anonymous_class;
50use crate::scanner::class_like::register_class;
51use crate::scanner::class_like::register_enum;
52use crate::scanner::class_like::register_interface;
53use crate::scanner::class_like::register_trait;
54use crate::scanner::constant::scan_constant;
55use crate::scanner::constant::scan_defined_constant;
56use crate::scanner::function_like::scan_arrow_function;
57use crate::scanner::function_like::scan_closure;
58use crate::scanner::function_like::scan_function;
59use crate::scanner::function_like::scan_method;
60use crate::scanner::property::scan_promoted_property;
61use crate::ttype::resolution::TypeResolutionContext;
62use crate::ttype::template::GenericTemplate;
63
64mod attribute;
65mod class_like;
66mod class_like_constant;
67mod constant;
68mod docblock;
69mod enum_case;
70mod function_like;
71
72pub mod inference;
73
74mod parameter;
75mod property;
76mod ttype;
77
78#[inline]
79pub fn scan_program<'arena, 'ctx>(
80 arena: &'arena Bump,
81 file: &'ctx File,
82 program: &'arena Program<'arena>,
83 resolved_names: &'ctx ResolvedNames<'arena>,
84) -> CodebaseMetadata {
85 let mut context = Context::new(arena, file, program, resolved_names);
86 let mut scanner = Scanner::new();
87
88 scanner.walk_program(program, &mut context);
89
90 scanner.codebase
91}
92
93#[derive(Clone, Debug)]
94struct Context<'ctx, 'arena> {
95 pub arena: &'arena Bump,
96 pub file: &'ctx File,
97 pub program: &'arena Program<'arena>,
98 pub resolved_names: &'arena ResolvedNames<'arena>,
99}
100
101impl<'ctx, 'arena> Context<'ctx, 'arena> {
102 pub fn new(
103 arena: &'arena Bump,
104 file: &'ctx File,
105 program: &'arena Program<'arena>,
106 resolved_names: &'arena ResolvedNames<'arena>,
107 ) -> Self {
108 Self { arena, file, program, resolved_names }
109 }
110
111 pub fn get_docblock(&self, node: impl HasSpan) -> Option<&'arena Trivia<'arena>> {
112 get_docblock_for_node(self.program, self.file, node)
113 }
114}
115
116type TemplateConstraint = (Atom, GenericTemplate);
117type TemplateConstraintList = Vec<TemplateConstraint>;
118
119#[derive(Debug, Default)]
120struct Scanner {
121 codebase: CodebaseMetadata,
122 stack: Vec<Atom>,
123 template_constraints: Vec<TemplateConstraintList>,
124 scope: NamespaceScope,
125 has_constructor: bool,
126 file_type_aliases: AtomSet,
127 file_imported_aliases: AtomMap<(Atom, Atom)>,
128 polyfill_depth: u32,
129}
130
131#[derive(Debug, Clone, Copy, Eq, PartialEq)]
132enum PolyfillGuardBranch {
133 Then,
134 Else,
135 None,
136}
137
138const POLYFILL_GUARD_FUNCTIONS: &[&str] =
139 &["class_exists", "interface_exists", "trait_exists", "enum_exists", "function_exists", "defined"];
140
141fn classify_polyfill_guard(cond: &Expression<'_>) -> PolyfillGuardBranch {
142 let cond = cond.unparenthesized();
143
144 if is_polyfill_existence_check(cond) {
145 return PolyfillGuardBranch::Else;
146 }
147
148 if let Expression::UnaryPrefix(UnaryPrefix { operator: UnaryPrefixOperator::Not(_), operand }) = cond
149 && is_polyfill_existence_check(operand.unparenthesized())
150 {
151 return PolyfillGuardBranch::Then;
152 }
153
154 PolyfillGuardBranch::None
155}
156
157fn is_polyfill_existence_check(expr: &Expression<'_>) -> bool {
161 let Expression::Call(Call::Function(FunctionCall { function, .. })) = expr.unparenthesized() else {
162 return false;
163 };
164 let Expression::Identifier(identifier) = function.unparenthesized() else {
165 return false;
166 };
167 let last = identifier.last_segment();
168 POLYFILL_GUARD_FUNCTIONS.iter().any(|name| last.eq_ignore_ascii_case(name))
169}
170
171impl Scanner {
172 pub fn new() -> Self {
173 Self::default()
174 }
175
176 fn get_current_type_resolution_context(&self) -> TypeResolutionContext {
177 let mut context = TypeResolutionContext::new();
178 context = context.with_type_aliases(self.file_type_aliases.clone());
179
180 for (local_name, (source_class, original_name)) in &self.file_imported_aliases {
181 context = context.with_imported_type_alias(*local_name, *source_class, *original_name);
182 }
183
184 for template_constraint_list in self.template_constraints.iter().rev() {
185 for (name, constraint) in template_constraint_list {
186 if !context.has_template_definition(*name) {
187 context = context.with_template_definition(*name, vec![constraint.clone()]);
188 }
189 }
190 }
191
192 context
193 }
194
195 fn apply_polyfill_flag_to_class_like(&mut self, id: Atom) {
196 if self.polyfill_depth == 0 {
197 return;
198 }
199
200 if let Some(metadata) = self.codebase.class_likes.get_mut(&id) {
201 metadata.flags |= MetadataFlags::POLYFILL;
202 }
203 }
204}
205
206impl<'ctx, 'arena> MutWalker<'arena, 'arena, Context<'ctx, 'arena>> for Scanner {
207 #[inline]
208 fn walk_in_namespace(&mut self, namespace: &'arena Namespace<'arena>, _context: &mut Context<'ctx, 'arena>) {
209 self.scope = match &namespace.name {
210 Some(name) => NamespaceScope::for_namespace(name.value()),
211 None => NamespaceScope::global(),
212 };
213 }
214
215 #[inline]
216 fn walk_out_namespace(&mut self, _namespace: &'arena Namespace<'arena>, _context: &mut Context<'ctx, 'arena>) {
217 self.scope = NamespaceScope::global();
218 }
219
220 #[inline]
221 fn walk_in_use(&mut self, r#use: &'arena Use<'arena>, _context: &mut Context<'ctx, 'arena>) {
222 self.scope.populate_from_use(r#use);
223 }
224
225 fn walk_if(&mut self, r#if: &'arena If<'arena>, context: &mut Context<'ctx, 'arena>) {
226 self.walk_keyword(&r#if.r#if, context);
227 self.walk_expression(r#if.condition, context);
228
229 let guard = classify_polyfill_guard(r#if.condition);
230
231 match &r#if.body {
232 IfBody::Statement(body) => {
233 let then_polyfill = matches!(guard, PolyfillGuardBranch::Then);
234 if then_polyfill {
235 self.polyfill_depth = self.polyfill_depth.saturating_add(1);
236 }
237 self.walk_statement(body.statement, context);
238 if then_polyfill {
239 self.polyfill_depth = self.polyfill_depth.saturating_sub(1);
240 }
241
242 for else_if_clause in &body.else_if_clauses {
243 self.walk_if_statement_body_else_if_clause(else_if_clause, context);
244 }
245
246 if let Some(else_clause) = &body.else_clause {
247 let else_polyfill = matches!(guard, PolyfillGuardBranch::Else);
248 if else_polyfill {
249 self.polyfill_depth = self.polyfill_depth.saturating_add(1);
250 }
251 self.walk_if_statement_body_else_clause(else_clause, context);
252 if else_polyfill {
253 self.polyfill_depth = self.polyfill_depth.saturating_sub(1);
254 }
255 }
256 }
257 IfBody::ColonDelimited(body) => {
258 let then_polyfill = matches!(guard, PolyfillGuardBranch::Then);
259 if then_polyfill {
260 self.polyfill_depth = self.polyfill_depth.saturating_add(1);
261 }
262 for statement in &body.statements {
263 self.walk_statement(statement, context);
264 }
265 if then_polyfill {
266 self.polyfill_depth = self.polyfill_depth.saturating_sub(1);
267 }
268
269 for else_if_clause in &body.else_if_clauses {
270 self.walk_if_colon_delimited_body_else_if_clause(else_if_clause, context);
271 }
272
273 if let Some(else_clause) = &body.else_clause {
274 let else_polyfill = matches!(guard, PolyfillGuardBranch::Else);
275 if else_polyfill {
276 self.polyfill_depth = self.polyfill_depth.saturating_add(1);
277 }
278 self.walk_if_colon_delimited_body_else_clause(else_clause, context);
279 if else_polyfill {
280 self.polyfill_depth = self.polyfill_depth.saturating_sub(1);
281 }
282 }
283
284 self.walk_keyword(&body.endif, context);
285 self.walk_terminator(&body.terminator, context);
286 }
287 }
288 }
289
290 #[inline]
291 fn walk_in_function(&mut self, function: &'arena Function<'arena>, context: &mut Context<'ctx, 'arena>) {
292 let type_context = self.get_current_type_resolution_context();
293
294 let name = ascii_lowercase_atom(context.resolved_names.get(&function.name));
295 let identifier = (empty_atom(), name);
296 let mut metadata = scan_function(
297 identifier,
298 function,
299 self.stack.last().copied(),
300 context,
301 &mut self.scope,
302 type_context,
303 Some(&self.codebase.constants),
304 );
305
306 self.template_constraints.push({
307 let mut constraints: TemplateConstraintList = vec![];
308 for (template_name, template_constraints) in &metadata.template_types {
309 constraints.push((*template_name, template_constraints.clone()));
310 }
311
312 constraints
313 });
314
315 if self.polyfill_depth > 0 {
316 metadata.flags |= MetadataFlags::POLYFILL;
317 }
318
319 self.codebase.function_likes.entry(identifier).or_insert(metadata);
320 }
321
322 #[inline]
323 fn walk_out_function(&mut self, _function: &'arena Function<'arena>, _context: &mut Context<'ctx, 'arena>) {
324 self.template_constraints.pop().expect("Expected template stack to be non-empty");
325 }
326
327 #[inline]
328 fn walk_in_closure(&mut self, closure: &'arena Closure<'arena>, context: &mut Context<'ctx, 'arena>) {
329 let span = closure.span();
330
331 let file_ref = u64_atom(span.file_id.as_u64());
332 let closure_ref = u32_atom(span.start.offset);
333 let identifier = (file_ref, closure_ref);
334
335 let type_resolution_context = self.get_current_type_resolution_context();
336 let metadata = scan_closure(
337 identifier,
338 closure,
339 self.stack.last().copied(),
340 context,
341 &mut self.scope,
342 type_resolution_context,
343 );
344
345 self.template_constraints.push({
346 let mut constraints: TemplateConstraintList = vec![];
347 for (template_name, template_constraints) in &metadata.template_types {
348 constraints.push((*template_name, template_constraints.clone()));
349 }
350
351 constraints
352 });
353
354 self.codebase.function_likes.entry(identifier).or_insert(metadata);
355 }
356
357 #[inline]
358 fn walk_out_closure(&mut self, _closure: &'arena Closure<'arena>, _context: &mut Context<'ctx, 'arena>) {
359 self.template_constraints.pop().expect("Expected template stack to be non-empty");
360 }
361
362 #[inline]
363 fn walk_in_arrow_function(
364 &mut self,
365 arrow_function: &'arena ArrowFunction<'arena>,
366 context: &mut Context<'ctx, 'arena>,
367 ) {
368 let span = arrow_function.span();
369
370 let file_ref = u64_atom(span.file_id.as_u64());
371 let closure_ref = u32_atom(span.start.offset);
372 let identifier = (file_ref, closure_ref);
373
374 let type_resolution_context = self.get_current_type_resolution_context();
375
376 let metadata = scan_arrow_function(
377 identifier,
378 arrow_function,
379 self.stack.last().copied(),
380 context,
381 &mut self.scope,
382 type_resolution_context,
383 );
384
385 self.template_constraints.push({
386 let mut constraints: TemplateConstraintList = vec![];
387 for (template_name, template_constraints) in &metadata.template_types {
388 constraints.push((*template_name, template_constraints.clone()));
389 }
390
391 constraints
392 });
393 self.codebase.function_likes.entry(identifier).or_insert(metadata);
394 }
395
396 #[inline]
397 fn walk_out_arrow_function(
398 &mut self,
399 _arrow_function: &'arena ArrowFunction<'arena>,
400 _context: &mut Context<'ctx, 'arena>,
401 ) {
402 self.template_constraints.pop().expect("Expected template stack to be non-empty");
403 }
404
405 #[inline]
406 fn walk_in_constant(&mut self, constant: &'arena Constant<'arena>, context: &mut Context<'ctx, 'arena>) {
407 let constants = scan_constant(constant, context, &self.get_current_type_resolution_context(), &self.scope);
408
409 for mut constant_metadata in constants {
410 if self.polyfill_depth > 0 {
411 constant_metadata.flags |= MetadataFlags::POLYFILL;
412 }
413 let constant_name = constant_metadata.name;
414 self.codebase.constants.entry(constant_name).or_insert(constant_metadata);
415 }
416 }
417
418 #[inline]
419 fn walk_in_function_call(
420 &mut self,
421 function_call: &'arena FunctionCall<'arena>,
422 context: &mut Context<'ctx, 'arena>,
423 ) {
424 let Some(mut constant_metadata) =
425 scan_defined_constant(function_call, context, &self.get_current_type_resolution_context(), &self.scope)
426 else {
427 return;
428 };
429
430 if self.polyfill_depth > 0 {
431 constant_metadata.flags |= MetadataFlags::POLYFILL;
432 }
433
434 self.codebase.constants.entry(constant_metadata.name).or_insert(constant_metadata);
435 }
436
437 #[inline]
438 fn walk_anonymous_class(
439 &mut self,
440 anonymous_class: &'arena AnonymousClass<'arena>,
441 context: &mut Context<'ctx, 'arena>,
442 ) {
443 if let Some((id, template_definition, type_aliases, imported_aliases)) =
444 register_anonymous_class(&mut self.codebase, anonymous_class, context, &mut self.scope)
445 {
446 self.apply_polyfill_flag_to_class_like(id);
447 self.file_type_aliases.extend(type_aliases);
448 self.file_imported_aliases.extend(imported_aliases);
449 self.stack.push(id);
450 self.template_constraints.push(template_definition);
451
452 walk_anonymous_class_mut(self, anonymous_class, context);
453 }
454 }
455
456 #[inline]
457 fn walk_class(&mut self, class: &'arena Class<'arena>, context: &mut Context<'ctx, 'arena>) {
458 if let Some((id, templates, type_aliases, imported_aliases)) =
459 register_class(&mut self.codebase, class, context, &mut self.scope)
460 {
461 self.apply_polyfill_flag_to_class_like(id);
462 self.file_type_aliases.extend(type_aliases);
463 self.file_imported_aliases.extend(imported_aliases);
464 self.stack.push(id);
465 self.template_constraints.push(templates);
466
467 walk_class_mut(self, class, context);
468 }
469 }
470
471 #[inline]
472 fn walk_trait(&mut self, r#trait: &'arena Trait<'arena>, context: &mut Context<'ctx, 'arena>) {
473 if let Some((id, templates, type_aliases, imported_aliases)) =
474 register_trait(&mut self.codebase, r#trait, context, &mut self.scope)
475 {
476 self.apply_polyfill_flag_to_class_like(id);
477 self.file_type_aliases.extend(type_aliases);
478 self.file_imported_aliases.extend(imported_aliases);
479 self.stack.push(id);
480 self.template_constraints.push(templates);
481
482 walk_trait_mut(self, r#trait, context);
483 }
484 }
485
486 #[inline]
487 fn walk_enum(&mut self, r#enum: &'arena Enum<'arena>, context: &mut Context<'ctx, 'arena>) {
488 if let Some((id, templates, type_aliases, imported_aliases)) =
489 register_enum(&mut self.codebase, r#enum, context, &mut self.scope)
490 {
491 self.apply_polyfill_flag_to_class_like(id);
492 self.file_type_aliases.extend(type_aliases);
493 self.file_imported_aliases.extend(imported_aliases);
494 self.stack.push(id);
495 self.template_constraints.push(templates);
496
497 walk_enum_mut(self, r#enum, context);
498 }
499 }
500
501 #[inline]
502 fn walk_interface(&mut self, interface: &'arena Interface<'arena>, context: &mut Context<'ctx, 'arena>) {
503 if let Some((id, templates, type_aliases, imported_aliases)) =
504 register_interface(&mut self.codebase, interface, context, &mut self.scope)
505 {
506 self.apply_polyfill_flag_to_class_like(id);
507 self.file_type_aliases.extend(type_aliases);
508 self.file_imported_aliases.extend(imported_aliases);
509 self.stack.push(id);
510 self.template_constraints.push(templates);
511
512 walk_interface_mut(self, interface, context);
513 }
514 }
515
516 #[inline]
517 fn walk_in_method(&mut self, method: &'arena Method<'arena>, context: &mut Context<'ctx, 'arena>) {
518 let current_class = self.stack.last().copied().expect("Expected class-like stack to be non-empty");
519 let mut class_like_metadata =
520 self.codebase.class_likes.remove(¤t_class).expect("Expected class-like metadata to be present");
521
522 let name = ascii_lowercase_atom(method.name.value);
523
524 if class_like_metadata.methods.contains(&name) {
525 if class_like_metadata.pseudo_methods.contains(&name)
526 && let Some(existing_method) = self.codebase.function_likes.get_mut(&(class_like_metadata.name, name))
527 {
528 class_like_metadata.pseudo_methods.remove(&name);
529 existing_method.flags.remove(MetadataFlags::MAGIC_METHOD);
530 }
531
532 self.codebase.class_likes.insert(current_class, class_like_metadata);
533 self.template_constraints.push(vec![]);
534
535 return;
536 }
537
538 let method_id = (class_like_metadata.name, name);
539 let type_resolution_context = {
540 let mut context = self.get_current_type_resolution_context();
541
542 for alias_name in class_like_metadata.type_aliases.keys() {
543 context = context.with_type_alias(*alias_name);
544 }
545
546 for (alias_name, (source_class, original_name, _span)) in &class_like_metadata.imported_type_aliases {
547 context = context.with_imported_type_alias(*alias_name, *source_class, *original_name);
548 }
549
550 context
551 };
552
553 let mut function_like_metadata = scan_method(
554 method_id,
555 method,
556 &class_like_metadata,
557 context,
558 &mut self.scope,
559 Some(type_resolution_context),
560 );
561
562 let Some(method_metadata) = &function_like_metadata.method_metadata else {
563 unreachable!("Method info should be present for method.",);
564 };
565
566 let mut is_constructor = false;
567 let mut is_clone = false;
568 if method_metadata.is_constructor {
569 is_constructor = true;
570 self.has_constructor = true;
571
572 let type_context = self.get_current_type_resolution_context();
573 for (index, param) in method.parameter_list.parameters.iter().enumerate() {
574 if !param.is_promoted_property() {
575 continue;
576 }
577
578 let Some(parameter_metadata) = function_like_metadata.parameters.get_mut(index) else {
579 continue;
580 };
581
582 let property_metadata = scan_promoted_property(
583 param,
584 parameter_metadata,
585 &mut class_like_metadata,
586 current_class,
587 &type_context,
588 context,
589 &self.scope,
590 );
591
592 class_like_metadata.add_property_metadata(property_metadata);
593 }
594 } else {
595 is_clone = name == atom("__clone");
596 }
597
598 class_like_metadata.methods.insert(name);
599 let method_identifier = MethodIdentifier::new(class_like_metadata.name, name);
600 class_like_metadata.add_declaring_method_id(name, method_identifier);
601 if !method_metadata.visibility.is_private() || is_constructor || is_clone || class_like_metadata.kind.is_trait()
602 {
603 class_like_metadata.inheritable_method_ids.insert(name, method_identifier);
604 }
605
606 if method_metadata.is_final && is_constructor {
607 class_like_metadata.flags |= MetadataFlags::CONSISTENT_CONSTRUCTOR;
608 }
609
610 self.template_constraints.push({
611 let mut constraints: TemplateConstraintList = vec![];
612 for (template_name, template_constraints) in &function_like_metadata.template_types {
613 constraints.push((*template_name, template_constraints.clone()));
614 }
615
616 constraints
617 });
618
619 self.codebase.class_likes.entry(current_class).or_insert(class_like_metadata);
620 self.codebase.function_likes.entry(method_id).or_insert(function_like_metadata);
621 }
622
623 #[inline]
624 fn walk_out_method(&mut self, _method: &'arena Method<'arena>, _context: &mut Context<'ctx, 'arena>) {
625 self.template_constraints.pop().expect("Expected template stack to be non-empty");
626 }
627
628 #[inline]
629 fn walk_out_anonymous_class(
630 &mut self,
631 _anonymous_class: &'arena AnonymousClass<'arena>,
632 _context: &mut Context<'ctx, 'arena>,
633 ) {
634 self.stack.pop().expect("Expected class stack to be non-empty");
635 self.template_constraints.pop().expect("Expected template stack to be non-empty");
636 }
637
638 #[inline]
639 fn walk_out_class(&mut self, _class: &'arena Class<'arena>, context: &mut Context<'ctx, 'arena>) {
640 finalize_class_like(self, context);
641 }
642
643 #[inline]
644 fn walk_out_trait(&mut self, _trait: &'arena Trait<'arena>, context: &mut Context<'ctx, 'arena>) {
645 finalize_class_like(self, context);
646 }
647
648 #[inline]
649 fn walk_out_enum(&mut self, _enum: &'arena Enum<'arena>, context: &mut Context<'ctx, 'arena>) {
650 finalize_class_like(self, context);
651 }
652
653 #[inline]
654 fn walk_out_interface(&mut self, _interface: &'arena Interface<'arena>, context: &mut Context<'ctx, 'arena>) {
655 finalize_class_like(self, context);
656 }
657}
658
659fn finalize_class_like(scanner: &mut Scanner, context: &mut Context<'_, '_>) {
660 let has_constructor = scanner.has_constructor;
661 scanner.has_constructor = false;
662
663 let class_like_id = scanner.stack.pop().expect("Expected class stack to be non-empty");
664 scanner.template_constraints.pop().expect("Expected template stack to be non-empty");
665
666 if has_constructor {
667 return;
668 }
669
670 let Some(mut class_like_metadata) = scanner.codebase.class_likes.remove(&class_like_id) else {
671 return;
672 };
673
674 if class_like_metadata.flags.has_consistent_constructor() {
675 let constructor_name = atom("__construct");
676
677 class_like_metadata.methods.insert(constructor_name);
678 let constructor_method_id = MethodIdentifier::new(class_like_metadata.name, constructor_name);
679 class_like_metadata.add_declaring_method_id(constructor_name, constructor_method_id);
680 class_like_metadata.inheritable_method_ids.insert(constructor_name, constructor_method_id);
681
682 let mut flags = MetadataFlags::PURE;
683 if context.file.file_type.is_host() {
684 flags |= MetadataFlags::USER_DEFINED;
685 } else if context.file.file_type.is_builtin() {
686 flags |= MetadataFlags::BUILTIN;
687 }
688
689 scanner.codebase.function_likes.insert(
690 (class_like_metadata.name, constructor_name),
691 FunctionLikeMetadata::new(FunctionLikeKind::Method, class_like_metadata.span, flags),
692 );
693 }
694
695 scanner.codebase.class_likes.insert(class_like_id, class_like_metadata);
696}
697
698#[cfg(test)]
699mod polyfill_tests {
700 use std::borrow::Cow;
701
702 use bumpalo::Bump;
703
704 use mago_atom::ascii_lowercase_atom;
705 use mago_atom::atom;
706 use mago_atom::empty_atom;
707 use mago_database::Database;
708 use mago_database::DatabaseConfiguration;
709 use mago_database::DatabaseReader;
710 use mago_database::file::File;
711 use mago_names::resolver::NameResolver;
712 use mago_syntax::parser::parse_file;
713
714 use crate::metadata::CodebaseMetadata;
715 use crate::metadata::flags::MetadataFlags;
716 use crate::scanner::scan_program;
717
718 fn scan(code: &'static str) -> CodebaseMetadata {
719 let file = File::ephemeral(Cow::Borrowed("code.php"), Cow::Borrowed(code));
720 let config =
721 DatabaseConfiguration::new(std::path::Path::new("/"), vec![], vec![], vec![], vec![]).into_static();
722 let database = Database::single(file, config);
723
724 let mut codebase = CodebaseMetadata::new();
725 let arena = Bump::new();
726 for file in database.files() {
727 let program = parse_file(&arena, &file);
728 assert!(!program.has_errors(), "parse failed: {:?}", program.errors);
729 let resolved_names = NameResolver::new(&arena).resolve(program);
730 codebase.extend(scan_program(&arena, &file, program, &resolved_names));
731 }
732 codebase
733 }
734
735 fn class_flags(codebase: &CodebaseMetadata, name: &str) -> MetadataFlags {
736 codebase
737 .class_likes
738 .get(&ascii_lowercase_atom(name))
739 .unwrap_or_else(|| panic!("class-like `{name}` not found; have {:?}", codebase.class_likes.keys()))
740 .flags
741 }
742
743 fn function_flags(codebase: &CodebaseMetadata, name: &str) -> MetadataFlags {
744 codebase
745 .function_likes
746 .get(&(empty_atom(), ascii_lowercase_atom(name)))
747 .unwrap_or_else(|| panic!("function `{name}` not found"))
748 .flags
749 }
750
751 fn constant_flags(codebase: &CodebaseMetadata, name: &str) -> MetadataFlags {
752 codebase.constants.get(&atom(name)).unwrap_or_else(|| panic!("constant `{name}` not found")).flags
753 }
754
755 #[test]
756 fn class_in_not_class_exists_is_polyfill() {
757 let code = r#"<?php
758 if (!class_exists('Foo')) {
759 class Foo {}
760 }
761 "#;
762 assert!(class_flags(&scan(code), "Foo").is_polyfill());
763 }
764
765 #[test]
766 fn interface_in_not_interface_exists_is_polyfill() {
767 let code = r#"<?php
768 if (!interface_exists('Bar')) {
769 interface Bar {}
770 }
771 "#;
772 assert!(class_flags(&scan(code), "Bar").is_polyfill());
773 }
774
775 #[test]
776 fn trait_in_not_trait_exists_is_polyfill() {
777 let code = r#"<?php
778 if (!trait_exists('Mix')) {
779 trait Mix {}
780 }
781 "#;
782 assert!(class_flags(&scan(code), "Mix").is_polyfill());
783 }
784
785 #[test]
786 fn enum_in_not_enum_exists_is_polyfill() {
787 let code = r#"<?php
788 if (!enum_exists('Kind')) {
789 enum Kind { case A; }
790 }
791 "#;
792 assert!(class_flags(&scan(code), "Kind").is_polyfill());
793 }
794
795 #[test]
796 fn function_in_not_function_exists_is_polyfill() {
797 let code = r#"<?php
798 if (!function_exists('foo')) {
799 function foo(): void {}
800 }
801 "#;
802 assert!(function_flags(&scan(code), "foo").is_polyfill());
803 }
804
805 #[test]
806 fn const_in_not_defined_is_polyfill() {
807 let code = r#"<?php
808 if (!defined('FOO')) {
809 const FOO = 1;
810 }
811 "#;
812 assert!(constant_flags(&scan(code), "FOO").is_polyfill());
813 }
814
815 #[test]
816 fn define_call_in_not_defined_is_polyfill() {
817 let code = r#"<?php
818 if (!defined('BAR')) {
819 define('BAR', 1);
820 }
821 "#;
822 assert!(constant_flags(&scan(code), "BAR").is_polyfill());
823 }
824
825 #[test]
826 fn class_in_else_branch_of_positive_check_is_polyfill() {
827 let code = r#"<?php
828 if (class_exists('Foo')) {
829 } else {
830 class Foo {}
831 }
832 "#;
833 assert!(class_flags(&scan(code), "Foo").is_polyfill());
834 }
835
836 #[test]
837 fn class_in_then_branch_of_positive_check_is_not_polyfill() {
838 let code = r#"<?php
839 if (class_exists('Foo')) {
840 class Bar {}
841 }
842 "#;
843 assert!(!class_flags(&scan(code), "Bar").is_polyfill());
844 }
845
846 #[test]
847 fn top_level_class_is_not_polyfill() {
848 let code = "<?php class Plain {}";
849 assert!(!class_flags(&scan(code), "Plain").is_polyfill());
850 }
851
852 #[test]
853 fn class_inside_unrelated_if_is_not_polyfill() {
854 let code = r#"<?php
855 if (PHP_VERSION_ID > 80000) {
856 class Modern {}
857 }
858 "#;
859 assert!(!class_flags(&scan(code), "Modern").is_polyfill());
860 }
861
862 #[test]
863 fn class_in_then_branch_when_condition_is_not_exists_check_is_not_polyfill() {
864 let code = r#"<?php
865 if (!some_other_check()) {
866 class Other {}
867 }
868 "#;
869 assert!(!class_flags(&scan(code), "Other").is_polyfill());
870 }
871
872 #[test]
873 fn polyfill_flag_does_not_leak_to_siblings() {
874 let code = r#"<?php
875 if (!class_exists('Polyfilled')) {
876 class Polyfilled {}
877 }
878
879 class Real {}
880 "#;
881 let codebase = scan(code);
882 assert!(class_flags(&codebase, "Polyfilled").is_polyfill());
883 assert!(!class_flags(&codebase, "Real").is_polyfill());
884 }
885
886 #[test]
887 fn class_inside_else_does_not_leak_to_preceding_sibling() {
888 let code = r#"<?php
889 if (class_exists('Gate')) {
890 class Sibling {}
891 } else {
892 class Gate {}
893 }
894 "#;
895 let codebase = scan(code);
896 assert!(!class_flags(&codebase, "Sibling").is_polyfill());
897 assert!(class_flags(&codebase, "Gate").is_polyfill());
898 }
899
900 #[test]
901 fn class_nested_inside_polyfill_guard_is_still_polyfill() {
902 let code = r#"<?php
903 if (!class_exists('Wrapper')) {
904 if (PHP_VERSION_ID >= 80000) {
905 class Wrapper {}
906 }
907 }
908 "#;
909 assert!(class_flags(&scan(code), "Wrapper").is_polyfill());
910 }
911
912 #[test]
913 fn nested_polyfill_guards_unwind_correctly() {
914 let code = r#"<?php
915 if (!class_exists('A')) {
916 class A {}
917 }
918 class B {}
919 if (!class_exists('C')) {
920 class C {}
921 }
922 class D {}
923 "#;
924 let codebase = scan(code);
925 assert!(class_flags(&codebase, "A").is_polyfill());
926 assert!(!class_flags(&codebase, "B").is_polyfill());
927 assert!(class_flags(&codebase, "C").is_polyfill());
928 assert!(!class_flags(&codebase, "D").is_polyfill());
929 }
930
931 #[test]
932 fn polyfill_within_namespace_gets_full_fqn_flagged() {
933 let code = r#"<?php
934 namespace Pkg;
935 if (!class_exists('Pkg\\Stub')) {
936 class Stub {}
937 }
938 "#;
939 assert!(class_flags(&scan(code), "Pkg\\Stub").is_polyfill());
940 }
941
942 #[test]
943 fn class_in_alternative_syntax_then_branch_is_polyfill() {
944 let code = r#"<?php
945 if (!class_exists('Alt')):
946 class Alt {}
947 endif;
948 "#;
949 assert!(class_flags(&scan(code), "Alt").is_polyfill());
950 }
951
952 #[test]
953 fn class_in_alternative_syntax_else_branch_is_polyfill() {
954 let code = r#"<?php
955 if (class_exists('AltElse')):
956 else:
957 class AltElse {}
958 endif;
959 "#;
960 assert!(class_flags(&scan(code), "AltElse").is_polyfill());
961 }
962
963 #[test]
964 fn leading_backslash_on_guard_function_is_recognized() {
965 let code = r#"<?php
966 if (!\class_exists('Qualified')) {
967 class Qualified {}
968 }
969 "#;
970 assert!(class_flags(&scan(code), "Qualified").is_polyfill());
971 }
972
973 #[test]
974 fn guard_function_case_insensitive() {
975 let code = r#"<?php
976 if (!CLASS_EXISTS('Uppercase')) {
977 class Uppercase {}
978 }
979 "#;
980 assert!(class_flags(&scan(code), "Uppercase").is_polyfill());
981 }
982
983 #[test]
984 fn parenthesized_guard_expression_is_recognized() {
985 let code = r#"<?php
986 if (!(class_exists('Parenned'))) {
987 class Parenned {}
988 }
989 "#;
990 assert!(class_flags(&scan(code), "Parenned").is_polyfill());
991 }
992
993 #[test]
994 fn doubly_parenthesized_guard_is_recognized() {
995 let code = r#"<?php
996 if ((!((class_exists('DoubleParen'))))) {
997 class DoubleParen {}
998 }
999 "#;
1000 assert!(class_flags(&scan(code), "DoubleParen").is_polyfill());
1001 }
1002
1003 #[test]
1004 fn class_in_elseif_branch_is_not_polyfill() {
1005 let code = r#"<?php
1006 if (false) {
1007 } elseif (!class_exists('Never')) {
1008 class Never {}
1009 }
1010 "#;
1011 assert!(!class_flags(&scan(code), "Never").is_polyfill());
1012 }
1013
1014 #[test]
1015 fn merge_non_polyfill_overrides_polyfill() {
1016 let mut stub = scan(
1017 r#"<?php
1018 if (!class_exists('Shared')) {
1019 class Shared {}
1020 }
1021 "#,
1022 );
1023 let real = scan("<?php class Shared { public int $x = 1; }");
1024 stub.extend(real);
1025 let flags = class_flags(&stub, "Shared");
1026 assert!(!flags.is_polyfill(), "polyfill should have been replaced by real: flags = {flags:?}");
1027 }
1028
1029 #[test]
1030 fn merge_polyfill_does_not_override_non_polyfill() {
1031 let mut real = scan("<?php class Shared { public int $x = 1; }");
1032 let stub = scan(
1033 r#"<?php
1034 if (!class_exists('Shared')) {
1035 class Shared {}
1036 }
1037 "#,
1038 );
1039 real.extend(stub);
1040 assert!(!class_flags(&real, "Shared").is_polyfill());
1041 }
1042
1043 #[test]
1044 fn merge_only_polyfill_is_kept() {
1045 let codebase = scan(
1046 r#"<?php
1047 if (!class_exists('OnlyStub')) {
1048 class OnlyStub {}
1049 }
1050 "#,
1051 );
1052 assert!(class_flags(&codebase, "OnlyStub").is_polyfill());
1053 }
1054
1055 #[test]
1056 fn merge_function_non_polyfill_overrides_polyfill() {
1057 let mut stub = scan(
1058 r#"<?php
1059 if (!function_exists('array_is_list')) {
1060 function array_is_list(array $arr): bool { return true; }
1061 }
1062 "#,
1063 );
1064 let real = scan("<?php function array_is_list(array $arr): bool { return false; }");
1065 stub.extend(real);
1066 assert!(!function_flags(&stub, "array_is_list").is_polyfill());
1067 }
1068
1069 #[test]
1070 fn merge_constant_non_polyfill_overrides_polyfill() {
1071 let mut stub = scan(
1072 r#"<?php
1073 if (!defined('MY_CONST')) {
1074 const MY_CONST = 1;
1075 }
1076 "#,
1077 );
1078 let real = scan("<?php const MY_CONST = 2;");
1079 stub.extend(real);
1080 assert!(!constant_flags(&stub, "MY_CONST").is_polyfill());
1081 }
1082
1083 #[test]
1084 fn phpunit_test_case_stub_scenario_prefers_real() {
1085 let mut codebase = scan(
1086 r#"<?php
1087 namespace PHPUnit\Framework;
1088
1089 if (!class_exists('PHPUnit\\Framework\\TestCase')) {
1090 abstract class TestCase {}
1091 }
1092 "#,
1093 );
1094 let real = scan(
1095 r#"<?php
1096 namespace PHPUnit\Framework {
1097 abstract class Assert {}
1098 abstract class TestCase extends Assert {}
1099 }
1100 "#,
1101 );
1102 codebase.extend(real);
1103
1104 let tc = codebase
1105 .class_likes
1106 .get(&ascii_lowercase_atom("PHPUnit\\Framework\\TestCase"))
1107 .expect("TestCase should be present in merged codebase");
1108
1109 assert!(!tc.flags.is_polyfill(), "merged TestCase should be the real definition");
1110 assert_eq!(
1111 tc.direct_parent_class.map(|p| p.to_string()),
1112 Some("PHPUnit\\Framework\\Assert".to_ascii_lowercase()),
1113 );
1114 }
1115}