phanalist

E0031: Division by Zero

E0031 reports division whose divisor can be zero. It is the division member of Phanalist’s bounded formal verification family (E0031-E0043). The family shares a Z3-backed engine that encodes path conditions and values, so a finding comes with a solver-backed counterexample rather than a heuristic guess.

Proofs are relative to the modeled subset, the configured checks, and the project’s interprocedural summaries. A clean result means no violation was found in that model; it is not a proof that every possible PHP execution is safe.

What it does not prove

What it checks

Configuration

Each verification code has its own settings block. The relevant check is:

rules:
  E0031:
    php_version: "8.4"
    check_division: true

check_types controls PHPDoc @param/@var refinements. Native PHP type hints are always honoured. php_version is accepted for configuration compatibility; the modelled semantics are the same across PHP 8.0-8.4. loop_strategy: none limits loop bodies to one iteration; every other value uses unroll_depth. unknown_policy: report (the default) reports a solver unknown as an unverified finding so it cannot be mistaken for a proof; ignore suppresses that diagnostic.

Each verification code is enabled, disabled, and configured independently. Settings under E0031 apply only to division-by-zero checks; other codes do not inherit them.

Precision and recall benchmark

The supported_fragment_precision_and_recall test runs a labelled corpus of clean and buggy PHP cases through the verification family, then reports precision and recall for the supported fragment. The corpus includes division, modulo, intdiv(), array, string, nullability, return-contract, assertion, unreachable-code, switch, and loop zero-iteration cases. Dedicated benchmark fixtures live in src/rules/examples/e31/benchmark/.

Current result: 49 labelled cases and 50 expected findings, with 0 false positives and 0 false negatives (100% precision and recall). This is a regression benchmark for the supported fragment, not a random sample of arbitrary PHP projects. Run it with:

cargo test supported_fragment_precision_and_recall -- --nocapture

Pinned analyzer reference

The loop fixtures are database-independent: they take an iterable and never open a connection. Compare them with PHPStan 2.3.2, level max, and no baseline. The pinned configuration is src/rules/examples/e31/benchmark/phpstan.neon; run it with the pinned phar:

curl -L -o /tmp/phpstan-2.3.2.phar \
  https://github.com/phpstan/phpstan/releases/download/2.3.2/phpstan.phar
php /tmp/phpstan-2.3.2.phar analyse \
  --configuration=src/rules/examples/e31/benchmark/phpstan.neon \
  --no-progress --error-format=table

PHPStan does not report the division by zero on the feasible zero-iteration path. E0031 reports division-by-zero on the unguarded fixture and no finding on the guarded one.

For the list fixtures, PHPStan 2.3.2 at level max and Psalm 6.20.1 at errorLevel="1" both report the literal -1 access on non-empty-list<int>, but neither reports $items[count($items)] nor the for ($i = 0; $i <= count($items); $i++) access. E0036 reports all three.