Search by

yoeunes / regex-parser

yoeunes

A PCRE regex parser with lexer, AST builder, validation, ReDoS analysis, and syntax highlighting. Zero runtime dependencies.

Package info

github.com/php-regex/php-regex

Documentation

pkg:composer/yoeunes/regex-parser

Fund package maintenance!

yoeunes

Statistics

Installs: 160 187

Dependents: 0

Suggesters: 0

Stars: 29

Open Issues: 0

v1.3.0 2026-01-12 20:49 UTC

README

PHPRegex

CI Status Badge PHPStan Level Badge Author Badge GitHub Release Badge License Badge Packagist Downloads Badge GitHub Stars Badge Supported PHP Version Badge

PHPRegex: Static Analysis, Linter & Logic Solver

PHPRegex is a PHP 8.2+ library that treats regular expressions as code.

Unlike simple wrappers around preg_match, PHPRegex implements a complete compiler pipeline (Lexer → Parser → AST) and an Automata-based Logic Solver (normalized form → NFA → DFA).

This architecture allows for advanced static analysis:

  • Linting: Detect redundancy, useless flags, and common mistakes.
  • Safety: Prove a pattern safe from catastrophic backtracking (ReDoS), or hand you the input that triggers it.
  • Logic: Compare patterns via NFA/DFA (Intersection, Equivalence, Subset) for the regular subset it supports.

Built for learning, validation, and robust tooling in PHP projects.

⚠️ What this is and is not. PHPRegex is a side project and a learning exercise. It is not a hardened security product and should not be your only line of defense. A ReDoS verdict is proven only for the subset of PCRE the analysis models, one match attempt at a time; outside it, structural heuristics decide, and the verdict says so. The parser aims for PCRE compatibility but does not cover every edge case of the PCRE engine.

If you are new to regex, start with the Regex Tutorial. If you want a short overview, see the Quick Start Guide.

Getting started

# Install the library
composer require php-regex/regex-toolkit

# Install the command-line tool, and try it
composer require --dev php-regex/regex-cli
vendor/bin/regex explain '/\d{4}-\d{2}-\d{2}/'

PHPRegex is a family of packages, developed together in php-regex/php-regex and released with one version number. Install the one you need:

package what it holds
php-regex/regex-toolkit the Regex facade: every library below in one call
php-regex/regex-parser lexer, parser, immutable AST, validator, PCRE2 release targeting, AST cache
php-regex/regex-explain explanations, highlighting, ASCII tree, Mermaid and railroad diagrams
php-regex/regex-optimizer shorter equivalent patterns, modernized syntax
php-regex/regex-generator sample strings and test cases
php-regex/regex-automata language equivalence, intersection and subset
php-regex/regex-redos catastrophic backtracking (ReDoS) analysis
php-regex/regex-transpiler JavaScript and Python targets
php-regex/regex-linter lint rules and pattern extraction from PHP sources
php-regex/regex-cli the regex command
php-regex/regex-language-server diagnostics in any LSP editor
php-regex/regex-phpstan the PHPStan extension
php-regex/regex-symfony the Symfony bundle
php-regex/regex-laravel the Laravel integration

Coming from 1.x (yoeunes/regex-parser)? See UPGRADE-2.0.md.

What PHPRegex provides

  • 🏗️ Deep Parsing: Parse /pattern/flags into a structured, typed AST.
  • 🧠 Logic Solver: Compare two regexes using NFA/DFA transformation (intersection, equivalence, subset). Works for patterns in the regular subset it supports — every character set asked from the running PCRE2 — and refuses the rest with the reason named.
  • 🛡️ ReDoS Analysis: Prove the backtracking cost of a pattern — linear, polynomial or exponential — with the attack input when it is vulnerable, and replay that attack on the running PCRE. Outside the modelled subset, structural heuristics decide and say so.
  • 🧹 Linter: Detect useless flags, redundant groups, and common mistakes via the CLI.
  • 📖 Explanation: Explain patterns in plain English.
  • 🔧 Visitor API: A flexible API for building custom regex tooling.

Philosophy & Accuracy

PHPRegex separates what it can guarantee from what is heuristic:

  • Guaranteed: parsing and AST structure for the targeted PHP/PCRE version.
  • Measured: syntax validation and error offsets follow PHP's engine; the PCRE2 conformance page publishes how closely, case by case.
  • Proven: a ReDoS verdict marked (proven) holds for one match attempt on a model of PCRE's backtracking; safe (proven) means no input makes that attempt backtrack beyond a linear number of steps. The ReDoS guide lists its limits.
  • Heuristic: patterns outside that model (backreferences, conditionals, recursion, …) are judged by structural rules, marked (heuristic); treat those as potential risk unless confirmed.
  • Context matters: PCRE version, JIT, and backtrack/recursion limits change practical impact.

Tested against the real engine

Round-trip correctness (compile(parse(x)) behaves like the original) is verified in CI by differential tests that compare PHPRegex's output against PHP's native preg_match():

  • A fixture of 212 PCRE patterns is checked for validity, match result, and captured groups (OfficialPcreComplianceTest).
  • Additional behavioral tests cover named groups, lookarounds, conditionals, atomic groups, and other features (BehavioralComplianceTest, AdvancedFeaturesComplianceTest).
  • PCRE2's own official test suite (10.48, pinned) is replayed at compile level under PHP's compile options: the compile verdict agrees on more than 4,100 of the roughly 4,400 extractable cases, and fewer than 100 patterns that PHP refuses to compile are accepted. The exact counts, the skipped cases and the fix plan are on the PCRE2 conformance page.

These tests compare against a limited set of subjects, so they catch clear regressions but are not a formal proof of full PCRE equivalence. There may be edge cases that the test suite does not yet cover.

Separately, the linter and ReDoS analyzer have been run over a corpus of over 1,700 unique patterns collected from around 200 real-world PHP projects (Symfony, Laravel, Composer, PHPUnit, …). The lint results are in var/log/corpus.log. This corpus run is not part of the automated CI differential test — it is a snapshot used to validate that the lint and ReDoS rules produce sensible output on real code.

The corpus checkouts themselves are not committed. corpus.json, at the root next to composer.json, lists every repository with its URL, branch and the commit it was pinned to, much like a lock file. bin/corpus manages the checkouts, composer-style:

php bin/corpus install                 # rebuild corpus/ exactly at the pinned commits
php bin/corpus update                  # prune, clone what is missing, then pull everything,
                                       # and write the new commits back to corpus.json
php bin/corpus update --no-prune       # keep checkouts that are no longer listed
php bin/corpus update --add https://github.com/vendor/repo.git [--as path] [--branch main]
php bin/corpus update --write-manifest # rewrite corpus.json from what is on disk

install never writes to corpus.json: delete corpus/ at any time and one command rebuilds it, repository by repository, at the exact commits the manifest pins. update is the only command that moves the pins. A checkout with local changes is never reset or removed unless --force is given, and a repository cloned by hand is removed on the next run unless it is added with --add or listed with --write-manifest first.

Regenerate var/log/corpus.log after updating the corpus, from a terminal:

php bin/regex lint corpus/ --php-version=runtime --output=var/log/corpus.log

--php-version=runtime judges the corpus for the PHP running the command and the PCRE2 it links; without it, the command would judge for the lowest PHP this repository's composer.json allows.

Piping the command instead of running it in a terminal renders the severity badges without their padding, which reformats every severity line of the tracked file.

How to report a vulnerability responsibly

If you believe a pattern is exploitable:

  1. Run confirmed mode: it replays the attack on your PCRE and prints the input length that makes preg_match() fail.
  2. Include the pattern, input lengths, timings, JIT setting, and PCRE limits.
  3. Verify impact in the real code path before filing a security issue.

See SECURITY.md for reporting channels.

Safer rewrites (verify behavior)

These techniques reduce backtracking but can change matching behavior. Always validate with tests.

/(a+)+$/     -> /a+$/      (semantics often preserved, but verify captures)
/(a+)+$/     -> /a++$/     (possessive, no backtracking)
/(a|aa)+/    -> /a+/       (only if alternation is redundant)
/(a|aa)+/    -> /(?>a|aa)+/ (atomic, avoids backtracking)

How it works

  • Regex::parse() splits the literal into pattern and flags.
  • The lexer produces a token stream.
  • The parser builds an AST (RegexNode).
  • Visitors walk the AST to validate, explain, analyze, or transform.

For the full architecture, see docs/ARCHITECTURE.md.

CLI quick tour

# Parse and validate a pattern
vendor/bin/regex parse '/^hello world$/'

# Get plain English explanation
vendor/bin/regex explain '/\d{4}-\d{2}-\d{2}/'

# Check for ReDoS: a proven verdict, and the attack when vulnerable
vendor/bin/regex analyze '/(a+)+$/'

# Colorize pattern for better readability
vendor/bin/regex highlight '/\d+/'

# Lint your entire codebase
vendor/bin/regex lint src/

Regex Lint Output

PHP API at a glance

use PHPRegex\Toolkit\Regex;
use PHPRegex\Redos\RedosMode;

$regex = Regex::create([
    'runtime_pcre_validation' => true,
]);

// Parse a pattern into AST
$ast = $regex->parse('/^hello world$/i');

// Validate pattern syntax and semantics
$result = $regex->validate('/(?<=test)foo/');
if (!$result->isValid) {
    echo $result->error;
}

// Check for ReDoS (theoretical by default)
$analysis = $regex->redos('/(a+)+$/');
echo $analysis->severity->value;   // 'critical'
echo $analysis->headline();        // 'Exponential backtracking (proven)'
echo $analysis->witness->render(); // '"a" x n . "!"': the input that triggers it

// Optional: replay the attack on the running PCRE
$confirmed = $regex->redos('/(a+)+$/', mode: RedosMode::Confirmed);
echo $confirmed->isConfirmed() ? 'confirmed' : 'theoretical'; // 'confirmed'

// Get human-readable explanation
echo $regex->explain('/\d{4}-\d{2}-\d{2}/');

Integrations

PHPRegex integrates with common PHP tooling:

  • Symfony bundle: the Symfony guide
  • Laravel: the Laravel guide
  • Language server: the language server guide
  • PHPStan: enabled by extension-installer, or through vendor/php-regex/regex-phpstan/extension.neon. It reports a pattern your target PHP refuses while the PHP running PHPStan compiles it; lint rules and ReDoS analysis come with rules.neon. See the PHPStan guide
  • GitHub Actions: vendor/bin/regex lint in your CI pipeline

Performance

PHPRegex ships lightweight benchmark scripts in benchmarks/ to track parser, compiler, and formatter throughput.

  • Run formatter benchmarks: php benchmarks/benchmark_formatters.php
  • Run all benchmarks: for file in benchmarks/benchmark_*.php; do echo "Running $file"; php "$file"; echo; done

Documentation

Start here:

Key references:

Contributing

Contributions are welcome! See CONTRIBUTING.md to get started.

# Set up development environment
composer install

# Run tests
composer phpunit

# Check code style
composer phpcs

# Run static analysis
composer phpstan

Sponsors

Sponsor

If PHPRegex saves you time, consider sponsoring its maintenance.

Support

If you run into issues or have questions, please open an issue on GitHub: https://github.com/php-regex/php-regex/issues.

License

Released under the MIT License.