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