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 let (line, col_start) = self.offset_to_line_col(a.target.span.start);
209 let (line_end, col_end) = self.offset_to_line_col(a.target.span.end);
210 ctx.record_var_location(&var_name, line, col_start, line_end, col_end);
211 }
212 Union::single(Atomic::TString)
213 }
214 AssignOp::Plus
215 | AssignOp::Minus
216 | AssignOp::Mul
217 | AssignOp::Div
218 | AssignOp::Mod
219 | AssignOp::Pow => {
220 let lhs_ty = self.analyze(a.target, ctx);
221 let result_ty = infer_arithmetic(&lhs_ty, &rhs_ty);
222 if let Some(var_name) = extract_simple_var(a.target) {
223 ctx.set_var(&var_name, result_ty.clone());
224 let (line, col_start) = self.offset_to_line_col(a.target.span.start);
225 let (line_end, col_end) = self.offset_to_line_col(a.target.span.end);
226 ctx.record_var_location(&var_name, line, col_start, line_end, col_end);
227 }
228 result_ty
229 }
230 AssignOp::Coalesce => {
231 let lhs_ty = self.analyze(a.target, ctx);
233 let merged = Union::merge(&lhs_ty.remove_null(), &rhs_ty);
234 if let Some(var_name) = extract_simple_var(a.target) {
235 ctx.set_var(&var_name, merged.clone());
236 let (line, col_start) = self.offset_to_line_col(a.target.span.start);
237 let (line_end, col_end) = self.offset_to_line_col(a.target.span.end);
238 ctx.record_var_location(&var_name, line, col_start, line_end, col_end);
239 }
240 merged
241 }
242 _ => {
243 if let Some(var_name) = extract_simple_var(a.target) {
244 ctx.set_var(&var_name, Union::mixed());
245 let (line, col_start) = self.offset_to_line_col(a.target.span.start);
246 let (line_end, col_end) = self.offset_to_line_col(a.target.span.end);
247 ctx.record_var_location(&var_name, line, col_start, line_end, col_end);
248 }
249 Union::mixed()
250 }
251 }
252 }
253
254 ExprKind::Binary(b) => self.analyze_binary(b, expr.span, ctx),
256
257 ExprKind::UnaryPrefix(u) => {
259 let operand_ty = self.analyze(u.operand, ctx);
260 match u.op {
261 UnaryPrefixOp::BooleanNot => Union::single(Atomic::TBool),
262 UnaryPrefixOp::Negate => {
263 if operand_ty.contains(|t| t.is_int()) {
264 Union::single(Atomic::TInt)
265 } else {
266 Union::single(Atomic::TFloat)
267 }
268 }
269 UnaryPrefixOp::Plus => operand_ty,
270 UnaryPrefixOp::BitwiseNot => Union::single(Atomic::TInt),
271 UnaryPrefixOp::PreIncrement | UnaryPrefixOp::PreDecrement => {
272 if let Some(var_name) = extract_simple_var(u.operand) {
274 let ty = ctx.get_var(&var_name);
275 let new_ty = if ty.contains(|t| {
276 matches!(t, Atomic::TFloat | Atomic::TLiteralFloat(..))
277 }) {
278 Union::single(Atomic::TFloat)
279 } else {
280 Union::single(Atomic::TInt)
281 };
282 ctx.set_var(&var_name, new_ty.clone());
283 let (line, col_start) = self.offset_to_line_col(u.operand.span.start);
284 let (line_end, col_end) = self.offset_to_line_col(u.operand.span.end);
285 ctx.record_var_location(&var_name, line, col_start, line_end, col_end);
286 new_ty
287 } else {
288 Union::single(Atomic::TInt)
289 }
290 }
291 }
292 }
293
294 ExprKind::UnaryPostfix(u) => {
295 let operand_ty = self.analyze(u.operand, ctx);
296 match u.op {
298 UnaryPostfixOp::PostIncrement | UnaryPostfixOp::PostDecrement => {
299 if let Some(var_name) = extract_simple_var(u.operand) {
300 let new_ty = if operand_ty.contains(|t| {
301 matches!(t, Atomic::TFloat | Atomic::TLiteralFloat(..))
302 }) {
303 Union::single(Atomic::TFloat)
304 } else {
305 Union::single(Atomic::TInt)
306 };
307 ctx.set_var(&var_name, new_ty);
308 let (line, col_start) = self.offset_to_line_col(u.operand.span.start);
309 let (line_end, col_end) = self.offset_to_line_col(u.operand.span.end);
310 ctx.record_var_location(&var_name, line, col_start, line_end, col_end);
311 }
312 operand_ty }
314 }
315 }
316
317 ExprKind::Ternary(t) => {
319 let cond_ty = self.analyze(t.condition, ctx);
320 match &t.then_expr {
321 Some(then_expr) => {
322 let mut then_ctx = ctx.fork();
323 crate::narrowing::narrow_from_condition(
324 t.condition,
325 &mut then_ctx,
326 true,
327 self.db,
328 &self.file,
329 );
330 let then_ty = self.analyze(then_expr, &mut then_ctx);
331
332 let mut else_ctx = ctx.fork();
333 crate::narrowing::narrow_from_condition(
334 t.condition,
335 &mut else_ctx,
336 false,
337 self.db,
338 &self.file,
339 );
340 let else_ty = self.analyze(t.else_expr, &mut else_ctx);
341
342 for name in then_ctx.read_vars.iter().chain(else_ctx.read_vars.iter()) {
344 ctx.read_vars.insert(name.clone());
345 }
346
347 Union::merge(&then_ty, &else_ty)
348 }
349 None => {
350 let else_ty = self.analyze(t.else_expr, ctx);
352 let truthy_ty = cond_ty.narrow_to_truthy();
353 if truthy_ty.is_empty() {
354 else_ty
355 } else {
356 Union::merge(&truthy_ty, &else_ty)
357 }
358 }
359 }
360 }
361
362 ExprKind::NullCoalesce(nc) => {
363 let left_ty = self.analyze(nc.left, ctx);
364 let right_ty = self.analyze(nc.right, ctx);
365 let non_null_left = left_ty.remove_null();
367 if non_null_left.is_empty() {
368 right_ty
369 } else {
370 Union::merge(&non_null_left, &right_ty)
371 }
372 }
373
374 ExprKind::Cast(kind, inner) => {
376 let _inner_ty = self.analyze(inner, ctx);
377 match kind {
378 CastKind::Int => Union::single(Atomic::TInt),
379 CastKind::Float => Union::single(Atomic::TFloat),
380 CastKind::String => Union::single(Atomic::TString),
381 CastKind::Bool => Union::single(Atomic::TBool),
382 CastKind::Array => Union::single(Atomic::TArray {
383 key: Box::new(Union::single(Atomic::TMixed)),
384 value: Box::new(Union::mixed()),
385 }),
386 CastKind::Object => Union::single(Atomic::TObject),
387 CastKind::Unset | CastKind::Void => Union::single(Atomic::TNull),
388 }
389 }
390
391 ExprKind::ErrorSuppress(inner) => self.analyze(inner, ctx),
393
394 ExprKind::Parenthesized(inner) => self.analyze(inner, ctx),
396
397 ExprKind::Array(elements) => {
399 use mir_types::atomic::{ArrayKey, KeyedProperty};
400
401 if elements.is_empty() {
402 return Union::single(Atomic::TKeyedArray {
403 properties: indexmap::IndexMap::new(),
404 is_open: false,
405 is_list: true,
406 });
407 }
408
409 let mut keyed_props: indexmap::IndexMap<ArrayKey, KeyedProperty> =
412 indexmap::IndexMap::new();
413 let mut is_list = true;
414 let mut can_be_keyed = true;
415 let mut next_int_key: i64 = 0;
416
417 for elem in elements.iter() {
418 if elem.unpack {
419 self.analyze(&elem.value, ctx);
420 can_be_keyed = false;
421 break;
422 }
423 let value_ty = self.analyze(&elem.value, ctx);
424 let array_key = if let Some(key_expr) = &elem.key {
425 is_list = false;
426 let key_ty = self.analyze(key_expr, ctx);
427 match key_ty.types.as_slice() {
429 [Atomic::TLiteralString(s)] => ArrayKey::String(s.clone()),
430 [Atomic::TLiteralInt(i)] => {
431 next_int_key = *i + 1;
432 ArrayKey::Int(*i)
433 }
434 _ => {
435 can_be_keyed = false;
436 break;
437 }
438 }
439 } else {
440 let k = ArrayKey::Int(next_int_key);
441 next_int_key += 1;
442 k
443 };
444 keyed_props.insert(
445 array_key,
446 KeyedProperty {
447 ty: value_ty,
448 optional: false,
449 },
450 );
451 }
452
453 if can_be_keyed {
454 return Union::single(Atomic::TKeyedArray {
455 properties: keyed_props,
456 is_open: false,
457 is_list,
458 });
459 }
460
461 let mut all_value_types = Union::empty();
463 let mut key_union = Union::empty();
464 let mut has_unpack = false;
465 for elem in elements.iter() {
466 let value_ty = self.analyze(&elem.value, ctx);
467 if elem.unpack {
468 has_unpack = true;
469 } else {
470 all_value_types = Union::merge(&all_value_types, &value_ty);
471 if let Some(key_expr) = &elem.key {
472 let key_ty = self.analyze(key_expr, ctx);
473 key_union = Union::merge(&key_union, &key_ty);
474 } else {
475 key_union.add_type(Atomic::TInt);
476 }
477 }
478 }
479 if has_unpack {
480 return Union::single(Atomic::TArray {
481 key: Box::new(Union::single(Atomic::TMixed)),
482 value: Box::new(Union::mixed()),
483 });
484 }
485 if key_union.is_empty() {
486 key_union.add_type(Atomic::TInt);
487 }
488 Union::single(Atomic::TArray {
489 key: Box::new(key_union),
490 value: Box::new(all_value_types),
491 })
492 }
493
494 ExprKind::ArrayAccess(aa) => {
496 let arr_ty = self.analyze(aa.array, ctx);
497
498 if let Some(idx) = &aa.index {
500 self.analyze(idx, ctx);
501 }
502
503 if arr_ty.contains(|t| matches!(t, Atomic::TNull)) && arr_ty.is_single() {
505 self.emit(IssueKind::NullArrayAccess, Severity::Error, expr.span);
506 return Union::mixed();
507 }
508 if arr_ty.is_nullable() {
509 self.emit(
510 IssueKind::PossiblyNullArrayAccess,
511 Severity::Info,
512 expr.span,
513 );
514 }
515
516 let literal_key: Option<mir_types::atomic::ArrayKey> =
518 aa.index.as_ref().and_then(|idx| match &idx.kind {
519 ExprKind::String(s) => {
520 Some(mir_types::atomic::ArrayKey::String(Arc::from(&**s)))
521 }
522 ExprKind::Int(i) => Some(mir_types::atomic::ArrayKey::Int(*i)),
523 _ => None,
524 });
525
526 for atomic in &arr_ty.types {
528 match atomic {
529 Atomic::TKeyedArray { properties, .. } => {
530 if let Some(ref key) = literal_key {
532 if let Some(prop) = properties.get(key) {
533 return prop.ty.clone();
534 }
535 }
536 let mut result = Union::empty();
538 for prop in properties.values() {
539 result = Union::merge(&result, &prop.ty);
540 }
541 return if result.types.is_empty() {
542 Union::mixed()
543 } else {
544 result
545 };
546 }
547 Atomic::TArray { value, .. } | Atomic::TNonEmptyArray { value, .. } => {
548 return *value.clone();
549 }
550 Atomic::TList { value } | Atomic::TNonEmptyList { value } => {
551 return *value.clone();
552 }
553 Atomic::TString | Atomic::TLiteralString(_) => {
554 return Union::single(Atomic::TString);
555 }
556 _ => {}
557 }
558 }
559 Union::mixed()
560 }
561
562 ExprKind::Isset(exprs) => {
564 for e in exprs.iter() {
565 self.analyze(e, ctx);
566 }
567 Union::single(Atomic::TBool)
568 }
569 ExprKind::Empty(inner) => {
570 self.analyze(inner, ctx);
571 Union::single(Atomic::TBool)
572 }
573
574 ExprKind::Print(inner) => {
576 self.analyze(inner, ctx);
577 Union::single(Atomic::TLiteralInt(1))
578 }
579
580 ExprKind::Clone(inner) => self.analyze(inner, ctx),
582 ExprKind::CloneWith(inner, _props) => self.analyze(inner, ctx),
583
584 ExprKind::New(n) => {
586 let arg_types: Vec<Union> = n
588 .args
589 .iter()
590 .map(|a| {
591 let ty = self.analyze(&a.value, ctx);
592 if a.unpack {
593 crate::call::spread_element_type(&ty)
594 } else {
595 ty
596 }
597 })
598 .collect();
599 let arg_spans: Vec<php_ast::Span> = n.args.iter().map(|a| a.span).collect();
600 let arg_names: Vec<Option<String>> = n
601 .args
602 .iter()
603 .map(|a| a.name.as_ref().map(|nm| nm.to_string_repr().into_owned()))
604 .collect();
605 let arg_can_be_byref: Vec<bool> = n
606 .args
607 .iter()
608 .map(|a| crate::call::expr_can_be_passed_by_reference(&a.value))
609 .collect();
610
611 let class_ty = match &n.class.kind {
612 ExprKind::Identifier(name) => {
613 let resolved =
614 crate::db::resolve_name_via_db(self.db, &self.file, name.as_ref());
615 let fqcn: Arc<str> = match resolved.as_str() {
617 "self" | "static" => ctx
618 .self_fqcn
619 .clone()
620 .or_else(|| ctx.static_fqcn.clone())
621 .unwrap_or_else(|| Arc::from(resolved.as_str())),
622 "parent" => ctx
623 .parent_fqcn
624 .clone()
625 .unwrap_or_else(|| Arc::from(resolved.as_str())),
626 _ => Arc::from(resolved.as_str()),
627 };
628 let type_exists = crate::db::type_exists_via_db(self.db, fqcn.as_ref());
629 if !matches!(resolved.as_str(), "self" | "static" | "parent")
630 && !type_exists
631 {
632 self.emit(
633 IssueKind::UndefinedClass {
634 name: resolved.clone(),
635 },
636 Severity::Error,
637 n.class.span,
638 );
639 } else if type_exists {
640 if let Some(node) = self
641 .db
642 .lookup_class_node(fqcn.as_ref())
643 .filter(|n| n.active(self.db))
644 {
645 if let Some(msg) = node.deprecated(self.db) {
646 self.emit(
647 IssueKind::DeprecatedClass {
648 name: fqcn.to_string(),
649 message: Some(msg).filter(|m| !m.is_empty()),
650 },
651 Severity::Info,
652 n.class.span,
653 );
654 }
655 }
656 let ctor_params =
659 crate::db::lookup_method_in_chain(self.db, &fqcn, "__construct")
660 .map(|n| n.params(self.db).to_vec());
661 if let Some(ctor_params) = ctor_params {
662 crate::call::check_constructor_args(
663 self,
664 &fqcn,
665 crate::call::CheckArgsParams {
666 fn_name: "__construct",
667 params: &ctor_params,
668 arg_types: &arg_types,
669 arg_spans: &arg_spans,
670 arg_names: &arg_names,
671 arg_can_be_byref: &arg_can_be_byref,
672 call_span: expr.span,
673 has_spread: n.args.iter().any(|a| a.unpack),
674 },
675 );
676 }
677 }
678 let ty = Union::single(Atomic::TNamedObject {
679 fqcn: fqcn.clone(),
680 type_params: vec![],
681 });
682 self.record_symbol(
683 n.class.span,
684 SymbolKind::ClassReference(fqcn.clone()),
685 ty.clone(),
686 );
687 if !self.inference_only {
690 let (line, col_start, col_end) = self.span_to_ref_loc(n.class.span);
691 self.db.record_reference_location(crate::db::RefLoc {
692 symbol_key: fqcn.clone(),
693 file: self.file.clone(),
694 line,
695 col_start,
696 col_end,
697 });
698 }
699 ty
700 }
701 _ => {
702 self.analyze(n.class, ctx);
703 Union::single(Atomic::TObject)
704 }
705 };
706 class_ty
707 }
708
709 ExprKind::AnonymousClass(_) => Union::single(Atomic::TObject),
710
711 ExprKind::PropertyAccess(pa) => {
713 let obj_ty = self.analyze(pa.object, ctx);
714 let prop_name = extract_string_from_expr(pa.property)
715 .unwrap_or_else(|| "<dynamic>".to_string());
716
717 if obj_ty.contains(|t| matches!(t, Atomic::TNull)) && obj_ty.is_single() {
718 self.emit(
719 IssueKind::NullPropertyFetch {
720 property: prop_name.clone(),
721 },
722 Severity::Error,
723 expr.span,
724 );
725 return Union::mixed();
726 }
727 if obj_ty.is_nullable() {
728 self.emit(
729 IssueKind::PossiblyNullPropertyFetch {
730 property: prop_name.clone(),
731 },
732 Severity::Info,
733 expr.span,
734 );
735 }
736
737 if prop_name == "<dynamic>" {
739 return Union::mixed();
740 }
741 let resolved = self.resolve_property_type(&obj_ty, &prop_name, pa.property.span);
744 for atomic in &obj_ty.types {
746 if let Atomic::TNamedObject { fqcn, .. } = atomic {
747 self.record_symbol(
748 pa.property.span,
749 SymbolKind::PropertyAccess {
750 class: fqcn.clone(),
751 property: Arc::from(prop_name.as_str()),
752 },
753 resolved.clone(),
754 );
755 break;
756 }
757 }
758 resolved
759 }
760
761 ExprKind::NullsafePropertyAccess(pa) => {
762 let obj_ty = self.analyze(pa.object, ctx);
763 let prop_name = extract_string_from_expr(pa.property)
764 .unwrap_or_else(|| "<dynamic>".to_string());
765 if prop_name == "<dynamic>" {
766 return Union::mixed();
767 }
768 let non_null_ty = obj_ty.remove_null();
770 let mut prop_ty =
773 self.resolve_property_type(&non_null_ty, &prop_name, pa.property.span);
774 prop_ty.add_type(Atomic::TNull); for atomic in &non_null_ty.types {
777 if let Atomic::TNamedObject { fqcn, .. } = atomic {
778 self.record_symbol(
779 pa.property.span,
780 SymbolKind::PropertyAccess {
781 class: fqcn.clone(),
782 property: Arc::from(prop_name.as_str()),
783 },
784 prop_ty.clone(),
785 );
786 break;
787 }
788 }
789 prop_ty
790 }
791
792 ExprKind::StaticPropertyAccess(spa) => {
793 if let ExprKind::Identifier(id) = &spa.class.kind {
794 let resolved = crate::db::resolve_name_via_db(self.db, &self.file, id.as_ref());
795 if !matches!(resolved.as_str(), "self" | "static" | "parent")
796 && !crate::db::type_exists_via_db(self.db, &resolved)
797 {
798 self.emit(
799 IssueKind::UndefinedClass { name: resolved },
800 Severity::Error,
801 spa.class.span,
802 );
803 }
804 }
805 Union::mixed()
806 }
807
808 ExprKind::ClassConstAccess(cca) => {
809 if cca.member.name_str() == Some("class") {
811 let fqcn = if let ExprKind::Identifier(id) = &cca.class.kind {
813 let resolved =
814 crate::db::resolve_name_via_db(self.db, &self.file, id.as_ref());
815 Some(Arc::from(resolved.as_str()))
816 } else {
817 None
818 };
819 return Union::single(Atomic::TClassString(fqcn));
820 }
821
822 let const_name = match cca.member.name_str() {
823 Some(n) => n.to_string(),
824 None => return Union::mixed(),
825 };
826
827 let fqcn = match &cca.class.kind {
828 ExprKind::Identifier(id) => {
829 let resolved =
830 crate::db::resolve_name_via_db(self.db, &self.file, id.as_ref());
831 if matches!(resolved.as_str(), "self" | "static" | "parent") {
833 return Union::mixed();
834 }
835 resolved
836 }
837 _ => return Union::mixed(),
838 };
839
840 if !crate::db::type_exists_via_db(self.db, &fqcn) {
841 self.emit(
842 IssueKind::UndefinedClass { name: fqcn },
843 Severity::Error,
844 cca.class.span,
845 );
846 return Union::mixed();
847 }
848
849 let const_exists =
850 crate::db::class_constant_exists_in_chain(self.db, &fqcn, &const_name);
851 if !const_exists && !crate::db::has_unknown_ancestor_via_db(self.db, &fqcn) {
852 self.emit(
853 IssueKind::UndefinedConstant {
854 name: format!("{fqcn}::{const_name}"),
855 },
856 Severity::Error,
857 expr.span,
858 );
859 }
860 Union::mixed()
861 }
862
863 ExprKind::ClassConstAccessDynamic { .. } => Union::mixed(),
864 ExprKind::StaticPropertyAccessDynamic { .. } => Union::mixed(),
865
866 ExprKind::MethodCall(mc) => {
868 CallAnalyzer::analyze_method_call(self, mc, ctx, expr.span, false)
869 }
870
871 ExprKind::NullsafeMethodCall(mc) => {
872 CallAnalyzer::analyze_method_call(self, mc, ctx, expr.span, true)
873 }
874
875 ExprKind::StaticMethodCall(smc) => {
876 CallAnalyzer::analyze_static_method_call(self, smc, ctx, expr.span)
877 }
878
879 ExprKind::StaticDynMethodCall(smc) => {
880 CallAnalyzer::analyze_static_dyn_method_call(self, smc, ctx)
881 }
882
883 ExprKind::FunctionCall(fc) => {
885 CallAnalyzer::analyze_function_call(self, fc, ctx, expr.span)
886 }
887
888 ExprKind::Closure(c) => {
890 for param in c.params.iter() {
892 if let Some(hint) = ¶m.type_hint {
893 self.check_type_hint(hint);
894 }
895 }
896 if let Some(hint) = &c.return_type {
897 self.check_type_hint(hint);
898 }
899
900 let params = ast_params_to_fn_params_resolved(
901 &c.params,
902 ctx.self_fqcn.as_deref(),
903 self.db,
904 &self.file,
905 );
906 let return_ty_hint = c
907 .return_type
908 .as_ref()
909 .map(|h| crate::parser::type_from_hint(h, ctx.self_fqcn.as_deref()))
910 .map(|u| resolve_named_objects_in_union(u, self.db, &self.file));
911
912 let mut closure_ctx = crate::context::Context::for_function(
916 ¶ms,
917 return_ty_hint.clone(),
918 ctx.self_fqcn.clone(),
919 ctx.parent_fqcn.clone(),
920 ctx.static_fqcn.clone(),
921 ctx.strict_types,
922 c.is_static,
923 );
924 for use_var in c.use_vars.iter() {
925 let name = use_var.name.trim_start_matches('$');
926 closure_ctx.set_var(name, ctx.get_var(name));
927 if ctx.is_tainted(name) {
928 closure_ctx.taint_var(name);
929 }
930 }
931
932 let inferred_return = {
934 let mut sa = crate::stmt::StatementsAnalyzer::new(
935 self.db,
936 self.file.clone(),
937 self.source,
938 self.source_map,
939 self.issues,
940 self.symbols,
941 self.php_version,
942 self.inference_only,
943 );
944 sa.analyze_stmts(&c.body, &mut closure_ctx);
945 let ret = crate::project::merge_return_types(&sa.return_types);
946 drop(sa);
947 ret
948 };
949
950 for name in &closure_ctx.read_vars {
952 ctx.read_vars.insert(name.clone());
953 }
954
955 let return_ty = return_ty_hint.unwrap_or(inferred_return);
956 let closure_params: Vec<mir_types::atomic::FnParam> = params
957 .iter()
958 .map(|p| mir_types::atomic::FnParam {
959 name: p.name.clone(),
960 ty: p.ty.clone(),
961 default: p.default.clone(),
962 is_variadic: p.is_variadic,
963 is_byref: p.is_byref,
964 is_optional: p.is_optional,
965 })
966 .collect();
967
968 Union::single(Atomic::TClosure {
969 params: closure_params,
970 return_type: Box::new(return_ty),
971 this_type: ctx.self_fqcn.clone().map(|f| {
972 Box::new(Union::single(Atomic::TNamedObject {
973 fqcn: f,
974 type_params: vec![],
975 }))
976 }),
977 })
978 }
979
980 ExprKind::ArrowFunction(af) => {
981 for param in af.params.iter() {
983 if let Some(hint) = ¶m.type_hint {
984 self.check_type_hint(hint);
985 }
986 }
987 if let Some(hint) = &af.return_type {
988 self.check_type_hint(hint);
989 }
990
991 let params = ast_params_to_fn_params_resolved(
992 &af.params,
993 ctx.self_fqcn.as_deref(),
994 self.db,
995 &self.file,
996 );
997 let return_ty_hint = af
998 .return_type
999 .as_ref()
1000 .map(|h| crate::parser::type_from_hint(h, ctx.self_fqcn.as_deref()))
1001 .map(|u| resolve_named_objects_in_union(u, self.db, &self.file));
1002
1003 let mut arrow_ctx = crate::context::Context::for_function(
1006 ¶ms,
1007 return_ty_hint.clone(),
1008 ctx.self_fqcn.clone(),
1009 ctx.parent_fqcn.clone(),
1010 ctx.static_fqcn.clone(),
1011 ctx.strict_types,
1012 af.is_static,
1013 );
1014 for (name, ty) in &ctx.vars {
1016 if !arrow_ctx.vars.contains_key(name) {
1017 arrow_ctx.set_var(name, ty.clone());
1018 }
1019 }
1020
1021 let inferred_return = self.analyze(af.body, &mut arrow_ctx);
1023
1024 for name in &arrow_ctx.read_vars {
1026 ctx.read_vars.insert(name.clone());
1027 }
1028
1029 let return_ty = return_ty_hint.unwrap_or(inferred_return);
1030 let closure_params: Vec<mir_types::atomic::FnParam> = params
1031 .iter()
1032 .map(|p| mir_types::atomic::FnParam {
1033 name: p.name.clone(),
1034 ty: p.ty.clone(),
1035 default: p.default.clone(),
1036 is_variadic: p.is_variadic,
1037 is_byref: p.is_byref,
1038 is_optional: p.is_optional,
1039 })
1040 .collect();
1041
1042 Union::single(Atomic::TClosure {
1043 params: closure_params,
1044 return_type: Box::new(return_ty),
1045 this_type: if af.is_static {
1046 None
1047 } else {
1048 ctx.self_fqcn.clone().map(|f| {
1049 Box::new(Union::single(Atomic::TNamedObject {
1050 fqcn: f,
1051 type_params: vec![],
1052 }))
1053 })
1054 },
1055 })
1056 }
1057
1058 ExprKind::CallableCreate(_) => Union::single(Atomic::TCallable {
1059 params: None,
1060 return_type: None,
1061 }),
1062
1063 ExprKind::Match(m) => {
1065 let subject_ty = self.analyze(m.subject, ctx);
1066 let subject_var = match &m.subject.kind {
1068 ExprKind::Variable(name) => {
1069 Some(name.as_str().trim_start_matches('$').to_string())
1070 }
1071 _ => None,
1072 };
1073
1074 let mut result = Union::empty();
1075 for arm in m.arms.iter() {
1076 let mut arm_ctx = ctx.fork();
1078
1079 if let (Some(var), Some(conditions)) = (&subject_var, &arm.conditions) {
1081 let mut arm_ty = Union::empty();
1083 for cond in conditions.iter() {
1084 let cond_ty = self.analyze(cond, ctx);
1085 arm_ty = Union::merge(&arm_ty, &cond_ty);
1086 }
1087 if !arm_ty.is_empty() && !arm_ty.is_mixed() {
1089 let narrowed = subject_ty.intersect_with(&arm_ty);
1091 if !narrowed.is_empty() {
1092 arm_ctx.set_var(var, narrowed);
1093 }
1094 }
1095 }
1096
1097 if let Some(conditions) = &arm.conditions {
1100 for cond in conditions.iter() {
1101 crate::narrowing::narrow_from_condition(
1102 cond,
1103 &mut arm_ctx,
1104 true,
1105 self.db,
1106 &self.file,
1107 );
1108 }
1109 }
1110
1111 let arm_body_ty = self.analyze(&arm.body, &mut arm_ctx);
1112 result = Union::merge(&result, &arm_body_ty);
1113
1114 for name in &arm_ctx.read_vars {
1116 ctx.read_vars.insert(name.clone());
1117 }
1118 }
1119 if result.is_empty() {
1120 Union::mixed()
1121 } else {
1122 result
1123 }
1124 }
1125
1126 ExprKind::ThrowExpr(e) => {
1128 self.analyze(e, ctx);
1129 Union::single(Atomic::TNever)
1130 }
1131
1132 ExprKind::Yield(y) => {
1134 if let Some(key) = &y.key {
1135 self.analyze(key, ctx);
1136 }
1137 if let Some(value) = &y.value {
1138 self.analyze(value, ctx);
1139 }
1140 Union::mixed()
1141 }
1142
1143 ExprKind::MagicConst(kind) => match kind {
1145 MagicConstKind::Line => Union::single(Atomic::TInt),
1146 MagicConstKind::File
1147 | MagicConstKind::Dir
1148 | MagicConstKind::Function
1149 | MagicConstKind::Class
1150 | MagicConstKind::Method
1151 | MagicConstKind::Namespace
1152 | MagicConstKind::Trait
1153 | MagicConstKind::Property => Union::single(Atomic::TString),
1154 },
1155
1156 ExprKind::Include(_, inner) => {
1158 self.analyze(inner, ctx);
1159 Union::mixed()
1160 }
1161
1162 ExprKind::Eval(inner) => {
1164 self.analyze(inner, ctx);
1165 Union::mixed()
1166 }
1167
1168 ExprKind::Exit(opt) => {
1170 if let Some(e) = opt {
1171 self.analyze(e, ctx);
1172 }
1173 ctx.diverges = true;
1174 Union::single(Atomic::TNever)
1175 }
1176
1177 ExprKind::Error => Union::mixed(),
1179
1180 ExprKind::Omit => Union::single(Atomic::TNull),
1182 }
1183 }
1184
1185 fn analyze_binary<'arena, 'src>(
1190 &mut self,
1191 b: &php_ast::ast::BinaryExpr<'arena, 'src>,
1192 _span: php_ast::Span,
1193 ctx: &mut Context,
1194 ) -> Union {
1195 use php_ast::ast::BinaryOp as B;
1201 if matches!(
1202 b.op,
1203 B::BooleanAnd | B::LogicalAnd | B::BooleanOr | B::LogicalOr
1204 ) {
1205 let _left_ty = self.analyze(b.left, ctx);
1206 let mut right_ctx = ctx.fork();
1207 let is_and = matches!(b.op, B::BooleanAnd | B::LogicalAnd);
1208 crate::narrowing::narrow_from_condition(
1209 b.left,
1210 &mut right_ctx,
1211 is_and,
1212 self.db,
1213 &self.file,
1214 );
1215 if !right_ctx.diverges {
1218 let _right_ty = self.analyze(b.right, &mut right_ctx);
1219 }
1220 for v in right_ctx.read_vars {
1224 ctx.read_vars.insert(v.clone());
1225 }
1226 for (name, ty) in &right_ctx.vars {
1227 if !ctx.vars.contains_key(name.as_str()) {
1228 ctx.vars.insert(name.clone(), ty.clone());
1230 ctx.possibly_assigned_vars.insert(name.clone());
1231 }
1232 }
1233 return Union::single(Atomic::TBool);
1234 }
1235
1236 if b.op == B::Instanceof {
1238 let _left_ty = self.analyze(b.left, ctx);
1239 if let ExprKind::Identifier(name) = &b.right.kind {
1240 let resolved = crate::db::resolve_name_via_db(self.db, &self.file, name.as_ref());
1241 let fqcn: std::sync::Arc<str> = std::sync::Arc::from(resolved.as_str());
1242 if !matches!(resolved.as_str(), "self" | "static" | "parent")
1243 && !crate::db::type_exists_via_db(self.db, &fqcn)
1244 {
1245 self.emit(
1246 IssueKind::UndefinedClass { name: resolved },
1247 Severity::Error,
1248 b.right.span,
1249 );
1250 }
1251 }
1252 return Union::single(Atomic::TBool);
1253 }
1254
1255 let left_ty = self.analyze(b.left, ctx);
1256 let right_ty = self.analyze(b.right, ctx);
1257
1258 match b.op {
1259 BinaryOp::Add
1261 | BinaryOp::Sub
1262 | BinaryOp::Mul
1263 | BinaryOp::Div
1264 | BinaryOp::Mod
1265 | BinaryOp::Pow => infer_arithmetic(&left_ty, &right_ty),
1266
1267 BinaryOp::Concat => Union::single(Atomic::TString),
1269
1270 BinaryOp::Equal
1272 | BinaryOp::NotEqual
1273 | BinaryOp::Identical
1274 | BinaryOp::NotIdentical
1275 | BinaryOp::Less
1276 | BinaryOp::Greater
1277 | BinaryOp::LessOrEqual
1278 | BinaryOp::GreaterOrEqual => Union::single(Atomic::TBool),
1279
1280 BinaryOp::Spaceship => Union::single(Atomic::TIntRange {
1282 min: Some(-1),
1283 max: Some(1),
1284 }),
1285
1286 BinaryOp::BooleanAnd
1288 | BinaryOp::BooleanOr
1289 | BinaryOp::LogicalAnd
1290 | BinaryOp::LogicalOr
1291 | BinaryOp::LogicalXor => Union::single(Atomic::TBool),
1292
1293 BinaryOp::BitwiseAnd
1295 | BinaryOp::BitwiseOr
1296 | BinaryOp::BitwiseXor
1297 | BinaryOp::ShiftLeft
1298 | BinaryOp::ShiftRight => Union::single(Atomic::TInt),
1299
1300 BinaryOp::Pipe => right_ty,
1302
1303 BinaryOp::Instanceof => Union::single(Atomic::TBool),
1305 }
1306 }
1307
1308 fn resolve_property_type(
1313 &mut self,
1314 obj_ty: &Union,
1315 prop_name: &str,
1316 span: php_ast::Span,
1317 ) -> Union {
1318 for atomic in &obj_ty.types {
1319 match atomic {
1320 Atomic::TNamedObject { fqcn, .. }
1321 if crate::db::class_kind_via_db(self.db, fqcn.as_ref())
1322 .is_some_and(|k| !k.is_interface && !k.is_trait && !k.is_enum) =>
1323 {
1324 let prop_found: Option<Union> =
1328 crate::db::lookup_property_in_chain(self.db, fqcn.as_ref(), prop_name)
1329 .map(|node| node.ty(self.db).unwrap_or_else(Union::mixed));
1330 if let Some(ty) = prop_found {
1331 if !self.inference_only {
1333 let (line, col_start, col_end) = self.span_to_ref_loc(span);
1334 self.db.record_reference_location(crate::db::RefLoc {
1335 symbol_key: Arc::from(format!("{}::{}", fqcn, prop_name)),
1336 file: self.file.clone(),
1337 line,
1338 col_start,
1339 col_end,
1340 });
1341 }
1342 return ty;
1343 }
1344 if !crate::db::has_unknown_ancestor_via_db(self.db, fqcn.as_ref())
1346 && !crate::db::method_exists_via_db(self.db, fqcn.as_ref(), "__get")
1347 {
1348 self.emit(
1349 IssueKind::UndefinedProperty {
1350 class: fqcn.to_string(),
1351 property: prop_name.to_string(),
1352 },
1353 Severity::Warning,
1354 span,
1355 );
1356 }
1357 return Union::mixed();
1358 }
1359 Atomic::TNamedObject { fqcn, .. }
1360 if crate::db::class_kind_via_db(self.db, fqcn.as_ref())
1361 .is_some_and(|k| k.is_enum) =>
1362 {
1363 match prop_name {
1364 "name" => return Union::single(Atomic::TNonEmptyString),
1365 "value" => {
1366 if let Some(node) = self
1367 .db
1368 .lookup_class_node(fqcn.as_ref())
1369 .filter(|n| n.active(self.db))
1370 {
1371 if let Some(scalar_ty) = node.enum_scalar_type(self.db) {
1372 return scalar_ty;
1373 }
1374 }
1375 self.emit(
1377 IssueKind::UndefinedProperty {
1378 class: fqcn.to_string(),
1379 property: prop_name.to_string(),
1380 },
1381 Severity::Warning,
1382 span,
1383 );
1384 return Union::mixed();
1385 }
1386 _ => {
1387 self.emit(
1388 IssueKind::UndefinedProperty {
1389 class: fqcn.to_string(),
1390 property: prop_name.to_string(),
1391 },
1392 Severity::Warning,
1393 span,
1394 );
1395 return Union::mixed();
1396 }
1397 }
1398 }
1399 Atomic::TMixed => return Union::mixed(),
1400 _ => {}
1401 }
1402 }
1403 Union::mixed()
1404 }
1405
1406 fn assign_to_target<'arena, 'src>(
1411 &mut self,
1412 target: &php_ast::ast::Expr<'arena, 'src>,
1413 ty: Union,
1414 ctx: &mut Context,
1415 span: php_ast::Span,
1416 ) {
1417 match &target.kind {
1418 ExprKind::Variable(name) => {
1419 let name_str = name.as_str().trim_start_matches('$').to_string();
1420 if ctx.byref_param_names.contains(&name_str) {
1421 ctx.read_vars.insert(name_str.clone());
1422 }
1423 ctx.set_var(name_str.clone(), ty);
1424 let (line, col_start) = self.offset_to_line_col(target.span.start);
1425 let (line_end, col_end) = self.offset_to_line_col(target.span.end);
1426 ctx.record_var_location(&name_str, line, col_start, line_end, col_end);
1427 }
1428 ExprKind::Array(elements) => {
1429 let has_non_array = ty.contains(|a| matches!(a, Atomic::TFalse | Atomic::TNull));
1433 let has_array = ty.contains(|a| {
1434 matches!(
1435 a,
1436 Atomic::TArray { .. }
1437 | Atomic::TList { .. }
1438 | Atomic::TNonEmptyArray { .. }
1439 | Atomic::TNonEmptyList { .. }
1440 | Atomic::TKeyedArray { .. }
1441 )
1442 });
1443 if has_non_array && has_array {
1444 let actual = format!("{ty}");
1445 self.emit(
1446 IssueKind::PossiblyInvalidArrayOffset {
1447 expected: "array".to_string(),
1448 actual,
1449 },
1450 Severity::Warning,
1451 span,
1452 );
1453 }
1454
1455 let value_ty: Union = ty
1457 .types
1458 .iter()
1459 .find_map(|a| match a {
1460 Atomic::TArray { value, .. }
1461 | Atomic::TList { value }
1462 | Atomic::TNonEmptyArray { value, .. }
1463 | Atomic::TNonEmptyList { value } => Some(*value.clone()),
1464 _ => None,
1465 })
1466 .unwrap_or_else(Union::mixed);
1467
1468 for elem in elements.iter() {
1469 self.assign_to_target(&elem.value, value_ty.clone(), ctx, span);
1470 }
1471 }
1472 ExprKind::PropertyAccess(pa) => {
1473 let obj_ty = self.analyze(pa.object, ctx);
1475 if let Some(prop_name) = extract_string_from_expr(pa.property) {
1476 for atomic in &obj_ty.types {
1477 if let Atomic::TNamedObject { fqcn, .. } = atomic {
1478 let db = self.db;
1482 let prop_info: Option<(bool, Option<Union>)> = db
1483 .lookup_property_node(fqcn, &prop_name)
1484 .filter(|n| n.active(db))
1485 .map(|n| (n.is_readonly(db), n.ty(db)));
1486 if let Some((is_readonly, prop_ty)) = prop_info {
1487 if is_readonly && !ctx.inside_constructor {
1488 self.emit(
1489 IssueKind::ReadonlyPropertyAssignment {
1490 class: fqcn.to_string(),
1491 property: prop_name.clone(),
1492 },
1493 Severity::Error,
1494 span,
1495 );
1496 }
1497 if let Some(prop_ty) = &prop_ty {
1498 if !prop_ty.is_mixed()
1499 && !ty.is_mixed()
1500 && !property_assign_compatible(&ty, prop_ty, self.db)
1501 {
1502 self.emit(
1503 IssueKind::InvalidPropertyAssignment {
1504 property: prop_name.clone(),
1505 expected: format!("{prop_ty}"),
1506 actual: format!("{ty}"),
1507 },
1508 Severity::Warning,
1509 span,
1510 );
1511 }
1512 }
1513 }
1514 }
1515 }
1516 }
1517 }
1518 ExprKind::StaticPropertyAccess(_) => {
1519 }
1521 ExprKind::ArrayAccess(aa) => {
1522 if let Some(idx) = &aa.index {
1525 self.analyze(idx, ctx);
1526 }
1527 let mut base = aa.array;
1530 loop {
1531 match &base.kind {
1532 ExprKind::Variable(name) => {
1533 let name_str = name.as_str().trim_start_matches('$');
1534 if !ctx.var_is_defined(name_str) {
1535 ctx.vars.insert(
1536 name_str.to_string(),
1537 Union::single(Atomic::TArray {
1538 key: Box::new(Union::mixed()),
1539 value: Box::new(ty.clone()),
1540 }),
1541 );
1542 ctx.assigned_vars.insert(name_str.to_string());
1543 } else {
1544 let current = ctx.get_var(name_str);
1547 let updated = widen_array_with_value(¤t, &ty);
1548 ctx.set_var(name_str, updated);
1549 }
1550 break;
1551 }
1552 ExprKind::ArrayAccess(inner) => {
1553 if let Some(idx) = &inner.index {
1554 self.analyze(idx, ctx);
1555 }
1556 base = inner.array;
1557 }
1558 _ => break,
1559 }
1560 }
1561 }
1562 _ => {}
1563 }
1564 }
1565
1566 fn offset_to_line_col(&self, offset: u32) -> (u32, u16) {
1573 let lc = self.source_map.offset_to_line_col(offset);
1574 let line = lc.line + 1;
1575
1576 let byte_offset = offset as usize;
1577 let line_start_byte = if byte_offset == 0 {
1578 0
1579 } else {
1580 self.source[..byte_offset]
1581 .rfind('\n')
1582 .map(|p| p + 1)
1583 .unwrap_or(0)
1584 };
1585
1586 let col = self.source[line_start_byte..byte_offset].chars().count() as u16;
1587
1588 (line, col)
1589 }
1590
1591 pub(crate) fn span_to_ref_loc(&self, span: php_ast::Span) -> (u32, u16, u16) {
1593 let (line, col_start) = self.offset_to_line_col(span.start);
1594 let end_off = (span.end as usize).min(self.source.len());
1595 let end_line_start = self.source[..end_off]
1596 .rfind('\n')
1597 .map(|p| p + 1)
1598 .unwrap_or(0);
1599 let col_end = self.source[end_line_start..end_off].chars().count() as u16;
1600 (line, col_start, col_end)
1601 }
1602
1603 fn check_type_hint(&mut self, hint: &php_ast::ast::TypeHint<'_, '_>) {
1605 use php_ast::ast::TypeHintKind;
1606 match &hint.kind {
1607 TypeHintKind::Named(name) => {
1608 let name_str = crate::parser::name_to_string(name);
1609 if matches!(
1610 name_str.to_lowercase().as_str(),
1611 "self"
1612 | "static"
1613 | "parent"
1614 | "null"
1615 | "true"
1616 | "false"
1617 | "never"
1618 | "void"
1619 | "mixed"
1620 | "object"
1621 | "callable"
1622 | "iterable"
1623 ) {
1624 return;
1625 }
1626 let resolved = crate::db::resolve_name_via_db(self.db, &self.file, &name_str);
1627 if !crate::db::type_exists_via_db(self.db, &resolved) {
1628 self.emit(
1629 IssueKind::UndefinedClass { name: resolved },
1630 Severity::Error,
1631 hint.span,
1632 );
1633 }
1634 }
1635 TypeHintKind::Nullable(inner) => self.check_type_hint(inner),
1636 TypeHintKind::Union(parts) | TypeHintKind::Intersection(parts) => {
1637 for part in parts.iter() {
1638 self.check_type_hint(part);
1639 }
1640 }
1641 TypeHintKind::Keyword(_, _) => {}
1642 }
1643 }
1644
1645 pub fn emit(&mut self, kind: IssueKind, severity: Severity, span: php_ast::Span) {
1646 let (line, col_start) = self.offset_to_line_col(span.start);
1647
1648 let (line_end, col_end) = if span.start < span.end {
1649 let (end_line, end_col) = self.offset_to_line_col(span.end);
1650 (end_line, end_col)
1651 } else {
1652 (line, col_start)
1653 };
1654
1655 let mut issue = Issue::new(
1656 kind,
1657 Location {
1658 file: self.file.clone(),
1659 line,
1660 line_end,
1661 col_start,
1662 col_end: col_end.max(col_start + 1),
1663 },
1664 );
1665 issue.severity = severity;
1666 if span.start < span.end {
1668 let s = span.start as usize;
1669 let e = (span.end as usize).min(self.source.len());
1670 if let Some(text) = self.source.get(s..e) {
1671 let trimmed = text.trim();
1672 if !trimmed.is_empty() {
1673 issue.snippet = Some(trimmed.to_string());
1674 }
1675 }
1676 }
1677 self.issues.add(issue);
1678 }
1679}
1680
1681fn widen_array_with_value(current: &Union, new_value: &Union) -> Union {
1689 let mut result = Union::empty();
1690 result.possibly_undefined = current.possibly_undefined;
1691 result.from_docblock = current.from_docblock;
1692 let mut found_array = false;
1693 for atomic in ¤t.types {
1694 match atomic {
1695 Atomic::TKeyedArray { properties, .. } => {
1696 let mut all_values = new_value.clone();
1698 for prop in properties.values() {
1699 all_values = Union::merge(&all_values, &prop.ty);
1700 }
1701 result.add_type(Atomic::TArray {
1702 key: Box::new(Union::mixed()),
1703 value: Box::new(all_values),
1704 });
1705 found_array = true;
1706 }
1707 Atomic::TArray { key, value } => {
1708 let merged = Union::merge(value, new_value);
1709 result.add_type(Atomic::TArray {
1710 key: key.clone(),
1711 value: Box::new(merged),
1712 });
1713 found_array = true;
1714 }
1715 Atomic::TList { value } | Atomic::TNonEmptyList { value } => {
1716 let merged = Union::merge(value, new_value);
1717 result.add_type(Atomic::TList {
1718 value: Box::new(merged),
1719 });
1720 found_array = true;
1721 }
1722 Atomic::TMixed => {
1723 return Union::mixed();
1724 }
1725 other => {
1726 result.add_type(other.clone());
1727 }
1728 }
1729 }
1730 if !found_array {
1731 return current.clone();
1734 }
1735 result
1736}
1737
1738pub fn infer_arithmetic(left: &Union, right: &Union) -> Union {
1739 if left.is_mixed() || right.is_mixed() {
1741 return Union::mixed();
1742 }
1743
1744 let left_is_array = left.contains(|t| {
1746 matches!(
1747 t,
1748 Atomic::TArray { .. }
1749 | Atomic::TNonEmptyArray { .. }
1750 | Atomic::TList { .. }
1751 | Atomic::TNonEmptyList { .. }
1752 | Atomic::TKeyedArray { .. }
1753 )
1754 });
1755 let right_is_array = right.contains(|t| {
1756 matches!(
1757 t,
1758 Atomic::TArray { .. }
1759 | Atomic::TNonEmptyArray { .. }
1760 | Atomic::TList { .. }
1761 | Atomic::TNonEmptyList { .. }
1762 | Atomic::TKeyedArray { .. }
1763 )
1764 });
1765 if left_is_array || right_is_array {
1766 let merged_left = if left_is_array {
1768 left.clone()
1769 } else {
1770 Union::single(Atomic::TArray {
1771 key: Box::new(Union::single(Atomic::TMixed)),
1772 value: Box::new(Union::mixed()),
1773 })
1774 };
1775 return merged_left;
1776 }
1777
1778 let left_is_float = left.contains(|t| matches!(t, Atomic::TFloat | Atomic::TLiteralFloat(..)));
1779 let right_is_float =
1780 right.contains(|t| matches!(t, Atomic::TFloat | Atomic::TLiteralFloat(..)));
1781 if left_is_float || right_is_float {
1782 Union::single(Atomic::TFloat)
1783 } else if left.contains(|t| t.is_int()) && right.contains(|t| t.is_int()) {
1784 Union::single(Atomic::TInt)
1785 } else {
1786 let mut u = Union::empty();
1788 u.add_type(Atomic::TInt);
1789 u.add_type(Atomic::TFloat);
1790 u
1791 }
1792}
1793
1794pub fn extract_simple_var<'arena, 'src>(expr: &php_ast::ast::Expr<'arena, 'src>) -> Option<String> {
1795 match &expr.kind {
1796 ExprKind::Variable(name) => Some(name.as_str().trim_start_matches('$').to_string()),
1797 ExprKind::Parenthesized(inner) => extract_simple_var(inner),
1798 _ => None,
1799 }
1800}
1801
1802pub fn extract_destructure_vars<'arena, 'src>(
1806 expr: &php_ast::ast::Expr<'arena, 'src>,
1807) -> Vec<String> {
1808 match &expr.kind {
1809 ExprKind::Array(elements) => {
1810 let mut vars = vec![];
1811 for elem in elements.iter() {
1812 let sub = extract_destructure_vars(&elem.value);
1814 if sub.is_empty() {
1815 if let Some(v) = extract_simple_var(&elem.value) {
1816 vars.push(v);
1817 }
1818 } else {
1819 vars.extend(sub);
1820 }
1821 }
1822 vars
1823 }
1824 _ => vec![],
1825 }
1826}
1827
1828fn ast_params_to_fn_params_resolved<'arena, 'src>(
1830 params: &php_ast::ast::ArenaVec<'arena, php_ast::ast::Param<'arena, 'src>>,
1831 self_fqcn: Option<&str>,
1832 db: &dyn MirDatabase,
1833 file: &str,
1834) -> Vec<mir_codebase::FnParam> {
1835 params
1836 .iter()
1837 .map(|p| {
1838 let ty = p
1839 .type_hint
1840 .as_ref()
1841 .map(|h| crate::parser::type_from_hint(h, self_fqcn))
1842 .map(|u| resolve_named_objects_in_union(u, db, file));
1843 mir_codebase::FnParam {
1844 name: p.name.trim_start_matches('$').into(),
1845 ty,
1846 default: p.default.as_ref().map(|_| Union::mixed()),
1847 is_variadic: p.variadic,
1848 is_byref: p.by_ref,
1849 is_optional: p.default.is_some() || p.variadic,
1850 }
1851 })
1852 .collect()
1853}
1854
1855fn resolve_named_objects_in_union(union: Union, db: &dyn MirDatabase, file: &str) -> Union {
1857 use mir_types::Atomic;
1858 let from_docblock = union.from_docblock;
1859 let possibly_undefined = union.possibly_undefined;
1860 let types: Vec<Atomic> = union
1861 .types
1862 .into_iter()
1863 .map(|a| match a {
1864 Atomic::TNamedObject { fqcn, type_params } => {
1865 let resolved = crate::db::resolve_name_via_db(db, file, fqcn.as_ref());
1866 Atomic::TNamedObject {
1867 fqcn: resolved.into(),
1868 type_params,
1869 }
1870 }
1871 other => other,
1872 })
1873 .collect();
1874 let mut result = Union::from_vec(types);
1875 result.from_docblock = from_docblock;
1876 result.possibly_undefined = possibly_undefined;
1877 result
1878}
1879
1880fn extract_string_from_expr<'arena, 'src>(
1881 expr: &php_ast::ast::Expr<'arena, 'src>,
1882) -> Option<String> {
1883 match &expr.kind {
1884 ExprKind::Identifier(s) => Some(s.trim_start_matches('$').to_string()),
1885 ExprKind::Variable(_) => None,
1887 ExprKind::String(s) => Some(s.to_string()),
1888 _ => None,
1889 }
1890}
1891
1892fn property_assign_compatible(
1895 value_ty: &mir_types::Union,
1896 prop_ty: &mir_types::Union,
1897 db: &dyn crate::db::MirDatabase,
1898) -> bool {
1899 if value_ty.is_subtype_of_simple(prop_ty) {
1900 return true;
1901 }
1902 value_ty.types.iter().all(|a| match a {
1904 mir_types::Atomic::TNamedObject { fqcn: arg_fqcn, .. }
1906 | mir_types::Atomic::TSelf { fqcn: arg_fqcn }
1907 | mir_types::Atomic::TStaticObject { fqcn: arg_fqcn }
1908 | mir_types::Atomic::TParent { fqcn: arg_fqcn } => {
1909 prop_ty.types.iter().any(|p| match p {
1910 mir_types::Atomic::TNamedObject { fqcn: prop_fqcn, .. } => {
1911 arg_fqcn == prop_fqcn
1912 || crate::db::extends_or_implements_via_db(
1913 db,
1914 arg_fqcn.as_ref(),
1915 prop_fqcn.as_ref(),
1916 )
1917 }
1918 mir_types::Atomic::TObject | mir_types::Atomic::TMixed => true,
1919 _ => false,
1920 })
1921 }
1922 mir_types::Atomic::TTemplateParam { .. } => true,
1924 mir_types::Atomic::TClosure { .. } | mir_types::Atomic::TCallable { .. } => {
1926 prop_ty.types.iter().any(|p| matches!(p, mir_types::Atomic::TClosure { .. } | mir_types::Atomic::TCallable { .. })
1927 || matches!(p, mir_types::Atomic::TNamedObject { fqcn, .. } if fqcn.as_ref() == "Closure"))
1928 }
1929 mir_types::Atomic::TNever => true,
1930 mir_types::Atomic::TNull => prop_ty.is_nullable(),
1932 _ => false,
1934 })
1935}
1936
1937#[cfg(test)]
1942mod tests {
1943 fn create_source_map(source: &str) -> php_rs_parser::source_map::SourceMap {
1945 let bump = bumpalo::Bump::new();
1946 let result = php_rs_parser::parse(&bump, source);
1947 result.source_map
1948 }
1949
1950 fn test_offset_conversion(source: &str, offset: u32) -> (u32, u16) {
1952 let source_map = create_source_map(source);
1953 let lc = source_map.offset_to_line_col(offset);
1954 let line = lc.line + 1;
1955
1956 let byte_offset = offset as usize;
1957 let line_start_byte = if byte_offset == 0 {
1958 0
1959 } else {
1960 source[..byte_offset]
1961 .rfind('\n')
1962 .map(|p| p + 1)
1963 .unwrap_or(0)
1964 };
1965
1966 let col = source[line_start_byte..byte_offset].chars().count() as u16;
1967
1968 (line, col)
1969 }
1970
1971 #[test]
1972 fn col_conversion_simple_ascii() {
1973 let source = "<?php\n$var = 123;";
1974
1975 let (line, col) = test_offset_conversion(source, 6);
1977 assert_eq!(line, 2);
1978 assert_eq!(col, 0);
1979
1980 let (line, col) = test_offset_conversion(source, 7);
1982 assert_eq!(line, 2);
1983 assert_eq!(col, 1);
1984 }
1985
1986 #[test]
1987 fn col_conversion_different_lines() {
1988 let source = "<?php\n$x = 1;\n$y = 2;";
1989 let (line, col) = test_offset_conversion(source, 0);
1994 assert_eq!((line, col), (1, 0));
1995
1996 let (line, col) = test_offset_conversion(source, 6);
1997 assert_eq!((line, col), (2, 0));
1998
1999 let (line, col) = test_offset_conversion(source, 14);
2000 assert_eq!((line, col), (3, 0));
2001 }
2002
2003 #[test]
2004 fn col_conversion_accented_characters() {
2005 let source = "<?php\n$café = 1;";
2007 let (line, col) = test_offset_conversion(source, 9);
2012 assert_eq!((line, col), (2, 3));
2013
2014 let (line, col) = test_offset_conversion(source, 10);
2016 assert_eq!((line, col), (2, 4));
2017 }
2018
2019 #[test]
2020 fn col_conversion_emoji_counts_as_one_char() {
2021 let source = "<?php\n$y = \"🎉x\";";
2024 let emoji_start = source.find("🎉").unwrap();
2028 let after_emoji = emoji_start + "🎉".len(); let (line, col) = test_offset_conversion(source, after_emoji as u32);
2032 assert_eq!(line, 2);
2033 assert_eq!(col, 7); }
2035
2036 #[test]
2037 fn col_conversion_emoji_start_position() {
2038 let source = "<?php\n$y = \"🎉\";";
2040 let quote_pos = source.find('"').unwrap();
2044 let emoji_pos = quote_pos + 1; let (line, col) = test_offset_conversion(source, quote_pos as u32);
2047 assert_eq!(line, 2);
2048 assert_eq!(col, 5); let (line, col) = test_offset_conversion(source, emoji_pos as u32);
2051 assert_eq!(line, 2);
2052 assert_eq!(col, 6); }
2054
2055 #[test]
2056 fn col_end_minimum_width() {
2057 let col_start = 0u16;
2059 let col_end = 0u16; let effective_col_end = col_end.max(col_start + 1);
2061
2062 assert_eq!(
2063 effective_col_end, 1,
2064 "col_end should be at least col_start + 1"
2065 );
2066 }
2067
2068 #[test]
2069 fn col_conversion_multiline_span() {
2070 let source = "<?php\n$x = [\n 'a',\n 'b'\n];";
2072 let bracket_open = source.find('[').unwrap();
2080 let (line_start, _col_start) = test_offset_conversion(source, bracket_open as u32);
2081 assert_eq!(line_start, 2);
2082
2083 let bracket_close = source.rfind(']').unwrap();
2085 let (line_end, col_end) = test_offset_conversion(source, bracket_close as u32);
2086 assert_eq!(line_end, 5);
2087 assert_eq!(col_end, 0); }
2089
2090 #[test]
2091 fn col_end_handles_emoji_in_span() {
2092 let source = "<?php\n$greeting = \"Hello 🎉\";";
2094
2095 let emoji_pos = source.find('🎉').unwrap();
2097 let hello_pos = source.find("Hello").unwrap();
2098
2099 let (line, col) = test_offset_conversion(source, hello_pos as u32);
2101 assert_eq!(line, 2);
2102 assert_eq!(col, 13); let (line, col) = test_offset_conversion(source, emoji_pos as u32);
2106 assert_eq!(line, 2);
2107 assert_eq!(col, 19);
2109 }
2110}