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