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