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::Info,
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 match a.op {
185 AssignOp::Assign => {
186 self.assign_to_target(a.target, rhs_ty.clone(), ctx, expr.span);
187 if rhs_tainted {
189 if let ExprKind::Variable(name) = &a.target.kind {
190 ctx.taint_var(name.as_ref());
191 }
192 }
193 rhs_ty
194 }
195 AssignOp::Concat => {
196 if let Some(var_name) = extract_simple_var(a.target) {
198 ctx.set_var(&var_name, Union::single(Atomic::TString));
199 }
200 Union::single(Atomic::TString)
201 }
202 AssignOp::Plus
203 | AssignOp::Minus
204 | AssignOp::Mul
205 | AssignOp::Div
206 | AssignOp::Mod
207 | AssignOp::Pow => {
208 let lhs_ty = self.analyze(a.target, ctx);
209 let result_ty = infer_arithmetic(&lhs_ty, &rhs_ty);
210 if let Some(var_name) = extract_simple_var(a.target) {
211 ctx.set_var(&var_name, result_ty.clone());
212 }
213 result_ty
214 }
215 AssignOp::Coalesce => {
216 let lhs_ty = self.analyze(a.target, ctx);
218 let merged = Union::merge(&lhs_ty.remove_null(), &rhs_ty);
219 if let Some(var_name) = extract_simple_var(a.target) {
220 ctx.set_var(&var_name, merged.clone());
221 }
222 merged
223 }
224 _ => {
225 if let Some(var_name) = extract_simple_var(a.target) {
226 ctx.set_var(&var_name, Union::mixed());
227 }
228 Union::mixed()
229 }
230 }
231 }
232
233 ExprKind::Binary(b) => self.analyze_binary(b, expr.span, ctx),
235
236 ExprKind::UnaryPrefix(u) => {
238 let operand_ty = self.analyze(u.operand, ctx);
239 match u.op {
240 UnaryPrefixOp::BooleanNot => Union::single(Atomic::TBool),
241 UnaryPrefixOp::Negate => {
242 if operand_ty.contains(|t| t.is_int()) {
243 Union::single(Atomic::TInt)
244 } else {
245 Union::single(Atomic::TFloat)
246 }
247 }
248 UnaryPrefixOp::Plus => operand_ty,
249 UnaryPrefixOp::BitwiseNot => Union::single(Atomic::TInt),
250 UnaryPrefixOp::PreIncrement | UnaryPrefixOp::PreDecrement => {
251 if let Some(var_name) = extract_simple_var(u.operand) {
253 let ty = ctx.get_var(&var_name);
254 let new_ty = if ty.contains(|t| {
255 matches!(t, Atomic::TFloat | Atomic::TLiteralFloat(..))
256 }) {
257 Union::single(Atomic::TFloat)
258 } else {
259 Union::single(Atomic::TInt)
260 };
261 ctx.set_var(&var_name, new_ty.clone());
262 new_ty
263 } else {
264 Union::single(Atomic::TInt)
265 }
266 }
267 }
268 }
269
270 ExprKind::UnaryPostfix(u) => {
271 let operand_ty = self.analyze(u.operand, ctx);
272 match u.op {
274 UnaryPostfixOp::PostIncrement | UnaryPostfixOp::PostDecrement => {
275 if let Some(var_name) = extract_simple_var(u.operand) {
276 let new_ty = if operand_ty.contains(|t| {
277 matches!(t, Atomic::TFloat | Atomic::TLiteralFloat(..))
278 }) {
279 Union::single(Atomic::TFloat)
280 } else {
281 Union::single(Atomic::TInt)
282 };
283 ctx.set_var(&var_name, new_ty);
284 }
285 operand_ty }
287 }
288 }
289
290 ExprKind::Ternary(t) => {
292 let cond_ty = self.analyze(t.condition, ctx);
293 match &t.then_expr {
294 Some(then_expr) => {
295 let mut then_ctx = ctx.fork();
296 crate::narrowing::narrow_from_condition(
297 t.condition,
298 &mut then_ctx,
299 true,
300 self.codebase,
301 &self.file,
302 );
303 let then_ty = self.analyze(then_expr, &mut then_ctx);
304
305 let mut else_ctx = ctx.fork();
306 crate::narrowing::narrow_from_condition(
307 t.condition,
308 &mut else_ctx,
309 false,
310 self.codebase,
311 &self.file,
312 );
313 let else_ty = self.analyze(t.else_expr, &mut else_ctx);
314
315 for name in then_ctx.read_vars.iter().chain(else_ctx.read_vars.iter()) {
317 ctx.read_vars.insert(name.clone());
318 }
319
320 Union::merge(&then_ty, &else_ty)
321 }
322 None => {
323 let else_ty = self.analyze(t.else_expr, ctx);
325 let truthy_ty = cond_ty.narrow_to_truthy();
326 if truthy_ty.is_empty() {
327 else_ty
328 } else {
329 Union::merge(&truthy_ty, &else_ty)
330 }
331 }
332 }
333 }
334
335 ExprKind::NullCoalesce(nc) => {
336 let left_ty = self.analyze(nc.left, ctx);
337 let right_ty = self.analyze(nc.right, ctx);
338 let non_null_left = left_ty.remove_null();
340 if non_null_left.is_empty() {
341 right_ty
342 } else {
343 Union::merge(&non_null_left, &right_ty)
344 }
345 }
346
347 ExprKind::Cast(kind, inner) => {
349 let _inner_ty = self.analyze(inner, ctx);
350 match kind {
351 CastKind::Int => Union::single(Atomic::TInt),
352 CastKind::Float => Union::single(Atomic::TFloat),
353 CastKind::String => Union::single(Atomic::TString),
354 CastKind::Bool => Union::single(Atomic::TBool),
355 CastKind::Array => Union::single(Atomic::TArray {
356 key: Box::new(Union::single(Atomic::TMixed)),
357 value: Box::new(Union::mixed()),
358 }),
359 CastKind::Object => Union::single(Atomic::TObject),
360 CastKind::Unset | CastKind::Void => Union::single(Atomic::TNull),
361 }
362 }
363
364 ExprKind::ErrorSuppress(inner) => self.analyze(inner, ctx),
366
367 ExprKind::Parenthesized(inner) => self.analyze(inner, ctx),
369
370 ExprKind::Array(elements) => {
372 use mir_types::atomic::{ArrayKey, KeyedProperty};
373
374 if elements.is_empty() {
375 return Union::single(Atomic::TKeyedArray {
376 properties: indexmap::IndexMap::new(),
377 is_open: false,
378 is_list: true,
379 });
380 }
381
382 let mut keyed_props: indexmap::IndexMap<ArrayKey, KeyedProperty> =
385 indexmap::IndexMap::new();
386 let mut is_list = true;
387 let mut can_be_keyed = true;
388 let mut next_int_key: i64 = 0;
389
390 for elem in elements.iter() {
391 if elem.unpack {
392 self.analyze(&elem.value, ctx);
393 can_be_keyed = false;
394 break;
395 }
396 let value_ty = self.analyze(&elem.value, ctx);
397 let array_key = if let Some(key_expr) = &elem.key {
398 is_list = false;
399 let key_ty = self.analyze(key_expr, ctx);
400 match key_ty.types.as_slice() {
402 [Atomic::TLiteralString(s)] => ArrayKey::String(s.clone()),
403 [Atomic::TLiteralInt(i)] => {
404 next_int_key = *i + 1;
405 ArrayKey::Int(*i)
406 }
407 _ => {
408 can_be_keyed = false;
409 break;
410 }
411 }
412 } else {
413 let k = ArrayKey::Int(next_int_key);
414 next_int_key += 1;
415 k
416 };
417 keyed_props.insert(
418 array_key,
419 KeyedProperty {
420 ty: value_ty,
421 optional: false,
422 },
423 );
424 }
425
426 if can_be_keyed {
427 return Union::single(Atomic::TKeyedArray {
428 properties: keyed_props,
429 is_open: false,
430 is_list,
431 });
432 }
433
434 let mut all_value_types = Union::empty();
436 let mut key_union = Union::empty();
437 let mut has_unpack = false;
438 for elem in elements.iter() {
439 let value_ty = self.analyze(&elem.value, ctx);
440 if elem.unpack {
441 has_unpack = true;
442 } else {
443 all_value_types = Union::merge(&all_value_types, &value_ty);
444 if let Some(key_expr) = &elem.key {
445 let key_ty = self.analyze(key_expr, ctx);
446 key_union = Union::merge(&key_union, &key_ty);
447 } else {
448 key_union.add_type(Atomic::TInt);
449 }
450 }
451 }
452 if has_unpack {
453 return Union::single(Atomic::TArray {
454 key: Box::new(Union::single(Atomic::TMixed)),
455 value: Box::new(Union::mixed()),
456 });
457 }
458 if key_union.is_empty() {
459 key_union.add_type(Atomic::TInt);
460 }
461 Union::single(Atomic::TArray {
462 key: Box::new(key_union),
463 value: Box::new(all_value_types),
464 })
465 }
466
467 ExprKind::ArrayAccess(aa) => {
469 let arr_ty = self.analyze(aa.array, ctx);
470
471 if let Some(idx) = &aa.index {
473 self.analyze(idx, ctx);
474 }
475
476 if arr_ty.contains(|t| matches!(t, Atomic::TNull)) && arr_ty.is_single() {
478 self.emit(IssueKind::NullArrayAccess, Severity::Error, expr.span);
479 return Union::mixed();
480 }
481 if arr_ty.is_nullable() {
482 self.emit(
483 IssueKind::PossiblyNullArrayAccess,
484 Severity::Info,
485 expr.span,
486 );
487 }
488
489 let literal_key: Option<mir_types::atomic::ArrayKey> =
491 aa.index.as_ref().and_then(|idx| match &idx.kind {
492 ExprKind::String(s) => {
493 Some(mir_types::atomic::ArrayKey::String(Arc::from(&**s)))
494 }
495 ExprKind::Int(i) => Some(mir_types::atomic::ArrayKey::Int(*i)),
496 _ => None,
497 });
498
499 for atomic in &arr_ty.types {
501 match atomic {
502 Atomic::TKeyedArray { properties, .. } => {
503 if let Some(ref key) = literal_key {
505 if let Some(prop) = properties.get(key) {
506 return prop.ty.clone();
507 }
508 }
509 let mut result = Union::empty();
511 for prop in properties.values() {
512 result = Union::merge(&result, &prop.ty);
513 }
514 return if result.types.is_empty() {
515 Union::mixed()
516 } else {
517 result
518 };
519 }
520 Atomic::TArray { value, .. } | Atomic::TNonEmptyArray { value, .. } => {
521 return *value.clone();
522 }
523 Atomic::TList { value } | Atomic::TNonEmptyList { value } => {
524 return *value.clone();
525 }
526 Atomic::TString | Atomic::TLiteralString(_) => {
527 return Union::single(Atomic::TString);
528 }
529 _ => {}
530 }
531 }
532 Union::mixed()
533 }
534
535 ExprKind::Isset(exprs) => {
537 for e in exprs.iter() {
538 self.analyze(e, ctx);
539 }
540 Union::single(Atomic::TBool)
541 }
542 ExprKind::Empty(inner) => {
543 self.analyze(inner, ctx);
544 Union::single(Atomic::TBool)
545 }
546
547 ExprKind::Print(inner) => {
549 self.analyze(inner, ctx);
550 Union::single(Atomic::TLiteralInt(1))
551 }
552
553 ExprKind::Clone(inner) => self.analyze(inner, ctx),
555 ExprKind::CloneWith(inner, _props) => self.analyze(inner, ctx),
556
557 ExprKind::New(n) => {
559 let arg_types: Vec<Union> = n
561 .args
562 .iter()
563 .map(|a| {
564 let ty = self.analyze(&a.value, ctx);
565 if a.unpack {
566 crate::call::spread_element_type(&ty)
567 } else {
568 ty
569 }
570 })
571 .collect();
572 let arg_spans: Vec<php_ast::Span> = n.args.iter().map(|a| a.span).collect();
573 let arg_names: Vec<Option<String>> = n
574 .args
575 .iter()
576 .map(|a| a.name.as_ref().map(|nm| nm.to_string_repr().into_owned()))
577 .collect();
578
579 let class_ty = match &n.class.kind {
580 ExprKind::Identifier(name) => {
581 let resolved = self.codebase.resolve_class_name(&self.file, name.as_ref());
582 let fqcn: Arc<str> = match resolved.as_str() {
584 "self" | "static" => ctx
585 .self_fqcn
586 .clone()
587 .or_else(|| ctx.static_fqcn.clone())
588 .unwrap_or_else(|| Arc::from(resolved.as_str())),
589 "parent" => ctx
590 .parent_fqcn
591 .clone()
592 .unwrap_or_else(|| Arc::from(resolved.as_str())),
593 _ => Arc::from(resolved.as_str()),
594 };
595 if !matches!(resolved.as_str(), "self" | "static" | "parent")
596 && !self.codebase.type_exists(&fqcn)
597 {
598 self.emit(
599 IssueKind::UndefinedClass {
600 name: resolved.clone(),
601 },
602 Severity::Error,
603 n.class.span,
604 );
605 } else if self.codebase.type_exists(&fqcn) {
606 if let Some(cls) = self.codebase.classes.get(fqcn.as_ref()) {
607 if let Some(msg) = cls.deprecated.clone() {
608 self.emit(
609 IssueKind::DeprecatedClass {
610 name: fqcn.to_string(),
611 message: Some(msg).filter(|m| !m.is_empty()),
612 },
613 Severity::Info,
614 n.class.span,
615 );
616 }
617 }
618 if let Some(ctor) = self.codebase.get_method(&fqcn, "__construct") {
620 crate::call::check_constructor_args(
621 self,
622 &fqcn,
623 crate::call::CheckArgsParams {
624 fn_name: "__construct",
625 params: &ctor.params,
626 arg_types: &arg_types,
627 arg_spans: &arg_spans,
628 arg_names: &arg_names,
629 call_span: expr.span,
630 has_spread: n.args.iter().any(|a| a.unpack),
631 },
632 );
633 }
634 }
635 let ty = Union::single(Atomic::TNamedObject {
636 fqcn: fqcn.clone(),
637 type_params: vec![],
638 });
639 self.record_symbol(
640 n.class.span,
641 SymbolKind::ClassReference(fqcn.clone()),
642 ty.clone(),
643 );
644 self.codebase.mark_class_referenced_at(
647 &fqcn,
648 self.file.clone(),
649 n.class.span.start,
650 n.class.span.end,
651 );
652 ty
653 }
654 _ => {
655 self.analyze(n.class, ctx);
656 Union::single(Atomic::TObject)
657 }
658 };
659 class_ty
660 }
661
662 ExprKind::AnonymousClass(_) => Union::single(Atomic::TObject),
663
664 ExprKind::PropertyAccess(pa) => {
666 let obj_ty = self.analyze(pa.object, ctx);
667 let prop_name = extract_string_from_expr(pa.property)
668 .unwrap_or_else(|| "<dynamic>".to_string());
669
670 if obj_ty.contains(|t| matches!(t, Atomic::TNull)) && obj_ty.is_single() {
671 self.emit(
672 IssueKind::NullPropertyFetch {
673 property: prop_name.clone(),
674 },
675 Severity::Error,
676 expr.span,
677 );
678 return Union::mixed();
679 }
680 if obj_ty.is_nullable() {
681 self.emit(
682 IssueKind::PossiblyNullPropertyFetch {
683 property: prop_name.clone(),
684 },
685 Severity::Info,
686 expr.span,
687 );
688 }
689
690 if prop_name == "<dynamic>" {
692 return Union::mixed();
693 }
694 let resolved = self.resolve_property_type(&obj_ty, &prop_name, pa.property.span);
697 for atomic in &obj_ty.types {
699 if let Atomic::TNamedObject { fqcn, .. } = atomic {
700 self.record_symbol(
701 pa.property.span,
702 SymbolKind::PropertyAccess {
703 class: fqcn.clone(),
704 property: Arc::from(prop_name.as_str()),
705 },
706 resolved.clone(),
707 );
708 break;
709 }
710 }
711 resolved
712 }
713
714 ExprKind::NullsafePropertyAccess(pa) => {
715 let obj_ty = self.analyze(pa.object, ctx);
716 let prop_name = extract_string_from_expr(pa.property)
717 .unwrap_or_else(|| "<dynamic>".to_string());
718 if prop_name == "<dynamic>" {
719 return Union::mixed();
720 }
721 let non_null_ty = obj_ty.remove_null();
723 let mut prop_ty =
726 self.resolve_property_type(&non_null_ty, &prop_name, pa.property.span);
727 prop_ty.add_type(Atomic::TNull); for atomic in &non_null_ty.types {
730 if let Atomic::TNamedObject { fqcn, .. } = atomic {
731 self.record_symbol(
732 pa.property.span,
733 SymbolKind::PropertyAccess {
734 class: fqcn.clone(),
735 property: Arc::from(prop_name.as_str()),
736 },
737 prop_ty.clone(),
738 );
739 break;
740 }
741 }
742 prop_ty
743 }
744
745 ExprKind::StaticPropertyAccess(spa) => {
746 if let ExprKind::Identifier(id) = &spa.class.kind {
747 let resolved = self.codebase.resolve_class_name(&self.file, id.as_ref());
748 if !matches!(resolved.as_str(), "self" | "static" | "parent")
749 && !self.codebase.type_exists(&resolved)
750 {
751 self.emit(
752 IssueKind::UndefinedClass { name: resolved },
753 Severity::Error,
754 spa.class.span,
755 );
756 }
757 }
758 Union::mixed()
759 }
760
761 ExprKind::ClassConstAccess(cca) => {
762 if cca.member.name_str() == Some("class") {
764 let fqcn = if let ExprKind::Identifier(id) = &cca.class.kind {
766 let resolved = self.codebase.resolve_class_name(&self.file, id.as_ref());
767 Some(Arc::from(resolved.as_str()))
768 } else {
769 None
770 };
771 return Union::single(Atomic::TClassString(fqcn));
772 }
773
774 let const_name = match cca.member.name_str() {
775 Some(n) => n.to_string(),
776 None => return Union::mixed(),
777 };
778
779 let fqcn = match &cca.class.kind {
780 ExprKind::Identifier(id) => {
781 let resolved = self.codebase.resolve_class_name(&self.file, id.as_ref());
782 if matches!(resolved.as_str(), "self" | "static" | "parent") {
784 return Union::mixed();
785 }
786 resolved
787 }
788 _ => return Union::mixed(),
789 };
790
791 if !self.codebase.type_exists(&fqcn) {
792 self.emit(
793 IssueKind::UndefinedClass { name: fqcn },
794 Severity::Error,
795 cca.class.span,
796 );
797 return Union::mixed();
798 }
799
800 if self
801 .codebase
802 .get_class_constant(&fqcn, &const_name)
803 .is_none()
804 && !self.codebase.has_unknown_ancestor(&fqcn)
805 {
806 self.emit(
807 IssueKind::UndefinedConstant {
808 name: format!("{}::{}", fqcn, const_name),
809 },
810 Severity::Error,
811 expr.span,
812 );
813 }
814 Union::mixed()
815 }
816
817 ExprKind::ClassConstAccessDynamic { .. } => Union::mixed(),
818 ExprKind::StaticPropertyAccessDynamic { .. } => Union::mixed(),
819
820 ExprKind::MethodCall(mc) => {
822 CallAnalyzer::analyze_method_call(self, mc, ctx, expr.span, false)
823 }
824
825 ExprKind::NullsafeMethodCall(mc) => {
826 CallAnalyzer::analyze_method_call(self, mc, ctx, expr.span, true)
827 }
828
829 ExprKind::StaticMethodCall(smc) => {
830 CallAnalyzer::analyze_static_method_call(self, smc, ctx, expr.span)
831 }
832
833 ExprKind::StaticDynMethodCall(smc) => {
834 CallAnalyzer::analyze_static_dyn_method_call(self, smc, ctx)
835 }
836
837 ExprKind::FunctionCall(fc) => {
839 CallAnalyzer::analyze_function_call(self, fc, ctx, expr.span)
840 }
841
842 ExprKind::Closure(c) => {
844 for param in c.params.iter() {
846 if let Some(hint) = ¶m.type_hint {
847 self.check_type_hint(hint);
848 }
849 }
850 if let Some(hint) = &c.return_type {
851 self.check_type_hint(hint);
852 }
853
854 let params = ast_params_to_fn_params_resolved(
855 &c.params,
856 ctx.self_fqcn.as_deref(),
857 self.codebase,
858 &self.file,
859 );
860 let return_ty_hint = c
861 .return_type
862 .as_ref()
863 .map(|h| crate::parser::type_from_hint(h, ctx.self_fqcn.as_deref()))
864 .map(|u| resolve_named_objects_in_union(u, self.codebase, &self.file));
865
866 let mut closure_ctx = crate::context::Context::for_function(
870 ¶ms,
871 return_ty_hint.clone(),
872 ctx.self_fqcn.clone(),
873 ctx.parent_fqcn.clone(),
874 ctx.static_fqcn.clone(),
875 ctx.strict_types,
876 c.is_static,
877 );
878 for use_var in c.use_vars.iter() {
879 let name = use_var.name.trim_start_matches('$');
880 closure_ctx.set_var(name, ctx.get_var(name));
881 if ctx.is_tainted(name) {
882 closure_ctx.taint_var(name);
883 }
884 }
885
886 let inferred_return = {
888 let mut sa = crate::stmt::StatementsAnalyzer::new(
889 self.codebase,
890 self.file.clone(),
891 self.source,
892 self.source_map,
893 self.issues,
894 self.symbols,
895 self.php_version,
896 );
897 sa.analyze_stmts(&c.body, &mut closure_ctx);
898 let ret = crate::project::merge_return_types(&sa.return_types);
899 drop(sa);
900 ret
901 };
902
903 for name in &closure_ctx.read_vars {
905 ctx.read_vars.insert(name.clone());
906 }
907
908 let return_ty = return_ty_hint.unwrap_or(inferred_return);
909 let closure_params: Vec<mir_types::atomic::FnParam> = params
910 .iter()
911 .map(|p| mir_types::atomic::FnParam {
912 name: p.name.clone(),
913 ty: p.ty.clone(),
914 default: p.default.clone(),
915 is_variadic: p.is_variadic,
916 is_byref: p.is_byref,
917 is_optional: p.is_optional,
918 })
919 .collect();
920
921 Union::single(Atomic::TClosure {
922 params: closure_params,
923 return_type: Box::new(return_ty),
924 this_type: ctx.self_fqcn.clone().map(|f| {
925 Box::new(Union::single(Atomic::TNamedObject {
926 fqcn: f,
927 type_params: vec![],
928 }))
929 }),
930 })
931 }
932
933 ExprKind::ArrowFunction(af) => {
934 for param in af.params.iter() {
936 if let Some(hint) = ¶m.type_hint {
937 self.check_type_hint(hint);
938 }
939 }
940 if let Some(hint) = &af.return_type {
941 self.check_type_hint(hint);
942 }
943
944 let params = ast_params_to_fn_params_resolved(
945 &af.params,
946 ctx.self_fqcn.as_deref(),
947 self.codebase,
948 &self.file,
949 );
950 let return_ty_hint = af
951 .return_type
952 .as_ref()
953 .map(|h| crate::parser::type_from_hint(h, ctx.self_fqcn.as_deref()))
954 .map(|u| resolve_named_objects_in_union(u, self.codebase, &self.file));
955
956 let mut arrow_ctx = crate::context::Context::for_function(
959 ¶ms,
960 return_ty_hint.clone(),
961 ctx.self_fqcn.clone(),
962 ctx.parent_fqcn.clone(),
963 ctx.static_fqcn.clone(),
964 ctx.strict_types,
965 af.is_static,
966 );
967 for (name, ty) in &ctx.vars {
969 if !arrow_ctx.vars.contains_key(name) {
970 arrow_ctx.set_var(name, ty.clone());
971 }
972 }
973
974 let inferred_return = self.analyze(af.body, &mut arrow_ctx);
976
977 for name in &arrow_ctx.read_vars {
979 ctx.read_vars.insert(name.clone());
980 }
981
982 let return_ty = return_ty_hint.unwrap_or(inferred_return);
983 let closure_params: Vec<mir_types::atomic::FnParam> = params
984 .iter()
985 .map(|p| mir_types::atomic::FnParam {
986 name: p.name.clone(),
987 ty: p.ty.clone(),
988 default: p.default.clone(),
989 is_variadic: p.is_variadic,
990 is_byref: p.is_byref,
991 is_optional: p.is_optional,
992 })
993 .collect();
994
995 Union::single(Atomic::TClosure {
996 params: closure_params,
997 return_type: Box::new(return_ty),
998 this_type: if af.is_static {
999 None
1000 } else {
1001 ctx.self_fqcn.clone().map(|f| {
1002 Box::new(Union::single(Atomic::TNamedObject {
1003 fqcn: f,
1004 type_params: vec![],
1005 }))
1006 })
1007 },
1008 })
1009 }
1010
1011 ExprKind::CallableCreate(_) => Union::single(Atomic::TCallable {
1012 params: None,
1013 return_type: None,
1014 }),
1015
1016 ExprKind::Match(m) => {
1018 let subject_ty = self.analyze(m.subject, ctx);
1019 let subject_var = match &m.subject.kind {
1021 ExprKind::Variable(name) => {
1022 Some(name.as_str().trim_start_matches('$').to_string())
1023 }
1024 _ => None,
1025 };
1026
1027 let mut result = Union::empty();
1028 for arm in m.arms.iter() {
1029 let mut arm_ctx = ctx.fork();
1031
1032 if let (Some(var), Some(conditions)) = (&subject_var, &arm.conditions) {
1034 let mut arm_ty = Union::empty();
1036 for cond in conditions.iter() {
1037 let cond_ty = self.analyze(cond, ctx);
1038 arm_ty = Union::merge(&arm_ty, &cond_ty);
1039 }
1040 if !arm_ty.is_empty() && !arm_ty.is_mixed() {
1042 let narrowed = subject_ty.intersect_with(&arm_ty);
1044 if !narrowed.is_empty() {
1045 arm_ctx.set_var(var, narrowed);
1046 }
1047 }
1048 }
1049
1050 if let Some(conditions) = &arm.conditions {
1053 for cond in conditions.iter() {
1054 crate::narrowing::narrow_from_condition(
1055 cond,
1056 &mut arm_ctx,
1057 true,
1058 self.codebase,
1059 &self.file,
1060 );
1061 }
1062 }
1063
1064 let arm_body_ty = self.analyze(&arm.body, &mut arm_ctx);
1065 result = Union::merge(&result, &arm_body_ty);
1066
1067 for name in &arm_ctx.read_vars {
1069 ctx.read_vars.insert(name.clone());
1070 }
1071 }
1072 if result.is_empty() {
1073 Union::mixed()
1074 } else {
1075 result
1076 }
1077 }
1078
1079 ExprKind::ThrowExpr(e) => {
1081 self.analyze(e, ctx);
1082 Union::single(Atomic::TNever)
1083 }
1084
1085 ExprKind::Yield(y) => {
1087 if let Some(key) = &y.key {
1088 self.analyze(key, ctx);
1089 }
1090 if let Some(value) = &y.value {
1091 self.analyze(value, ctx);
1092 }
1093 Union::mixed()
1094 }
1095
1096 ExprKind::MagicConst(kind) => match kind {
1098 MagicConstKind::Line => Union::single(Atomic::TInt),
1099 MagicConstKind::File
1100 | MagicConstKind::Dir
1101 | MagicConstKind::Function
1102 | MagicConstKind::Class
1103 | MagicConstKind::Method
1104 | MagicConstKind::Namespace
1105 | MagicConstKind::Trait
1106 | MagicConstKind::Property => Union::single(Atomic::TString),
1107 },
1108
1109 ExprKind::Include(_, inner) => {
1111 self.analyze(inner, ctx);
1112 Union::mixed()
1113 }
1114
1115 ExprKind::Eval(inner) => {
1117 self.analyze(inner, ctx);
1118 Union::mixed()
1119 }
1120
1121 ExprKind::Exit(opt) => {
1123 if let Some(e) = opt {
1124 self.analyze(e, ctx);
1125 }
1126 Union::single(Atomic::TNever)
1127 }
1128
1129 ExprKind::Error => Union::mixed(),
1131
1132 ExprKind::Omit => Union::single(Atomic::TNull),
1134 }
1135 }
1136
1137 fn analyze_binary<'arena, 'src>(
1142 &mut self,
1143 b: &php_ast::ast::BinaryExpr<'arena, 'src>,
1144 _span: php_ast::Span,
1145 ctx: &mut Context,
1146 ) -> Union {
1147 use php_ast::ast::BinaryOp as B;
1153 if matches!(
1154 b.op,
1155 B::BooleanAnd | B::LogicalAnd | B::BooleanOr | B::LogicalOr
1156 ) {
1157 let _left_ty = self.analyze(b.left, ctx);
1158 let mut right_ctx = ctx.fork();
1159 let is_and = matches!(b.op, B::BooleanAnd | B::LogicalAnd);
1160 crate::narrowing::narrow_from_condition(
1161 b.left,
1162 &mut right_ctx,
1163 is_and,
1164 self.codebase,
1165 &self.file,
1166 );
1167 if !right_ctx.diverges {
1170 let _right_ty = self.analyze(b.right, &mut right_ctx);
1171 }
1172 for v in right_ctx.read_vars {
1176 ctx.read_vars.insert(v.clone());
1177 }
1178 for (name, ty) in &right_ctx.vars {
1179 if !ctx.vars.contains_key(name.as_str()) {
1180 ctx.vars.insert(name.clone(), ty.clone());
1182 ctx.possibly_assigned_vars.insert(name.clone());
1183 }
1184 }
1185 return Union::single(Atomic::TBool);
1186 }
1187
1188 if b.op == B::Instanceof {
1190 let _left_ty = self.analyze(b.left, ctx);
1191 if let ExprKind::Identifier(name) = &b.right.kind {
1192 let resolved = self.codebase.resolve_class_name(&self.file, name.as_ref());
1193 let fqcn: std::sync::Arc<str> = std::sync::Arc::from(resolved.as_str());
1194 if !matches!(resolved.as_str(), "self" | "static" | "parent")
1195 && !self.codebase.type_exists(&fqcn)
1196 {
1197 self.emit(
1198 IssueKind::UndefinedClass { name: resolved },
1199 Severity::Error,
1200 b.right.span,
1201 );
1202 }
1203 }
1204 return Union::single(Atomic::TBool);
1205 }
1206
1207 let left_ty = self.analyze(b.left, ctx);
1208 let right_ty = self.analyze(b.right, ctx);
1209
1210 match b.op {
1211 BinaryOp::Add
1213 | BinaryOp::Sub
1214 | BinaryOp::Mul
1215 | BinaryOp::Div
1216 | BinaryOp::Mod
1217 | BinaryOp::Pow => infer_arithmetic(&left_ty, &right_ty),
1218
1219 BinaryOp::Concat => Union::single(Atomic::TString),
1221
1222 BinaryOp::Equal
1224 | BinaryOp::NotEqual
1225 | BinaryOp::Identical
1226 | BinaryOp::NotIdentical
1227 | BinaryOp::Less
1228 | BinaryOp::Greater
1229 | BinaryOp::LessOrEqual
1230 | BinaryOp::GreaterOrEqual => Union::single(Atomic::TBool),
1231
1232 BinaryOp::Spaceship => Union::single(Atomic::TIntRange {
1234 min: Some(-1),
1235 max: Some(1),
1236 }),
1237
1238 BinaryOp::BooleanAnd
1240 | BinaryOp::BooleanOr
1241 | BinaryOp::LogicalAnd
1242 | BinaryOp::LogicalOr
1243 | BinaryOp::LogicalXor => Union::single(Atomic::TBool),
1244
1245 BinaryOp::BitwiseAnd
1247 | BinaryOp::BitwiseOr
1248 | BinaryOp::BitwiseXor
1249 | BinaryOp::ShiftLeft
1250 | BinaryOp::ShiftRight => Union::single(Atomic::TInt),
1251
1252 BinaryOp::Pipe => right_ty,
1254
1255 BinaryOp::Instanceof => Union::single(Atomic::TBool),
1257 }
1258 }
1259
1260 fn resolve_property_type(
1265 &mut self,
1266 obj_ty: &Union,
1267 prop_name: &str,
1268 span: php_ast::Span,
1269 ) -> Union {
1270 for atomic in &obj_ty.types {
1271 match atomic {
1272 Atomic::TNamedObject { fqcn, .. }
1273 if self.codebase.classes.contains_key(fqcn.as_ref()) =>
1274 {
1275 if let Some(prop) = self.codebase.get_property(fqcn.as_ref(), prop_name) {
1276 self.codebase.mark_property_referenced_at(
1278 fqcn,
1279 prop_name,
1280 self.file.clone(),
1281 span.start,
1282 span.end,
1283 );
1284 return prop.ty.clone().unwrap_or_else(Union::mixed);
1285 }
1286 if !self.codebase.has_unknown_ancestor(fqcn.as_ref())
1288 && !self.codebase.has_magic_get(fqcn.as_ref())
1289 {
1290 self.emit(
1291 IssueKind::UndefinedProperty {
1292 class: fqcn.to_string(),
1293 property: prop_name.to_string(),
1294 },
1295 Severity::Warning,
1296 span,
1297 );
1298 }
1299 return Union::mixed();
1300 }
1301 Atomic::TMixed => return Union::mixed(),
1302 _ => {}
1303 }
1304 }
1305 Union::mixed()
1306 }
1307
1308 fn assign_to_target<'arena, 'src>(
1313 &mut self,
1314 target: &php_ast::ast::Expr<'arena, 'src>,
1315 ty: Union,
1316 ctx: &mut Context,
1317 span: php_ast::Span,
1318 ) {
1319 match &target.kind {
1320 ExprKind::Variable(name) => {
1321 let name_str = name.as_str().trim_start_matches('$').to_string();
1322 ctx.set_var(name_str, ty);
1323 }
1324 ExprKind::Array(elements) => {
1325 let has_non_array = ty.contains(|a| matches!(a, Atomic::TFalse | Atomic::TNull));
1329 let has_array = ty.contains(|a| {
1330 matches!(
1331 a,
1332 Atomic::TArray { .. }
1333 | Atomic::TList { .. }
1334 | Atomic::TNonEmptyArray { .. }
1335 | Atomic::TNonEmptyList { .. }
1336 | Atomic::TKeyedArray { .. }
1337 )
1338 });
1339 if has_non_array && has_array {
1340 let actual = format!("{}", ty);
1341 self.emit(
1342 IssueKind::PossiblyInvalidArrayOffset {
1343 expected: "array".to_string(),
1344 actual,
1345 },
1346 Severity::Warning,
1347 span,
1348 );
1349 }
1350
1351 let value_ty: Union = ty
1353 .types
1354 .iter()
1355 .find_map(|a| match a {
1356 Atomic::TArray { value, .. }
1357 | Atomic::TList { value }
1358 | Atomic::TNonEmptyArray { value, .. }
1359 | Atomic::TNonEmptyList { value } => Some(*value.clone()),
1360 _ => None,
1361 })
1362 .unwrap_or_else(Union::mixed);
1363
1364 for elem in elements.iter() {
1365 self.assign_to_target(&elem.value, value_ty.clone(), ctx, span);
1366 }
1367 }
1368 ExprKind::PropertyAccess(pa) => {
1369 let obj_ty = self.analyze(pa.object, ctx);
1371 if let Some(prop_name) = extract_string_from_expr(pa.property) {
1372 for atomic in &obj_ty.types {
1373 if let Atomic::TNamedObject { fqcn, .. } = atomic {
1374 if let Some(cls) = self.codebase.classes.get(fqcn.as_ref()) {
1375 if let Some(prop) = cls.get_property(&prop_name) {
1376 if prop.is_readonly && !ctx.inside_constructor {
1377 self.emit(
1378 IssueKind::ReadonlyPropertyAssignment {
1379 class: fqcn.to_string(),
1380 property: prop_name.clone(),
1381 },
1382 Severity::Error,
1383 span,
1384 );
1385 }
1386 }
1387 }
1388 }
1389 }
1390 }
1391 }
1392 ExprKind::StaticPropertyAccess(_) => {
1393 }
1395 ExprKind::ArrayAccess(aa) => {
1396 if let Some(idx) = &aa.index {
1399 self.analyze(idx, ctx);
1400 }
1401 let mut base = aa.array;
1404 loop {
1405 match &base.kind {
1406 ExprKind::Variable(name) => {
1407 let name_str = name.as_str().trim_start_matches('$');
1408 if !ctx.var_is_defined(name_str) {
1409 ctx.vars.insert(
1410 name_str.to_string(),
1411 Union::single(Atomic::TArray {
1412 key: Box::new(Union::mixed()),
1413 value: Box::new(ty.clone()),
1414 }),
1415 );
1416 ctx.assigned_vars.insert(name_str.to_string());
1417 } else {
1418 let current = ctx.get_var(name_str);
1421 let updated = widen_array_with_value(¤t, &ty);
1422 ctx.set_var(name_str, updated);
1423 }
1424 break;
1425 }
1426 ExprKind::ArrayAccess(inner) => {
1427 if let Some(idx) = &inner.index {
1428 self.analyze(idx, ctx);
1429 }
1430 base = inner.array;
1431 }
1432 _ => break,
1433 }
1434 }
1435 }
1436 _ => {}
1437 }
1438 }
1439
1440 fn offset_to_line_col(&self, offset: u32) -> (u32, u16) {
1447 let lc = self.source_map.offset_to_line_col(offset);
1448 let line = lc.line + 1;
1449
1450 let byte_offset = offset as usize;
1451 let line_start_byte = if byte_offset == 0 {
1452 0
1453 } else {
1454 self.source[..byte_offset]
1455 .rfind('\n')
1456 .map(|p| p + 1)
1457 .unwrap_or(0)
1458 };
1459
1460 let col = self.source[line_start_byte..byte_offset].chars().count() as u16;
1461
1462 (line, col)
1463 }
1464
1465 fn check_type_hint(&mut self, hint: &php_ast::ast::TypeHint<'_, '_>) {
1467 use php_ast::ast::TypeHintKind;
1468 match &hint.kind {
1469 TypeHintKind::Named(name) => {
1470 let name_str = crate::parser::name_to_string(name);
1471 if matches!(
1472 name_str.to_lowercase().as_str(),
1473 "self"
1474 | "static"
1475 | "parent"
1476 | "null"
1477 | "true"
1478 | "false"
1479 | "never"
1480 | "void"
1481 | "mixed"
1482 | "object"
1483 | "callable"
1484 | "iterable"
1485 ) {
1486 return;
1487 }
1488 let resolved = self.codebase.resolve_class_name(&self.file, &name_str);
1489 if !self.codebase.type_exists(&resolved) {
1490 self.emit(
1491 IssueKind::UndefinedClass { name: resolved },
1492 Severity::Error,
1493 hint.span,
1494 );
1495 }
1496 }
1497 TypeHintKind::Nullable(inner) => self.check_type_hint(inner),
1498 TypeHintKind::Union(parts) | TypeHintKind::Intersection(parts) => {
1499 for part in parts.iter() {
1500 self.check_type_hint(part);
1501 }
1502 }
1503 TypeHintKind::Keyword(_, _) => {}
1504 }
1505 }
1506
1507 pub fn emit(&mut self, kind: IssueKind, severity: Severity, span: php_ast::Span) {
1508 let (line, col_start) = self.offset_to_line_col(span.start);
1509
1510 let col_end = if span.start < span.end {
1513 let (_end_line, end_col) = self.offset_to_line_col(span.end);
1514 end_col
1515 } else {
1516 col_start
1517 };
1518
1519 let mut issue = Issue::new(
1520 kind,
1521 Location {
1522 file: self.file.clone(),
1523 line,
1524 col_start,
1525 col_end: col_end.max(col_start + 1),
1526 },
1527 );
1528 issue.severity = severity;
1529 if span.start < span.end {
1531 let s = span.start as usize;
1532 let e = (span.end as usize).min(self.source.len());
1533 if let Some(text) = self.source.get(s..e) {
1534 let trimmed = text.trim();
1535 if !trimmed.is_empty() {
1536 issue.snippet = Some(trimmed.to_string());
1537 }
1538 }
1539 }
1540 self.issues.add(issue);
1541 }
1542}
1543
1544fn widen_array_with_value(current: &Union, new_value: &Union) -> Union {
1552 let mut result = Union::empty();
1553 result.possibly_undefined = current.possibly_undefined;
1554 result.from_docblock = current.from_docblock;
1555 let mut found_array = false;
1556 for atomic in ¤t.types {
1557 match atomic {
1558 Atomic::TKeyedArray { properties, .. } => {
1559 let mut all_values = new_value.clone();
1561 for prop in properties.values() {
1562 all_values = Union::merge(&all_values, &prop.ty);
1563 }
1564 result.add_type(Atomic::TArray {
1565 key: Box::new(Union::mixed()),
1566 value: Box::new(all_values),
1567 });
1568 found_array = true;
1569 }
1570 Atomic::TArray { key, value } => {
1571 let merged = Union::merge(value, new_value);
1572 result.add_type(Atomic::TArray {
1573 key: key.clone(),
1574 value: Box::new(merged),
1575 });
1576 found_array = true;
1577 }
1578 Atomic::TList { value } | Atomic::TNonEmptyList { value } => {
1579 let merged = Union::merge(value, new_value);
1580 result.add_type(Atomic::TList {
1581 value: Box::new(merged),
1582 });
1583 found_array = true;
1584 }
1585 Atomic::TMixed => {
1586 return Union::mixed();
1587 }
1588 other => {
1589 result.add_type(other.clone());
1590 }
1591 }
1592 }
1593 if !found_array {
1594 return current.clone();
1597 }
1598 result
1599}
1600
1601pub fn infer_arithmetic(left: &Union, right: &Union) -> Union {
1602 if left.is_mixed() || right.is_mixed() {
1604 return Union::mixed();
1605 }
1606
1607 let left_is_array = left.contains(|t| {
1609 matches!(
1610 t,
1611 Atomic::TArray { .. }
1612 | Atomic::TNonEmptyArray { .. }
1613 | Atomic::TList { .. }
1614 | Atomic::TNonEmptyList { .. }
1615 | Atomic::TKeyedArray { .. }
1616 )
1617 });
1618 let right_is_array = right.contains(|t| {
1619 matches!(
1620 t,
1621 Atomic::TArray { .. }
1622 | Atomic::TNonEmptyArray { .. }
1623 | Atomic::TList { .. }
1624 | Atomic::TNonEmptyList { .. }
1625 | Atomic::TKeyedArray { .. }
1626 )
1627 });
1628 if left_is_array || right_is_array {
1629 let merged_left = if left_is_array {
1631 left.clone()
1632 } else {
1633 Union::single(Atomic::TArray {
1634 key: Box::new(Union::single(Atomic::TMixed)),
1635 value: Box::new(Union::mixed()),
1636 })
1637 };
1638 return merged_left;
1639 }
1640
1641 let left_is_float = left.contains(|t| matches!(t, Atomic::TFloat | Atomic::TLiteralFloat(..)));
1642 let right_is_float =
1643 right.contains(|t| matches!(t, Atomic::TFloat | Atomic::TLiteralFloat(..)));
1644 if left_is_float || right_is_float {
1645 Union::single(Atomic::TFloat)
1646 } else if left.contains(|t| t.is_int()) && right.contains(|t| t.is_int()) {
1647 Union::single(Atomic::TInt)
1648 } else {
1649 let mut u = Union::empty();
1651 u.add_type(Atomic::TInt);
1652 u.add_type(Atomic::TFloat);
1653 u
1654 }
1655}
1656
1657pub fn extract_simple_var<'arena, 'src>(expr: &php_ast::ast::Expr<'arena, 'src>) -> Option<String> {
1658 match &expr.kind {
1659 ExprKind::Variable(name) => Some(name.as_str().trim_start_matches('$').to_string()),
1660 ExprKind::Parenthesized(inner) => extract_simple_var(inner),
1661 _ => None,
1662 }
1663}
1664
1665pub fn extract_destructure_vars<'arena, 'src>(
1669 expr: &php_ast::ast::Expr<'arena, 'src>,
1670) -> Vec<String> {
1671 match &expr.kind {
1672 ExprKind::Array(elements) => {
1673 let mut vars = vec![];
1674 for elem in elements.iter() {
1675 let sub = extract_destructure_vars(&elem.value);
1677 if sub.is_empty() {
1678 if let Some(v) = extract_simple_var(&elem.value) {
1679 vars.push(v);
1680 }
1681 } else {
1682 vars.extend(sub);
1683 }
1684 }
1685 vars
1686 }
1687 _ => vec![],
1688 }
1689}
1690
1691fn ast_params_to_fn_params_resolved<'arena, 'src>(
1693 params: &php_ast::ast::ArenaVec<'arena, php_ast::ast::Param<'arena, 'src>>,
1694 self_fqcn: Option<&str>,
1695 codebase: &mir_codebase::Codebase,
1696 file: &str,
1697) -> Vec<mir_codebase::FnParam> {
1698 params
1699 .iter()
1700 .map(|p| {
1701 let ty = p
1702 .type_hint
1703 .as_ref()
1704 .map(|h| crate::parser::type_from_hint(h, self_fqcn))
1705 .map(|u| resolve_named_objects_in_union(u, codebase, file));
1706 mir_codebase::FnParam {
1707 name: p.name.trim_start_matches('$').into(),
1708 ty,
1709 default: p.default.as_ref().map(|_| Union::mixed()),
1710 is_variadic: p.variadic,
1711 is_byref: p.by_ref,
1712 is_optional: p.default.is_some() || p.variadic,
1713 }
1714 })
1715 .collect()
1716}
1717
1718fn resolve_named_objects_in_union(
1720 union: Union,
1721 codebase: &mir_codebase::Codebase,
1722 file: &str,
1723) -> Union {
1724 use mir_types::Atomic;
1725 let from_docblock = union.from_docblock;
1726 let possibly_undefined = union.possibly_undefined;
1727 let types: Vec<Atomic> = union
1728 .types
1729 .into_iter()
1730 .map(|a| match a {
1731 Atomic::TNamedObject { fqcn, type_params } => {
1732 let resolved = codebase.resolve_class_name(file, fqcn.as_ref());
1733 Atomic::TNamedObject {
1734 fqcn: resolved.into(),
1735 type_params,
1736 }
1737 }
1738 other => other,
1739 })
1740 .collect();
1741 let mut result = Union::from_vec(types);
1742 result.from_docblock = from_docblock;
1743 result.possibly_undefined = possibly_undefined;
1744 result
1745}
1746
1747fn extract_string_from_expr<'arena, 'src>(
1748 expr: &php_ast::ast::Expr<'arena, 'src>,
1749) -> Option<String> {
1750 match &expr.kind {
1751 ExprKind::Identifier(s) => Some(s.trim_start_matches('$').to_string()),
1752 ExprKind::Variable(_) => None,
1754 ExprKind::String(s) => Some(s.to_string()),
1755 _ => None,
1756 }
1757}
1758
1759#[cfg(test)]
1760mod tests {
1761 fn create_source_map(source: &str) -> php_rs_parser::source_map::SourceMap {
1763 let bump = bumpalo::Bump::new();
1764 let result = php_rs_parser::parse(&bump, source);
1765 result.source_map
1766 }
1767
1768 fn test_offset_conversion(source: &str, offset: u32) -> (u32, u16) {
1770 let source_map = create_source_map(source);
1771 let lc = source_map.offset_to_line_col(offset);
1772 let line = lc.line + 1;
1773
1774 let byte_offset = offset as usize;
1775 let line_start_byte = if byte_offset == 0 {
1776 0
1777 } else {
1778 source[..byte_offset]
1779 .rfind('\n')
1780 .map(|p| p + 1)
1781 .unwrap_or(0)
1782 };
1783
1784 let col = source[line_start_byte..byte_offset].chars().count() as u16;
1785
1786 (line, col)
1787 }
1788
1789 #[test]
1790 fn col_conversion_simple_ascii() {
1791 let source = "<?php\n$var = 123;";
1792
1793 let (line, col) = test_offset_conversion(source, 6);
1795 assert_eq!(line, 2);
1796 assert_eq!(col, 0);
1797
1798 let (line, col) = test_offset_conversion(source, 7);
1800 assert_eq!(line, 2);
1801 assert_eq!(col, 1);
1802 }
1803
1804 #[test]
1805 fn col_conversion_different_lines() {
1806 let source = "<?php\n$x = 1;\n$y = 2;";
1807 let (line, col) = test_offset_conversion(source, 0);
1812 assert_eq!((line, col), (1, 0));
1813
1814 let (line, col) = test_offset_conversion(source, 6);
1815 assert_eq!((line, col), (2, 0));
1816
1817 let (line, col) = test_offset_conversion(source, 14);
1818 assert_eq!((line, col), (3, 0));
1819 }
1820
1821 #[test]
1822 fn col_conversion_accented_characters() {
1823 let source = "<?php\n$café = 1;";
1825 let (line, col) = test_offset_conversion(source, 9);
1830 assert_eq!((line, col), (2, 3));
1831
1832 let (line, col) = test_offset_conversion(source, 10);
1834 assert_eq!((line, col), (2, 4));
1835 }
1836
1837 #[test]
1838 fn col_conversion_emoji_counts_as_one_char() {
1839 let source = "<?php\n$y = \"🎉x\";";
1842 let emoji_start = source.find("🎉").unwrap();
1846 let after_emoji = emoji_start + "🎉".len(); let (line, col) = test_offset_conversion(source, after_emoji as u32);
1850 assert_eq!(line, 2);
1851 assert_eq!(col, 7); }
1853
1854 #[test]
1855 fn col_conversion_emoji_start_position() {
1856 let source = "<?php\n$y = \"🎉\";";
1858 let quote_pos = source.find('"').unwrap();
1862 let emoji_pos = quote_pos + 1; let (line, col) = test_offset_conversion(source, quote_pos as u32);
1865 assert_eq!(line, 2);
1866 assert_eq!(col, 5); let (line, col) = test_offset_conversion(source, emoji_pos as u32);
1869 assert_eq!(line, 2);
1870 assert_eq!(col, 6); }
1872
1873 #[test]
1874 fn col_end_minimum_width() {
1875 let col_start = 0u16;
1877 let col_end = 0u16; let effective_col_end = col_end.max(col_start + 1);
1879
1880 assert_eq!(
1881 effective_col_end, 1,
1882 "col_end should be at least col_start + 1"
1883 );
1884 }
1885
1886 #[test]
1887 fn col_conversion_multiline_span() {
1888 let source = "<?php\n$x = [\n 'a',\n 'b'\n];";
1890 let bracket_open = source.find('[').unwrap();
1898 let (line_start, _col_start) = test_offset_conversion(source, bracket_open as u32);
1899 assert_eq!(line_start, 2);
1900
1901 let bracket_close = source.rfind(']').unwrap();
1903 let (line_end, col_end) = test_offset_conversion(source, bracket_close as u32);
1904 assert_eq!(line_end, 5);
1905 assert_eq!(col_end, 0); }
1907
1908 #[test]
1909 fn col_end_handles_emoji_in_span() {
1910 let source = "<?php\n$greeting = \"Hello 🎉\";";
1912
1913 let emoji_pos = source.find('🎉').unwrap();
1915 let hello_pos = source.find("Hello").unwrap();
1916
1917 let (line, col) = test_offset_conversion(source, hello_pos as u32);
1919 assert_eq!(line, 2);
1920 assert_eq!(col, 13); let (line, col) = test_offset_conversion(source, emoji_pos as u32);
1924 assert_eq!(line, 2);
1925 assert_eq!(col, 19);
1927 }
1928}