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.all_parent_classes.contains(required_extend) {
465 continue;
466 }
467
468 if self.kind.is_interface() && self.all_parent_interfaces.contains(required_extend) {
469 continue;
470 }
471
472 if (self.flags.is_abstract() || self.kind.is_trait()) && self.require_extends.contains(required_extend) {
473 continue; }
475
476 return Some(required_extend);
477 }
478
479 None
480 }
481
482 #[must_use]
483 pub fn is_permitted_to_inherit(&self, other: &ClassLikeMetadata) -> bool {
484 if self.kind.is_trait() || self.flags.is_abstract() {
485 return true; }
487
488 let Some(permitted_inheritors) = &other.permitted_inheritors else {
489 return true; };
491
492 if permitted_inheritors.contains(&self.name) {
493 return true; }
495
496 self.all_parent_interfaces.iter().any(|parent_interface| permitted_inheritors.contains(parent_interface))
497 || self.all_parent_classes.iter().any(|parent_class| permitted_inheritors.contains(parent_class))
498 || self.used_traits.iter().any(|used_trait| permitted_inheritors.contains(used_trait))
499 }
500
501 #[inline]
502 pub fn mark_as_populated(&mut self) {
503 self.flags |= MetadataFlags::POPULATED;
504 self.shrink_to_fit();
505 }
506
507 pub fn apply_patch(&mut self, patch: &mut ClassLikeMetadata, inherited_methods: &WordSet) {
515 debug_assert_eq!(self.name, patch.name, "patch class name must match the patched class");
518
519 if self.report_kind_mismatch(patch) || self.report_hierarchy_mismatch(patch) {
522 return;
523 }
524
525 self.report_readonly_mismatch(patch);
526 self.report_trait_usage(patch);
527
528 self.patch_templates(patch);
529 self.patch_methods(patch, inherited_methods);
530 self.patch_properties(patch);
531 self.patch_constants(patch);
532 self.report_enum_cases(patch);
533
534 self.type_aliases.extend(patch.type_aliases.iter().map(|(k, v)| (*k, v.clone())));
536 }
537
538 fn report_kind_mismatch(&self, patch: &mut ClassLikeMetadata) -> bool {
543 if self.kind == patch.kind {
544 return false;
545 }
546
547 patch.issues.push(
548 Issue::error(format!(
549 "Patch declares `{}` as a {} but the original symbol is a {}; patch members are ignored.",
550 patch.original_name,
551 patch.kind.as_str(),
552 self.kind.as_str(),
553 ))
554 .with_code(ScanningIssueKind::PatchKindMismatch)
555 .with_annotation(Annotation::primary(patch.span))
556 .with_help(format!(
557 "Declare `{}` as a {} in the patch so it matches the original symbol, \
558 or remove the patch if it targets the wrong symbol.",
559 patch.original_name,
560 self.kind.as_str(),
561 )),
562 );
563
564 true
565 }
566
567 fn report_hierarchy_mismatch(&self, patch: &mut ClassLikeMetadata) -> bool {
573 let hierarchy_mismatch = (patch.direct_parent_class.is_some()
574 && patch.direct_parent_class != self.direct_parent_class)
575 || (!patch.direct_parent_interfaces.is_empty()
576 && patch.direct_parent_interfaces != self.direct_parent_interfaces)
577 || (!patch.require_extends.is_empty() && patch.require_extends != self.require_extends)
578 || (!patch.require_implements.is_empty() && patch.require_implements != self.require_implements);
579
580 if !hierarchy_mismatch {
581 return false;
582 }
583
584 patch.issues.push(
585 Issue::error(format!(
586 "Patch for `{}` declares hierarchy that does not match the original; patch members are ignored.",
587 patch.original_name,
588 ))
589 .with_code(ScanningIssueKind::PatchHierarchyMismatch)
590 .with_annotation(Annotation::primary(patch.span))
591 .with_help(
592 "Patches do not need to restate hierarchy. Drop the parent class, interface, \
593 `@require-extends`, or `@require-implements` declarations from the patch, or \
594 correct them to match the original exactly.",
595 ),
596 );
597
598 true
599 }
600
601 fn report_readonly_mismatch(&self, patch: &mut ClassLikeMetadata) {
606 if patch.flags.contains(MetadataFlags::READONLY) == self.flags.contains(MetadataFlags::READONLY) {
607 return;
608 }
609
610 patch.issues.push(
611 Issue::warning(format!(
612 "Patch declares `{}` as a {} class but the original is {}; readonly modifier is ignored.",
613 patch.original_name,
614 if patch.flags.contains(MetadataFlags::READONLY) { "readonly" } else { "non-readonly" },
615 if self.flags.contains(MetadataFlags::READONLY) { "readonly" } else { "non-readonly" },
616 ))
617 .with_code(ScanningIssueKind::PatchReadonlyMismatch)
618 .with_annotation(Annotation::primary(patch.span))
619 .with_help(
620 "The `readonly` class modifier is structural and cannot be changed by a patch. \
621 Match the original by adding or removing `readonly` on the patched class declaration.",
622 ),
623 );
624 }
625
626 fn report_trait_usage(&self, patch: &mut ClassLikeMetadata) {
630 if patch.used_traits.is_empty() {
631 return;
632 }
633
634 patch.issues.push(
635 Issue::warning(format!(
636 "Patch for `{}` declares `use` traits; patches refine member type information only and trait usage declarations are ignored.",
637 patch.original_name,
638 ))
639 .with_code(ScanningIssueKind::PatchDeclaresTrait)
640 .with_annotation(Annotation::primary(patch.span))
641 .with_help(
642 "Remove the `use` trait statement from the patch. To refine the type information \
643 of members the trait contributes, patch those members on the class directly.",
644 ),
645 );
646 }
647
648 fn patch_templates(&mut self, patch: &ClassLikeMetadata) {
654 if patch.template_types.is_empty() {
655 return;
656 }
657
658 let mut name_to_variance: WordMap<Variance> = self
660 .template_types
661 .keys()
662 .enumerate()
663 .map(|(i, name)| (*name, self.template_variance.get(i).copied().unwrap_or(Variance::Invariant)))
664 .collect();
665
666 name_to_variance.extend(
668 patch
669 .template_types
670 .keys()
671 .enumerate()
672 .map(|(i, name)| (*name, patch.template_variance.get(i).copied().unwrap_or(Variance::Invariant))),
673 );
674
675 self.template_types.extend(patch.template_types.iter().map(|(k, v)| (*k, v.clone())));
677
678 self.template_variance = self
681 .template_types
682 .keys()
683 .map(|name| name_to_variance.get(name).copied().unwrap_or(Variance::Invariant))
684 .collect();
685 self.template_readonly.extend(patch.template_readonly.iter().copied());
686 }
687
688 fn patch_methods(&mut self, patch: &mut ClassLikeMetadata, inherited_methods: &WordSet) {
697 for method_name in &patch.methods {
698 if patch.pseudo_methods.contains(method_name) || patch.static_pseudo_methods.contains(method_name) {
699 continue;
700 }
701 if inherited_methods.contains(method_name) {
702 if self.methods.insert(*method_name) {
708 if let Some(id) = patch.declaring_method_ids.get(method_name) {
709 self.declaring_method_ids.insert(*method_name, *id);
710 }
711 if let Some(id) = patch.appearing_method_ids.get(method_name) {
712 self.appearing_method_ids.insert(*method_name, *id);
713 }
714 if let Some(id) = patch.inheritable_method_ids.get(method_name) {
715 self.inheritable_method_ids.insert(*method_name, *id);
716 }
717 }
718 } else if !self.methods.contains(method_name) {
719 patch.issues.push(
720 Issue::error(format!(
721 "Patch for `{}` declares method `{}` which does not exist in the original \
722 or any of its ancestors; patches cannot introduce new methods.",
723 patch.original_name, method_name,
724 ))
725 .with_code(ScanningIssueKind::PatchIntroducesNewMethod)
726 .with_annotation(Annotation::primary(patch.span))
727 .with_help(format!(
728 "Remove `{method_name}` from the patch, or correct its name to match a method \
729 that already exists on `{}` or one of its ancestors. To annotate a magic \
730 method handled by `__call`/`__callStatic`, declare it with `@method` instead. \
731 Patches can only refine the types of existing methods.",
732 patch.original_name,
733 )),
734 );
735 }
736 }
737
738 for name in patch.pseudo_methods.iter().chain(patch.static_pseudo_methods.iter()) {
739 if let Some(id) = patch.declaring_method_ids.get(name) {
740 self.declaring_method_ids.insert(*name, *id);
741 }
742 if let Some(id) = patch.appearing_method_ids.get(name) {
743 self.appearing_method_ids.insert(*name, *id);
744 }
745 if let Some(id) = patch.inheritable_method_ids.get(name) {
746 self.inheritable_method_ids.insert(*name, *id);
747 }
748 }
749
750 self.pseudo_methods.extend(patch.pseudo_methods.iter().copied());
751 self.static_pseudo_methods.extend(patch.static_pseudo_methods.iter().copied());
752 }
753
754 fn patch_properties(&mut self, patch: &mut ClassLikeMetadata) {
758 let patch_span = patch.span;
759 let patch_name = patch.original_name;
760 for (name, prop_metadata) in &patch.properties {
761 if let Some(slot) = self.properties.get_mut(name) {
762 let visibility_mismatch = prop_metadata.read_visibility != slot.read_visibility
767 || prop_metadata.write_visibility != slot.write_visibility;
768 let structural_flag_mismatch =
772 [MetadataFlags::READONLY, MetadataFlags::STATIC, MetadataFlags::ABSTRACT]
773 .iter()
774 .any(|&f| prop_metadata.flags.contains(f) != slot.flags.contains(f))
775 || (slot.flags.contains(MetadataFlags::FINAL)
776 && !prop_metadata.flags.contains(MetadataFlags::FINAL));
777 let has_hooks = !prop_metadata.hooks.is_empty();
778
779 if visibility_mismatch || structural_flag_mismatch || has_hooks {
780 patch.issues.push(
781 Issue::error(format!(
782 "Patch for `{}::{}` declares structural attributes (visibility, modifiers, \
783 or hooks) that differ from the original; only type annotations are applied.",
784 patch_name, name,
785 ))
786 .with_code(ScanningIssueKind::PatchPropertyStructuralMismatch)
787 .with_annotation(Annotation::primary(prop_metadata.span.unwrap_or(patch_span)))
788 .with_help(format!(
789 "Declare `{patch_name}::{name}` with the same visibility and modifiers as \
790 the original and drop any property hooks; a patch may only refine the \
791 property's type.",
792 )),
793 );
794 }
795
796 slot.type_declaration_metadata.clone_from(&prop_metadata.type_declaration_metadata);
797 slot.type_metadata.clone_from(&prop_metadata.type_metadata);
798 } else if prop_metadata.flags.is_magic_property() {
799 self.add_property(*name, prop_metadata.clone());
800 } else {
801 patch.issues.push(
802 Issue::error(format!(
803 "Patch declares property `{}::{}` which does not exist in the original; \
804 patches cannot introduce new properties.",
805 patch_name, name,
806 ))
807 .with_code(ScanningIssueKind::PatchIntroducesNewProperty)
808 .with_annotation(Annotation::primary(patch_span))
809 .with_help(format!(
810 "Remove `{patch_name}::{name}` from the patch, or correct its name to match an \
811 existing property. To annotate a magic property handled by `__get`/`__set`, \
812 declare it with `@property`, `@property-read`, or `@property-write` instead.",
813 )),
814 );
815 }
816 }
817 }
818
819 fn patch_constants(&mut self, patch: &mut ClassLikeMetadata) {
822 let patch_span = patch.span;
823 let patch_name = patch.original_name;
824 for (name, const_metadata) in &patch.constants {
825 if let Some(slot) = self.constants.get_mut(name) {
826 let visibility_mismatch = const_metadata.visibility != slot.visibility;
827 let structural_flag_mismatch = const_metadata.flags.contains(MetadataFlags::ABSTRACT)
831 != slot.flags.contains(MetadataFlags::ABSTRACT)
832 || (slot.flags.contains(MetadataFlags::FINAL)
833 && !const_metadata.flags.contains(MetadataFlags::FINAL));
834
835 if visibility_mismatch || structural_flag_mismatch {
836 patch.issues.push(
837 Issue::error(format!(
838 "Patch for `{}::{}` declares structural attributes (visibility or modifiers) \
839 that differ from the original; only type annotations are applied.",
840 patch_name, name,
841 ))
842 .with_code(ScanningIssueKind::PatchConstantStructuralMismatch)
843 .with_annotation(Annotation::primary(const_metadata.span))
844 .with_help(format!(
845 "Declare `{patch_name}::{name}` with the same visibility and modifiers as \
846 the original; a patch may only refine the constant's type.",
847 )),
848 );
849 }
850
851 slot.type_declaration.clone_from(&const_metadata.type_declaration);
852 slot.type_metadata.clone_from(&const_metadata.type_metadata);
853 } else {
854 patch.issues.push(
855 Issue::error(format!(
856 "Patch declares constant `{}::{}` which does not exist in the original; \
857 patches cannot introduce new constants.",
858 patch_name, name,
859 ))
860 .with_code(ScanningIssueKind::PatchIntroducesNewConstant)
861 .with_annotation(Annotation::primary(patch_span))
862 .with_help(format!(
863 "Remove `{patch_name}::{name}` from the patch, or correct its name to match a \
864 constant that already exists on the original. Patches can only refine the \
865 types of existing constants.",
866 )),
867 );
868 }
869 }
870 }
871
872 fn report_enum_cases(&self, patch: &mut ClassLikeMetadata) {
877 if patch.enum_cases.is_empty() {
878 return;
879 }
880
881 patch.issues.push(
882 Issue::error(format!(
883 "Patch for `{}` declares enum case(s); enum cases are structural and cannot be \
884 refined — patch enum cases are ignored.",
885 patch.original_name,
886 ))
887 .with_code(ScanningIssueKind::PatchEnumCasesIgnored)
888 .with_annotation(Annotation::primary(patch.span))
889 .with_help(
890 "Remove the enum case declarations from the patch. Enum cases define the runtime \
891 values of the enum and must stay in the original definition; a patch can only \
892 refine the types of existing members.",
893 ),
894 );
895 }
896
897 #[inline]
898 pub fn shrink_to_fit(&mut self) {
899 self.properties.shrink_to_fit();
900 self.initialized_properties.shrink_to_fit();
901 self.appearing_property_ids.shrink_to_fit();
902 self.declaring_property_ids.shrink_to_fit();
903 self.inheritable_property_ids.shrink_to_fit();
904 self.overridden_property_ids.shrink_to_fit();
905 self.appearing_method_ids.shrink_to_fit();
906 self.declaring_method_ids.shrink_to_fit();
907 self.inheritable_method_ids.shrink_to_fit();
908 self.overridden_method_ids.shrink_to_fit();
909 self.attributes.shrink_to_fit();
910 self.constants.shrink_to_fit();
911 self.enum_cases.shrink_to_fit();
912 self.type_aliases.shrink_to_fit();
913 }
914}
915
916#[must_use]
920pub fn collect_ancestor_methods(class_meta: &ClassLikeMetadata, class_likes: &WordMap<ClassLikeMetadata>) -> WordSet {
921 let mut visited = WordSet::default();
922 let mut methods = WordSet::default();
923 collect_ancestor_methods_inner(class_meta, class_likes, &mut visited, &mut methods);
924 methods
925}
926
927fn collect_ancestor_methods_inner(
928 class_meta: &ClassLikeMetadata,
929 class_likes: &WordMap<ClassLikeMetadata>,
930 visited: &mut WordSet,
931 methods: &mut WordSet,
932) {
933 if !visited.insert(class_meta.name) {
934 return;
935 }
936 if let Some(parent_name) = class_meta.direct_parent_class
937 && let Some(parent_meta) = class_likes.get(&parent_name)
938 {
939 methods.extend(parent_meta.methods.iter().copied());
940 collect_ancestor_methods_inner(parent_meta, class_likes, visited, methods);
941 }
942 for interface_name in &class_meta.direct_parent_interfaces {
943 if let Some(interface_meta) = class_likes.get(interface_name) {
944 methods.extend(interface_meta.methods.iter().copied());
945 collect_ancestor_methods_inner(interface_meta, class_likes, visited, methods);
946 }
947 }
948 for trait_name in &class_meta.used_traits {
949 if let Some(trait_meta) = class_likes.get(trait_name) {
950 methods.extend(trait_meta.methods.iter().copied());
951 collect_ancestor_methods_inner(trait_meta, class_likes, visited, methods);
952 }
953 }
954}
955
956#[cfg(test)]
957mod tests {
958 use std::iter::once;
959
960 use mago_span::Span;
961 use mago_word::WordSet;
962 use mago_word::word;
963
964 use crate::identifier::method::MethodIdentifier;
965 use crate::issue::ScanningIssueKind;
966 use crate::metadata::class_like_constant::ClassLikeConstantMetadata;
967 use crate::metadata::enum_case::EnumCaseMetadata;
968 use crate::metadata::flags::MetadataFlags;
969 use crate::metadata::property::PropertyMetadata;
970 use crate::misc::GenericParent;
971 use crate::misc::VariableIdentifier;
972 use crate::symbol::SymbolKind;
973 use crate::ttype;
974 use crate::ttype::template::GenericTemplate;
975 use crate::ttype::template::variance::Variance;
976 use crate::visibility::Visibility;
977
978 use super::ClassLikeMetadata;
979
980 fn has_code(issues: &[mago_reporting::Issue], kind: ScanningIssueKind) -> bool {
981 let code = kind.to_string();
982 issues.iter().any(|i| i.code.as_deref() == Some(code.as_str()))
983 }
984
985 fn make(name: &str) -> ClassLikeMetadata {
986 let a = word(name);
987 ClassLikeMetadata::new(a, a, Span::dummy(0, 10), None, MetadataFlags::empty())
988 }
989
990 #[test]
991 fn apply_patch_adds_override_for_inherited_real_method() {
992 let class_name = word("VendorClass");
993 let mut vendored = make("VendorClass");
994 let method_existing = word("existing");
995 vendored.methods.insert(method_existing);
996 vendored.declaring_method_ids.insert(method_existing, MethodIdentifier::new(class_name, method_existing));
997
998 let mut patch = make("VendorClass");
999 let method_override = word("inherited_method");
1000 patch.methods.insert(method_override);
1001 patch.declaring_method_ids.insert(method_override, MethodIdentifier::new(class_name, method_override));
1002 patch.appearing_method_ids.insert(method_override, MethodIdentifier::new(class_name, method_override));
1003 patch.inheritable_method_ids.insert(method_override, MethodIdentifier::new(class_name, method_override));
1004
1005 let inherited: WordSet = once(method_override).collect();
1006 vendored.apply_patch(&mut patch, &inherited);
1007 let issues = patch.issues;
1008
1009 assert!(vendored.methods.contains(&method_override));
1014 assert_eq!(
1015 vendored.declaring_method_ids.get(&method_override),
1016 Some(&MethodIdentifier::new(class_name, method_override)),
1017 );
1018 assert_eq!(
1019 vendored.appearing_method_ids.get(&method_override),
1020 Some(&MethodIdentifier::new(class_name, method_override)),
1021 );
1022 assert!(vendored.inheritable_method_ids.contains_key(&method_override));
1023
1024 assert!(issues.is_empty());
1026 }
1027
1028 #[test]
1029 fn apply_patch_adds_pseudo_methods() {
1030 let class_name = word("VendorClass");
1031 let mut vendored = make("VendorClass");
1032
1033 let mut patch = make("VendorClass");
1034 let pseudo = word("magicMethod");
1035 patch.pseudo_methods.insert(pseudo);
1036 patch.declaring_method_ids.insert(pseudo, MethodIdentifier::new(class_name, pseudo));
1037 patch.appearing_method_ids.insert(pseudo, MethodIdentifier::new(class_name, pseudo));
1038 patch.inheritable_method_ids.insert(pseudo, MethodIdentifier::new(class_name, pseudo));
1039
1040 vendored.apply_patch(&mut patch, &WordSet::default());
1041 let issues = patch.issues;
1042
1043 assert!(vendored.pseudo_methods.contains(&pseudo));
1045 assert!(vendored.declaring_method_ids.contains_key(&pseudo));
1046 assert!(vendored.appearing_method_ids.contains_key(&pseudo));
1047 assert!(vendored.inheritable_method_ids.contains_key(&pseudo));
1048
1049 assert!(!vendored.methods.contains(&pseudo));
1051
1052 assert!(issues.is_empty());
1054 }
1055
1056 #[test]
1057 fn apply_patch_accepts_new_magic_property() {
1058 let mut vendored = make("VendorClass");
1059
1060 let mut patch = make("VendorClass");
1061 let prop_magic = word("$magic");
1062 patch.properties.insert(
1063 prop_magic,
1064 PropertyMetadata::new(VariableIdentifier(prop_magic), MetadataFlags::PATCH | MetadataFlags::MAGIC_PROPERTY),
1065 );
1066
1067 vendored.apply_patch(&mut patch, &WordSet::default());
1068 let issues = patch.issues;
1069
1070 assert!(vendored.properties.contains_key(&prop_magic));
1071 assert!(issues.is_empty());
1072 }
1073
1074 #[test]
1075 fn apply_patch_does_not_touch_initialized_or_override_maps() {
1076 let mut vendored = make("VendorClass");
1077 let prop = word("$x");
1078 vendored.properties.insert(prop, PropertyMetadata::new(VariableIdentifier(prop), MetadataFlags::empty()));
1079 vendored.initialized_properties.insert(prop);
1080 vendored.overridden_property_ids.insert(prop, once(word("ParentClass")).collect());
1081
1082 let mut patch = make("VendorClass");
1083 patch.properties.insert(prop, PropertyMetadata::new(VariableIdentifier(prop), MetadataFlags::PATCH));
1084 vendored.apply_patch(&mut patch, &WordSet::default());
1087 let issues = patch.issues;
1088
1089 assert!(vendored.initialized_properties.contains(&prop));
1091 assert!(vendored.overridden_property_ids.contains_key(&prop));
1093 assert!(issues.is_empty());
1094 }
1095
1096 #[test]
1097 fn apply_patch_adds_template_types() {
1098 let class_name = word("VendorClass");
1099 let mut vendored = make("VendorClass");
1100
1101 let mut patch = make("VendorClass");
1102 let t = word("T");
1103 patch.template_types.insert(t, GenericTemplate::new(GenericParent::ClassLike(class_name), ttype::get_mixed()));
1104 patch.template_variance.push(Variance::Covariant);
1105 patch.template_readonly.insert(t);
1106
1107 vendored.apply_patch(&mut patch, &WordSet::default());
1108 let issues = patch.issues;
1109
1110 assert!(vendored.template_types.contains_key(&t));
1111 assert_eq!(vendored.template_variance.first().copied(), Some(Variance::Covariant));
1112 assert!(vendored.template_readonly.contains(&t));
1113 assert!(issues.is_empty());
1114 }
1115
1116 #[test]
1117 fn apply_patch_refines_existing_template_and_appends_new() {
1118 let class_name = word("VendorClass");
1119 let mut vendored = make("VendorClass");
1120 let t = word("T");
1121 let u = word("U");
1122
1123 vendored
1125 .template_types
1126 .insert(t, GenericTemplate::new(GenericParent::ClassLike(class_name), ttype::get_mixed()));
1127 vendored
1128 .template_types
1129 .insert(u, GenericTemplate::new(GenericParent::ClassLike(class_name), ttype::get_mixed()));
1130 vendored.template_variance = vec![Variance::Invariant, Variance::Covariant];
1131
1132 let mut patch = make("VendorClass");
1134 let v = word("V");
1135 patch.template_types.insert(t, GenericTemplate::new(GenericParent::ClassLike(class_name), ttype::get_int()));
1136 patch.template_types.insert(v, GenericTemplate::new(GenericParent::ClassLike(class_name), ttype::get_string()));
1137 patch.template_variance = vec![Variance::Contravariant, Variance::Invariant];
1138
1139 vendored.apply_patch(&mut patch, &WordSet::default());
1140 let issues = patch.issues;
1141
1142 assert_eq!(vendored.template_types.keys().copied().collect::<Vec<_>>(), [t, u, v]);
1144 assert_eq!(vendored.template_variance, [Variance::Contravariant, Variance::Covariant, Variance::Invariant]);
1145
1146 assert!(issues.is_empty());
1147 }
1148
1149 #[test]
1150 fn apply_patch_preserves_original_only_readonly_template() {
1151 let class_name = word("VendorClass");
1155 let mut vendored = make("VendorClass");
1156 let t = word("T");
1157 vendored
1158 .template_types
1159 .insert(t, GenericTemplate::new(GenericParent::ClassLike(class_name), ttype::get_mixed()));
1160 vendored.template_variance.push(Variance::Invariant);
1161 vendored.template_readonly.insert(t);
1162
1163 let mut patch = make("VendorClass");
1164 let u = word("U");
1165 patch.template_types.insert(u, GenericTemplate::new(GenericParent::ClassLike(class_name), ttype::get_mixed()));
1166 patch.template_variance.push(Variance::Invariant);
1167
1168 vendored.apply_patch(&mut patch, &WordSet::default());
1169 let issues = patch.issues;
1170
1171 assert!(vendored.template_readonly.contains(&t), "T should remain readonly after patch");
1172 assert!(!vendored.template_readonly.contains(&u), "U was not declared readonly by the patch");
1173 assert!(issues.is_empty());
1174 }
1175
1176 #[test]
1177 fn apply_patch_rejects_kind_mismatch() {
1178 let mut vendored = make("VendorClass");
1179 let mut patch = make("VendorClass");
1180 patch.kind = SymbolKind::Interface;
1181
1182 vendored.apply_patch(&mut patch, &WordSet::default());
1183 let issues = patch.issues;
1184
1185 assert!(has_code(&issues, ScanningIssueKind::PatchKindMismatch));
1186 }
1187
1188 #[test]
1189 fn apply_patch_rejects_trait_use() {
1190 let mut vendored = make("VendorClass");
1191 let mut patch = make("VendorClass");
1192 patch.used_traits.insert(word("SomeTrait"));
1193
1194 vendored.apply_patch(&mut patch, &WordSet::default());
1195 let issues = patch.issues;
1196
1197 assert!(has_code(&issues, ScanningIssueKind::PatchDeclaresTrait));
1198 }
1199
1200 #[test]
1201 fn apply_patch_rejects_hierarchy_mismatch() {
1202 let mut vendored = make("VendorClass");
1203 vendored.direct_parent_class = Some(word("ActualParent"));
1204
1205 let mut patch = make("VendorClass");
1206 patch.direct_parent_class = Some(word("WrongParent"));
1207
1208 vendored.apply_patch(&mut patch, &WordSet::default());
1209 let issues = patch.issues;
1210
1211 assert!(has_code(&issues, ScanningIssueKind::PatchHierarchyMismatch));
1212 }
1213
1214 #[test]
1215 fn apply_patch_rejects_new_method_not_in_ancestors() {
1216 let mut vendored = make("VendorClass");
1217 let mut patch = make("VendorClass");
1218 patch.methods.insert(word("newMethod"));
1219
1220 vendored.apply_patch(&mut patch, &WordSet::default());
1221 let issues = patch.issues;
1222
1223 assert!(has_code(&issues, ScanningIssueKind::PatchIntroducesNewMethod));
1224 }
1225
1226 #[test]
1227 fn apply_patch_rejects_new_property() {
1228 let mut vendored = make("VendorClass");
1229 let mut patch = make("VendorClass");
1230 let prop = word("$newProp");
1231 patch.properties.insert(prop, PropertyMetadata::new(VariableIdentifier(prop), MetadataFlags::PATCH));
1232
1233 vendored.apply_patch(&mut patch, &WordSet::default());
1234 let issues = patch.issues;
1235
1236 assert!(has_code(&issues, ScanningIssueKind::PatchIntroducesNewProperty));
1237 }
1238
1239 #[test]
1240 fn apply_patch_rejects_new_constant() {
1241 let mut vendored = make("VendorClass");
1242 let mut patch = make("VendorClass");
1243 let c = word("NEW_CONST");
1244 patch
1245 .constants
1246 .insert(c, ClassLikeConstantMetadata::new(c, Span::dummy(0, 5), Visibility::Public, MetadataFlags::PATCH));
1247
1248 vendored.apply_patch(&mut patch, &WordSet::default());
1249 let issues = patch.issues;
1250
1251 assert!(has_code(&issues, ScanningIssueKind::PatchIntroducesNewConstant));
1252 }
1253
1254 #[test]
1255 fn apply_patch_rejects_enum_cases() {
1256 let mut vendored = make("VendorClass");
1257 let mut patch = make("VendorClass");
1258 let case = word("CaseA");
1259 patch
1260 .enum_cases
1261 .insert(case, EnumCaseMetadata::new(case, Span::dummy(0, 3), Span::dummy(0, 5), MetadataFlags::PATCH));
1262
1263 vendored.apply_patch(&mut patch, &WordSet::default());
1264 let issues = patch.issues;
1265
1266 assert!(has_code(&issues, ScanningIssueKind::PatchEnumCasesIgnored));
1267 }
1268
1269 #[test]
1270 fn apply_patch_rejects_property_structural_mismatch() {
1271 let mut vendored = make("VendorClass");
1272 let prop = word("$x");
1273 vendored.properties.insert(prop, PropertyMetadata::new(VariableIdentifier(prop), MetadataFlags::empty()));
1274
1275 let mut patch = make("VendorClass");
1276 patch.properties.insert(
1277 prop,
1278 PropertyMetadata::new(VariableIdentifier(prop), MetadataFlags::PATCH | MetadataFlags::STATIC),
1279 );
1280
1281 vendored.apply_patch(&mut patch, &WordSet::default());
1282 let issues = patch.issues;
1283
1284 assert!(has_code(&issues, ScanningIssueKind::PatchPropertyStructuralMismatch));
1285 }
1286
1287 #[test]
1288 fn apply_patch_rejects_constant_structural_mismatch() {
1289 let mut vendored = make("VendorClass");
1290 let c = word("MY_CONST");
1291 vendored.constants.insert(
1292 c,
1293 ClassLikeConstantMetadata::new(c, Span::dummy(0, 5), Visibility::Private, MetadataFlags::empty()),
1294 );
1295
1296 let mut patch = make("VendorClass");
1297 patch
1298 .constants
1299 .insert(c, ClassLikeConstantMetadata::new(c, Span::dummy(0, 5), Visibility::Public, MetadataFlags::PATCH));
1300
1301 vendored.apply_patch(&mut patch, &WordSet::default());
1302 let issues = patch.issues;
1303
1304 assert!(has_code(&issues, ScanningIssueKind::PatchConstantStructuralMismatch));
1305 }
1306}