Skip to main content

mago_codex/metadata/
class_like.rs

1use foldhash::fast::RandomState;
2use indexmap::IndexMap;
3use mago_php_version::PHPVersion;
4use mago_php_version::PHPVersionRange;
5
6use mago_reporting::Annotation;
7use mago_reporting::Issue;
8use mago_span::Span;
9use mago_word::Word;
10use mago_word::WordMap;
11use mago_word::WordSet;
12
13use crate::flags::attribute::AttributeFlags;
14use crate::identifier::method::MethodIdentifier;
15use crate::issue::ScanningIssueKind;
16use crate::metadata::attribute::AttributeMetadata;
17use crate::metadata::class_like_constant::ClassLikeConstantMetadata;
18use crate::metadata::enum_case::EnumCaseMetadata;
19use crate::metadata::flags::MetadataFlags;
20use crate::metadata::property::PropertyMetadata;
21use crate::metadata::ttype::TypeMetadata;
22use crate::metadata::version_constraint::VersionConstraint;
23use crate::symbol::SymbolKind;
24use crate::ttype::atomic::TAtomic;
25use crate::ttype::template::GenericTemplate;
26use crate::ttype::template::variance::Variance;
27use crate::ttype::union::TUnion;
28use crate::visibility::Visibility;
29
30/// Type alias for template types stored in metadata.
31/// Maps template parameter names to their defining entity and constraint type.
32pub type TemplateTypes = IndexMap<Word, GenericTemplate, RandomState>;
33
34/// Contains comprehensive metadata for a PHP class-like structure (class, interface, trait, enum).
35///
36/// Aggregates information about inheritance, traits, generics, methods, properties, constants,
37/// attributes, docblock tags, analysis flags, and more.
38#[derive(Clone, Debug, PartialEq, Eq)]
39#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
40#[non_exhaustive]
41pub struct ClassLikeMetadata {
42    pub name: Word,
43    pub original_name: Word,
44    pub span: Span,
45    pub direct_parent_interfaces: WordSet,
46    pub all_parent_interfaces: WordSet,
47    pub direct_parent_class: Option<Word>,
48    pub require_extends: WordSet,
49    pub require_implements: WordSet,
50    pub all_parent_classes: WordSet,
51    pub used_traits: WordSet,
52    pub trait_alias_map: WordMap<Word>,
53    pub trait_visibility_map: WordMap<Visibility>,
54    pub trait_final_map: WordSet,
55    pub child_class_likes: Option<WordSet>,
56    pub name_span: Option<Span>,
57    pub kind: SymbolKind,
58    pub template_types: TemplateTypes,
59    pub template_readonly: WordSet,
60    pub template_variance: Vec<Variance>,
61    pub template_extended_offsets: WordMap<Vec<TUnion>>,
62    pub template_extended_parameters: WordMap<IndexMap<Word, TUnion, RandomState>>,
63    pub template_extended_parameter_paths: WordMap<Vec<IndexMap<Word, TUnion, RandomState>>>,
64    pub template_type_extends_count: WordMap<usize>,
65    pub template_type_implements_count: WordMap<usize>,
66    pub template_type_uses_count: WordMap<usize>,
67    pub methods: WordSet,
68    pub pseudo_methods: WordSet,
69    pub static_pseudo_methods: WordSet,
70    pub declaring_method_ids: WordMap<MethodIdentifier>,
71    pub appearing_method_ids: WordMap<MethodIdentifier>,
72    pub inheritable_method_ids: WordMap<MethodIdentifier>,
73    pub overridden_method_ids: WordMap<IndexMap<Word, MethodIdentifier, RandomState>>,
74    pub properties: WordMap<PropertyMetadata>,
75    pub appearing_property_ids: WordMap<Word>,
76    pub declaring_property_ids: WordMap<Word>,
77    pub inheritable_property_ids: WordMap<Word>,
78    pub overridden_property_ids: WordMap<WordSet>,
79    pub initialized_properties: WordSet,
80    pub constants: WordMap<ClassLikeConstantMetadata>,
81    pub trait_constant_ids: WordMap<Word>,
82    pub enum_cases: WordMap<EnumCaseMetadata>,
83    pub invalid_dependencies: WordSet,
84    pub attributes: Vec<AttributeMetadata>,
85    pub enum_type: Option<TAtomic>,
86    pub has_sealed_methods: Option<bool>,
87    pub has_sealed_properties: Option<bool>,
88    pub permitted_inheritors: Option<WordSet>,
89    pub issues: Vec<Issue>,
90    pub attribute_flags: Option<AttributeFlags>,
91    pub flags: MetadataFlags,
92    pub type_aliases: WordMap<TypeMetadata>,
93    /// Imported type aliases in the form of (`from_fqcn`, `type_name`, span)
94    pub imported_type_aliases: WordMap<(Word, Word, Span)>,
95    /// Mixin types from @mixin annotations - these types' methods/properties
96    /// can be accessed via magic methods (__call, __get, __set, __callStatic)
97    pub mixins: Vec<TUnion>,
98    pub version_constraint: VersionConstraint,
99}
100
101impl ClassLikeMetadata {
102    #[must_use]
103    pub fn new(
104        name: Word,
105        original_name: Word,
106        span: Span,
107        name_span: Option<Span>,
108        flags: MetadataFlags,
109    ) -> ClassLikeMetadata {
110        ClassLikeMetadata {
111            constants: WordMap::default(),
112            trait_constant_ids: WordMap::default(),
113            enum_cases: WordMap::default(),
114            flags,
115            kind: SymbolKind::Class,
116            direct_parent_interfaces: WordSet::default(),
117            all_parent_classes: WordSet::default(),
118            appearing_method_ids: WordMap::default(),
119            attributes: Vec::new(),
120            all_parent_interfaces: WordSet::default(),
121            declaring_method_ids: WordMap::default(),
122            appearing_property_ids: WordMap::default(),
123            declaring_property_ids: WordMap::default(),
124            direct_parent_class: None,
125            require_extends: WordSet::default(),
126            require_implements: WordSet::default(),
127            inheritable_method_ids: WordMap::default(),
128            enum_type: None,
129            inheritable_property_ids: WordMap::default(),
130            initialized_properties: WordSet::default(),
131            invalid_dependencies: WordSet::default(),
132            span,
133            name_span,
134            methods: WordSet::default(),
135            pseudo_methods: WordSet::default(),
136            static_pseudo_methods: WordSet::default(),
137            overridden_method_ids: WordMap::default(),
138            overridden_property_ids: WordMap::default(),
139            properties: WordMap::default(),
140            template_variance: Vec::new(),
141            template_type_extends_count: WordMap::default(),
142            template_extended_parameters: WordMap::default(),
143            template_extended_parameter_paths: WordMap::default(),
144            template_extended_offsets: WordMap::default(),
145            template_type_implements_count: WordMap::default(),
146            template_type_uses_count: WordMap::default(),
147            template_types: TemplateTypes::default(),
148            used_traits: WordSet::default(),
149            trait_alias_map: WordMap::default(),
150            trait_visibility_map: WordMap::default(),
151            trait_final_map: WordSet::default(),
152            name,
153            original_name,
154            child_class_likes: None,
155            template_readonly: WordSet::default(),
156            has_sealed_methods: None,
157            has_sealed_properties: None,
158            permitted_inheritors: None,
159            issues: vec![],
160            attribute_flags: None,
161            type_aliases: WordMap::default(),
162            imported_type_aliases: WordMap::default(),
163            mixins: Vec::default(),
164            version_constraint: VersionConstraint::unconstrained(),
165        }
166    }
167
168    /// Returns `true` when this class-like is available in the given PHP
169    /// version.
170    #[inline]
171    #[must_use]
172    pub fn is_available_in_version(&self, version: PHPVersion) -> bool {
173        self.version_constraint.allows_version(version)
174    }
175
176    /// Returns `true` when this class-like is available across the entire
177    /// supplied [`PHPVersionRange`].
178    #[inline]
179    #[must_use]
180    pub fn is_available_in_version_range(&self, range: PHPVersionRange) -> bool {
181        self.version_constraint.allows_version_range(range)
182    }
183
184    /// Returns a reference to the map of trait method aliases.
185    #[inline]
186    #[must_use]
187    pub fn get_trait_alias_map(&self) -> &WordMap<Word> {
188        &self.trait_alias_map
189    }
190
191    /// Returns a vector of the generic type parameter names.
192    #[inline]
193    #[must_use]
194    pub fn get_template_type_names(&self) -> Vec<Word> {
195        self.template_types.keys().copied().collect()
196    }
197
198    /// Returns type parameters for a specific generic parameter name.
199    #[inline]
200    #[must_use]
201    pub fn get_template_type(&self, name: Word) -> Option<&GenericTemplate> {
202        self.template_types.get(&name)
203    }
204
205    /// Returns type parameters for a specific generic parameter name with its index.
206    #[inline]
207    #[must_use]
208    pub fn get_template_type_with_index(&self, name: Word) -> Option<(usize, &GenericTemplate)> {
209        self.template_types.get_full(&name).map(|(index, _, types)| (index, types))
210    }
211
212    #[must_use]
213    pub fn get_template_for_index(&self, index: usize) -> Option<(Word, &GenericTemplate)> {
214        self.template_types.get_index(index).map(|(name, types)| (*name, types))
215    }
216
217    #[must_use]
218    pub fn get_template_name_for_index(&self, index: usize) -> Option<Word> {
219        self.template_types.get_index(index).map(|(name, _)| *name)
220    }
221
222    #[must_use]
223    pub fn get_template_index_for_name(&self, name: Word) -> Option<usize> {
224        self.template_types.get_index_of(&name)
225    }
226
227    /// Checks if a specific parent is either a parent class or interface.
228    #[inline]
229    #[must_use]
230    pub fn has_parent(&self, parent: Word) -> bool {
231        self.all_parent_classes.contains(&parent) || self.all_parent_interfaces.contains(&parent)
232    }
233
234    /// Checks if a specific parent has template extended parameters.
235    #[inline]
236    #[must_use]
237    pub fn has_template_extended_parameter(&self, parent: Word) -> bool {
238        self.template_extended_parameters.contains_key(&parent)
239    }
240
241    /// Checks if a specific method appears in this class-like.
242    #[inline]
243    #[must_use]
244    pub fn has_appearing_method(&self, method: Word) -> bool {
245        self.appearing_method_ids.contains_key(&method)
246    }
247
248    /// Returns a vector of property names.
249    #[inline]
250    #[must_use]
251    pub fn get_property_names(&self) -> WordSet {
252        self.properties.keys().copied().collect()
253    }
254
255    /// Checks if a specific property appears in this class-like.
256    #[inline]
257    #[must_use]
258    pub fn has_appearing_property(&self, name: Word) -> bool {
259        self.appearing_property_ids.contains_key(&name)
260    }
261
262    /// Checks if a specific property is declared in this class-like.
263    #[inline]
264    #[must_use]
265    pub fn has_declaring_property(&self, name: Word) -> bool {
266        self.declaring_property_ids.contains_key(&name)
267    }
268
269    /// Takes ownership of the issues found for this class-like structure.
270    #[inline]
271    pub fn take_issues(&mut self) -> Vec<Issue> {
272        std::mem::take(&mut self.issues)
273    }
274
275    /// Adds a single direct parent interface.
276    #[inline]
277    pub fn add_direct_parent_interface(&mut self, interface: Word) {
278        self.direct_parent_interfaces.insert(interface);
279        self.all_parent_interfaces.insert(interface);
280    }
281
282    /// Adds a single interface to the list of all parent interfaces. Use with caution, normally derived.
283    #[inline]
284    pub fn add_all_parent_interface(&mut self, interface: Word) {
285        self.all_parent_interfaces.insert(interface);
286    }
287
288    /// Adds multiple interfaces to the list of all parent interfaces. Use with caution.
289    #[inline]
290    pub fn add_all_parent_interfaces(&mut self, interfaces: impl IntoIterator<Item = Word>) {
291        self.all_parent_interfaces.extend(interfaces);
292    }
293
294    /// Adds multiple ancestor classes. Use with caution.
295    #[inline]
296    pub fn add_all_parent_classes(&mut self, classes: impl IntoIterator<Item = Word>) {
297        self.all_parent_classes.extend(classes);
298    }
299
300    /// Adds a single used trait. Returns `true` if the trait was not already present.
301    #[inline]
302    pub fn add_used_trait(&mut self, trait_name: Word) -> bool {
303        self.used_traits.insert(trait_name)
304    }
305
306    /// Adds multiple used traits.
307    #[inline]
308    pub fn add_used_traits(&mut self, traits: impl IntoIterator<Item = Word>) {
309        self.used_traits.extend(traits);
310    }
311
312    /// Adds or updates a single trait alias. Returns the previous original name if one existed for the alias.
313    #[inline]
314    pub fn add_trait_alias(&mut self, method: Word, alias: Word) -> Option<Word> {
315        self.trait_alias_map.insert(method, alias)
316    }
317
318    /// Adds or updates a single trait visibility override. Returns the previous visibility if one existed.
319    #[inline]
320    pub fn add_trait_visibility(&mut self, method: Word, visibility: Visibility) -> Option<Visibility> {
321        self.trait_visibility_map.insert(method, visibility)
322    }
323
324    /// Adds a single template type definition.
325    #[inline]
326    pub fn add_template_type(&mut self, name: Word, constraint: GenericTemplate) {
327        self.template_types.insert(name, constraint);
328    }
329
330    /// Set the variance for the template parameters
331    #[inline]
332    pub fn set_template_variance(&mut self, template_variance: Vec<Variance>) {
333        self.template_variance = template_variance;
334    }
335
336    /// Adds or replaces the offset types for a specific template parameter name.
337    #[inline]
338    pub fn add_template_extended_offset(&mut self, name: Word, types: Vec<TUnion>) -> Option<Vec<TUnion>> {
339        self.template_extended_offsets.insert(name, types)
340    }
341
342    /// Adds or replaces the resolved parameters for a specific parent FQCN.
343    #[inline]
344    pub fn extend_template_extended_parameters(
345        &mut self,
346        template_extended_parameters: WordMap<IndexMap<Word, TUnion, RandomState>>,
347    ) {
348        self.template_extended_parameters.extend(template_extended_parameters);
349    }
350
351    /// Adds or replaces a single resolved parameter for the parent FQCN.
352    #[inline]
353    pub fn add_template_extended_parameter(
354        &mut self,
355        parent_fqcn: Word,
356        parameter_name: Word,
357        parameter_type: TUnion,
358    ) -> Option<TUnion> {
359        self.template_extended_parameters.entry(parent_fqcn).or_default().insert(parameter_name, parameter_type)
360    }
361
362    /// Records one complete parameterization of `ancestor` (a single inheritance
363    /// path), de-duplicating against parameterizations already recorded.
364    #[inline]
365    pub fn record_template_extended_path(&mut self, ancestor: Word, parameters: IndexMap<Word, TUnion, RandomState>) {
366        if parameters.is_empty() {
367            return;
368        }
369
370        let paths = self.template_extended_parameter_paths.entry(ancestor).or_default();
371        if !paths.contains(&parameters) {
372            paths.push(parameters);
373        }
374    }
375
376    /// Adds or updates the declaring method identifier for a method name.
377    #[inline]
378    pub fn add_declaring_method_id(
379        &mut self,
380        method: Word,
381        declaring_method_id: MethodIdentifier,
382    ) -> Option<MethodIdentifier> {
383        self.add_appearing_method_id(method, declaring_method_id);
384        self.declaring_method_ids.insert(method, declaring_method_id)
385    }
386
387    /// Adds or updates the appearing method identifier for a method name.
388    #[inline]
389    pub fn add_appearing_method_id(
390        &mut self,
391        method: Word,
392        appearing_method_id: MethodIdentifier,
393    ) -> Option<MethodIdentifier> {
394        self.appearing_method_ids.insert(method, appearing_method_id)
395    }
396
397    /// Adds a parent method identifier to the map for an overridden method. Initializes map if needed. Returns the previous value if one existed.
398    #[inline]
399    pub fn add_overridden_method_parent(
400        &mut self,
401        method: Word,
402        parent_method_id: MethodIdentifier,
403    ) -> Option<MethodIdentifier> {
404        self.overridden_method_ids
405            .entry(method)
406            .or_default()
407            .insert(parent_method_id.get_class_name(), parent_method_id)
408    }
409
410    /// Adds or updates a property's metadata. Returns the previous metadata if the property existed.
411    #[inline]
412    pub fn add_property(&mut self, name: Word, property_metadata: PropertyMetadata) -> Option<PropertyMetadata> {
413        let class_name = self.name;
414
415        self.add_declaring_property_id(name, class_name);
416        if property_metadata.flags.has_default() {
417            self.initialized_properties.insert(name);
418        }
419
420        if !property_metadata.is_final() {
421            self.inheritable_property_ids.insert(name, class_name);
422        }
423
424        self.properties.insert(name, property_metadata)
425    }
426
427    /// Adds or updates a property's metadata using just the property metadata. Returns the previous metadata if the property existed.
428    #[inline]
429    pub fn add_property_metadata(&mut self, property_metadata: PropertyMetadata) -> Option<PropertyMetadata> {
430        let name = property_metadata.get_name().0;
431
432        self.add_property(name, property_metadata)
433    }
434
435    /// Adds or updates the declaring class FQCN for a property name.
436    #[inline]
437    pub fn add_declaring_property_id(&mut self, prop: Word, declaring_fqcn: Word) -> Option<Word> {
438        self.appearing_property_ids.insert(prop, declaring_fqcn);
439        self.declaring_property_ids.insert(prop, declaring_fqcn)
440    }
441
442    #[must_use]
443    pub fn get_missing_required_interface<'meta>(&self, other: &'meta ClassLikeMetadata) -> Option<&'meta Word> {
444        for required_interface in &other.require_implements {
445            if self.all_parent_interfaces.contains(required_interface) {
446                continue;
447            }
448
449            if (self.flags.is_abstract() || self.kind.is_trait())
450                && self.require_implements.contains(required_interface)
451            {
452                continue; // Abstract classes and traits can require interfaces they implement
453            }
454
455            return Some(required_interface);
456        }
457
458        None
459    }
460
461    #[must_use]
462    pub fn get_missing_required_extends<'meta>(&self, other: &'meta ClassLikeMetadata) -> Option<&'meta Word> {
463        for required_extend in &other.require_extends {
464            if self.all_parent_classes.contains(required_extend) {
465                continue;
466            }
467
468            if self.kind.is_interface() && self.all_parent_interfaces.contains(required_extend) {
469                continue;
470            }
471
472            if (self.flags.is_abstract() || self.kind.is_trait()) && self.require_extends.contains(required_extend) {
473                continue; // Abstract classes and traits can require classes they extend
474            }
475
476            return Some(required_extend);
477        }
478
479        None
480    }
481
482    #[must_use]
483    pub fn is_permitted_to_inherit(&self, other: &ClassLikeMetadata) -> bool {
484        if self.kind.is_trait() || self.flags.is_abstract() {
485            return true; // Traits and abstract classes can always inherit
486        }
487
488        let Some(permitted_inheritors) = &other.permitted_inheritors else {
489            return true; // No restrictions, inheriting is allowed
490        };
491
492        if permitted_inheritors.contains(&self.name) {
493            return true; // This class-like is explicitly permitted to inherit
494        }
495
496        self.all_parent_interfaces.iter().any(|parent_interface| permitted_inheritors.contains(parent_interface))
497            || self.all_parent_classes.iter().any(|parent_class| permitted_inheritors.contains(parent_class))
498            || self.used_traits.iter().any(|used_trait| permitted_inheritors.contains(used_trait))
499    }
500
501    #[inline]
502    pub fn mark_as_populated(&mut self) {
503        self.flags |= MetadataFlags::POPULATED;
504        self.shrink_to_fit();
505    }
506
507    /// Applies a patch to this class in place, refining member type information while leaving
508    /// structure (hierarchy, override chains, initialization state, enum cases) untouched.
509    ///
510    /// At most one patch targets a given symbol, so there is no accumulation to worry about.
511    /// Diagnostics about the patch are appended to `patch.issues`, where scan-time diagnostics
512    /// also live, so they flow out through `take_issues`. The full set of rules a patch must
513    /// obey is documented in the `[source]` patching guide.
514    pub fn apply_patch(&mut self, patch: &mut ClassLikeMetadata, inherited_methods: &WordSet) {
515        // Patches redeclare the same class by name, so their member identifiers
516        // already carry the correct class name and merge cleanly.
517        debug_assert_eq!(self.name, patch.name, "patch class name must match the patched class");
518
519        // A kind or hierarchy mismatch means the patch targets a different symbol; reject
520        // it wholesale rather than refining members against the wrong class.
521        if self.report_kind_mismatch(patch) || self.report_hierarchy_mismatch(patch) {
522            return;
523        }
524
525        self.report_readonly_mismatch(patch);
526        self.report_trait_usage(patch);
527
528        self.patch_templates(patch);
529        self.patch_methods(patch, inherited_methods);
530        self.patch_properties(patch);
531        self.patch_constants(patch);
532        self.report_enum_cases(patch);
533
534        // Type aliases
535        self.type_aliases.extend(patch.type_aliases.iter().map(|(k, v)| (*k, v.clone())));
536    }
537
538    /// Reports a `kind` mismatch between the patch and the original symbol.
539    ///
540    /// Returns `true` when the kinds differ, in which case the patch must be rejected
541    /// wholesale because it targets a different kind of symbol.
542    fn report_kind_mismatch(&self, patch: &mut ClassLikeMetadata) -> bool {
543        if self.kind == patch.kind {
544            return false;
545        }
546
547        patch.issues.push(
548            Issue::error(format!(
549                "Patch declares `{}` as a {} but the original symbol is a {}; patch members are ignored.",
550                patch.original_name,
551                patch.kind.as_str(),
552                self.kind.as_str(),
553            ))
554            .with_code(ScanningIssueKind::PatchKindMismatch)
555            .with_annotation(Annotation::primary(patch.span))
556            .with_help(format!(
557                "Declare `{}` as a {} in the patch so it matches the original symbol, \
558                 or remove the patch if it targets the wrong symbol.",
559                patch.original_name,
560                self.kind.as_str(),
561            )),
562        );
563
564        true
565    }
566
567    /// Reports a hierarchy mismatch between the patch and the original symbol.
568    ///
569    /// Hierarchy declarations must match the original exactly if declared; a mismatch means
570    /// the patch is describing a different class. Returns `true` when they differ, in which
571    /// case the patch must be rejected wholesale.
572    fn report_hierarchy_mismatch(&self, patch: &mut ClassLikeMetadata) -> bool {
573        let hierarchy_mismatch = (patch.direct_parent_class.is_some()
574            && patch.direct_parent_class != self.direct_parent_class)
575            || (!patch.direct_parent_interfaces.is_empty()
576                && patch.direct_parent_interfaces != self.direct_parent_interfaces)
577            || (!patch.require_extends.is_empty() && patch.require_extends != self.require_extends)
578            || (!patch.require_implements.is_empty() && patch.require_implements != self.require_implements);
579
580        if !hierarchy_mismatch {
581            return false;
582        }
583
584        patch.issues.push(
585            Issue::error(format!(
586                "Patch for `{}` declares hierarchy that does not match the original; patch members are ignored.",
587                patch.original_name,
588            ))
589            .with_code(ScanningIssueKind::PatchHierarchyMismatch)
590            .with_annotation(Annotation::primary(patch.span))
591            .with_help(
592                "Patches do not need to restate hierarchy. Drop the parent class, interface, \
593                 `@require-extends`, or `@require-implements` declarations from the patch, or \
594                 correct them to match the original exactly.",
595            ),
596        );
597
598        true
599    }
600
601    /// Reports a `readonly class` modifier mismatch.
602    ///
603    /// `readonly class` is a structural modifier — a patch cannot add or remove it. This is a
604    /// warning only and does not abort the patch.
605    fn report_readonly_mismatch(&self, patch: &mut ClassLikeMetadata) {
606        if patch.flags.contains(MetadataFlags::READONLY) == self.flags.contains(MetadataFlags::READONLY) {
607            return;
608        }
609
610        patch.issues.push(
611            Issue::warning(format!(
612                "Patch declares `{}` as a {} class but the original is {}; readonly modifier is ignored.",
613                patch.original_name,
614                if patch.flags.contains(MetadataFlags::READONLY) { "readonly" } else { "non-readonly" },
615                if self.flags.contains(MetadataFlags::READONLY) { "readonly" } else { "non-readonly" },
616            ))
617            .with_code(ScanningIssueKind::PatchReadonlyMismatch)
618            .with_annotation(Annotation::primary(patch.span))
619            .with_help(
620                "The `readonly` class modifier is structural and cannot be changed by a patch. \
621                 Match the original by adding or removing `readonly` on the patched class declaration.",
622            ),
623        );
624    }
625
626    /// Reports `use` trait declarations on the patch, which are never valid.
627    ///
628    /// Patches refine member type information only; trait usage declarations are ignored.
629    fn report_trait_usage(&self, patch: &mut ClassLikeMetadata) {
630        if patch.used_traits.is_empty() {
631            return;
632        }
633
634        patch.issues.push(
635            Issue::warning(format!(
636                "Patch for `{}` declares `use` traits; patches refine member type information only and trait usage declarations are ignored.",
637                patch.original_name,
638            ))
639            .with_code(ScanningIssueKind::PatchDeclaresTrait)
640            .with_annotation(Annotation::primary(patch.span))
641            .with_help(
642                "Remove the `use` trait statement from the patch. To refine the type information \
643                 of members the trait contributes, patch those members on the class directly.",
644            ),
645        );
646    }
647
648    /// Merges `@template` declarations from the patch: existing names are overridden, new
649    /// names appended.
650    ///
651    /// `template_variance` is keyed by position in the `template_types` IndexMap and must be
652    /// rebuilt after the merge; `template_readonly` is name-keyed and can be extended directly.
653    fn patch_templates(&mut self, patch: &ClassLikeMetadata) {
654        if patch.template_types.is_empty() {
655            return;
656        }
657
658        // Collect name → variance for the current (original) state.
659        let mut name_to_variance: WordMap<Variance> = self
660            .template_types
661            .keys()
662            .enumerate()
663            .map(|(i, name)| (*name, self.template_variance.get(i).copied().unwrap_or(Variance::Invariant)))
664            .collect();
665
666        // Patch overrides existing entries and contributes new ones.
667        name_to_variance.extend(
668            patch
669                .template_types
670                .keys()
671                .enumerate()
672                .map(|(i, name)| (*name, patch.template_variance.get(i).copied().unwrap_or(Variance::Invariant))),
673        );
674
675        // Extend the IndexMap: existing names get updated definitions, new ones are appended.
676        self.template_types.extend(patch.template_types.iter().map(|(k, v)| (*k, v.clone())));
677
678        // Rebuild position-indexed variance vec to match the merged IndexMap order.
679        // template_readonly is name-keyed so it doesn't need rebuilding — just extend it.
680        self.template_variance = self
681            .template_types
682            .keys()
683            .map(|name| name_to_variance.get(name).copied().unwrap_or(Variance::Invariant))
684            .collect();
685        self.template_readonly.extend(patch.template_readonly.iter().copied());
686    }
687
688    /// Applies method declarations from the patch.
689    ///
690    /// A real method already declared on the original needs no change here — its type info
691    /// flows through `FunctionLikeMetadata::apply_patch` on the function-like. A method
692    /// inherited from an ancestor is added to the structural maps so the populator treats this
693    /// class as the declaring site, and `apply_patches_pass` then materializes the
694    /// function-like. A method that exists nowhere in the chain is rejected as a new member.
695    /// Pseudo-methods (`@method`) are class-level and always update the structural maps.
696    fn patch_methods(&mut self, patch: &mut ClassLikeMetadata, inherited_methods: &WordSet) {
697        for method_name in &patch.methods {
698            if patch.pseudo_methods.contains(method_name) || patch.static_pseudo_methods.contains(method_name) {
699                continue;
700            }
701            if inherited_methods.contains(method_name) {
702                // The patch introduces an override for an inherited method. Mirror what the
703                // scanner records for a directly-declared method so the populator preserves
704                // this class as the declaring/appearing site (rather than re-inheriting the
705                // ancestor's ids) and the refined function-like at `(self, method)` wins.
706                // `apply_patches_pass` materializes that function-like from the ancestor.
707                if self.methods.insert(*method_name) {
708                    if let Some(id) = patch.declaring_method_ids.get(method_name) {
709                        self.declaring_method_ids.insert(*method_name, *id);
710                    }
711                    if let Some(id) = patch.appearing_method_ids.get(method_name) {
712                        self.appearing_method_ids.insert(*method_name, *id);
713                    }
714                    if let Some(id) = patch.inheritable_method_ids.get(method_name) {
715                        self.inheritable_method_ids.insert(*method_name, *id);
716                    }
717                }
718            } else if !self.methods.contains(method_name) {
719                patch.issues.push(
720                    Issue::error(format!(
721                        "Patch for `{}` declares method `{}` which does not exist in the original \
722                         or any of its ancestors; patches cannot introduce new methods.",
723                        patch.original_name, method_name,
724                    ))
725                    .with_code(ScanningIssueKind::PatchIntroducesNewMethod)
726                    .with_annotation(Annotation::primary(patch.span))
727                    .with_help(format!(
728                        "Remove `{method_name}` from the patch, or correct its name to match a method \
729                         that already exists on `{}` or one of its ancestors. To annotate a magic \
730                         method handled by `__call`/`__callStatic`, declare it with `@method` instead. \
731                         Patches can only refine the types of existing methods.",
732                        patch.original_name,
733                    )),
734                );
735            }
736        }
737
738        for name in patch.pseudo_methods.iter().chain(patch.static_pseudo_methods.iter()) {
739            if let Some(id) = patch.declaring_method_ids.get(name) {
740                self.declaring_method_ids.insert(*name, *id);
741            }
742            if let Some(id) = patch.appearing_method_ids.get(name) {
743                self.appearing_method_ids.insert(*name, *id);
744            }
745            if let Some(id) = patch.inheritable_method_ids.get(name) {
746                self.inheritable_method_ids.insert(*name, *id);
747            }
748        }
749
750        self.pseudo_methods.extend(patch.pseudo_methods.iter().copied());
751        self.static_pseudo_methods.extend(patch.static_pseudo_methods.iter().copied());
752    }
753
754    /// Refines type annotations on existing properties. New properties are rejected unless
755    /// they are magic properties (`@property`/`@property-read`/`@property-write`), which carry
756    /// no runtime existence claim. Structural attributes must match the original.
757    fn patch_properties(&mut self, patch: &mut ClassLikeMetadata) {
758        let patch_span = patch.span;
759        let patch_name = patch.original_name;
760        for (name, prop_metadata) in &patch.properties {
761            if let Some(slot) = self.properties.get_mut(name) {
762                // Patches can only refine type annotations. Structural attributes
763                // (visibility, modifiers, hooks) must match the original exactly;
764                // if they differ the patch is wrong and we should say so rather than
765                // silently discarding the mismatch.
766                let visibility_mismatch = prop_metadata.read_visibility != slot.read_visibility
767                    || prop_metadata.write_visibility != slot.write_visibility;
768                // READONLY, STATIC, ABSTRACT: any mismatch is structural.
769                // FINAL: only an error when removed (vendor has it, patch doesn't);
770                //        adding final via a patch is allowed.
771                let structural_flag_mismatch =
772                    [MetadataFlags::READONLY, MetadataFlags::STATIC, MetadataFlags::ABSTRACT]
773                        .iter()
774                        .any(|&f| prop_metadata.flags.contains(f) != slot.flags.contains(f))
775                        || (slot.flags.contains(MetadataFlags::FINAL)
776                            && !prop_metadata.flags.contains(MetadataFlags::FINAL));
777                let has_hooks = !prop_metadata.hooks.is_empty();
778
779                if visibility_mismatch || structural_flag_mismatch || has_hooks {
780                    patch.issues.push(
781                        Issue::error(format!(
782                            "Patch for `{}::{}` declares structural attributes (visibility, modifiers, \
783                             or hooks) that differ from the original; only type annotations are applied.",
784                            patch_name, name,
785                        ))
786                        .with_code(ScanningIssueKind::PatchPropertyStructuralMismatch)
787                        .with_annotation(Annotation::primary(prop_metadata.span.unwrap_or(patch_span)))
788                        .with_help(format!(
789                            "Declare `{patch_name}::{name}` with the same visibility and modifiers as \
790                             the original and drop any property hooks; a patch may only refine the \
791                             property's type.",
792                        )),
793                    );
794                }
795
796                slot.type_declaration_metadata.clone_from(&prop_metadata.type_declaration_metadata);
797                slot.type_metadata.clone_from(&prop_metadata.type_metadata);
798            } else if prop_metadata.flags.is_magic_property() {
799                self.add_property(*name, prop_metadata.clone());
800            } else {
801                patch.issues.push(
802                    Issue::error(format!(
803                        "Patch declares property `{}::{}` which does not exist in the original; \
804                         patches cannot introduce new properties.",
805                        patch_name, name,
806                    ))
807                    .with_code(ScanningIssueKind::PatchIntroducesNewProperty)
808                    .with_annotation(Annotation::primary(patch_span))
809                    .with_help(format!(
810                        "Remove `{patch_name}::{name}` from the patch, or correct its name to match an \
811                         existing property. To annotate a magic property handled by `__get`/`__set`, \
812                         declare it with `@property`, `@property-read`, or `@property-write` instead.",
813                    )),
814                );
815            }
816        }
817    }
818
819    /// Refines type annotations on existing constants; new constants are rejected and
820    /// structural attributes must match the original.
821    fn patch_constants(&mut self, patch: &mut ClassLikeMetadata) {
822        let patch_span = patch.span;
823        let patch_name = patch.original_name;
824        for (name, const_metadata) in &patch.constants {
825            if let Some(slot) = self.constants.get_mut(name) {
826                let visibility_mismatch = const_metadata.visibility != slot.visibility;
827                // ABSTRACT: any mismatch is structural.
828                // FINAL: only an error when removed (vendor has it, patch doesn't);
829                //        adding final via a patch is allowed.
830                let structural_flag_mismatch = const_metadata.flags.contains(MetadataFlags::ABSTRACT)
831                    != slot.flags.contains(MetadataFlags::ABSTRACT)
832                    || (slot.flags.contains(MetadataFlags::FINAL)
833                        && !const_metadata.flags.contains(MetadataFlags::FINAL));
834
835                if visibility_mismatch || structural_flag_mismatch {
836                    patch.issues.push(
837                        Issue::error(format!(
838                            "Patch for `{}::{}` declares structural attributes (visibility or modifiers) \
839                             that differ from the original; only type annotations are applied.",
840                            patch_name, name,
841                        ))
842                        .with_code(ScanningIssueKind::PatchConstantStructuralMismatch)
843                        .with_annotation(Annotation::primary(const_metadata.span))
844                        .with_help(format!(
845                            "Declare `{patch_name}::{name}` with the same visibility and modifiers as \
846                             the original; a patch may only refine the constant's type.",
847                        )),
848                    );
849                }
850
851                slot.type_declaration.clone_from(&const_metadata.type_declaration);
852                slot.type_metadata.clone_from(&const_metadata.type_metadata);
853            } else {
854                patch.issues.push(
855                    Issue::error(format!(
856                        "Patch declares constant `{}::{}` which does not exist in the original; \
857                         patches cannot introduce new constants.",
858                        patch_name, name,
859                    ))
860                    .with_code(ScanningIssueKind::PatchIntroducesNewConstant)
861                    .with_annotation(Annotation::primary(patch_span))
862                    .with_help(format!(
863                        "Remove `{patch_name}::{name}` from the patch, or correct its name to match a \
864                         constant that already exists on the original. Patches can only refine the \
865                         types of existing constants.",
866                    )),
867                );
868            }
869        }
870    }
871
872    /// Reports enum case declarations on the patch, which are never valid.
873    ///
874    /// Enum cases are structural (they define the valid runtime values of an enum)
875    /// and cannot be modified by a patch.
876    fn report_enum_cases(&self, patch: &mut ClassLikeMetadata) {
877        if patch.enum_cases.is_empty() {
878            return;
879        }
880
881        patch.issues.push(
882            Issue::error(format!(
883                "Patch for `{}` declares enum case(s); enum cases are structural and cannot be \
884                 refined — patch enum cases are ignored.",
885                patch.original_name,
886            ))
887            .with_code(ScanningIssueKind::PatchEnumCasesIgnored)
888            .with_annotation(Annotation::primary(patch.span))
889            .with_help(
890                "Remove the enum case declarations from the patch. Enum cases define the runtime \
891                 values of the enum and must stay in the original definition; a patch can only \
892                 refine the types of existing members.",
893            ),
894        );
895    }
896
897    #[inline]
898    pub fn shrink_to_fit(&mut self) {
899        self.properties.shrink_to_fit();
900        self.initialized_properties.shrink_to_fit();
901        self.appearing_property_ids.shrink_to_fit();
902        self.declaring_property_ids.shrink_to_fit();
903        self.inheritable_property_ids.shrink_to_fit();
904        self.overridden_property_ids.shrink_to_fit();
905        self.appearing_method_ids.shrink_to_fit();
906        self.declaring_method_ids.shrink_to_fit();
907        self.inheritable_method_ids.shrink_to_fit();
908        self.overridden_method_ids.shrink_to_fit();
909        self.attributes.shrink_to_fit();
910        self.constants.shrink_to_fit();
911        self.enum_cases.shrink_to_fit();
912        self.type_aliases.shrink_to_fit();
913    }
914}
915
916/// Collects all method names reachable through the ancestors of `class_meta`.
917///
918/// Does not include methods defined directly on `class_meta` itself.
919#[must_use]
920pub fn collect_ancestor_methods(class_meta: &ClassLikeMetadata, class_likes: &WordMap<ClassLikeMetadata>) -> WordSet {
921    let mut visited = WordSet::default();
922    let mut methods = WordSet::default();
923    collect_ancestor_methods_inner(class_meta, class_likes, &mut visited, &mut methods);
924    methods
925}
926
927fn collect_ancestor_methods_inner(
928    class_meta: &ClassLikeMetadata,
929    class_likes: &WordMap<ClassLikeMetadata>,
930    visited: &mut WordSet,
931    methods: &mut WordSet,
932) {
933    if !visited.insert(class_meta.name) {
934        return;
935    }
936    if let Some(parent_name) = class_meta.direct_parent_class
937        && let Some(parent_meta) = class_likes.get(&parent_name)
938    {
939        methods.extend(parent_meta.methods.iter().copied());
940        collect_ancestor_methods_inner(parent_meta, class_likes, visited, methods);
941    }
942    for interface_name in &class_meta.direct_parent_interfaces {
943        if let Some(interface_meta) = class_likes.get(interface_name) {
944            methods.extend(interface_meta.methods.iter().copied());
945            collect_ancestor_methods_inner(interface_meta, class_likes, visited, methods);
946        }
947    }
948    for trait_name in &class_meta.used_traits {
949        if let Some(trait_meta) = class_likes.get(trait_name) {
950            methods.extend(trait_meta.methods.iter().copied());
951            collect_ancestor_methods_inner(trait_meta, class_likes, visited, methods);
952        }
953    }
954}
955
956#[cfg(test)]
957mod tests {
958    use std::iter::once;
959
960    use mago_span::Span;
961    use mago_word::WordSet;
962    use mago_word::word;
963
964    use crate::identifier::method::MethodIdentifier;
965    use crate::issue::ScanningIssueKind;
966    use crate::metadata::class_like_constant::ClassLikeConstantMetadata;
967    use crate::metadata::enum_case::EnumCaseMetadata;
968    use crate::metadata::flags::MetadataFlags;
969    use crate::metadata::property::PropertyMetadata;
970    use crate::misc::GenericParent;
971    use crate::misc::VariableIdentifier;
972    use crate::symbol::SymbolKind;
973    use crate::ttype;
974    use crate::ttype::template::GenericTemplate;
975    use crate::ttype::template::variance::Variance;
976    use crate::visibility::Visibility;
977
978    use super::ClassLikeMetadata;
979
980    fn has_code(issues: &[mago_reporting::Issue], kind: ScanningIssueKind) -> bool {
981        let code = kind.to_string();
982        issues.iter().any(|i| i.code.as_deref() == Some(code.as_str()))
983    }
984
985    fn make(name: &str) -> ClassLikeMetadata {
986        let a = word(name);
987        ClassLikeMetadata::new(a, a, Span::dummy(0, 10), None, MetadataFlags::empty())
988    }
989
990    #[test]
991    fn apply_patch_adds_override_for_inherited_real_method() {
992        let class_name = word("VendorClass");
993        let mut vendored = make("VendorClass");
994        let method_existing = word("existing");
995        vendored.methods.insert(method_existing);
996        vendored.declaring_method_ids.insert(method_existing, MethodIdentifier::new(class_name, method_existing));
997
998        let mut patch = make("VendorClass");
999        let method_override = word("inherited_method");
1000        patch.methods.insert(method_override);
1001        patch.declaring_method_ids.insert(method_override, MethodIdentifier::new(class_name, method_override));
1002        patch.appearing_method_ids.insert(method_override, MethodIdentifier::new(class_name, method_override));
1003        patch.inheritable_method_ids.insert(method_override, MethodIdentifier::new(class_name, method_override));
1004
1005        let inherited: WordSet = once(method_override).collect();
1006        vendored.apply_patch(&mut patch, &inherited);
1007        let issues = patch.issues;
1008
1009        // The override is added to the real method set and to all three method-id maps, mirroring
1010        // what the scanner records for a directly-declared method. Seeding declaring/appearing is
1011        // what makes the populator keep this class (not the ancestor) as the declaring site, so the
1012        // refined function-like materialized at `(self, method)` actually wins.
1013        assert!(vendored.methods.contains(&method_override));
1014        assert_eq!(
1015            vendored.declaring_method_ids.get(&method_override),
1016            Some(&MethodIdentifier::new(class_name, method_override)),
1017        );
1018        assert_eq!(
1019            vendored.appearing_method_ids.get(&method_override),
1020            Some(&MethodIdentifier::new(class_name, method_override)),
1021        );
1022        assert!(vendored.inheritable_method_ids.contains_key(&method_override));
1023
1024        // No warning: patch overrides of inherited methods are expected and intentional.
1025        assert!(issues.is_empty());
1026    }
1027
1028    #[test]
1029    fn apply_patch_adds_pseudo_methods() {
1030        let class_name = word("VendorClass");
1031        let mut vendored = make("VendorClass");
1032
1033        let mut patch = make("VendorClass");
1034        let pseudo = word("magicMethod");
1035        patch.pseudo_methods.insert(pseudo);
1036        patch.declaring_method_ids.insert(pseudo, MethodIdentifier::new(class_name, pseudo));
1037        patch.appearing_method_ids.insert(pseudo, MethodIdentifier::new(class_name, pseudo));
1038        patch.inheritable_method_ids.insert(pseudo, MethodIdentifier::new(class_name, pseudo));
1039
1040        vendored.apply_patch(&mut patch, &WordSet::default());
1041        let issues = patch.issues;
1042
1043        // Pseudo-method added to the right sets and ID maps.
1044        assert!(vendored.pseudo_methods.contains(&pseudo));
1045        assert!(vendored.declaring_method_ids.contains_key(&pseudo));
1046        assert!(vendored.appearing_method_ids.contains_key(&pseudo));
1047        assert!(vendored.inheritable_method_ids.contains_key(&pseudo));
1048
1049        // Must not appear as a real method.
1050        assert!(!vendored.methods.contains(&pseudo));
1051
1052        // No issues.
1053        assert!(issues.is_empty());
1054    }
1055
1056    #[test]
1057    fn apply_patch_accepts_new_magic_property() {
1058        let mut vendored = make("VendorClass");
1059
1060        let mut patch = make("VendorClass");
1061        let prop_magic = word("$magic");
1062        patch.properties.insert(
1063            prop_magic,
1064            PropertyMetadata::new(VariableIdentifier(prop_magic), MetadataFlags::PATCH | MetadataFlags::MAGIC_PROPERTY),
1065        );
1066
1067        vendored.apply_patch(&mut patch, &WordSet::default());
1068        let issues = patch.issues;
1069
1070        assert!(vendored.properties.contains_key(&prop_magic));
1071        assert!(issues.is_empty());
1072    }
1073
1074    #[test]
1075    fn apply_patch_does_not_touch_initialized_or_override_maps() {
1076        let mut vendored = make("VendorClass");
1077        let prop = word("$x");
1078        vendored.properties.insert(prop, PropertyMetadata::new(VariableIdentifier(prop), MetadataFlags::empty()));
1079        vendored.initialized_properties.insert(prop);
1080        vendored.overridden_property_ids.insert(prop, once(word("ParentClass")).collect());
1081
1082        let mut patch = make("VendorClass");
1083        patch.properties.insert(prop, PropertyMetadata::new(VariableIdentifier(prop), MetadataFlags::PATCH));
1084        // patch has no initialized_properties entry and no overridden_property_ids
1085
1086        vendored.apply_patch(&mut patch, &WordSet::default());
1087        let issues = patch.issues;
1088
1089        // initialized_properties must not be cleared by the patch
1090        assert!(vendored.initialized_properties.contains(&prop));
1091        // overridden_property_ids must not be cleared by the patch
1092        assert!(vendored.overridden_property_ids.contains_key(&prop));
1093        assert!(issues.is_empty());
1094    }
1095
1096    #[test]
1097    fn apply_patch_adds_template_types() {
1098        let class_name = word("VendorClass");
1099        let mut vendored = make("VendorClass");
1100
1101        let mut patch = make("VendorClass");
1102        let t = word("T");
1103        patch.template_types.insert(t, GenericTemplate::new(GenericParent::ClassLike(class_name), ttype::get_mixed()));
1104        patch.template_variance.push(Variance::Covariant);
1105        patch.template_readonly.insert(t);
1106
1107        vendored.apply_patch(&mut patch, &WordSet::default());
1108        let issues = patch.issues;
1109
1110        assert!(vendored.template_types.contains_key(&t));
1111        assert_eq!(vendored.template_variance.first().copied(), Some(Variance::Covariant));
1112        assert!(vendored.template_readonly.contains(&t));
1113        assert!(issues.is_empty());
1114    }
1115
1116    #[test]
1117    fn apply_patch_refines_existing_template_and_appends_new() {
1118        let class_name = word("VendorClass");
1119        let mut vendored = make("VendorClass");
1120        let t = word("T");
1121        let u = word("U");
1122
1123        // Original has T (invariant, constraint = mixed) and U (covariant).
1124        vendored
1125            .template_types
1126            .insert(t, GenericTemplate::new(GenericParent::ClassLike(class_name), ttype::get_mixed()));
1127        vendored
1128            .template_types
1129            .insert(u, GenericTemplate::new(GenericParent::ClassLike(class_name), ttype::get_mixed()));
1130        vendored.template_variance = vec![Variance::Invariant, Variance::Covariant];
1131
1132        // Patch refines T (now contravariant) and adds V (invariant).
1133        let mut patch = make("VendorClass");
1134        let v = word("V");
1135        patch.template_types.insert(t, GenericTemplate::new(GenericParent::ClassLike(class_name), ttype::get_int()));
1136        patch.template_types.insert(v, GenericTemplate::new(GenericParent::ClassLike(class_name), ttype::get_string()));
1137        patch.template_variance = vec![Variance::Contravariant, Variance::Invariant];
1138
1139        vendored.apply_patch(&mut patch, &WordSet::default());
1140        let issues = patch.issues;
1141
1142        // T refined, U preserved, V appended → order T=0, U=1, V=2
1143        assert_eq!(vendored.template_types.keys().copied().collect::<Vec<_>>(), [t, u, v]);
1144        assert_eq!(vendored.template_variance, [Variance::Contravariant, Variance::Covariant, Variance::Invariant]);
1145
1146        assert!(issues.is_empty());
1147    }
1148
1149    #[test]
1150    fn apply_patch_preserves_original_only_readonly_template() {
1151        // Original: T is readonly. Patch adds U but does not re-declare T.
1152        // T must remain readonly after the patch — the patch can add readonly
1153        // entries but must not strip existing ones by omitting them.
1154        let class_name = word("VendorClass");
1155        let mut vendored = make("VendorClass");
1156        let t = word("T");
1157        vendored
1158            .template_types
1159            .insert(t, GenericTemplate::new(GenericParent::ClassLike(class_name), ttype::get_mixed()));
1160        vendored.template_variance.push(Variance::Invariant);
1161        vendored.template_readonly.insert(t);
1162
1163        let mut patch = make("VendorClass");
1164        let u = word("U");
1165        patch.template_types.insert(u, GenericTemplate::new(GenericParent::ClassLike(class_name), ttype::get_mixed()));
1166        patch.template_variance.push(Variance::Invariant);
1167
1168        vendored.apply_patch(&mut patch, &WordSet::default());
1169        let issues = patch.issues;
1170
1171        assert!(vendored.template_readonly.contains(&t), "T should remain readonly after patch");
1172        assert!(!vendored.template_readonly.contains(&u), "U was not declared readonly by the patch");
1173        assert!(issues.is_empty());
1174    }
1175
1176    #[test]
1177    fn apply_patch_rejects_kind_mismatch() {
1178        let mut vendored = make("VendorClass");
1179        let mut patch = make("VendorClass");
1180        patch.kind = SymbolKind::Interface;
1181
1182        vendored.apply_patch(&mut patch, &WordSet::default());
1183        let issues = patch.issues;
1184
1185        assert!(has_code(&issues, ScanningIssueKind::PatchKindMismatch));
1186    }
1187
1188    #[test]
1189    fn apply_patch_rejects_trait_use() {
1190        let mut vendored = make("VendorClass");
1191        let mut patch = make("VendorClass");
1192        patch.used_traits.insert(word("SomeTrait"));
1193
1194        vendored.apply_patch(&mut patch, &WordSet::default());
1195        let issues = patch.issues;
1196
1197        assert!(has_code(&issues, ScanningIssueKind::PatchDeclaresTrait));
1198    }
1199
1200    #[test]
1201    fn apply_patch_rejects_hierarchy_mismatch() {
1202        let mut vendored = make("VendorClass");
1203        vendored.direct_parent_class = Some(word("ActualParent"));
1204
1205        let mut patch = make("VendorClass");
1206        patch.direct_parent_class = Some(word("WrongParent"));
1207
1208        vendored.apply_patch(&mut patch, &WordSet::default());
1209        let issues = patch.issues;
1210
1211        assert!(has_code(&issues, ScanningIssueKind::PatchHierarchyMismatch));
1212    }
1213
1214    #[test]
1215    fn apply_patch_rejects_new_method_not_in_ancestors() {
1216        let mut vendored = make("VendorClass");
1217        let mut patch = make("VendorClass");
1218        patch.methods.insert(word("newMethod"));
1219
1220        vendored.apply_patch(&mut patch, &WordSet::default());
1221        let issues = patch.issues;
1222
1223        assert!(has_code(&issues, ScanningIssueKind::PatchIntroducesNewMethod));
1224    }
1225
1226    #[test]
1227    fn apply_patch_rejects_new_property() {
1228        let mut vendored = make("VendorClass");
1229        let mut patch = make("VendorClass");
1230        let prop = word("$newProp");
1231        patch.properties.insert(prop, PropertyMetadata::new(VariableIdentifier(prop), MetadataFlags::PATCH));
1232
1233        vendored.apply_patch(&mut patch, &WordSet::default());
1234        let issues = patch.issues;
1235
1236        assert!(has_code(&issues, ScanningIssueKind::PatchIntroducesNewProperty));
1237    }
1238
1239    #[test]
1240    fn apply_patch_rejects_new_constant() {
1241        let mut vendored = make("VendorClass");
1242        let mut patch = make("VendorClass");
1243        let c = word("NEW_CONST");
1244        patch
1245            .constants
1246            .insert(c, ClassLikeConstantMetadata::new(c, Span::dummy(0, 5), Visibility::Public, MetadataFlags::PATCH));
1247
1248        vendored.apply_patch(&mut patch, &WordSet::default());
1249        let issues = patch.issues;
1250
1251        assert!(has_code(&issues, ScanningIssueKind::PatchIntroducesNewConstant));
1252    }
1253
1254    #[test]
1255    fn apply_patch_rejects_enum_cases() {
1256        let mut vendored = make("VendorClass");
1257        let mut patch = make("VendorClass");
1258        let case = word("CaseA");
1259        patch
1260            .enum_cases
1261            .insert(case, EnumCaseMetadata::new(case, Span::dummy(0, 3), Span::dummy(0, 5), MetadataFlags::PATCH));
1262
1263        vendored.apply_patch(&mut patch, &WordSet::default());
1264        let issues = patch.issues;
1265
1266        assert!(has_code(&issues, ScanningIssueKind::PatchEnumCasesIgnored));
1267    }
1268
1269    #[test]
1270    fn apply_patch_rejects_property_structural_mismatch() {
1271        let mut vendored = make("VendorClass");
1272        let prop = word("$x");
1273        vendored.properties.insert(prop, PropertyMetadata::new(VariableIdentifier(prop), MetadataFlags::empty()));
1274
1275        let mut patch = make("VendorClass");
1276        patch.properties.insert(
1277            prop,
1278            PropertyMetadata::new(VariableIdentifier(prop), MetadataFlags::PATCH | MetadataFlags::STATIC),
1279        );
1280
1281        vendored.apply_patch(&mut patch, &WordSet::default());
1282        let issues = patch.issues;
1283
1284        assert!(has_code(&issues, ScanningIssueKind::PatchPropertyStructuralMismatch));
1285    }
1286
1287    #[test]
1288    fn apply_patch_rejects_constant_structural_mismatch() {
1289        let mut vendored = make("VendorClass");
1290        let c = word("MY_CONST");
1291        vendored.constants.insert(
1292            c,
1293            ClassLikeConstantMetadata::new(c, Span::dummy(0, 5), Visibility::Private, MetadataFlags::empty()),
1294        );
1295
1296        let mut patch = make("VendorClass");
1297        patch
1298            .constants
1299            .insert(c, ClassLikeConstantMetadata::new(c, Span::dummy(0, 5), Visibility::Public, MetadataFlags::PATCH));
1300
1301        vendored.apply_patch(&mut patch, &WordSet::default());
1302        let issues = patch.issues;
1303
1304        assert!(has_code(&issues, ScanningIssueKind::PatchConstantStructuralMismatch));
1305    }
1306}