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 appearing_property_ids: WordMap<Word>,
76 pub declaring_property_ids: WordMap<Word>,
77 pub inheritable_property_ids: WordMap<Word>,
78 pub overridden_property_ids: WordMap<WordSet>,
79 pub initialized_properties: WordSet,
80 pub constants: WordMap<ClassLikeConstantMetadata>,
81 pub trait_constant_ids: WordMap<Word>,
82 pub enum_cases: WordMap<EnumCaseMetadata>,
83 pub invalid_dependencies: WordSet,
84 pub attributes: Vec<AttributeMetadata>,
85 pub enum_type: Option<TAtomic>,
86 pub has_sealed_methods: Option<bool>,
87 pub has_sealed_properties: Option<bool>,
88 pub permitted_inheritors: Option<WordSet>,
89 pub issues: Vec<Issue>,
90 pub attribute_flags: Option<AttributeFlags>,
91 pub flags: MetadataFlags,
92 pub type_aliases: WordMap<TypeMetadata>,
93 pub imported_type_aliases: WordMap<(Word, Word, Span)>,
95 pub mixins: Vec<TUnion>,
98 pub version_constraint: VersionConstraint,
99}
100
101impl ClassLikeMetadata {
102 #[must_use]
103 pub fn new(
104 name: Word,
105 original_name: Word,
106 span: Span,
107 name_span: Option<Span>,
108 flags: MetadataFlags,
109 ) -> ClassLikeMetadata {
110 ClassLikeMetadata {
111 constants: WordMap::default(),
112 trait_constant_ids: WordMap::default(),
113 enum_cases: WordMap::default(),
114 flags,
115 kind: SymbolKind::Class,
116 direct_parent_interfaces: WordSet::default(),
117 all_parent_classes: WordSet::default(),
118 appearing_method_ids: WordMap::default(),
119 attributes: Vec::new(),
120 all_parent_interfaces: WordSet::default(),
121 declaring_method_ids: WordMap::default(),
122 appearing_property_ids: WordMap::default(),
123 declaring_property_ids: WordMap::default(),
124 direct_parent_class: None,
125 require_extends: WordSet::default(),
126 require_implements: WordSet::default(),
127 inheritable_method_ids: WordMap::default(),
128 enum_type: None,
129 inheritable_property_ids: WordMap::default(),
130 initialized_properties: WordSet::default(),
131 invalid_dependencies: WordSet::default(),
132 span,
133 name_span,
134 methods: WordSet::default(),
135 pseudo_methods: WordSet::default(),
136 static_pseudo_methods: WordSet::default(),
137 overridden_method_ids: WordMap::default(),
138 overridden_property_ids: WordMap::default(),
139 properties: WordMap::default(),
140 template_variance: Vec::new(),
141 template_type_extends_count: WordMap::default(),
142 template_extended_parameters: WordMap::default(),
143 template_extended_parameter_paths: WordMap::default(),
144 template_extended_offsets: WordMap::default(),
145 template_type_implements_count: WordMap::default(),
146 template_type_uses_count: WordMap::default(),
147 template_types: TemplateTypes::default(),
148 used_traits: WordSet::default(),
149 trait_alias_map: WordMap::default(),
150 trait_visibility_map: WordMap::default(),
151 trait_final_map: WordSet::default(),
152 name,
153 original_name,
154 child_class_likes: None,
155 template_readonly: WordSet::default(),
156 has_sealed_methods: None,
157 has_sealed_properties: None,
158 permitted_inheritors: None,
159 issues: vec![],
160 attribute_flags: None,
161 type_aliases: WordMap::default(),
162 imported_type_aliases: WordMap::default(),
163 mixins: Vec::default(),
164 version_constraint: VersionConstraint::unconstrained(),
165 }
166 }
167
168 #[inline]
171 #[must_use]
172 pub fn is_available_in_version(&self, version: PHPVersion) -> bool {
173 self.version_constraint.allows_version(version)
174 }
175
176 #[inline]
179 #[must_use]
180 pub fn is_available_in_version_range(&self, range: PHPVersionRange) -> bool {
181 self.version_constraint.allows_version_range(range)
182 }
183
184 #[inline]
186 #[must_use]
187 pub fn get_trait_alias_map(&self) -> &WordMap<Word> {
188 &self.trait_alias_map
189 }
190
191 #[inline]
193 #[must_use]
194 pub fn get_template_type_names(&self) -> Vec<Word> {
195 self.template_types.keys().copied().collect()
196 }
197
198 #[inline]
200 #[must_use]
201 pub fn get_template_type(&self, name: Word) -> Option<&GenericTemplate> {
202 self.template_types.get(&name)
203 }
204
205 #[inline]
207 #[must_use]
208 pub fn get_template_type_with_index(&self, name: Word) -> Option<(usize, &GenericTemplate)> {
209 self.template_types.get_full(&name).map(|(index, _, types)| (index, types))
210 }
211
212 #[must_use]
213 pub fn get_template_for_index(&self, index: usize) -> Option<(Word, &GenericTemplate)> {
214 self.template_types.get_index(index).map(|(name, types)| (*name, types))
215 }
216
217 #[must_use]
218 pub fn get_template_name_for_index(&self, index: usize) -> Option<Word> {
219 self.template_types.get_index(index).map(|(name, _)| *name)
220 }
221
222 #[must_use]
223 pub fn get_template_index_for_name(&self, name: Word) -> Option<usize> {
224 self.template_types.get_index_of(&name)
225 }
226
227 #[inline]
229 #[must_use]
230 pub fn has_parent(&self, parent: Word) -> bool {
231 self.all_parent_classes.contains(&parent) || self.all_parent_interfaces.contains(&parent)
232 }
233
234 #[inline]
236 #[must_use]
237 pub fn has_template_extended_parameter(&self, parent: Word) -> bool {
238 self.template_extended_parameters.contains_key(&parent)
239 }
240
241 #[inline]
243 #[must_use]
244 pub fn has_appearing_method(&self, method: Word) -> bool {
245 self.appearing_method_ids.contains_key(&method)
246 }
247
248 #[inline]
250 #[must_use]
251 pub fn get_property_names(&self) -> WordSet {
252 self.properties.keys().copied().collect()
253 }
254
255 #[inline]
257 #[must_use]
258 pub fn has_appearing_property(&self, name: Word) -> bool {
259 self.appearing_property_ids.contains_key(&name)
260 }
261
262 #[inline]
264 #[must_use]
265 pub fn has_declaring_property(&self, name: Word) -> bool {
266 self.declaring_property_ids.contains_key(&name)
267 }
268
269 #[inline]
271 pub fn take_issues(&mut self) -> Vec<Issue> {
272 std::mem::take(&mut self.issues)
273 }
274
275 #[inline]
277 pub fn add_direct_parent_interface(&mut self, interface: Word) {
278 self.direct_parent_interfaces.insert(interface);
279 self.all_parent_interfaces.insert(interface);
280 }
281
282 #[inline]
284 pub fn add_all_parent_interface(&mut self, interface: Word) {
285 self.all_parent_interfaces.insert(interface);
286 }
287
288 #[inline]
290 pub fn add_all_parent_interfaces(&mut self, interfaces: impl IntoIterator<Item = Word>) {
291 self.all_parent_interfaces.extend(interfaces);
292 }
293
294 #[inline]
296 pub fn add_all_parent_classes(&mut self, classes: impl IntoIterator<Item = Word>) {
297 self.all_parent_classes.extend(classes);
298 }
299
300 #[inline]
302 pub fn add_used_trait(&mut self, trait_name: Word) -> bool {
303 self.used_traits.insert(trait_name)
304 }
305
306 #[inline]
308 pub fn add_used_traits(&mut self, traits: impl IntoIterator<Item = Word>) {
309 self.used_traits.extend(traits);
310 }
311
312 #[inline]
314 pub fn add_trait_alias(&mut self, method: Word, alias: Word) -> Option<Word> {
315 self.trait_alias_map.insert(method, alias)
316 }
317
318 #[inline]
320 pub fn add_trait_visibility(&mut self, method: Word, visibility: Visibility) -> Option<Visibility> {
321 self.trait_visibility_map.insert(method, visibility)
322 }
323
324 #[inline]
326 pub fn add_template_type(&mut self, name: Word, constraint: GenericTemplate) {
327 self.template_types.insert(name, constraint);
328 }
329
330 #[inline]
332 pub fn set_template_variance(&mut self, template_variance: Vec<Variance>) {
333 self.template_variance = template_variance;
334 }
335
336 #[inline]
338 pub fn add_template_extended_offset(&mut self, name: Word, types: Vec<TUnion>) -> Option<Vec<TUnion>> {
339 self.template_extended_offsets.insert(name, types)
340 }
341
342 #[inline]
344 pub fn extend_template_extended_parameters(
345 &mut self,
346 template_extended_parameters: WordMap<IndexMap<Word, TUnion, RandomState>>,
347 ) {
348 self.template_extended_parameters.extend(template_extended_parameters);
349 }
350
351 #[inline]
353 pub fn add_template_extended_parameter(
354 &mut self,
355 parent_fqcn: Word,
356 parameter_name: Word,
357 parameter_type: TUnion,
358 ) -> Option<TUnion> {
359 self.template_extended_parameters.entry(parent_fqcn).or_default().insert(parameter_name, parameter_type)
360 }
361
362 #[inline]
365 pub fn record_template_extended_path(&mut self, ancestor: Word, parameters: IndexMap<Word, TUnion, RandomState>) {
366 if parameters.is_empty() {
367 return;
368 }
369
370 let paths = self.template_extended_parameter_paths.entry(ancestor).or_default();
371 if !paths.contains(¶meters) {
372 paths.push(parameters);
373 }
374 }
375
376 #[inline]
378 pub fn add_declaring_method_id(
379 &mut self,
380 method: Word,
381 declaring_method_id: MethodIdentifier,
382 ) -> Option<MethodIdentifier> {
383 self.add_appearing_method_id(method, declaring_method_id);
384 self.declaring_method_ids.insert(method, declaring_method_id)
385 }
386
387 #[inline]
389 pub fn add_appearing_method_id(
390 &mut self,
391 method: Word,
392 appearing_method_id: MethodIdentifier,
393 ) -> Option<MethodIdentifier> {
394 self.appearing_method_ids.insert(method, appearing_method_id)
395 }
396
397 #[inline]
399 pub fn add_overridden_method_parent(
400 &mut self,
401 method: Word,
402 parent_method_id: MethodIdentifier,
403 ) -> Option<MethodIdentifier> {
404 self.overridden_method_ids
405 .entry(method)
406 .or_default()
407 .insert(parent_method_id.get_class_name(), parent_method_id)
408 }
409
410 #[inline]
412 pub fn add_property(&mut self, name: Word, property_metadata: PropertyMetadata) -> Option<PropertyMetadata> {
413 let class_name = self.name;
414
415 self.add_declaring_property_id(name, class_name);
416 if property_metadata.flags.has_default() {
417 self.initialized_properties.insert(name);
418 }
419
420 if !property_metadata.is_final() {
421 self.inheritable_property_ids.insert(name, class_name);
422 }
423
424 self.properties.insert(name, property_metadata)
425 }
426
427 #[inline]
429 pub fn add_property_metadata(&mut self, property_metadata: PropertyMetadata) -> Option<PropertyMetadata> {
430 let name = property_metadata.get_name().0;
431
432 self.add_property(name, property_metadata)
433 }
434
435 #[inline]
437 pub fn add_declaring_property_id(&mut self, prop: Word, declaring_fqcn: Word) -> Option<Word> {
438 self.appearing_property_ids.insert(prop, declaring_fqcn);
439 self.declaring_property_ids.insert(prop, declaring_fqcn)
440 }
441
442 #[must_use]
443 pub fn get_missing_required_interface<'meta>(&self, other: &'meta ClassLikeMetadata) -> Option<&'meta Word> {
444 for required_interface in &other.require_implements {
445 if self.all_parent_interfaces.contains(required_interface) {
446 continue;
447 }
448
449 if (self.flags.is_abstract() || self.kind.is_trait())
450 && self.require_implements.contains(required_interface)
451 {
452 continue; }
454
455 return Some(required_interface);
456 }
457
458 None
459 }
460
461 #[must_use]
462 pub fn get_missing_required_extends<'meta>(&self, other: &'meta ClassLikeMetadata) -> Option<&'meta Word> {
463 for required_extend in &other.require_extends {
464 if self.name == *required_extend {
465 continue;
466 }
467
468 if self.all_parent_classes.contains(required_extend) {
469 continue;
470 }
471
472 if self.kind.is_interface() && self.all_parent_interfaces.contains(required_extend) {
473 continue;
474 }
475
476 if (self.flags.is_abstract() || self.kind.is_trait()) && self.require_extends.contains(required_extend) {
477 continue; }
479
480 return Some(required_extend);
481 }
482
483 None
484 }
485
486 #[must_use]
487 pub fn is_permitted_to_inherit(&self, other: &ClassLikeMetadata) -> bool {
488 if self.kind.is_trait() || self.flags.is_abstract() {
489 return true; }
491
492 let Some(permitted_inheritors) = &other.permitted_inheritors else {
493 return true; };
495
496 if permitted_inheritors.contains(&self.name) {
497 return true; }
499
500 self.all_parent_interfaces.iter().any(|parent_interface| permitted_inheritors.contains(parent_interface))
501 || self.all_parent_classes.iter().any(|parent_class| permitted_inheritors.contains(parent_class))
502 || self.used_traits.iter().any(|used_trait| permitted_inheritors.contains(used_trait))
503 }
504
505 #[inline]
506 pub fn mark_as_populated(&mut self) {
507 self.flags |= MetadataFlags::POPULATED;
508 self.shrink_to_fit();
509 }
510
511 pub fn apply_patch(&mut self, patch: &mut ClassLikeMetadata, inherited_methods: &WordSet) {
519 debug_assert_eq!(self.name, patch.name, "patch class name must match the patched class");
522
523 if self.report_kind_mismatch(patch) || self.report_hierarchy_mismatch(patch) {
526 return;
527 }
528
529 self.report_readonly_mismatch(patch);
530 self.report_trait_usage(patch);
531
532 self.patch_templates(patch);
533 self.patch_methods(patch, inherited_methods);
534 self.patch_properties(patch);
535 self.patch_constants(patch);
536 self.report_enum_cases(patch);
537
538 self.type_aliases.extend(patch.type_aliases.iter().map(|(k, v)| (*k, v.clone())));
540 }
541
542 fn report_kind_mismatch(&self, patch: &mut ClassLikeMetadata) -> bool {
547 if self.kind == patch.kind {
548 return false;
549 }
550
551 patch.issues.push(
552 Issue::error(format!(
553 "Patch declares `{}` as a {} but the original symbol is a {}; patch members are ignored.",
554 patch.original_name,
555 patch.kind.as_str(),
556 self.kind.as_str(),
557 ))
558 .with_code(ScanningIssueKind::PatchKindMismatch)
559 .with_annotation(Annotation::primary(patch.span))
560 .with_help(format!(
561 "Declare `{}` as a {} in the patch so it matches the original symbol, \
562 or remove the patch if it targets the wrong symbol.",
563 patch.original_name,
564 self.kind.as_str(),
565 )),
566 );
567
568 true
569 }
570
571 fn report_hierarchy_mismatch(&self, patch: &mut ClassLikeMetadata) -> bool {
577 let hierarchy_mismatch = (patch.direct_parent_class.is_some()
578 && patch.direct_parent_class != self.direct_parent_class)
579 || (!patch.direct_parent_interfaces.is_empty()
580 && patch.direct_parent_interfaces != self.direct_parent_interfaces)
581 || (!patch.require_extends.is_empty() && patch.require_extends != self.require_extends)
582 || (!patch.require_implements.is_empty() && patch.require_implements != self.require_implements);
583
584 if !hierarchy_mismatch {
585 return false;
586 }
587
588 patch.issues.push(
589 Issue::error(format!(
590 "Patch for `{}` declares hierarchy that does not match the original; patch members are ignored.",
591 patch.original_name,
592 ))
593 .with_code(ScanningIssueKind::PatchHierarchyMismatch)
594 .with_annotation(Annotation::primary(patch.span))
595 .with_help(
596 "Patches do not need to restate hierarchy. Drop the parent class, interface, \
597 `@require-extends`, or `@require-implements` declarations from the patch, or \
598 correct them to match the original exactly.",
599 ),
600 );
601
602 true
603 }
604
605 fn report_readonly_mismatch(&self, patch: &mut ClassLikeMetadata) {
610 if patch.flags.contains(MetadataFlags::READONLY) == self.flags.contains(MetadataFlags::READONLY) {
611 return;
612 }
613
614 patch.issues.push(
615 Issue::warning(format!(
616 "Patch declares `{}` as a {} class but the original is {}; readonly modifier is ignored.",
617 patch.original_name,
618 if patch.flags.contains(MetadataFlags::READONLY) { "readonly" } else { "non-readonly" },
619 if self.flags.contains(MetadataFlags::READONLY) { "readonly" } else { "non-readonly" },
620 ))
621 .with_code(ScanningIssueKind::PatchReadonlyMismatch)
622 .with_annotation(Annotation::primary(patch.span))
623 .with_help(
624 "The `readonly` class modifier is structural and cannot be changed by a patch. \
625 Match the original by adding or removing `readonly` on the patched class declaration.",
626 ),
627 );
628 }
629
630 fn report_trait_usage(&self, patch: &mut ClassLikeMetadata) {
634 if patch.used_traits.is_empty() {
635 return;
636 }
637
638 patch.issues.push(
639 Issue::warning(format!(
640 "Patch for `{}` declares `use` traits; patches refine member type information only and trait usage declarations are ignored.",
641 patch.original_name,
642 ))
643 .with_code(ScanningIssueKind::PatchDeclaresTrait)
644 .with_annotation(Annotation::primary(patch.span))
645 .with_help(
646 "Remove the `use` trait statement from the patch. To refine the type information \
647 of members the trait contributes, patch those members on the class directly.",
648 ),
649 );
650 }
651
652 fn patch_templates(&mut self, patch: &ClassLikeMetadata) {
658 if patch.template_types.is_empty() {
659 return;
660 }
661
662 let mut name_to_variance: WordMap<Variance> = self
664 .template_types
665 .keys()
666 .enumerate()
667 .map(|(i, name)| (*name, self.template_variance.get(i).copied().unwrap_or(Variance::Invariant)))
668 .collect();
669
670 name_to_variance.extend(
672 patch
673 .template_types
674 .keys()
675 .enumerate()
676 .map(|(i, name)| (*name, patch.template_variance.get(i).copied().unwrap_or(Variance::Invariant))),
677 );
678
679 self.template_types.extend(patch.template_types.iter().map(|(k, v)| (*k, v.clone())));
681
682 self.template_variance = self
685 .template_types
686 .keys()
687 .map(|name| name_to_variance.get(name).copied().unwrap_or(Variance::Invariant))
688 .collect();
689 self.template_readonly.extend(patch.template_readonly.iter().copied());
690 }
691
692 fn patch_methods(&mut self, patch: &mut ClassLikeMetadata, inherited_methods: &WordSet) {
701 for method_name in &patch.methods {
702 if patch.pseudo_methods.contains(method_name) || patch.static_pseudo_methods.contains(method_name) {
703 continue;
704 }
705 if inherited_methods.contains(method_name) {
706 if self.methods.insert(*method_name) {
712 if let Some(id) = patch.declaring_method_ids.get(method_name) {
713 self.declaring_method_ids.insert(*method_name, *id);
714 }
715 if let Some(id) = patch.appearing_method_ids.get(method_name) {
716 self.appearing_method_ids.insert(*method_name, *id);
717 }
718 if let Some(id) = patch.inheritable_method_ids.get(method_name) {
719 self.inheritable_method_ids.insert(*method_name, *id);
720 }
721 }
722 } else if !self.methods.contains(method_name) {
723 patch.issues.push(
724 Issue::error(format!(
725 "Patch for `{}` declares method `{}` which does not exist in the original \
726 or any of its ancestors; patches cannot introduce new methods.",
727 patch.original_name, method_name,
728 ))
729 .with_code(ScanningIssueKind::PatchIntroducesNewMethod)
730 .with_annotation(Annotation::primary(patch.span))
731 .with_help(format!(
732 "Remove `{method_name}` from the patch, or correct its name to match a method \
733 that already exists on `{}` or one of its ancestors. To annotate a magic \
734 method handled by `__call`/`__callStatic`, declare it with `@method` instead. \
735 Patches can only refine the types of existing methods.",
736 patch.original_name,
737 )),
738 );
739 }
740 }
741
742 for name in patch.pseudo_methods.iter().chain(patch.static_pseudo_methods.iter()) {
743 if let Some(id) = patch.declaring_method_ids.get(name) {
744 self.declaring_method_ids.insert(*name, *id);
745 }
746 if let Some(id) = patch.appearing_method_ids.get(name) {
747 self.appearing_method_ids.insert(*name, *id);
748 }
749 if let Some(id) = patch.inheritable_method_ids.get(name) {
750 self.inheritable_method_ids.insert(*name, *id);
751 }
752 }
753
754 self.pseudo_methods.extend(patch.pseudo_methods.iter().copied());
755 self.static_pseudo_methods.extend(patch.static_pseudo_methods.iter().copied());
756 }
757
758 fn patch_properties(&mut self, patch: &mut ClassLikeMetadata) {
762 let patch_span = patch.span;
763 let patch_name = patch.original_name;
764 for (name, prop_metadata) in &patch.properties {
765 if let Some(slot) = self.properties.get_mut(name) {
766 let visibility_mismatch = prop_metadata.read_visibility != slot.read_visibility
771 || prop_metadata.write_visibility != slot.write_visibility;
772 let structural_flag_mismatch =
776 [MetadataFlags::READONLY, MetadataFlags::STATIC, MetadataFlags::ABSTRACT]
777 .iter()
778 .any(|&f| prop_metadata.flags.contains(f) != slot.flags.contains(f))
779 || (slot.flags.contains(MetadataFlags::FINAL)
780 && !prop_metadata.flags.contains(MetadataFlags::FINAL));
781 let has_hooks = !prop_metadata.hooks.is_empty();
782
783 if visibility_mismatch || structural_flag_mismatch || has_hooks {
784 patch.issues.push(
785 Issue::error(format!(
786 "Patch for `{}::{}` declares structural attributes (visibility, modifiers, \
787 or hooks) that differ from the original; only type annotations are applied.",
788 patch_name, name,
789 ))
790 .with_code(ScanningIssueKind::PatchPropertyStructuralMismatch)
791 .with_annotation(Annotation::primary(prop_metadata.span.unwrap_or(patch_span)))
792 .with_help(format!(
793 "Declare `{patch_name}::{name}` with the same visibility and modifiers as \
794 the original and drop any property hooks; a patch may only refine the \
795 property's type.",
796 )),
797 );
798 }
799
800 slot.type_declaration_metadata.clone_from(&prop_metadata.type_declaration_metadata);
801 slot.type_metadata.clone_from(&prop_metadata.type_metadata);
802 } else if prop_metadata.flags.is_magic_property() {
803 self.add_property(*name, prop_metadata.clone());
804 } else {
805 patch.issues.push(
806 Issue::error(format!(
807 "Patch declares property `{}::{}` which does not exist in the original; \
808 patches cannot introduce new properties.",
809 patch_name, name,
810 ))
811 .with_code(ScanningIssueKind::PatchIntroducesNewProperty)
812 .with_annotation(Annotation::primary(patch_span))
813 .with_help(format!(
814 "Remove `{patch_name}::{name}` from the patch, or correct its name to match an \
815 existing property. To annotate a magic property handled by `__get`/`__set`, \
816 declare it with `@property`, `@property-read`, or `@property-write` instead.",
817 )),
818 );
819 }
820 }
821 }
822
823 fn patch_constants(&mut self, patch: &mut ClassLikeMetadata) {
826 let patch_span = patch.span;
827 let patch_name = patch.original_name;
828 for (name, const_metadata) in &patch.constants {
829 if let Some(slot) = self.constants.get_mut(name) {
830 let visibility_mismatch = const_metadata.visibility != slot.visibility;
831 let structural_flag_mismatch = const_metadata.flags.contains(MetadataFlags::ABSTRACT)
835 != slot.flags.contains(MetadataFlags::ABSTRACT)
836 || (slot.flags.contains(MetadataFlags::FINAL)
837 && !const_metadata.flags.contains(MetadataFlags::FINAL));
838
839 if visibility_mismatch || structural_flag_mismatch {
840 patch.issues.push(
841 Issue::error(format!(
842 "Patch for `{}::{}` declares structural attributes (visibility or modifiers) \
843 that differ from the original; only type annotations are applied.",
844 patch_name, name,
845 ))
846 .with_code(ScanningIssueKind::PatchConstantStructuralMismatch)
847 .with_annotation(Annotation::primary(const_metadata.span))
848 .with_help(format!(
849 "Declare `{patch_name}::{name}` with the same visibility and modifiers as \
850 the original; a patch may only refine the constant's type.",
851 )),
852 );
853 }
854
855 slot.type_declaration.clone_from(&const_metadata.type_declaration);
856 slot.type_metadata.clone_from(&const_metadata.type_metadata);
857 } else {
858 patch.issues.push(
859 Issue::error(format!(
860 "Patch declares constant `{}::{}` which does not exist in the original; \
861 patches cannot introduce new constants.",
862 patch_name, name,
863 ))
864 .with_code(ScanningIssueKind::PatchIntroducesNewConstant)
865 .with_annotation(Annotation::primary(patch_span))
866 .with_help(format!(
867 "Remove `{patch_name}::{name}` from the patch, or correct its name to match a \
868 constant that already exists on the original. Patches can only refine the \
869 types of existing constants.",
870 )),
871 );
872 }
873 }
874 }
875
876 fn report_enum_cases(&self, patch: &mut ClassLikeMetadata) {
881 if patch.enum_cases.is_empty() {
882 return;
883 }
884
885 patch.issues.push(
886 Issue::error(format!(
887 "Patch for `{}` declares enum case(s); enum cases are structural and cannot be \
888 refined — patch enum cases are ignored.",
889 patch.original_name,
890 ))
891 .with_code(ScanningIssueKind::PatchEnumCasesIgnored)
892 .with_annotation(Annotation::primary(patch.span))
893 .with_help(
894 "Remove the enum case declarations from the patch. Enum cases define the runtime \
895 values of the enum and must stay in the original definition; a patch can only \
896 refine the types of existing members.",
897 ),
898 );
899 }
900
901 #[inline]
902 pub fn shrink_to_fit(&mut self) {
903 self.properties.shrink_to_fit();
904 self.initialized_properties.shrink_to_fit();
905 self.appearing_property_ids.shrink_to_fit();
906 self.declaring_property_ids.shrink_to_fit();
907 self.inheritable_property_ids.shrink_to_fit();
908 self.overridden_property_ids.shrink_to_fit();
909 self.appearing_method_ids.shrink_to_fit();
910 self.declaring_method_ids.shrink_to_fit();
911 self.inheritable_method_ids.shrink_to_fit();
912 self.overridden_method_ids.shrink_to_fit();
913 self.attributes.shrink_to_fit();
914 self.constants.shrink_to_fit();
915 self.enum_cases.shrink_to_fit();
916 self.type_aliases.shrink_to_fit();
917 }
918}
919
920#[must_use]
924pub fn collect_ancestor_methods(class_meta: &ClassLikeMetadata, class_likes: &WordMap<ClassLikeMetadata>) -> WordSet {
925 let mut visited = WordSet::default();
926 let mut methods = WordSet::default();
927 collect_ancestor_methods_inner(class_meta, class_likes, &mut visited, &mut methods);
928 methods
929}
930
931fn collect_ancestor_methods_inner(
932 class_meta: &ClassLikeMetadata,
933 class_likes: &WordMap<ClassLikeMetadata>,
934 visited: &mut WordSet,
935 methods: &mut WordSet,
936) {
937 if !visited.insert(class_meta.name) {
938 return;
939 }
940 if let Some(parent_name) = class_meta.direct_parent_class
941 && let Some(parent_meta) = class_likes.get(&parent_name)
942 {
943 methods.extend(parent_meta.methods.iter().copied());
944 collect_ancestor_methods_inner(parent_meta, class_likes, visited, methods);
945 }
946 for interface_name in &class_meta.direct_parent_interfaces {
947 if let Some(interface_meta) = class_likes.get(interface_name) {
948 methods.extend(interface_meta.methods.iter().copied());
949 collect_ancestor_methods_inner(interface_meta, class_likes, visited, methods);
950 }
951 }
952 for trait_name in &class_meta.used_traits {
953 if let Some(trait_meta) = class_likes.get(trait_name) {
954 methods.extend(trait_meta.methods.iter().copied());
955 collect_ancestor_methods_inner(trait_meta, class_likes, visited, methods);
956 }
957 }
958}
959
960#[cfg(test)]
961mod tests {
962 use std::iter::once;
963
964 use mago_span::Span;
965 use mago_word::WordSet;
966 use mago_word::word;
967
968 use crate::identifier::method::MethodIdentifier;
969 use crate::issue::ScanningIssueKind;
970 use crate::metadata::class_like_constant::ClassLikeConstantMetadata;
971 use crate::metadata::enum_case::EnumCaseMetadata;
972 use crate::metadata::flags::MetadataFlags;
973 use crate::metadata::property::PropertyMetadata;
974 use crate::misc::GenericParent;
975 use crate::misc::VariableIdentifier;
976 use crate::symbol::SymbolKind;
977 use crate::ttype;
978 use crate::ttype::template::GenericTemplate;
979 use crate::ttype::template::variance::Variance;
980 use crate::visibility::Visibility;
981
982 use super::ClassLikeMetadata;
983
984 fn has_code(issues: &[mago_reporting::Issue], kind: ScanningIssueKind) -> bool {
985 let code = kind.to_string();
986 issues.iter().any(|i| i.code.as_deref() == Some(code.as_str()))
987 }
988
989 fn make(name: &str) -> ClassLikeMetadata {
990 let a = word(name);
991 ClassLikeMetadata::new(a, a, Span::dummy(0, 10), None, MetadataFlags::empty())
992 }
993
994 #[test]
995 fn apply_patch_adds_override_for_inherited_real_method() {
996 let class_name = word("VendorClass");
997 let mut vendored = make("VendorClass");
998 let method_existing = word("existing");
999 vendored.methods.insert(method_existing);
1000 vendored.declaring_method_ids.insert(method_existing, MethodIdentifier::new(class_name, method_existing));
1001
1002 let mut patch = make("VendorClass");
1003 let method_override = word("inherited_method");
1004 patch.methods.insert(method_override);
1005 patch.declaring_method_ids.insert(method_override, MethodIdentifier::new(class_name, method_override));
1006 patch.appearing_method_ids.insert(method_override, MethodIdentifier::new(class_name, method_override));
1007 patch.inheritable_method_ids.insert(method_override, MethodIdentifier::new(class_name, method_override));
1008
1009 let inherited: WordSet = once(method_override).collect();
1010 vendored.apply_patch(&mut patch, &inherited);
1011 let issues = patch.issues;
1012
1013 assert!(vendored.methods.contains(&method_override));
1018 assert_eq!(
1019 vendored.declaring_method_ids.get(&method_override),
1020 Some(&MethodIdentifier::new(class_name, method_override)),
1021 );
1022 assert_eq!(
1023 vendored.appearing_method_ids.get(&method_override),
1024 Some(&MethodIdentifier::new(class_name, method_override)),
1025 );
1026 assert!(vendored.inheritable_method_ids.contains_key(&method_override));
1027
1028 assert!(issues.is_empty());
1030 }
1031
1032 #[test]
1033 fn apply_patch_adds_pseudo_methods() {
1034 let class_name = word("VendorClass");
1035 let mut vendored = make("VendorClass");
1036
1037 let mut patch = make("VendorClass");
1038 let pseudo = word("magicMethod");
1039 patch.pseudo_methods.insert(pseudo);
1040 patch.declaring_method_ids.insert(pseudo, MethodIdentifier::new(class_name, pseudo));
1041 patch.appearing_method_ids.insert(pseudo, MethodIdentifier::new(class_name, pseudo));
1042 patch.inheritable_method_ids.insert(pseudo, MethodIdentifier::new(class_name, pseudo));
1043
1044 vendored.apply_patch(&mut patch, &WordSet::default());
1045 let issues = patch.issues;
1046
1047 assert!(vendored.pseudo_methods.contains(&pseudo));
1049 assert!(vendored.declaring_method_ids.contains_key(&pseudo));
1050 assert!(vendored.appearing_method_ids.contains_key(&pseudo));
1051 assert!(vendored.inheritable_method_ids.contains_key(&pseudo));
1052
1053 assert!(!vendored.methods.contains(&pseudo));
1055
1056 assert!(issues.is_empty());
1058 }
1059
1060 #[test]
1061 fn apply_patch_accepts_new_magic_property() {
1062 let mut vendored = make("VendorClass");
1063
1064 let mut patch = make("VendorClass");
1065 let prop_magic = word("$magic");
1066 patch.properties.insert(
1067 prop_magic,
1068 PropertyMetadata::new(VariableIdentifier(prop_magic), MetadataFlags::PATCH | MetadataFlags::MAGIC_PROPERTY),
1069 );
1070
1071 vendored.apply_patch(&mut patch, &WordSet::default());
1072 let issues = patch.issues;
1073
1074 assert!(vendored.properties.contains_key(&prop_magic));
1075 assert!(issues.is_empty());
1076 }
1077
1078 #[test]
1079 fn apply_patch_does_not_touch_initialized_or_override_maps() {
1080 let mut vendored = make("VendorClass");
1081 let prop = word("$x");
1082 vendored.properties.insert(prop, PropertyMetadata::new(VariableIdentifier(prop), MetadataFlags::empty()));
1083 vendored.initialized_properties.insert(prop);
1084 vendored.overridden_property_ids.insert(prop, once(word("ParentClass")).collect());
1085
1086 let mut patch = make("VendorClass");
1087 patch.properties.insert(prop, PropertyMetadata::new(VariableIdentifier(prop), MetadataFlags::PATCH));
1088 vendored.apply_patch(&mut patch, &WordSet::default());
1091 let issues = patch.issues;
1092
1093 assert!(vendored.initialized_properties.contains(&prop));
1095 assert!(vendored.overridden_property_ids.contains_key(&prop));
1097 assert!(issues.is_empty());
1098 }
1099
1100 #[test]
1101 fn apply_patch_adds_template_types() {
1102 let class_name = word("VendorClass");
1103 let mut vendored = make("VendorClass");
1104
1105 let mut patch = make("VendorClass");
1106 let t = word("T");
1107 patch.template_types.insert(t, GenericTemplate::new(GenericParent::ClassLike(class_name), ttype::get_mixed()));
1108 patch.template_variance.push(Variance::Covariant);
1109 patch.template_readonly.insert(t);
1110
1111 vendored.apply_patch(&mut patch, &WordSet::default());
1112 let issues = patch.issues;
1113
1114 assert!(vendored.template_types.contains_key(&t));
1115 assert_eq!(vendored.template_variance.first().copied(), Some(Variance::Covariant));
1116 assert!(vendored.template_readonly.contains(&t));
1117 assert!(issues.is_empty());
1118 }
1119
1120 #[test]
1121 fn apply_patch_refines_existing_template_and_appends_new() {
1122 let class_name = word("VendorClass");
1123 let mut vendored = make("VendorClass");
1124 let t = word("T");
1125 let u = word("U");
1126
1127 vendored
1129 .template_types
1130 .insert(t, GenericTemplate::new(GenericParent::ClassLike(class_name), ttype::get_mixed()));
1131 vendored
1132 .template_types
1133 .insert(u, GenericTemplate::new(GenericParent::ClassLike(class_name), ttype::get_mixed()));
1134 vendored.template_variance = vec![Variance::Invariant, Variance::Covariant];
1135
1136 let mut patch = make("VendorClass");
1138 let v = word("V");
1139 patch.template_types.insert(t, GenericTemplate::new(GenericParent::ClassLike(class_name), ttype::get_int()));
1140 patch.template_types.insert(v, GenericTemplate::new(GenericParent::ClassLike(class_name), ttype::get_string()));
1141 patch.template_variance = vec![Variance::Contravariant, Variance::Invariant];
1142
1143 vendored.apply_patch(&mut patch, &WordSet::default());
1144 let issues = patch.issues;
1145
1146 assert_eq!(vendored.template_types.keys().copied().collect::<Vec<_>>(), [t, u, v]);
1148 assert_eq!(vendored.template_variance, [Variance::Contravariant, Variance::Covariant, Variance::Invariant]);
1149
1150 assert!(issues.is_empty());
1151 }
1152
1153 #[test]
1154 fn apply_patch_preserves_original_only_readonly_template() {
1155 let class_name = word("VendorClass");
1159 let mut vendored = make("VendorClass");
1160 let t = word("T");
1161 vendored
1162 .template_types
1163 .insert(t, GenericTemplate::new(GenericParent::ClassLike(class_name), ttype::get_mixed()));
1164 vendored.template_variance.push(Variance::Invariant);
1165 vendored.template_readonly.insert(t);
1166
1167 let mut patch = make("VendorClass");
1168 let u = word("U");
1169 patch.template_types.insert(u, GenericTemplate::new(GenericParent::ClassLike(class_name), ttype::get_mixed()));
1170 patch.template_variance.push(Variance::Invariant);
1171
1172 vendored.apply_patch(&mut patch, &WordSet::default());
1173 let issues = patch.issues;
1174
1175 assert!(vendored.template_readonly.contains(&t), "T should remain readonly after patch");
1176 assert!(!vendored.template_readonly.contains(&u), "U was not declared readonly by the patch");
1177 assert!(issues.is_empty());
1178 }
1179
1180 #[test]
1181 fn apply_patch_rejects_kind_mismatch() {
1182 let mut vendored = make("VendorClass");
1183 let mut patch = make("VendorClass");
1184 patch.kind = SymbolKind::Interface;
1185
1186 vendored.apply_patch(&mut patch, &WordSet::default());
1187 let issues = patch.issues;
1188
1189 assert!(has_code(&issues, ScanningIssueKind::PatchKindMismatch));
1190 }
1191
1192 #[test]
1193 fn apply_patch_rejects_trait_use() {
1194 let mut vendored = make("VendorClass");
1195 let mut patch = make("VendorClass");
1196 patch.used_traits.insert(word("SomeTrait"));
1197
1198 vendored.apply_patch(&mut patch, &WordSet::default());
1199 let issues = patch.issues;
1200
1201 assert!(has_code(&issues, ScanningIssueKind::PatchDeclaresTrait));
1202 }
1203
1204 #[test]
1205 fn apply_patch_rejects_hierarchy_mismatch() {
1206 let mut vendored = make("VendorClass");
1207 vendored.direct_parent_class = Some(word("ActualParent"));
1208
1209 let mut patch = make("VendorClass");
1210 patch.direct_parent_class = Some(word("WrongParent"));
1211
1212 vendored.apply_patch(&mut patch, &WordSet::default());
1213 let issues = patch.issues;
1214
1215 assert!(has_code(&issues, ScanningIssueKind::PatchHierarchyMismatch));
1216 }
1217
1218 #[test]
1219 fn apply_patch_rejects_new_method_not_in_ancestors() {
1220 let mut vendored = make("VendorClass");
1221 let mut patch = make("VendorClass");
1222 patch.methods.insert(word("newMethod"));
1223
1224 vendored.apply_patch(&mut patch, &WordSet::default());
1225 let issues = patch.issues;
1226
1227 assert!(has_code(&issues, ScanningIssueKind::PatchIntroducesNewMethod));
1228 }
1229
1230 #[test]
1231 fn apply_patch_rejects_new_property() {
1232 let mut vendored = make("VendorClass");
1233 let mut patch = make("VendorClass");
1234 let prop = word("$newProp");
1235 patch.properties.insert(prop, PropertyMetadata::new(VariableIdentifier(prop), MetadataFlags::PATCH));
1236
1237 vendored.apply_patch(&mut patch, &WordSet::default());
1238 let issues = patch.issues;
1239
1240 assert!(has_code(&issues, ScanningIssueKind::PatchIntroducesNewProperty));
1241 }
1242
1243 #[test]
1244 fn apply_patch_rejects_new_constant() {
1245 let mut vendored = make("VendorClass");
1246 let mut patch = make("VendorClass");
1247 let c = word("NEW_CONST");
1248 patch
1249 .constants
1250 .insert(c, ClassLikeConstantMetadata::new(c, Span::dummy(0, 5), Visibility::Public, MetadataFlags::PATCH));
1251
1252 vendored.apply_patch(&mut patch, &WordSet::default());
1253 let issues = patch.issues;
1254
1255 assert!(has_code(&issues, ScanningIssueKind::PatchIntroducesNewConstant));
1256 }
1257
1258 #[test]
1259 fn apply_patch_rejects_enum_cases() {
1260 let mut vendored = make("VendorClass");
1261 let mut patch = make("VendorClass");
1262 let case = word("CaseA");
1263 patch
1264 .enum_cases
1265 .insert(case, EnumCaseMetadata::new(case, Span::dummy(0, 3), Span::dummy(0, 5), MetadataFlags::PATCH));
1266
1267 vendored.apply_patch(&mut patch, &WordSet::default());
1268 let issues = patch.issues;
1269
1270 assert!(has_code(&issues, ScanningIssueKind::PatchEnumCasesIgnored));
1271 }
1272
1273 #[test]
1274 fn apply_patch_rejects_property_structural_mismatch() {
1275 let mut vendored = make("VendorClass");
1276 let prop = word("$x");
1277 vendored.properties.insert(prop, PropertyMetadata::new(VariableIdentifier(prop), MetadataFlags::empty()));
1278
1279 let mut patch = make("VendorClass");
1280 patch.properties.insert(
1281 prop,
1282 PropertyMetadata::new(VariableIdentifier(prop), MetadataFlags::PATCH | MetadataFlags::STATIC),
1283 );
1284
1285 vendored.apply_patch(&mut patch, &WordSet::default());
1286 let issues = patch.issues;
1287
1288 assert!(has_code(&issues, ScanningIssueKind::PatchPropertyStructuralMismatch));
1289 }
1290
1291 #[test]
1292 fn apply_patch_rejects_constant_structural_mismatch() {
1293 let mut vendored = make("VendorClass");
1294 let c = word("MY_CONST");
1295 vendored.constants.insert(
1296 c,
1297 ClassLikeConstantMetadata::new(c, Span::dummy(0, 5), Visibility::Private, MetadataFlags::empty()),
1298 );
1299
1300 let mut patch = make("VendorClass");
1301 patch
1302 .constants
1303 .insert(c, ClassLikeConstantMetadata::new(c, Span::dummy(0, 5), Visibility::Public, MetadataFlags::PATCH));
1304
1305 vendored.apply_patch(&mut patch, &WordSet::default());
1306 let issues = patch.issues;
1307
1308 assert!(has_code(&issues, ScanningIssueKind::PatchConstantStructuralMismatch));
1309 }
1310}