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::union::TUnion;
13use mago_reporting::Issue;
14use mago_span::HasSpan;
15use mago_span::Span;
16use mago_syntax::ast::Argument;
17use mago_syntax::ast::Expression;
18use mago_syntax::ast::PartialArgument;
19
20use crate::artifacts::AnalysisArtifacts;
21use crate::code::IssueCode;
22use crate::context::block::BlockContext;
23use crate::invocation::Invocation;
24use crate::invocation::InvocationArgument;
25use crate::invocation::InvocationArgumentsSource;
26
27pub struct ReportedIssue {
28    pub code: IssueCode,
29    pub issue: Issue,
30}
31
32pub struct ProviderContext<'a, 'b, 'c> {
33    pub(crate) codebase: &'a CodebaseMetadata,
34    pub(crate) artifacts: &'b AnalysisArtifacts,
35    pub(crate) block_context: &'c BlockContext<'a>,
36    pub(crate) reported_issues: RefCell<Vec<ReportedIssue>>,
37}
38
39impl<'a, 'b, 'c> ProviderContext<'a, 'b, 'c> {
40    pub(crate) fn new(
41        codebase: &'a CodebaseMetadata,
42        block_context: &'c BlockContext<'a>,
43        artifacts: &'b AnalysisArtifacts,
44    ) -> Self {
45        Self { codebase, artifacts, block_context, reported_issues: RefCell::new(Vec::new()) }
46    }
47
48    pub fn report(&self, code: IssueCode, issue: Issue) {
49        self.reported_issues.borrow_mut().push(ReportedIssue { code, issue });
50    }
51
52    pub(crate) fn take_issues(&self) -> Vec<ReportedIssue> {
53        std::mem::take(&mut *self.reported_issues.borrow_mut())
54    }
55
56    #[inline]
57    pub fn codebase(&self) -> &'a CodebaseMetadata {
58        self.codebase
59    }
60
61    #[inline]
62    pub fn get_expression_type<T: HasSpan>(&self, expr: &T) -> Option<&TUnion> {
63        self.artifacts.get_expression_type(expr)
64    }
65
66    #[inline]
67    pub fn get_rc_expression_type<T: HasSpan>(&self, expr: &T) -> Option<&Rc<TUnion>> {
68        self.artifacts.get_rc_expression_type(expr)
69    }
70
71    #[inline]
72    pub fn get_variable_type(&self, name: &str) -> Option<&Rc<TUnion>> {
73        self.block_context.locals.get(&atom(name))
74    }
75
76    #[inline]
77    pub fn scope(&self) -> &ScopeContext<'a> {
78        &self.block_context.scope
79    }
80
81    #[inline]
82    pub fn is_instance_of(&self, class: &str, parent: &str) -> bool {
83        self.codebase.is_instance_of(class, parent)
84    }
85
86    #[inline]
87    pub fn get_closure_metadata<'arena>(&self, expr: &Expression<'arena>) -> Option<&'a FunctionLikeMetadata> {
88        match expr {
89            Expression::ArrowFunction(arrow_fn) => {
90                let span = arrow_fn.span();
91                self.codebase.get_closure(&span.file_id, &span.start)
92            }
93            Expression::Closure(closure) => {
94                let span = closure.span();
95                self.codebase.get_closure(&span.file_id, &span.start)
96            }
97            _ => None,
98        }
99    }
100
101    #[inline]
102    pub fn get_class_like(&self, name: &Atom) -> Option<&ClassLikeMetadata> {
103        self.codebase.get_class_like(name)
104    }
105
106    #[inline]
107    pub fn current_class_name(&self) -> Option<Atom> {
108        self.block_context.scope.get_class_like_name()
109    }
110}
111
112/// Context for hooks that provides mutable access to analysis state.
113///
114/// Unlike `ProviderContext` which is read-only, `HookContext` allows hooks
115/// to modify the analysis state (expression types, variable types, assertions).
116pub struct HookContext<'ctx, 'a> {
117    pub(crate) codebase: &'ctx CodebaseMetadata,
118    pub(crate) block_context: &'a mut BlockContext<'ctx>,
119    pub(crate) artifacts: &'a mut AnalysisArtifacts,
120    pub(crate) reported_issues: RefCell<Vec<ReportedIssue>>,
121}
122
123impl<'ctx, 'a> HookContext<'ctx, 'a> {
124    pub(crate) fn new(
125        codebase: &'ctx CodebaseMetadata,
126        block_context: &'a mut BlockContext<'ctx>,
127        artifacts: &'a mut AnalysisArtifacts,
128    ) -> Self {
129        Self { codebase, artifacts, block_context, reported_issues: RefCell::new(Vec::new()) }
130    }
131
132    /// Report an issue from a hook.
133    pub fn report(&self, code: IssueCode, issue: Issue) {
134        self.reported_issues.borrow_mut().push(ReportedIssue { code, issue });
135    }
136
137    pub(crate) fn take_issues(&self) -> Vec<ReportedIssue> {
138        std::mem::take(&mut *self.reported_issues.borrow_mut())
139    }
140
141    /// Get access to the codebase metadata.
142    #[inline]
143    pub fn codebase(&self) -> &'ctx CodebaseMetadata {
144        self.codebase
145    }
146
147    /// Get the type of an expression.
148    #[inline]
149    pub fn get_expression_type<T: HasSpan>(&self, expr: &T) -> Option<&TUnion> {
150        self.artifacts.get_expression_type(expr)
151    }
152
153    /// Get the type of an expression as an Rc.
154    #[inline]
155    pub fn get_rc_expression_type<T: HasSpan>(&self, expr: &T) -> Option<&Rc<TUnion>> {
156        self.artifacts.get_rc_expression_type(expr)
157    }
158
159    /// Get the type of a variable.
160    #[inline]
161    pub fn get_variable_type(&self, name: &str) -> Option<&Rc<TUnion>> {
162        self.block_context.locals.get(&atom(name))
163    }
164
165    /// Get the current scope context.
166    #[inline]
167    pub fn scope(&self) -> &ScopeContext<'ctx> {
168        &self.block_context.scope
169    }
170
171    /// Check if a class is an instance of another class.
172    #[inline]
173    pub fn is_instance_of(&self, class: &str, parent: &str) -> bool {
174        self.codebase.is_instance_of(class, parent)
175    }
176
177    /// Get metadata for a closure expression.
178    #[inline]
179    pub fn get_closure_metadata<'arena>(&self, expr: &Expression<'arena>) -> Option<&'ctx FunctionLikeMetadata> {
180        match expr {
181            Expression::ArrowFunction(arrow_fn) => {
182                let span = arrow_fn.span();
183                self.codebase.get_closure(&span.file_id, &span.start)
184            }
185            Expression::Closure(closure) => {
186                let span = closure.span();
187                self.codebase.get_closure(&span.file_id, &span.start)
188            }
189            _ => None,
190        }
191    }
192
193    /// Get metadata for a class-like by name.
194    #[inline]
195    pub fn get_class_like(&self, name: &Atom) -> Option<&ClassLikeMetadata> {
196        self.codebase.get_class_like(name)
197    }
198
199    /// Get the current class name if inside a class.
200    #[inline]
201    pub fn current_class_name(&self) -> Option<Atom> {
202        self.block_context.scope.get_class_like_name()
203    }
204
205    /// Set the type of an expression.
206    #[inline]
207    pub fn set_expression_type<T: HasSpan>(&mut self, expr: &T, ty: TUnion) {
208        self.artifacts.set_expression_type(expr, ty);
209    }
210
211    /// Set the type of a variable.
212    #[inline]
213    pub fn set_variable_type(&mut self, name: &str, ty: TUnion) {
214        self.block_context.locals.insert(atom(name), Rc::new(ty));
215    }
216
217    /// Get mutable access to the analysis artifacts.
218    #[inline]
219    pub fn artifacts_mut(&mut self) -> &mut AnalysisArtifacts {
220        self.artifacts
221    }
222
223    /// Get immutable access to the analysis artifacts.
224    #[inline]
225    pub fn artifacts(&self) -> &AnalysisArtifacts {
226        self.artifacts
227    }
228
229    /// Get mutable access to the block context.
230    #[inline]
231    pub fn block_context_mut(&mut self) -> &mut BlockContext<'ctx> {
232        self.block_context
233    }
234
235    /// Get immutable access to the block context.
236    #[inline]
237    pub fn block_context(&self) -> &BlockContext<'ctx> {
238        self.block_context
239    }
240}
241
242pub struct InvocationInfo<'ctx, 'ast, 'arena> {
243    pub(crate) invocation: &'ctx Invocation<'ctx, 'ast, 'arena>,
244}
245
246impl<'ctx, 'ast, 'arena> InvocationInfo<'ctx, 'ast, 'arena> {
247    pub(crate) fn new(invocation: &'ctx Invocation<'ctx, 'ast, 'arena>) -> Self {
248        Self { invocation }
249    }
250
251    #[inline]
252    #[must_use]
253    pub fn get_argument(&self, index: usize, names: &[&str]) -> Option<&'ast Expression<'arena>> {
254        get_argument(self.invocation.arguments_source, index, names)
255    }
256
257    #[inline]
258    #[must_use]
259    pub fn arguments(&self) -> Vec<InvocationArgument<'ast, 'arena>> {
260        self.invocation.arguments_source.get_arguments()
261    }
262
263    #[inline]
264    #[must_use]
265    pub fn argument_count(&self) -> usize {
266        self.invocation.arguments_source.get_arguments().len()
267    }
268
269    #[inline]
270    #[must_use]
271    pub fn has_no_arguments(&self) -> bool {
272        self.invocation.arguments_source.get_arguments().is_empty()
273    }
274
275    #[inline]
276    #[must_use]
277    pub fn span(&self) -> Span {
278        self.invocation.span
279    }
280
281    #[inline]
282    #[must_use]
283    pub fn inner(&self) -> &'ctx Invocation<'ctx, 'ast, 'arena> {
284        self.invocation
285    }
286
287    #[inline]
288    #[must_use]
289    pub fn function_name(&self) -> String {
290        self.invocation.target.guess_name()
291    }
292}
293
294impl HasSpan for InvocationInfo<'_, '_, '_> {
295    fn span(&self) -> Span {
296        self.invocation.span
297    }
298}
299
300fn get_argument<'ast, 'arena>(
301    call_arguments: InvocationArgumentsSource<'ast, 'arena>,
302    index: usize,
303    names: &[&str],
304) -> Option<&'ast Expression<'arena>> {
305    match call_arguments {
306        InvocationArgumentsSource::ArgumentList(argument_list) => {
307            if let Some(Argument::Positional(argument)) = argument_list.arguments.get(index) {
308                return Some(&argument.value);
309            }
310
311            for argument in &argument_list.arguments {
312                if let Argument::Named(named_argument) = argument
313                    && names.contains(&named_argument.name.value)
314                {
315                    return Some(&named_argument.value);
316                }
317            }
318
319            None
320        }
321        InvocationArgumentsSource::PartialArgumentList(partial_argument_list) => {
322            if let Some(PartialArgument::Positional(argument)) = partial_argument_list.arguments.get(index) {
323                return Some(&argument.value);
324            }
325
326            for argument in &partial_argument_list.arguments {
327                if let PartialArgument::Named(named_argument) = argument
328                    && names.contains(&named_argument.name.value)
329                {
330                    return Some(&named_argument.value);
331                }
332            }
333
334            None
335        }
336        InvocationArgumentsSource::PipeInput(pipe) => {
337            if index == 0 {
338                Some(pipe.input)
339            } else {
340                None
341            }
342        }
343        InvocationArgumentsSource::None(_) => None,
344    }
345}