mago_analyzer/plugin/libraries/stdlib/math/
intdiv.rs1use 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}