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