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