Skip to main content

mago_codex/scanner/
mod.rs

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