1use php_ast::ast::{BinaryOp, ExprKind, UnaryPrefixOp};
6
7use mir_codebase::Codebase;
8use mir_types::{Atomic, Union};
9
10use crate::context::Context;
11
12pub fn narrow_from_condition<'arena, 'src>(
18 expr: &php_ast::ast::Expr<'arena, 'src>,
19 ctx: &mut Context,
20 is_true: bool,
21 codebase: &Codebase,
22 file: &str,
23) {
24 match &expr.kind {
25 ExprKind::Parenthesized(inner) => {
27 narrow_from_condition(inner, ctx, is_true, codebase, file);
28 }
29
30 ExprKind::UnaryPrefix(u) if u.op == UnaryPrefixOp::BooleanNot => {
32 narrow_from_condition(u.operand, ctx, !is_true, codebase, file);
33 }
34
35 ExprKind::Binary(b) if b.op == BinaryOp::BooleanAnd || b.op == BinaryOp::LogicalAnd => {
37 if is_true {
38 narrow_from_condition(b.left, ctx, true, codebase, file);
39 narrow_from_condition(b.right, ctx, true, codebase, file);
40 }
41 }
42
43 ExprKind::Binary(b) if b.op == BinaryOp::BooleanOr || b.op == BinaryOp::LogicalOr => {
45 if !is_true {
46 narrow_from_condition(b.left, ctx, false, codebase, file);
47 narrow_from_condition(b.right, ctx, false, codebase, file);
48 } else {
49 narrow_or_instanceof_true(b.left, b.right, ctx, codebase, file);
51 }
52 }
53
54 ExprKind::Binary(b) if b.op == BinaryOp::Identical || b.op == BinaryOp::NotIdentical => {
56 let is_identical = b.op == BinaryOp::Identical;
57 let effective_true = if is_identical { is_true } else { !is_true };
58
59 if matches!(b.right.kind, ExprKind::Null) {
61 if let Some(name) = extract_var_name(b.left) {
62 narrow_var_null(ctx, &name, effective_true);
63 }
64 } else if matches!(b.left.kind, ExprKind::Null) {
65 if let Some(name) = extract_var_name(b.right) {
66 narrow_var_null(ctx, &name, effective_true);
67 }
68 }
69 else if matches!(b.right.kind, ExprKind::Bool(true)) {
71 if let Some(name) = extract_var_name(b.left) {
72 narrow_var_bool(ctx, &name, true, effective_true);
73 }
74 } else if matches!(b.right.kind, ExprKind::Bool(false)) {
75 if let Some(name) = extract_var_name(b.left) {
76 narrow_var_bool(ctx, &name, false, effective_true);
77 }
78 }
79 else if let ExprKind::String(s) = &b.right.kind {
81 if let Some(name) = extract_var_name(b.left) {
82 narrow_var_literal_string(ctx, &name, s, effective_true);
83 }
84 } else if let ExprKind::String(s) = &b.left.kind {
85 if let Some(name) = extract_var_name(b.right) {
86 narrow_var_literal_string(ctx, &name, s, effective_true);
87 }
88 }
89 else if let ExprKind::Int(n) = &b.right.kind {
91 if let Some(name) = extract_var_name(b.left) {
92 narrow_var_literal_int(ctx, &name, *n, effective_true);
93 }
94 } else if let ExprKind::Int(n) = &b.left.kind {
95 if let Some(name) = extract_var_name(b.right) {
96 narrow_var_literal_int(ctx, &name, *n, effective_true);
97 }
98 }
99 }
100
101 ExprKind::Binary(b) if b.op == BinaryOp::Equal || b.op == BinaryOp::NotEqual => {
103 let is_equal = b.op == BinaryOp::Equal;
104 let effective_true = if is_equal { is_true } else { !is_true };
105 if matches!(b.right.kind, ExprKind::Null) {
106 if let Some(name) = extract_var_name(b.left) {
107 narrow_var_null(ctx, &name, effective_true);
108 }
109 } else if matches!(b.left.kind, ExprKind::Null) {
110 if let Some(name) = extract_var_name(b.right) {
111 narrow_var_null(ctx, &name, effective_true);
112 }
113 }
114 }
115
116 ExprKind::Binary(b) if b.op == BinaryOp::Instanceof => {
121 let (lhs, extra_negation) = match &b.left.kind {
123 ExprKind::UnaryPrefix(u) if u.op == UnaryPrefixOp::BooleanNot => (u.operand, true),
124 ExprKind::Parenthesized(inner) => match &inner.kind {
125 ExprKind::UnaryPrefix(u) if u.op == UnaryPrefixOp::BooleanNot => {
126 (u.operand, true)
127 }
128 _ => (b.left, false),
129 },
130 _ => (b.left, false),
131 };
132 let effective_is_true = if extra_negation { !is_true } else { is_true };
133 if let Some(var_name) = extract_var_name(lhs) {
134 if let Some(raw_name) = extract_class_name(b.right) {
135 let class_name = codebase.resolve_class_name(file, &raw_name);
137 let current = ctx.get_var(&var_name);
138 let narrowed = if effective_is_true {
139 current.narrow_instanceof(&class_name)
140 } else {
141 current.filter_out_named_object(&class_name)
143 };
144 ctx.set_var(&var_name, narrowed);
145 }
146 }
147 }
148
149 ExprKind::FunctionCall(call) => {
152 let fn_name_opt: Option<&str> = match &call.name.kind {
153 ExprKind::Identifier(name) => Some(name),
154 ExprKind::Variable(name) => Some(name.as_ref()),
155 _ => None,
156 };
157 if let Some(fn_name) = fn_name_opt {
158 if fn_name.eq_ignore_ascii_case("assert") {
159 if let Some(arg_expr) = call.args.first() {
161 narrow_from_condition(&arg_expr.value, ctx, is_true, codebase, file);
162 }
163 } else if let Some(arg_expr) = call.args.first() {
164 if let Some(var_name) = extract_var_name(&arg_expr.value) {
165 narrow_from_type_fn(ctx, fn_name, &var_name, is_true);
166 }
167 }
168 }
169 }
170
171 ExprKind::Isset(vars) => {
173 for var_expr in vars.iter() {
174 if let Some(var_name) = extract_var_name(var_expr) {
175 if is_true {
176 let current = ctx.get_var(&var_name);
178 ctx.set_var(&var_name, current.remove_null());
179 ctx.assigned_vars.insert(var_name);
180 }
181 }
182 }
183 }
184
185 _ => {
187 if let Some(var_name) = extract_var_name(expr) {
188 let current = ctx.get_var(&var_name);
189 let narrowed = if is_true {
190 current.narrow_to_truthy()
191 } else {
192 current.narrow_to_falsy()
193 };
194 if !narrowed.is_empty() {
195 ctx.set_var(&var_name, narrowed);
196 } else if !current.is_empty() && !current.is_mixed() {
197 ctx.diverges = true;
200 }
201 }
202 }
203 }
204}
205
206fn narrow_or_instanceof_true<'arena, 'src>(
213 left: &php_ast::ast::Expr<'arena, 'src>,
214 right: &php_ast::ast::Expr<'arena, 'src>,
215 ctx: &mut Context,
216 codebase: &Codebase,
217 file: &str,
218) {
219 let mut var_name: Option<String> = None;
221 let mut class_names: Vec<String> = vec![];
222
223 fn collect_instanceof<'a, 's>(
224 expr: &php_ast::ast::Expr<'a, 's>,
225 var_name: &mut Option<String>,
226 class_names: &mut Vec<String>,
227 codebase: &Codebase,
228 file: &str,
229 ) -> bool {
230 match &expr.kind {
231 ExprKind::Binary(b) if b.op == BinaryOp::Instanceof => {
232 if let (Some(vn), Some(cn)) =
233 (extract_var_name(b.left), extract_class_name(b.right))
234 {
235 let resolved = codebase.resolve_class_name(file, &cn);
236 match var_name {
237 None => {
238 *var_name = Some(vn);
239 class_names.push(resolved);
240 true
241 }
242 Some(existing) if existing == &vn => {
243 class_names.push(resolved);
244 true
245 }
246 _ => false, }
248 } else {
249 false
250 }
251 }
252 ExprKind::Binary(b) if b.op == BinaryOp::BooleanOr || b.op == BinaryOp::LogicalOr => {
253 collect_instanceof(b.left, var_name, class_names, codebase, file)
254 && collect_instanceof(b.right, var_name, class_names, codebase, file)
255 }
256 ExprKind::Parenthesized(inner) => {
257 collect_instanceof(inner, var_name, class_names, codebase, file)
258 }
259 _ => false,
260 }
261 }
262
263 let left_ok = collect_instanceof(left, &mut var_name, &mut class_names, codebase, file);
265 let right_ok = collect_instanceof(right, &mut var_name, &mut class_names, codebase, file);
266
267 if left_ok && right_ok {
268 if let Some(vn) = var_name {
269 if !class_names.is_empty() {
270 let current = ctx.get_var(&vn);
271 let mut narrowed = Union::empty();
273 for cn in &class_names {
274 let n = current.narrow_instanceof(cn);
275 narrowed = Union::merge(&narrowed, &n);
276 }
277 let result = if narrowed.is_empty() {
279 current.clone()
280 } else {
281 narrowed
282 };
283 if !result.is_empty() {
284 ctx.set_var(&vn, result);
285 }
286 }
287 }
288 }
289}
290
291fn narrow_var_null(ctx: &mut Context, name: &str, is_null: bool) {
292 let current = ctx.get_var(name);
293 let narrowed = if is_null {
294 current.narrow_to_null()
295 } else {
296 current.remove_null()
297 };
298 if !narrowed.is_empty() {
299 ctx.set_var(name, narrowed);
300 } else if !current.is_empty() && !current.is_mixed() {
301 ctx.diverges = true;
303 }
304}
305
306fn narrow_var_bool(ctx: &mut Context, name: &str, value: bool, is_value: bool) {
307 let current = ctx.get_var(name);
308 let narrowed = if is_value {
309 if value {
310 current.filter(|t| matches!(t, Atomic::TTrue | Atomic::TBool | Atomic::TMixed))
311 } else {
312 current.filter(|t| matches!(t, Atomic::TFalse | Atomic::TBool | Atomic::TMixed))
313 }
314 } else if value {
315 current.filter(|t| !matches!(t, Atomic::TTrue))
316 } else {
317 current.filter(|t| !matches!(t, Atomic::TFalse))
318 };
319 if !narrowed.is_empty() {
320 ctx.set_var(name, narrowed);
321 }
322}
323
324fn narrow_from_type_fn(ctx: &mut Context, fn_name: &str, var_name: &str, is_true: bool) {
325 let current = ctx.get_var(var_name);
326 let narrowed = match fn_name.to_lowercase().as_str() {
327 "is_string" => {
328 if is_true {
329 current.narrow_to_string()
330 } else {
331 current.filter(|t| !t.is_string())
332 }
333 }
334 "is_int" | "is_integer" | "is_long" => {
335 if is_true {
336 current.narrow_to_int()
337 } else {
338 current.filter(|t| !t.is_int())
339 }
340 }
341 "is_float" | "is_double" | "is_real" => {
342 if is_true {
343 current.narrow_to_float()
344 } else {
345 current.filter(|t| !matches!(t, Atomic::TFloat | Atomic::TLiteralFloat(..)))
346 }
347 }
348 "is_bool" => {
349 if is_true {
350 current.narrow_to_bool()
351 } else {
352 current.filter(|t| !matches!(t, Atomic::TBool | Atomic::TTrue | Atomic::TFalse))
353 }
354 }
355 "is_null" => {
356 if is_true {
357 current.narrow_to_null()
358 } else {
359 current.remove_null()
360 }
361 }
362 "is_array" => {
363 if is_true {
364 current.narrow_to_array()
365 } else {
366 current.filter(|t| !t.is_array())
367 }
368 }
369 "is_object" => {
370 if is_true {
371 current.narrow_to_object()
372 } else {
373 current.filter(|t| !t.is_object())
374 }
375 }
376 "is_callable" => {
377 if is_true {
378 current.narrow_to_callable()
379 } else {
380 current.filter(|t| !t.is_callable())
381 }
382 }
383 "is_numeric" => {
384 if is_true {
385 current.filter(|t| {
386 matches!(
387 t,
388 Atomic::TInt
389 | Atomic::TFloat
390 | Atomic::TNumeric
391 | Atomic::TNumericString
392 | Atomic::TLiteralInt(_)
393 | Atomic::TMixed
394 )
395 })
396 } else {
397 current.filter(|t| {
398 !matches!(
399 t,
400 Atomic::TInt
401 | Atomic::TFloat
402 | Atomic::TNumeric
403 | Atomic::TNumericString
404 | Atomic::TLiteralInt(_)
405 )
406 })
407 }
408 }
409 "method_exists" | "property_exists" => {
412 if is_true {
413 Union::single(Atomic::TObject)
414 } else {
415 current.clone()
416 }
417 }
418 _ => return,
419 };
420 if !narrowed.is_empty() {
421 ctx.set_var(var_name, narrowed);
422 } else if !current.is_empty() && !current.is_mixed() {
423 ctx.diverges = true;
425 }
426}
427
428fn narrow_var_literal_string(ctx: &mut Context, name: &str, value: &str, is_value: bool) {
429 let current = ctx.get_var(name);
430 let narrowed = if is_value {
431 current.filter(|t| match t {
433 Atomic::TLiteralString(s) => s.as_ref() == value,
434 Atomic::TString | Atomic::TScalar | Atomic::TMixed => true,
435 _ => false,
436 })
437 } else {
438 current.filter(|t| !matches!(t, Atomic::TLiteralString(s) if s.as_ref() == value))
440 };
441 if !narrowed.is_empty() {
442 ctx.set_var(name, narrowed);
443 }
444}
445
446fn narrow_var_literal_int(ctx: &mut Context, name: &str, value: i64, is_value: bool) {
447 let current = ctx.get_var(name);
448 let narrowed = if is_value {
449 current.filter(|t| match t {
450 Atomic::TLiteralInt(n) => *n == value,
451 Atomic::TInt | Atomic::TScalar | Atomic::TNumeric | Atomic::TMixed => true,
452 _ => false,
453 })
454 } else {
455 current.filter(|t| !matches!(t, Atomic::TLiteralInt(n) if *n == value))
456 };
457 if !narrowed.is_empty() {
458 ctx.set_var(name, narrowed);
459 }
460}
461
462fn extract_var_name<'a, 'arena, 'src>(
463 expr: &'a php_ast::ast::Expr<'arena, 'src>,
464) -> Option<String> {
465 match &expr.kind {
466 ExprKind::Variable(name) => Some(name.as_str().trim_start_matches('$').to_string()),
467 ExprKind::Parenthesized(inner) => extract_var_name(inner),
468 _ => None,
469 }
470}
471
472fn extract_class_name<'arena, 'src>(expr: &php_ast::ast::Expr<'arena, 'src>) -> Option<String> {
473 match &expr.kind {
474 ExprKind::Identifier(name) => Some(name.to_string()),
475 ExprKind::Variable(_name) => None, _ => None,
477 }
478}
479
480trait UnionNarrowExt {
485 fn filter<F: Fn(&Atomic) -> bool>(&self, f: F) -> Union;
486 fn filter_out_named_object(&self, fqcn: &str) -> Union;
487}
488
489impl UnionNarrowExt for Union {
490 fn filter<F: Fn(&Atomic) -> bool>(&self, f: F) -> Union {
491 let mut result = Union::empty();
492 result.possibly_undefined = self.possibly_undefined;
493 result.from_docblock = self.from_docblock;
494 for atomic in &self.types {
495 if f(atomic) {
496 result.types.push(atomic.clone());
497 }
498 }
499 result
500 }
501
502 fn filter_out_named_object(&self, fqcn: &str) -> Union {
503 self.filter(|t| match t {
504 Atomic::TNamedObject { fqcn: f, .. } => f.as_ref() != fqcn,
505 _ => true,
506 })
507 }
508}