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