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