Skip to main content

mago_codex/metadata/
class_like.rs

1use foldhash::fast::RandomState;
2use indexmap::IndexMap;
3use mago_php_version::PHPVersion;
4use mago_php_version::PHPVersionRange;
5use serde::Deserialize;
6use serde::Serialize;
7
8use mago_reporting::Issue;
9use mago_span::Span;
10use mago_word::Word;
11use mago_word::WordMap;
12use mago_word::WordSet;
13
14use crate::flags::attribute::AttributeFlags;
15use crate::identifier::method::MethodIdentifier;
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
30/// Type alias for template types stored in metadata.
31/// Maps template parameter names to their defining entity and constraint type.
32pub type TemplateTypes = IndexMap<Word, GenericTemplate, RandomState>;
33
34/// Contains comprehensive metadata for a PHP class-like structure (class, interface, trait, enum).
35///
36/// Aggregates information about inheritance, traits, generics, methods, properties, constants,
37/// attributes, docblock tags, analysis flags, and more.
38#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
39#[non_exhaustive]
40pub struct ClassLikeMetadata {
41    pub name: Word,
42    pub original_name: Word,
43    pub span: Span,
44    pub direct_parent_interfaces: WordSet,
45    pub all_parent_interfaces: WordSet,
46    pub direct_parent_class: Option<Word>,
47    pub require_extends: WordSet,
48    pub require_implements: WordSet,
49    pub all_parent_classes: WordSet,
50    pub used_traits: WordSet,
51    pub trait_alias_map: WordMap<Word>,
52    pub trait_visibility_map: WordMap<Visibility>,
53    pub trait_final_map: WordSet,
54    pub child_class_likes: Option<WordSet>,
55    pub name_span: Option<Span>,
56    pub kind: SymbolKind,
57    pub template_types: TemplateTypes,
58    pub template_readonly: WordSet,
59    pub template_variance: Vec<Variance>,
60    pub template_extended_offsets: WordMap<Vec<TUnion>>,
61    pub template_extended_parameters: WordMap<IndexMap<Word, TUnion, RandomState>>,
62    pub template_extended_parameter_paths: WordMap<Vec<IndexMap<Word, TUnion, RandomState>>>,
63    pub template_type_extends_count: WordMap<usize>,
64    pub template_type_implements_count: WordMap<usize>,
65    pub template_type_uses_count: WordMap<usize>,
66    pub methods: WordSet,
67    pub pseudo_methods: WordSet,
68    pub static_pseudo_methods: WordSet,
69    pub declaring_method_ids: WordMap<MethodIdentifier>,
70    pub appearing_method_ids: WordMap<MethodIdentifier>,
71    pub inheritable_method_ids: WordMap<MethodIdentifier>,
72    pub overridden_method_ids: WordMap<IndexMap<Word, MethodIdentifier, RandomState>>,
73    pub properties: WordMap<PropertyMetadata>,
74    pub appearing_property_ids: WordMap<Word>,
75    pub declaring_property_ids: WordMap<Word>,
76    pub inheritable_property_ids: WordMap<Word>,
77    pub overridden_property_ids: WordMap<WordSet>,
78    pub initialized_properties: WordSet,
79    pub constants: WordMap<ClassLikeConstantMetadata>,
80    pub trait_constant_ids: WordMap<Word>,
81    pub enum_cases: WordMap<EnumCaseMetadata>,
82    pub invalid_dependencies: WordSet,
83    pub attributes: Vec<AttributeMetadata>,
84    pub enum_type: Option<TAtomic>,
85    pub has_sealed_methods: Option<bool>,
86    pub has_sealed_properties: Option<bool>,
87    pub permitted_inheritors: Option<WordSet>,
88    pub issues: Vec<Issue>,
89    pub attribute_flags: Option<AttributeFlags>,
90    pub flags: MetadataFlags,
91    pub type_aliases: WordMap<TypeMetadata>,
92    /// Imported type aliases in the form of (`from_fqcn`, `type_name`, span)
93    pub imported_type_aliases: WordMap<(Word, Word, Span)>,
94    /// Mixin types from @mixin annotations - these types' methods/properties
95    /// can be accessed via magic methods (__call, __get, __set, __callStatic)
96    pub mixins: Vec<TUnion>,
97    pub version_constraint: VersionConstraint,
98}
99
100impl ClassLikeMetadata {
101    #[must_use]
102    pub fn new(
103        name: Word,
104        original_name: Word,
105        span: Span,
106        name_span: Option<Span>,
107        flags: MetadataFlags,
108    ) -> ClassLikeMetadata {
109        ClassLikeMetadata {
110            constants: WordMap::default(),
111            trait_constant_ids: WordMap::default(),
112            enum_cases: WordMap::default(),
113            flags,
114            kind: SymbolKind::Class,
115            direct_parent_interfaces: WordSet::default(),
116            all_parent_classes: WordSet::default(),
117            appearing_method_ids: WordMap::default(),
118            attributes: Vec::new(),
119            all_parent_interfaces: WordSet::default(),
120            declaring_method_ids: WordMap::default(),
121            appearing_property_ids: WordMap::default(),
122            declaring_property_ids: WordMap::default(),
123            direct_parent_class: None,
124            require_extends: WordSet::default(),
125            require_implements: WordSet::default(),
126            inheritable_method_ids: WordMap::default(),
127            enum_type: None,
128            inheritable_property_ids: WordMap::default(),
129            initialized_properties: WordSet::default(),
130            invalid_dependencies: WordSet::default(),
131            span,
132            name_span,
133            methods: WordSet::default(),
134            pseudo_methods: WordSet::default(),
135            static_pseudo_methods: WordSet::default(),
136            overridden_method_ids: WordMap::default(),
137            overridden_property_ids: WordMap::default(),
138            properties: WordMap::default(),
139            template_variance: Vec::new(),
140            template_type_extends_count: WordMap::default(),
141            template_extended_parameters: WordMap::default(),
142            template_extended_parameter_paths: WordMap::default(),
143            template_extended_offsets: WordMap::default(),
144            template_type_implements_count: WordMap::default(),
145            template_type_uses_count: WordMap::default(),
146            template_types: TemplateTypes::default(),
147            used_traits: WordSet::default(),
148            trait_alias_map: WordMap::default(),
149            trait_visibility_map: WordMap::default(),
150            trait_final_map: WordSet::default(),
151            name,
152            original_name,
153            child_class_likes: None,
154            template_readonly: WordSet::default(),
155            has_sealed_methods: None,
156            has_sealed_properties: None,
157            permitted_inheritors: None,
158            issues: vec![],
159            attribute_flags: None,
160            type_aliases: WordMap::default(),
161            imported_type_aliases: WordMap::default(),
162            mixins: Vec::default(),
163            version_constraint: VersionConstraint::unconstrained(),
164        }
165    }
166
167    /// Returns `true` when this class-like is available in the given PHP
168    /// version.
169    #[inline]
170    #[must_use]
171    pub fn is_available_in_version(&self, version: PHPVersion) -> bool {
172        self.version_constraint.allows_version(version)
173    }
174
175    /// Returns `true` when this class-like is available across the entire
176    /// supplied [`PHPVersionRange`].
177    #[inline]
178    #[must_use]
179    pub fn is_available_in_version_range(&self, range: PHPVersionRange) -> bool {
180        self.version_constraint.allows_version_range(range)
181    }
182
183    /// Returns a reference to the map of trait method aliases.
184    #[inline]
185    #[must_use]
186    pub fn get_trait_alias_map(&self) -> &WordMap<Word> {
187        &self.trait_alias_map
188    }
189
190    /// Returns a vector of the generic type parameter names.
191    #[inline]
192    #[must_use]
193    pub fn get_template_type_names(&self) -> Vec<Word> {
194        self.template_types.keys().copied().collect()
195    }
196
197    /// Returns type parameters for a specific generic parameter name.
198    #[inline]
199    #[must_use]
200    pub fn get_template_type(&self, name: Word) -> Option<&GenericTemplate> {
201        self.template_types.get(&name)
202    }
203
204    /// Returns type parameters for a specific generic parameter name with its index.
205    #[inline]
206    #[must_use]
207    pub fn get_template_type_with_index(&self, name: Word) -> Option<(usize, &GenericTemplate)> {
208        self.template_types.get_full(&name).map(|(index, _, types)| (index, types))
209    }
210
211    #[must_use]
212    pub fn get_template_for_index(&self, index: usize) -> Option<(Word, &GenericTemplate)> {
213        self.template_types.get_index(index).map(|(name, types)| (*name, types))
214    }
215
216    #[must_use]
217    pub fn get_template_name_for_index(&self, index: usize) -> Option<Word> {
218        self.template_types.get_index(index).map(|(name, _)| *name)
219    }
220
221    #[must_use]
222    pub fn get_template_index_for_name(&self, name: Word) -> Option<usize> {
223        self.template_types.get_index_of(&name)
224    }
225
226    /// Checks if a specific parent is either a parent class or interface.
227    #[inline]
228    #[must_use]
229    pub fn has_parent(&self, parent: Word) -> bool {
230        self.all_parent_classes.contains(&parent) || self.all_parent_interfaces.contains(&parent)
231    }
232
233    /// Checks if a specific parent has template extended parameters.
234    #[inline]
235    #[must_use]
236    pub fn has_template_extended_parameter(&self, parent: Word) -> bool {
237        self.template_extended_parameters.contains_key(&parent)
238    }
239
240    /// Checks if a specific method appears in this class-like.
241    #[inline]
242    #[must_use]
243    pub fn has_appearing_method(&self, method: Word) -> bool {
244        self.appearing_method_ids.contains_key(&method)
245    }
246
247    /// Returns a vector of property names.
248    #[inline]
249    #[must_use]
250    pub fn get_property_names(&self) -> WordSet {
251        self.properties.keys().copied().collect()
252    }
253
254    /// Checks if a specific property appears in this class-like.
255    #[inline]
256    #[must_use]
257    pub fn has_appearing_property(&self, name: Word) -> bool {
258        self.appearing_property_ids.contains_key(&name)
259    }
260
261    /// Checks if a specific property is declared in this class-like.
262    #[inline]
263    #[must_use]
264    pub fn has_declaring_property(&self, name: Word) -> bool {
265        self.declaring_property_ids.contains_key(&name)
266    }
267
268    /// Takes ownership of the issues found for this class-like structure.
269    #[inline]
270    pub fn take_issues(&mut self) -> Vec<Issue> {
271        std::mem::take(&mut self.issues)
272    }
273
274    /// Adds a single direct parent interface.
275    #[inline]
276    pub fn add_direct_parent_interface(&mut self, interface: Word) {
277        self.direct_parent_interfaces.insert(interface);
278        self.all_parent_interfaces.insert(interface);
279    }
280
281    /// Adds a single interface to the list of all parent interfaces. Use with caution, normally derived.
282    #[inline]
283    pub fn add_all_parent_interface(&mut self, interface: Word) {
284        self.all_parent_interfaces.insert(interface);
285    }
286
287    /// Adds multiple interfaces to the list of all parent interfaces. Use with caution.
288    #[inline]
289    pub fn add_all_parent_interfaces(&mut self, interfaces: impl IntoIterator<Item = Word>) {
290        self.all_parent_interfaces.extend(interfaces);
291    }
292
293    /// Adds multiple ancestor classes. Use with caution.
294    #[inline]
295    pub fn add_all_parent_classes(&mut self, classes: impl IntoIterator<Item = Word>) {
296        self.all_parent_classes.extend(classes);
297    }
298
299    /// Adds a single used trait. Returns `true` if the trait was not already present.
300    #[inline]
301    pub fn add_used_trait(&mut self, trait_name: Word) -> bool {
302        self.used_traits.insert(trait_name)
303    }
304
305    /// Adds multiple used traits.
306    #[inline]
307    pub fn add_used_traits(&mut self, traits: impl IntoIterator<Item = Word>) {
308        self.used_traits.extend(traits);
309    }
310
311    /// Adds or updates a single trait alias. Returns the previous original name if one existed for the alias.
312    #[inline]
313    pub fn add_trait_alias(&mut self, method: Word, alias: Word) -> Option<Word> {
314        self.trait_alias_map.insert(method, alias)
315    }
316
317    /// Adds or updates a single trait visibility override. Returns the previous visibility if one existed.
318    #[inline]
319    pub fn add_trait_visibility(&mut self, method: Word, visibility: Visibility) -> Option<Visibility> {
320        self.trait_visibility_map.insert(method, visibility)
321    }
322
323    /// Adds a single template type definition.
324    #[inline]
325    pub fn add_template_type(&mut self, name: Word, constraint: GenericTemplate) {
326        self.template_types.insert(name, constraint);
327    }
328
329    /// Set the variance for the template parameters
330    #[inline]
331    pub fn set_template_variance(&mut self, template_variance: Vec<Variance>) {
332        self.template_variance = template_variance;
333    }
334
335    /// Adds or replaces the offset types for a specific template parameter name.
336    #[inline]
337    pub fn add_template_extended_offset(&mut self, name: Word, types: Vec<TUnion>) -> Option<Vec<TUnion>> {
338        self.template_extended_offsets.insert(name, types)
339    }
340
341    /// Adds or replaces the resolved parameters for a specific parent FQCN.
342    #[inline]
343    pub fn extend_template_extended_parameters(
344        &mut self,
345        template_extended_parameters: WordMap<IndexMap<Word, TUnion, RandomState>>,
346    ) {
347        self.template_extended_parameters.extend(template_extended_parameters);
348    }
349
350    /// Adds or replaces a single resolved parameter for the parent FQCN.
351    #[inline]
352    pub fn add_template_extended_parameter(
353        &mut self,
354        parent_fqcn: Word,
355        parameter_name: Word,
356        parameter_type: TUnion,
357    ) -> Option<TUnion> {
358        self.template_extended_parameters.entry(parent_fqcn).or_default().insert(parameter_name, parameter_type)
359    }
360
361    /// Records one complete parameterization of `ancestor` (a single inheritance
362    /// path), de-duplicating against parameterizations already recorded.
363    #[inline]
364    pub fn record_template_extended_path(&mut self, ancestor: Word, parameters: IndexMap<Word, TUnion, RandomState>) {
365        if parameters.is_empty() {
366            return;
367        }
368
369        let paths = self.template_extended_parameter_paths.entry(ancestor).or_default();
370        if !paths.contains(&parameters) {
371            paths.push(parameters);
372        }
373    }
374
375    /// Adds or updates the declaring method identifier for a method name.
376    #[inline]
377    pub fn add_declaring_method_id(
378        &mut self,
379        method: Word,
380        declaring_method_id: MethodIdentifier,
381    ) -> Option<MethodIdentifier> {
382        self.add_appearing_method_id(method, declaring_method_id);
383        self.declaring_method_ids.insert(method, declaring_method_id)
384    }
385
386    /// Adds or updates the appearing method identifier for a method name.
387    #[inline]
388    pub fn add_appearing_method_id(
389        &mut self,
390        method: Word,
391        appearing_method_id: MethodIdentifier,
392    ) -> Option<MethodIdentifier> {
393        self.appearing_method_ids.insert(method, appearing_method_id)
394    }
395
396    /// Adds a parent method identifier to the map for an overridden method. Initializes map if needed. Returns the previous value if one existed.
397    #[inline]
398    pub fn add_overridden_method_parent(
399        &mut self,
400        method: Word,
401        parent_method_id: MethodIdentifier,
402    ) -> Option<MethodIdentifier> {
403        self.overridden_method_ids
404            .entry(method)
405            .or_default()
406            .insert(parent_method_id.get_class_name(), parent_method_id)
407    }
408
409    /// Adds or updates a property's metadata. Returns the previous metadata if the property existed.
410    #[inline]
411    pub fn add_property(&mut self, name: Word, property_metadata: PropertyMetadata) -> Option<PropertyMetadata> {
412        let class_name = self.name;
413
414        self.add_declaring_property_id(name, class_name);
415        if property_metadata.flags.has_default() {
416            self.initialized_properties.insert(name);
417        }
418
419        if !property_metadata.is_final() {
420            self.inheritable_property_ids.insert(name, class_name);
421        }
422
423        self.properties.insert(name, property_metadata)
424    }
425
426    /// Adds or updates a property's metadata using just the property metadata. Returns the previous metadata if the property existed.
427    #[inline]
428    pub fn add_property_metadata(&mut self, property_metadata: PropertyMetadata) -> Option<PropertyMetadata> {
429        let name = property_metadata.get_name().0;
430
431        self.add_property(name, property_metadata)
432    }
433
434    /// Adds or updates the declaring class FQCN for a property name.
435    #[inline]
436    pub fn add_declaring_property_id(&mut self, prop: Word, declaring_fqcn: Word) -> Option<Word> {
437        self.appearing_property_ids.insert(prop, declaring_fqcn);
438        self.declaring_property_ids.insert(prop, declaring_fqcn)
439    }
440
441    #[must_use]
442    pub fn get_missing_required_interface<'meta>(&self, other: &'meta ClassLikeMetadata) -> Option<&'meta Word> {
443        for required_interface in &other.require_implements {
444            if self.all_parent_interfaces.contains(required_interface) {
445                continue;
446            }
447
448            if (self.flags.is_abstract() || self.kind.is_trait())
449                && self.require_implements.contains(required_interface)
450            {
451                continue; // Abstract classes and traits can require interfaces they implement
452            }
453
454            return Some(required_interface);
455        }
456
457        None
458    }
459
460    #[must_use]
461    pub fn get_missing_required_extends<'meta>(&self, other: &'meta ClassLikeMetadata) -> Option<&'meta Word> {
462        for required_extend in &other.require_extends {
463            if self.all_parent_classes.contains(required_extend) {
464                continue;
465            }
466
467            if self.kind.is_interface() && self.all_parent_interfaces.contains(required_extend) {
468                continue;
469            }
470
471            if (self.flags.is_abstract() || self.kind.is_trait()) && self.require_extends.contains(required_extend) {
472                continue; // Abstract classes and traits can require classes they extend
473            }
474
475            return Some(required_extend);
476        }
477
478        None
479    }
480
481    #[must_use]
482    pub fn is_permitted_to_inherit(&self, other: &ClassLikeMetadata) -> bool {
483        if self.kind.is_trait() || self.flags.is_abstract() {
484            return true; // Traits and abstract classes can always inherit
485        }
486
487        let Some(permitted_inheritors) = &other.permitted_inheritors else {
488            return true; // No restrictions, inheriting is allowed
489        };
490
491        if permitted_inheritors.contains(&self.name) {
492            return true; // This class-like is explicitly permitted to inherit
493        }
494
495        self.all_parent_interfaces.iter().any(|parent_interface| permitted_inheritors.contains(parent_interface))
496            || self.all_parent_classes.iter().any(|parent_class| permitted_inheritors.contains(parent_class))
497            || self.used_traits.iter().any(|used_trait| permitted_inheritors.contains(used_trait))
498    }
499
500    #[inline]
501    pub fn mark_as_populated(&mut self) {
502        self.flags |= MetadataFlags::POPULATED;
503        self.shrink_to_fit();
504    }
505
506    #[inline]
507    pub fn shrink_to_fit(&mut self) {
508        self.properties.shrink_to_fit();
509        self.initialized_properties.shrink_to_fit();
510        self.appearing_property_ids.shrink_to_fit();
511        self.declaring_property_ids.shrink_to_fit();
512        self.inheritable_property_ids.shrink_to_fit();
513        self.overridden_property_ids.shrink_to_fit();
514        self.appearing_method_ids.shrink_to_fit();
515        self.declaring_method_ids.shrink_to_fit();
516        self.inheritable_method_ids.shrink_to_fit();
517        self.overridden_method_ids.shrink_to_fit();
518        self.attributes.shrink_to_fit();
519        self.constants.shrink_to_fit();
520        self.enum_cases.shrink_to_fit();
521        self.type_aliases.shrink_to_fit();
522    }
523}