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
30pub type TemplateTypes = IndexMap<Word, GenericTemplate, RandomState>;
33
34#[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 magic_properties: WordMap<PropertyMetadata>,
83 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 pub imported_type_aliases: WordMap<(Word, Word, Span)>,
108 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 #[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 #[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 #[inline]
201 #[must_use]
202 pub fn get_trait_alias_map(&self) -> &WordMap<Word> {
203 &self.trait_alias_map
204 }
205
206 #[inline]
208 #[must_use]
209 pub fn get_template_type_names(&self) -> Vec<Word> {
210 self.template_types.keys().copied().collect()
211 }
212
213 #[inline]
215 #[must_use]
216 pub fn get_template_type(&self, name: Word) -> Option<&GenericTemplate> {
217 self.template_types.get(&name)
218 }
219
220 #[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 #[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 #[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 #[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 #[inline]
265 #[must_use]
266 pub fn get_property_names(&self) -> WordSet {
267 self.properties.keys().copied().collect()
268 }
269
270 #[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 #[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 #[inline]
286 pub fn take_issues(&mut self) -> Vec<Issue> {
287 std::mem::take(&mut self.issues)
288 }
289
290 #[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 #[inline]
299 pub fn add_all_parent_interface(&mut self, interface: Word) {
300 self.all_parent_interfaces.insert(interface);
301 }
302
303 #[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 #[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 #[inline]
317 pub fn add_used_trait(&mut self, trait_name: Word) -> bool {
318 self.used_traits.insert(trait_name)
319 }
320
321 #[inline]
323 pub fn add_used_traits(&mut self, traits: impl IntoIterator<Item = Word>) {
324 self.used_traits.extend(traits);
325 }
326
327 #[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 #[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 #[inline]
341 pub fn add_template_type(&mut self, name: Word, constraint: GenericTemplate) {
342 self.template_types.insert(name, constraint);
343 }
344
345 #[inline]
347 pub fn set_template_variance(&mut self, template_variance: Vec<Variance>) {
348 self.template_variance = template_variance;
349 }
350
351 #[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 #[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 #[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 #[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(¶meters) {
387 paths.push(parameters);
388 }
389 }
390
391 #[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 #[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 #[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 #[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 #[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 #[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; }
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; }
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; }
506
507 let Some(permitted_inheritors) = &other.permitted_inheritors else {
508 return true; };
510
511 if permitted_inheritors.contains(&self.name) {
512 return true; }
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 pub fn apply_patch(&mut self, patch: &mut ClassLikeMetadata, inherited_methods: &WordSet) {
534 debug_assert_eq!(self.name, patch.name, "patch class name must match the patched class");
537
538 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 self.type_aliases.extend(patch.type_aliases.iter().map(|(k, v)| (*k, v.clone())));
555 }
556
557 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 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 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 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 fn patch_templates(&mut self, patch: &ClassLikeMetadata) {
673 if patch.template_types.is_empty() {
674 return;
675 }
676
677 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 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 self.template_types.extend(patch.template_types.iter().map(|(k, v)| (*k, v.clone())));
696
697 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 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 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 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 let visibility_mismatch = prop_metadata.read_visibility != slot.read_visibility
786 || prop_metadata.write_visibility != slot.write_visibility;
787 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 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 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 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 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#[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 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 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 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 assert!(!vendored.methods.contains(&pseudo));
1078
1079 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 vendored.apply_patch(&mut patch, &WordSet::default());
1114 let issues = patch.issues;
1115
1116 assert!(vendored.initialized_properties.contains(&prop));
1118 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 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 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 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 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}