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 self.codebase.mark_class_referenced_at(
656 &fqcn,
657 self.file.clone(),
658 n.class.span.start,
659 n.class.span.end,
660 );
661 ty
662 }
663 _ => {
664 self.analyze(n.class, ctx);
665 Union::single(Atomic::TObject)
666 }
667 };
668 class_ty
669 }
670
671 ExprKind::AnonymousClass(_) => Union::single(Atomic::TObject),
672
673 ExprKind::PropertyAccess(pa) => {
675 let obj_ty = self.analyze(pa.object, ctx);
676 let prop_name = extract_string_from_expr(pa.property)
677 .unwrap_or_else(|| "<dynamic>".to_string());
678
679 if obj_ty.contains(|t| matches!(t, Atomic::TNull)) && obj_ty.is_single() {
680 self.emit(
681 IssueKind::NullPropertyFetch {
682 property: prop_name.clone(),
683 },
684 Severity::Error,
685 expr.span,
686 );
687 return Union::mixed();
688 }
689 if obj_ty.is_nullable() {
690 self.emit(
691 IssueKind::PossiblyNullPropertyFetch {
692 property: prop_name.clone(),
693 },
694 Severity::Info,
695 expr.span,
696 );
697 }
698
699 if prop_name == "<dynamic>" {
701 return Union::mixed();
702 }
703 let resolved = self.resolve_property_type(&obj_ty, &prop_name, pa.property.span);
706 for atomic in &obj_ty.types {
708 if let Atomic::TNamedObject { fqcn, .. } = atomic {
709 self.record_symbol(
710 pa.property.span,
711 SymbolKind::PropertyAccess {
712 class: fqcn.clone(),
713 property: Arc::from(prop_name.as_str()),
714 },
715 resolved.clone(),
716 );
717 break;
718 }
719 }
720 resolved
721 }
722
723 ExprKind::NullsafePropertyAccess(pa) => {
724 let obj_ty = self.analyze(pa.object, ctx);
725 let prop_name = extract_string_from_expr(pa.property)
726 .unwrap_or_else(|| "<dynamic>".to_string());
727 if prop_name == "<dynamic>" {
728 return Union::mixed();
729 }
730 let non_null_ty = obj_ty.remove_null();
732 let mut prop_ty =
735 self.resolve_property_type(&non_null_ty, &prop_name, pa.property.span);
736 prop_ty.add_type(Atomic::TNull); for atomic in &non_null_ty.types {
739 if let Atomic::TNamedObject { fqcn, .. } = atomic {
740 self.record_symbol(
741 pa.property.span,
742 SymbolKind::PropertyAccess {
743 class: fqcn.clone(),
744 property: Arc::from(prop_name.as_str()),
745 },
746 prop_ty.clone(),
747 );
748 break;
749 }
750 }
751 prop_ty
752 }
753
754 ExprKind::StaticPropertyAccess(spa) => {
755 if let ExprKind::Identifier(id) = &spa.class.kind {
756 let resolved = self.codebase.resolve_class_name(&self.file, id.as_ref());
757 if !matches!(resolved.as_str(), "self" | "static" | "parent")
758 && !self.codebase.type_exists(&resolved)
759 {
760 self.emit(
761 IssueKind::UndefinedClass { name: resolved },
762 Severity::Error,
763 spa.class.span,
764 );
765 }
766 }
767 Union::mixed()
768 }
769
770 ExprKind::ClassConstAccess(cca) => {
771 if cca.member.name_str() == Some("class") {
773 let fqcn = if let ExprKind::Identifier(id) = &cca.class.kind {
775 let resolved = self.codebase.resolve_class_name(&self.file, id.as_ref());
776 Some(Arc::from(resolved.as_str()))
777 } else {
778 None
779 };
780 return Union::single(Atomic::TClassString(fqcn));
781 }
782
783 let const_name = match cca.member.name_str() {
784 Some(n) => n.to_string(),
785 None => return Union::mixed(),
786 };
787
788 let fqcn = match &cca.class.kind {
789 ExprKind::Identifier(id) => {
790 let resolved = self.codebase.resolve_class_name(&self.file, id.as_ref());
791 if matches!(resolved.as_str(), "self" | "static" | "parent") {
793 return Union::mixed();
794 }
795 resolved
796 }
797 _ => return Union::mixed(),
798 };
799
800 if !self.codebase.type_exists(&fqcn) {
801 self.emit(
802 IssueKind::UndefinedClass { name: fqcn },
803 Severity::Error,
804 cca.class.span,
805 );
806 return Union::mixed();
807 }
808
809 if self
810 .codebase
811 .get_class_constant(&fqcn, &const_name)
812 .is_none()
813 && !self.codebase.has_unknown_ancestor(&fqcn)
814 {
815 self.emit(
816 IssueKind::UndefinedConstant {
817 name: format!("{fqcn}::{const_name}"),
818 },
819 Severity::Error,
820 expr.span,
821 );
822 }
823 Union::mixed()
824 }
825
826 ExprKind::ClassConstAccessDynamic { .. } => Union::mixed(),
827 ExprKind::StaticPropertyAccessDynamic { .. } => Union::mixed(),
828
829 ExprKind::MethodCall(mc) => {
831 CallAnalyzer::analyze_method_call(self, mc, ctx, expr.span, false)
832 }
833
834 ExprKind::NullsafeMethodCall(mc) => {
835 CallAnalyzer::analyze_method_call(self, mc, ctx, expr.span, true)
836 }
837
838 ExprKind::StaticMethodCall(smc) => {
839 CallAnalyzer::analyze_static_method_call(self, smc, ctx, expr.span)
840 }
841
842 ExprKind::StaticDynMethodCall(smc) => {
843 CallAnalyzer::analyze_static_dyn_method_call(self, smc, ctx)
844 }
845
846 ExprKind::FunctionCall(fc) => {
848 CallAnalyzer::analyze_function_call(self, fc, ctx, expr.span)
849 }
850
851 ExprKind::Closure(c) => {
853 for param in c.params.iter() {
855 if let Some(hint) = ¶m.type_hint {
856 self.check_type_hint(hint);
857 }
858 }
859 if let Some(hint) = &c.return_type {
860 self.check_type_hint(hint);
861 }
862
863 let params = ast_params_to_fn_params_resolved(
864 &c.params,
865 ctx.self_fqcn.as_deref(),
866 self.codebase,
867 &self.file,
868 );
869 let return_ty_hint = c
870 .return_type
871 .as_ref()
872 .map(|h| crate::parser::type_from_hint(h, ctx.self_fqcn.as_deref()))
873 .map(|u| resolve_named_objects_in_union(u, self.codebase, &self.file));
874
875 let mut closure_ctx = crate::context::Context::for_function(
879 ¶ms,
880 return_ty_hint.clone(),
881 ctx.self_fqcn.clone(),
882 ctx.parent_fqcn.clone(),
883 ctx.static_fqcn.clone(),
884 ctx.strict_types,
885 c.is_static,
886 );
887 for use_var in c.use_vars.iter() {
888 let name = use_var.name.trim_start_matches('$');
889 closure_ctx.set_var(name, ctx.get_var(name));
890 if ctx.is_tainted(name) {
891 closure_ctx.taint_var(name);
892 }
893 }
894
895 let inferred_return = {
897 let mut sa = crate::stmt::StatementsAnalyzer::new(
898 self.codebase,
899 self.file.clone(),
900 self.source,
901 self.source_map,
902 self.issues,
903 self.symbols,
904 self.php_version,
905 );
906 sa.analyze_stmts(&c.body, &mut closure_ctx);
907 let ret = crate::project::merge_return_types(&sa.return_types);
908 drop(sa);
909 ret
910 };
911
912 for name in &closure_ctx.read_vars {
914 ctx.read_vars.insert(name.clone());
915 }
916
917 let return_ty = return_ty_hint.unwrap_or(inferred_return);
918 let closure_params: Vec<mir_types::atomic::FnParam> = params
919 .iter()
920 .map(|p| mir_types::atomic::FnParam {
921 name: p.name.clone(),
922 ty: p.ty.clone(),
923 default: p.default.clone(),
924 is_variadic: p.is_variadic,
925 is_byref: p.is_byref,
926 is_optional: p.is_optional,
927 })
928 .collect();
929
930 Union::single(Atomic::TClosure {
931 params: closure_params,
932 return_type: Box::new(return_ty),
933 this_type: ctx.self_fqcn.clone().map(|f| {
934 Box::new(Union::single(Atomic::TNamedObject {
935 fqcn: f,
936 type_params: vec![],
937 }))
938 }),
939 })
940 }
941
942 ExprKind::ArrowFunction(af) => {
943 for param in af.params.iter() {
945 if let Some(hint) = ¶m.type_hint {
946 self.check_type_hint(hint);
947 }
948 }
949 if let Some(hint) = &af.return_type {
950 self.check_type_hint(hint);
951 }
952
953 let params = ast_params_to_fn_params_resolved(
954 &af.params,
955 ctx.self_fqcn.as_deref(),
956 self.codebase,
957 &self.file,
958 );
959 let return_ty_hint = af
960 .return_type
961 .as_ref()
962 .map(|h| crate::parser::type_from_hint(h, ctx.self_fqcn.as_deref()))
963 .map(|u| resolve_named_objects_in_union(u, self.codebase, &self.file));
964
965 let mut arrow_ctx = crate::context::Context::for_function(
968 ¶ms,
969 return_ty_hint.clone(),
970 ctx.self_fqcn.clone(),
971 ctx.parent_fqcn.clone(),
972 ctx.static_fqcn.clone(),
973 ctx.strict_types,
974 af.is_static,
975 );
976 for (name, ty) in &ctx.vars {
978 if !arrow_ctx.vars.contains_key(name) {
979 arrow_ctx.set_var(name, ty.clone());
980 }
981 }
982
983 let inferred_return = self.analyze(af.body, &mut arrow_ctx);
985
986 for name in &arrow_ctx.read_vars {
988 ctx.read_vars.insert(name.clone());
989 }
990
991 let return_ty = return_ty_hint.unwrap_or(inferred_return);
992 let closure_params: Vec<mir_types::atomic::FnParam> = params
993 .iter()
994 .map(|p| mir_types::atomic::FnParam {
995 name: p.name.clone(),
996 ty: p.ty.clone(),
997 default: p.default.clone(),
998 is_variadic: p.is_variadic,
999 is_byref: p.is_byref,
1000 is_optional: p.is_optional,
1001 })
1002 .collect();
1003
1004 Union::single(Atomic::TClosure {
1005 params: closure_params,
1006 return_type: Box::new(return_ty),
1007 this_type: if af.is_static {
1008 None
1009 } else {
1010 ctx.self_fqcn.clone().map(|f| {
1011 Box::new(Union::single(Atomic::TNamedObject {
1012 fqcn: f,
1013 type_params: vec![],
1014 }))
1015 })
1016 },
1017 })
1018 }
1019
1020 ExprKind::CallableCreate(_) => Union::single(Atomic::TCallable {
1021 params: None,
1022 return_type: None,
1023 }),
1024
1025 ExprKind::Match(m) => {
1027 let subject_ty = self.analyze(m.subject, ctx);
1028 let subject_var = match &m.subject.kind {
1030 ExprKind::Variable(name) => {
1031 Some(name.as_str().trim_start_matches('$').to_string())
1032 }
1033 _ => None,
1034 };
1035
1036 let mut result = Union::empty();
1037 for arm in m.arms.iter() {
1038 let mut arm_ctx = ctx.fork();
1040
1041 if let (Some(var), Some(conditions)) = (&subject_var, &arm.conditions) {
1043 let mut arm_ty = Union::empty();
1045 for cond in conditions.iter() {
1046 let cond_ty = self.analyze(cond, ctx);
1047 arm_ty = Union::merge(&arm_ty, &cond_ty);
1048 }
1049 if !arm_ty.is_empty() && !arm_ty.is_mixed() {
1051 let narrowed = subject_ty.intersect_with(&arm_ty);
1053 if !narrowed.is_empty() {
1054 arm_ctx.set_var(var, narrowed);
1055 }
1056 }
1057 }
1058
1059 if let Some(conditions) = &arm.conditions {
1062 for cond in conditions.iter() {
1063 crate::narrowing::narrow_from_condition(
1064 cond,
1065 &mut arm_ctx,
1066 true,
1067 self.codebase,
1068 &self.file,
1069 );
1070 }
1071 }
1072
1073 let arm_body_ty = self.analyze(&arm.body, &mut arm_ctx);
1074 result = Union::merge(&result, &arm_body_ty);
1075
1076 for name in &arm_ctx.read_vars {
1078 ctx.read_vars.insert(name.clone());
1079 }
1080 }
1081 if result.is_empty() {
1082 Union::mixed()
1083 } else {
1084 result
1085 }
1086 }
1087
1088 ExprKind::ThrowExpr(e) => {
1090 self.analyze(e, ctx);
1091 Union::single(Atomic::TNever)
1092 }
1093
1094 ExprKind::Yield(y) => {
1096 if let Some(key) = &y.key {
1097 self.analyze(key, ctx);
1098 }
1099 if let Some(value) = &y.value {
1100 self.analyze(value, ctx);
1101 }
1102 Union::mixed()
1103 }
1104
1105 ExprKind::MagicConst(kind) => match kind {
1107 MagicConstKind::Line => Union::single(Atomic::TInt),
1108 MagicConstKind::File
1109 | MagicConstKind::Dir
1110 | MagicConstKind::Function
1111 | MagicConstKind::Class
1112 | MagicConstKind::Method
1113 | MagicConstKind::Namespace
1114 | MagicConstKind::Trait
1115 | MagicConstKind::Property => Union::single(Atomic::TString),
1116 },
1117
1118 ExprKind::Include(_, inner) => {
1120 self.analyze(inner, ctx);
1121 Union::mixed()
1122 }
1123
1124 ExprKind::Eval(inner) => {
1126 self.analyze(inner, ctx);
1127 Union::mixed()
1128 }
1129
1130 ExprKind::Exit(opt) => {
1132 if let Some(e) = opt {
1133 self.analyze(e, ctx);
1134 }
1135 ctx.diverges = true;
1136 Union::single(Atomic::TNever)
1137 }
1138
1139 ExprKind::Error => Union::mixed(),
1141
1142 ExprKind::Omit => Union::single(Atomic::TNull),
1144 }
1145 }
1146
1147 fn analyze_binary<'arena, 'src>(
1152 &mut self,
1153 b: &php_ast::ast::BinaryExpr<'arena, 'src>,
1154 _span: php_ast::Span,
1155 ctx: &mut Context,
1156 ) -> Union {
1157 use php_ast::ast::BinaryOp as B;
1163 if matches!(
1164 b.op,
1165 B::BooleanAnd | B::LogicalAnd | B::BooleanOr | B::LogicalOr
1166 ) {
1167 let _left_ty = self.analyze(b.left, ctx);
1168 let mut right_ctx = ctx.fork();
1169 let is_and = matches!(b.op, B::BooleanAnd | B::LogicalAnd);
1170 crate::narrowing::narrow_from_condition(
1171 b.left,
1172 &mut right_ctx,
1173 is_and,
1174 self.codebase,
1175 &self.file,
1176 );
1177 if !right_ctx.diverges {
1180 let _right_ty = self.analyze(b.right, &mut right_ctx);
1181 }
1182 for v in right_ctx.read_vars {
1186 ctx.read_vars.insert(v.clone());
1187 }
1188 for (name, ty) in &right_ctx.vars {
1189 if !ctx.vars.contains_key(name.as_str()) {
1190 ctx.vars.insert(name.clone(), ty.clone());
1192 ctx.possibly_assigned_vars.insert(name.clone());
1193 }
1194 }
1195 return Union::single(Atomic::TBool);
1196 }
1197
1198 if b.op == B::Instanceof {
1200 let _left_ty = self.analyze(b.left, ctx);
1201 if let ExprKind::Identifier(name) = &b.right.kind {
1202 let resolved = self.codebase.resolve_class_name(&self.file, name.as_ref());
1203 let fqcn: std::sync::Arc<str> = std::sync::Arc::from(resolved.as_str());
1204 if !matches!(resolved.as_str(), "self" | "static" | "parent")
1205 && !self.codebase.type_exists(&fqcn)
1206 {
1207 self.emit(
1208 IssueKind::UndefinedClass { name: resolved },
1209 Severity::Error,
1210 b.right.span,
1211 );
1212 }
1213 }
1214 return Union::single(Atomic::TBool);
1215 }
1216
1217 let left_ty = self.analyze(b.left, ctx);
1218 let right_ty = self.analyze(b.right, ctx);
1219
1220 match b.op {
1221 BinaryOp::Add
1223 | BinaryOp::Sub
1224 | BinaryOp::Mul
1225 | BinaryOp::Div
1226 | BinaryOp::Mod
1227 | BinaryOp::Pow => infer_arithmetic(&left_ty, &right_ty),
1228
1229 BinaryOp::Concat => Union::single(Atomic::TString),
1231
1232 BinaryOp::Equal
1234 | BinaryOp::NotEqual
1235 | BinaryOp::Identical
1236 | BinaryOp::NotIdentical
1237 | BinaryOp::Less
1238 | BinaryOp::Greater
1239 | BinaryOp::LessOrEqual
1240 | BinaryOp::GreaterOrEqual => Union::single(Atomic::TBool),
1241
1242 BinaryOp::Spaceship => Union::single(Atomic::TIntRange {
1244 min: Some(-1),
1245 max: Some(1),
1246 }),
1247
1248 BinaryOp::BooleanAnd
1250 | BinaryOp::BooleanOr
1251 | BinaryOp::LogicalAnd
1252 | BinaryOp::LogicalOr
1253 | BinaryOp::LogicalXor => Union::single(Atomic::TBool),
1254
1255 BinaryOp::BitwiseAnd
1257 | BinaryOp::BitwiseOr
1258 | BinaryOp::BitwiseXor
1259 | BinaryOp::ShiftLeft
1260 | BinaryOp::ShiftRight => Union::single(Atomic::TInt),
1261
1262 BinaryOp::Pipe => right_ty,
1264
1265 BinaryOp::Instanceof => Union::single(Atomic::TBool),
1267 }
1268 }
1269
1270 fn resolve_property_type(
1275 &mut self,
1276 obj_ty: &Union,
1277 prop_name: &str,
1278 span: php_ast::Span,
1279 ) -> Union {
1280 for atomic in &obj_ty.types {
1281 match atomic {
1282 Atomic::TNamedObject { fqcn, .. }
1283 if self.codebase.classes.contains_key(fqcn.as_ref()) =>
1284 {
1285 if let Some(prop) = self.codebase.get_property(fqcn.as_ref(), prop_name) {
1286 self.codebase.mark_property_referenced_at(
1288 fqcn,
1289 prop_name,
1290 self.file.clone(),
1291 span.start,
1292 span.end,
1293 );
1294 return prop.ty.clone().unwrap_or_else(Union::mixed);
1295 }
1296 if !self.codebase.has_unknown_ancestor(fqcn.as_ref())
1298 && !self.codebase.has_magic_get(fqcn.as_ref())
1299 {
1300 self.emit(
1301 IssueKind::UndefinedProperty {
1302 class: fqcn.to_string(),
1303 property: prop_name.to_string(),
1304 },
1305 Severity::Warning,
1306 span,
1307 );
1308 }
1309 return Union::mixed();
1310 }
1311 Atomic::TMixed => return Union::mixed(),
1312 _ => {}
1313 }
1314 }
1315 Union::mixed()
1316 }
1317
1318 fn assign_to_target<'arena, 'src>(
1323 &mut self,
1324 target: &php_ast::ast::Expr<'arena, 'src>,
1325 ty: Union,
1326 ctx: &mut Context,
1327 span: php_ast::Span,
1328 ) {
1329 match &target.kind {
1330 ExprKind::Variable(name) => {
1331 let name_str = name.as_str().trim_start_matches('$').to_string();
1332 if ctx.byref_param_names.contains(&name_str) {
1333 ctx.read_vars.insert(name_str.clone());
1334 }
1335 ctx.set_var(name_str, ty);
1336 }
1337 ExprKind::Array(elements) => {
1338 let has_non_array = ty.contains(|a| matches!(a, Atomic::TFalse | Atomic::TNull));
1342 let has_array = ty.contains(|a| {
1343 matches!(
1344 a,
1345 Atomic::TArray { .. }
1346 | Atomic::TList { .. }
1347 | Atomic::TNonEmptyArray { .. }
1348 | Atomic::TNonEmptyList { .. }
1349 | Atomic::TKeyedArray { .. }
1350 )
1351 });
1352 if has_non_array && has_array {
1353 let actual = format!("{ty}");
1354 self.emit(
1355 IssueKind::PossiblyInvalidArrayOffset {
1356 expected: "array".to_string(),
1357 actual,
1358 },
1359 Severity::Warning,
1360 span,
1361 );
1362 }
1363
1364 let value_ty: Union = ty
1366 .types
1367 .iter()
1368 .find_map(|a| match a {
1369 Atomic::TArray { value, .. }
1370 | Atomic::TList { value }
1371 | Atomic::TNonEmptyArray { value, .. }
1372 | Atomic::TNonEmptyList { value } => Some(*value.clone()),
1373 _ => None,
1374 })
1375 .unwrap_or_else(Union::mixed);
1376
1377 for elem in elements.iter() {
1378 self.assign_to_target(&elem.value, value_ty.clone(), ctx, span);
1379 }
1380 }
1381 ExprKind::PropertyAccess(pa) => {
1382 let obj_ty = self.analyze(pa.object, ctx);
1384 if let Some(prop_name) = extract_string_from_expr(pa.property) {
1385 for atomic in &obj_ty.types {
1386 if let Atomic::TNamedObject { fqcn, .. } = atomic {
1387 if let Some(cls) = self.codebase.classes.get(fqcn.as_ref()) {
1388 if let Some(prop) = cls.get_property(&prop_name) {
1389 if prop.is_readonly && !ctx.inside_constructor {
1390 self.emit(
1391 IssueKind::ReadonlyPropertyAssignment {
1392 class: fqcn.to_string(),
1393 property: prop_name.clone(),
1394 },
1395 Severity::Error,
1396 span,
1397 );
1398 }
1399 if let Some(prop_ty) = &prop.ty {
1400 if !prop_ty.is_mixed()
1401 && !ty.is_mixed()
1402 && !property_assign_compatible(
1403 &ty,
1404 prop_ty,
1405 self.codebase,
1406 )
1407 {
1408 self.emit(
1409 IssueKind::InvalidPropertyAssignment {
1410 property: prop_name.clone(),
1411 expected: format!("{prop_ty}"),
1412 actual: format!("{ty}"),
1413 },
1414 Severity::Warning,
1415 span,
1416 );
1417 }
1418 }
1419 }
1420 }
1421 }
1422 }
1423 }
1424 }
1425 ExprKind::StaticPropertyAccess(_) => {
1426 }
1428 ExprKind::ArrayAccess(aa) => {
1429 if let Some(idx) = &aa.index {
1432 self.analyze(idx, ctx);
1433 }
1434 let mut base = aa.array;
1437 loop {
1438 match &base.kind {
1439 ExprKind::Variable(name) => {
1440 let name_str = name.as_str().trim_start_matches('$');
1441 if !ctx.var_is_defined(name_str) {
1442 ctx.vars.insert(
1443 name_str.to_string(),
1444 Union::single(Atomic::TArray {
1445 key: Box::new(Union::mixed()),
1446 value: Box::new(ty.clone()),
1447 }),
1448 );
1449 ctx.assigned_vars.insert(name_str.to_string());
1450 } else {
1451 let current = ctx.get_var(name_str);
1454 let updated = widen_array_with_value(¤t, &ty);
1455 ctx.set_var(name_str, updated);
1456 }
1457 break;
1458 }
1459 ExprKind::ArrayAccess(inner) => {
1460 if let Some(idx) = &inner.index {
1461 self.analyze(idx, ctx);
1462 }
1463 base = inner.array;
1464 }
1465 _ => break,
1466 }
1467 }
1468 }
1469 _ => {}
1470 }
1471 }
1472
1473 fn offset_to_line_col(&self, offset: u32) -> (u32, u16) {
1480 let lc = self.source_map.offset_to_line_col(offset);
1481 let line = lc.line + 1;
1482
1483 let byte_offset = offset as usize;
1484 let line_start_byte = if byte_offset == 0 {
1485 0
1486 } else {
1487 self.source[..byte_offset]
1488 .rfind('\n')
1489 .map(|p| p + 1)
1490 .unwrap_or(0)
1491 };
1492
1493 let col = self.source[line_start_byte..byte_offset].chars().count() as u16;
1494
1495 (line, col)
1496 }
1497
1498 fn check_type_hint(&mut self, hint: &php_ast::ast::TypeHint<'_, '_>) {
1500 use php_ast::ast::TypeHintKind;
1501 match &hint.kind {
1502 TypeHintKind::Named(name) => {
1503 let name_str = crate::parser::name_to_string(name);
1504 if matches!(
1505 name_str.to_lowercase().as_str(),
1506 "self"
1507 | "static"
1508 | "parent"
1509 | "null"
1510 | "true"
1511 | "false"
1512 | "never"
1513 | "void"
1514 | "mixed"
1515 | "object"
1516 | "callable"
1517 | "iterable"
1518 ) {
1519 return;
1520 }
1521 let resolved = self.codebase.resolve_class_name(&self.file, &name_str);
1522 if !self.codebase.type_exists(&resolved) {
1523 self.emit(
1524 IssueKind::UndefinedClass { name: resolved },
1525 Severity::Error,
1526 hint.span,
1527 );
1528 }
1529 }
1530 TypeHintKind::Nullable(inner) => self.check_type_hint(inner),
1531 TypeHintKind::Union(parts) | TypeHintKind::Intersection(parts) => {
1532 for part in parts.iter() {
1533 self.check_type_hint(part);
1534 }
1535 }
1536 TypeHintKind::Keyword(_, _) => {}
1537 }
1538 }
1539
1540 pub fn emit(&mut self, kind: IssueKind, severity: Severity, span: php_ast::Span) {
1541 let (line, col_start) = self.offset_to_line_col(span.start);
1542
1543 let (line_end, col_end) = if span.start < span.end {
1544 let (end_line, end_col) = self.offset_to_line_col(span.end);
1545 (end_line, end_col)
1546 } else {
1547 (line, col_start)
1548 };
1549
1550 let mut issue = Issue::new(
1551 kind,
1552 Location {
1553 file: self.file.clone(),
1554 line,
1555 line_end,
1556 col_start,
1557 col_end: col_end.max(col_start + 1),
1558 },
1559 );
1560 issue.severity = severity;
1561 if span.start < span.end {
1563 let s = span.start as usize;
1564 let e = (span.end as usize).min(self.source.len());
1565 if let Some(text) = self.source.get(s..e) {
1566 let trimmed = text.trim();
1567 if !trimmed.is_empty() {
1568 issue.snippet = Some(trimmed.to_string());
1569 }
1570 }
1571 }
1572 self.issues.add(issue);
1573 }
1574}
1575
1576fn widen_array_with_value(current: &Union, new_value: &Union) -> Union {
1584 let mut result = Union::empty();
1585 result.possibly_undefined = current.possibly_undefined;
1586 result.from_docblock = current.from_docblock;
1587 let mut found_array = false;
1588 for atomic in ¤t.types {
1589 match atomic {
1590 Atomic::TKeyedArray { properties, .. } => {
1591 let mut all_values = new_value.clone();
1593 for prop in properties.values() {
1594 all_values = Union::merge(&all_values, &prop.ty);
1595 }
1596 result.add_type(Atomic::TArray {
1597 key: Box::new(Union::mixed()),
1598 value: Box::new(all_values),
1599 });
1600 found_array = true;
1601 }
1602 Atomic::TArray { key, value } => {
1603 let merged = Union::merge(value, new_value);
1604 result.add_type(Atomic::TArray {
1605 key: key.clone(),
1606 value: Box::new(merged),
1607 });
1608 found_array = true;
1609 }
1610 Atomic::TList { value } | Atomic::TNonEmptyList { value } => {
1611 let merged = Union::merge(value, new_value);
1612 result.add_type(Atomic::TList {
1613 value: Box::new(merged),
1614 });
1615 found_array = true;
1616 }
1617 Atomic::TMixed => {
1618 return Union::mixed();
1619 }
1620 other => {
1621 result.add_type(other.clone());
1622 }
1623 }
1624 }
1625 if !found_array {
1626 return current.clone();
1629 }
1630 result
1631}
1632
1633pub fn infer_arithmetic(left: &Union, right: &Union) -> Union {
1634 if left.is_mixed() || right.is_mixed() {
1636 return Union::mixed();
1637 }
1638
1639 let left_is_array = left.contains(|t| {
1641 matches!(
1642 t,
1643 Atomic::TArray { .. }
1644 | Atomic::TNonEmptyArray { .. }
1645 | Atomic::TList { .. }
1646 | Atomic::TNonEmptyList { .. }
1647 | Atomic::TKeyedArray { .. }
1648 )
1649 });
1650 let right_is_array = right.contains(|t| {
1651 matches!(
1652 t,
1653 Atomic::TArray { .. }
1654 | Atomic::TNonEmptyArray { .. }
1655 | Atomic::TList { .. }
1656 | Atomic::TNonEmptyList { .. }
1657 | Atomic::TKeyedArray { .. }
1658 )
1659 });
1660 if left_is_array || right_is_array {
1661 let merged_left = if left_is_array {
1663 left.clone()
1664 } else {
1665 Union::single(Atomic::TArray {
1666 key: Box::new(Union::single(Atomic::TMixed)),
1667 value: Box::new(Union::mixed()),
1668 })
1669 };
1670 return merged_left;
1671 }
1672
1673 let left_is_float = left.contains(|t| matches!(t, Atomic::TFloat | Atomic::TLiteralFloat(..)));
1674 let right_is_float =
1675 right.contains(|t| matches!(t, Atomic::TFloat | Atomic::TLiteralFloat(..)));
1676 if left_is_float || right_is_float {
1677 Union::single(Atomic::TFloat)
1678 } else if left.contains(|t| t.is_int()) && right.contains(|t| t.is_int()) {
1679 Union::single(Atomic::TInt)
1680 } else {
1681 let mut u = Union::empty();
1683 u.add_type(Atomic::TInt);
1684 u.add_type(Atomic::TFloat);
1685 u
1686 }
1687}
1688
1689pub fn extract_simple_var<'arena, 'src>(expr: &php_ast::ast::Expr<'arena, 'src>) -> Option<String> {
1690 match &expr.kind {
1691 ExprKind::Variable(name) => Some(name.as_str().trim_start_matches('$').to_string()),
1692 ExprKind::Parenthesized(inner) => extract_simple_var(inner),
1693 _ => None,
1694 }
1695}
1696
1697pub fn extract_destructure_vars<'arena, 'src>(
1701 expr: &php_ast::ast::Expr<'arena, 'src>,
1702) -> Vec<String> {
1703 match &expr.kind {
1704 ExprKind::Array(elements) => {
1705 let mut vars = vec![];
1706 for elem in elements.iter() {
1707 let sub = extract_destructure_vars(&elem.value);
1709 if sub.is_empty() {
1710 if let Some(v) = extract_simple_var(&elem.value) {
1711 vars.push(v);
1712 }
1713 } else {
1714 vars.extend(sub);
1715 }
1716 }
1717 vars
1718 }
1719 _ => vec![],
1720 }
1721}
1722
1723fn ast_params_to_fn_params_resolved<'arena, 'src>(
1725 params: &php_ast::ast::ArenaVec<'arena, php_ast::ast::Param<'arena, 'src>>,
1726 self_fqcn: Option<&str>,
1727 codebase: &mir_codebase::Codebase,
1728 file: &str,
1729) -> Vec<mir_codebase::FnParam> {
1730 params
1731 .iter()
1732 .map(|p| {
1733 let ty = p
1734 .type_hint
1735 .as_ref()
1736 .map(|h| crate::parser::type_from_hint(h, self_fqcn))
1737 .map(|u| resolve_named_objects_in_union(u, codebase, file));
1738 mir_codebase::FnParam {
1739 name: p.name.trim_start_matches('$').into(),
1740 ty,
1741 default: p.default.as_ref().map(|_| Union::mixed()),
1742 is_variadic: p.variadic,
1743 is_byref: p.by_ref,
1744 is_optional: p.default.is_some() || p.variadic,
1745 }
1746 })
1747 .collect()
1748}
1749
1750fn resolve_named_objects_in_union(
1752 union: Union,
1753 codebase: &mir_codebase::Codebase,
1754 file: &str,
1755) -> Union {
1756 use mir_types::Atomic;
1757 let from_docblock = union.from_docblock;
1758 let possibly_undefined = union.possibly_undefined;
1759 let types: Vec<Atomic> = union
1760 .types
1761 .into_iter()
1762 .map(|a| match a {
1763 Atomic::TNamedObject { fqcn, type_params } => {
1764 let resolved = codebase.resolve_class_name(file, fqcn.as_ref());
1765 Atomic::TNamedObject {
1766 fqcn: resolved.into(),
1767 type_params,
1768 }
1769 }
1770 other => other,
1771 })
1772 .collect();
1773 let mut result = Union::from_vec(types);
1774 result.from_docblock = from_docblock;
1775 result.possibly_undefined = possibly_undefined;
1776 result
1777}
1778
1779fn extract_string_from_expr<'arena, 'src>(
1780 expr: &php_ast::ast::Expr<'arena, 'src>,
1781) -> Option<String> {
1782 match &expr.kind {
1783 ExprKind::Identifier(s) => Some(s.trim_start_matches('$').to_string()),
1784 ExprKind::Variable(_) => None,
1786 ExprKind::String(s) => Some(s.to_string()),
1787 _ => None,
1788 }
1789}
1790
1791fn property_assign_compatible(
1794 value_ty: &mir_types::Union,
1795 prop_ty: &mir_types::Union,
1796 codebase: &mir_codebase::Codebase,
1797) -> bool {
1798 if value_ty.is_subtype_of_simple(prop_ty) {
1799 return true;
1800 }
1801 value_ty.types.iter().all(|a| match a {
1803 mir_types::Atomic::TNamedObject { fqcn: arg_fqcn, .. }
1805 | mir_types::Atomic::TSelf { fqcn: arg_fqcn }
1806 | mir_types::Atomic::TStaticObject { fqcn: arg_fqcn }
1807 | mir_types::Atomic::TParent { fqcn: arg_fqcn } => {
1808 prop_ty.types.iter().any(|p| match p {
1809 mir_types::Atomic::TNamedObject { fqcn: prop_fqcn, .. } => {
1810 arg_fqcn == prop_fqcn
1811 || codebase.extends_or_implements(arg_fqcn.as_ref(), prop_fqcn.as_ref())
1812 }
1813 mir_types::Atomic::TObject | mir_types::Atomic::TMixed => true,
1814 _ => false,
1815 })
1816 }
1817 mir_types::Atomic::TTemplateParam { .. } => true,
1819 mir_types::Atomic::TClosure { .. } | mir_types::Atomic::TCallable { .. } => {
1821 prop_ty.types.iter().any(|p| matches!(p, mir_types::Atomic::TClosure { .. } | mir_types::Atomic::TCallable { .. })
1822 || matches!(p, mir_types::Atomic::TNamedObject { fqcn, .. } if fqcn.as_ref() == "Closure"))
1823 }
1824 mir_types::Atomic::TNever => true,
1825 mir_types::Atomic::TNull => prop_ty.is_nullable(),
1827 _ => false,
1829 })
1830}
1831
1832#[cfg(test)]
1833mod tests {
1834 fn create_source_map(source: &str) -> php_rs_parser::source_map::SourceMap {
1836 let bump = bumpalo::Bump::new();
1837 let result = php_rs_parser::parse(&bump, source);
1838 result.source_map
1839 }
1840
1841 fn test_offset_conversion(source: &str, offset: u32) -> (u32, u16) {
1843 let source_map = create_source_map(source);
1844 let lc = source_map.offset_to_line_col(offset);
1845 let line = lc.line + 1;
1846
1847 let byte_offset = offset as usize;
1848 let line_start_byte = if byte_offset == 0 {
1849 0
1850 } else {
1851 source[..byte_offset]
1852 .rfind('\n')
1853 .map(|p| p + 1)
1854 .unwrap_or(0)
1855 };
1856
1857 let col = source[line_start_byte..byte_offset].chars().count() as u16;
1858
1859 (line, col)
1860 }
1861
1862 #[test]
1863 fn col_conversion_simple_ascii() {
1864 let source = "<?php\n$var = 123;";
1865
1866 let (line, col) = test_offset_conversion(source, 6);
1868 assert_eq!(line, 2);
1869 assert_eq!(col, 0);
1870
1871 let (line, col) = test_offset_conversion(source, 7);
1873 assert_eq!(line, 2);
1874 assert_eq!(col, 1);
1875 }
1876
1877 #[test]
1878 fn col_conversion_different_lines() {
1879 let source = "<?php\n$x = 1;\n$y = 2;";
1880 let (line, col) = test_offset_conversion(source, 0);
1885 assert_eq!((line, col), (1, 0));
1886
1887 let (line, col) = test_offset_conversion(source, 6);
1888 assert_eq!((line, col), (2, 0));
1889
1890 let (line, col) = test_offset_conversion(source, 14);
1891 assert_eq!((line, col), (3, 0));
1892 }
1893
1894 #[test]
1895 fn col_conversion_accented_characters() {
1896 let source = "<?php\n$café = 1;";
1898 let (line, col) = test_offset_conversion(source, 9);
1903 assert_eq!((line, col), (2, 3));
1904
1905 let (line, col) = test_offset_conversion(source, 10);
1907 assert_eq!((line, col), (2, 4));
1908 }
1909
1910 #[test]
1911 fn col_conversion_emoji_counts_as_one_char() {
1912 let source = "<?php\n$y = \"🎉x\";";
1915 let emoji_start = source.find("🎉").unwrap();
1919 let after_emoji = emoji_start + "🎉".len(); let (line, col) = test_offset_conversion(source, after_emoji as u32);
1923 assert_eq!(line, 2);
1924 assert_eq!(col, 7); }
1926
1927 #[test]
1928 fn col_conversion_emoji_start_position() {
1929 let source = "<?php\n$y = \"🎉\";";
1931 let quote_pos = source.find('"').unwrap();
1935 let emoji_pos = quote_pos + 1; let (line, col) = test_offset_conversion(source, quote_pos as u32);
1938 assert_eq!(line, 2);
1939 assert_eq!(col, 5); let (line, col) = test_offset_conversion(source, emoji_pos as u32);
1942 assert_eq!(line, 2);
1943 assert_eq!(col, 6); }
1945
1946 #[test]
1947 fn col_end_minimum_width() {
1948 let col_start = 0u16;
1950 let col_end = 0u16; let effective_col_end = col_end.max(col_start + 1);
1952
1953 assert_eq!(
1954 effective_col_end, 1,
1955 "col_end should be at least col_start + 1"
1956 );
1957 }
1958
1959 #[test]
1960 fn col_conversion_multiline_span() {
1961 let source = "<?php\n$x = [\n 'a',\n 'b'\n];";
1963 let bracket_open = source.find('[').unwrap();
1971 let (line_start, _col_start) = test_offset_conversion(source, bracket_open as u32);
1972 assert_eq!(line_start, 2);
1973
1974 let bracket_close = source.rfind(']').unwrap();
1976 let (line_end, col_end) = test_offset_conversion(source, bracket_close as u32);
1977 assert_eq!(line_end, 5);
1978 assert_eq!(col_end, 0); }
1980
1981 #[test]
1982 fn col_end_handles_emoji_in_span() {
1983 let source = "<?php\n$greeting = \"Hello 🎉\";";
1985
1986 let emoji_pos = source.find('🎉').unwrap();
1988 let hello_pos = source.find("Hello").unwrap();
1989
1990 let (line, col) = test_offset_conversion(source, hello_pos as u32);
1992 assert_eq!(line, 2);
1993 assert_eq!(col, 13); let (line, col) = test_offset_conversion(source, emoji_pos as u32);
1997 assert_eq!(line, 2);
1998 assert_eq!(col, 19);
2000 }
2001}