1use 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 #[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#[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 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 #[inline]
239 pub fn codebase(&self) -> &'ctx CodebaseMetadata {
240 self.codebase
241 }
242
243 #[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 #[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 #[inline]
263 pub fn get_variable_type(&self, name: &[u8]) -> Option<&Rc<TUnion>> {
264 self.block_context.locals.get(&word(name))
265 }
266
267 #[inline]
269 pub fn scope(&self) -> &ScopeContext<'ctx> {
270 &self.block_context.scope
271 }
272
273 #[inline]
275 pub fn is_instance_of(&self, class: &[u8], parent: &[u8]) -> bool {
276 self.codebase.is_instance_of(class, parent)
277 }
278
279 #[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 #[inline]
291 pub fn get_class_like(&self, name: Word) -> Option<&ClassLikeMetadata> {
292 self.codebase.get_class_like(name.as_bytes())
293 }
294
295 #[inline]
297 pub fn current_class_name(&self) -> Option<Word> {
298 self.block_context.scope.get_class_like_name()
299 }
300
301 #[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 #[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 #[inline]
318 pub fn artifacts_mut(&mut self) -> &mut AnalysisArtifacts {
319 self.artifacts
320 }
321
322 #[inline]
324 pub fn artifacts(&self) -> &AnalysisArtifacts {
325 self.artifacts
326 }
327
328 #[inline]
330 pub fn block_context_mut(&mut self) -> &mut BlockContext<'ctx> {
331 self.block_context
332 }
333
334 #[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}