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.name == *required_extend {
465                continue;
466            }
467
468            if self.all_parent_classes.contains(required_extend) {
469                continue;
470            }
471
472            if self.kind.is_interface() && self.all_parent_interfaces.contains(required_extend) {
473                continue;
474            }
475
476            if (self.flags.is_abstract() || self.kind.is_trait()) && self.require_extends.contains(required_extend) {
477                continue; // Abstract classes and traits can require classes they extend
478            }
479
480            return Some(required_extend);
481        }
482
483        None
484    }
485
486    #[must_use]
487    pub fn is_permitted_to_inherit(&self, other: &ClassLikeMetadata) -> bool {
488        if self.kind.is_trait() || self.flags.is_abstract() {
489            return true; // Traits and abstract classes can always inherit
490        }
491
492        let Some(permitted_inheritors) = &other.permitted_inheritors else {
493            return true; // No restrictions, inheriting is allowed
494        };
495
496        if permitted_inheritors.contains(&self.name) {
497            return true; // This class-like is explicitly permitted to inherit
498        }
499
500        self.all_parent_interfaces.iter().any(|parent_interface| permitted_inheritors.contains(parent_interface))
501            || self.all_parent_classes.iter().any(|parent_class| permitted_inheritors.contains(parent_class))
502            || self.used_traits.iter().any(|used_trait| permitted_inheritors.contains(used_trait))
503    }
504
505    #[inline]
506    pub fn mark_as_populated(&mut self) {
507        self.flags |= MetadataFlags::POPULATED;
508        self.shrink_to_fit();
509    }
510
511    /// Applies a patch to this class in place, refining member type information while leaving
512    /// structure (hierarchy, override chains, initialization state, enum cases) untouched.
513    ///
514    /// At most one patch targets a given symbol, so there is no accumulation to worry about.
515    /// Diagnostics about the patch are appended to `patch.issues`, where scan-time diagnostics
516    /// also live, so they flow out through `take_issues`. The full set of rules a patch must
517    /// obey is documented in the `[source]` patching guide.
518    pub fn apply_patch(&mut self, patch: &mut ClassLikeMetadata, inherited_methods: &WordSet) {
519        // Patches redeclare the same class by name, so their member identifiers
520        // already carry the correct class name and merge cleanly.
521        debug_assert_eq!(self.name, patch.name, "patch class name must match the patched class");
522
523        // A kind or hierarchy mismatch means the patch targets a different symbol; reject
524        // it wholesale rather than refining members against the wrong class.
525        if self.report_kind_mismatch(patch) || self.report_hierarchy_mismatch(patch) {
526            return;
527        }
528
529        self.report_readonly_mismatch(patch);
530        self.report_trait_usage(patch);
531
532        self.patch_templates(patch);
533        self.patch_methods(patch, inherited_methods);
534        self.patch_properties(patch);
535        self.patch_constants(patch);
536        self.report_enum_cases(patch);
537
538        // Type aliases
539        self.type_aliases.extend(patch.type_aliases.iter().map(|(k, v)| (*k, v.clone())));
540    }
541
542    /// Reports a `kind` mismatch between the patch and the original symbol.
543    ///
544    /// Returns `true` when the kinds differ, in which case the patch must be rejected
545    /// wholesale because it targets a different kind of symbol.
546    fn report_kind_mismatch(&self, patch: &mut ClassLikeMetadata) -> bool {
547        if self.kind == patch.kind {
548            return false;
549        }
550
551        patch.issues.push(
552            Issue::error(format!(
553                "Patch declares `{}` as a {} but the original symbol is a {}; patch members are ignored.",
554                patch.original_name,
555                patch.kind.as_str(),
556                self.kind.as_str(),
557            ))
558            .with_code(ScanningIssueKind::PatchKindMismatch)
559            .with_annotation(Annotation::primary(patch.span))
560            .with_help(format!(
561                "Declare `{}` as a {} in the patch so it matches the original symbol, \
562                 or remove the patch if it targets the wrong symbol.",
563                patch.original_name,
564                self.kind.as_str(),
565            )),
566        );
567
568        true
569    }
570
571    /// Reports a hierarchy mismatch between the patch and the original symbol.
572    ///
573    /// Hierarchy declarations must match the original exactly if declared; a mismatch means
574    /// the patch is describing a different class. Returns `true` when they differ, in which
575    /// case the patch must be rejected wholesale.
576    fn report_hierarchy_mismatch(&self, patch: &mut ClassLikeMetadata) -> bool {
577        let hierarchy_mismatch = (patch.direct_parent_class.is_some()
578            && patch.direct_parent_class != self.direct_parent_class)
579            || (!patch.direct_parent_interfaces.is_empty()
580                && patch.direct_parent_interfaces != self.direct_parent_interfaces)
581            || (!patch.require_extends.is_empty() && patch.require_extends != self.require_extends)
582            || (!patch.require_implements.is_empty() && patch.require_implements != self.require_implements);
583
584        if !hierarchy_mismatch {
585            return false;
586        }
587
588        patch.issues.push(
589            Issue::error(format!(
590                "Patch for `{}` declares hierarchy that does not match the original; patch members are ignored.",
591                patch.original_name,
592            ))
593            .with_code(ScanningIssueKind::PatchHierarchyMismatch)
594            .with_annotation(Annotation::primary(patch.span))
595            .with_help(
596                "Patches do not need to restate hierarchy. Drop the parent class, interface, \
597                 `@require-extends`, or `@require-implements` declarations from the patch, or \
598                 correct them to match the original exactly.",
599            ),
600        );
601
602        true
603    }
604
605    /// Reports a `readonly class` modifier mismatch.
606    ///
607    /// `readonly class` is a structural modifier — a patch cannot add or remove it. This is a
608    /// warning only and does not abort the patch.
609    fn report_readonly_mismatch(&self, patch: &mut ClassLikeMetadata) {
610        if patch.flags.contains(MetadataFlags::READONLY) == self.flags.contains(MetadataFlags::READONLY) {
611            return;
612        }
613
614        patch.issues.push(
615            Issue::warning(format!(
616                "Patch declares `{}` as a {} class but the original is {}; readonly modifier is ignored.",
617                patch.original_name,
618                if patch.flags.contains(MetadataFlags::READONLY) { "readonly" } else { "non-readonly" },
619                if self.flags.contains(MetadataFlags::READONLY) { "readonly" } else { "non-readonly" },
620            ))
621            .with_code(ScanningIssueKind::PatchReadonlyMismatch)
622            .with_annotation(Annotation::primary(patch.span))
623            .with_help(
624                "The `readonly` class modifier is structural and cannot be changed by a patch. \
625                 Match the original by adding or removing `readonly` on the patched class declaration.",
626            ),
627        );
628    }
629
630    /// Reports `use` trait declarations on the patch, which are never valid.
631    ///
632    /// Patches refine member type information only; trait usage declarations are ignored.
633    fn report_trait_usage(&self, patch: &mut ClassLikeMetadata) {
634        if patch.used_traits.is_empty() {
635            return;
636        }
637
638        patch.issues.push(
639            Issue::warning(format!(
640                "Patch for `{}` declares `use` traits; patches refine member type information only and trait usage declarations are ignored.",
641                patch.original_name,
642            ))
643            .with_code(ScanningIssueKind::PatchDeclaresTrait)
644            .with_annotation(Annotation::primary(patch.span))
645            .with_help(
646                "Remove the `use` trait statement from the patch. To refine the type information \
647                 of members the trait contributes, patch those members on the class directly.",
648            ),
649        );
650    }
651
652    /// Merges `@template` declarations from the patch: existing names are overridden, new
653    /// names appended.
654    ///
655    /// `template_variance` is keyed by position in the `template_types` IndexMap and must be
656    /// rebuilt after the merge; `template_readonly` is name-keyed and can be extended directly.
657    fn patch_templates(&mut self, patch: &ClassLikeMetadata) {
658        if patch.template_types.is_empty() {
659            return;
660        }
661
662        // Collect name → variance for the current (original) state.
663        let mut name_to_variance: WordMap<Variance> = self
664            .template_types
665            .keys()
666            .enumerate()
667            .map(|(i, name)| (*name, self.template_variance.get(i).copied().unwrap_or(Variance::Invariant)))
668            .collect();
669
670        // Patch overrides existing entries and contributes new ones.
671        name_to_variance.extend(
672            patch
673                .template_types
674                .keys()
675                .enumerate()
676                .map(|(i, name)| (*name, patch.template_variance.get(i).copied().unwrap_or(Variance::Invariant))),
677        );
678
679        // Extend the IndexMap: existing names get updated definitions, new ones are appended.
680        self.template_types.extend(patch.template_types.iter().map(|(k, v)| (*k, v.clone())));
681
682        // Rebuild position-indexed variance vec to match the merged IndexMap order.
683        // template_readonly is name-keyed so it doesn't need rebuilding — just extend it.
684        self.template_variance = self
685            .template_types
686            .keys()
687            .map(|name| name_to_variance.get(name).copied().unwrap_or(Variance::Invariant))
688            .collect();
689        self.template_readonly.extend(patch.template_readonly.iter().copied());
690    }
691
692    /// Applies method declarations from the patch.
693    ///
694    /// A real method already declared on the original needs no change here — its type info
695    /// flows through `FunctionLikeMetadata::apply_patch` on the function-like. A method
696    /// inherited from an ancestor is added to the structural maps so the populator treats this
697    /// class as the declaring site, and `apply_patches_pass` then materializes the
698    /// function-like. A method that exists nowhere in the chain is rejected as a new member.
699    /// Pseudo-methods (`@method`) are class-level and always update the structural maps.
700    fn patch_methods(&mut self, patch: &mut ClassLikeMetadata, inherited_methods: &WordSet) {
701        for method_name in &patch.methods {
702            if patch.pseudo_methods.contains(method_name) || patch.static_pseudo_methods.contains(method_name) {
703                continue;
704            }
705            if inherited_methods.contains(method_name) {
706                // The patch introduces an override for an inherited method. Mirror what the
707                // scanner records for a directly-declared method so the populator preserves
708                // this class as the declaring/appearing site (rather than re-inheriting the
709                // ancestor's ids) and the refined function-like at `(self, method)` wins.
710                // `apply_patches_pass` materializes that function-like from the ancestor.
711                if self.methods.insert(*method_name) {
712                    if let Some(id) = patch.declaring_method_ids.get(method_name) {
713                        self.declaring_method_ids.insert(*method_name, *id);
714                    }
715                    if let Some(id) = patch.appearing_method_ids.get(method_name) {
716                        self.appearing_method_ids.insert(*method_name, *id);
717                    }
718                    if let Some(id) = patch.inheritable_method_ids.get(method_name) {
719                        self.inheritable_method_ids.insert(*method_name, *id);
720                    }
721                }
722            } else if !self.methods.contains(method_name) {
723                patch.issues.push(
724                    Issue::error(format!(
725                        "Patch for `{}` declares method `{}` which does not exist in the original \
726                         or any of its ancestors; patches cannot introduce new methods.",
727                        patch.original_name, method_name,
728                    ))
729                    .with_code(ScanningIssueKind::PatchIntroducesNewMethod)
730                    .with_annotation(Annotation::primary(patch.span))
731                    .with_help(format!(
732                        "Remove `{method_name}` from the patch, or correct its name to match a method \
733                         that already exists on `{}` or one of its ancestors. To annotate a magic \
734                         method handled by `__call`/`__callStatic`, declare it with `@method` instead. \
735                         Patches can only refine the types of existing methods.",
736                        patch.original_name,
737                    )),
738                );
739            }
740        }
741
742        for name in patch.pseudo_methods.iter().chain(patch.static_pseudo_methods.iter()) {
743            if let Some(id) = patch.declaring_method_ids.get(name) {
744                self.declaring_method_ids.insert(*name, *id);
745            }
746            if let Some(id) = patch.appearing_method_ids.get(name) {
747                self.appearing_method_ids.insert(*name, *id);
748            }
749            if let Some(id) = patch.inheritable_method_ids.get(name) {
750                self.inheritable_method_ids.insert(*name, *id);
751            }
752        }
753
754        self.pseudo_methods.extend(patch.pseudo_methods.iter().copied());
755        self.static_pseudo_methods.extend(patch.static_pseudo_methods.iter().copied());
756    }
757
758    /// Refines type annotations on existing properties. New properties are rejected unless
759    /// they are magic properties (`@property`/`@property-read`/`@property-write`), which carry
760    /// no runtime existence claim. Structural attributes must match the original.
761    fn patch_properties(&mut self, patch: &mut ClassLikeMetadata) {
762        let patch_span = patch.span;
763        let patch_name = patch.original_name;
764        for (name, prop_metadata) in &patch.properties {
765            if let Some(slot) = self.properties.get_mut(name) {
766                // Patches can only refine type annotations. Structural attributes
767                // (visibility, modifiers, hooks) must match the original exactly;
768                // if they differ the patch is wrong and we should say so rather than
769                // silently discarding the mismatch.
770                let visibility_mismatch = prop_metadata.read_visibility != slot.read_visibility
771                    || prop_metadata.write_visibility != slot.write_visibility;
772                // READONLY, STATIC, ABSTRACT: any mismatch is structural.
773                // FINAL: only an error when removed (vendor has it, patch doesn't);
774                //        adding final via a patch is allowed.
775                let structural_flag_mismatch =
776                    [MetadataFlags::READONLY, MetadataFlags::STATIC, MetadataFlags::ABSTRACT]
777                        .iter()
778                        .any(|&f| prop_metadata.flags.contains(f) != slot.flags.contains(f))
779                        || (slot.flags.contains(MetadataFlags::FINAL)
780                            && !prop_metadata.flags.contains(MetadataFlags::FINAL));
781                let has_hooks = !prop_metadata.hooks.is_empty();
782
783                if visibility_mismatch || structural_flag_mismatch || has_hooks {
784                    patch.issues.push(
785                        Issue::error(format!(
786                            "Patch for `{}::{}` declares structural attributes (visibility, modifiers, \
787                             or hooks) that differ from the original; only type annotations are applied.",
788                            patch_name, name,
789                        ))
790                        .with_code(ScanningIssueKind::PatchPropertyStructuralMismatch)
791                        .with_annotation(Annotation::primary(prop_metadata.span.unwrap_or(patch_span)))
792                        .with_help(format!(
793                            "Declare `{patch_name}::{name}` with the same visibility and modifiers as \
794                             the original and drop any property hooks; a patch may only refine the \
795                             property's type.",
796                        )),
797                    );
798                }
799
800                slot.type_declaration_metadata.clone_from(&prop_metadata.type_declaration_metadata);
801                slot.type_metadata.clone_from(&prop_metadata.type_metadata);
802            } else if prop_metadata.flags.is_magic_property() {
803                self.add_property(*name, prop_metadata.clone());
804            } else {
805                patch.issues.push(
806                    Issue::error(format!(
807                        "Patch declares property `{}::{}` which does not exist in the original; \
808                         patches cannot introduce new properties.",
809                        patch_name, name,
810                    ))
811                    .with_code(ScanningIssueKind::PatchIntroducesNewProperty)
812                    .with_annotation(Annotation::primary(patch_span))
813                    .with_help(format!(
814                        "Remove `{patch_name}::{name}` from the patch, or correct its name to match an \
815                         existing property. To annotate a magic property handled by `__get`/`__set`, \
816                         declare it with `@property`, `@property-read`, or `@property-write` instead.",
817                    )),
818                );
819            }
820        }
821    }
822
823    /// Refines type annotations on existing constants; new constants are rejected and
824    /// structural attributes must match the original.
825    fn patch_constants(&mut self, patch: &mut ClassLikeMetadata) {
826        let patch_span = patch.span;
827        let patch_name = patch.original_name;
828        for (name, const_metadata) in &patch.constants {
829            if let Some(slot) = self.constants.get_mut(name) {
830                let visibility_mismatch = const_metadata.visibility != slot.visibility;
831                // ABSTRACT: any mismatch is structural.
832                // FINAL: only an error when removed (vendor has it, patch doesn't);
833                //        adding final via a patch is allowed.
834                let structural_flag_mismatch = const_metadata.flags.contains(MetadataFlags::ABSTRACT)
835                    != slot.flags.contains(MetadataFlags::ABSTRACT)
836                    || (slot.flags.contains(MetadataFlags::FINAL)
837                        && !const_metadata.flags.contains(MetadataFlags::FINAL));
838
839                if visibility_mismatch || structural_flag_mismatch {
840                    patch.issues.push(
841                        Issue::error(format!(
842                            "Patch for `{}::{}` declares structural attributes (visibility or modifiers) \
843                             that differ from the original; only type annotations are applied.",
844                            patch_name, name,
845                        ))
846                        .with_code(ScanningIssueKind::PatchConstantStructuralMismatch)
847                        .with_annotation(Annotation::primary(const_metadata.span))
848                        .with_help(format!(
849                            "Declare `{patch_name}::{name}` with the same visibility and modifiers as \
850                             the original; a patch may only refine the constant's type.",
851                        )),
852                    );
853                }
854
855                slot.type_declaration.clone_from(&const_metadata.type_declaration);
856                slot.type_metadata.clone_from(&const_metadata.type_metadata);
857            } else {
858                patch.issues.push(
859                    Issue::error(format!(
860                        "Patch declares constant `{}::{}` which does not exist in the original; \
861                         patches cannot introduce new constants.",
862                        patch_name, name,
863                    ))
864                    .with_code(ScanningIssueKind::PatchIntroducesNewConstant)
865                    .with_annotation(Annotation::primary(patch_span))
866                    .with_help(format!(
867                        "Remove `{patch_name}::{name}` from the patch, or correct its name to match a \
868                         constant that already exists on the original. Patches can only refine the \
869                         types of existing constants.",
870                    )),
871                );
872            }
873        }
874    }
875
876    /// Reports enum case declarations on the patch, which are never valid.
877    ///
878    /// Enum cases are structural (they define the valid runtime values of an enum)
879    /// and cannot be modified by a patch.
880    fn report_enum_cases(&self, patch: &mut ClassLikeMetadata) {
881        if patch.enum_cases.is_empty() {
882            return;
883        }
884
885        patch.issues.push(
886            Issue::error(format!(
887                "Patch for `{}` declares enum case(s); enum cases are structural and cannot be \
888                 refined — patch enum cases are ignored.",
889                patch.original_name,
890            ))
891            .with_code(ScanningIssueKind::PatchEnumCasesIgnored)
892            .with_annotation(Annotation::primary(patch.span))
893            .with_help(
894                "Remove the enum case declarations from the patch. Enum cases define the runtime \
895                 values of the enum and must stay in the original definition; a patch can only \
896                 refine the types of existing members.",
897            ),
898        );
899    }
900
901    #[inline]
902    pub fn shrink_to_fit(&mut self) {
903        self.properties.shrink_to_fit();
904        self.initialized_properties.shrink_to_fit();
905        self.appearing_property_ids.shrink_to_fit();
906        self.declaring_property_ids.shrink_to_fit();
907        self.inheritable_property_ids.shrink_to_fit();
908        self.overridden_property_ids.shrink_to_fit();
909        self.appearing_method_ids.shrink_to_fit();
910        self.declaring_method_ids.shrink_to_fit();
911        self.inheritable_method_ids.shrink_to_fit();
912        self.overridden_method_ids.shrink_to_fit();
913        self.attributes.shrink_to_fit();
914        self.constants.shrink_to_fit();
915        self.enum_cases.shrink_to_fit();
916        self.type_aliases.shrink_to_fit();
917    }
918}
919
920/// Collects all method names reachable through the ancestors of `class_meta`.
921///
922/// Does not include methods defined directly on `class_meta` itself.
923#[must_use]
924pub fn collect_ancestor_methods(class_meta: &ClassLikeMetadata, class_likes: &WordMap<ClassLikeMetadata>) -> WordSet {
925    let mut visited = WordSet::default();
926    let mut methods = WordSet::default();
927    collect_ancestor_methods_inner(class_meta, class_likes, &mut visited, &mut methods);
928    methods
929}
930
931fn collect_ancestor_methods_inner(
932    class_meta: &ClassLikeMetadata,
933    class_likes: &WordMap<ClassLikeMetadata>,
934    visited: &mut WordSet,
935    methods: &mut WordSet,
936) {
937    if !visited.insert(class_meta.name) {
938        return;
939    }
940    if let Some(parent_name) = class_meta.direct_parent_class
941        && let Some(parent_meta) = class_likes.get(&parent_name)
942    {
943        methods.extend(parent_meta.methods.iter().copied());
944        collect_ancestor_methods_inner(parent_meta, class_likes, visited, methods);
945    }
946    for interface_name in &class_meta.direct_parent_interfaces {
947        if let Some(interface_meta) = class_likes.get(interface_name) {
948            methods.extend(interface_meta.methods.iter().copied());
949            collect_ancestor_methods_inner(interface_meta, class_likes, visited, methods);
950        }
951    }
952    for trait_name in &class_meta.used_traits {
953        if let Some(trait_meta) = class_likes.get(trait_name) {
954            methods.extend(trait_meta.methods.iter().copied());
955            collect_ancestor_methods_inner(trait_meta, class_likes, visited, methods);
956        }
957    }
958}
959
960#[cfg(test)]
961mod tests {
962    use std::iter::once;
963
964    use mago_span::Span;
965    use mago_word::WordSet;
966    use mago_word::word;
967
968    use crate::identifier::method::MethodIdentifier;
969    use crate::issue::ScanningIssueKind;
970    use crate::metadata::class_like_constant::ClassLikeConstantMetadata;
971    use crate::metadata::enum_case::EnumCaseMetadata;
972    use crate::metadata::flags::MetadataFlags;
973    use crate::metadata::property::PropertyMetadata;
974    use crate::misc::GenericParent;
975    use crate::misc::VariableIdentifier;
976    use crate::symbol::SymbolKind;
977    use crate::ttype;
978    use crate::ttype::template::GenericTemplate;
979    use crate::ttype::template::variance::Variance;
980    use crate::visibility::Visibility;
981
982    use super::ClassLikeMetadata;
983
984    fn has_code(issues: &[mago_reporting::Issue], kind: ScanningIssueKind) -> bool {
985        let code = kind.to_string();
986        issues.iter().any(|i| i.code.as_deref() == Some(code.as_str()))
987    }
988
989    fn make(name: &str) -> ClassLikeMetadata {
990        let a = word(name);
991        ClassLikeMetadata::new(a, a, Span::dummy(0, 10), None, MetadataFlags::empty())
992    }
993
994    #[test]
995    fn apply_patch_adds_override_for_inherited_real_method() {
996        let class_name = word("VendorClass");
997        let mut vendored = make("VendorClass");
998        let method_existing = word("existing");
999        vendored.methods.insert(method_existing);
1000        vendored.declaring_method_ids.insert(method_existing, MethodIdentifier::new(class_name, method_existing));
1001
1002        let mut patch = make("VendorClass");
1003        let method_override = word("inherited_method");
1004        patch.methods.insert(method_override);
1005        patch.declaring_method_ids.insert(method_override, MethodIdentifier::new(class_name, method_override));
1006        patch.appearing_method_ids.insert(method_override, MethodIdentifier::new(class_name, method_override));
1007        patch.inheritable_method_ids.insert(method_override, MethodIdentifier::new(class_name, method_override));
1008
1009        let inherited: WordSet = once(method_override).collect();
1010        vendored.apply_patch(&mut patch, &inherited);
1011        let issues = patch.issues;
1012
1013        // The override is added to the real method set and to all three method-id maps, mirroring
1014        // what the scanner records for a directly-declared method. Seeding declaring/appearing is
1015        // what makes the populator keep this class (not the ancestor) as the declaring site, so the
1016        // refined function-like materialized at `(self, method)` actually wins.
1017        assert!(vendored.methods.contains(&method_override));
1018        assert_eq!(
1019            vendored.declaring_method_ids.get(&method_override),
1020            Some(&MethodIdentifier::new(class_name, method_override)),
1021        );
1022        assert_eq!(
1023            vendored.appearing_method_ids.get(&method_override),
1024            Some(&MethodIdentifier::new(class_name, method_override)),
1025        );
1026        assert!(vendored.inheritable_method_ids.contains_key(&method_override));
1027
1028        // No warning: patch overrides of inherited methods are expected and intentional.
1029        assert!(issues.is_empty());
1030    }
1031
1032    #[test]
1033    fn apply_patch_adds_pseudo_methods() {
1034        let class_name = word("VendorClass");
1035        let mut vendored = make("VendorClass");
1036
1037        let mut patch = make("VendorClass");
1038        let pseudo = word("magicMethod");
1039        patch.pseudo_methods.insert(pseudo);
1040        patch.declaring_method_ids.insert(pseudo, MethodIdentifier::new(class_name, pseudo));
1041        patch.appearing_method_ids.insert(pseudo, MethodIdentifier::new(class_name, pseudo));
1042        patch.inheritable_method_ids.insert(pseudo, MethodIdentifier::new(class_name, pseudo));
1043
1044        vendored.apply_patch(&mut patch, &WordSet::default());
1045        let issues = patch.issues;
1046
1047        // Pseudo-method added to the right sets and ID maps.
1048        assert!(vendored.pseudo_methods.contains(&pseudo));
1049        assert!(vendored.declaring_method_ids.contains_key(&pseudo));
1050        assert!(vendored.appearing_method_ids.contains_key(&pseudo));
1051        assert!(vendored.inheritable_method_ids.contains_key(&pseudo));
1052
1053        // Must not appear as a real method.
1054        assert!(!vendored.methods.contains(&pseudo));
1055
1056        // No issues.
1057        assert!(issues.is_empty());
1058    }
1059
1060    #[test]
1061    fn apply_patch_accepts_new_magic_property() {
1062        let mut vendored = make("VendorClass");
1063
1064        let mut patch = make("VendorClass");
1065        let prop_magic = word("$magic");
1066        patch.properties.insert(
1067            prop_magic,
1068            PropertyMetadata::new(VariableIdentifier(prop_magic), MetadataFlags::PATCH | MetadataFlags::MAGIC_PROPERTY),
1069        );
1070
1071        vendored.apply_patch(&mut patch, &WordSet::default());
1072        let issues = patch.issues;
1073
1074        assert!(vendored.properties.contains_key(&prop_magic));
1075        assert!(issues.is_empty());
1076    }
1077
1078    #[test]
1079    fn apply_patch_does_not_touch_initialized_or_override_maps() {
1080        let mut vendored = make("VendorClass");
1081        let prop = word("$x");
1082        vendored.properties.insert(prop, PropertyMetadata::new(VariableIdentifier(prop), MetadataFlags::empty()));
1083        vendored.initialized_properties.insert(prop);
1084        vendored.overridden_property_ids.insert(prop, once(word("ParentClass")).collect());
1085
1086        let mut patch = make("VendorClass");
1087        patch.properties.insert(prop, PropertyMetadata::new(VariableIdentifier(prop), MetadataFlags::PATCH));
1088        // patch has no initialized_properties entry and no overridden_property_ids
1089
1090        vendored.apply_patch(&mut patch, &WordSet::default());
1091        let issues = patch.issues;
1092
1093        // initialized_properties must not be cleared by the patch
1094        assert!(vendored.initialized_properties.contains(&prop));
1095        // overridden_property_ids must not be cleared by the patch
1096        assert!(vendored.overridden_property_ids.contains_key(&prop));
1097        assert!(issues.is_empty());
1098    }
1099
1100    #[test]
1101    fn apply_patch_adds_template_types() {
1102        let class_name = word("VendorClass");
1103        let mut vendored = make("VendorClass");
1104
1105        let mut patch = make("VendorClass");
1106        let t = word("T");
1107        patch.template_types.insert(t, GenericTemplate::new(GenericParent::ClassLike(class_name), ttype::get_mixed()));
1108        patch.template_variance.push(Variance::Covariant);
1109        patch.template_readonly.insert(t);
1110
1111        vendored.apply_patch(&mut patch, &WordSet::default());
1112        let issues = patch.issues;
1113
1114        assert!(vendored.template_types.contains_key(&t));
1115        assert_eq!(vendored.template_variance.first().copied(), Some(Variance::Covariant));
1116        assert!(vendored.template_readonly.contains(&t));
1117        assert!(issues.is_empty());
1118    }
1119
1120    #[test]
1121    fn apply_patch_refines_existing_template_and_appends_new() {
1122        let class_name = word("VendorClass");
1123        let mut vendored = make("VendorClass");
1124        let t = word("T");
1125        let u = word("U");
1126
1127        // Original has T (invariant, constraint = mixed) and U (covariant).
1128        vendored
1129            .template_types
1130            .insert(t, GenericTemplate::new(GenericParent::ClassLike(class_name), ttype::get_mixed()));
1131        vendored
1132            .template_types
1133            .insert(u, GenericTemplate::new(GenericParent::ClassLike(class_name), ttype::get_mixed()));
1134        vendored.template_variance = vec![Variance::Invariant, Variance::Covariant];
1135
1136        // Patch refines T (now contravariant) and adds V (invariant).
1137        let mut patch = make("VendorClass");
1138        let v = word("V");
1139        patch.template_types.insert(t, GenericTemplate::new(GenericParent::ClassLike(class_name), ttype::get_int()));
1140        patch.template_types.insert(v, GenericTemplate::new(GenericParent::ClassLike(class_name), ttype::get_string()));
1141        patch.template_variance = vec![Variance::Contravariant, Variance::Invariant];
1142
1143        vendored.apply_patch(&mut patch, &WordSet::default());
1144        let issues = patch.issues;
1145
1146        // T refined, U preserved, V appended → order T=0, U=1, V=2
1147        assert_eq!(vendored.template_types.keys().copied().collect::<Vec<_>>(), [t, u, v]);
1148        assert_eq!(vendored.template_variance, [Variance::Contravariant, Variance::Covariant, Variance::Invariant]);
1149
1150        assert!(issues.is_empty());
1151    }
1152
1153    #[test]
1154    fn apply_patch_preserves_original_only_readonly_template() {
1155        // Original: T is readonly. Patch adds U but does not re-declare T.
1156        // T must remain readonly after the patch — the patch can add readonly
1157        // entries but must not strip existing ones by omitting them.
1158        let class_name = word("VendorClass");
1159        let mut vendored = make("VendorClass");
1160        let t = word("T");
1161        vendored
1162            .template_types
1163            .insert(t, GenericTemplate::new(GenericParent::ClassLike(class_name), ttype::get_mixed()));
1164        vendored.template_variance.push(Variance::Invariant);
1165        vendored.template_readonly.insert(t);
1166
1167        let mut patch = make("VendorClass");
1168        let u = word("U");
1169        patch.template_types.insert(u, GenericTemplate::new(GenericParent::ClassLike(class_name), ttype::get_mixed()));
1170        patch.template_variance.push(Variance::Invariant);
1171
1172        vendored.apply_patch(&mut patch, &WordSet::default());
1173        let issues = patch.issues;
1174
1175        assert!(vendored.template_readonly.contains(&t), "T should remain readonly after patch");
1176        assert!(!vendored.template_readonly.contains(&u), "U was not declared readonly by the patch");
1177        assert!(issues.is_empty());
1178    }
1179
1180    #[test]
1181    fn apply_patch_rejects_kind_mismatch() {
1182        let mut vendored = make("VendorClass");
1183        let mut patch = make("VendorClass");
1184        patch.kind = SymbolKind::Interface;
1185
1186        vendored.apply_patch(&mut patch, &WordSet::default());
1187        let issues = patch.issues;
1188
1189        assert!(has_code(&issues, ScanningIssueKind::PatchKindMismatch));
1190    }
1191
1192    #[test]
1193    fn apply_patch_rejects_trait_use() {
1194        let mut vendored = make("VendorClass");
1195        let mut patch = make("VendorClass");
1196        patch.used_traits.insert(word("SomeTrait"));
1197
1198        vendored.apply_patch(&mut patch, &WordSet::default());
1199        let issues = patch.issues;
1200
1201        assert!(has_code(&issues, ScanningIssueKind::PatchDeclaresTrait));
1202    }
1203
1204    #[test]
1205    fn apply_patch_rejects_hierarchy_mismatch() {
1206        let mut vendored = make("VendorClass");
1207        vendored.direct_parent_class = Some(word("ActualParent"));
1208
1209        let mut patch = make("VendorClass");
1210        patch.direct_parent_class = Some(word("WrongParent"));
1211
1212        vendored.apply_patch(&mut patch, &WordSet::default());
1213        let issues = patch.issues;
1214
1215        assert!(has_code(&issues, ScanningIssueKind::PatchHierarchyMismatch));
1216    }
1217
1218    #[test]
1219    fn apply_patch_rejects_new_method_not_in_ancestors() {
1220        let mut vendored = make("VendorClass");
1221        let mut patch = make("VendorClass");
1222        patch.methods.insert(word("newMethod"));
1223
1224        vendored.apply_patch(&mut patch, &WordSet::default());
1225        let issues = patch.issues;
1226
1227        assert!(has_code(&issues, ScanningIssueKind::PatchIntroducesNewMethod));
1228    }
1229
1230    #[test]
1231    fn apply_patch_rejects_new_property() {
1232        let mut vendored = make("VendorClass");
1233        let mut patch = make("VendorClass");
1234        let prop = word("$newProp");
1235        patch.properties.insert(prop, PropertyMetadata::new(VariableIdentifier(prop), MetadataFlags::PATCH));
1236
1237        vendored.apply_patch(&mut patch, &WordSet::default());
1238        let issues = patch.issues;
1239
1240        assert!(has_code(&issues, ScanningIssueKind::PatchIntroducesNewProperty));
1241    }
1242
1243    #[test]
1244    fn apply_patch_rejects_new_constant() {
1245        let mut vendored = make("VendorClass");
1246        let mut patch = make("VendorClass");
1247        let c = word("NEW_CONST");
1248        patch
1249            .constants
1250            .insert(c, ClassLikeConstantMetadata::new(c, Span::dummy(0, 5), Visibility::Public, MetadataFlags::PATCH));
1251
1252        vendored.apply_patch(&mut patch, &WordSet::default());
1253        let issues = patch.issues;
1254
1255        assert!(has_code(&issues, ScanningIssueKind::PatchIntroducesNewConstant));
1256    }
1257
1258    #[test]
1259    fn apply_patch_rejects_enum_cases() {
1260        let mut vendored = make("VendorClass");
1261        let mut patch = make("VendorClass");
1262        let case = word("CaseA");
1263        patch
1264            .enum_cases
1265            .insert(case, EnumCaseMetadata::new(case, Span::dummy(0, 3), Span::dummy(0, 5), MetadataFlags::PATCH));
1266
1267        vendored.apply_patch(&mut patch, &WordSet::default());
1268        let issues = patch.issues;
1269
1270        assert!(has_code(&issues, ScanningIssueKind::PatchEnumCasesIgnored));
1271    }
1272
1273    #[test]
1274    fn apply_patch_rejects_property_structural_mismatch() {
1275        let mut vendored = make("VendorClass");
1276        let prop = word("$x");
1277        vendored.properties.insert(prop, PropertyMetadata::new(VariableIdentifier(prop), MetadataFlags::empty()));
1278
1279        let mut patch = make("VendorClass");
1280        patch.properties.insert(
1281            prop,
1282            PropertyMetadata::new(VariableIdentifier(prop), MetadataFlags::PATCH | MetadataFlags::STATIC),
1283        );
1284
1285        vendored.apply_patch(&mut patch, &WordSet::default());
1286        let issues = patch.issues;
1287
1288        assert!(has_code(&issues, ScanningIssueKind::PatchPropertyStructuralMismatch));
1289    }
1290
1291    #[test]
1292    fn apply_patch_rejects_constant_structural_mismatch() {
1293        let mut vendored = make("VendorClass");
1294        let c = word("MY_CONST");
1295        vendored.constants.insert(
1296            c,
1297            ClassLikeConstantMetadata::new(c, Span::dummy(0, 5), Visibility::Private, MetadataFlags::empty()),
1298        );
1299
1300        let mut patch = make("VendorClass");
1301        patch
1302            .constants
1303            .insert(c, ClassLikeConstantMetadata::new(c, Span::dummy(0, 5), Visibility::Public, MetadataFlags::PATCH));
1304
1305        vendored.apply_patch(&mut patch, &WordSet::default());
1306        let issues = patch.issues;
1307
1308        assert!(has_code(&issues, ScanningIssueKind::PatchConstantStructuralMismatch));
1309    }
1310}