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