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