mago_codex/metadata/
mod.rs

1use std::collections::hash_map::Entry;
2
3use ahash::HashMap;
4use ahash::HashSet;
5use serde::Deserialize;
6use serde::Serialize;
7
8use mago_interner::StringIdentifier;
9use mago_interner::ThreadedInterner;
10use mago_reporting::IssueCollection;
11
12use crate::get_closure;
13use crate::get_function;
14use crate::get_method;
15use crate::identifier::function_like::FunctionLikeIdentifier;
16use crate::identifier::method::MethodIdentifier;
17use crate::metadata::class_like::ClassLikeMetadata;
18use crate::metadata::constant::ConstantMetadata;
19use crate::metadata::function_like::FunctionLikeMetadata;
20use crate::metadata::property::PropertyMetadata;
21use crate::metadata::ttype::TypeMetadata;
22use crate::symbol::SymbolKind;
23use crate::symbol::Symbols;
24use crate::ttype::atomic::TAtomic;
25use crate::ttype::union::TUnion;
26
27pub mod attribute;
28pub mod class_like;
29pub mod class_like_constant;
30pub mod constant;
31pub mod enum_case;
32pub mod flags;
33pub mod function_like;
34pub mod parameter;
35pub mod property;
36pub mod ttype;
37
38/// Holds all analyzed information about the symbols, structures, and relationships within a codebase.
39///
40/// This acts as the central repository for metadata gathered during static analysis,
41/// including details about classes, interfaces, traits, enums, functions, constants,
42/// their members, inheritance, dependencies, and associated types.
43#[derive(Clone, Serialize, Deserialize, Debug, PartialEq)]
44pub struct CodebaseMetadata {
45    /// Configuration flag: Should types be inferred based on usage patterns?
46    pub infer_types_from_usage: bool,
47    /// Map from type alias name (`StringIdentifier`) to its metadata (`TypeMetadata`).
48    pub aliases: HashMap<StringIdentifier, TypeMetadata>,
49    /// Map from class-like FQCN (`StringIdentifier`) to its detailed metadata (`ClassLikeMetadata`).
50    pub class_likes: HashMap<StringIdentifier, ClassLikeMetadata>,
51    /// Map from a function/method identifier tuple `(scope_id, function_id)` to its metadata (`FunctionLikeMetadata`).
52    /// `scope_id` is the FQCN for methods or often `StringIdentifier::empty()` for global functions.
53    pub function_likes: HashMap<(StringIdentifier, StringIdentifier), FunctionLikeMetadata>,
54    /// Stores the kind (Class, Interface, etc.) for every known symbol FQCN.
55    pub symbols: Symbols,
56    /// Map from global constant FQN (`StringIdentifier`) to its metadata (`ConstantMetadata`).
57    pub constants: HashMap<StringIdentifier, ConstantMetadata>,
58    /// Map from class/interface FQCN to the set of all its descendants (recursive).
59    pub all_class_like_descendants: HashMap<StringIdentifier, HashSet<StringIdentifier>>,
60    /// Map from class/interface FQCN to the set of its direct descendants (children).
61    pub direct_classlike_descendants: HashMap<StringIdentifier, HashSet<StringIdentifier>>,
62    /// Set of symbols (FQCNs) considered "safe" or trusted (e.g., immutable, well-defined).
63    pub safe_symbols: HashSet<StringIdentifier>,
64    /// Set of specific members `(SymbolFQCN, MemberName)` considered "safe" or trusted.
65    pub safe_symbol_members: HashSet<(StringIdentifier, StringIdentifier)>,
66}
67
68impl CodebaseMetadata {
69    /// Creates a new, empty `CodebaseMetadata` with default values.
70    #[inline]
71    pub fn new() -> Self {
72        Self::default()
73    }
74
75    /// Checks if a class-like structure can be part of an intersection.
76    /// Generally, only final classes cannot be intersected further down the hierarchy.
77    #[inline]
78    pub fn is_inheritable(&self, fq_class_name: &StringIdentifier) -> bool {
79        match self.symbols.get_kind(fq_class_name) {
80            Some(SymbolKind::Class) => {
81                // Check if the class metadata exists and if it's NOT final
82                self.class_likes.get(fq_class_name).is_some_and(|meta| !meta.flags.is_final())
83            }
84            Some(SymbolKind::Enum) => {
85                // Enums are final and cannot be part of intersections
86                false
87            }
88            Some(SymbolKind::Interface) | Some(SymbolKind::Trait) | None => {
89                // Interfaces, Enums, Traits, or non-existent symbols can conceptually be part of intersections
90                true
91            }
92        }
93    }
94
95    #[inline]
96    pub fn class_or_trait_can_use_trait(
97        &self,
98        child_class: &StringIdentifier,
99        parent_trait: &StringIdentifier,
100    ) -> bool {
101        if let Some(metadata) = self.class_likes.get(child_class) {
102            if metadata.used_traits.contains(parent_trait) {
103                return true;
104            }
105
106            return metadata.used_traits.contains(parent_trait);
107        }
108        false
109    }
110
111    /// Retrieves the literal value (as a `TAtomic`) of a class constant, if it was inferred.
112    /// Returns `None` if the class/constant doesn't exist or the value type wasn't inferred.
113    #[inline]
114    pub fn get_classconst_literal_value(
115        &self,
116        fq_class_name: &StringIdentifier,
117        const_name: &StringIdentifier,
118    ) -> Option<&TAtomic> {
119        self.class_likes
120            .get(fq_class_name)
121            .and_then(|class_metadata| class_metadata.constants.get(const_name))
122            .and_then(|constant_metadata| constant_metadata.inferred_type.as_ref())
123    }
124
125    /// Checks if a property with the given name exists (is declared or inherited) within the class-like structure.
126    /// Relies on `ClassLikeMetadata::has_appearing_property`.
127    #[inline]
128    pub fn property_exists(&self, classlike_name: &StringIdentifier, property_name: &StringIdentifier) -> bool {
129        self.class_likes
130            .get(classlike_name)
131            .is_some_and(|metadata| metadata.appearing_property_ids.contains_key(property_name))
132    }
133
134    /// Checks if a method with the given name exists within the class-like structure.
135    /// Relies on `ClassLikeMetadata::has_method`.
136    #[inline]
137    pub fn method_exists(&self, classlike_name: &StringIdentifier, method_name: &StringIdentifier) -> bool {
138        self.class_likes.get(classlike_name).is_some_and(|metadata| metadata.methods.contains(method_name))
139    }
140
141    /// Checks if a method with the given name exists (is declared or inherited) within the class-like structure.
142    /// Relies on `ClassLikeMetadata::has_appearing_method`.
143    #[inline]
144    pub fn appearing_method_exists(&self, classlike_name: &StringIdentifier, method_name: &StringIdentifier) -> bool {
145        self.class_likes.get(classlike_name).is_some_and(|metadata| metadata.has_appearing_method(method_name))
146    }
147
148    /// Checks specifically if a method is *declared* directly within the given class-like (not just inherited).
149    #[inline]
150    pub fn declaring_method_exists(&self, classlike_name: &StringIdentifier, method_name: &StringIdentifier) -> bool {
151        self.class_likes.get(classlike_name).and_then(|metadata| metadata.declaring_method_ids.get(method_name))
152            == Some(classlike_name) // Check if declaring class is this class
153    }
154
155    /// Finds the FQCN of the class/trait where a property was originally declared for a given class context.
156    /// Returns `None` if the property doesn't appear in the class hierarchy.
157    #[inline]
158    pub fn get_declaring_class_for_property(
159        &self,
160        fq_class_name: &StringIdentifier,
161        property_name: &StringIdentifier,
162    ) -> Option<&StringIdentifier> {
163        self.class_likes.get(fq_class_name).and_then(|metadata| metadata.declaring_property_ids.get(property_name))
164    }
165
166    /// Retrieves the full metadata for a property as it appears in the context of a specific class.
167    /// This might be the metadata from the declaring class.
168    /// Returns `None` if the class or property doesn't exist in this context.
169    #[inline]
170    pub fn get_property_metadata(
171        &self,
172        fq_class_name: &StringIdentifier,
173        property_name: &StringIdentifier,
174    ) -> Option<&PropertyMetadata> {
175        // Find where the property appears (could be inherited)
176        let appearing_class_fqcn =
177            self.class_likes.get(fq_class_name).and_then(|meta| meta.appearing_property_ids.get(property_name)); // Assumes get_appearing_property_ids
178
179        // Get the metadata from the class where it appears
180        appearing_class_fqcn
181            .and_then(|fqcn| self.class_likes.get(fqcn))
182            .and_then(|meta| meta.properties.get(property_name))
183    }
184
185    /// Retrieves the type union for a property within the context of a specific class.
186    /// It finds the declaring class of the property and returns its type signature.
187    /// Returns `None` if the property or its type cannot be found.
188    #[inline]
189    pub fn get_property_type(
190        &self,
191        fq_class_name: &StringIdentifier,
192        property_name: &StringIdentifier,
193    ) -> Option<&TUnion> {
194        // Find the class where the property was originally declared
195        let declaring_class_fqcn = self.get_declaring_class_for_property(fq_class_name, property_name)?;
196        // Get the metadata for that property from its declaring class
197        let property_metadata = self.class_likes.get(declaring_class_fqcn)?.properties.get(property_name)?;
198
199        // Return the type metadata's union from that metadata
200        property_metadata.type_metadata.as_ref().map(|tm| &tm.type_union)
201    }
202
203    /// Resolves a `MethodIdentifier` to the identifier of the method as it *appears* in the given class context.
204    /// This could be the declaring class or an ancestor if inherited.
205    #[inline]
206    pub fn get_appearing_method_id(&self, method_id: &MethodIdentifier) -> MethodIdentifier {
207        self.class_likes
208            .get(method_id.get_class_name())
209            .and_then(|metadata| metadata.appearing_method_ids.get(method_id.get_method_name()))
210            .map_or(*method_id, |appearing_fqcn| MethodIdentifier::new(*appearing_fqcn, *method_id.get_method_name()))
211    }
212
213    /// Retrieves the metadata for a specific function-like construct using its identifier.
214    #[inline]
215    pub fn get_function_like(
216        &self,
217        identifier: &FunctionLikeIdentifier,
218        interner: &ThreadedInterner,
219    ) -> Option<&FunctionLikeMetadata> {
220        match identifier {
221            FunctionLikeIdentifier::Function(fq_function_name) => get_function(self, interner, fq_function_name),
222            FunctionLikeIdentifier::Method(fq_classlike_name, method_name) => {
223                get_method(self, interner, fq_classlike_name, method_name)
224            }
225            FunctionLikeIdentifier::Closure(file_id, position) => get_closure(self, interner, file_id, position),
226        }
227    }
228
229    /// Merges information from another `CodebaseMetadata` into this one.
230    /// Collections are extended. For HashMaps, entries in `other` may overwrite existing ones.
231    #[inline]
232    pub fn extend(&mut self, other: CodebaseMetadata) {
233        for (k, v) in other.aliases {
234            self.aliases.entry(k).or_insert(v);
235        }
236
237        // Merge class-likes with priority
238        for (k, v) in other.class_likes {
239            let metadata_to_keep = match self.class_likes.entry(k) {
240                Entry::Occupied(entry) => {
241                    let existing_metadata = entry.remove();
242
243                    if v.flags.is_user_defined() {
244                        v
245                    } else if existing_metadata.flags.is_user_defined() {
246                        existing_metadata
247                    } else if v.flags.is_built_in() {
248                        v
249                    } else if existing_metadata.flags.is_built_in() {
250                        existing_metadata
251                    } else {
252                        v
253                    }
254                }
255                Entry::Vacant(_) => v,
256            };
257            self.class_likes.insert(k, metadata_to_keep);
258        }
259
260        for (k, v) in other.function_likes {
261            let metadata_to_keep = match self.function_likes.entry(k) {
262                Entry::Occupied(entry) => {
263                    let existing_metadata = entry.remove();
264
265                    if v.flags.is_user_defined() {
266                        v
267                    } else if existing_metadata.flags.is_user_defined() {
268                        existing_metadata
269                    } else if v.flags.is_built_in() {
270                        v
271                    } else if existing_metadata.flags.is_built_in() {
272                        existing_metadata
273                    } else {
274                        v
275                    }
276                }
277                Entry::Vacant(_) => v,
278            };
279            self.function_likes.insert(k, metadata_to_keep);
280        }
281
282        for (k, v) in other.constants {
283            let metadata_to_keep = match self.constants.entry(k) {
284                Entry::Occupied(entry) => {
285                    let existing_metadata = entry.remove();
286
287                    if v.flags.is_user_defined() {
288                        v
289                    } else if existing_metadata.flags.is_user_defined() {
290                        existing_metadata
291                    } else if v.flags.is_built_in() {
292                        v
293                    } else if existing_metadata.flags.is_built_in() {
294                        existing_metadata
295                    } else {
296                        v
297                    }
298                }
299                Entry::Vacant(_) => v,
300            };
301            self.constants.insert(k, metadata_to_keep);
302        }
303
304        self.symbols.extend(other.symbols);
305
306        for (k, v) in other.all_class_like_descendants {
307            self.all_class_like_descendants.entry(k).or_default().extend(v);
308        }
309
310        for (k, v) in other.direct_classlike_descendants {
311            self.direct_classlike_descendants.entry(k).or_default().extend(v);
312        }
313
314        self.safe_symbols.extend(other.safe_symbols);
315        self.safe_symbol_members.extend(other.safe_symbol_members);
316        self.infer_types_from_usage |= other.infer_types_from_usage;
317    }
318
319    pub fn take_issues(&mut self, user_defined: bool) -> IssueCollection {
320        let mut issues = IssueCollection::new();
321
322        for metadata in self.class_likes.values_mut() {
323            if user_defined && !metadata.flags.is_user_defined() {
324                continue;
325            }
326
327            issues.extend(metadata.take_issues());
328        }
329
330        for metadata in self.function_likes.values_mut() {
331            if user_defined && !metadata.flags.is_user_defined() {
332                continue;
333            }
334
335            issues.extend(metadata.take_issues());
336        }
337
338        for metadata in self.constants.values_mut() {
339            if user_defined && !metadata.flags.is_user_defined() {
340                continue;
341            }
342
343            issues.extend(metadata.take_issues());
344        }
345
346        issues
347    }
348}
349
350/// Provides a default, empty `CodebaseMetadata`.
351impl Default for CodebaseMetadata {
352    #[inline]
353    fn default() -> Self {
354        Self {
355            class_likes: HashMap::default(),
356            aliases: HashMap::default(),
357            function_likes: HashMap::default(),
358            symbols: Symbols::new(),
359            infer_types_from_usage: false,
360            constants: HashMap::default(),
361            all_class_like_descendants: HashMap::default(),
362            direct_classlike_descendants: HashMap::default(),
363            safe_symbols: HashSet::default(),
364            safe_symbol_members: HashSet::default(),
365        }
366    }
367}