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