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