1use std::sync::Arc;
3
4use php_ast::ast::{
5 AssignOp, BinaryOp, CastKind, ExprKind, MagicConstKind, UnaryPostfixOp, UnaryPrefixOp,
6};
7
8use mir_codebase::Codebase;
9use mir_issues::{Issue, IssueBuffer, IssueKind, Location, Severity};
10use mir_types::{Atomic, Union};
11
12use crate::call::CallAnalyzer;
13use crate::context::Context;
14use crate::php_version::PhpVersion;
15use crate::symbol::{ResolvedSymbol, SymbolKind};
16
17pub struct ExpressionAnalyzer<'a> {
22 pub codebase: &'a Codebase,
23 pub file: Arc<str>,
24 pub source: &'a str,
25 pub source_map: &'a php_rs_parser::source_map::SourceMap,
26 pub issues: &'a mut IssueBuffer,
27 pub symbols: &'a mut Vec<ResolvedSymbol>,
28 pub php_version: PhpVersion,
29}
30
31impl<'a> ExpressionAnalyzer<'a> {
32 pub fn new(
33 codebase: &'a Codebase,
34 file: Arc<str>,
35 source: &'a str,
36 source_map: &'a php_rs_parser::source_map::SourceMap,
37 issues: &'a mut IssueBuffer,
38 symbols: &'a mut Vec<ResolvedSymbol>,
39 php_version: PhpVersion,
40 ) -> Self {
41 Self {
42 codebase,
43 file,
44 source,
45 source_map,
46 issues,
47 symbols,
48 php_version,
49 }
50 }
51
52 pub fn record_symbol(&mut self, span: php_ast::Span, kind: SymbolKind, resolved_type: Union) {
54 self.symbols.push(ResolvedSymbol {
55 file: self.file.clone(),
56 span,
57 kind,
58 resolved_type,
59 });
60 }
61
62 pub fn analyze<'arena, 'src>(
63 &mut self,
64 expr: &php_ast::ast::Expr<'arena, 'src>,
65 ctx: &mut Context,
66 ) -> Union {
67 match &expr.kind {
68 ExprKind::Int(n) => Union::single(Atomic::TLiteralInt(*n)),
70 ExprKind::Float(f) => {
71 let bits = f.to_bits();
72 Union::single(Atomic::TLiteralFloat(
73 (bits >> 32) as i64,
74 (bits & 0xFFFF_FFFF) as i64,
75 ))
76 }
77 ExprKind::String(s) => Union::single(Atomic::TLiteralString((*s).into())),
78 ExprKind::Bool(b) => {
79 if *b {
80 Union::single(Atomic::TTrue)
81 } else {
82 Union::single(Atomic::TFalse)
83 }
84 }
85 ExprKind::Null => Union::single(Atomic::TNull),
86
87 ExprKind::InterpolatedString(parts) | ExprKind::Heredoc { parts, .. } => {
89 for part in parts.iter() {
90 if let php_ast::StringPart::Expr(e) = part {
91 self.analyze(e, ctx);
92 }
93 }
94 Union::single(Atomic::TString)
95 }
96
97 ExprKind::Nowdoc { .. } => Union::single(Atomic::TString),
98 ExprKind::ShellExec(_) => Union::single(Atomic::TString),
99
100 ExprKind::Variable(name) => {
102 let name_str = name.as_str().trim_start_matches('$');
103 if !ctx.var_is_defined(name_str) {
104 if ctx.var_possibly_defined(name_str) {
105 self.emit(
106 IssueKind::PossiblyUndefinedVariable {
107 name: name_str.to_string(),
108 },
109 Severity::Warning,
110 expr.span,
111 );
112 } else if name_str == "this" {
113 self.emit(
114 IssueKind::InvalidScope {
115 in_class: ctx.self_fqcn.is_some(),
116 },
117 Severity::Error,
118 expr.span,
119 );
120 } else {
121 self.emit(
122 IssueKind::UndefinedVariable {
123 name: name_str.to_string(),
124 },
125 Severity::Error,
126 expr.span,
127 );
128 }
129 }
130 ctx.read_vars.insert(name_str.to_string());
131 let ty = if name_str == "this" && !ctx.var_is_defined("this") {
132 Union::never()
133 } else {
134 ctx.get_var(name_str)
135 };
136 self.record_symbol(
137 expr.span,
138 SymbolKind::Variable(name_str.to_string()),
139 ty.clone(),
140 );
141 ty
142 }
143
144 ExprKind::VariableVariable(_) => Union::mixed(), ExprKind::Identifier(name) => {
147 let name_str: &str = name.as_ref();
149
150 let name_str = name_str.strip_prefix('\\').unwrap_or(name_str);
152
153 let found = {
155 let ns_qualified = self
156 .codebase
157 .file_namespaces
158 .get(self.file.as_ref())
159 .map(|ns| format!("{}\\{}", *ns, name_str));
160
161 ns_qualified
162 .as_deref()
163 .map(|q| self.codebase.constants.contains_key(q))
164 .unwrap_or(false)
165 || self.codebase.constants.contains_key(name_str)
166 };
167
168 if !found {
169 self.emit(
170 IssueKind::UndefinedConstant {
171 name: name_str.to_string(),
172 },
173 Severity::Error,
174 expr.span,
175 );
176 }
177 Union::mixed()
178 }
179
180 ExprKind::Assign(a) => {
182 let rhs_tainted = crate::taint::is_expr_tainted(a.value, ctx);
183 let rhs_ty = self.analyze(a.value, ctx);
184 if rhs_ty.is_never() {
185 return rhs_ty;
186 }
187 match a.op {
188 AssignOp::Assign => {
189 self.assign_to_target(a.target, rhs_ty.clone(), ctx, expr.span);
190 if rhs_tainted {
192 if let ExprKind::Variable(name) = &a.target.kind {
193 ctx.taint_var(name.as_ref());
194 }
195 }
196 rhs_ty
197 }
198 AssignOp::Concat => {
199 if let Some(var_name) = extract_simple_var(a.target) {
201 ctx.set_var(&var_name, Union::single(Atomic::TString));
202 }
203 Union::single(Atomic::TString)
204 }
205 AssignOp::Plus
206 | AssignOp::Minus
207 | AssignOp::Mul
208 | AssignOp::Div
209 | AssignOp::Mod
210 | AssignOp::Pow => {
211 let lhs_ty = self.analyze(a.target, ctx);
212 let result_ty = infer_arithmetic(&lhs_ty, &rhs_ty);
213 if let Some(var_name) = extract_simple_var(a.target) {
214 ctx.set_var(&var_name, result_ty.clone());
215 }
216 result_ty
217 }
218 AssignOp::Coalesce => {
219 let lhs_ty = self.analyze(a.target, ctx);
221 let merged = Union::merge(&lhs_ty.remove_null(), &rhs_ty);
222 if let Some(var_name) = extract_simple_var(a.target) {
223 ctx.set_var(&var_name, merged.clone());
224 }
225 merged
226 }
227 _ => {
228 if let Some(var_name) = extract_simple_var(a.target) {
229 ctx.set_var(&var_name, Union::mixed());
230 }
231 Union::mixed()
232 }
233 }
234 }
235
236 ExprKind::Binary(b) => self.analyze_binary(b, expr.span, ctx),
238
239 ExprKind::UnaryPrefix(u) => {
241 let operand_ty = self.analyze(u.operand, ctx);
242 match u.op {
243 UnaryPrefixOp::BooleanNot => Union::single(Atomic::TBool),
244 UnaryPrefixOp::Negate => {
245 if operand_ty.contains(|t| t.is_int()) {
246 Union::single(Atomic::TInt)
247 } else {
248 Union::single(Atomic::TFloat)
249 }
250 }
251 UnaryPrefixOp::Plus => operand_ty,
252 UnaryPrefixOp::BitwiseNot => Union::single(Atomic::TInt),
253 UnaryPrefixOp::PreIncrement | UnaryPrefixOp::PreDecrement => {
254 if let Some(var_name) = extract_simple_var(u.operand) {
256 let ty = ctx.get_var(&var_name);
257 let new_ty = if ty.contains(|t| {
258 matches!(t, Atomic::TFloat | Atomic::TLiteralFloat(..))
259 }) {
260 Union::single(Atomic::TFloat)
261 } else {
262 Union::single(Atomic::TInt)
263 };
264 ctx.set_var(&var_name, new_ty.clone());
265 new_ty
266 } else {
267 Union::single(Atomic::TInt)
268 }
269 }
270 }
271 }
272
273 ExprKind::UnaryPostfix(u) => {
274 let operand_ty = self.analyze(u.operand, ctx);
275 match u.op {
277 UnaryPostfixOp::PostIncrement | UnaryPostfixOp::PostDecrement => {
278 if let Some(var_name) = extract_simple_var(u.operand) {
279 let new_ty = if operand_ty.contains(|t| {
280 matches!(t, Atomic::TFloat | Atomic::TLiteralFloat(..))
281 }) {
282 Union::single(Atomic::TFloat)
283 } else {
284 Union::single(Atomic::TInt)
285 };
286 ctx.set_var(&var_name, new_ty);
287 }
288 operand_ty }
290 }
291 }
292
293 ExprKind::Ternary(t) => {
295 let cond_ty = self.analyze(t.condition, ctx);
296 match &t.then_expr {
297 Some(then_expr) => {
298 let mut then_ctx = ctx.fork();
299 crate::narrowing::narrow_from_condition(
300 t.condition,
301 &mut then_ctx,
302 true,
303 self.codebase,
304 &self.file,
305 );
306 let then_ty = self.analyze(then_expr, &mut then_ctx);
307
308 let mut else_ctx = ctx.fork();
309 crate::narrowing::narrow_from_condition(
310 t.condition,
311 &mut else_ctx,
312 false,
313 self.codebase,
314 &self.file,
315 );
316 let else_ty = self.analyze(t.else_expr, &mut else_ctx);
317
318 for name in then_ctx.read_vars.iter().chain(else_ctx.read_vars.iter()) {
320 ctx.read_vars.insert(name.clone());
321 }
322
323 Union::merge(&then_ty, &else_ty)
324 }
325 None => {
326 let else_ty = self.analyze(t.else_expr, ctx);
328 let truthy_ty = cond_ty.narrow_to_truthy();
329 if truthy_ty.is_empty() {
330 else_ty
331 } else {
332 Union::merge(&truthy_ty, &else_ty)
333 }
334 }
335 }
336 }
337
338 ExprKind::NullCoalesce(nc) => {
339 let left_ty = self.analyze(nc.left, ctx);
340 let right_ty = self.analyze(nc.right, ctx);
341 let non_null_left = left_ty.remove_null();
343 if non_null_left.is_empty() {
344 right_ty
345 } else {
346 Union::merge(&non_null_left, &right_ty)
347 }
348 }
349
350 ExprKind::Cast(kind, inner) => {
352 let _inner_ty = self.analyze(inner, ctx);
353 match kind {
354 CastKind::Int => Union::single(Atomic::TInt),
355 CastKind::Float => Union::single(Atomic::TFloat),
356 CastKind::String => Union::single(Atomic::TString),
357 CastKind::Bool => Union::single(Atomic::TBool),
358 CastKind::Array => Union::single(Atomic::TArray {
359 key: Box::new(Union::single(Atomic::TMixed)),
360 value: Box::new(Union::mixed()),
361 }),
362 CastKind::Object => Union::single(Atomic::TObject),
363 CastKind::Unset | CastKind::Void => Union::single(Atomic::TNull),
364 }
365 }
366
367 ExprKind::ErrorSuppress(inner) => self.analyze(inner, ctx),
369
370 ExprKind::Parenthesized(inner) => self.analyze(inner, ctx),
372
373 ExprKind::Array(elements) => {
375 use mir_types::atomic::{ArrayKey, KeyedProperty};
376
377 if elements.is_empty() {
378 return Union::single(Atomic::TKeyedArray {
379 properties: indexmap::IndexMap::new(),
380 is_open: false,
381 is_list: true,
382 });
383 }
384
385 let mut keyed_props: indexmap::IndexMap<ArrayKey, KeyedProperty> =
388 indexmap::IndexMap::new();
389 let mut is_list = true;
390 let mut can_be_keyed = true;
391 let mut next_int_key: i64 = 0;
392
393 for elem in elements.iter() {
394 if elem.unpack {
395 self.analyze(&elem.value, ctx);
396 can_be_keyed = false;
397 break;
398 }
399 let value_ty = self.analyze(&elem.value, ctx);
400 let array_key = if let Some(key_expr) = &elem.key {
401 is_list = false;
402 let key_ty = self.analyze(key_expr, ctx);
403 match key_ty.types.as_slice() {
405 [Atomic::TLiteralString(s)] => ArrayKey::String(s.clone()),
406 [Atomic::TLiteralInt(i)] => {
407 next_int_key = *i + 1;
408 ArrayKey::Int(*i)
409 }
410 _ => {
411 can_be_keyed = false;
412 break;
413 }
414 }
415 } else {
416 let k = ArrayKey::Int(next_int_key);
417 next_int_key += 1;
418 k
419 };
420 keyed_props.insert(
421 array_key,
422 KeyedProperty {
423 ty: value_ty,
424 optional: false,
425 },
426 );
427 }
428
429 if can_be_keyed {
430 return Union::single(Atomic::TKeyedArray {
431 properties: keyed_props,
432 is_open: false,
433 is_list,
434 });
435 }
436
437 let mut all_value_types = Union::empty();
439 let mut key_union = Union::empty();
440 let mut has_unpack = false;
441 for elem in elements.iter() {
442 let value_ty = self.analyze(&elem.value, ctx);
443 if elem.unpack {
444 has_unpack = true;
445 } else {
446 all_value_types = Union::merge(&all_value_types, &value_ty);
447 if let Some(key_expr) = &elem.key {
448 let key_ty = self.analyze(key_expr, ctx);
449 key_union = Union::merge(&key_union, &key_ty);
450 } else {
451 key_union.add_type(Atomic::TInt);
452 }
453 }
454 }
455 if has_unpack {
456 return Union::single(Atomic::TArray {
457 key: Box::new(Union::single(Atomic::TMixed)),
458 value: Box::new(Union::mixed()),
459 });
460 }
461 if key_union.is_empty() {
462 key_union.add_type(Atomic::TInt);
463 }
464 Union::single(Atomic::TArray {
465 key: Box::new(key_union),
466 value: Box::new(all_value_types),
467 })
468 }
469
470 ExprKind::ArrayAccess(aa) => {
472 let arr_ty = self.analyze(aa.array, ctx);
473
474 if let Some(idx) = &aa.index {
476 self.analyze(idx, ctx);
477 }
478
479 if arr_ty.contains(|t| matches!(t, Atomic::TNull)) && arr_ty.is_single() {
481 self.emit(IssueKind::NullArrayAccess, Severity::Error, expr.span);
482 return Union::mixed();
483 }
484 if arr_ty.is_nullable() {
485 self.emit(
486 IssueKind::PossiblyNullArrayAccess,
487 Severity::Info,
488 expr.span,
489 );
490 }
491
492 let literal_key: Option<mir_types::atomic::ArrayKey> =
494 aa.index.as_ref().and_then(|idx| match &idx.kind {
495 ExprKind::String(s) => {
496 Some(mir_types::atomic::ArrayKey::String(Arc::from(&**s)))
497 }
498 ExprKind::Int(i) => Some(mir_types::atomic::ArrayKey::Int(*i)),
499 _ => None,
500 });
501
502 for atomic in &arr_ty.types {
504 match atomic {
505 Atomic::TKeyedArray { properties, .. } => {
506 if let Some(ref key) = literal_key {
508 if let Some(prop) = properties.get(key) {
509 return prop.ty.clone();
510 }
511 }
512 let mut result = Union::empty();
514 for prop in properties.values() {
515 result = Union::merge(&result, &prop.ty);
516 }
517 return if result.types.is_empty() {
518 Union::mixed()
519 } else {
520 result
521 };
522 }
523 Atomic::TArray { value, .. } | Atomic::TNonEmptyArray { value, .. } => {
524 return *value.clone();
525 }
526 Atomic::TList { value } | Atomic::TNonEmptyList { value } => {
527 return *value.clone();
528 }
529 Atomic::TString | Atomic::TLiteralString(_) => {
530 return Union::single(Atomic::TString);
531 }
532 _ => {}
533 }
534 }
535 Union::mixed()
536 }
537
538 ExprKind::Isset(exprs) => {
540 for e in exprs.iter() {
541 self.analyze(e, ctx);
542 }
543 Union::single(Atomic::TBool)
544 }
545 ExprKind::Empty(inner) => {
546 self.analyze(inner, ctx);
547 Union::single(Atomic::TBool)
548 }
549
550 ExprKind::Print(inner) => {
552 self.analyze(inner, ctx);
553 Union::single(Atomic::TLiteralInt(1))
554 }
555
556 ExprKind::Clone(inner) => self.analyze(inner, ctx),
558 ExprKind::CloneWith(inner, _props) => self.analyze(inner, ctx),
559
560 ExprKind::New(n) => {
562 let arg_types: Vec<Union> = n
564 .args
565 .iter()
566 .map(|a| {
567 let ty = self.analyze(&a.value, ctx);
568 if a.unpack {
569 crate::call::spread_element_type(&ty)
570 } else {
571 ty
572 }
573 })
574 .collect();
575 let arg_spans: Vec<php_ast::Span> = n.args.iter().map(|a| a.span).collect();
576 let arg_names: Vec<Option<String>> = n
577 .args
578 .iter()
579 .map(|a| a.name.as_ref().map(|nm| nm.to_string_repr().into_owned()))
580 .collect();
581
582 let class_ty = match &n.class.kind {
583 ExprKind::Identifier(name) => {
584 let resolved = self.codebase.resolve_class_name(&self.file, name.as_ref());
585 let fqcn: Arc<str> = match resolved.as_str() {
587 "self" | "static" => ctx
588 .self_fqcn
589 .clone()
590 .or_else(|| ctx.static_fqcn.clone())
591 .unwrap_or_else(|| Arc::from(resolved.as_str())),
592 "parent" => ctx
593 .parent_fqcn
594 .clone()
595 .unwrap_or_else(|| Arc::from(resolved.as_str())),
596 _ => Arc::from(resolved.as_str()),
597 };
598 if !matches!(resolved.as_str(), "self" | "static" | "parent")
599 && !self.codebase.type_exists(&fqcn)
600 {
601 self.emit(
602 IssueKind::UndefinedClass {
603 name: resolved.clone(),
604 },
605 Severity::Error,
606 n.class.span,
607 );
608 } else if self.codebase.type_exists(&fqcn) {
609 if let Some(cls) = self.codebase.classes.get(fqcn.as_ref()) {
610 if let Some(msg) = cls.deprecated.clone() {
611 self.emit(
612 IssueKind::DeprecatedClass {
613 name: fqcn.to_string(),
614 message: Some(msg).filter(|m| !m.is_empty()),
615 },
616 Severity::Info,
617 n.class.span,
618 );
619 }
620 }
621 if let Some(ctor) = self.codebase.get_method(&fqcn, "__construct") {
623 crate::call::check_constructor_args(
624 self,
625 &fqcn,
626 crate::call::CheckArgsParams {
627 fn_name: "__construct",
628 params: &ctor.params,
629 arg_types: &arg_types,
630 arg_spans: &arg_spans,
631 arg_names: &arg_names,
632 call_span: expr.span,
633 has_spread: n.args.iter().any(|a| a.unpack),
634 },
635 );
636 }
637 }
638 let ty = Union::single(Atomic::TNamedObject {
639 fqcn: fqcn.clone(),
640 type_params: vec![],
641 });
642 self.record_symbol(
643 n.class.span,
644 SymbolKind::ClassReference(fqcn.clone()),
645 ty.clone(),
646 );
647 self.codebase.mark_class_referenced_at(
650 &fqcn,
651 self.file.clone(),
652 n.class.span.start,
653 n.class.span.end,
654 );
655 ty
656 }
657 _ => {
658 self.analyze(n.class, ctx);
659 Union::single(Atomic::TObject)
660 }
661 };
662 class_ty
663 }
664
665 ExprKind::AnonymousClass(_) => Union::single(Atomic::TObject),
666
667 ExprKind::PropertyAccess(pa) => {
669 let obj_ty = self.analyze(pa.object, ctx);
670 let prop_name = extract_string_from_expr(pa.property)
671 .unwrap_or_else(|| "<dynamic>".to_string());
672
673 if obj_ty.contains(|t| matches!(t, Atomic::TNull)) && obj_ty.is_single() {
674 self.emit(
675 IssueKind::NullPropertyFetch {
676 property: prop_name.clone(),
677 },
678 Severity::Error,
679 expr.span,
680 );
681 return Union::mixed();
682 }
683 if obj_ty.is_nullable() {
684 self.emit(
685 IssueKind::PossiblyNullPropertyFetch {
686 property: prop_name.clone(),
687 },
688 Severity::Info,
689 expr.span,
690 );
691 }
692
693 if prop_name == "<dynamic>" {
695 return Union::mixed();
696 }
697 let resolved = self.resolve_property_type(&obj_ty, &prop_name, pa.property.span);
700 for atomic in &obj_ty.types {
702 if let Atomic::TNamedObject { fqcn, .. } = atomic {
703 self.record_symbol(
704 pa.property.span,
705 SymbolKind::PropertyAccess {
706 class: fqcn.clone(),
707 property: Arc::from(prop_name.as_str()),
708 },
709 resolved.clone(),
710 );
711 break;
712 }
713 }
714 resolved
715 }
716
717 ExprKind::NullsafePropertyAccess(pa) => {
718 let obj_ty = self.analyze(pa.object, ctx);
719 let prop_name = extract_string_from_expr(pa.property)
720 .unwrap_or_else(|| "<dynamic>".to_string());
721 if prop_name == "<dynamic>" {
722 return Union::mixed();
723 }
724 let non_null_ty = obj_ty.remove_null();
726 let mut prop_ty =
729 self.resolve_property_type(&non_null_ty, &prop_name, pa.property.span);
730 prop_ty.add_type(Atomic::TNull); for atomic in &non_null_ty.types {
733 if let Atomic::TNamedObject { fqcn, .. } = atomic {
734 self.record_symbol(
735 pa.property.span,
736 SymbolKind::PropertyAccess {
737 class: fqcn.clone(),
738 property: Arc::from(prop_name.as_str()),
739 },
740 prop_ty.clone(),
741 );
742 break;
743 }
744 }
745 prop_ty
746 }
747
748 ExprKind::StaticPropertyAccess(spa) => {
749 if let ExprKind::Identifier(id) = &spa.class.kind {
750 let resolved = self.codebase.resolve_class_name(&self.file, id.as_ref());
751 if !matches!(resolved.as_str(), "self" | "static" | "parent")
752 && !self.codebase.type_exists(&resolved)
753 {
754 self.emit(
755 IssueKind::UndefinedClass { name: resolved },
756 Severity::Error,
757 spa.class.span,
758 );
759 }
760 }
761 Union::mixed()
762 }
763
764 ExprKind::ClassConstAccess(cca) => {
765 if cca.member.name_str() == Some("class") {
767 let fqcn = if let ExprKind::Identifier(id) = &cca.class.kind {
769 let resolved = self.codebase.resolve_class_name(&self.file, id.as_ref());
770 Some(Arc::from(resolved.as_str()))
771 } else {
772 None
773 };
774 return Union::single(Atomic::TClassString(fqcn));
775 }
776
777 let const_name = match cca.member.name_str() {
778 Some(n) => n.to_string(),
779 None => return Union::mixed(),
780 };
781
782 let fqcn = match &cca.class.kind {
783 ExprKind::Identifier(id) => {
784 let resolved = self.codebase.resolve_class_name(&self.file, id.as_ref());
785 if matches!(resolved.as_str(), "self" | "static" | "parent") {
787 return Union::mixed();
788 }
789 resolved
790 }
791 _ => return Union::mixed(),
792 };
793
794 if !self.codebase.type_exists(&fqcn) {
795 self.emit(
796 IssueKind::UndefinedClass { name: fqcn },
797 Severity::Error,
798 cca.class.span,
799 );
800 return Union::mixed();
801 }
802
803 if self
804 .codebase
805 .get_class_constant(&fqcn, &const_name)
806 .is_none()
807 && !self.codebase.has_unknown_ancestor(&fqcn)
808 {
809 self.emit(
810 IssueKind::UndefinedConstant {
811 name: format!("{}::{}", fqcn, const_name),
812 },
813 Severity::Error,
814 expr.span,
815 );
816 }
817 Union::mixed()
818 }
819
820 ExprKind::ClassConstAccessDynamic { .. } => Union::mixed(),
821 ExprKind::StaticPropertyAccessDynamic { .. } => Union::mixed(),
822
823 ExprKind::MethodCall(mc) => {
825 CallAnalyzer::analyze_method_call(self, mc, ctx, expr.span, false)
826 }
827
828 ExprKind::NullsafeMethodCall(mc) => {
829 CallAnalyzer::analyze_method_call(self, mc, ctx, expr.span, true)
830 }
831
832 ExprKind::StaticMethodCall(smc) => {
833 CallAnalyzer::analyze_static_method_call(self, smc, ctx, expr.span)
834 }
835
836 ExprKind::StaticDynMethodCall(smc) => {
837 CallAnalyzer::analyze_static_dyn_method_call(self, smc, ctx)
838 }
839
840 ExprKind::FunctionCall(fc) => {
842 CallAnalyzer::analyze_function_call(self, fc, ctx, expr.span)
843 }
844
845 ExprKind::Closure(c) => {
847 for param in c.params.iter() {
849 if let Some(hint) = ¶m.type_hint {
850 self.check_type_hint(hint);
851 }
852 }
853 if let Some(hint) = &c.return_type {
854 self.check_type_hint(hint);
855 }
856
857 let params = ast_params_to_fn_params_resolved(
858 &c.params,
859 ctx.self_fqcn.as_deref(),
860 self.codebase,
861 &self.file,
862 );
863 let return_ty_hint = c
864 .return_type
865 .as_ref()
866 .map(|h| crate::parser::type_from_hint(h, ctx.self_fqcn.as_deref()))
867 .map(|u| resolve_named_objects_in_union(u, self.codebase, &self.file));
868
869 let mut closure_ctx = crate::context::Context::for_function(
873 ¶ms,
874 return_ty_hint.clone(),
875 ctx.self_fqcn.clone(),
876 ctx.parent_fqcn.clone(),
877 ctx.static_fqcn.clone(),
878 ctx.strict_types,
879 c.is_static,
880 );
881 for use_var in c.use_vars.iter() {
882 let name = use_var.name.trim_start_matches('$');
883 closure_ctx.set_var(name, ctx.get_var(name));
884 if ctx.is_tainted(name) {
885 closure_ctx.taint_var(name);
886 }
887 }
888
889 let inferred_return = {
891 let mut sa = crate::stmt::StatementsAnalyzer::new(
892 self.codebase,
893 self.file.clone(),
894 self.source,
895 self.source_map,
896 self.issues,
897 self.symbols,
898 self.php_version,
899 );
900 sa.analyze_stmts(&c.body, &mut closure_ctx);
901 let ret = crate::project::merge_return_types(&sa.return_types);
902 drop(sa);
903 ret
904 };
905
906 for name in &closure_ctx.read_vars {
908 ctx.read_vars.insert(name.clone());
909 }
910
911 let return_ty = return_ty_hint.unwrap_or(inferred_return);
912 let closure_params: Vec<mir_types::atomic::FnParam> = params
913 .iter()
914 .map(|p| mir_types::atomic::FnParam {
915 name: p.name.clone(),
916 ty: p.ty.clone(),
917 default: p.default.clone(),
918 is_variadic: p.is_variadic,
919 is_byref: p.is_byref,
920 is_optional: p.is_optional,
921 })
922 .collect();
923
924 Union::single(Atomic::TClosure {
925 params: closure_params,
926 return_type: Box::new(return_ty),
927 this_type: ctx.self_fqcn.clone().map(|f| {
928 Box::new(Union::single(Atomic::TNamedObject {
929 fqcn: f,
930 type_params: vec![],
931 }))
932 }),
933 })
934 }
935
936 ExprKind::ArrowFunction(af) => {
937 for param in af.params.iter() {
939 if let Some(hint) = ¶m.type_hint {
940 self.check_type_hint(hint);
941 }
942 }
943 if let Some(hint) = &af.return_type {
944 self.check_type_hint(hint);
945 }
946
947 let params = ast_params_to_fn_params_resolved(
948 &af.params,
949 ctx.self_fqcn.as_deref(),
950 self.codebase,
951 &self.file,
952 );
953 let return_ty_hint = af
954 .return_type
955 .as_ref()
956 .map(|h| crate::parser::type_from_hint(h, ctx.self_fqcn.as_deref()))
957 .map(|u| resolve_named_objects_in_union(u, self.codebase, &self.file));
958
959 let mut arrow_ctx = crate::context::Context::for_function(
962 ¶ms,
963 return_ty_hint.clone(),
964 ctx.self_fqcn.clone(),
965 ctx.parent_fqcn.clone(),
966 ctx.static_fqcn.clone(),
967 ctx.strict_types,
968 af.is_static,
969 );
970 for (name, ty) in &ctx.vars {
972 if !arrow_ctx.vars.contains_key(name) {
973 arrow_ctx.set_var(name, ty.clone());
974 }
975 }
976
977 let inferred_return = self.analyze(af.body, &mut arrow_ctx);
979
980 for name in &arrow_ctx.read_vars {
982 ctx.read_vars.insert(name.clone());
983 }
984
985 let return_ty = return_ty_hint.unwrap_or(inferred_return);
986 let closure_params: Vec<mir_types::atomic::FnParam> = params
987 .iter()
988 .map(|p| mir_types::atomic::FnParam {
989 name: p.name.clone(),
990 ty: p.ty.clone(),
991 default: p.default.clone(),
992 is_variadic: p.is_variadic,
993 is_byref: p.is_byref,
994 is_optional: p.is_optional,
995 })
996 .collect();
997
998 Union::single(Atomic::TClosure {
999 params: closure_params,
1000 return_type: Box::new(return_ty),
1001 this_type: if af.is_static {
1002 None
1003 } else {
1004 ctx.self_fqcn.clone().map(|f| {
1005 Box::new(Union::single(Atomic::TNamedObject {
1006 fqcn: f,
1007 type_params: vec![],
1008 }))
1009 })
1010 },
1011 })
1012 }
1013
1014 ExprKind::CallableCreate(_) => Union::single(Atomic::TCallable {
1015 params: None,
1016 return_type: None,
1017 }),
1018
1019 ExprKind::Match(m) => {
1021 let subject_ty = self.analyze(m.subject, ctx);
1022 let subject_var = match &m.subject.kind {
1024 ExprKind::Variable(name) => {
1025 Some(name.as_str().trim_start_matches('$').to_string())
1026 }
1027 _ => None,
1028 };
1029
1030 let mut result = Union::empty();
1031 for arm in m.arms.iter() {
1032 let mut arm_ctx = ctx.fork();
1034
1035 if let (Some(var), Some(conditions)) = (&subject_var, &arm.conditions) {
1037 let mut arm_ty = Union::empty();
1039 for cond in conditions.iter() {
1040 let cond_ty = self.analyze(cond, ctx);
1041 arm_ty = Union::merge(&arm_ty, &cond_ty);
1042 }
1043 if !arm_ty.is_empty() && !arm_ty.is_mixed() {
1045 let narrowed = subject_ty.intersect_with(&arm_ty);
1047 if !narrowed.is_empty() {
1048 arm_ctx.set_var(var, narrowed);
1049 }
1050 }
1051 }
1052
1053 if let Some(conditions) = &arm.conditions {
1056 for cond in conditions.iter() {
1057 crate::narrowing::narrow_from_condition(
1058 cond,
1059 &mut arm_ctx,
1060 true,
1061 self.codebase,
1062 &self.file,
1063 );
1064 }
1065 }
1066
1067 let arm_body_ty = self.analyze(&arm.body, &mut arm_ctx);
1068 result = Union::merge(&result, &arm_body_ty);
1069
1070 for name in &arm_ctx.read_vars {
1072 ctx.read_vars.insert(name.clone());
1073 }
1074 }
1075 if result.is_empty() {
1076 Union::mixed()
1077 } else {
1078 result
1079 }
1080 }
1081
1082 ExprKind::ThrowExpr(e) => {
1084 self.analyze(e, ctx);
1085 Union::single(Atomic::TNever)
1086 }
1087
1088 ExprKind::Yield(y) => {
1090 if let Some(key) = &y.key {
1091 self.analyze(key, ctx);
1092 }
1093 if let Some(value) = &y.value {
1094 self.analyze(value, ctx);
1095 }
1096 Union::mixed()
1097 }
1098
1099 ExprKind::MagicConst(kind) => match kind {
1101 MagicConstKind::Line => Union::single(Atomic::TInt),
1102 MagicConstKind::File
1103 | MagicConstKind::Dir
1104 | MagicConstKind::Function
1105 | MagicConstKind::Class
1106 | MagicConstKind::Method
1107 | MagicConstKind::Namespace
1108 | MagicConstKind::Trait
1109 | MagicConstKind::Property => Union::single(Atomic::TString),
1110 },
1111
1112 ExprKind::Include(_, inner) => {
1114 self.analyze(inner, ctx);
1115 Union::mixed()
1116 }
1117
1118 ExprKind::Eval(inner) => {
1120 self.analyze(inner, ctx);
1121 Union::mixed()
1122 }
1123
1124 ExprKind::Exit(opt) => {
1126 if let Some(e) = opt {
1127 self.analyze(e, ctx);
1128 }
1129 ctx.diverges = true;
1130 Union::single(Atomic::TNever)
1131 }
1132
1133 ExprKind::Error => Union::mixed(),
1135
1136 ExprKind::Omit => Union::single(Atomic::TNull),
1138 }
1139 }
1140
1141 fn analyze_binary<'arena, 'src>(
1146 &mut self,
1147 b: &php_ast::ast::BinaryExpr<'arena, 'src>,
1148 _span: php_ast::Span,
1149 ctx: &mut Context,
1150 ) -> Union {
1151 use php_ast::ast::BinaryOp as B;
1157 if matches!(
1158 b.op,
1159 B::BooleanAnd | B::LogicalAnd | B::BooleanOr | B::LogicalOr
1160 ) {
1161 let _left_ty = self.analyze(b.left, ctx);
1162 let mut right_ctx = ctx.fork();
1163 let is_and = matches!(b.op, B::BooleanAnd | B::LogicalAnd);
1164 crate::narrowing::narrow_from_condition(
1165 b.left,
1166 &mut right_ctx,
1167 is_and,
1168 self.codebase,
1169 &self.file,
1170 );
1171 if !right_ctx.diverges {
1174 let _right_ty = self.analyze(b.right, &mut right_ctx);
1175 }
1176 for v in right_ctx.read_vars {
1180 ctx.read_vars.insert(v.clone());
1181 }
1182 for (name, ty) in &right_ctx.vars {
1183 if !ctx.vars.contains_key(name.as_str()) {
1184 ctx.vars.insert(name.clone(), ty.clone());
1186 ctx.possibly_assigned_vars.insert(name.clone());
1187 }
1188 }
1189 return Union::single(Atomic::TBool);
1190 }
1191
1192 if b.op == B::Instanceof {
1194 let _left_ty = self.analyze(b.left, ctx);
1195 if let ExprKind::Identifier(name) = &b.right.kind {
1196 let resolved = self.codebase.resolve_class_name(&self.file, name.as_ref());
1197 let fqcn: std::sync::Arc<str> = std::sync::Arc::from(resolved.as_str());
1198 if !matches!(resolved.as_str(), "self" | "static" | "parent")
1199 && !self.codebase.type_exists(&fqcn)
1200 {
1201 self.emit(
1202 IssueKind::UndefinedClass { name: resolved },
1203 Severity::Error,
1204 b.right.span,
1205 );
1206 }
1207 }
1208 return Union::single(Atomic::TBool);
1209 }
1210
1211 let left_ty = self.analyze(b.left, ctx);
1212 let right_ty = self.analyze(b.right, ctx);
1213
1214 match b.op {
1215 BinaryOp::Add
1217 | BinaryOp::Sub
1218 | BinaryOp::Mul
1219 | BinaryOp::Div
1220 | BinaryOp::Mod
1221 | BinaryOp::Pow => infer_arithmetic(&left_ty, &right_ty),
1222
1223 BinaryOp::Concat => Union::single(Atomic::TString),
1225
1226 BinaryOp::Equal
1228 | BinaryOp::NotEqual
1229 | BinaryOp::Identical
1230 | BinaryOp::NotIdentical
1231 | BinaryOp::Less
1232 | BinaryOp::Greater
1233 | BinaryOp::LessOrEqual
1234 | BinaryOp::GreaterOrEqual => Union::single(Atomic::TBool),
1235
1236 BinaryOp::Spaceship => Union::single(Atomic::TIntRange {
1238 min: Some(-1),
1239 max: Some(1),
1240 }),
1241
1242 BinaryOp::BooleanAnd
1244 | BinaryOp::BooleanOr
1245 | BinaryOp::LogicalAnd
1246 | BinaryOp::LogicalOr
1247 | BinaryOp::LogicalXor => Union::single(Atomic::TBool),
1248
1249 BinaryOp::BitwiseAnd
1251 | BinaryOp::BitwiseOr
1252 | BinaryOp::BitwiseXor
1253 | BinaryOp::ShiftLeft
1254 | BinaryOp::ShiftRight => Union::single(Atomic::TInt),
1255
1256 BinaryOp::Pipe => right_ty,
1258
1259 BinaryOp::Instanceof => Union::single(Atomic::TBool),
1261 }
1262 }
1263
1264 fn resolve_property_type(
1269 &mut self,
1270 obj_ty: &Union,
1271 prop_name: &str,
1272 span: php_ast::Span,
1273 ) -> Union {
1274 for atomic in &obj_ty.types {
1275 match atomic {
1276 Atomic::TNamedObject { fqcn, .. }
1277 if self.codebase.classes.contains_key(fqcn.as_ref()) =>
1278 {
1279 if let Some(prop) = self.codebase.get_property(fqcn.as_ref(), prop_name) {
1280 self.codebase.mark_property_referenced_at(
1282 fqcn,
1283 prop_name,
1284 self.file.clone(),
1285 span.start,
1286 span.end,
1287 );
1288 return prop.ty.clone().unwrap_or_else(Union::mixed);
1289 }
1290 if !self.codebase.has_unknown_ancestor(fqcn.as_ref())
1292 && !self.codebase.has_magic_get(fqcn.as_ref())
1293 {
1294 self.emit(
1295 IssueKind::UndefinedProperty {
1296 class: fqcn.to_string(),
1297 property: prop_name.to_string(),
1298 },
1299 Severity::Warning,
1300 span,
1301 );
1302 }
1303 return Union::mixed();
1304 }
1305 Atomic::TMixed => return Union::mixed(),
1306 _ => {}
1307 }
1308 }
1309 Union::mixed()
1310 }
1311
1312 fn assign_to_target<'arena, 'src>(
1317 &mut self,
1318 target: &php_ast::ast::Expr<'arena, 'src>,
1319 ty: Union,
1320 ctx: &mut Context,
1321 span: php_ast::Span,
1322 ) {
1323 match &target.kind {
1324 ExprKind::Variable(name) => {
1325 let name_str = name.as_str().trim_start_matches('$').to_string();
1326 ctx.set_var(name_str, ty);
1327 }
1328 ExprKind::Array(elements) => {
1329 let has_non_array = ty.contains(|a| matches!(a, Atomic::TFalse | Atomic::TNull));
1333 let has_array = ty.contains(|a| {
1334 matches!(
1335 a,
1336 Atomic::TArray { .. }
1337 | Atomic::TList { .. }
1338 | Atomic::TNonEmptyArray { .. }
1339 | Atomic::TNonEmptyList { .. }
1340 | Atomic::TKeyedArray { .. }
1341 )
1342 });
1343 if has_non_array && has_array {
1344 let actual = format!("{}", ty);
1345 self.emit(
1346 IssueKind::PossiblyInvalidArrayOffset {
1347 expected: "array".to_string(),
1348 actual,
1349 },
1350 Severity::Warning,
1351 span,
1352 );
1353 }
1354
1355 let value_ty: Union = ty
1357 .types
1358 .iter()
1359 .find_map(|a| match a {
1360 Atomic::TArray { value, .. }
1361 | Atomic::TList { value }
1362 | Atomic::TNonEmptyArray { value, .. }
1363 | Atomic::TNonEmptyList { value } => Some(*value.clone()),
1364 _ => None,
1365 })
1366 .unwrap_or_else(Union::mixed);
1367
1368 for elem in elements.iter() {
1369 self.assign_to_target(&elem.value, value_ty.clone(), ctx, span);
1370 }
1371 }
1372 ExprKind::PropertyAccess(pa) => {
1373 let obj_ty = self.analyze(pa.object, ctx);
1375 if let Some(prop_name) = extract_string_from_expr(pa.property) {
1376 for atomic in &obj_ty.types {
1377 if let Atomic::TNamedObject { fqcn, .. } = atomic {
1378 if let Some(cls) = self.codebase.classes.get(fqcn.as_ref()) {
1379 if let Some(prop) = cls.get_property(&prop_name) {
1380 if prop.is_readonly && !ctx.inside_constructor {
1381 self.emit(
1382 IssueKind::ReadonlyPropertyAssignment {
1383 class: fqcn.to_string(),
1384 property: prop_name.clone(),
1385 },
1386 Severity::Error,
1387 span,
1388 );
1389 }
1390 }
1391 }
1392 }
1393 }
1394 }
1395 }
1396 ExprKind::StaticPropertyAccess(_) => {
1397 }
1399 ExprKind::ArrayAccess(aa) => {
1400 if let Some(idx) = &aa.index {
1403 self.analyze(idx, ctx);
1404 }
1405 let mut base = aa.array;
1408 loop {
1409 match &base.kind {
1410 ExprKind::Variable(name) => {
1411 let name_str = name.as_str().trim_start_matches('$');
1412 if !ctx.var_is_defined(name_str) {
1413 ctx.vars.insert(
1414 name_str.to_string(),
1415 Union::single(Atomic::TArray {
1416 key: Box::new(Union::mixed()),
1417 value: Box::new(ty.clone()),
1418 }),
1419 );
1420 ctx.assigned_vars.insert(name_str.to_string());
1421 } else {
1422 let current = ctx.get_var(name_str);
1425 let updated = widen_array_with_value(¤t, &ty);
1426 ctx.set_var(name_str, updated);
1427 }
1428 break;
1429 }
1430 ExprKind::ArrayAccess(inner) => {
1431 if let Some(idx) = &inner.index {
1432 self.analyze(idx, ctx);
1433 }
1434 base = inner.array;
1435 }
1436 _ => break,
1437 }
1438 }
1439 }
1440 _ => {}
1441 }
1442 }
1443
1444 fn offset_to_line_col(&self, offset: u32) -> (u32, u16) {
1451 let lc = self.source_map.offset_to_line_col(offset);
1452 let line = lc.line + 1;
1453
1454 let byte_offset = offset as usize;
1455 let line_start_byte = if byte_offset == 0 {
1456 0
1457 } else {
1458 self.source[..byte_offset]
1459 .rfind('\n')
1460 .map(|p| p + 1)
1461 .unwrap_or(0)
1462 };
1463
1464 let col = self.source[line_start_byte..byte_offset].chars().count() as u16;
1465
1466 (line, col)
1467 }
1468
1469 fn check_type_hint(&mut self, hint: &php_ast::ast::TypeHint<'_, '_>) {
1471 use php_ast::ast::TypeHintKind;
1472 match &hint.kind {
1473 TypeHintKind::Named(name) => {
1474 let name_str = crate::parser::name_to_string(name);
1475 if matches!(
1476 name_str.to_lowercase().as_str(),
1477 "self"
1478 | "static"
1479 | "parent"
1480 | "null"
1481 | "true"
1482 | "false"
1483 | "never"
1484 | "void"
1485 | "mixed"
1486 | "object"
1487 | "callable"
1488 | "iterable"
1489 ) {
1490 return;
1491 }
1492 let resolved = self.codebase.resolve_class_name(&self.file, &name_str);
1493 if !self.codebase.type_exists(&resolved) {
1494 self.emit(
1495 IssueKind::UndefinedClass { name: resolved },
1496 Severity::Error,
1497 hint.span,
1498 );
1499 }
1500 }
1501 TypeHintKind::Nullable(inner) => self.check_type_hint(inner),
1502 TypeHintKind::Union(parts) | TypeHintKind::Intersection(parts) => {
1503 for part in parts.iter() {
1504 self.check_type_hint(part);
1505 }
1506 }
1507 TypeHintKind::Keyword(_, _) => {}
1508 }
1509 }
1510
1511 pub fn emit(&mut self, kind: IssueKind, severity: Severity, span: php_ast::Span) {
1512 let (line, col_start) = self.offset_to_line_col(span.start);
1513
1514 let col_end = if span.start < span.end {
1517 let (_end_line, end_col) = self.offset_to_line_col(span.end);
1518 end_col
1519 } else {
1520 col_start
1521 };
1522
1523 let mut issue = Issue::new(
1524 kind,
1525 Location {
1526 file: self.file.clone(),
1527 line,
1528 col_start,
1529 col_end: col_end.max(col_start + 1),
1530 },
1531 );
1532 issue.severity = severity;
1533 if span.start < span.end {
1535 let s = span.start as usize;
1536 let e = (span.end as usize).min(self.source.len());
1537 if let Some(text) = self.source.get(s..e) {
1538 let trimmed = text.trim();
1539 if !trimmed.is_empty() {
1540 issue.snippet = Some(trimmed.to_string());
1541 }
1542 }
1543 }
1544 self.issues.add(issue);
1545 }
1546}
1547
1548fn widen_array_with_value(current: &Union, new_value: &Union) -> Union {
1556 let mut result = Union::empty();
1557 result.possibly_undefined = current.possibly_undefined;
1558 result.from_docblock = current.from_docblock;
1559 let mut found_array = false;
1560 for atomic in ¤t.types {
1561 match atomic {
1562 Atomic::TKeyedArray { properties, .. } => {
1563 let mut all_values = new_value.clone();
1565 for prop in properties.values() {
1566 all_values = Union::merge(&all_values, &prop.ty);
1567 }
1568 result.add_type(Atomic::TArray {
1569 key: Box::new(Union::mixed()),
1570 value: Box::new(all_values),
1571 });
1572 found_array = true;
1573 }
1574 Atomic::TArray { key, value } => {
1575 let merged = Union::merge(value, new_value);
1576 result.add_type(Atomic::TArray {
1577 key: key.clone(),
1578 value: Box::new(merged),
1579 });
1580 found_array = true;
1581 }
1582 Atomic::TList { value } | Atomic::TNonEmptyList { value } => {
1583 let merged = Union::merge(value, new_value);
1584 result.add_type(Atomic::TList {
1585 value: Box::new(merged),
1586 });
1587 found_array = true;
1588 }
1589 Atomic::TMixed => {
1590 return Union::mixed();
1591 }
1592 other => {
1593 result.add_type(other.clone());
1594 }
1595 }
1596 }
1597 if !found_array {
1598 return current.clone();
1601 }
1602 result
1603}
1604
1605pub fn infer_arithmetic(left: &Union, right: &Union) -> Union {
1606 if left.is_mixed() || right.is_mixed() {
1608 return Union::mixed();
1609 }
1610
1611 let left_is_array = left.contains(|t| {
1613 matches!(
1614 t,
1615 Atomic::TArray { .. }
1616 | Atomic::TNonEmptyArray { .. }
1617 | Atomic::TList { .. }
1618 | Atomic::TNonEmptyList { .. }
1619 | Atomic::TKeyedArray { .. }
1620 )
1621 });
1622 let right_is_array = right.contains(|t| {
1623 matches!(
1624 t,
1625 Atomic::TArray { .. }
1626 | Atomic::TNonEmptyArray { .. }
1627 | Atomic::TList { .. }
1628 | Atomic::TNonEmptyList { .. }
1629 | Atomic::TKeyedArray { .. }
1630 )
1631 });
1632 if left_is_array || right_is_array {
1633 let merged_left = if left_is_array {
1635 left.clone()
1636 } else {
1637 Union::single(Atomic::TArray {
1638 key: Box::new(Union::single(Atomic::TMixed)),
1639 value: Box::new(Union::mixed()),
1640 })
1641 };
1642 return merged_left;
1643 }
1644
1645 let left_is_float = left.contains(|t| matches!(t, Atomic::TFloat | Atomic::TLiteralFloat(..)));
1646 let right_is_float =
1647 right.contains(|t| matches!(t, Atomic::TFloat | Atomic::TLiteralFloat(..)));
1648 if left_is_float || right_is_float {
1649 Union::single(Atomic::TFloat)
1650 } else if left.contains(|t| t.is_int()) && right.contains(|t| t.is_int()) {
1651 Union::single(Atomic::TInt)
1652 } else {
1653 let mut u = Union::empty();
1655 u.add_type(Atomic::TInt);
1656 u.add_type(Atomic::TFloat);
1657 u
1658 }
1659}
1660
1661pub fn extract_simple_var<'arena, 'src>(expr: &php_ast::ast::Expr<'arena, 'src>) -> Option<String> {
1662 match &expr.kind {
1663 ExprKind::Variable(name) => Some(name.as_str().trim_start_matches('$').to_string()),
1664 ExprKind::Parenthesized(inner) => extract_simple_var(inner),
1665 _ => None,
1666 }
1667}
1668
1669pub fn extract_destructure_vars<'arena, 'src>(
1673 expr: &php_ast::ast::Expr<'arena, 'src>,
1674) -> Vec<String> {
1675 match &expr.kind {
1676 ExprKind::Array(elements) => {
1677 let mut vars = vec![];
1678 for elem in elements.iter() {
1679 let sub = extract_destructure_vars(&elem.value);
1681 if sub.is_empty() {
1682 if let Some(v) = extract_simple_var(&elem.value) {
1683 vars.push(v);
1684 }
1685 } else {
1686 vars.extend(sub);
1687 }
1688 }
1689 vars
1690 }
1691 _ => vec![],
1692 }
1693}
1694
1695fn ast_params_to_fn_params_resolved<'arena, 'src>(
1697 params: &php_ast::ast::ArenaVec<'arena, php_ast::ast::Param<'arena, 'src>>,
1698 self_fqcn: Option<&str>,
1699 codebase: &mir_codebase::Codebase,
1700 file: &str,
1701) -> Vec<mir_codebase::FnParam> {
1702 params
1703 .iter()
1704 .map(|p| {
1705 let ty = p
1706 .type_hint
1707 .as_ref()
1708 .map(|h| crate::parser::type_from_hint(h, self_fqcn))
1709 .map(|u| resolve_named_objects_in_union(u, codebase, file));
1710 mir_codebase::FnParam {
1711 name: p.name.trim_start_matches('$').into(),
1712 ty,
1713 default: p.default.as_ref().map(|_| Union::mixed()),
1714 is_variadic: p.variadic,
1715 is_byref: p.by_ref,
1716 is_optional: p.default.is_some() || p.variadic,
1717 }
1718 })
1719 .collect()
1720}
1721
1722fn resolve_named_objects_in_union(
1724 union: Union,
1725 codebase: &mir_codebase::Codebase,
1726 file: &str,
1727) -> Union {
1728 use mir_types::Atomic;
1729 let from_docblock = union.from_docblock;
1730 let possibly_undefined = union.possibly_undefined;
1731 let types: Vec<Atomic> = union
1732 .types
1733 .into_iter()
1734 .map(|a| match a {
1735 Atomic::TNamedObject { fqcn, type_params } => {
1736 let resolved = codebase.resolve_class_name(file, fqcn.as_ref());
1737 Atomic::TNamedObject {
1738 fqcn: resolved.into(),
1739 type_params,
1740 }
1741 }
1742 other => other,
1743 })
1744 .collect();
1745 let mut result = Union::from_vec(types);
1746 result.from_docblock = from_docblock;
1747 result.possibly_undefined = possibly_undefined;
1748 result
1749}
1750
1751fn extract_string_from_expr<'arena, 'src>(
1752 expr: &php_ast::ast::Expr<'arena, 'src>,
1753) -> Option<String> {
1754 match &expr.kind {
1755 ExprKind::Identifier(s) => Some(s.trim_start_matches('$').to_string()),
1756 ExprKind::Variable(_) => None,
1758 ExprKind::String(s) => Some(s.to_string()),
1759 _ => None,
1760 }
1761}
1762
1763#[cfg(test)]
1764mod tests {
1765 fn create_source_map(source: &str) -> php_rs_parser::source_map::SourceMap {
1767 let bump = bumpalo::Bump::new();
1768 let result = php_rs_parser::parse(&bump, source);
1769 result.source_map
1770 }
1771
1772 fn test_offset_conversion(source: &str, offset: u32) -> (u32, u16) {
1774 let source_map = create_source_map(source);
1775 let lc = source_map.offset_to_line_col(offset);
1776 let line = lc.line + 1;
1777
1778 let byte_offset = offset as usize;
1779 let line_start_byte = if byte_offset == 0 {
1780 0
1781 } else {
1782 source[..byte_offset]
1783 .rfind('\n')
1784 .map(|p| p + 1)
1785 .unwrap_or(0)
1786 };
1787
1788 let col = source[line_start_byte..byte_offset].chars().count() as u16;
1789
1790 (line, col)
1791 }
1792
1793 #[test]
1794 fn col_conversion_simple_ascii() {
1795 let source = "<?php\n$var = 123;";
1796
1797 let (line, col) = test_offset_conversion(source, 6);
1799 assert_eq!(line, 2);
1800 assert_eq!(col, 0);
1801
1802 let (line, col) = test_offset_conversion(source, 7);
1804 assert_eq!(line, 2);
1805 assert_eq!(col, 1);
1806 }
1807
1808 #[test]
1809 fn col_conversion_different_lines() {
1810 let source = "<?php\n$x = 1;\n$y = 2;";
1811 let (line, col) = test_offset_conversion(source, 0);
1816 assert_eq!((line, col), (1, 0));
1817
1818 let (line, col) = test_offset_conversion(source, 6);
1819 assert_eq!((line, col), (2, 0));
1820
1821 let (line, col) = test_offset_conversion(source, 14);
1822 assert_eq!((line, col), (3, 0));
1823 }
1824
1825 #[test]
1826 fn col_conversion_accented_characters() {
1827 let source = "<?php\n$café = 1;";
1829 let (line, col) = test_offset_conversion(source, 9);
1834 assert_eq!((line, col), (2, 3));
1835
1836 let (line, col) = test_offset_conversion(source, 10);
1838 assert_eq!((line, col), (2, 4));
1839 }
1840
1841 #[test]
1842 fn col_conversion_emoji_counts_as_one_char() {
1843 let source = "<?php\n$y = \"🎉x\";";
1846 let emoji_start = source.find("🎉").unwrap();
1850 let after_emoji = emoji_start + "🎉".len(); let (line, col) = test_offset_conversion(source, after_emoji as u32);
1854 assert_eq!(line, 2);
1855 assert_eq!(col, 7); }
1857
1858 #[test]
1859 fn col_conversion_emoji_start_position() {
1860 let source = "<?php\n$y = \"🎉\";";
1862 let quote_pos = source.find('"').unwrap();
1866 let emoji_pos = quote_pos + 1; let (line, col) = test_offset_conversion(source, quote_pos as u32);
1869 assert_eq!(line, 2);
1870 assert_eq!(col, 5); let (line, col) = test_offset_conversion(source, emoji_pos as u32);
1873 assert_eq!(line, 2);
1874 assert_eq!(col, 6); }
1876
1877 #[test]
1878 fn col_end_minimum_width() {
1879 let col_start = 0u16;
1881 let col_end = 0u16; let effective_col_end = col_end.max(col_start + 1);
1883
1884 assert_eq!(
1885 effective_col_end, 1,
1886 "col_end should be at least col_start + 1"
1887 );
1888 }
1889
1890 #[test]
1891 fn col_conversion_multiline_span() {
1892 let source = "<?php\n$x = [\n 'a',\n 'b'\n];";
1894 let bracket_open = source.find('[').unwrap();
1902 let (line_start, _col_start) = test_offset_conversion(source, bracket_open as u32);
1903 assert_eq!(line_start, 2);
1904
1905 let bracket_close = source.rfind(']').unwrap();
1907 let (line_end, col_end) = test_offset_conversion(source, bracket_close as u32);
1908 assert_eq!(line_end, 5);
1909 assert_eq!(col_end, 0); }
1911
1912 #[test]
1913 fn col_end_handles_emoji_in_span() {
1914 let source = "<?php\n$greeting = \"Hello 🎉\";";
1916
1917 let emoji_pos = source.find('🎉').unwrap();
1919 let hello_pos = source.find("Hello").unwrap();
1920
1921 let (line, col) = test_offset_conversion(source, hello_pos as u32);
1923 assert_eq!(line, 2);
1924 assert_eq!(col, 13); let (line, col) = test_offset_conversion(source, emoji_pos as u32);
1928 assert_eq!(line, 2);
1929 assert_eq!(col, 19);
1931 }
1932}