Skip to main content

mago_analyzer/plugin/libraries/stdlib/math/
intdiv.rs

1//! `intdiv()` divide-by-zero detector.
2//!
3//! `intdiv($num, $divisor)` throws `DivisionByZeroError` at runtime when
4//! `$divisor` is zero. Mago can detect this statically when the divisor's
5//! type is a literal `0` or an integer range that includes only zero.
6
7use mago_codex::ttype::atomic::TAtomic;
8use mago_codex::ttype::atomic::scalar::TScalar;
9use mago_codex::ttype::union::TUnion;
10use mago_reporting::Annotation;
11use mago_reporting::Issue;
12use mago_span::HasSpan;
13use mago_syntax::cst::Argument;
14use mago_syntax::cst::Expression;
15use mago_syntax::cst::FunctionCall;
16
17use crate::code::IssueCode;
18use crate::plugin::context::HookContext;
19use crate::plugin::hook::FunctionCallHook;
20use crate::plugin::hook::HookResult;
21use crate::plugin::provider::Provider;
22use crate::plugin::provider::ProviderMeta;
23
24#[derive(Default)]
25pub struct IntdivHook;
26
27impl Provider for IntdivHook {
28    fn meta() -> &'static ProviderMeta {
29        static META: ProviderMeta =
30            ProviderMeta::new("php::math::intdiv", "intdiv", "Detects intdiv() calls with a statically zero divisor.");
31
32        &META
33    }
34}
35
36impl FunctionCallHook for IntdivHook {
37    fn after_function_call(&self, call: &FunctionCall<'_>, context: &mut HookContext<'_, '_>) -> HookResult<()> {
38        let Expression::Identifier(identifier) = call.function else {
39            return Ok(());
40        };
41
42        if !identifier.value().eq_ignore_ascii_case(b"intdiv") {
43            return Ok(());
44        }
45
46        let Some(divisor_expr) = lookup_divisor_argument(call) else {
47            return Ok(());
48        };
49
50        let Some(divisor_type) = context.get_expression_type(divisor_expr) else {
51            return Ok(());
52        };
53
54        if !is_definitely_zero(divisor_type) {
55            return Ok(());
56        }
57
58        context.report(
59            IssueCode::InvalidOperand,
60            Issue::error("Call to `intdiv()` with a zero divisor.")
61                .with_annotation(Annotation::primary(divisor_expr.span()).with_message("This divisor is zero"))
62                .with_annotation(Annotation::secondary(call.function.span()).with_message("In this `intdiv()` call"))
63                .with_note("`intdiv($num, 0)` throws `DivisionByZeroError` at runtime.")
64                .with_help("Guard the call with `$divisor !== 0` or restrict the divisor's type to exclude zero."),
65        );
66
67        Ok(())
68    }
69}
70
71fn lookup_divisor_argument<'arena>(call: &FunctionCall<'arena>) -> Option<&'arena Expression<'arena>> {
72    let mut seen_positional = 0;
73    for argument in call.argument_list.arguments.iter() {
74        match argument {
75            Argument::Positional(arg) => {
76                seen_positional += 1;
77                if seen_positional == 2 {
78                    return Some(arg.value);
79                }
80            }
81            Argument::Named(arg) if arg.name.value == b"divisor" => {
82                return Some(arg.value);
83            }
84            Argument::Named(_) => {}
85        }
86    }
87
88    None
89}
90
91fn is_definitely_zero(ty: &TUnion) -> bool {
92    if ty.types.is_empty() {
93        return false;
94    }
95
96    ty.types.iter().all(|atomic| match atomic {
97        TAtomic::Scalar(TScalar::Integer(integer)) => integer.is_zero(),
98        _ => false,
99    })
100}