Skip to main content

mago_codex/metadata/
function_like.rs

1use std::collections::BTreeMap;
2
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::assertion::Assertion;
14use crate::issue::ScanningIssueKind;
15use crate::metadata::attribute::AttributeMetadata;
16use crate::metadata::class_like::TemplateTypes;
17use crate::metadata::flags::MetadataFlags;
18use crate::metadata::parameter::FunctionLikeParameterMetadata;
19use crate::metadata::ttype::TypeMetadata;
20use crate::metadata::version_constraint::VersionConstraint;
21use crate::ttype::resolution::TypeResolutionContext;
22use crate::ttype::template::GenericTemplate;
23use crate::visibility::Visibility;
24
25/// Contains metadata specific to methods defined within classes, interfaces, enums, or traits.
26///
27/// This complements the more general `FunctionLikeMetadata`.
28#[derive(Debug, Clone, PartialEq, Eq, Default)]
29#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
30#[non_exhaustive]
31pub struct MethodMetadata {
32    /// Marks whether this method is declared as `final`, preventing further overriding.
33    pub is_final: bool,
34
35    /// Marks whether this method is declared as `abstract`, requiring implementation in subclasses.
36    pub is_abstract: bool,
37
38    /// Marks whether this method is declared as `static`, allowing it to be called without an instance.
39    pub is_static: bool,
40
41    /// Marks whether this method is a constructor (`__construct`).
42    pub is_constructor: bool,
43
44    /// Marks whether this method is declared as `public`, `protected`, or `private`.
45    pub visibility: Visibility,
46
47    /// A map of constraints defined by `@where` docblock tags.
48    ///
49    /// The key is the name of a class-level template parameter (e.g., `T`), and the value
50    /// is the `TUnion` type constraint that `T` must satisfy for this specific method
51    /// to be considered callable.
52    pub where_constraints: WordMap<TypeMetadata>,
53}
54
55/// Distinguishes between different kinds of callable constructs in PHP.
56#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
57#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
58pub enum FunctionLikeKind {
59    /// Represents a standard function declared in the global scope or a namespace (`function foo() {}`).
60    Function,
61    /// Represents a method defined within a class, trait, enum, or interface (`class C { function bar() {} }`).
62    Method,
63    /// Represents an anonymous function created using `function() {}`.
64    Closure,
65    /// Represents an arrow function (short closure syntax) introduced in PHP 7.4 (`fn() => ...`).
66    ArrowFunction,
67}
68
69#[derive(Debug, Clone, PartialEq, Eq)]
70#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
71pub struct FunctionLikeMetadata {
72    /// The kind of function-like structure this metadata represents.
73    pub kind: FunctionLikeKind,
74
75    /// The source code location (span) covering the entire function/method/closure definition.
76    /// For closures/arrow functions, this covers the `function(...) { ... }` or `fn(...) => ...` part.
77    pub span: Span,
78
79    /// The lookup name of the function or method. For named functions and
80    /// methods this is the lowercased identifier (PHP-style case-insensitive
81    /// lookup); for closures and arrow functions it is the synthetic
82    /// `{closure:path:line:col}` word produced by
83    /// [`crate::build_synthetic_name`]. Always set.
84    /// Example: `processrequest`, `__construct`, `my_global_func`,
85    /// `{closure:src/foo.php:12:5}`.
86    pub name: Word,
87
88    /// The original-case name. Matches the source identifier for named
89    /// functions/methods, and matches [`Self::name`] verbatim for closures.
90    pub original_name: Word,
91
92    /// The specific source code location (span) of the function or method name identifier.
93    /// `None` if the function/method has no name (closures/arrow functions).
94    pub name_span: Option<Span>,
95
96    /// Ordered list of metadata for each parameter defined in the signature.
97    pub parameters: Vec<FunctionLikeParameterMetadata>,
98
99    /// The explicit return type declaration (type hint).
100    ///
101    /// Example: For `function getName(): string`, this holds metadata for `string`.
102    /// `None` if no return type is specified.
103    pub return_type_declaration_metadata: Option<TypeMetadata>,
104
105    /// The explicit return type declaration (type hint) or docblock type (`@return`).
106    ///
107    /// Example: For `function getName(): string`, this holds metadata for `string`,
108    /// or for ` /** @return string */ function getName() { .. }`, this holds metadata for `string`.
109    /// `None` if neither is specified.
110    pub return_type_metadata: Option<TypeMetadata>,
111
112    /// Generic type parameters (templates) defined for the function/method (e.g., `@template T`).
113    /// Stores the template name and its constraint (defining entity and bound type).
114    /// Example: `{ "T" => (GenericParent::FunctionLike(("funcName", "")), TUnion::object()) }`
115    pub template_types: TemplateTypes,
116
117    /// Attributes attached to the function/method/closure declaration (`#[Attribute] function foo() {}`).
118    pub attributes: Vec<AttributeMetadata>,
119
120    /// Specific metadata relevant only to methods (visibility, final, static, etc.).
121    /// This is `Some` if `kind` is `FunctionLikeKind::Method`, `None` otherwise.
122    pub method_metadata: Option<MethodMetadata>,
123
124    /// Contains context information needed for resolving types within this function's scope
125    /// (e.g., `use` statements, current namespace, class context). Often populated during analysis.
126    pub type_resolution_context: Option<TypeResolutionContext>,
127
128    /// A list of types that this function/method might throw, derived from `@throws` docblock tags
129    /// or inferred from `throw` statements within the body.
130    pub thrown_types: Vec<TypeMetadata>,
131
132    /// List of issues specifically related to parsing or interpreting this function's docblock.
133    pub issues: Vec<Issue>,
134
135    /// Assertions about parameter types or variable types that are guaranteed to be true
136    /// *after* this function/method returns normally. From `@psalm-assert`, `@phpstan-assert`, etc.
137    /// Maps variable/parameter name to a list of type assertions.
138    pub assertions: BTreeMap<Word, Vec<Assertion>>,
139
140    /// Assertions about parameter/variable types that are guaranteed to be true if this
141    /// function/method returns `true`. From `@psalm-assert-if-true`, etc.
142    pub if_true_assertions: BTreeMap<Word, Vec<Assertion>>,
143
144    /// Assertions about parameter/variable types that are guaranteed to be true if this
145    /// function/method returns `false`. From `@psalm-assert-if-false`, etc.
146    pub if_false_assertions: BTreeMap<Word, Vec<Assertion>>,
147
148    /// Set when the assertions in `if_true_assertions` / `if_false_assertions` were
149    /// auto-inferred from the body rather than declared explicitly via docblock. The
150    /// populator uses this to know it can safely override them with assertions
151    /// inherited from a parent method, so explicit contracts on a parent always win.
152    pub assertions_inferred: bool,
153
154    /// Names of variables this function/method imports from the global scope via a
155    /// `global $x;` statement anywhere in its body. Used by the invocation post-processor
156    /// to invalidate caller-side narrowings of those globals on every call, since the
157    /// callee can reassign them behind the caller's back.
158    pub globals_accessed: WordSet,
159
160    /// Tracks whether this function/method has a docblock comment.
161    /// Used to determine if docblock inheritance should occur implicitly.
162    pub has_docblock: bool,
163
164    pub flags: MetadataFlags,
165
166    /// PHP version range in which this function-like is available, derived
167    /// from `Mago\AvailableSince` / `Mago\AvailableUntil` attributes during
168    /// scanning.
169    pub version_constraint: VersionConstraint,
170}
171
172impl FunctionLikeKind {
173    /// Checks if this kind represents a class/trait/enum/interface method.
174    #[inline]
175    #[must_use]
176    pub const fn is_method(&self) -> bool {
177        matches!(self, Self::Method)
178    }
179
180    /// Checks if this kind represents a globally/namespace-scoped function.
181    #[inline]
182    #[must_use]
183    pub const fn is_function(&self) -> bool {
184        matches!(self, Self::Function)
185    }
186
187    /// Checks if this kind represents an anonymous function (`function() {}`).
188    #[inline]
189    #[must_use]
190    pub const fn is_closure(&self) -> bool {
191        matches!(self, Self::Closure)
192    }
193
194    /// Checks if this kind represents an arrow function (`fn() => ...`).
195    #[inline]
196    #[must_use]
197    pub const fn is_arrow_function(&self) -> bool {
198        matches!(self, Self::ArrowFunction)
199    }
200}
201
202/// Contains comprehensive metadata for any function-like structure in PHP.
203impl FunctionLikeMetadata {
204    /// Creates new `FunctionLikeMetadata` with basic information and default flags.
205    ///
206    /// Pass the lookup `name` (lowercased identifier for named functions/methods,
207    /// synthetic `{closure:...}` word for closures) and `original_name` (source
208    /// casing for named items, identical to `name` for closures).
209    #[must_use]
210    pub fn new(kind: FunctionLikeKind, name: Word, original_name: Word, span: Span, flags: MetadataFlags) -> Self {
211        let method_metadata = if kind.is_method() { Some(MethodMetadata::default()) } else { None };
212
213        Self {
214            kind,
215            span,
216            flags,
217            name,
218            original_name,
219            name_span: None,
220            parameters: vec![],
221            return_type_declaration_metadata: None,
222            return_type_metadata: None,
223            template_types: TemplateTypes::default(),
224            attributes: vec![],
225            method_metadata,
226            type_resolution_context: None,
227            thrown_types: vec![],
228            assertions: BTreeMap::new(),
229            if_true_assertions: BTreeMap::new(),
230            if_false_assertions: BTreeMap::new(),
231            assertions_inferred: false,
232            globals_accessed: WordSet::default(),
233            has_docblock: false,
234            issues: vec![],
235            version_constraint: VersionConstraint::unconstrained(),
236        }
237    }
238
239    /// Returns `true` when this function-like is available in the given PHP
240    /// version.
241    #[inline]
242    #[must_use]
243    pub fn is_available_in_version(&self, version: PHPVersion) -> bool {
244        self.version_constraint.allows_version(version)
245    }
246
247    /// Returns `true` when this function-like is available across the entire
248    /// supplied [`PHPVersionRange`].
249    #[inline]
250    #[must_use]
251    pub fn is_available_in_version_range(&self, range: PHPVersionRange) -> bool {
252        self.version_constraint.allows_version_range(range)
253    }
254
255    /// Returns the kind of function-like (Function, Method, Closure, `ArrowFunction`).
256    #[inline]
257    #[must_use]
258    pub fn get_kind(&self) -> FunctionLikeKind {
259        self.kind
260    }
261
262    /// Returns a mutable slice of the parameter metadata.
263    #[inline]
264    pub fn get_parameters_mut(&mut self) -> &mut [FunctionLikeParameterMetadata] {
265        &mut self.parameters
266    }
267
268    /// Returns a reference to specific parameter metadata by name, if it exists.
269    #[inline]
270    #[must_use]
271    pub fn get_parameter(&self, name: Word) -> Option<&FunctionLikeParameterMetadata> {
272        self.parameters.iter().find(|parameter| parameter.get_name().0 == name)
273    }
274
275    /// Returns a mutable reference to specific parameter metadata by name, if it exists.
276    #[inline]
277    pub fn get_parameter_mut(&mut self, name: Word) -> Option<&mut FunctionLikeParameterMetadata> {
278        self.parameters.iter_mut().find(|parameter| parameter.get_name().0 == name)
279    }
280
281    /// Returns a mutable reference to the template type parameters.
282    #[inline]
283    pub fn get_template_types_mut(&mut self) -> &mut TemplateTypes {
284        &mut self.template_types
285    }
286
287    /// Returns a slice of the attributes.
288    #[inline]
289    #[must_use]
290    pub fn get_attributes(&self) -> &[AttributeMetadata] {
291        &self.attributes
292    }
293
294    /// Returns a mutable reference to the method-specific info, if this is a method.
295    #[inline]
296    pub fn get_method_metadata_mut(&mut self) -> Option<&mut MethodMetadata> {
297        self.method_metadata.as_mut()
298    }
299
300    /// Returns a mutable slice of docblock issues.
301    #[inline]
302    pub fn take_issues(&mut self) -> Vec<Issue> {
303        std::mem::take(&mut self.issues)
304    }
305
306    /// Sets the parameters, replacing existing ones.
307    #[inline]
308    pub fn set_parameters(&mut self, parameters: impl IntoIterator<Item = FunctionLikeParameterMetadata>) {
309        self.parameters = parameters.into_iter().collect();
310    }
311
312    /// Returns a new instance with the parameters replaced.
313    #[inline]
314    #[must_use]
315    pub fn with_parameters(mut self, parameters: impl IntoIterator<Item = FunctionLikeParameterMetadata>) -> Self {
316        self.set_parameters(parameters);
317        self
318    }
319
320    #[inline]
321    pub fn set_return_type_metadata(&mut self, return_type: Option<TypeMetadata>) {
322        self.return_type_metadata = return_type;
323    }
324
325    #[inline]
326    pub fn set_return_type_declaration_metadata(&mut self, return_type: Option<TypeMetadata>) {
327        if self.return_type_metadata.is_none() {
328            self.return_type_metadata.clone_from(&return_type);
329        }
330
331        self.return_type_declaration_metadata = return_type;
332    }
333
334    /// Adds a single template type definition.
335    #[inline]
336    pub fn add_template_type(&mut self, name: Word, constraint: GenericTemplate) {
337        self.template_types.insert(name, constraint);
338    }
339
340    /// Applies a patch to this entry in place, refining type information only.
341    ///
342    /// Refined fields — return type, per-parameter types, `@param-out`, default-value types,
343    /// `@throws`, `@template`, and assertions — are each copied only when the patch specifies
344    /// them, so a sparsely-typed patch never erases richer existing information. Structural
345    /// identity (span, file, kind, parameter count, visibility) is left to whichever non-patch
346    /// source declared the symbol. Diagnostics are appended to `patch.issues`; the full set of
347    /// patching rules is documented in the `[source]` patching guide.
348    pub fn apply_patch(&mut self, patch: &mut FunctionLikeMetadata) {
349        // A parameter count or name mismatch means types cannot be mapped positionally;
350        // reject the patch wholesale rather than risk a silent misapply.
351        if self.report_parameter_count_mismatch(patch) || self.report_parameter_name_mismatch(patch) {
352            return;
353        }
354
355        self.report_method_structural_mismatch(patch);
356
357        self.patch_return_type(patch);
358        self.patch_parameters(patch);
359        self.patch_templates(patch);
360        self.patch_throws_and_assertions(patch);
361    }
362
363    /// Reports a parameter count mismatch between the patch and the original.
364    ///
365    /// Returns `true` when the counts differ, in which case the patch must be rejected
366    /// wholesale — there is no sensible positional mapping.
367    fn report_parameter_count_mismatch(&self, patch: &mut FunctionLikeMetadata) -> bool {
368        if patch.parameters.len() == self.parameters.len() {
369            return false;
370        }
371
372        patch.issues.push(
373            Issue::error(format!(
374                "Patch for `{}` declares {} parameter(s) but the original has {}; \
375                 patches cannot change the number of parameters.",
376                patch.original_name,
377                patch.parameters.len(),
378                self.parameters.len(),
379            ))
380            .with_code(ScanningIssueKind::PatchFunctionParameterMismatch)
381            .with_annotation(Annotation::primary(patch.span))
382            .with_help(format!(
383                "Declare exactly {} parameter(s) in the patch to match the original signature. \
384                 Patches refine parameter types only and cannot add or remove parameters.",
385                self.parameters.len(),
386            )),
387        );
388
389        true
390    }
391
392    /// Reports a parameter name mismatch at any position.
393    ///
394    /// Types are refined by position, so a name mismatch (a wrong order, or a patch drifted
395    /// out of sync with the vendor code) would apply them to the wrong parameter. Returns
396    /// `true` on the first mismatch so the patch is rejected wholesale.
397    fn report_parameter_name_mismatch(&self, patch: &mut FunctionLikeMetadata) -> bool {
398        for (index, (base_param, patch_param)) in self.parameters.iter().zip(patch.parameters.iter()).enumerate() {
399            if base_param.name != patch_param.name {
400                patch.issues.push(
401                    Issue::error(format!(
402                        "Patch for `{}` names parameter #{} `{}` but the original declares `{}` there; \
403                         patches refine parameter types by position and the names must match.",
404                        patch.original_name,
405                        index + 1,
406                        patch_param.name.0,
407                        base_param.name.0,
408                    ))
409                    .with_code(ScanningIssueKind::PatchFunctionParameterNameMismatch)
410                    .with_annotation(Annotation::primary(patch_param.name_span))
411                    .with_help(
412                        "Declare the patch's parameters with the same names in the same order as the \
413                         original signature so their types are applied to the intended parameters.",
414                    ),
415                );
416                return true;
417            }
418        }
419
420        false
421    }
422
423    /// Reports structural method-attribute mismatches.
424    ///
425    /// For methods, visibility, static, and removing final are all structural changes a patch
426    /// may not make. Adding final is allowed. This is reported as an error but does not abort
427    /// the patch — type annotations are still applied.
428    ///
429    /// `abstract` is deliberately excluded: it is implied by writing the method with a trailing
430    /// `;` instead of a `{}` body — the idiomatic form for a signature-only type patch — rather
431    /// than being an explicit modifier the author chose. The patch never changes the original's
432    /// abstractness either way, so a difference is harmless and would only produce a spurious
433    /// error for the natural patch syntax.
434    fn report_method_structural_mismatch(&self, patch: &mut FunctionLikeMetadata) {
435        let (Some(patch_m), Some(base_m)) = (&patch.method_metadata, &self.method_metadata) else {
436            return;
437        };
438
439        let visibility_mismatch = patch_m.visibility != base_m.visibility;
440        let static_mismatch = patch_m.is_static != base_m.is_static;
441        let final_removed = base_m.is_final && !patch_m.is_final;
442
443        if visibility_mismatch || static_mismatch || final_removed {
444            patch.issues.push(
445                Issue::error(format!(
446                    "Patch for `{}` declares structural attributes (visibility, static, or \
447                     removing final) that differ from the original; only type annotations are applied.",
448                    patch.original_name,
449                ))
450                .with_code(ScanningIssueKind::PatchMethodStructuralMismatch)
451                .with_annotation(Annotation::primary(patch.span))
452                .with_help(
453                    "Declare the method with the same visibility and the same `static` and \
454                     `final` modifiers as the original (adding `final` is allowed); \
455                     a patch may only refine the method's types.",
456                ),
457            );
458        }
459    }
460
461    /// Refines the return type (declaration and docblock) when the patch specifies it.
462    fn patch_return_type(&mut self, patch: &FunctionLikeMetadata) {
463        if let Some(decl) = &patch.return_type_declaration_metadata {
464            self.return_type_declaration_metadata = Some(decl.clone());
465        }
466        if let Some(ty) = &patch.return_type_metadata {
467            self.return_type_metadata = Some(ty.clone());
468        }
469    }
470
471    /// Refines per-parameter types by position; each field is copied only when the patch
472    /// specifies it, so a sparsely-typed patch does not erase richer existing information.
473    fn patch_parameters(&mut self, patch: &FunctionLikeMetadata) {
474        for (slot, replacement) in self.parameters.iter_mut().zip(patch.parameters.iter()) {
475            if let Some(decl) = &replacement.type_declaration_metadata {
476                slot.type_declaration_metadata = Some(decl.clone());
477            }
478            if let Some(ty) = &replacement.type_metadata {
479                slot.type_metadata = Some(ty.clone());
480            }
481            if let Some(out) = &replacement.out_type {
482                slot.out_type = Some(out.clone());
483            }
484            if let Some(default) = &replacement.default_type {
485                slot.default_type = Some(default.clone());
486            }
487        }
488    }
489
490    /// Merges `@template` declarations from the patch.
491    fn patch_templates(&mut self, patch: &FunctionLikeMetadata) {
492        if !patch.template_types.is_empty() {
493            self.template_types.extend(patch.template_types.iter().map(|(k, v)| (*k, v.clone())));
494        }
495    }
496
497    /// Replaces `@throws` and `@psalm-assert`-style annotations when the patch declares any.
498    fn patch_throws_and_assertions(&mut self, patch: &FunctionLikeMetadata) {
499        if !patch.thrown_types.is_empty() {
500            self.thrown_types.clone_from(&patch.thrown_types);
501        }
502        if !patch.assertions.is_empty() {
503            self.assertions = patch.assertions.clone();
504        }
505        if !patch.if_true_assertions.is_empty() {
506            self.if_true_assertions = patch.if_true_assertions.clone();
507        }
508        if !patch.if_false_assertions.is_empty() {
509            self.if_false_assertions = patch.if_false_assertions.clone();
510        }
511    }
512}