Skip to content

DateLastErrorsFalseUnchecked ​

warning native on by default PHP ≥ 8.2

Group: Probable bugs

Detect direct array access on getLastErrors result or a proven alias without excluding false on PHP 8.2+.

Example ​

php
<?php
$errors=DateTimeImmutable::getLastErrors(); echo $errors['warning_count'];

Reported:

  • line 2: Handle the false result before reading parsing diagnostics.

Detection ​

  • Report direct array access on getLastErrors result or a proven alias without excluding false on PHP 8.2+.
  • Resolve builtin calls and named arguments before checking the contract. Project-local callees are considered only when their bodies or immutable summaries establish the required facts. The default is enabled, with positive proof required.
  • For flow-dependent conditions, require a reachable source-to-use path with no intervening invalidation. Unknown calls, ambiguous aliases or recursion cannot establish definite facts. When the proof is inconclusive, nothing is reported.

Exceptions (no report) ​

  • Truthiness guards, strict false guards, null/false-aware normalization and PHP before 8.2 are excluded.
  • Silent probes are not reads: an access that is the operand of isset, of empty, or the tested (left) side of ?? yields no diagnostic on false, including when further offsets are chained on it. An access used as the index of such a probe, or on the fallback side of ??, is still an ordinary read.
  • A guard counts when it must have held for the same value to reach the access:
    • the access is the right operand of && / and whose left operand proves the value, or of || / or whose left operand can only be false when the value is proven;
    • the access is in the taken arm of a ternary, in the body of an if or elseif, or in a later elseif condition, elseif body or else body that is reached only after an earlier condition of the same statement failed;
    • an earlier statement of an enclosing statement list is an if without elseif/else whose body never falls through (return, throw, exit, continue, break) and whose condition holds whenever the value is false.
  • A condition proves the value when its being true (or, under !, false) implies it: the value itself (truthiness), !empty(value), isset(value[...]), is_array(value) resolved to the builtin, a strict or loose comparison with the false constant in either operand order, a plain assignment of the producing call tested in place, any conjunct of a true && chain and any disjunct of a false || chain. isset(value) alone, comparisons with other constants and guards on a different variable prove nothing.
  • A value the shared flow analysis proves is not false at the use is never reported, whatever form the proof takes: a truthiness or empty() test, a loose comparison with false or null, a type predicate, or an early exit whose if also has an else branch that continues. In particular if (!$e) { return 0; } else { echo 1; } guards the statements after it. The same holds for a statement that leaves on the failing outcome ($v !== false or exit;, $v or throw …;, $v ?: throw …;), for a failing branch that ends in a call that never returns (a callee declared never, or a function or a final or private method every path of which throws or exits), for a test stored in a boolean and tested later while neither the boolean nor the tested variable has been written, and, on PHP 8.0 and later outside any try with a catch, for a builtin call that returned after receiving the value for a parameter that only accepts arrays, objects or callables: such a call throws a TypeError for false. A switch (true) case is read like the condition of an if/elseif chain; in a switch on the variable itself with constant labels, the cases after a false or null label and the default hold a value that is neither, as does a true case. Positions PHP may skip prove nothing: the right side of ??=, the arguments of a null-safe call and anything chained after ?->. A null-safe call to a never-returning method does not end the path, and f(...) creates a closure without calling f. An arm of match (true) is read like a switch (true) case; an arm that throws, exits or calls a function that never returns takes no part in what holds after the match. | and & between two tests are read like || and && for the outcome that fixes both operands (a false |, a true &), and a strict in_array($v, [...], true) over literals that include neither false nor null excludes both where it holds. goto jumps are followed, including a jump out of a try or catch block, which runs the finally blocks it leaves on its way. Where a goto label is written before the use, and after the place where its scope first names the checked variable, a variable that can hold the same value, or code that may write any variable (extract, compact, get_defined_vars, parse_str given one argument, a variable variable, $GLOBALS, eval, an included file), a jump can land between a guard and the use: only what the flow analysis establishes counts there, and a guard recognized by where it is written proves nothing. Two variables can hold the same value when one is written from the other by a plain assignment, an assignment by reference or ??=, when a foreach takes one out of the other, or through a chain of those; in the value assigned, the receiver and the arguments of a call, the operands of an operator other than ??, the variables a string embeds and an element key do not count, and a statement that only reads (echo $prefix . $parts['host'];) ties nothing. A parameter is named from the start of the function, and a checked expression that reads $this or no variable counts every label before it. In a scope the flow analysis does not follow (one that evaluates or includes code, which may write any variable) a guarded use after such a label is therefore reported. A generator is followed: while it is suspended at a yield, the code that consumes it may write only what an unknown call may write, and, when the generator returns by reference, the variable it yields. A label written before all of that, such as a retry: label ahead of the try that produces and checks the value, changes nothing, whatever else the function reads beside the checked value; at file level every label written before the use counts. What was established of a static local holds until the next call, or code that runs without a call being written (an object converted to text, a magic property or offset access, the destructor of an object a variable held when it is given another value), that may enter the function again and assign the variable. A call of PHP's own counts as such when it may run code of the project: it is given a callback other than the name of a PHP function that only computes on text and numbers and raises no warning whatever text it is given (a closed list: strlen, trim, intval and the like, not hexdec, abs or chr, which warn about text that is no number), an object it may convert, a class name it loads (method_exists($name, 'm'), is_callable('A::b'), defined('A::B'), class_alias($name, 'B'), new ReflectionClass($name), $arrayObject->setIteratorClass($name)), a class to construct ($statement->fetchObject('Row')) or a filter that one of PHP's own FILTER_… constants does not name, which may be the one that calls back, or it runs what was registered earlier (an error, session or output-buffer handler, the callbacks of a cURL transfer or an XML parser, an iterator wrapped in another, a filter added to a stream). A warning that a PHP function raises by itself when called directly (hex2bin given an odd number of digits, a file that cannot be read) is not counted, although the error handler of the project receives it. This limits every guard form of this specification, not only the ones the flow analysis reads.
  • The guard and the access must observe the same value: the same expression text with the same reaching assignment. A reassignment between the guard and the access voids the guard, even when it calls the same method again.
  • A parse that failed always records an error, so getLastErrors() then returns an array: the call is not reported when it only runs after a parse returned false. Two shapes are read, with a parse being DateTime::createFromFormat, DateTimeImmutable::createFromFormat, date_create, date_create_immutable, date_create_from_format or date_create_immutable_from_format, possibly assigned in place:
    • the call is in the first statement of the body of an if whose condition is !parse, false === parse or parse === false;
    • the call is in the statement right after an if without elseif/else whose condition is the parse tested for truth or false !== parse and whose body never falls through (if ($d = DateTime::createFromFormat(…)) { return $d; }). A catch after new DateTime(…) is not such a failure: an error thrown before parsing (a TypeError) leaves no parse error. The statement holding the call must construct no other date and call no other parse (an object of a class that cannot be resolved counts as a date).
  • Unresolved targets, incomplete required facts, suppressed findings and PHP versions lacking the referenced API or language feature produce no report. User declarations shadowing builtin names are not builtin contracts.

Fix ​

No automatic fix: substituting a fabricated diagnostics array changes handling policy.

PHP versions ​

Available from PHP 8.2. Builtin behavior and argument contracts follow the configured PHP version. Extension-specific calls require resolved extension API symbols; do not treat same-named application functions as extension calls.

Options ​

None. Enable or disable this rule through normal rule configuration.

Configure ​

In custos.json:

json
{
  "rules": {
    "DateLastErrorsFalseUnchecked": {
      "enabled": false
    }
  }
}

Suppress ​

Before the statement or declaration (or the first statement of the file):

php
// @custos-ignore DateLastErrorsFalseUnchecked

Released under the MIT License. Native rules and rules modeled on Php Inspections (EA Extended); independent clean-room implementation.