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