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