Skip to content

OptionalRegexCaptureReadWithoutNullFlag ​

warning native on by default

Group: Probable bugs

An unmatched optional regex capture can be absent from the results. Guard the access or request null placeholders for unmatched captures.

Example ​

php
<?php
preg_match("/^(a)?b$/","b",$m);echo $m[1];

Reported:

  • line 2: Guard the unmatched optional capture or request null placeholders.

Detection ​

  • Resolved preg_match literal pattern has a trailing optional capture and known literal subject proves that capture unmatched. Subsequent unconditional literal-index access reads the absent capture, without isset/array_key_exists/coalesce guard or PREG_UNMATCHED_AS_NULL.
  • Regex proof supports ordinary ASCII patterns compatible with RE2, no modifiers, lookarounds, backreferences, or unsupported PCRE extensions. Unknown matching behavior is excluded.
  • Regex proof requires an ASCII subject and an ordinary compatible PCRE subset without modifiers or whitespace escapes \s or \S. Reject patterns matching an empty string and subjects larger than 32768 bytes.
  • Require an actual value read of the absent capture. Exclude writes, unset, by-reference uses and unknown argument uses, and exclude statements that also mutate the result array. The regex offset must be omitted or statically known zero; nonzero or unknown offsets are unsupported.
  • D1e. All argument uses are excluded because they can involve reference passing. Also exclude increment/decrement, unset and write contexts. Any same-statement mutation or call receiving the result array discards the actual-read proof.
  • D1f. A read is guarded when it is the operand of isset/empty, the left operand of ??, or when the conditions guarding it prove the same capture (same result variable, same literal index) exists: the right operand of &&/||, the ternary branch, match (true) arm or if body entered only once isset($m[i]) or !empty($m[i]) (seen through parentheses, !, &&/|| and comparisons with booleans) established it. The right operand of ?? is not guarded by that operator. A statement calling array_key_exists on the result array is already excluded by D1e.
  • D1g. A by-reference bind of the capture ($x = &$m[i], [&$m[i]]) creates the element instead of reading it and is not reported.
  • Resolve builtin functions, classes, constants and methods; user-defined lookalikes are excluded. Follow only bounded local proof in one lexical scope.
  • Emit one finding per violating operation, not per supporting evidence statement. When the proof is inconclusive, nothing is reported.

Exceptions (no report) ​

  • Required captures, unknown match outcomes, guarded reads (D1f), by-reference binds (D1g), and unsupported regex syntax are excluded.
  • Both standard suppression forms suppress this native ID.

Fix ​

None. Choosing a repair requires intent or additional runtime information.

PHP versions ​

Available throughout supported PHP 5.3–8.5 where the referenced builtin/extension is available. Respect syntax/version-specific receiver contracts; unavailable APIs are not this rule’s concern.

See the PHP regex capture and offset contract for the relevant builtin behavior.

Configure ​

In custos.json:

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

Suppress ​

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

php
// @custos-ignore OptionalRegexCaptureReadWithoutNullFlag

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