Skip to main content

mago_analyzer/plugin/
context.rs

1//! Context types for providers and hooks.
2
3use std::cell::RefCell;
4use std::rc::Rc;
5
6use mago_codex::context::ScopeContext;
7use mago_codex::metadata::CodebaseMetadata;
8use mago_codex::metadata::class_like::ClassLikeMetadata;
9use mago_codex::metadata::function_like::FunctionLikeMetadata;
10use mago_codex::ttype::atomic::TAtomic;
11use mago_codex::ttype::atomic::scalar::TScalar;
12use mago_codex::ttype::atomic::scalar::string::TString;
13use mago_codex::ttype::atomic::scalar::string::TStringLiteral;
14use mago_codex::ttype::union::TUnion;
15use mago_database::file::File;
16use mago_reporting::Issue;
17use mago_span::HasSpan;
18use mago_span::Span;
19use mago_syntax::cst::Argument;
20use mago_syntax::cst::ClassLikeMemberSelector;
21use mago_syntax::cst::Expression;
22use mago_syntax::cst::PartialApplication;
23use mago_syntax::cst::PartialArgument;
24use mago_word::Word;
25use mago_word::word;
26
27use crate::artifacts::AnalysisArtifacts;
28use crate::code::IssueCode;
29use crate::context::block::BlockContext;
30use crate::invocation::Invocation;
31use crate::invocation::InvocationArgument;
32use crate::invocation::InvocationArgumentsSource;
33
34pub struct ReportedIssue {
35    pub code: IssueCode,
36    pub issue: Issue,
37}
38
39#[allow(clippy::field_scoped_visibility_modifiers)]
40pub struct ProviderContext<'codebase, 'artifacts, 'block> {
41    pub(crate) codebase: &'codebase CodebaseMetadata,
42    pub(crate) source_file: &'codebase File,
43    pub(crate) artifacts: &'artifacts AnalysisArtifacts,
44    pub(crate) block_context: &'block BlockContext<'codebase>,
45    pub(crate) reported_issues: RefCell<Vec<ReportedIssue>>,
46}
47
48impl<'codebase, 'artifacts, 'block> ProviderContext<'codebase, 'artifacts, 'block> {
49    pub(crate) fn new(
50        codebase: &'codebase CodebaseMetadata,
51        source_file: &'codebase File,
52        block_context: &'block BlockContext<'codebase>,
53        artifacts: &'artifacts AnalysisArtifacts,
54    ) -> Self {
55        Self { codebase, source_file, artifacts, block_context, reported_issues: RefCell::new(Vec::new()) }
56    }
57
58    pub fn report(&self, code: IssueCode, issue: Issue) {
59        self.reported_issues.borrow_mut().push(ReportedIssue { code, issue });
60    }
61
62    pub(crate) fn take_issues(&self) -> Vec<ReportedIssue> {
63        std::mem::take(&mut *self.reported_issues.borrow_mut())
64    }
65
66    #[inline]
67    pub fn codebase(&self) -> &'codebase CodebaseMetadata {
68        self.codebase
69    }
70
71    #[inline]
72    pub fn get_expression_type<T>(&self, expr: &T) -> Option<&TUnion>
73    where
74        T: HasSpan,
75    {
76        self.artifacts.get_expression_type(expr)
77    }
78
79    #[inline]
80    pub fn get_rc_expression_type<T>(&self, expr: &T) -> Option<&Rc<TUnion>>
81    where
82        T: HasSpan,
83    {
84        self.artifacts.get_rc_expression_type(expr)
85    }
86
87    #[inline]
88    pub fn get_variable_type(&self, name: &[u8]) -> Option<&Rc<TUnion>> {
89        self.block_context.locals.get(&word(name))
90    }
91
92    #[inline]
93    pub fn scope(&self) -> &ScopeContext<'codebase> {
94        &self.block_context.scope
95    }
96
97    #[inline]
98    pub fn is_instance_of(&self, class: &[u8], parent: &[u8]) -> bool {
99        self.codebase.is_instance_of(class, parent)
100    }
101
102    #[inline]
103    pub fn get_closure_metadata<'arena>(&self, expr: &Expression<'arena>) -> Option<&'codebase FunctionLikeMetadata> {
104        match expr {
105            Expression::ArrowFunction(arrow_fn) => self.codebase.get_closure_at(self.source_file, arrow_fn.span()),
106            Expression::Closure(closure) => self.codebase.get_closure_at(self.source_file, closure.span()),
107            _ => None,
108        }
109    }
110
111    /// Get metadata for a callable expression (closure, arrow function, or first-class callable).
112    ///
113    /// This method extends `get_closure_metadata` to also handle first-class callables
114    /// like `is_string(...)` or `SomeClass::method(...)`, as well as string literals representing callables.
115    #[inline]
116    pub fn get_callable_metadata<'arena>(&self, expr: &Expression<'arena>) -> Option<&'codebase FunctionLikeMetadata> {
117        match expr {
118            Expression::ArrowFunction(arrow_fn) => self.codebase.get_closure_at(self.source_file, arrow_fn.span()),
119            Expression::Closure(closure) => self.codebase.get_closure_at(self.source_file, closure.span()),
120            Expression::PartialApplication(partial) => match partial {
121                PartialApplication::Function(func_partial) => {
122                    if !func_partial.argument_list.is_first_class_callable() {
123                        return None;
124                    }
125
126                    if let Expression::Identifier(identifier) = func_partial.function {
127                        self.codebase.get_function(identifier.value())
128                    } else {
129                        None
130                    }
131                }
132                PartialApplication::StaticMethod(static_partial) => {
133                    if !static_partial.argument_list.is_first_class_callable() {
134                        return None;
135                    }
136
137                    if let Expression::Identifier(class_id) = static_partial.class {
138                        if let ClassLikeMemberSelector::Identifier(method_id) = &static_partial.method {
139                            self.codebase.get_method(class_id.value(), method_id.value)
140                        } else {
141                            None
142                        }
143                    } else {
144                        None
145                    }
146                }
147                PartialApplication::Method(method_partial) => {
148                    if !method_partial.argument_list.is_first_class_callable() {
149                        return None;
150                    }
151
152                    let ClassLikeMemberSelector::Identifier(method_id) = &method_partial.method else {
153                        return None;
154                    };
155
156                    let object_type = self.get_rc_expression_type(method_partial.object)?;
157                    let single_object = object_type.get_single_named_object()?;
158                    let class_name = single_object.get_name();
159
160                    self.codebase.get_method(class_name.as_bytes(), method_id.value)
161                }
162            },
163            _ => {
164                let expr_type = self.get_rc_expression_type(expr)?;
165                if !expr_type.is_single() {
166                    return None;
167                }
168
169                match expr_type.get_single() {
170                    TAtomic::Callable(first_callable) => {
171                        if let Some(identifier) = first_callable.get_alias() {
172                            self.codebase.get_function_like(identifier)
173                        } else {
174                            None
175                        }
176                    }
177                    TAtomic::Scalar(TScalar::String(TString {
178                        literal: Some(TStringLiteral::Value(literal_string)),
179                        ..
180                    })) => {
181                        let bytes = literal_string.as_bytes();
182                        if let Some(pos) = memchr::memmem::find(bytes, b"::") {
183                            self.codebase.get_method(&bytes[..pos], &bytes[pos + 2..])
184                        } else {
185                            self.codebase.get_function(bytes)
186                        }
187                    }
188                    _ => None,
189                }
190            }
191        }
192    }
193
194    #[inline]
195    pub fn get_class_like(&self, name: Word) -> Option<&ClassLikeMetadata> {
196        self.codebase.get_class_like(name.as_bytes())
197    }
198
199    #[inline]
200    pub fn current_class_name(&self) -> Option<Word> {
201        self.block_context.scope.get_class_like_name()
202    }
203}
204
205/// Context for hooks that provides mutable access to analysis state.
206///
207/// Unlike `ProviderContext` which is read-only, `HookContext` allows hooks
208/// to modify the analysis state (expression types, variable types, assertions).
209#[allow(clippy::field_scoped_visibility_modifiers)]
210pub struct HookContext<'ctx, 'block> {
211    pub(crate) codebase: &'ctx CodebaseMetadata,
212    pub(crate) source_file: &'ctx File,
213    pub(crate) block_context: &'block mut BlockContext<'ctx>,
214    pub(crate) artifacts: &'block mut AnalysisArtifacts,
215    pub(crate) reported_issues: RefCell<Vec<ReportedIssue>>,
216}
217
218impl<'ctx, 'block> HookContext<'ctx, 'block> {
219    pub(crate) fn new(
220        codebase: &'ctx CodebaseMetadata,
221        source_file: &'ctx File,
222        block_context: &'block mut BlockContext<'ctx>,
223        artifacts: &'block mut AnalysisArtifacts,
224    ) -> Self {
225        Self { codebase, source_file, artifacts, block_context, reported_issues: RefCell::new(Vec::new()) }
226    }
227
228    /// Report an issue from a hook.
229    pub fn report(&self, code: IssueCode, issue: Issue) {
230        self.reported_issues.borrow_mut().push(ReportedIssue { code, issue });
231    }
232
233    pub(crate) fn take_issues(&self) -> Vec<ReportedIssue> {
234        std::mem::take(&mut *self.reported_issues.borrow_mut())
235    }
236
237    /// Get access to the codebase metadata.
238    #[inline]
239    pub fn codebase(&self) -> &'ctx CodebaseMetadata {
240        self.codebase
241    }
242
243    /// Get the type of an expression.
244    #[inline]
245    pub fn get_expression_type<T>(&self, expr: &T) -> Option<&TUnion>
246    where
247        T: HasSpan,
248    {
249        self.artifacts.get_expression_type(expr)
250    }
251
252    /// Get the type of an expression as an Rc.
253    #[inline]
254    pub fn get_rc_expression_type<T>(&self, expr: &T) -> Option<&Rc<TUnion>>
255    where
256        T: HasSpan,
257    {
258        self.artifacts.get_rc_expression_type(expr)
259    }
260
261    /// Get the type of a variable.
262    #[inline]
263    pub fn get_variable_type(&self, name: &[u8]) -> Option<&Rc<TUnion>> {
264        self.block_context.locals.get(&word(name))
265    }
266
267    /// Get the current scope context.
268    #[inline]
269    pub fn scope(&self) -> &ScopeContext<'ctx> {
270        &self.block_context.scope
271    }
272
273    /// Check if a class is an instance of another class.
274    #[inline]
275    pub fn is_instance_of(&self, class: &[u8], parent: &[u8]) -> bool {
276        self.codebase.is_instance_of(class, parent)
277    }
278
279    /// Get metadata for a closure expression.
280    #[inline]
281    pub fn get_closure_metadata<'arena>(&self, expr: &Expression<'arena>) -> Option<&'ctx FunctionLikeMetadata> {
282        match expr {
283            Expression::ArrowFunction(arrow_fn) => self.codebase.get_closure_at(self.source_file, arrow_fn.span()),
284            Expression::Closure(closure) => self.codebase.get_closure_at(self.source_file, closure.span()),
285            _ => None,
286        }
287    }
288
289    /// Get metadata for a class-like by name.
290    #[inline]
291    pub fn get_class_like(&self, name: Word) -> Option<&ClassLikeMetadata> {
292        self.codebase.get_class_like(name.as_bytes())
293    }
294
295    /// Get the current class name if inside a class.
296    #[inline]
297    pub fn current_class_name(&self) -> Option<Word> {
298        self.block_context.scope.get_class_like_name()
299    }
300
301    /// Set the type of an expression.
302    #[inline]
303    pub fn set_expression_type<T>(&mut self, expr: &T, ty: TUnion)
304    where
305        T: HasSpan,
306    {
307        self.artifacts.set_expression_type(expr, ty);
308    }
309
310    /// Set the type of a variable.
311    #[inline]
312    pub fn set_variable_type(&mut self, name: &[u8], ty: TUnion) {
313        self.block_context.locals.insert(word(name), Rc::new(ty));
314    }
315
316    /// Get mutable access to the analysis artifacts.
317    #[inline]
318    pub fn artifacts_mut(&mut self) -> &mut AnalysisArtifacts {
319        self.artifacts
320    }
321
322    /// Get immutable access to the analysis artifacts.
323    #[inline]
324    pub fn artifacts(&self) -> &AnalysisArtifacts {
325        self.artifacts
326    }
327
328    /// Get mutable access to the block context.
329    #[inline]
330    pub fn block_context_mut(&mut self) -> &mut BlockContext<'ctx> {
331        self.block_context
332    }
333
334    /// Get immutable access to the block context.
335    #[inline]
336    pub fn block_context(&self) -> &BlockContext<'ctx> {
337        self.block_context
338    }
339}
340
341#[allow(clippy::field_scoped_visibility_modifiers)]
342pub struct InvocationInfo<'ctx, 'ast, 'arena> {
343    pub(crate) invocation: &'ctx Invocation<'ctx, 'ast, 'arena>,
344}
345
346impl<'ctx, 'ast, 'arena> InvocationInfo<'ctx, 'ast, 'arena> {
347    pub(crate) fn new(invocation: &'ctx Invocation<'ctx, 'ast, 'arena>) -> Self {
348        Self { invocation }
349    }
350
351    #[inline]
352    #[must_use]
353    pub fn get_argument(&self, index: usize, names: &[&[u8]]) -> Option<&'ast Expression<'arena>> {
354        get_argument(self.invocation.arguments_source, index, names)
355    }
356
357    #[inline]
358    #[must_use]
359    pub fn arguments(&self) -> Vec<InvocationArgument<'ast, 'arena>> {
360        self.invocation.arguments_source.get_arguments()
361    }
362
363    #[inline]
364    #[must_use]
365    pub fn argument_count(&self) -> usize {
366        self.invocation.arguments_source.argument_count()
367    }
368
369    #[inline]
370    #[must_use]
371    pub fn has_no_arguments(&self) -> bool {
372        self.invocation.arguments_source.is_empty()
373    }
374
375    #[inline]
376    #[must_use]
377    pub fn span(&self) -> Span {
378        self.invocation.span
379    }
380
381    #[inline]
382    #[must_use]
383    pub fn inner(&self) -> &'ctx Invocation<'ctx, 'ast, 'arena> {
384        self.invocation
385    }
386
387    #[inline]
388    #[must_use]
389    pub fn function_name(&self) -> String {
390        self.invocation.target.get_function_like_identifier().map(|identifier| identifier.as_string()).unwrap_or_else(
391            || {
392                if self.invocation.target.is_non_closure_callable() {
393                    "callable".to_string()
394                } else {
395                    "Closure".to_string()
396                }
397            },
398        )
399    }
400}
401
402impl HasSpan for InvocationInfo<'_, '_, '_> {
403    fn span(&self) -> Span {
404        self.invocation.span
405    }
406}
407
408fn get_argument<'ast, 'arena>(
409    call_arguments: InvocationArgumentsSource<'ast, 'arena>,
410    index: usize,
411    names: &[&[u8]],
412) -> Option<&'ast Expression<'arena>> {
413    match call_arguments {
414        InvocationArgumentsSource::ArgumentList(argument_list) => {
415            if let Some(Argument::Positional(argument)) = argument_list.arguments.get(index) {
416                return Some(argument.value);
417            }
418
419            for argument in &argument_list.arguments {
420                if let Argument::Named(named_argument) = argument
421                    && names.contains(&named_argument.name.value)
422                {
423                    return Some(named_argument.value);
424                }
425            }
426
427            None
428        }
429        InvocationArgumentsSource::PartialArgumentList(partial_argument_list) => {
430            if let Some(PartialArgument::Positional(argument)) = partial_argument_list.arguments.get(index) {
431                return Some(argument.value);
432            }
433
434            for argument in &partial_argument_list.arguments {
435                if let PartialArgument::Named(named_argument) = argument
436                    && names.contains(&named_argument.name.value)
437                {
438                    return Some(named_argument.value);
439                }
440            }
441
442            None
443        }
444        InvocationArgumentsSource::PipeInput(pipe) => {
445            if index == 0 {
446                Some(pipe.input)
447            } else {
448                None
449            }
450        }
451        InvocationArgumentsSource::None(_) => None,
452    }
453}