Skip to content

About

[READ-ONLY] PHPStan extension that reports the regex patterns your target PHP refuses, and, opt-in, lint and ReDoS findings. Split of php-regex/php-regex.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

PHPRegex PHPStan

PHPRegex PHPStan

PHPStan extension that reports the regex patterns your target PHP refuses, and, opt-in, lint, ReDoS and optimization findings.

Requires PHP 8.2+ and PHPStan 2.x. MIT licensed.

Features

  • Reports the patterns your target PHP refuses while the engine running PHPStan compiles them; what that engine refuses stays with PHPStan core, never reported twice.
  • Reads the eight preg_* functions, patterns held in constants and constant expressions, and the array keys of preg_replace_callback_array.
  • Reads the constant pattern passed to a parameter marked #[PHPRegex\Parser\Attribute\RegexPattern] or PhpStorm's #[Language('RegExp')], in calls to functions, static and instance methods and constructors: a pattern the running PHP refuses is reported there under PHPStan's regexp.pattern, as PHPStan core reads only preg_* calls, and every other check applies as to a preg_* pattern. PHPStan offers parameter attributes from 2.1.31 at least; on an older 2.x release that lacks them, no parameter is read.
  • Reports, always, a constant replacement of preg_replace() or preg_filter() that refers to a group the pattern does not have ($2, ${2}, \2, and $10, which PHP reads as group 10, never group 1 then 0), or that names a group (${name}, which PHP never substitutes). The replacement is read after PHP's string escapes, an array of patterns paired with the replacement of the same position. When both the pattern and the replacement may take several values, which may vary together (a ternary on one condition, two maps read with one key), a reference is reported only if no possible pattern defines its group.
  • Opt-in lint: the 40 lint rules of php-regex/regex-linter, each finding carrying its rule identifier and a tip.
  • Opt-in ReDoS analysis, theoretical only — the pattern is read, never run inside PHPStan — with four severity thresholds: a proven exponential or polynomial verdict, or a heuristic one, with the attack input in the tip, and, under regex.redos.search, the quadratic cost of an unanchored search whose every attempt is linear. A call whose subject PHPStan knows to be constant is not reported: no input can reach it.
  • Opt-in optimization suggestions behind a minimum-savings setting; every rewrite is proven equivalent by the automata solver before it is reported.
  • With optimizations on, a preg_match($pattern, $subject) a string function answers alike is reported with the function: /^https:/ is str_starts_with($subject, 'https:'), /^(?:GET|POST)\z/ an in_array(). Each is proven by the automata; /^foo$/ is no ===, as $ also takes "foo\n".
  • Stable identifiers for ignoreErrors and baselines: regex.invalidForTarget, regexp.pattern (a marked parameter), regex.replacement.undefinedGroup, regex.redos, regex.redos.search, regex.optimization, regex.trivialMatch, regex.lint.<rule>.
  • The phpRegex parameter is validated by a Neon schema before analysis starts; a version or threshold that names no real value stops the run there.

Installation

composer require --dev php-regex/regex-phpstan

With phpstan/extension-installer this is everything: the extension registers itself.

Without it, include the extension in your phpstan.neon:

includes:
    - vendor/php-regex/regex-phpstan/extension.neon

rules.neon turns the lint rules and the ReDoS analysis on — optimizations stay off, enable them with your own parameters:

includes:
    - vendor/php-regex/regex-phpstan/extension.neon
    - vendor/php-regex/regex-phpstan/rules.neon

Configuration

Everything lives under the phpRegex parameter; suppress findings by identifier in ignoreErrors, as with any PHPStan rule. The defaults as shipped:

Option Default Meaning
phpVersion null PHP the patterns are judged for: null for PHPStan's phpVersion (a {min, max} range there is validated at each later PHP up to max where a rule changes), 'runtime' for the PHP running the analysis, '8.2' or 80200 for a release
pcreVersion null PCRE2 release the patterns are judged for, '10.42'; null for the one the PHP version bundles
checks.lint.enabled false lint rules
checks.redos.enabled false ReDoS analysis
checks.redos.threshold critical lowest severity reported: low, medium, high or critical
checks.optimizations.enabled false optimization suggestions
checks.optimizations.minSavings 1 characters saved before a suggestion is reported
checks.optimizations.options as shipped which rewrites the optimizer may apply: digits, word, ranges, canonicalizeCharClasses and verifyWithAutomata on, possessive and factorize off, minQuantifierCount at 4

Usage

The default check needs no configuration beyond the include — run vendor/bin/phpstan analyse; the target is PHPStan's phpVersion, here PHP 8.2:

// (*scs:...) arrived in PCRE2 10.45; PHP 8.2 bundles 10.40.
preg_match('/(a)(*scs:(1)a)/', $value);
Regex pattern is invalid for PHP 8.2 with PCRE2 10.40: Invalid or unsupported PCRE verb: "scs".
🪪 regex.invalidForTarget

A replacement that refers to a missing group is reported with no configuration either:

preg_replace('/(a)/', '[$10]', $value); // "[]": there is no group 10
Replacement reference $10 names group 10, but /(a)/ has 1 capturing group: preg_replace() substitutes an empty string.
🪪 regex.replacement.undefinedGroup
💡 Two digits are read after "$": write ${1}0 for group 1 followed by "0".

With rules.neon, lint and ReDoS findings appear:

preg_match('/no_dot/s', $value);   // flag 's' with no dot to match
preg_match('/(a+)+$/', $value);    // nested unbounded quantifiers
Flag 's' is useless: the pattern contains no unescaped dot outside a character class.
🪪 regex.lint.flag.useless.s
Nested quantifiers can cause catastrophic backtracking.
🪪 regex.lint.quantifier.nested
💡 Consider atomic groups (?>...) or possessive quantifiers — verify the rewrite still matches everything you need.
Exponential backtracking (ReDoS): /(a+)+$/
🪪 regex.redos
💡 Severity: critical, exponential (proven).
💡 Attack: "a" x n . "!"

A ReDoS message is Exponential backtracking (ReDoS), Polynomial backtracking (ReDoS) or Potential backtracking (ReDoS), then the pattern as the console shows it (escaped, on one line, cut after 50 characters); the text of each stays the same for all of 2.x, and the severity, how the verdict was reached and the attack (str_repeat("a", $n) . "!") are in the tip. When the analysis improves, an error may appear, disappear or change class: regenerate the baseline after such an upgrade, and once after moving from 1.x.

Every lint issue is a PHPStan error, whatever the rule's severity: an issue the regex lint console prints as INFO, such as regex.lint.group.quantifiedCapture on an unnamed group, is reported too, under its own identifier, so you can ignore it by identifier. The rules the linter leaves off by default, such as regex.lint.unicode.shorthandWithoutU or the style rule regex.lint.charclass.single, do not run here. Lint messages and the set of reported issues moved in 2.0.0: after upgrading, regenerate the baseline once with vendor/bin/phpstan analyse --generate-baseline.

Optimizations, once enabled, suggest the shorter equivalent as a tip:

preg_match('/[0-9]+/', $value);
Regex pattern can be optimized: "/[0-9]+/"
🪪 regex.optimization
💡 Consider using: /\d+/

Documentation

  • PHPStan guide — the target model, each check, every identifier
  • Diagnostics — how findings are reported and how to read them
  • ReDoS guide — risky shapes, severities, mitigations
  • Quick start — the PHPRegex packages in five commands

This package is part of PHPRegex, released with its siblings under one version number. Read the backward compatibility promise.

Resources

Sponsors

Sponsor

If PHPRegex saves you time, consider sponsoring its maintenance.

License

MIT. See LICENSE.

About

[READ-ONLY] PHPStan extension that reports the regex patterns your target PHP refuses, and, opt-in, lint and ReDoS findings. Split of php-regex/php-regex.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages