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