From 7c9c231c9e463d8c2bf3602e8cc12df052fb8eef Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Mon, 26 Oct 2026 10:56:44 +0000 Subject: [PATCH 01/11] Replace Hoa with a regex parser generated by phplrt The grammar is ported to phplrt's PP3 format in build/regex-parser/RegexGrammar.pp3 and compiled with `make regex-parser` into src/Type/Regex/Parser/RegexParser.php. The compiler runs on PHP 8.4+ and is not a dependency of PHPStan. The generated parser only needs the phplrt runtime, provided by ondrejmirtes/phplrt-runtime, a fork downgraded to PHP 7.4. The parser builds the same trees Hoa did, so RegexGroupParser and IgnoredRegexValidator walk them without changes in behaviour. Amp-Thread-ID: https://ampcode.com/threads/T-01a177b5-960f-71d2-a3e2-fa7649f39d2c --- CLAUDE.md | 13 +- Makefile | 8 + bin/phpstan | 7 - build/phpstan.neon | 1 + build/regex-parser/.gitignore | 1 + build/regex-parser/RegexGrammar.pp3 | 365 +++ build/regex-parser/composer.json | 13 + build/regex-parser/composer.lock | 2049 +++++++++++++++++ build/regex-parser/generate.php | 54 + compiler/build/scoper-namespaces.php | 1 - compiler/build/scoper.inc.php | 5 - composer.json | 34 +- composer.lock | 972 +------- patches/Buffer.patch | 54 - patches/Consistency.patch | 45 - patches/File.patch | 11 - patches/HoaException.patch | 11 - patches/HoaParser.patch | 11 - patches/Idle.patch | 11 - patches/Invocation.patch | 11 - patches/Lexer.patch | 12 - patches/Lookahead.patch | 52 - patches/Node.patch | 46 - patches/Prelude.patch | 11 - patches/Read.patch | 11 - patches/Rule.patch | 14 - patches/Stream.patch | 32 - patches/TreeNode.patch | 14 - patches/Wrapper.patch | 27 - phpcs.xml | 2 + resources/RegexGrammar.pp | 224 -- src/Command/IgnoredRegexValidator.php | 31 +- .../ValidateIgnoredErrorsExtension.php | 6 +- src/Type/Php/RegexArrayShapeMatcher.php | 2 +- src/Type/Regex/Parser/RegexNode.php | 65 + src/Type/Regex/Parser/RegexParser.php | 1279 ++++++++++ .../Regex/Parser/RegexSyntaxException.php | 11 + src/Type/Regex/RegexGroupParser.php | 65 +- .../Command/IgnoredRegexValidatorTest.php | 7 +- tests/PHPStan/Composer/AutoloadFilesTest.php | 2 - 40 files changed, 3986 insertions(+), 1604 deletions(-) create mode 100644 build/regex-parser/.gitignore create mode 100644 build/regex-parser/RegexGrammar.pp3 create mode 100644 build/regex-parser/composer.json create mode 100644 build/regex-parser/composer.lock create mode 100644 build/regex-parser/generate.php delete mode 100644 patches/Buffer.patch delete mode 100644 patches/Consistency.patch delete mode 100644 patches/File.patch delete mode 100644 patches/HoaException.patch delete mode 100644 patches/HoaParser.patch delete mode 100644 patches/Idle.patch delete mode 100644 patches/Invocation.patch delete mode 100644 patches/Lexer.patch delete mode 100644 patches/Lookahead.patch delete mode 100644 patches/Node.patch delete mode 100644 patches/Prelude.patch delete mode 100644 patches/Read.patch delete mode 100644 patches/Rule.patch delete mode 100644 patches/Stream.patch delete mode 100644 patches/TreeNode.patch delete mode 100644 patches/Wrapper.patch delete mode 100644 resources/RegexGrammar.pp create mode 100644 src/Type/Regex/Parser/RegexNode.php create mode 100644 src/Type/Regex/Parser/RegexParser.php create mode 100644 src/Type/Regex/Parser/RegexSyntaxException.php diff --git a/CLAUDE.md b/CLAUDE.md index bf706000363..7766526269e 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -185,6 +185,17 @@ Collects structural information about a class during AST traversal: properties, - **`PHPStan\Cache`** - Caching infrastructure - **`PHPStan\Testing`** - `RuleTestCase`, `TypeInferenceTestCase`, and other test utilities +### Regex parser (`src/Type/Regex/`) + +`RegexGroupParser` parses regex patterns to infer `preg_match()` array shapes (via `RegexArrayShapeMatcher`) and `IgnoredRegexValidator` uses it to validate `ignoreErrors` patterns. The parser itself is generated by [phplrt](https://github.com/phplrt/phplrt): + +- **Grammar**: `build/regex-parser/RegexGrammar.pp3` (phplrt `.pp3` format, derived from Hoa's regex grammar). The reducers in the grammar build a tree of `PHPStan\Type\Regex\Parser\RegexNode` objects (`#capturing`, `#alternation`, `#quantification`, `token`, ...) that `RegexGroupParser` walks. +- **Generated parser**: `src/Type/Regex/Parser/RegexParser.php`. Never edit it by hand. Change the grammar and regenerate, then commit both files together. +- **Regenerating**: `make regex-parser`. It installs the phplrt compiler into `build/regex-parser/vendor` (separate `build/regex-parser/composer.json`, needs PHP 8.4+) and runs `build/regex-parser/generate.php`. The script targets PHP 8.1 syntax and post-processes the output so that the PHAR's PHP 7.4 downgrader can handle it (first-class callables become `\Closure::fromCallable()`) and adds `@throws` to `parse()`. +- **Runtime**: the generated parser only needs the phplrt runtime, which comes from the `ondrejmirtes/phplrt-runtime` fork (a PHP 7.4 compatible downgrade of the `phplrt/*` runtime packages that replaces them). The compiler is a build-time dependency only and is never shipped. +- Parse failures throw `Phplrt\Contracts\Parser\Exception\ParserExceptionInterface` or `RegexSyntaxException`; callers treat both as "pattern not supported" and infer no shape. +- After changing the grammar or `RegexGroupParser`, run the regex tests: `tests/PHPStan/Analyser/nsrt/preg_*.php`, `tests/PHPStan/Type/Php/`, `tests/PHPStan/Type/Regex/` and `tests/PHPStan/Rules/Regexp/`. + ## Extension points PHPStan is highly extensible. Key extension interfaces: @@ -339,4 +350,4 @@ When adding or editing PHPDoc comments in this codebase, follow these guidelines - `nette/neon` - Configuration file format - `react/child-process`, `react/async` - Parallel analysis - `symfony/console` - CLI interface -- `hoa/compiler` - Used for regex type parsing +- `ondrejmirtes/phplrt-runtime` - PHP 7.4 compatible fork of the phplrt runtime, runs the regex parser generated from `build/regex-parser/RegexGrammar.pp3` (`make regex-parser`) diff --git a/Makefile b/Makefile index deab32ea712..3cdbc82fcab 100644 --- a/Makefile +++ b/Makefile @@ -177,6 +177,14 @@ phpstan-generate-baseline-php: phpstan-pro: php -d memory_limit=450M bin/phpstan --pro +# Regenerates src/Type/Regex/Parser/RegexParser.php from +# build/regex-parser/RegexGrammar.pp3 with the phplrt compiler. The compiler +# needs PHP 8.4+, the generated parser only needs the phplrt runtime. +.PHONY: regex-parser +regex-parser: + composer install --working-dir build/regex-parser + php build/regex-parser/generate.php + name-collision: php vendor/bin/detect-collisions --configuration build/collision-detector.json diff --git a/bin/phpstan b/bin/phpstan index ee20a379e1e..85fd6e79bc2 100755 --- a/bin/phpstan +++ b/bin/phpstan @@ -52,13 +52,6 @@ use Symfony\Component\Console\Helper\ProgressBar; } $devOrPharLoader->unregister(); - // fix missing key for vendor/hoa/protocol/Wrapper.php - $existingComposerAutoloadFiles = $GLOBALS['__composer_autoload_files'] ?? []; - $GLOBALS['__composer_autoload_files'] = array_merge( - $existingComposerAutoloadFiles, - array_fill_keys(['3e76f7f02b41af8cea96018933f6b7e3'], true), - ); - $autoloaderInWorkingDirectory = $vendorDirectory . '/autoload.php'; $composerAutoloaderProjectPaths = []; diff --git a/build/phpstan.neon b/build/phpstan.neon index 3c8716e50e2..e4c1650ff6a 100644 --- a/build/phpstan.neon +++ b/build/phpstan.neon @@ -41,6 +41,7 @@ parameters: - ../tests/PHPStan/Rules/Properties/UninitializedPropertyRuleTest.php - ../tests/PHPStan/Command/IgnoredRegexValidatorTest.php - ../src/Command/IgnoredRegexValidator.php + - ../src/Type/Regex/Parser/RegexParser.php exceptions: uncheckedExceptionClasses: - 'PHPStan\ShouldNotHappenException' diff --git a/build/regex-parser/.gitignore b/build/regex-parser/.gitignore new file mode 100644 index 00000000000..22d0d82f809 --- /dev/null +++ b/build/regex-parser/.gitignore @@ -0,0 +1 @@ +vendor diff --git a/build/regex-parser/RegexGrammar.pp3 b/build/regex-parser/RegexGrammar.pp3 new file mode 100644 index 00000000000..64adaf69a93 --- /dev/null +++ b/build/regex-parser/RegexGrammar.pp3 @@ -0,0 +1,365 @@ +// +// Hoa +// +// +// @license +// +// New BSD License +// +// Copyright © 2007-2017, Hoa community. All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are met: +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of the Hoa nor the names of its contributors may be +// used to endorse or promote products derived from this software without +// specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS AND CONTRIBUTORS BE +// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +// POSSIBILITY OF SUCH DAMAGE. +// +// Grammar \Hoa\Regex\Grammar. +// +// Provide grammar of PCRE (Perl Compatible Regular Expression)for the LL(k) +// parser. More informations at http://pcre.org/pcre.txt, sections pcrepattern & +// pcresyntax. +// +// @copyright Copyright © 2007-2017 Hoa community. +// @license New BSD License +// +// Ported to the phplrt PP3 grammar format by the PHPStan project. +// The generated parser lives in src/Type/Regex/Parser/RegexParser.php, +// regenerate it with `make regex-parser`. + + +%pragma root Expression + +// Character classes. +// The "fc" variants are followed by a "]" which is a literal and does not close the class. +%token T_NEGATIVE_CLASS_FC \[\^\] -> state(class) +%token T_CLASS_FC \[\] -> state(class) +%token T_NEGATIVE_CLASS \[\^ -> state(class) +%token T_CLASS \[ -> state(class) +%token class:T_POSIX_CLASS \[:\^?[a-z]+:\] +%token class:T_CLASS_NESTED \[ +%token class:T_CLASS_CLOSE \] -> exit() +%token class:T_RANGE \- +// \b means backspace in character classes +%token class:T_CLASS_CHARACTER \\(?:[aefnrtb]|c[\x00-\x7f]) +%token class:T_CLASS_DYNAMIC_CHARACTER \\(?:[0-7]{3}|x[0-9a-zA-Z]{2}|x{[0-9a-zA-Z]+}) +%token class:T_CLASS_CHARACTER_TYPE \\(?:[CdDhHNRsSvVwWX]|[pP]{[^}]+}) +%token class:T_CLASS_LITERAL \\.|.|\n + +// Internal options. +%token T_INTERNAL_OPTION \(\?[imsxnJUX^]*-?[imsxnJUX^]+\) + +// Lookahead and lookbehind assertions. +%token T_LOOKAHEAD \(\?= +%token T_NEGATIVE_LOOKAHEAD \(\?! +%token T_LOOKBEHIND \(\?<= +%token T_NEGATIVE_LOOKBEHIND \(\?\) +%token T_ABSOLUTE_REFERENCE \(\?\((\d+)\) +%token T_RELATIVE_REFERENCE \(\?\(([\+\-]\d+)\) +%token T_ASSERTION_REFERENCE \(\?\( + +// Comments. +%token T_COMMENT \(\?#(.*?)(? +%token T_NON_CAPTURING \(\?: +%token T_NON_CAPTURING_INTERNAL_OPTION \(\?[imsxnJUX^]*-?[imsxnJUX^]+: +%token T_NON_CAPTURING_RESET \(\?\| +%token T_ATOMIC_GROUP \(\?> +%token T_CAPTURING \( +%token T_CAPTURING_CLOSE \) + +// Quantifiers (by default, greedy). +%token T_ZERO_OR_ONE_POSSESSIVE \?\+ +%token T_ZERO_OR_ONE_LAZY \?\? +%token T_ZERO_OR_ONE \? +%token T_ZERO_OR_MORE_POSSESSIVE \*\+ +%token T_ZERO_OR_MORE_LAZY \*\? +%token T_ZERO_OR_MORE \* +%token T_ONE_OR_MORE_POSSESSIVE \+\+ +%token T_ONE_OR_MORE_LAZY \+\? +%token T_ONE_OR_MORE \+ +%token T_EXACTLY_N \{[0-9]+\} +%token T_N_TO_M_POSSESSIVE \{[0-9]+,[0-9]+\}\+ +%token T_N_TO_M_LAZY \{[0-9]+,[0-9]+\}\? +%token T_N_TO_M \{[0-9]+,[0-9]+\} +%token T_N_OR_MORE_POSSESSIVE \{[0-9]+,\}\+ +%token T_N_OR_MORE_LAZY \{[0-9]+,\}\? +%token T_N_OR_MORE \{[0-9]+,\} + +// Alternation. +%token T_ALTERNATION \| + +// Literals. +%token T_CHARACTER \\(?:[aefnrt]|c[\x00-\x7f]) +%token T_DYNAMIC_CHARACTER \\(?:[0-7]{3}|x[0-9a-zA-Z]{2}|x{[0-9a-zA-Z]+}) +%token T_CHARACTER_TYPE \\(?:[CdDhHNRsSvVwWX]|[pP]{[^}]+}) +%token T_ANCHOR \\[bBAZzG]|\^|\$ +%token T_MATCH_POINT_RESET \\K +%token T_LITERAL \\.|.|\n + +Expression -> { + return new RegexNode('#expression', children: \is_array($children) ? $children : [$children]); +} + : Alternation()? + ; + +Alternation -> { + if (!\is_array($children)) { + return $children; + } + $nodes = []; + $isAlternation = false; + foreach ($children as $child) { + if ($child instanceof TokenInterface) { + $isAlternation = true; + $child = new RegexNode('token', 'alternation', $child->value); + } + $nodes[] = $child; + } + if (!$isAlternation) { + return $children[0] ?? []; + } + return new RegexNode('#alternation', children: $nodes); +} + : Concatenation()? ( Concatenation()?)* + ; + +Concatenation -> { + if (!\is_array($children)) { + return $children; + } + if (\count($children) === 1) { + return $children[0]; + } + return new RegexNode('#concatenation', children: $children); +} + : (InternalOptions() | Assertion() | Quantification() | Condition())+ + ; + +InternalOptions -> { + return new RegexNode('#internal_options', children: [ + new RegexNode('token', 'internal_option', $children->value), + ]); +} + : + ; + +Condition -> { + $reference = \array_shift($children); + switch ($reference->id) { + case self::T_NAMED_REFERENCE: + \array_unshift($children, new RegexNode('token', 'capturing_name', $reference->captures[0])); + return new RegexNode('#namedcondition', children: $children); + case self::T_ABSOLUTE_REFERENCE: + \array_unshift($children, new RegexNode('token', 'index', $reference->captures[0])); + return new RegexNode('#absolutecondition', children: $children); + case self::T_RELATIVE_REFERENCE: + \array_unshift($children, new RegexNode('token', 'index', $reference->captures[0])); + return new RegexNode('#relativecondition', children: $children); + default: + return new RegexNode('#assertioncondition', children: $children); + } +} + : ( + + | + | + | Alternation()? ::T_CAPTURING_CLOSE:: + ) + Alternation()? + ::T_CAPTURING_CLOSE:: + ; + +Assertion -> { + $assertion = \array_shift($children); + $ids = [ + self::T_LOOKAHEAD => '#lookahead', + self::T_NEGATIVE_LOOKAHEAD => '#negativelookahead', + self::T_LOOKBEHIND => '#lookbehind', + self::T_NEGATIVE_LOOKBEHIND => '#negativelookbehind', + ]; + return new RegexNode($ids[$assertion->id], children: $children); +} + : ( | | | ) + Alternation()? + ::T_CAPTURING_CLOSE:: + ; + +Quantification -> { + if (!\is_array($children)) { + return $children; + } + if (\count($children) === 1) { + return $children[0]; + } + return new RegexNode('#quantification', children: [ + $children[0], + new RegexNode('token', \strtolower(\substr($children[1]->name, 2)), $children[1]->value), + ]); +} + : (CharacterClass() | Capturing() | Literal()) Quantifier()? + ; + +Quantifier + : | | + | | | + | | | + | + | | | + | | | + ; + +CharacterClass -> { + $tokens = $children->children; + $close = \array_pop($tokens); + if ($close === null || $close->name !== 'T_CLASS_CLOSE') { + throw new RegexSyntaxException('Unclosed character class'); + } + + $nodes = []; + if ($children->id === self::T_NEGATIVE_CLASS_FC || $children->id === self::T_CLASS_FC) { + $nodes[] = new RegexNode('token', '_class', ']'); + } + + $literalNames = [ + 'T_CLASS_CHARACTER' => 'character', + 'T_CLASS_DYNAMIC_CHARACTER' => 'dynamic_character', + 'T_CLASS_CHARACTER_TYPE' => 'character_type', + 'T_CLASS_LITERAL' => 'literal', + ]; + + $count = \count($tokens); + $i = 0; + // ? ( | | range() ? | literal() )* ? + if ($i < $count && $tokens[$i]->name === 'T_RANGE') { + $nodes[] = new RegexNode('token', 'range', $tokens[$i]->value); + $i++; + } + while ($i < $count) { + $token = $tokens[$i]; + if ($token->name === 'T_POSIX_CLASS') { + $nodes[] = new RegexNode('token', 'posix_class', $token->value); + $i++; + continue; + } + if ($token->name === 'T_CLASS_NESTED') { + $nodes[] = new RegexNode('token', 'class_', $token->value); + $i++; + continue; + } + if (!isset($literalNames[$token->name])) { + break; + } + $literal = new RegexNode('token', $literalNames[$token->name], $token->value); + if ( + $i + 2 < $count + && $tokens[$i + 1]->name === 'T_RANGE' + && isset($literalNames[$tokens[$i + 2]->name]) + ) { + $nodes[] = new RegexNode('#range', children: [ + $literal, + new RegexNode('token', $literalNames[$tokens[$i + 2]->name], $tokens[$i + 2]->value), + ]); + $i += 3; + if ($i < $count && $tokens[$i]->name === 'T_RANGE') { + $nodes[] = new RegexNode('token', 'range', $tokens[$i]->value); + $i++; + } + continue; + } + $nodes[] = $literal; + $i++; + } + if ($i < $count && $tokens[$i]->name === 'T_RANGE') { + $nodes[] = new RegexNode('token', 'range', $tokens[$i]->value); + $i++; + } + if ($i !== $count) { + throw new RegexSyntaxException('Unexpected token in character class'); + } + + $negative = $children->id === self::T_NEGATIVE_CLASS_FC || $children->id === self::T_NEGATIVE_CLASS; + return new RegexNode($negative ? '#negativeclass' : '#class', children: $nodes); +} + : | | | + ; + +Capturing -> { + if (!\is_array($children)) { + $children = [$children]; + } + $opening = \array_shift($children); + switch ($opening->id) { + case self::T_MARKER: + return new RegexNode('#mark', children: [ + new RegexNode('token', 'name', $opening->captures[0]), + ]); + case self::T_COMMENT: + return new RegexNode('#comment', children: $opening->captures[0] === '' ? [] : [ + new RegexNode('token', 'comment', $opening->captures[0]), + ]); + case self::T_NAMED_CAPTURING: + \array_unshift($children, new RegexNode('token', 'capturing_name', $opening->captures[0])); + return new RegexNode('#namedcapturing', children: $children); + case self::T_NON_CAPTURING: + return new RegexNode('#noncapturing', children: $children); + case self::T_NON_CAPTURING_INTERNAL_OPTION: + \array_unshift($children, new RegexNode('token', 'non_capturing_internal_option', $opening->value)); + return new RegexNode('#noncapturing', children: $children); + case self::T_NON_CAPTURING_RESET: + return new RegexNode('#noncapturingreset', children: $children); + case self::T_ATOMIC_GROUP: + return new RegexNode('#atomicgroup', children: $children); + default: + return new RegexNode('#capturing', children: $children); + } +} + : + | + | ( + + | + | + | + | + | + ) + Alternation()? + ::T_CAPTURING_CLOSE:: + ; + +Literal -> { + return new RegexNode('token', \strtolower(\substr($children->name, 2)), $children->value); +} + : + | + | + | + | + | + ; diff --git a/build/regex-parser/composer.json b/build/regex-parser/composer.json new file mode 100644 index 00000000000..4432594d8fa --- /dev/null +++ b/build/regex-parser/composer.json @@ -0,0 +1,13 @@ +{ + "name": "phpstan/regex-parser-generator", + "description": "Generates src/Type/Regex/Parser/RegexParser.php from RegexGrammar.pp3", + "type": "project", + "license": ["MIT"], + "require": { + "php": "^8.4", + "phplrt/compiler": "^4.2.4" + }, + "config": { + "sort-packages": true + } +} diff --git a/build/regex-parser/composer.lock b/build/regex-parser/composer.lock new file mode 100644 index 00000000000..99c7fa52c60 --- /dev/null +++ b/build/regex-parser/composer.lock @@ -0,0 +1,2049 @@ +{ + "_readme": [ + "This file locks the dependencies of your project to a known state", + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", + "This file is @generated automatically" + ], + "content-hash": "893c1c7b6bf054bfe0d0fb737629d08c", + "packages": [ + { + "name": "phplrt/compiler", + "version": "4.2.4", + "source": { + "type": "git", + "url": "https://github.com/phplrt/compiler.git", + "reference": "b093fc0b5c045d2632bea351137980263f3c82e7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phplrt/compiler/zipball/b093fc0b5c045d2632bea351137980263f3c82e7", + "reference": "b093fc0b5c045d2632bea351137980263f3c82e7", + "shasum": "" + }, + "require": { + "composer-runtime-api": "^2.0", + "php": "^8.1", + "phplrt/exception": "^4.2", + "phplrt/lexer-builder": "^4.2", + "phplrt/parser-builder": "^4.2", + "phplrt/source": "^4.2", + "psr/log": "^3.0", + "symfony/console": "^6.4|^7.0|^8.0", + "twig/twig": "^3.30" + }, + "require-dev": { + "phpstan/phpstan": "^2.3", + "testo/testo": "^0.10.45" + }, + "suggest": { + "llm/skills": "Delivers the bundled phplrt agent skill (a lexer/parser authoring guide) into your AI coding assistant" + }, + "bin": [ + "bin/phplrt" + ], + "type": "library", + "extra": { + "skills": { + "sources": [ + { + "ref": "self.version", + "from": "github", + "package": "phplrt/phplrt" + } + ] + }, + "branch-alias": { + "dev-main": "4.x-dev", + "dev-master": "4.x-dev" + } + }, + "autoload": { + "psr-4": { + "Phplrt\\Compiler\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Kirill Nesmeyanov", + "email": "nesk@xakep.ru" + } + ], + "description": "The phplrt component for compiling a grammar file into a lexer and a parser", + "homepage": "https://github.com/phplrt", + "keywords": [ + "compiler", + "generator", + "grammar", + "lexer", + "parser", + "pp2" + ], + "support": { + "issues": "https://github.com/phplrt/phplrt/issues", + "source": "https://github.com/phplrt/compiler" + }, + "time": "2026-10-16T08:44:06+00:00" + }, + { + "name": "phplrt/exception", + "version": "4.2.4", + "source": { + "type": "git", + "url": "https://github.com/phplrt/exception.git", + "reference": "b5df39f3661b8651baba684bb17f0d69ad366f98" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phplrt/exception/zipball/b5df39f3661b8651baba684bb17f0d69ad366f98", + "reference": "b5df39f3661b8651baba684bb17f0d69ad366f98", + "shasum": "" + }, + "require": { + "php": "^8.1", + "phplrt/lexer-contracts": "^4.2", + "phplrt/parser-contracts": "^4.2", + "phplrt/position": "^4.2", + "phplrt/source": "^4.2" + }, + "require-dev": { + "ext-intl": "*", + "ext-mbstring": "*", + "phpstan/phpstan": "^2.3", + "testo/testo": "^0.10.45" + }, + "suggest": { + "llm/skills": "Delivers the bundled phplrt agent skill (a lexer/parser authoring guide) into your AI coding assistant" + }, + "type": "library", + "extra": { + "skills": { + "sources": [ + { + "ref": "self.version", + "from": "github", + "package": "phplrt/phplrt" + } + ] + }, + "branch-alias": { + "dev-main": "4.x-dev", + "dev-master": "4.x-dev" + } + }, + "autoload": { + "psr-4": { + "Phplrt\\Exception\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Kirill Nesmeyanov", + "email": "nesk@xakep.ru" + } + ], + "description": "The phplrt library to display parsing errors", + "homepage": "https://github.com/phplrt", + "keywords": [ + "error", + "exception", + "formatter", + "parsing", + "printer" + ], + "support": { + "issues": "https://github.com/phplrt/phplrt/issues", + "source": "https://github.com/phplrt/exception" + }, + "time": "2026-10-15T12:10:17+00:00" + }, + { + "name": "phplrt/lexer", + "version": "4.2.4", + "source": { + "type": "git", + "url": "https://github.com/phplrt/lexer.git", + "reference": "ced66a094f69d4f874d70923eabda290ac1ef63e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phplrt/lexer/zipball/ced66a094f69d4f874d70923eabda290ac1ef63e", + "reference": "ced66a094f69d4f874d70923eabda290ac1ef63e", + "shasum": "" + }, + "require": { + "ext-pcre": "*", + "php": "^8.1", + "phplrt/lexer-contracts": "^4.2", + "phplrt/source-contracts": "^4.2" + }, + "provide": { + "phplrt/lexer-contracts-implementation": "^4.0" + }, + "require-dev": { + "ext-mbstring": "*", + "phplrt/exception": "^4.2", + "phplrt/lexer-builder": "^4.2", + "phplrt/source": "^4.2", + "phpstan/phpstan": "^2.3", + "testo/testo": "^0.10.45" + }, + "suggest": { + "llm/skills": "Delivers the bundled phplrt agent skill (a lexer/parser authoring guide) into your AI coding assistant", + "phplrt/exception": "Renders the syntax errors along with the fragment of the source code they occurred in" + }, + "type": "library", + "extra": { + "skills": { + "sources": [ + { + "ref": "self.version", + "from": "github", + "package": "phplrt/phplrt" + } + ] + }, + "branch-alias": { + "dev-main": "4.x-dev", + "dev-master": "4.x-dev" + } + }, + "autoload": { + "psr-4": { + "Phplrt\\Lexer\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Kirill Nesmeyanov", + "email": "nesk@xakep.ru" + } + ], + "description": "The phplrt lexer library that can be used for scanners implementation using PCRE", + "homepage": "https://github.com/phplrt", + "keywords": [ + "lexer", + "parsing", + "regex", + "scanner", + "stream", + "token", + "tokenizer" + ], + "support": { + "issues": "https://github.com/phplrt/phplrt/issues", + "source": "https://github.com/phplrt/lexer" + }, + "time": "2026-10-12T23:29:56+00:00" + }, + { + "name": "phplrt/lexer-builder", + "version": "4.2.4", + "source": { + "type": "git", + "url": "https://github.com/phplrt/lexer-builder.git", + "reference": "4280d42b54638a02f65e76ed365651260107b908" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phplrt/lexer-builder/zipball/4280d42b54638a02f65e76ed365651260107b908", + "reference": "4280d42b54638a02f65e76ed365651260107b908", + "shasum": "" + }, + "require": { + "php": "^8.1", + "phplrt/exception": "^4.2", + "phplrt/lexer": "^4.2", + "phplrt/source-contracts": "^4.2", + "psr/log": "^3.0" + }, + "require-dev": { + "phpstan/phpstan": "^2.3", + "testo/testo": "^0.10.45" + }, + "suggest": { + "llm/skills": "Delivers the bundled phplrt agent skill (a lexer/parser authoring guide) into your AI coding assistant" + }, + "type": "library", + "extra": { + "skills": { + "sources": [ + { + "ref": "self.version", + "from": "github", + "package": "phplrt/phplrt" + } + ] + }, + "branch-alias": { + "dev-main": "4.x-dev", + "dev-master": "4.x-dev" + } + }, + "autoload": { + "psr-4": { + "Phplrt\\Lexer\\Builder\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Kirill Nesmeyanov", + "email": "nesk@xakep.ru" + } + ], + "description": "The phplrt component for assembling a lexer", + "homepage": "https://github.com/phplrt", + "keywords": [ + "build", + "compiler", + "lexer", + "parsing", + "regex", + "scanner", + "stream", + "token", + "tokenizer" + ], + "support": { + "issues": "https://github.com/phplrt/phplrt/issues", + "source": "https://github.com/phplrt/lexer-builder" + }, + "time": "2026-10-16T08:43:44+00:00" + }, + { + "name": "phplrt/lexer-contracts", + "version": "4.2.4", + "source": { + "type": "git", + "url": "https://github.com/phplrt/lexer-contracts.git", + "reference": "8df53e70664674b0defeaa1212056b38ca3685f6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phplrt/lexer-contracts/zipball/8df53e70664674b0defeaa1212056b38ca3685f6", + "reference": "8df53e70664674b0defeaa1212056b38ca3685f6", + "shasum": "" + }, + "require": { + "php": "^8.1", + "phplrt/source-contracts": "^4.2" + }, + "require-dev": { + "phpstan/phpstan": "^2.3", + "testo/testo": "^0.10.45" + }, + "suggest": { + "llm/skills": "Delivers the bundled phplrt agent skill (a lexer/parser authoring guide) into your AI coding assistant" + }, + "type": "library", + "extra": { + "skills": { + "sources": [ + { + "ref": "self.version", + "from": "github", + "package": "phplrt/phplrt" + } + ] + }, + "branch-alias": { + "dev-main": "4.x-dev", + "dev-master": "4.x-dev" + } + }, + "autoload": { + "psr-4": { + "Phplrt\\Contracts\\Lexer\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Kirill Nesmeyanov", + "email": "nesk@xakep.ru" + } + ], + "description": "A set of phplrt lexer abstractions", + "homepage": "https://github.com/phplrt", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "lexer", + "parsing", + "regex", + "standards", + "stream", + "token", + "tokenizer" + ], + "support": { + "issues": "https://github.com/phplrt/phplrt/issues", + "source": "https://github.com/phplrt/lexer-contracts" + }, + "time": "2026-10-15T12:09:22+00:00" + }, + { + "name": "phplrt/parser", + "version": "4.2.4", + "source": { + "type": "git", + "url": "https://github.com/phplrt/parser.git", + "reference": "5ce04660ed44b22c4c595b6a8a180f97ef16c2fe" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phplrt/parser/zipball/5ce04660ed44b22c4c595b6a8a180f97ef16c2fe", + "reference": "5ce04660ed44b22c4c595b6a8a180f97ef16c2fe", + "shasum": "" + }, + "require": { + "ext-json": "*", + "ext-pcre": "*", + "php": "^8.1", + "phplrt/lexer": "^4.2", + "phplrt/parser-contracts": "^4.2", + "phplrt/position": "^4.2", + "phplrt/source-contracts": "^4.2" + }, + "provide": { + "phplrt/parser-contracts-implementation": "^4.0" + }, + "require-dev": { + "phplrt/exception": "^4.2", + "phplrt/parser-builder": "^4.2", + "phplrt/source": "^4.2", + "phpstan/phpstan": "^2.3", + "testo/testo": "^0.10.45" + }, + "suggest": { + "llm/skills": "Delivers the bundled phplrt agent skill (a lexer/parser authoring guide) into your AI coding assistant", + "phplrt/exception": "Renders the syntax errors along with the fragment of the source code they occurred in" + }, + "type": "library", + "extra": { + "skills": { + "sources": [ + { + "ref": "self.version", + "from": "github", + "package": "phplrt/phplrt" + } + ] + }, + "branch-alias": { + "dev-main": "4.x-dev", + "dev-master": "4.x-dev" + } + }, + "autoload": { + "psr-4": { + "Phplrt\\Parser\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Kirill Nesmeyanov", + "email": "nesk@xakep.ru" + } + ], + "description": "The phplrt parser library", + "homepage": "https://github.com/phplrt", + "keywords": [ + "LPeg", + "language", + "lookahead", + "parser", + "peg", + "php", + "tracing" + ], + "support": { + "issues": "https://github.com/phplrt/phplrt/issues", + "source": "https://github.com/phplrt/parser" + }, + "time": "2026-10-16T08:43:49+00:00" + }, + { + "name": "phplrt/parser-builder", + "version": "4.2.4", + "source": { + "type": "git", + "url": "https://github.com/phplrt/parser-builder.git", + "reference": "898c27614ec7a6e52d68a9fb1450281516a91192" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phplrt/parser-builder/zipball/898c27614ec7a6e52d68a9fb1450281516a91192", + "reference": "898c27614ec7a6e52d68a9fb1450281516a91192", + "shasum": "" + }, + "require": { + "php": "^8.1", + "phplrt/exception": "^4.2", + "phplrt/lexer": "^4.2", + "phplrt/lexer-builder": "^4.2", + "phplrt/parser": "^4.2", + "phplrt/source-contracts": "^4.2", + "psr/log": "^3.0" + }, + "require-dev": { + "phpstan/phpstan": "^2.3", + "testo/testo": "^0.10.45" + }, + "suggest": { + "llm/skills": "Delivers the bundled phplrt agent skill (a lexer/parser authoring guide) into your AI coding assistant" + }, + "type": "library", + "extra": { + "skills": { + "sources": [ + { + "ref": "self.version", + "from": "github", + "package": "phplrt/phplrt" + } + ] + }, + "branch-alias": { + "dev-main": "4.x-dev", + "dev-master": "4.x-dev" + } + }, + "autoload": { + "psr-4": { + "Phplrt\\Parser\\Builder\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Kirill Nesmeyanov", + "email": "nesk@xakep.ru" + } + ], + "description": "The phplrt component for assembling a parser", + "homepage": "https://github.com/phplrt", + "keywords": [ + "LPeg", + "build", + "compiler", + "parser", + "peg", + "regex" + ], + "support": { + "issues": "https://github.com/phplrt/phplrt/issues", + "source": "https://github.com/phplrt/parser-builder" + }, + "time": "2026-10-16T08:45:09+00:00" + }, + { + "name": "phplrt/parser-contracts", + "version": "4.2.4", + "source": { + "type": "git", + "url": "https://github.com/phplrt/parser-contracts.git", + "reference": "472a952aaaa4bea475a60a9c876abdee74a4e4bc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phplrt/parser-contracts/zipball/472a952aaaa4bea475a60a9c876abdee74a4e4bc", + "reference": "472a952aaaa4bea475a60a9c876abdee74a4e4bc", + "shasum": "" + }, + "require": { + "php": "^8.1", + "phplrt/lexer-contracts": "^4.2", + "phplrt/source-contracts": "^4.2" + }, + "require-dev": { + "phpstan/phpstan": "^2.3", + "testo/testo": "^0.10.45" + }, + "suggest": { + "llm/skills": "Delivers the bundled phplrt agent skill (a lexer/parser authoring guide) into your AI coding assistant" + }, + "type": "library", + "extra": { + "skills": { + "sources": [ + { + "ref": "self.version", + "from": "github", + "package": "phplrt/phplrt" + } + ] + }, + "branch-alias": { + "dev-main": "4.x-dev", + "dev-master": "4.x-dev" + } + }, + "autoload": { + "psr-4": { + "Phplrt\\Contracts\\Parser\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Kirill Nesmeyanov", + "email": "nesk@xakep.ru" + } + ], + "description": "A set of phplrt parser abstractions", + "homepage": "https://github.com/phplrt", + "keywords": [ + "abstractions", + "analysis", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "language", + "parser", + "php", + "standards", + "syntax" + ], + "support": { + "issues": "https://github.com/phplrt/phplrt/issues", + "source": "https://github.com/phplrt/parser-contracts" + }, + "time": "2026-10-15T12:08:13+00:00" + }, + { + "name": "phplrt/position", + "version": "4.2.4", + "source": { + "type": "git", + "url": "https://github.com/phplrt/position.git", + "reference": "2f4bcfa40967a9c73b7b772d5c3a42ee801aff46" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phplrt/position/zipball/2f4bcfa40967a9c73b7b772d5c3a42ee801aff46", + "reference": "2f4bcfa40967a9c73b7b772d5c3a42ee801aff46", + "shasum": "" + }, + "require": { + "php": "^8.1", + "phplrt/position-contracts": "^4.2", + "phplrt/position-factory-contracts": "^4.2", + "phplrt/source-contracts": "^4.2" + }, + "provide": { + "phplrt/position-contracts-implementation": "^4.0", + "phplrt/position-factory-contracts-implementation": "^4.0" + }, + "require-dev": { + "phplrt/source": "^4.2", + "phpstan/phpstan": "^2.3", + "testo/testo": "^0.10.45" + }, + "suggest": { + "llm/skills": "Delivers the bundled phplrt agent skill (a lexer/parser authoring guide) into your AI coding assistant" + }, + "type": "library", + "extra": { + "skills": { + "sources": [ + { + "ref": "self.version", + "from": "github", + "package": "phplrt/phplrt" + } + ] + }, + "branch-alias": { + "dev-main": "4.x-dev", + "dev-master": "4.x-dev" + } + }, + "autoload": { + "psr-4": { + "Phplrt\\Position\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Kirill Nesmeyanov", + "email": "nesk@xakep.ru" + } + ], + "description": "The phplrt position package", + "homepage": "https://github.com/phplrt", + "keywords": [ + "analyzing", + "column", + "line", + "location", + "offset", + "position", + "source" + ], + "support": { + "issues": "https://github.com/phplrt/phplrt/issues", + "source": "https://github.com/phplrt/position" + }, + "time": "2026-10-12T23:31:53+00:00" + }, + { + "name": "phplrt/position-contracts", + "version": "4.2.4", + "source": { + "type": "git", + "url": "https://github.com/phplrt/position-contracts.git", + "reference": "b63daaea00c7dae74b222a94d97cbc8f2be3a162" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phplrt/position-contracts/zipball/b63daaea00c7dae74b222a94d97cbc8f2be3a162", + "reference": "b63daaea00c7dae74b222a94d97cbc8f2be3a162", + "shasum": "" + }, + "require": { + "php": "^8.1" + }, + "require-dev": { + "phpstan/phpstan": "^2.3", + "testo/testo": "^0.10.45" + }, + "suggest": { + "llm/skills": "Delivers the bundled phplrt agent skill (a lexer/parser authoring guide) into your AI coding assistant" + }, + "type": "library", + "extra": { + "skills": { + "sources": [ + { + "ref": "self.version", + "from": "github", + "package": "phplrt/phplrt" + } + ] + }, + "branch-alias": { + "dev-main": "4.x-dev", + "dev-master": "4.x-dev" + } + }, + "autoload": { + "psr-4": { + "Phplrt\\Contracts\\Position\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Kirill Nesmeyanov", + "email": "nesk@xakep.ru" + } + ], + "description": "A set of phplrt position abstractions", + "homepage": "https://github.com/phplrt", + "keywords": [ + "column", + "line", + "location", + "position", + "source" + ], + "support": { + "issues": "https://github.com/phplrt/phplrt/issues", + "source": "https://github.com/phplrt/position-contracts" + }, + "time": "2026-10-12T22:31:16+00:00" + }, + { + "name": "phplrt/position-factory-contracts", + "version": "4.2.4", + "source": { + "type": "git", + "url": "https://github.com/phplrt/position-factory-contracts.git", + "reference": "cef59452df997b220207781ae4bf1b9ef28fb64c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phplrt/position-factory-contracts/zipball/cef59452df997b220207781ae4bf1b9ef28fb64c", + "reference": "cef59452df997b220207781ae4bf1b9ef28fb64c", + "shasum": "" + }, + "require": { + "php": "^8.1", + "phplrt/position-contracts": "^4.2", + "phplrt/source-contracts": "^4.2" + }, + "require-dev": { + "phpstan/phpstan": "^2.3", + "testo/testo": "^0.10.45" + }, + "suggest": { + "llm/skills": "Delivers the bundled phplrt agent skill (a lexer/parser authoring guide) into your AI coding assistant" + }, + "type": "library", + "extra": { + "skills": { + "sources": [ + { + "ref": "self.version", + "from": "github", + "package": "phplrt/phplrt" + } + ] + }, + "branch-alias": { + "dev-main": "4.x-dev", + "dev-master": "4.x-dev" + } + }, + "autoload": { + "psr-4": { + "Phplrt\\Contracts\\Position\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Kirill Nesmeyanov", + "email": "nesk@xakep.ru" + } + ], + "description": "A set of phplrt position factory abstractions", + "homepage": "https://github.com/phplrt", + "keywords": [ + "column", + "factory", + "line", + "location", + "position", + "source" + ], + "support": { + "issues": "https://github.com/phplrt/phplrt/issues", + "source": "https://github.com/phplrt/position-factory-contracts" + }, + "time": "2026-10-12T23:31:42+00:00" + }, + { + "name": "phplrt/source", + "version": "4.2.4", + "source": { + "type": "git", + "url": "https://github.com/phplrt/source.git", + "reference": "2df423fb5a05fd16f1d8889123a2944a6e806c69" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phplrt/source/zipball/2df423fb5a05fd16f1d8889123a2944a6e806c69", + "reference": "2df423fb5a05fd16f1d8889123a2944a6e806c69", + "shasum": "" + }, + "require": { + "php": "^8.1", + "phplrt/source-contracts": "^4.2", + "phplrt/source-factory-contracts": "^4.2" + }, + "provide": { + "phplrt/source-contracts-implementation": "^4.0", + "phplrt/source-factory-contracts-implementation": "^4.0" + }, + "require-dev": { + "phpstan/phpstan": "^2.3", + "testo/testo": "^0.10.45" + }, + "suggest": { + "llm/skills": "Delivers the bundled phplrt agent skill (a lexer/parser authoring guide) into your AI coding assistant" + }, + "type": "library", + "extra": { + "skills": { + "sources": [ + { + "ref": "self.version", + "from": "github", + "package": "phplrt/phplrt" + } + ] + }, + "branch-alias": { + "dev-main": "4.x-dev", + "dev-master": "4.x-dev" + } + }, + "autoload": { + "files": [ + "src/polyfill.php" + ], + "psr-4": { + "Phplrt\\Source\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Kirill Nesmeyanov", + "email": "nesk@xakep.ru" + } + ], + "description": "The phplrt sources package", + "homepage": "https://github.com/phplrt", + "keywords": [ + "abstraction", + "directory", + "file", + "file system", + "fs", + "reader" + ], + "support": { + "issues": "https://github.com/phplrt/phplrt/issues", + "source": "https://github.com/phplrt/source" + }, + "time": "2026-10-12T23:32:11+00:00" + }, + { + "name": "phplrt/source-contracts", + "version": "4.2.4", + "source": { + "type": "git", + "url": "https://github.com/phplrt/source-contracts.git", + "reference": "6fcd59e10d586bfd4e387aff3d387b00c4080462" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phplrt/source-contracts/zipball/6fcd59e10d586bfd4e387aff3d387b00c4080462", + "reference": "6fcd59e10d586bfd4e387aff3d387b00c4080462", + "shasum": "" + }, + "require": { + "php": "^8.1" + }, + "require-dev": { + "phpstan/phpstan": "^2.3", + "testo/testo": "^0.10.45" + }, + "suggest": { + "llm/skills": "Delivers the bundled phplrt agent skill (a lexer/parser authoring guide) into your AI coding assistant" + }, + "type": "library", + "extra": { + "skills": { + "sources": [ + { + "ref": "self.version", + "from": "github", + "package": "phplrt/phplrt" + } + ] + }, + "branch-alias": { + "dev-main": "4.x-dev", + "dev-master": "4.x-dev" + } + }, + "autoload": { + "psr-4": { + "Phplrt\\Contracts\\Source\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Kirill Nesmeyanov", + "email": "nesk@xakep.ru" + } + ], + "description": "A set of phplrt source abstractions", + "homepage": "https://github.com/phplrt", + "keywords": [ + "disk", + "file", + "io", + "read", + "source" + ], + "support": { + "issues": "https://github.com/phplrt/phplrt/issues", + "source": "https://github.com/phplrt/source-contracts" + }, + "time": "2026-10-12T22:31:34+00:00" + }, + { + "name": "phplrt/source-factory-contracts", + "version": "4.2.4", + "source": { + "type": "git", + "url": "https://github.com/phplrt/source-factory-contracts.git", + "reference": "59a7070f6e520ecc03b9544d88edfa2ea874e9bb" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phplrt/source-factory-contracts/zipball/59a7070f6e520ecc03b9544d88edfa2ea874e9bb", + "reference": "59a7070f6e520ecc03b9544d88edfa2ea874e9bb", + "shasum": "" + }, + "require": { + "php": "^8.1", + "phplrt/source-contracts": "^4.2" + }, + "require-dev": { + "phpstan/phpstan": "^2.3", + "testo/testo": "^0.10.45" + }, + "suggest": { + "llm/skills": "Delivers the bundled phplrt agent skill (a lexer/parser authoring guide) into your AI coding assistant" + }, + "type": "library", + "extra": { + "skills": { + "sources": [ + { + "ref": "self.version", + "from": "github", + "package": "phplrt/phplrt" + } + ] + }, + "branch-alias": { + "dev-main": "4.x-dev", + "dev-master": "4.x-dev" + } + }, + "autoload": { + "psr-4": { + "Phplrt\\Contracts\\Source\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Kirill Nesmeyanov", + "email": "nesk@xakep.ru" + } + ], + "description": "A set of phplrt source factory abstractions", + "homepage": "https://github.com/phplrt", + "keywords": [ + "disk", + "factory", + "file", + "io", + "read", + "source" + ], + "support": { + "issues": "https://github.com/phplrt/phplrt/issues", + "source": "https://github.com/phplrt/source-factory-contracts" + }, + "time": "2026-10-12T23:30:56+00:00" + }, + { + "name": "psr/container", + "version": "2.0.2", + "source": { + "type": "git", + "url": "https://github.com/php-fig/container.git", + "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/container/zipball/c71ecc56dfe541dbd90c5360474fbc405f8d5963", + "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963", + "shasum": "" + }, + "require": { + "php": ">=7.4.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Container\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common Container Interface (PHP FIG PSR-11)", + "homepage": "https://github.com/php-fig/container", + "keywords": [ + "PSR-11", + "container", + "container-interface", + "container-interop", + "psr" + ], + "support": { + "issues": "https://github.com/php-fig/container/issues", + "source": "https://github.com/php-fig/container/tree/2.0.2" + }, + "time": "2021-11-05T16:47:00+00:00" + }, + { + "name": "psr/log", + "version": "3.0.2", + "source": { + "type": "git", + "url": "https://github.com/php-fig/log.git", + "reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/log/zipball/f16e1d5863e37f8d8c2a01719f5b34baa2b714d3", + "reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3", + "shasum": "" + }, + "require": { + "php": ">=8.0.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Log\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for logging libraries", + "homepage": "https://github.com/php-fig/log", + "keywords": [ + "log", + "psr", + "psr-3" + ], + "support": { + "source": "https://github.com/php-fig/log/tree/3.0.2" + }, + "time": "2024-09-11T13:17:53+00:00" + }, + { + "name": "symfony/console", + "version": "v8.1.9", + "source": { + "type": "git", + "url": "https://github.com/symfony/console.git", + "reference": "630e74196a2635ec0f06c12bd3fb47cfedf85157" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/console/zipball/630e74196a2635ec0f06c12bd3fb47cfedf85157", + "reference": "630e74196a2635ec0f06c12bd3fb47cfedf85157", + "shasum": "" + }, + "require": { + "php": ">=8.4.1", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-mbstring": "^1.0", + "symfony/polyfill-php85": "^1.32", + "symfony/service-contracts": "^2.5|^3", + "symfony/string": "^7.4.6|^8.0.6" + }, + "conflict": { + "symfony/dependency-injection": "<8.1", + "symfony/event-dispatcher": "<8.1" + }, + "provide": { + "psr/log-implementation": "1.0|2.0|3.0" + }, + "require-dev": { + "psr/log": "^1|^2|^3", + "symfony/config": "^7.4|^8.0", + "symfony/dependency-injection": "^8.1", + "symfony/event-dispatcher": "^8.1", + "symfony/filesystem": "^7.4|^8.0", + "symfony/http-foundation": "^7.4|^8.0", + "symfony/http-kernel": "^7.4|^8.0", + "symfony/lock": "^7.4|^8.0", + "symfony/messenger": "^7.4|^8.0", + "symfony/mime": "^7.4|^8.0", + "symfony/process": "^7.4|^8.0", + "symfony/stopwatch": "^7.4|^8.0", + "symfony/uid": "^7.4|^8.0", + "symfony/validator": "^7.4|^8.0", + "symfony/var-dumper": "^7.4|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Console\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Eases the creation of beautiful and testable command line interfaces", + "homepage": "https://symfony.com", + "keywords": [ + "cli", + "command-line", + "console", + "terminal" + ], + "support": { + "source": "https://github.com/symfony/console/tree/v8.1.9" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-10-12T19:11:16+00:00" + }, + { + "name": "symfony/deprecation-contracts", + "version": "v3.7.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/deprecation-contracts.git", + "reference": "f3202fa1b5097b0af062dc978b32ecf63404e31d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/f3202fa1b5097b0af062dc978b32ecf63404e31d", + "reference": "f3202fa1b5097b0af062dc978b32ecf63404e31d", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.7-dev" + } + }, + "autoload": { + "files": [ + "function.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "A generic function and convention to trigger deprecation notices", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/deprecation-contracts/tree/v3.7.1" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-06-05T06:23:12+00:00" + }, + { + "name": "symfony/polyfill-ctype", + "version": "v1.37.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-ctype.git", + "reference": "141046a8f9477948ff284fa65be2095baafb94f2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/141046a8f9477948ff284fa65be2095baafb94f2", + "reference": "141046a8f9477948ff284fa65be2095baafb94f2", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "provide": { + "ext-ctype": "*" + }, + "suggest": { + "ext-ctype": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Ctype\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Gert de Pagter", + "email": "BackEndTea@gmail.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for ctype functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "ctype", + "polyfill", + "portable" + ], + "support": { + "source": "https://github.com/symfony/polyfill-ctype/tree/v1.37.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-04-10T16:19:22+00:00" + }, + { + "name": "symfony/polyfill-intl-grapheme", + "version": "v1.43.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-grapheme.git", + "reference": "5cac8f4db97208823aa353f59a01faad2da52c75" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/5cac8f4db97208823aa353f59a01faad2da52c75", + "reference": "5cac8f4db97208823aa353f59a01faad2da52c75", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "suggest": { + "ext-intl": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Intl\\Grapheme\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for intl's grapheme_* functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "grapheme", + "intl", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.43.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-09-25T15:46:47+00:00" + }, + { + "name": "symfony/polyfill-intl-normalizer", + "version": "v1.43.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-normalizer.git", + "reference": "ebd57ccc7edb93d632e184efbe7693d2517bab3b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/ebd57ccc7edb93d632e184efbe7693d2517bab3b", + "reference": "ebd57ccc7edb93d632e184efbe7693d2517bab3b", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "suggest": { + "ext-intl": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Intl\\Normalizer\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for intl's Normalizer class and related functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "intl", + "normalizer", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.43.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-09-23T22:02:09+00:00" + }, + { + "name": "symfony/polyfill-mbstring", + "version": "v1.43.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-mbstring.git", + "reference": "4d5d331bf731e0aedbc6b2f5e61135b7b7996a72" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/4d5d331bf731e0aedbc6b2f5e61135b7b7996a72", + "reference": "4d5d331bf731e0aedbc6b2f5e61135b7b7996a72", + "shasum": "" + }, + "require": { + "ext-iconv": "*", + "php": ">=7.2" + }, + "provide": { + "ext-mbstring": "*" + }, + "suggest": { + "ext-mbstring": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Mbstring\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for the Mbstring extension", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "mbstring", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.43.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-09-24T15:19:04+00:00" + }, + { + "name": "symfony/polyfill-php85", + "version": "v1.43.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php85.git", + "reference": "86a42cbcc07adc331481a5ab310a4024bfc227f6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php85/zipball/86a42cbcc07adc331481a5ab310a4024bfc227f6", + "reference": "86a42cbcc07adc331481a5ab310a4024bfc227f6", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php85\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 8.5+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php85/tree/v1.43.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-09-25T15:46:47+00:00" + }, + { + "name": "symfony/service-contracts", + "version": "v3.7.3", + "source": { + "type": "git", + "url": "https://github.com/symfony/service-contracts.git", + "reference": "15e6a07ec2a2c75ceb1b21dd98105ee8456d2257" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/service-contracts/zipball/15e6a07ec2a2c75ceb1b21dd98105ee8456d2257", + "reference": "15e6a07ec2a2c75ceb1b21dd98105ee8456d2257", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "psr/container": "^1.1|^2.0", + "symfony/deprecation-contracts": "^2.5|^3" + }, + "conflict": { + "ext-psr": "<1.1|>=2" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.7-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Contracts\\Service\\": "" + }, + "exclude-from-classmap": [ + "/Test/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Generic abstractions related to writing services", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "support": { + "source": "https://github.com/symfony/service-contracts/tree/v3.7.3" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-07-27T15:39:01+00:00" + }, + { + "name": "symfony/string", + "version": "v8.1.9", + "source": { + "type": "git", + "url": "https://github.com/symfony/string.git", + "reference": "07fb208558f941eab50ec49c89c2a6a93fb11f01" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/string/zipball/07fb208558f941eab50ec49c89c2a6a93fb11f01", + "reference": "07fb208558f941eab50ec49c89c2a6a93fb11f01", + "shasum": "" + }, + "require": { + "php": ">=8.4.1", + "symfony/polyfill-ctype": "^1.8", + "symfony/polyfill-intl-grapheme": "^1.33", + "symfony/polyfill-intl-normalizer": "^1.0", + "symfony/polyfill-mbstring": "^1.0" + }, + "conflict": { + "symfony/translation-contracts": "<2.5" + }, + "require-dev": { + "symfony/emoji": "^7.4|^8.0", + "symfony/http-client": "^7.4|^8.0", + "symfony/intl": "^7.4|^8.0", + "symfony/translation-contracts": "^2.5|^3.0", + "symfony/var-exporter": "^7.4|^8.0" + }, + "type": "library", + "autoload": { + "files": [ + "Resources/functions.php" + ], + "psr-4": { + "Symfony\\Component\\String\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides an object-oriented API to strings and deals with bytes, UTF-8 code points and grapheme clusters in a unified way", + "homepage": "https://symfony.com", + "keywords": [ + "grapheme", + "i18n", + "string", + "unicode", + "utf-8", + "utf8" + ], + "support": { + "source": "https://github.com/symfony/string/tree/v8.1.9" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-10-03T15:16:03+00:00" + }, + { + "name": "twig/twig", + "version": "v3.30.0", + "source": { + "type": "git", + "url": "https://github.com/twigphp/Twig.git", + "reference": "8c737079b726af72ff8ef3c595be9f6a810ea1ef" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/twigphp/Twig/zipball/8c737079b726af72ff8ef3c595be9f6a810ea1ef", + "reference": "8c737079b726af72ff8ef3c595be9f6a810ea1ef", + "shasum": "" + }, + "require": { + "php": ">=8.1.0", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-ctype": "^1.8", + "symfony/polyfill-mbstring": "^1.3" + }, + "require-dev": { + "php-cs-fixer/shim": "^3.0@stable", + "phpstan/phpstan": "^2.0@stable", + "psr/container": "^1.0|^2.0", + "symfony/phpunit-bridge": "^5.4.9|^6.4|^7.0" + }, + "type": "library", + "autoload": { + "files": [ + "src/Resources/core.php", + "src/Resources/debug.php", + "src/Resources/escaper.php", + "src/Resources/string_loader.php" + ], + "psr-4": { + "Twig\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com", + "homepage": "http://fabien.potencier.org", + "role": "Lead Developer" + }, + { + "name": "Twig Team", + "role": "Contributors" + }, + { + "name": "Armin Ronacher", + "email": "armin.ronacher@active-4.com", + "role": "Project Founder" + } + ], + "description": "Twig, the flexible, fast, and secure template language for PHP", + "homepage": "https://twig.symfony.com", + "keywords": [ + "templating" + ], + "support": { + "issues": "https://github.com/twigphp/Twig/issues", + "source": "https://github.com/twigphp/Twig/tree/v3.30.0" + }, + "funding": [ + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/twig/twig", + "type": "tidelift" + } + ], + "time": "2026-09-25T13:20:01+00:00" + } + ], + "packages-dev": [], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": {}, + "prefer-stable": false, + "prefer-lowest": false, + "platform": { + "php": "^8.4" + }, + "platform-dev": {}, + "plugin-api-version": "2.9.0" +} diff --git a/build/regex-parser/generate.php b/build/regex-parser/generate.php new file mode 100644 index 00000000000..0f6dca9c2f5 --- /dev/null +++ b/build/regex-parser/generate.php @@ -0,0 +1,54 @@ +load(FileSource::createFromPathname(__DIR__ . '/RegexGrammar.pp3')) + ->generate() + ->withNamespaceName('PHPStan\Type\Regex\Parser') + ->withClassName('RegexParser') + ->withClassImport('Phplrt\Contracts\Lexer\TokenInterface') + ->withClassModifier(ClassModifier::Final) + ->withTargetPhpVersion(TargetPhpVersion::Php81) + ->withoutContractsPreloading(); + +// PHPStan's source is downgraded to PHP 7.4 for the PHAR, +// and the downgrader does not handle first-class callable syntax. +$code = preg_replace( + '~\bself::(reduce\w+)\(\.\.\.\)~', + '\Closure::fromCallable([self::class, \'$1\'])', + $code, + -1, + $count, +); +if ($code === null || $count === 0) { + fwrite(STDERR, "Could not replace first-class callables in the generated parser.\n"); + exit(1); +} + +// ParserInterface::parse() declares @throws \Throwable, narrow it down +// to what this parser and its reducers actually throw. +$code = str_replace( + " public function parse(\\Phplrt\\Contracts\\Source\\ReadableInterface \$source): mixed\n", + " /**\n * @throws \\Phplrt\\Contracts\\Parser\\Exception\\ParserExceptionInterface\n * @throws RegexSyntaxException\n */\n public function parse(\\Phplrt\\Contracts\\Source\\ReadableInterface \$source): mixed\n", + $code, + $count, +); +if ($count !== 1) { + fwrite(STDERR, "Could not add @throws to the generated parse() method.\n"); + exit(1); +} + +file_put_contents($output, $code); +echo sprintf("Generated %s\n", $output); diff --git a/compiler/build/scoper-namespaces.php b/compiler/build/scoper-namespaces.php index e147c285245..7ddfe1f8e2e 100644 --- a/compiler/build/scoper-namespaces.php +++ b/compiler/build/scoper-namespaces.php @@ -13,7 +13,6 @@ 'PHPStanTurbo', 'PHPUnit', 'PhpParser', - 'Hoa', 'Symfony\Polyfill\Php80', 'Symfony\Polyfill\Php81', 'Symfony\Polyfill\Php83', diff --git a/compiler/build/scoper.inc.php b/compiler/build/scoper.inc.php index ac20519cee6..028c6b31363 100644 --- a/compiler/build/scoper.inc.php +++ b/compiler/build/scoper.inc.php @@ -5,7 +5,6 @@ $namespaces = require __DIR__ . '/scoper-namespaces.php'; $stubs = [ - '../../vendor/hoa/consistency/Prelude.php', '../../vendor/composer/InstalledVersions.php', '../../vendor/composer/installed.php', ]; @@ -106,10 +105,6 @@ function (string $filePath, string $prefix, string $content): string { return $class; } - if (strpos($class, 'Hoa\\') === 0) { - return $class; - } - if (strpos($class, '@') === 0) { return $class; } diff --git a/composer.json b/composer.json index 0d243c8cf8a..1f70f7b5302 100644 --- a/composer.json +++ b/composer.json @@ -22,9 +22,6 @@ "composer/semver": "^3.4", "fidry/cpu-core-counter": "^1.4", "guzzlehttp/guzzle": "^7.9", - "hoa/compiler": "3.17.08.08", - "hoa/exception": "^1.0", - "hoa/file": "1.17.07.11", "jetbrains/phpstorm-stubs": "dev-master#e4f5f6c3de39f3bab3e9f3fca4b8cdb8b061e681", "nette/bootstrap": "^3.0", "nette/di": "^3.1.10", @@ -37,6 +34,7 @@ "ondrejmirtes/better-reflection": "6.75.0.2", "ondrejmirtes/composer-attribute-collector": "^1.2.0", "ondrejmirtes/php-merge": "^4.1", + "ondrejmirtes/phplrt-runtime": "4.2.4.0", "phpstan/php-8-stubs": "0.4.43", "phpstan/phpdoc-parser": "2.3.6", "react/child-process": "^0.7", @@ -102,36 +100,6 @@ "composer/ca-bundle": [ "patches/cloudflare-ca.patch" ], - "hoa/exception": [ - "patches/Idle.patch" - ], - "hoa/file": [ - "patches/File.patch", - "patches/Read.patch" - ], - "hoa/iterator": [ - "patches/Buffer.patch", - "patches/Lookahead.patch" - ], - "hoa/compiler": [ - "patches/HoaException.patch", - "patches/HoaParser.patch", - "patches/Invocation.patch", - "patches/Rule.patch", - "patches/Lexer.patch", - "patches/TreeNode.patch" - ], - "hoa/consistency": [ - "patches/Consistency.patch", - "patches/Prelude.patch" - ], - "hoa/protocol": [ - "patches/Node.patch", - "patches/Wrapper.patch" - ], - "hoa/stream": [ - "patches/Stream.patch" - ], "jetbrains/phpstorm-stubs": [ "patches/ReflectionProperty.patch", "patches/SessionHandler.patch", diff --git a/composer.lock b/composer.lock index 13b78f6c028..92c5ee02b5d 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "f8bb376c28c2f35a86066ab8fc280c99", + "content-hash": "f9455710ed4217e15945f9a10e3eccaa", "packages": [ { "name": "clue/ndjson-react", @@ -587,906 +587,6 @@ ], "time": "2026-07-16T22:23:49+00:00" }, - { - "name": "hoa/compiler", - "version": "3.17.08.08", - "source": { - "type": "git", - "url": "https://github.com/hoaproject/Compiler.git", - "reference": "aa09caf0bf28adae6654ca6ee415ee2f522672de" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/hoaproject/Compiler/zipball/aa09caf0bf28adae6654ca6ee415ee2f522672de", - "reference": "aa09caf0bf28adae6654ca6ee415ee2f522672de", - "shasum": "" - }, - "require": { - "hoa/consistency": "~1.0", - "hoa/exception": "~1.0", - "hoa/file": "~1.0", - "hoa/iterator": "~2.0", - "hoa/math": "~1.0", - "hoa/protocol": "~1.0", - "hoa/regex": "~1.0", - "hoa/visitor": "~2.0" - }, - "require-dev": { - "hoa/json": "~2.0", - "hoa/test": "~2.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "3.x-dev" - } - }, - "autoload": { - "psr-4": { - "Hoa\\Compiler\\": "." - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Ivan Enderlin", - "email": "ivan.enderlin@hoa-project.net" - }, - { - "name": "Hoa community", - "homepage": "https://hoa-project.net/" - } - ], - "description": "The Hoa\\Compiler library.", - "homepage": "https://hoa-project.net/", - "keywords": [ - "algebraic", - "ast", - "compiler", - "context-free", - "coverage", - "exhaustive", - "grammar", - "isotropic", - "language", - "lexer", - "library", - "ll1", - "llk", - "parser", - "pp", - "random", - "regular", - "rule", - "sampler", - "syntax", - "token", - "trace", - "uniform" - ], - "support": { - "docs": "https://central.hoa-project.net/Documentation/Library/Compiler", - "email": "support@hoa-project.net", - "forum": "https://users.hoa-project.net/", - "irc": "irc://chat.freenode.net/hoaproject", - "issues": "https://github.com/hoaproject/Compiler/issues", - "source": "https://central.hoa-project.net/Resource/Library/Compiler" - }, - "abandoned": true, - "time": "2017-08-08T07:44:07+00:00" - }, - { - "name": "hoa/consistency", - "version": "1.17.05.02", - "source": { - "type": "git", - "url": "https://github.com/hoaproject/Consistency.git", - "reference": "fd7d0adc82410507f332516faf655b6ed22e4c2f" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/hoaproject/Consistency/zipball/fd7d0adc82410507f332516faf655b6ed22e4c2f", - "reference": "fd7d0adc82410507f332516faf655b6ed22e4c2f", - "shasum": "" - }, - "require": { - "hoa/exception": "~1.0", - "php": ">=5.5.0" - }, - "require-dev": { - "hoa/stream": "~1.0", - "hoa/test": "~2.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.x-dev" - } - }, - "autoload": { - "files": [ - "Prelude.php" - ], - "psr-4": { - "Hoa\\Consistency\\": "." - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Ivan Enderlin", - "email": "ivan.enderlin@hoa-project.net" - }, - { - "name": "Hoa community", - "homepage": "https://hoa-project.net/" - } - ], - "description": "The Hoa\\Consistency library.", - "homepage": "https://hoa-project.net/", - "keywords": [ - "autoloader", - "callable", - "consistency", - "entity", - "flex", - "keyword", - "library" - ], - "support": { - "docs": "https://central.hoa-project.net/Documentation/Library/Consistency", - "email": "support@hoa-project.net", - "forum": "https://users.hoa-project.net/", - "irc": "irc://chat.freenode.net/hoaproject", - "issues": "https://github.com/hoaproject/Consistency/issues", - "source": "https://central.hoa-project.net/Resource/Library/Consistency" - }, - "abandoned": true, - "time": "2017-05-02T12:18:12+00:00" - }, - { - "name": "hoa/event", - "version": "1.17.01.13", - "source": { - "type": "git", - "url": "https://github.com/hoaproject/Event.git", - "reference": "6c0060dced212ffa3af0e34bb46624f990b29c54" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/hoaproject/Event/zipball/6c0060dced212ffa3af0e34bb46624f990b29c54", - "reference": "6c0060dced212ffa3af0e34bb46624f990b29c54", - "shasum": "" - }, - "require": { - "hoa/consistency": "~1.0", - "hoa/exception": "~1.0" - }, - "require-dev": { - "hoa/test": "~2.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.x-dev" - } - }, - "autoload": { - "psr-4": { - "Hoa\\Event\\": "." - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Ivan Enderlin", - "email": "ivan.enderlin@hoa-project.net" - }, - { - "name": "Hoa community", - "homepage": "https://hoa-project.net/" - } - ], - "description": "The Hoa\\Event library.", - "homepage": "https://hoa-project.net/", - "keywords": [ - "event", - "library", - "listener", - "observer" - ], - "support": { - "docs": "https://central.hoa-project.net/Documentation/Library/Event", - "email": "support@hoa-project.net", - "forum": "https://users.hoa-project.net/", - "irc": "irc://chat.freenode.net/hoaproject", - "issues": "https://github.com/hoaproject/Event/issues", - "source": "https://central.hoa-project.net/Resource/Library/Event" - }, - "abandoned": true, - "time": "2017-01-13T15:30:50+00:00" - }, - { - "name": "hoa/exception", - "version": "1.17.01.16", - "source": { - "type": "git", - "url": "https://github.com/hoaproject/Exception.git", - "reference": "091727d46420a3d7468ef0595651488bfc3a458f" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/hoaproject/Exception/zipball/091727d46420a3d7468ef0595651488bfc3a458f", - "reference": "091727d46420a3d7468ef0595651488bfc3a458f", - "shasum": "" - }, - "require": { - "hoa/consistency": "~1.0", - "hoa/event": "~1.0" - }, - "require-dev": { - "hoa/test": "~2.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.x-dev" - } - }, - "autoload": { - "psr-4": { - "Hoa\\Exception\\": "." - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Ivan Enderlin", - "email": "ivan.enderlin@hoa-project.net" - }, - { - "name": "Hoa community", - "homepage": "https://hoa-project.net/" - } - ], - "description": "The Hoa\\Exception library.", - "homepage": "https://hoa-project.net/", - "keywords": [ - "exception", - "library" - ], - "support": { - "docs": "https://central.hoa-project.net/Documentation/Library/Exception", - "email": "support@hoa-project.net", - "forum": "https://users.hoa-project.net/", - "irc": "irc://chat.freenode.net/hoaproject", - "issues": "https://github.com/hoaproject/Exception/issues", - "source": "https://central.hoa-project.net/Resource/Library/Exception" - }, - "abandoned": true, - "time": "2017-01-16T07:53:27+00:00" - }, - { - "name": "hoa/file", - "version": "1.17.07.11", - "source": { - "type": "git", - "url": "https://github.com/hoaproject/File.git", - "reference": "35cb979b779bc54918d2f9a4e02ed6c7a1fa67ca" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/hoaproject/File/zipball/35cb979b779bc54918d2f9a4e02ed6c7a1fa67ca", - "reference": "35cb979b779bc54918d2f9a4e02ed6c7a1fa67ca", - "shasum": "" - }, - "require": { - "hoa/consistency": "~1.0", - "hoa/event": "~1.0", - "hoa/exception": "~1.0", - "hoa/iterator": "~2.0", - "hoa/stream": "~1.0" - }, - "require-dev": { - "hoa/test": "~2.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.x-dev" - } - }, - "autoload": { - "psr-4": { - "Hoa\\File\\": "." - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Ivan Enderlin", - "email": "ivan.enderlin@hoa-project.net" - }, - { - "name": "Hoa community", - "homepage": "https://hoa-project.net/" - } - ], - "description": "The Hoa\\File library.", - "homepage": "https://hoa-project.net/", - "keywords": [ - "Socket", - "directory", - "file", - "finder", - "library", - "link", - "temporary" - ], - "support": { - "docs": "https://central.hoa-project.net/Documentation/Library/File", - "email": "support@hoa-project.net", - "forum": "https://users.hoa-project.net/", - "irc": "irc://chat.freenode.net/hoaproject", - "issues": "https://github.com/hoaproject/File/issues", - "source": "https://central.hoa-project.net/Resource/Library/File" - }, - "abandoned": true, - "time": "2017-07-11T07:42:15+00:00" - }, - { - "name": "hoa/iterator", - "version": "2.17.01.10", - "source": { - "type": "git", - "url": "https://github.com/hoaproject/Iterator.git", - "reference": "d1120ba09cb4ccd049c86d10058ab94af245f0cc" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/hoaproject/Iterator/zipball/d1120ba09cb4ccd049c86d10058ab94af245f0cc", - "reference": "d1120ba09cb4ccd049c86d10058ab94af245f0cc", - "shasum": "" - }, - "require": { - "hoa/consistency": "~1.0", - "hoa/exception": "~1.0" - }, - "require-dev": { - "hoa/test": "~2.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.x-dev" - } - }, - "autoload": { - "psr-4": { - "Hoa\\Iterator\\": "." - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Ivan Enderlin", - "email": "ivan.enderlin@hoa-project.net" - }, - { - "name": "Hoa community", - "homepage": "https://hoa-project.net/" - } - ], - "description": "The Hoa\\Iterator library.", - "homepage": "https://hoa-project.net/", - "keywords": [ - "iterator", - "library" - ], - "support": { - "docs": "https://central.hoa-project.net/Documentation/Library/Iterator", - "email": "support@hoa-project.net", - "forum": "https://users.hoa-project.net/", - "irc": "irc://chat.freenode.net/hoaproject", - "issues": "https://github.com/hoaproject/Iterator/issues", - "source": "https://central.hoa-project.net/Resource/Library/Iterator" - }, - "abandoned": true, - "time": "2017-01-10T10:34:47+00:00" - }, - { - "name": "hoa/math", - "version": "1.17.05.16", - "source": { - "type": "git", - "url": "https://github.com/hoaproject/Math.git", - "reference": "7150785d30f5d565704912116a462e9f5bc83a0c" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/hoaproject/Math/zipball/7150785d30f5d565704912116a462e9f5bc83a0c", - "reference": "7150785d30f5d565704912116a462e9f5bc83a0c", - "shasum": "" - }, - "require": { - "hoa/compiler": "~3.0", - "hoa/consistency": "~1.0", - "hoa/exception": "~1.0", - "hoa/iterator": "~2.0", - "hoa/protocol": "~1.0", - "hoa/zformat": "~1.0" - }, - "require-dev": { - "hoa/test": "~2.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.x-dev" - } - }, - "autoload": { - "psr-4": { - "Hoa\\Math\\": "." - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Ivan Enderlin", - "email": "ivan.enderlin@hoa-project.net" - }, - { - "name": "Hoa community", - "homepage": "https://hoa-project.net/" - } - ], - "description": "The Hoa\\Math library.", - "homepage": "https://hoa-project.net/", - "keywords": [ - "arrangement", - "combination", - "combinatorics", - "counting", - "library", - "math", - "permutation", - "sampler", - "set" - ], - "support": { - "docs": "https://central.hoa-project.net/Documentation/Library/Math", - "email": "support@hoa-project.net", - "forum": "https://users.hoa-project.net/", - "irc": "irc://chat.freenode.net/hoaproject", - "issues": "https://github.com/hoaproject/Math/issues", - "source": "https://central.hoa-project.net/Resource/Library/Math" - }, - "abandoned": true, - "time": "2017-05-16T08:02:17+00:00" - }, - { - "name": "hoa/protocol", - "version": "1.17.01.14", - "source": { - "type": "git", - "url": "https://github.com/hoaproject/Protocol.git", - "reference": "5c2cf972151c45f373230da170ea015deecf19e2" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/hoaproject/Protocol/zipball/5c2cf972151c45f373230da170ea015deecf19e2", - "reference": "5c2cf972151c45f373230da170ea015deecf19e2", - "shasum": "" - }, - "require": { - "hoa/consistency": "~1.0", - "hoa/exception": "~1.0" - }, - "require-dev": { - "hoa/test": "~2.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.x-dev" - } - }, - "autoload": { - "files": [ - "Wrapper.php" - ], - "psr-4": { - "Hoa\\Protocol\\": "." - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Ivan Enderlin", - "email": "ivan.enderlin@hoa-project.net" - }, - { - "name": "Hoa community", - "homepage": "https://hoa-project.net/" - } - ], - "description": "The Hoa\\Protocol library.", - "homepage": "https://hoa-project.net/", - "keywords": [ - "library", - "protocol", - "resource", - "stream", - "wrapper" - ], - "support": { - "docs": "https://central.hoa-project.net/Documentation/Library/Protocol", - "email": "support@hoa-project.net", - "forum": "https://users.hoa-project.net/", - "irc": "irc://chat.freenode.net/hoaproject", - "issues": "https://github.com/hoaproject/Protocol/issues", - "source": "https://central.hoa-project.net/Resource/Library/Protocol" - }, - "abandoned": true, - "time": "2017-01-14T12:26:10+00:00" - }, - { - "name": "hoa/regex", - "version": "1.17.01.13", - "source": { - "type": "git", - "url": "https://github.com/hoaproject/Regex.git", - "reference": "7e263a61b6fb45c1d03d8e5ef77668518abd5bec" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/hoaproject/Regex/zipball/7e263a61b6fb45c1d03d8e5ef77668518abd5bec", - "reference": "7e263a61b6fb45c1d03d8e5ef77668518abd5bec", - "shasum": "" - }, - "require": { - "hoa/consistency": "~1.0", - "hoa/exception": "~1.0", - "hoa/math": "~1.0", - "hoa/protocol": "~1.0", - "hoa/ustring": "~4.0", - "hoa/visitor": "~2.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.x-dev" - } - }, - "autoload": { - "psr-4": { - "Hoa\\Regex\\": "." - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Ivan Enderlin", - "email": "ivan.enderlin@hoa-project.net" - }, - { - "name": "Hoa community", - "homepage": "https://hoa-project.net/" - } - ], - "description": "The Hoa\\Regex library.", - "homepage": "https://hoa-project.net/", - "keywords": [ - "compiler", - "library", - "regex" - ], - "support": { - "docs": "https://central.hoa-project.net/Documentation/Library/Regex", - "email": "support@hoa-project.net", - "forum": "https://users.hoa-project.net/", - "irc": "irc://chat.freenode.net/hoaproject", - "issues": "https://github.com/hoaproject/Regex/issues", - "source": "https://central.hoa-project.net/Resource/Library/Regex" - }, - "abandoned": true, - "time": "2017-01-13T16:10:24+00:00" - }, - { - "name": "hoa/stream", - "version": "1.17.02.21", - "source": { - "type": "git", - "url": "https://github.com/hoaproject/Stream.git", - "reference": "3293cfffca2de10525df51436adf88a559151d82" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/hoaproject/Stream/zipball/3293cfffca2de10525df51436adf88a559151d82", - "reference": "3293cfffca2de10525df51436adf88a559151d82", - "shasum": "" - }, - "require": { - "hoa/consistency": "~1.0", - "hoa/event": "~1.0", - "hoa/exception": "~1.0", - "hoa/protocol": "~1.0" - }, - "require-dev": { - "hoa/test": "~2.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.x-dev" - } - }, - "autoload": { - "psr-4": { - "Hoa\\Stream\\": "." - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Ivan Enderlin", - "email": "ivan.enderlin@hoa-project.net" - }, - { - "name": "Hoa community", - "homepage": "https://hoa-project.net/" - } - ], - "description": "The Hoa\\Stream library.", - "homepage": "https://hoa-project.net/", - "keywords": [ - "Context", - "bucket", - "composite", - "filter", - "in", - "library", - "out", - "protocol", - "stream", - "wrapper" - ], - "support": { - "docs": "https://central.hoa-project.net/Documentation/Library/Stream", - "email": "support@hoa-project.net", - "forum": "https://users.hoa-project.net/", - "irc": "irc://chat.freenode.net/hoaproject", - "issues": "https://github.com/hoaproject/Stream/issues", - "source": "https://central.hoa-project.net/Resource/Library/Stream" - }, - "abandoned": true, - "time": "2017-02-21T16:01:06+00:00" - }, - { - "name": "hoa/ustring", - "version": "4.17.01.16", - "source": { - "type": "git", - "url": "https://github.com/hoaproject/Ustring.git", - "reference": "e6326e2739178799b1fe3fdd92029f9517fa17a0" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/hoaproject/Ustring/zipball/e6326e2739178799b1fe3fdd92029f9517fa17a0", - "reference": "e6326e2739178799b1fe3fdd92029f9517fa17a0", - "shasum": "" - }, - "require": { - "hoa/consistency": "~1.0", - "hoa/exception": "~1.0" - }, - "require-dev": { - "hoa/test": "~2.0" - }, - "suggest": { - "ext-iconv": "ext/iconv must be present (or a third implementation) to use Hoa\\Ustring::transcode().", - "ext-intl": "To get a better Hoa\\Ustring::toAscii() and Hoa\\Ustring::compareTo()." - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "4.x-dev" - } - }, - "autoload": { - "psr-4": { - "Hoa\\Ustring\\": "." - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Ivan Enderlin", - "email": "ivan.enderlin@hoa-project.net" - }, - { - "name": "Hoa community", - "homepage": "https://hoa-project.net/" - } - ], - "description": "The Hoa\\Ustring library.", - "homepage": "https://hoa-project.net/", - "keywords": [ - "library", - "search", - "string", - "unicode" - ], - "support": { - "docs": "https://central.hoa-project.net/Documentation/Library/Ustring", - "email": "support@hoa-project.net", - "forum": "https://users.hoa-project.net/", - "irc": "irc://chat.freenode.net/hoaproject", - "issues": "https://github.com/hoaproject/Ustring/issues", - "source": "https://central.hoa-project.net/Resource/Library/Ustring" - }, - "abandoned": true, - "time": "2017-01-16T07:08:25+00:00" - }, - { - "name": "hoa/visitor", - "version": "2.17.01.16", - "source": { - "type": "git", - "url": "https://github.com/hoaproject/Visitor.git", - "reference": "c18fe1cbac98ae449e0d56e87469103ba08f224a" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/hoaproject/Visitor/zipball/c18fe1cbac98ae449e0d56e87469103ba08f224a", - "reference": "c18fe1cbac98ae449e0d56e87469103ba08f224a", - "shasum": "" - }, - "require": { - "hoa/consistency": "~1.0" - }, - "require-dev": { - "hoa/test": "~2.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.x-dev" - } - }, - "autoload": { - "psr-4": { - "Hoa\\Visitor\\": "." - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Ivan Enderlin", - "email": "ivan.enderlin@hoa-project.net" - }, - { - "name": "Hoa community", - "homepage": "https://hoa-project.net/" - } - ], - "description": "The Hoa\\Visitor library.", - "homepage": "https://hoa-project.net/", - "keywords": [ - "library", - "structure", - "visit", - "visitor" - ], - "support": { - "docs": "https://central.hoa-project.net/Documentation/Library/Visitor", - "email": "support@hoa-project.net", - "forum": "https://users.hoa-project.net/", - "irc": "irc://chat.freenode.net/hoaproject", - "issues": "https://github.com/hoaproject/Visitor/issues", - "source": "https://central.hoa-project.net/Resource/Library/Visitor" - }, - "abandoned": true, - "time": "2017-01-16T07:02:03+00:00" - }, - { - "name": "hoa/zformat", - "version": "1.17.01.10", - "source": { - "type": "git", - "url": "https://github.com/hoaproject/Zformat.git", - "reference": "522c381a2a075d4b9dbb42eb4592dd09520e4ac2" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/hoaproject/Zformat/zipball/522c381a2a075d4b9dbb42eb4592dd09520e4ac2", - "reference": "522c381a2a075d4b9dbb42eb4592dd09520e4ac2", - "shasum": "" - }, - "require": { - "hoa/consistency": "~1.0", - "hoa/exception": "~1.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.x-dev" - } - }, - "autoload": { - "psr-4": { - "Hoa\\Zformat\\": "." - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Ivan Enderlin", - "email": "ivan.enderlin@hoa-project.net" - }, - { - "name": "Hoa community", - "homepage": "https://hoa-project.net/" - } - ], - "description": "The Hoa\\Zformat library.", - "homepage": "https://hoa-project.net/", - "keywords": [ - "library", - "parameter", - "zformat" - ], - "support": { - "docs": "https://central.hoa-project.net/Documentation/Library/Zformat", - "email": "support@hoa-project.net", - "forum": "https://users.hoa-project.net/", - "irc": "irc://chat.freenode.net/hoaproject", - "issues": "https://github.com/hoaproject/Zformat/issues", - "source": "https://central.hoa-project.net/Resource/Library/Zformat" - }, - "abandoned": true, - "time": "2017-01-10T10:39:54+00:00" - }, { "name": "jetbrains/phpstorm-stubs", "version": "dev-master", @@ -2436,6 +1536,76 @@ }, "time": "2025-12-09T08:32:19+00:00" }, + { + "name": "ondrejmirtes/phplrt-runtime", + "version": "4.2.4.0", + "source": { + "type": "git", + "url": "https://github.com/ondrejmirtes/phplrt-runtime.git", + "reference": "cd7a818c306ef237d47723c2b784e72c0090073f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ondrejmirtes/phplrt-runtime/zipball/cd7a818c306ef237d47723c2b784e72c0090073f", + "reference": "cd7a818c306ef237d47723c2b784e72c0090073f", + "shasum": "" + }, + "require": { + "ext-json": "*", + "ext-pcre": "*", + "php": "^7.4 || ^8.0", + "symfony/polyfill-php80": "^1.23", + "symfony/polyfill-php81": "^1.27" + }, + "replace": { + "phplrt/lexer": "self.version", + "phplrt/lexer-contracts": "self.version", + "phplrt/parser": "self.version", + "phplrt/parser-contracts": "self.version", + "phplrt/position": "self.version", + "phplrt/position-contracts": "self.version", + "phplrt/position-factory-contracts": "self.version", + "phplrt/runtime": "self.version", + "phplrt/source": "self.version", + "phplrt/source-contracts": "self.version", + "phplrt/source-factory-contracts": "self.version" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "4.2.x-dev" + } + }, + "autoload": { + "psr-4": { + "Phplrt\\Lexer\\": "src/Lexer", + "Phplrt\\Parser\\": "src/Parser", + "Phplrt\\Source\\": "src/Source", + "Phplrt\\Position\\": "src/Position", + "Phplrt\\Contracts\\Lexer\\": "src/Contracts/Lexer", + "Phplrt\\Contracts\\Parser\\": "src/Contracts/Parser", + "Phplrt\\Contracts\\Source\\": "src/Contracts/Source", + "Phplrt\\Contracts\\Position\\": "src/Contracts/Position" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Kirill Nesmeyanov", + "email": "nesk@xakep.ru" + } + ], + "description": "phplrt runtime (lexer, parser, source, position and their contracts) downgraded to PHP 7.4", + "homepage": "https://github.com/phplrt/phplrt", + "support": { + "issues": "https://github.com/ondrejmirtes/phplrt-runtime/issues", + "source": "https://github.com/ondrejmirtes/phplrt-runtime/tree/4.2.4.0" + }, + "time": "2026-10-26T10:54:37+00:00" + }, { "name": "phpstan/php-8-stubs", "version": "0.4.43", diff --git a/patches/Buffer.patch b/patches/Buffer.patch deleted file mode 100644 index 1e50ecf1124..00000000000 --- a/patches/Buffer.patch +++ /dev/null @@ -1,54 +0,0 @@ ---- Buffer.php 2017-01-10 11:34:47.000000000 +0100 -+++ Buffer.php 2021-10-30 16:36:22.000000000 +0200 -@@ -103,7 +103,7 @@ - * - * @return \Iterator - */ -- public function getInnerIterator() -+ public function getInnerIterator(): ?\Iterator - { - return $this->_iterator; - } -@@ -133,6 +133,7 @@ - * - * @return mixed - */ -+ #[\ReturnTypeWillChange] - public function current() - { - return $this->getBuffer()->current()[self::BUFFER_VALUE]; -@@ -143,6 +144,7 @@ - * - * @return mixed - */ -+ #[\ReturnTypeWillChange] - public function key() - { - return $this->getBuffer()->current()[self::BUFFER_KEY]; -@@ -153,7 +155,7 @@ - * - * @return void - */ -- public function next() -+ public function next(): void - { - $innerIterator = $this->getInnerIterator(); - $buffer = $this->getBuffer(); -@@ -204,7 +206,7 @@ - * - * @return void - */ -- public function rewind() -+ public function rewind(): void - { - $innerIterator = $this->getInnerIterator(); - $buffer = $this->getBuffer(); -@@ -228,7 +230,7 @@ - * - * @return bool - */ -- public function valid() -+ public function valid(): bool - { - return - $this->getBuffer()->valid() && diff --git a/patches/Consistency.patch b/patches/Consistency.patch deleted file mode 100644 index 4409109b36d..00000000000 --- a/patches/Consistency.patch +++ /dev/null @@ -1,45 +0,0 @@ ---- Consistency.php 2017-05-02 14:18:12.000000000 +0200 -+++ Consistency.php 2020-05-05 08:28:35.000000000 +0200 -@@ -319,42 +319,6 @@ - $define('STREAM_CRYPTO_METHOD_ANY_CLIENT', 63); - } - --if (!function_exists('curry')) { -- /** -- * Curry. -- * Example: -- * $c = curry('str_replace', …, …, 'foobar'); -- * var_dump($c('foo', 'baz')); // bazbar -- * $c = curry('str_replace', 'foo', 'baz', …); -- * var_dump($c('foobarbaz')); // bazbarbaz -- * Nested curries also work: -- * $c1 = curry('str_replace', …, …, 'foobar'); -- * $c2 = curry($c1, 'foo', …); -- * var_dump($c2('baz')); // bazbar -- * Obviously, as the first argument is a callable, we can combine this with -- * \Hoa\Consistency\Xcallable ;-). -- * The “…” character is the HORIZONTAL ELLIPSIS Unicode character (Unicode: -- * 2026, UTF-8: E2 80 A6). -- * -- * @param mixed $callable Callable (two parts). -- * @param ... ... Arguments. -- * @return \Closure -- */ -- function curry($callable) -- { -- $arguments = func_get_args(); -- array_shift($arguments); -- $ii = array_keys($arguments, …, true); -- -- return function () use ($callable, $arguments, $ii) { -- return call_user_func_array( -- $callable, -- array_replace($arguments, array_combine($ii, func_get_args())) -- ); -- }; -- } --} -- - /** - * Flex entity. - */ diff --git a/patches/File.patch b/patches/File.patch deleted file mode 100644 index 0732eb0e555..00000000000 --- a/patches/File.patch +++ /dev/null @@ -1,11 +0,0 @@ ---- File.php 2017-07-11 09:42:15 -+++ File.php 2024-08-26 23:13:27 -@@ -192,7 +192,7 @@ - * @throws \Hoa\File\Exception\FileDoesNotExist - * @throws \Hoa\File\Exception - */ -- protected function &_open($streamName, Stream\Context $context = null) -+ protected function &_open($streamName, ?Stream\Context $context = null) - { - if (substr($streamName, 0, 4) == 'file' && - false === is_dir(dirname($streamName))) { diff --git a/patches/HoaException.patch b/patches/HoaException.patch deleted file mode 100644 index bada6165046..00000000000 --- a/patches/HoaException.patch +++ /dev/null @@ -1,11 +0,0 @@ ---- Exception/Exception.php 2024-06-24 15:17:26 -+++ Exception/Exception.php 2024-06-24 15:17:51 -@@ -37,7 +37,7 @@ - namespace Hoa\Compiler\Exception; - - use Hoa\Consistency; --use Hoa\Exception as HoaException; -+use Hoa\Exception\Exception as HoaException; - - /** - * Class \Hoa\Compiler\Exception. diff --git a/patches/HoaParser.patch b/patches/HoaParser.patch deleted file mode 100644 index 46829378e20..00000000000 --- a/patches/HoaParser.patch +++ /dev/null @@ -1,11 +0,0 @@ ---- Llk/Parser.php 2017-08-08 09:44:07 -+++ Llk/Parser.php 2025-09-08 12:20:08 -@@ -169,7 +169,7 @@ - $this->_trace = []; - $this->_todo = []; - -- if (false === array_key_exists($rule, $this->_rules)) { -+ if (false === array_key_exists($rule ?? '', $this->_rules)) { - $rule = $this->getRootRule(); - } - diff --git a/patches/Idle.patch b/patches/Idle.patch deleted file mode 100644 index 6f6f0dbe29b..00000000000 --- a/patches/Idle.patch +++ /dev/null @@ -1,11 +0,0 @@ ---- Idle.php 2017-01-16 08:53:27 -+++ Idle.php 2024-08-26 23:18:04 -@@ -100,7 +100,7 @@ - $message, - $code = 0, - $arguments = [], -- \Exception $previous = null -+ ?\Exception $previous = null - ) { - $this->_tmpArguments = $arguments; - parent::__construct($message, $code, $previous); diff --git a/patches/Invocation.patch b/patches/Invocation.patch deleted file mode 100644 index a3e9e10965d..00000000000 --- a/patches/Invocation.patch +++ /dev/null @@ -1,11 +0,0 @@ ---- Llk/Rule/Invocation.php 2017-08-08 09:44:07 -+++ Llk/Rule/Invocation.php 2024-08-26 23:11:25 -@@ -95,7 +95,7 @@ - public function __construct( - $rule, - $data, -- array $todo = null, -+ ?array $todo = null, - $depth = -1 - ) { - $this->_rule = $rule; diff --git a/patches/Lexer.patch b/patches/Lexer.patch deleted file mode 100644 index b2d84ed6e9b..00000000000 --- a/patches/Lexer.patch +++ /dev/null @@ -1,12 +0,0 @@ -diff --git a/Llk/Lexer.php b/Llk/Lexer.php -index 6851367..b8acf98 100644 ---- a/Llk/Lexer.php -+++ b/Llk/Lexer.php -@@ -281,7 +281,7 @@ class Lexer - $offset - ); - -- if (0 === $preg) { -+ if (0 === $preg || $preg === false) { - return null; - } diff --git a/patches/Lookahead.patch b/patches/Lookahead.patch deleted file mode 100644 index d17a3784444..00000000000 --- a/patches/Lookahead.patch +++ /dev/null @@ -1,52 +0,0 @@ ---- Lookahead.php 2017-01-10 11:34:47.000000000 +0100 -+++ Lookahead.php 2021-10-30 16:35:30.000000000 +0200 -@@ -93,7 +93,7 @@ - * - * @return \Iterator - */ -- public function getInnerIterator() -+ public function getInnerIterator(): ?\Iterator - { - return $this->_iterator; - } -@@ -103,6 +103,7 @@ - * - * @return mixed - */ -+ #[\ReturnTypeWillChange] - public function current() - { - return $this->_current; -@@ -113,6 +114,7 @@ - * - * @return mixed - */ -+ #[\ReturnTypeWillChange] - public function key() - { - return $this->_key; -@@ -123,6 +125,7 @@ - * - * @return void - */ -+ #[\ReturnTypeWillChange] - public function next() - { - $innerIterator = $this->getInnerIterator(); -@@ -143,6 +146,7 @@ - * - * @return void - */ -+ #[\ReturnTypeWillChange] - public function rewind() - { - $out = $this->getInnerIterator()->rewind(); -@@ -156,7 +160,7 @@ - * - * @return bool - */ -- public function valid() -+ public function valid(): bool - { - return $this->_valid; - } diff --git a/patches/Node.patch b/patches/Node.patch deleted file mode 100644 index d289251ebdc..00000000000 --- a/patches/Node.patch +++ /dev/null @@ -1,46 +0,0 @@ ---- Node/Node.php 2017-01-14 13:26:10.000000000 +0100 -+++ Node/Node.php 2021-10-30 16:32:43.000000000 +0200 -@@ -108,7 +108,7 @@ - * @return \Hoa\Protocol\Protocol - * @throws \Hoa\Protocol\Exception - */ -- public function offsetSet($name, $node) -+ public function offsetSet($name, $node): void - { - if (!($node instanceof self)) { - throw new Protocol\Exception( -@@ -141,6 +141,7 @@ - * @return \Hoa\Protocol\Protocol - * @throws \Hoa\Protocol\Exception - */ -+ #[\ReturnTypeWillChange] - public function offsetGet($name) - { - if (!isset($this[$name])) { -@@ -160,7 +161,7 @@ - * @param string $name Node's name. - * @return bool - */ -- public function offsetExists($name) -+ public function offsetExists($name): bool - { - return true === array_key_exists($name, $this->_children); - } -@@ -171,7 +172,7 @@ - * @param string $name Node's name to remove. - * @return void - */ -- public function offsetUnset($name) -+ public function offsetUnset($name): void - { - unset($this->_children[$name]); - -@@ -365,7 +366,7 @@ - * - * @return \ArrayIterator - */ -- public function getIterator() -+ public function getIterator(): \Traversable - { - return new \ArrayIterator($this->_children); - } diff --git a/patches/Prelude.patch b/patches/Prelude.patch deleted file mode 100644 index c1d385dd376..00000000000 --- a/patches/Prelude.patch +++ /dev/null @@ -1,11 +0,0 @@ ---- Prelude.php -+++ Prelude.php -@@ -49,7 +49,7 @@ - - $define = function ($constantName, $constantValue, $case = false) { - if (!defined($constantName)) { -- return define($constantName, $constantValue, $case); -+ return define($constantName, $constantValue); - } - - return false; diff --git a/patches/Read.patch b/patches/Read.patch deleted file mode 100644 index ad9b64c4458..00000000000 --- a/patches/Read.patch +++ /dev/null @@ -1,11 +0,0 @@ ---- Read.php 2017-07-11 09:42:15 -+++ Read.php 2024-08-26 23:09:54 -@@ -77,7 +77,7 @@ - * @throws \Hoa\File\Exception\FileDoesNotExist - * @throws \Hoa\File\Exception - */ -- protected function &_open($streamName, Stream\Context $context = null) -+ protected function &_open($streamName, ?Stream\Context $context = null) - { - static $createModes = [ - parent::MODE_READ diff --git a/patches/Rule.patch b/patches/Rule.patch deleted file mode 100644 index faf698a5ffb..00000000000 --- a/patches/Rule.patch +++ /dev/null @@ -1,14 +0,0 @@ ---- Llk/Rule/Rule.php 2017-08-08 09:44:07.000000000 +0200 -+++ Llk/Rule/Rule.php 2021-10-29 16:42:12.000000000 +0200 -@@ -118,7 +118,10 @@ - { - $this->setName($name); - $this->setChildren($children); -- $this->setNodeId($nodeId); -+ -+ if ($nodeId !== null) { -+ $this->setNodeId($nodeId); -+ } - - return; - } diff --git a/patches/Stream.patch b/patches/Stream.patch deleted file mode 100644 index 8dbb2e108e9..00000000000 --- a/patches/Stream.patch +++ /dev/null @@ -1,32 +0,0 @@ ---- Stream.php 2024-08-26 23:05:49 -+++ Stream.php 2024-08-26 23:01:08 -@@ -192,7 +192,7 @@ - * @return array - * @throws \Hoa\Stream\Exception - */ -- final private static function &_getStream( -+ private static function &_getStream( - $streamName, - Stream $handler, - $context = null -@@ -250,7 +250,7 @@ - * @return resource - * @throws \Hoa\Exception\Exception - */ -- abstract protected function &_open($streamName, Context $context = null); -+ abstract protected function &_open($streamName, ?Context $context = null); - - /** - * Close the current stream. -@@ -687,11 +687,6 @@ - Consistency::flexEntity('Hoa\Stream\Stream'); - - /** -- * Shutdown method. -- */ --Consistency::registerShutdownFunction(xcallable('Hoa\Stream\Stream::_Hoa_Stream')); -- --/** - * Add the `hoa://Library/Stream` node. Should be use to reach/get an entry - * in the stream register. - */ diff --git a/patches/TreeNode.patch b/patches/TreeNode.patch deleted file mode 100644 index 39e7917defc..00000000000 --- a/patches/TreeNode.patch +++ /dev/null @@ -1,14 +0,0 @@ ---- Llk/TreeNode.php 2017-08-08 09:44:07 -+++ Llk/TreeNode.php 2024-08-26 23:07:29 -@@ -95,9 +95,9 @@ - */ - public function __construct( - $id, -- array $value = null, -+ ?array $value = null, - array $children = [], -- self $parent = null -+ ?self $parent = null - ) { - $this->setId($id); - diff --git a/patches/Wrapper.patch b/patches/Wrapper.patch deleted file mode 100644 index b8282376bde..00000000000 --- a/patches/Wrapper.patch +++ /dev/null @@ -1,27 +0,0 @@ ---- Wrapper.php 2017-01-14 13:26:10.000000000 +0100 -+++ Wrapper.php 2020-05-05 08:39:18.000000000 +0200 -@@ -582,24 +582,3 @@ - stream_wrapper_register('hoa', Wrapper::class); - - } -- --namespace --{ -- --/** -- * Alias of `Hoa\Protocol::resolve` method. -- * -- * @param string $path Path to resolve. -- * @param bool $exists If `true`, try to find the first that exists, -- * else return the first solution. -- * @param bool $unfold Return all solutions instead of one. -- * @return mixed -- */ --if (!function_exists('resolve')) { -- function resolve($path, $exists = true, $unfold = false) -- { -- return Hoa\Protocol::getInstance()->resolve($path, $exists, $unfold); -- } --} -- --} diff --git a/phpcs.xml b/phpcs.xml index 5a9b9e83855..98e0caae54d 100644 --- a/phpcs.xml +++ b/phpcs.xml @@ -118,6 +118,8 @@ src/Type/TypeResult.php + build/regex-parser/vendor/* + src/Type/Regex/Parser/RegexParser.php compiler/tests/*/data/ tests/*/Fixture/ tests/*/cache/ diff --git a/resources/RegexGrammar.pp b/resources/RegexGrammar.pp deleted file mode 100644 index 3f49912a366..00000000000 --- a/resources/RegexGrammar.pp +++ /dev/null @@ -1,224 +0,0 @@ -// -// Hoa -// -// -// @license -// -// New BSD License -// -// Copyright © 2007-2017, Hoa community. All rights reserved. -// -// Redistribution and use in source and binary forms, with or without -// modification, are permitted provided that the following conditions are met: -// * Redistributions of source code must retain the above copyright -// notice, this list of conditions and the following disclaimer. -// * Redistributions in binary form must reproduce the above copyright -// notice, this list of conditions and the following disclaimer in the -// documentation and/or other materials provided with the distribution. -// * Neither the name of the Hoa nor the names of its contributors may be -// used to endorse or promote products derived from this software without -// specific prior written permission. -// -// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE -// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS AND CONTRIBUTORS BE -// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR -// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF -// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS -// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN -// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) -// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -// POSSIBILITY OF SUCH DAMAGE. -// -// Grammar \Hoa\Regex\Grammar. -// -// Provide grammar of PCRE (Perl Compatible Regular Expression)for the LL(k) -// parser. More informations at http://pcre.org/pcre.txt, sections pcrepattern & -// pcresyntax. -// -// @copyright Copyright © 2007-2017 Hoa community. -// @license New BSD License -// - -// Character classes. -// tokens suffixed with "fc_" are the same as without such suffix but followed by "class:_class" -%token negative_class_fc_ \[\^(?=\]) -> class_fc -%token class_fc_ \[(?=\]) -> class_fc -%token class_fc:_class \] -> class -%token negative_class_ \[\^ -> class -%token class_ \[ -> class -%token class:posix_class \[:\^?[a-z]+:\] -%token class:class_ \[ -%token class:_class \] -> default -%token class:range \- -// taken over from literals but class:character has \b support on top (backspace in character classes) -%token class:character \\([aefnrtb]|c[\x00-\x7f]) -%token class:dynamic_character \\([0-7]{3}|x[0-9a-zA-Z]{2}|x{[0-9a-zA-Z]+}) -%token class:character_type \\([CdDhHNRsSvVwWX]|[pP]{[^}]+}) -%token class:literal \\.|.|\n - -// Internal options. -// See https://www.regular-expressions.info/refmodifiers.html -// and https://www.php.net/manual/en/regexp.reference.internal-options.php -%token internal_option \(\?[imsxnJUX^]*-?[imsxnJUX^]+\) - -// Lookahead and lookbehind assertions. -%token lookahead_ \(\?= -%token negative_lookahead_ \(\?! -%token lookbehind_ \(\?<= -%token negative_lookbehind_ \(\? nc -%token absolute_reference_ \(\?\((?=\d) -> c -%token relative_reference_ \(\?\((?=[\+\-]) -> c -%token c:index [\+\-]?\d+ -> default -%token assertion_reference_ \(\?\( - -// Comments. -%token comment_ \(\?# -> co -%token co:_comment \) -> default -%token co:comment .*?(?=(? mark -%token mark:name [^)]+ -%token mark:_marker \) -> default - -// Capturing group. -%token named_capturing_ \(\?P?< -> nc -%token nc:_named_capturing > -> default -%token nc:capturing_name .+?(?=(?) -%token non_capturing_ \(\?: -%token non_capturing_internal_option \(\?[imsxnJUX^]*-?[imsxnJUX^]+: -%token non_capturing_reset_ \(\?\| -%token atomic_group_ \(\?> -%token capturing_ \( -%token _capturing \) - -// Quantifiers (by default, greedy). -%token zero_or_one_possessive \?\+ -%token zero_or_one_lazy \?\? -%token zero_or_one \? -%token zero_or_more_possessive \*\+ -%token zero_or_more_lazy \*\? -%token zero_or_more \* -%token one_or_more_possessive \+\+ -%token one_or_more_lazy \+\? -%token one_or_more \+ -%token exactly_n \{[0-9]+\} -%token n_to_m_possessive \{[0-9]+,[0-9]+\}\+ -%token n_to_m_lazy \{[0-9]+,[0-9]+\}\? -%token n_to_m \{[0-9]+,[0-9]+\} -%token n_or_more_possessive \{[0-9]+,\}\+ -%token n_or_more_lazy \{[0-9]+,\}\? -%token n_or_more \{[0-9]+,\} - -// Alternation. -%token alternation \| - -// Literal. -%token character \\([aefnrt]|c[\x00-\x7f]) -%token dynamic_character \\([0-7]{3}|x[0-9a-zA-Z]{2}|x{[0-9a-zA-Z]+}) -// Please, see PCRESYNTAX(3), General Category properties, PCRE special category -// properties and script names for \p{} and \P{}. -%token character_type \\([CdDhHNRsSvVwWX]|[pP]{[^}]+}) -%token anchor \\([bBAZzG])|\^|\$ -%token match_point_reset \\K -%token literal \\.|.|\n - - -// Rules. - -#expression: - alternation() - -alternation: - concatenation()? ( concatenation()? #alternation )* - -concatenation: - ( internal_options() | assertion() | quantification() | condition() ) - ( ( internal_options() | assertion() | quantification() | condition() ) #concatenation )* - -#internal_options: - - -#condition: - ( - ::named_reference_:: ::_named_capturing:: #namedcondition - | ( - ::relative_reference_:: #relativecondition - | ::absolute_reference_:: #absolutecondition - ) - - | ::assertion_reference_:: alternation() #assertioncondition - ) - ::_capturing:: - alternation() - ::_capturing:: - -assertion: - ( - ::lookahead_:: #lookahead - | ::negative_lookahead_:: #negativelookahead - | ::lookbehind_:: #lookbehind - | ::negative_lookbehind_:: #negativelookbehind - ) - alternation() - ::_capturing:: - -quantification: - ( class() | simple() ) ( quantifier() #quantification )? - -quantifier: - | | - | | | - | | | - | - | | | - | | | - -#class: - ( - ::negative_class_fc_:: #negativeclass - <_class> - | ::class_fc_:: - <_class> - | ::negative_class_:: #negativeclass - | ::class_:: - ) - ? ( | | range() ? | literal() )* ? - ::_class:: - -#range: - literal() ::range:: literal() - -simple: - capturing() - | literal() - -#capturing: - ::marker_:: ::_marker:: #mark - | ::comment_:: ? ::_comment:: #comment - | ( - ::named_capturing_:: ::_named_capturing:: #namedcapturing - | ::non_capturing_:: #noncapturing - | non_capturing_internal_options() #noncapturing - | ::non_capturing_reset_:: #noncapturingreset - | ::atomic_group_:: #atomicgroup - | ::capturing_:: - ) - alternation() - ::_capturing:: - -non_capturing_internal_options: - - -literal: - - | - | - | - | - | diff --git a/src/Command/IgnoredRegexValidator.php b/src/Command/IgnoredRegexValidator.php index fc096bdb782..cfcfb22524c 100644 --- a/src/Command/IgnoredRegexValidator.php +++ b/src/Command/IgnoredRegexValidator.php @@ -2,14 +2,16 @@ namespace PHPStan\Command; -use Hoa\Compiler\Llk\Parser; -use Hoa\Compiler\Llk\TreeNode; -use Hoa\Exception\Exception; use Nette\Utils\Strings; +use Phplrt\Contracts\Parser\Exception\ParserExceptionInterface; +use Phplrt\Source\StringSource; use PHPStan\PhpDoc\TypeStringResolver; use PHPStan\PhpDocParser\Parser\ParserException; use PHPStan\ShouldNotHappenException; use PHPStan\Type\ObjectType; +use PHPStan\Type\Regex\Parser\RegexNode; +use PHPStan\Type\Regex\Parser\RegexParser; +use PHPStan\Type\Regex\Parser\RegexSyntaxException; use PHPStan\Type\VerbosityLevel; use function count; use function strrpos; @@ -19,7 +21,7 @@ final class IgnoredRegexValidator { public function __construct( - private Parser $parser, + private RegexParser $parser, private TypeStringResolver $typeStringResolver, ) { @@ -30,11 +32,13 @@ public function validate(string $regex): IgnoredRegexValidatorResult $regex = $this->removeDelimiters($regex); try { - /** @var TreeNode $ast */ - $ast = $this->parser->parse($regex); - } catch (Exception) { + $ast = $this->parser->parse(StringSource::createFromString($regex)); + } catch (ParserExceptionInterface | RegexSyntaxException) { return new IgnoredRegexValidatorResult(ignoredTypes: [], anchorsInTheMiddle: false, allErrorsIgnored: false); } + if (!$ast instanceof RegexNode) { + throw new ShouldNotHappenException(); + } if (Strings::match($regex, '~(? */ - private function getIgnoredTypes(TreeNode $ast): array + private function getIgnoredTypes(RegexNode $ast): array { - /** @var TreeNode|null $alternation */ - $alternation = $ast->getChild(0); + $alternation = $ast->getChildren()[0] ?? null; if ($alternation === null) { return []; } @@ -110,7 +113,7 @@ private function removeDelimiters(string $regex): string return substr($regex, 1, $endDelimiterPosition - 1); } - private function getText(TreeNode $treeNode): ?string + private function getText(RegexNode $treeNode): ?string { if ($treeNode->getId() === 'token') { return $treeNode->getValueValue(); @@ -137,12 +140,10 @@ private function getText(TreeNode $treeNode): ?string return null; } - private function hasAnchorsInTheMiddle(TreeNode $ast): bool + private function hasAnchorsInTheMiddle(RegexNode $ast): bool { if ($ast->getId() === 'token') { - $valueArray = $ast->getValue(); - - return $valueArray['token'] === 'anchor' && $valueArray['value'] === '$'; + return $ast->getValueToken() === 'anchor' && $ast->getValueValue() === '$'; } $childrenCount = count($ast->getChildren()); foreach ($ast->getChildren() as $i => $child) { diff --git a/src/DependencyInjection/ValidateIgnoredErrorsExtension.php b/src/DependencyInjection/ValidateIgnoredErrorsExtension.php index 63c98a70d34..4cfdb396799 100644 --- a/src/DependencyInjection/ValidateIgnoredErrorsExtension.php +++ b/src/DependencyInjection/ValidateIgnoredErrorsExtension.php @@ -2,8 +2,6 @@ namespace PHPStan\DependencyInjection; -use Hoa\Compiler\Llk\Llk; -use Hoa\File\Read; use Nette\DI\CompilerExtension; use Nette\Utils\RegexpException; use Nette\Utils\Strings; @@ -33,6 +31,7 @@ use PHPStan\Type\DirectTypeAliasResolverProvider; use PHPStan\Type\ObjectType; use PHPStan\Type\OperatorTypeSpecifyingExtensionRegistry; +use PHPStan\Type\Regex\Parser\RegexParser; use PHPStan\Type\Type; use PHPStan\Type\TypeAliasResolver; use PHPStan\Type\UnaryOperatorTypeSpecifyingExtensionRegistry; @@ -68,8 +67,7 @@ public function loadConfiguration(): void return; } - /** @throws void */ - $parser = Llk::load(new Read(__DIR__ . '/../../resources/RegexGrammar.pp')); + $parser = new RegexParser(); $reflectionProvider = new DummyReflectionProvider(); $reflectionProviderProvider = new DirectReflectionProviderProvider($reflectionProvider); diff --git a/src/Type/Php/RegexArrayShapeMatcher.php b/src/Type/Php/RegexArrayShapeMatcher.php index 979d8b23f46..b26810cd8a7 100644 --- a/src/Type/Php/RegexArrayShapeMatcher.php +++ b/src/Type/Php/RegexArrayShapeMatcher.php @@ -133,7 +133,7 @@ private function matchRegex(string $regex, ?int $flags, TrinaryLogic $wasMatched { $astWalkResult = $this->regexGroupParser->parseGroups($regex); if ($astWalkResult === null) { - // regex could not be parsed by Hoa/Regex + // regex could not be parsed by RegexParser return null; } $groupList = $astWalkResult->getCapturingGroups(); diff --git a/src/Type/Regex/Parser/RegexNode.php b/src/Type/Regex/Parser/RegexNode.php new file mode 100644 index 00000000000..a1e7fff28dd --- /dev/null +++ b/src/Type/Regex/Parser/RegexNode.php @@ -0,0 +1,65 @@ + $children + */ + public function __construct( + private string $id, + private string $valueToken = '', + private string $valueValue = '', + private array $children = [], + ) + { + } + + public function getId(): string + { + return $this->id; + } + + public function getValueToken(): string + { + return $this->valueToken; + } + + public function getValueValue(): string + { + return $this->valueValue; + } + + /** @return list */ + public function getChildren(): array + { + return $this->children; + } + + public function getChild(int $i): self + { + if (!isset($this->children[$i])) { + throw new ShouldNotHappenException(); + } + + return $this->children[$i]; + } + + /** @param list $children */ + public function setChildren(array $children): void + { + $this->children = $children; + } + +} diff --git a/src/Type/Regex/Parser/RegexParser.php b/src/Type/Regex/Parser/RegexParser.php new file mode 100644 index 00000000000..715f82928c4 --- /dev/null +++ b/src/Type/Regex/Parser/RegexParser.php @@ -0,0 +1,1279 @@ + + * + * @readonly + */ +final class RegexParser implements \Phplrt\Contracts\Parser\ParserInterface +{ + /** @var int */ + public const T_NEGATIVE_CLASS_FC = 0; + /** @var int */ + public const T_CLASS_FC = 1; + /** @var int */ + public const T_NEGATIVE_CLASS = 2; + /** @var int */ + public const T_CLASS = 3; + /** @var int */ + public const T_INTERNAL_OPTION = 4; + /** @var int */ + public const T_LOOKAHEAD = 5; + /** @var int */ + public const T_NEGATIVE_LOOKAHEAD = 6; + /** @var int */ + public const T_LOOKBEHIND = 7; + /** @var int */ + public const T_NEGATIVE_LOOKBEHIND = 8; + /** @var int */ + public const T_NAMED_REFERENCE = 9; + /** @var int */ + public const T_ABSOLUTE_REFERENCE = 10; + /** @var int */ + public const T_RELATIVE_REFERENCE = 11; + /** @var int */ + public const T_ASSERTION_REFERENCE = 12; + /** @var int */ + public const T_COMMENT = 13; + /** @var int */ + public const T_MARKER = 14; + /** @var int */ + public const T_NAMED_CAPTURING = 15; + /** @var int */ + public const T_NON_CAPTURING = 16; + /** @var int */ + public const T_NON_CAPTURING_INTERNAL_OPTION = 17; + /** @var int */ + public const T_NON_CAPTURING_RESET = 18; + /** @var int */ + public const T_ATOMIC_GROUP = 19; + /** @var int */ + public const T_CAPTURING = 20; + /** @var int */ + public const T_CAPTURING_CLOSE = 21; + /** @var int */ + public const T_ZERO_OR_ONE_POSSESSIVE = 22; + /** @var int */ + public const T_ZERO_OR_ONE_LAZY = 23; + /** @var int */ + public const T_ZERO_OR_ONE = 24; + /** @var int */ + public const T_ZERO_OR_MORE_POSSESSIVE = 25; + /** @var int */ + public const T_ZERO_OR_MORE_LAZY = 26; + /** @var int */ + public const T_ZERO_OR_MORE = 27; + /** @var int */ + public const T_ONE_OR_MORE_POSSESSIVE = 28; + /** @var int */ + public const T_ONE_OR_MORE_LAZY = 29; + /** @var int */ + public const T_ONE_OR_MORE = 30; + /** @var int */ + public const T_EXACTLY_N = 31; + /** @var int */ + public const T_N_TO_M_POSSESSIVE = 32; + /** @var int */ + public const T_N_TO_M_LAZY = 33; + /** @var int */ + public const T_N_TO_M = 34; + /** @var int */ + public const T_N_OR_MORE_POSSESSIVE = 35; + /** @var int */ + public const T_N_OR_MORE_LAZY = 36; + /** @var int */ + public const T_N_OR_MORE = 37; + /** @var int */ + public const T_ALTERNATION = 38; + /** @var int */ + public const T_CHARACTER = 39; + /** @var int */ + public const T_DYNAMIC_CHARACTER = 40; + /** @var int */ + public const T_CHARACTER_TYPE = 41; + /** @var int */ + public const T_ANCHOR = 42; + /** @var int */ + public const T_MATCH_POINT_RESET = 43; + /** @var int */ + public const T_LITERAL = 44; + + /** @var array */ + private const PARSER_EXPECTATIONS = [ + 'T_NEGATIVE_CLASS_FC', + 'T_CLASS_FC', + 'T_NEGATIVE_CLASS', + 'T_CLASS', + 'T_INTERNAL_OPTION', + 'T_LOOKAHEAD', + 'T_NEGATIVE_LOOKAHEAD', + 'T_LOOKBEHIND', + 'T_NEGATIVE_LOOKBEHIND', + 'T_NAMED_REFERENCE', + 'T_ABSOLUTE_REFERENCE', + 'T_RELATIVE_REFERENCE', + 'T_ASSERTION_REFERENCE', + 'T_COMMENT', + 'T_MARKER', + 'T_NAMED_CAPTURING', + 'T_NON_CAPTURING', + 'T_NON_CAPTURING_INTERNAL_OPTION', + 'T_NON_CAPTURING_RESET', + 'T_ATOMIC_GROUP', + 'T_CAPTURING', + 'T_CAPTURING_CLOSE', + 'T_ZERO_OR_ONE_POSSESSIVE', + 'T_ZERO_OR_ONE_LAZY', + 'T_ZERO_OR_ONE', + 'T_ZERO_OR_MORE_POSSESSIVE', + 'T_ZERO_OR_MORE_LAZY', + 'T_ZERO_OR_MORE', + 'T_ONE_OR_MORE_POSSESSIVE', + 'T_ONE_OR_MORE_LAZY', + 'T_ONE_OR_MORE', + 'T_EXACTLY_N', + 'T_N_TO_M_POSSESSIVE', + 'T_N_TO_M_LAZY', + 'T_N_TO_M', + 'T_N_OR_MORE_POSSESSIVE', + 'T_N_OR_MORE_LAZY', + 'T_N_OR_MORE', + 'T_ALTERNATION', + 'T_CHARACTER', + 'T_DYNAMIC_CHARACTER', + 'T_CHARACTER_TYPE', + 'T_ANCHOR', + 'T_MATCH_POINT_RESET', + 'T_LITERAL', + '/[^\\s]++/', + ]; + + /** @var array */ + private const PARSER_KEPT_TABLE = [ + true, + true, + false, + true, + false, + true, + true, + false, + false, + false, + false, + false, + false, + true, + false, + true, + false, + false, + false, + false, + true, + false, + false, + true, + false, + false, + false, + false, + false, + false, + false, + true, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + true, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + ]; + + /** @var array|null> */ + private const PARSER_START_PREDICTION_TABLE = [ + null, + null, + null, + [ + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + 39 => true, + true, + true, + true, + true, + true, + ], + [ + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + 39 => true, + true, + true, + true, + true, + true, + ], + [ + 4 => true, + ], + [ + 5 => true, + true, + true, + true, + ], + [ + 5 => true, + true, + true, + true, + ], + [ + 5 => true, + ], + [ + 6 => true, + ], + [ + 7 => true, + ], + [ + 8 => true, + ], + [ + 21 => true, + ], + [ + true, + true, + true, + true, + 13 => true, + true, + true, + true, + true, + true, + true, + true, + 39 => true, + true, + true, + true, + true, + true, + ], + [ + true, + true, + true, + true, + 13 => true, + true, + true, + true, + true, + true, + true, + true, + 39 => true, + true, + true, + true, + true, + true, + ], + [ + true, + true, + true, + true, + ], + [ + true, + ], + [ + 1 => true, + ], + [ + 2 => true, + ], + [ + 3 => true, + ], + [ + 13 => true, + true, + true, + true, + true, + true, + true, + true, + ], + [ + 14 => true, + ], + [ + 13 => true, + ], + [ + 15 => true, + true, + true, + true, + true, + true, + ], + [ + 15 => true, + true, + true, + true, + true, + true, + ], + [ + 15 => true, + ], + [ + 16 => true, + ], + [ + 17 => true, + ], + [ + 18 => true, + ], + [ + 19 => true, + ], + [ + 20 => true, + ], + [ + 39 => true, + true, + true, + true, + true, + true, + ], + [ + 39 => true, + ], + [ + 40 => true, + ], + [ + 41 => true, + ], + [ + 42 => true, + ], + [ + 43 => true, + ], + [ + 44 => true, + ], + null, + [ + 22 => true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + true, + ], + [ + 22 => true, + ], + [ + 23 => true, + ], + [ + 24 => true, + ], + [ + 25 => true, + ], + [ + 26 => true, + ], + [ + 27 => true, + ], + [ + 28 => true, + ], + [ + 29 => true, + ], + [ + 30 => true, + ], + [ + 31 => true, + ], + [ + 32 => true, + ], + [ + 33 => true, + ], + [ + 34 => true, + ], + [ + 35 => true, + ], + [ + 36 => true, + ], + [ + 37 => true, + ], + [ + 9 => true, + true, + true, + true, + ], + [ + 9 => true, + true, + true, + true, + ], + [ + 9 => true, + ], + [ + 10 => true, + ], + [ + 11 => true, + ], + [ + 12 => true, + ], + [ + 12 => true, + ], + null, + [ + 38 => true, + ], + [ + 38 => true, + ], + [ + 38 => true, + ], + ]; + + /** @var array>> */ + private const PARSER_CHOICE_PREDICTION_TABLE = [ + 4 => [ + 4 => [ + -6, + ], + [ + 6, + ], + [ + 6, + ], + [ + 6, + ], + [ + 6, + ], + 0 => [ + 13, + ], + 1 => [ + 13, + ], + 2 => [ + 13, + ], + 3 => [ + 13, + ], + 13 => [ + 13, + ], + [ + 13, + ], + [ + 13, + ], + [ + 13, + ], + [ + 13, + ], + [ + 13, + ], + [ + 13, + ], + [ + 13, + ], + 39 => [ + 13, + ], + [ + 13, + ], + [ + 13, + ], + [ + 13, + ], + [ + 13, + ], + [ + 13, + ], + 9 => [ + 56, + ], + 10 => [ + 56, + ], + 11 => [ + 56, + ], + 12 => [ + 56, + ], + ], + 7 => [ + 5 => [ + -9, + ], + [ + -10, + ], + [ + -11, + ], + [ + -12, + ], + ], + 14 => [ + [ + 15, + ], + [ + 15, + ], + [ + 15, + ], + [ + 15, + ], + 13 => [ + 20, + ], + [ + 20, + ], + [ + 20, + ], + [ + 20, + ], + [ + 20, + ], + [ + 20, + ], + [ + 20, + ], + [ + 20, + ], + 39 => [ + 31, + ], + [ + 31, + ], + [ + 31, + ], + [ + 31, + ], + [ + 31, + ], + [ + 31, + ], + ], + [ + [ + -17, + ], + [ + -18, + ], + [ + -19, + ], + [ + -20, + ], + ], + 20 => [ + 14 => [ + -22, + ], + 13 => [ + -23, + ], + [ + 23, + ], + [ + 23, + ], + [ + 23, + ], + [ + 23, + ], + [ + 23, + ], + [ + 23, + ], + ], + 24 => [ + 15 => [ + -26, + ], + [ + -27, + ], + [ + -28, + ], + [ + -29, + ], + [ + -30, + ], + [ + -31, + ], + ], + 31 => [ + 39 => [ + -33, + ], + [ + -34, + ], + [ + -35, + ], + [ + -36, + ], + [ + -37, + ], + [ + -38, + ], + ], + 39 => [ + 22 => [ + -41, + ], + [ + -42, + ], + [ + -43, + ], + [ + -44, + ], + [ + -45, + ], + [ + -46, + ], + [ + -47, + ], + [ + -48, + ], + [ + -49, + ], + [ + -50, + ], + [ + -51, + ], + [ + -52, + ], + [ + -53, + ], + [ + -54, + ], + [ + -55, + ], + [ + -56, + ], + ], + 57 => [ + 9 => [ + -59, + ], + [ + -60, + ], + [ + -61, + ], + [ + 61, + ], + ], + ]; + + /** @var array> */ + private const PARSER_SEQUENCE_PREDICTION_TABLE = [ + 1 => [ + -4, + -65, + ], + 13 => [ + 14, + -40, + ], + 65 => [ + 66, + -4, + ], + ]; + + /** + * @var \Phplrt\Parser\Parser + */ + protected \Phplrt\Parser\Parser $parser; + + protected readonly \Phplrt\Contracts\Lexer\LexerInterface $lexer; + + public function __construct() + { + $state_class = new \Phplrt\Lexer\Lexer( + pattern: '/\\G(?|(?:(?:\\[:\\^?[a-z]+:\\])(*MARK:0))|(?:(?:\\[)(*MARK:1))|(?:(?:\\])(*MARK:2))|(?:(?:\\-)(*MARK:3))|(?:(?:\\\\(?:[aefnrtb]|c[\\x00-\\x7f]))(*MARK:4))|(?:(?:\\\\(?:[0-7]{3}|x[0-9a-zA-Z]{2}|x{[0-9a-zA-Z]+}))(*MARK:5))|(?:(?:\\\\(?:[CdDhHNRsSvVwWX]|[pP]{[^}]+}))(*MARK:6))|(?:(?:\\\\.|.|\\n)(*MARK:7))|(?:(?:[^\\s]++)(*MARK:8)))/Ssum', + channels: [ + 8 => 'Unknown', + ], + names: [ + 'T_POSIX_CLASS', + 'T_CLASS_NESTED', + 'T_CLASS_CLOSE', + 'T_RANGE', + 'T_CLASS_CHARACTER', + 'T_CLASS_DYNAMIC_CHARACTER', + 'T_CLASS_CHARACTER_TYPE', + 'T_CLASS_LITERAL', + ], + transitions: [ + 2 => null, + ], + ); + + $this->lexer = new \Phplrt\Lexer\Lexer( + pattern: '/\\G(?|(?:(?:\\[\\^\\])(*MARK:0))|(?:(?:\\[\\])(*MARK:1))|(?:(?:\\[\\^)(*MARK:2))|(?:(?:\\[)(*MARK:3))|(?:(?:\\(\\?[imsxnJUX^]*-?[imsxnJUX^]+\\))(*MARK:4))|(?:(?:\\(\\?=)(*MARK:5))|(?:(?:\\(\\?!)(*MARK:6))|(?:(?:\\(\\?<=)(*MARK:7))|(?:(?:\\(\\?\\))(*MARK:9))|(?:(?:\\(\\?\\((\\d+)\\))(*MARK:10))|(?:(?:\\(\\?\\(([\\+\\-]\\d+)\\))(*MARK:11))|(?:(?:\\(\\?\\()(*MARK:12))|(?:(?:\\(\\?#(.*?)(?)(*MARK:15))|(?:(?:\\(\\?:)(*MARK:16))|(?:(?:\\(\\?[imsxnJUX^]*-?[imsxnJUX^]+:)(*MARK:17))|(?:(?:\\(\\?\\|)(*MARK:18))|(?:(?:\\(\\?>)(*MARK:19))|(?:(?:\\()(*MARK:20))|(?:(?:\\))(*MARK:21))|(?:(?:\\?\\+)(*MARK:22))|(?:(?:\\?\\?)(*MARK:23))|(?:(?:\\?)(*MARK:24))|(?:(?:\\*\\+)(*MARK:25))|(?:(?:\\*\\?)(*MARK:26))|(?:(?:\\*)(*MARK:27))|(?:(?:\\+\\+)(*MARK:28))|(?:(?:\\+\\?)(*MARK:29))|(?:(?:\\+)(*MARK:30))|(?:(?:\\{[0-9]+\\})(*MARK:31))|(?:(?:\\{[0-9]+,[0-9]+\\}\\+)(*MARK:32))|(?:(?:\\{[0-9]+,[0-9]+\\}\\?)(*MARK:33))|(?:(?:\\{[0-9]+,[0-9]+\\})(*MARK:34))|(?:(?:\\{[0-9]+,\\}\\+)(*MARK:35))|(?:(?:\\{[0-9]+,\\}\\?)(*MARK:36))|(?:(?:\\{[0-9]+,\\})(*MARK:37))|(?:(?:\\|)(*MARK:38))|(?:(?:\\\\(?:[aefnrt]|c[\\x00-\\x7f]))(*MARK:39))|(?:(?:\\\\(?:[0-7]{3}|x[0-9a-zA-Z]{2}|x{[0-9a-zA-Z]+}))(*MARK:40))|(?:(?:\\\\(?:[CdDhHNRsSvVwWX]|[pP]{[^}]+}))(*MARK:41))|(?:(?:\\\\[bBAZzG]|\\^|\\$)(*MARK:42))|(?:(?:\\\\K)(*MARK:43))|(?:(?:\\\\.|.|\\n)(*MARK:44))|(?:(?:[^\\s]++)(*MARK:45)))/Ssum', + channels: [ + 45 => 'Unknown', + ], + names: [ + 'T_NEGATIVE_CLASS_FC', + 'T_CLASS_FC', + 'T_NEGATIVE_CLASS', + 'T_CLASS', + 'T_INTERNAL_OPTION', + 'T_LOOKAHEAD', + 'T_NEGATIVE_LOOKAHEAD', + 'T_LOOKBEHIND', + 'T_NEGATIVE_LOOKBEHIND', + 'T_NAMED_REFERENCE', + 'T_ABSOLUTE_REFERENCE', + 'T_RELATIVE_REFERENCE', + 'T_ASSERTION_REFERENCE', + 'T_COMMENT', + 'T_MARKER', + 'T_NAMED_CAPTURING', + 'T_NON_CAPTURING', + 'T_NON_CAPTURING_INTERNAL_OPTION', + 'T_NON_CAPTURING_RESET', + 'T_ATOMIC_GROUP', + 'T_CAPTURING', + 'T_CAPTURING_CLOSE', + 'T_ZERO_OR_ONE_POSSESSIVE', + 'T_ZERO_OR_ONE_LAZY', + 'T_ZERO_OR_ONE', + 'T_ZERO_OR_MORE_POSSESSIVE', + 'T_ZERO_OR_MORE_LAZY', + 'T_ZERO_OR_MORE', + 'T_ONE_OR_MORE_POSSESSIVE', + 'T_ONE_OR_MORE_LAZY', + 'T_ONE_OR_MORE', + 'T_EXACTLY_N', + 'T_N_TO_M_POSSESSIVE', + 'T_N_TO_M_LAZY', + 'T_N_TO_M', + 'T_N_OR_MORE_POSSESSIVE', + 'T_N_OR_MORE_LAZY', + 'T_N_OR_MORE', + 'T_ALTERNATION', + 'T_CHARACTER', + 'T_DYNAMIC_CHARACTER', + 'T_CHARACTER_TYPE', + 'T_ANCHOR', + 'T_MATCH_POINT_RESET', + 'T_LITERAL', + ], + transitions: [ + 0 => $state_class, + 1 => $state_class, + 2 => $state_class, + 3 => $state_class, + ], + subgroups: [ + 9 => 1, + 1, + 1, + 13 => 1, + 1, + 1, + ], + ); + + $this->parser = new \Phplrt\Parser\Parser( + lexer: $this->lexer, + grammar: [ + new \Phplrt\Parser\Grammar\Optional(1), + new \Phplrt\Parser\Grammar\Concatenation([2, 63]), + new \Phplrt\Parser\Grammar\Optional(3), + new \Phplrt\Parser\Grammar\Repetition(4, 1, \INF), + new \Phplrt\Parser\Grammar\Alternation([5, 6, 13, 56]), + new \Phplrt\Parser\Grammar\Lexeme(self::T_INTERNAL_OPTION, true), + new \Phplrt\Parser\Grammar\Concatenation([7, 1, 12]), + new \Phplrt\Parser\Grammar\Alternation([8, 9, 10, 11]), + new \Phplrt\Parser\Grammar\Lexeme(self::T_LOOKAHEAD, true), + new \Phplrt\Parser\Grammar\Lexeme(self::T_NEGATIVE_LOOKAHEAD, true), + new \Phplrt\Parser\Grammar\Lexeme(self::T_LOOKBEHIND, true), + new \Phplrt\Parser\Grammar\Lexeme(self::T_NEGATIVE_LOOKBEHIND, true), + new \Phplrt\Parser\Grammar\Lexeme(self::T_CAPTURING_CLOSE, false), + new \Phplrt\Parser\Grammar\Concatenation([14, 38]), + new \Phplrt\Parser\Grammar\Alternation([15, 20, 31]), + new \Phplrt\Parser\Grammar\Alternation([16, 17, 18, 19]), + new \Phplrt\Parser\Grammar\Lexeme(self::T_NEGATIVE_CLASS_FC, true), + new \Phplrt\Parser\Grammar\Lexeme(self::T_CLASS_FC, true), + new \Phplrt\Parser\Grammar\Lexeme(self::T_NEGATIVE_CLASS, true), + new \Phplrt\Parser\Grammar\Lexeme(self::T_CLASS, true), + new \Phplrt\Parser\Grammar\Alternation([21, 22, 23]), + new \Phplrt\Parser\Grammar\Lexeme(self::T_MARKER, true), + new \Phplrt\Parser\Grammar\Lexeme(self::T_COMMENT, true), + new \Phplrt\Parser\Grammar\Concatenation([24, 1, 12]), + new \Phplrt\Parser\Grammar\Alternation([25, 26, 27, 28, 29, 30]), + new \Phplrt\Parser\Grammar\Lexeme(self::T_NAMED_CAPTURING, true), + new \Phplrt\Parser\Grammar\Lexeme(self::T_NON_CAPTURING, true), + new \Phplrt\Parser\Grammar\Lexeme(self::T_NON_CAPTURING_INTERNAL_OPTION, true), + new \Phplrt\Parser\Grammar\Lexeme(self::T_NON_CAPTURING_RESET, true), + new \Phplrt\Parser\Grammar\Lexeme(self::T_ATOMIC_GROUP, true), + new \Phplrt\Parser\Grammar\Lexeme(self::T_CAPTURING, true), + new \Phplrt\Parser\Grammar\Alternation([32, 33, 34, 35, 36, 37]), + new \Phplrt\Parser\Grammar\Lexeme(self::T_CHARACTER, true), + new \Phplrt\Parser\Grammar\Lexeme(self::T_DYNAMIC_CHARACTER, true), + new \Phplrt\Parser\Grammar\Lexeme(self::T_CHARACTER_TYPE, true), + new \Phplrt\Parser\Grammar\Lexeme(self::T_ANCHOR, true), + new \Phplrt\Parser\Grammar\Lexeme(self::T_MATCH_POINT_RESET, true), + new \Phplrt\Parser\Grammar\Lexeme(self::T_LITERAL, true), + new \Phplrt\Parser\Grammar\Optional(39), + new \Phplrt\Parser\Grammar\Alternation([40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55]), + new \Phplrt\Parser\Grammar\Lexeme(self::T_ZERO_OR_ONE_POSSESSIVE, true), + new \Phplrt\Parser\Grammar\Lexeme(self::T_ZERO_OR_ONE_LAZY, true), + new \Phplrt\Parser\Grammar\Lexeme(self::T_ZERO_OR_ONE, true), + new \Phplrt\Parser\Grammar\Lexeme(self::T_ZERO_OR_MORE_POSSESSIVE, true), + new \Phplrt\Parser\Grammar\Lexeme(self::T_ZERO_OR_MORE_LAZY, true), + new \Phplrt\Parser\Grammar\Lexeme(self::T_ZERO_OR_MORE, true), + new \Phplrt\Parser\Grammar\Lexeme(self::T_ONE_OR_MORE_POSSESSIVE, true), + new \Phplrt\Parser\Grammar\Lexeme(self::T_ONE_OR_MORE_LAZY, true), + new \Phplrt\Parser\Grammar\Lexeme(self::T_ONE_OR_MORE, true), + new \Phplrt\Parser\Grammar\Lexeme(self::T_EXACTLY_N, true), + new \Phplrt\Parser\Grammar\Lexeme(self::T_N_TO_M_POSSESSIVE, true), + new \Phplrt\Parser\Grammar\Lexeme(self::T_N_TO_M_LAZY, true), + new \Phplrt\Parser\Grammar\Lexeme(self::T_N_TO_M, true), + new \Phplrt\Parser\Grammar\Lexeme(self::T_N_OR_MORE_POSSESSIVE, true), + new \Phplrt\Parser\Grammar\Lexeme(self::T_N_OR_MORE_LAZY, true), + new \Phplrt\Parser\Grammar\Lexeme(self::T_N_OR_MORE, true), + new \Phplrt\Parser\Grammar\Concatenation([57, 1, 12]), + new \Phplrt\Parser\Grammar\Alternation([58, 59, 60, 61]), + new \Phplrt\Parser\Grammar\Lexeme(self::T_NAMED_REFERENCE, true), + new \Phplrt\Parser\Grammar\Lexeme(self::T_ABSOLUTE_REFERENCE, true), + new \Phplrt\Parser\Grammar\Lexeme(self::T_RELATIVE_REFERENCE, true), + new \Phplrt\Parser\Grammar\Concatenation([62, 1, 12]), + new \Phplrt\Parser\Grammar\Lexeme(self::T_ASSERTION_REFERENCE, true), + new \Phplrt\Parser\Grammar\Optional(64), + new \Phplrt\Parser\Grammar\Repetition(65, 1, \INF), + new \Phplrt\Parser\Grammar\Concatenation([66, 2]), + new \Phplrt\Parser\Grammar\Lexeme(self::T_ALTERNATION, true), + ], + initial: 0, + reducers: [ + 0 => \Closure::fromCallable([self::class, 'reduceExpression']), + 1 => \Closure::fromCallable([self::class, 'reduceAlternation']), + 3 => \Closure::fromCallable([self::class, 'reduceConcatenation']), + 5 => \Closure::fromCallable([self::class, 'reduceInternalOptions']), + 6 => \Closure::fromCallable([self::class, 'reduceAssertion']), + 13 => \Closure::fromCallable([self::class, 'reduceQuantification']), + 15 => \Closure::fromCallable([self::class, 'reduceCharacterClass']), + 20 => \Closure::fromCallable([self::class, 'reduceCapturing']), + 31 => \Closure::fromCallable([self::class, 'reduceLiteral']), + 56 => \Closure::fromCallable([self::class, 'reduceCondition']), + ], + expectations: self::PARSER_EXPECTATIONS, + kept: self::PARSER_KEPT_TABLE, + startPrediction: self::PARSER_START_PREDICTION_TABLE, + choicePrediction: self::PARSER_CHOICE_PREDICTION_TABLE, + sequencePrediction: self::PARSER_SEQUENCE_PREDICTION_TABLE, + ); + } + + /** + * @throws \Phplrt\Contracts\Parser\Exception\ParserExceptionInterface + * @throws RegexSyntaxException + */ + public function parse(\Phplrt\Contracts\Source\ReadableInterface $source): mixed + { + return $this->parser->parse($source); + } + + private static function reduceExpression(\Phplrt\Parser\Context $ctx, mixed $children): mixed + { + return new RegexNode('#expression', children: \is_array($children) ? $children : [$children]); + } + + private static function reduceAlternation(\Phplrt\Parser\Context $ctx, mixed $children): mixed + { + if (!\is_array($children)) { + return $children; + } + $nodes = []; + $isAlternation = false; + foreach ($children as $child) { + if ($child instanceof TokenInterface) { + $isAlternation = true; + $child = new RegexNode('token', 'alternation', $child->value); + } + $nodes[] = $child; + } + if (!$isAlternation) { + return $children[0] ?? []; + } + return new RegexNode('#alternation', children: $nodes); + } + + private static function reduceConcatenation(\Phplrt\Parser\Context $ctx, mixed $children): mixed + { + if (!\is_array($children)) { + return $children; + } + if (\count($children) === 1) { + return $children[0]; + } + return new RegexNode('#concatenation', children: $children); + } + + private static function reduceInternalOptions(\Phplrt\Parser\Context $ctx, mixed $children): mixed + { + return new RegexNode('#internal_options', children: [ + new RegexNode('token', 'internal_option', $children->value), + ]); + } + + private static function reduceAssertion(\Phplrt\Parser\Context $ctx, mixed $children): mixed + { + $assertion = \array_shift($children); + $ids = [ + self::T_LOOKAHEAD => '#lookahead', + self::T_NEGATIVE_LOOKAHEAD => '#negativelookahead', + self::T_LOOKBEHIND => '#lookbehind', + self::T_NEGATIVE_LOOKBEHIND => '#negativelookbehind', + ]; + return new RegexNode($ids[$assertion->id], children: $children); + } + + private static function reduceQuantification(\Phplrt\Parser\Context $ctx, mixed $children): mixed + { + if (!\is_array($children)) { + return $children; + } + if (\count($children) === 1) { + return $children[0]; + } + return new RegexNode('#quantification', children: [ + $children[0], + new RegexNode('token', \strtolower(\substr($children[1]->name, 2)), $children[1]->value), + ]); + } + + private static function reduceCharacterClass(\Phplrt\Parser\Context $ctx, mixed $children): mixed + { + $tokens = $children->children; + $close = \array_pop($tokens); + if ($close === null || $close->name !== 'T_CLASS_CLOSE') { + throw new RegexSyntaxException('Unclosed character class'); + } + + $nodes = []; + if ($children->id === self::T_NEGATIVE_CLASS_FC || $children->id === self::T_CLASS_FC) { + $nodes[] = new RegexNode('token', '_class', ']'); + } + + $literalNames = [ + 'T_CLASS_CHARACTER' => 'character', + 'T_CLASS_DYNAMIC_CHARACTER' => 'dynamic_character', + 'T_CLASS_CHARACTER_TYPE' => 'character_type', + 'T_CLASS_LITERAL' => 'literal', + ]; + + $count = \count($tokens); + $i = 0; + // ? ( | | range() ? | literal() )* ? + if ($i < $count && $tokens[$i]->name === 'T_RANGE') { + $nodes[] = new RegexNode('token', 'range', $tokens[$i]->value); + $i++; + } + while ($i < $count) { + $token = $tokens[$i]; + if ($token->name === 'T_POSIX_CLASS') { + $nodes[] = new RegexNode('token', 'posix_class', $token->value); + $i++; + continue; + } + if ($token->name === 'T_CLASS_NESTED') { + $nodes[] = new RegexNode('token', 'class_', $token->value); + $i++; + continue; + } + if (!isset($literalNames[$token->name])) { + break; + } + $literal = new RegexNode('token', $literalNames[$token->name], $token->value); + if ( + $i + 2 < $count + && $tokens[$i + 1]->name === 'T_RANGE' + && isset($literalNames[$tokens[$i + 2]->name]) + ) { + $nodes[] = new RegexNode('#range', children: [ + $literal, + new RegexNode('token', $literalNames[$tokens[$i + 2]->name], $tokens[$i + 2]->value), + ]); + $i += 3; + if ($i < $count && $tokens[$i]->name === 'T_RANGE') { + $nodes[] = new RegexNode('token', 'range', $tokens[$i]->value); + $i++; + } + continue; + } + $nodes[] = $literal; + $i++; + } + if ($i < $count && $tokens[$i]->name === 'T_RANGE') { + $nodes[] = new RegexNode('token', 'range', $tokens[$i]->value); + $i++; + } + if ($i !== $count) { + throw new RegexSyntaxException('Unexpected token in character class'); + } + + $negative = $children->id === self::T_NEGATIVE_CLASS_FC || $children->id === self::T_NEGATIVE_CLASS; + return new RegexNode($negative ? '#negativeclass' : '#class', children: $nodes); + } + + private static function reduceCapturing(\Phplrt\Parser\Context $ctx, mixed $children): mixed + { + if (!\is_array($children)) { + $children = [$children]; + } + $opening = \array_shift($children); + switch ($opening->id) { + case self::T_MARKER: + return new RegexNode('#mark', children: [ + new RegexNode('token', 'name', $opening->captures[0]), + ]); + case self::T_COMMENT: + return new RegexNode('#comment', children: $opening->captures[0] === '' ? [] : [ + new RegexNode('token', 'comment', $opening->captures[0]), + ]); + case self::T_NAMED_CAPTURING: + \array_unshift($children, new RegexNode('token', 'capturing_name', $opening->captures[0])); + return new RegexNode('#namedcapturing', children: $children); + case self::T_NON_CAPTURING: + return new RegexNode('#noncapturing', children: $children); + case self::T_NON_CAPTURING_INTERNAL_OPTION: + \array_unshift($children, new RegexNode('token', 'non_capturing_internal_option', $opening->value)); + return new RegexNode('#noncapturing', children: $children); + case self::T_NON_CAPTURING_RESET: + return new RegexNode('#noncapturingreset', children: $children); + case self::T_ATOMIC_GROUP: + return new RegexNode('#atomicgroup', children: $children); + default: + return new RegexNode('#capturing', children: $children); + } + } + + private static function reduceLiteral(\Phplrt\Parser\Context $ctx, mixed $children): mixed + { + return new RegexNode('token', \strtolower(\substr($children->name, 2)), $children->value); + } + + private static function reduceCondition(\Phplrt\Parser\Context $ctx, mixed $children): mixed + { + $reference = \array_shift($children); + switch ($reference->id) { + case self::T_NAMED_REFERENCE: + \array_unshift($children, new RegexNode('token', 'capturing_name', $reference->captures[0])); + return new RegexNode('#namedcondition', children: $children); + case self::T_ABSOLUTE_REFERENCE: + \array_unshift($children, new RegexNode('token', 'index', $reference->captures[0])); + return new RegexNode('#absolutecondition', children: $children); + case self::T_RELATIVE_REFERENCE: + \array_unshift($children, new RegexNode('token', 'index', $reference->captures[0])); + return new RegexNode('#relativecondition', children: $children); + default: + return new RegexNode('#assertioncondition', children: $children); + } + } +} diff --git a/src/Type/Regex/Parser/RegexSyntaxException.php b/src/Type/Regex/Parser/RegexSyntaxException.php new file mode 100644 index 00000000000..be3012dccd4 --- /dev/null +++ b/src/Type/Regex/Parser/RegexSyntaxException.php @@ -0,0 +1,11 @@ + */ + /** @var array */ private static array $parsedAst = []; public function __construct( @@ -63,8 +63,7 @@ public function __construct( public function parseGroups(string $regex): ?RegexAstWalkResult { - /** @throws void */ - self::$parser ??= Llk::load(new Read(__DIR__ . '/../../../resources/RegexGrammar.pp')); + self::$parser ??= new RegexParser(); if (array_key_exists($regex, self::$parsedAst)) { $ast = self::$parsedAst[$regex]; @@ -96,10 +95,14 @@ public function parseGroups(string $regex): ?RegexAstWalkResult $rawRegex = $this->regexExpressionHelper->removeDelimitersAndModifiers($regex); try { - $ast = self::$parsedAst[$regex] = self::$parser->parse($rawRegex); - } catch (Exception) { + $ast = self::$parser->parse(StringSource::createFromString($rawRegex)); + } catch (ParserExceptionInterface | RegexSyntaxException) { return self::$parsedAst[$regex] = null; } + if (!$ast instanceof RegexNode) { + throw new ShouldNotHappenException(); + } + self::$parsedAst[$regex] = $ast; } $this->updateAlternationAstRemoveVerticalBarsAndAddEmptyToken($ast); @@ -145,12 +148,12 @@ public function parseGroups(string $regex): ?RegexAstWalkResult return $astWalkResult; } - private function createEmptyTokenTreeNode(TreeNode $parentAst): TreeNode + private function createEmptyTokenRegexNode(): RegexNode { - return new TreeNode('token', ['token' => 'literal', 'value' => '', 'namespace' => 'default'], parent: $parentAst); + return new RegexNode('token', 'literal', ''); } - private function updateAlternationAstRemoveVerticalBarsAndAddEmptyToken(TreeNode $ast): void + private function updateAlternationAstRemoveVerticalBarsAndAddEmptyToken(RegexNode $ast): void { $children = $ast->getChildren(); @@ -169,13 +172,13 @@ private function updateAlternationAstRemoveVerticalBarsAndAddEmptyToken(TreeNode continue; } - $children[$i] = $this->createEmptyTokenTreeNode($ast); + $children[$i] = $this->createEmptyTokenRegexNode(); } $ast->setChildren(array_values($children)); } - private function updateCapturingAstAddEmptyToken(TreeNode $ast): void + private function updateCapturingAstAddEmptyToken(RegexNode $ast): void { foreach ($ast->getChildren() as $child) { $this->updateCapturingAstAddEmptyToken($child); @@ -185,12 +188,10 @@ private function updateCapturingAstAddEmptyToken(TreeNode $ast): void return; } - $emptyAlternationAst = new TreeNode('#alternation', parent: $ast); - $emptyAlternationAst->setChildren([$this->createEmptyTokenTreeNode($emptyAlternationAst)]); - $ast->setChildren([$emptyAlternationAst]); + $ast->setChildren([new RegexNode('#alternation', children: [$this->createEmptyTokenRegexNode()])]); } - private function containsEscapeK(TreeNode $ast): bool + private function containsEscapeK(RegexNode $ast): bool { if ($ast->getId() === 'token' && $ast->getValueToken() === 'match_point_reset') { return true; @@ -206,7 +207,7 @@ private function containsEscapeK(TreeNode $ast): bool } private function walkRegexAst( - TreeNode $ast, + RegexNode $ast, ?RegexAlternation $alternation, int $combinationIndex, bool $inOptionalQuantification, @@ -349,7 +350,7 @@ private function allowConstantTypes( } /** @return array{?int, ?int} */ - private function getQuantificationRange(TreeNode $node): array + private function getQuantificationRange(RegexNode $node): array { if ($node->getId() !== '#quantification') { throw new ShouldNotHappenException(); @@ -358,12 +359,12 @@ private function getQuantificationRange(TreeNode $node): array $min = null; $max = null; - $lastChild = $node->getChild($node->getChildrenNumber() - 1); - $value = $lastChild->getValue(); + $children = $node->getChildren(); + $lastChild = $children[count($children) - 1]; // normalize away possessive and lazy quantifier-modifiers - $token = str_replace(['_possessive', '_lazy'], '', $value['token']); - $value = rtrim($value['value'], '+?'); + $token = str_replace(['_possessive', '_lazy'], '', $lastChild->getValueToken()); + $value = rtrim($lastChild->getValueValue(), '+?'); if ($token === 'n_to_m') { if (sscanf($value, '{%d,%d}', $n, $m) !== 2 || !is_int($n) || !is_int($m)) { @@ -397,7 +398,7 @@ private function getQuantificationRange(TreeNode $node): array return [$min, $max]; } - private function createGroupType(TreeNode $group, bool $maybeConstant, string $patternModifiers): Type + private function createGroupType(RegexNode $group, bool $maybeConstant, string $patternModifiers): Type { $rootAlternation = $this->getRootAlternation($group); if ($rootAlternation !== null) { @@ -448,7 +449,7 @@ private function createGroupType(TreeNode $group, bool $maybeConstant, string $p return new StringType(); } - private function getRootAlternation(TreeNode $group): ?TreeNode + private function getRootAlternation(RegexNode $group): ?RegexNode { if ( $group->getId() === '#capturing' @@ -471,7 +472,7 @@ private function getRootAlternation(TreeNode $group): ?TreeNode } private function walkGroupAst( - TreeNode $ast, + RegexNode $ast, bool $inClass, string $patternModifiers, RegexGroupWalkResult $walkResult, @@ -652,7 +653,7 @@ private function walkGroupAst( * * @return array|null */ - private function getQuantifiedLiterals(TreeNode $ast, ?int $min, ?int $max, bool $inClass, string $patternModifiers, RegexGroupWalkResult $walkResult): ?array + private function getQuantifiedLiterals(RegexNode $ast, ?int $min, ?int $max, bool $inClass, string $patternModifiers, RegexGroupWalkResult $walkResult): ?array { $prefixLiterals = $walkResult->getOnlyLiterals(); if ($prefixLiterals === null || $min === null || $max === null) { @@ -738,7 +739,7 @@ private function repeatLiterals(array $literals, int $min, int $max): ?array return array_values($collected); } - private function isMaybeEmptyNode(TreeNode $node, string $patternModifiers, bool &$isNonFalsy): bool + private function isMaybeEmptyNode(RegexNode $node, string $patternModifiers, bool &$isNonFalsy): bool { if ($node->getId() === '#quantification') { [$min] = $this->getQuantificationRange($node); @@ -792,7 +793,7 @@ private function isMaybeEmptyNode(TreeNode $node, string $patternModifiers, bool /** * @param array|null $onlyLiterals */ - private function getLiteralValue(TreeNode $node, ?array &$onlyLiterals, bool $appendLiterals, string $patternModifiers, bool $inCharacterClass): ?string + private function getLiteralValue(RegexNode $node, ?array &$onlyLiterals, bool $appendLiterals, string $patternModifiers, bool $inCharacterClass): ?string { if ($node->getId() !== 'token') { return null; diff --git a/tests/PHPStan/Command/IgnoredRegexValidatorTest.php b/tests/PHPStan/Command/IgnoredRegexValidatorTest.php index 2b64d9e534e..71c58e04a42 100644 --- a/tests/PHPStan/Command/IgnoredRegexValidatorTest.php +++ b/tests/PHPStan/Command/IgnoredRegexValidatorTest.php @@ -2,10 +2,9 @@ namespace PHPStan\Command; -use Hoa\Compiler\Llk\Llk; -use Hoa\File\Read; use PHPStan\PhpDoc\TypeStringResolver; use PHPStan\Testing\PHPStanTestCase; +use PHPStan\Type\Regex\Parser\RegexParser; use PHPUnit\Framework\Attributes\DataProvider; class IgnoredRegexValidatorTest extends PHPStanTestCase @@ -163,9 +162,7 @@ public function testValidate( bool $expectAllErrorsIgnored, ): void { - $grammar = new Read(__DIR__ . '/../../../resources/RegexGrammar.pp'); - $parser = Llk::load($grammar); - $validator = new IgnoredRegexValidator($parser, self::getContainer()->getByType(TypeStringResolver::class)); + $validator = new IgnoredRegexValidator(new RegexParser(), self::getContainer()->getByType(TypeStringResolver::class)); $result = $validator->validate($regex); $this->assertSame($expectedTypes, $result->getIgnoredTypes()); diff --git a/tests/PHPStan/Composer/AutoloadFilesTest.php b/tests/PHPStan/Composer/AutoloadFilesTest.php index 606d930370b..ac1bf405028 100644 --- a/tests/PHPStan/Composer/AutoloadFilesTest.php +++ b/tests/PHPStan/Composer/AutoloadFilesTest.php @@ -55,8 +55,6 @@ public function testExpectedFiles(): void $expectedFiles = [ 'guzzlehttp/guzzle/src/functions_include.php', // added to phpstan-dist/bootstrap.php - 'hoa/consistency/Prelude.php', // Hoa isn't prefixed, no need to load this eagerly - 'hoa/protocol/Wrapper.php', // Hoa isn't prefixed, no need to load this eagerly 'jetbrains/phpstorm-stubs/PhpStormStubsMap.php', // added to phpstan-dist/bootstrap.php 'myclabs/deep-copy/src/DeepCopy/deep_copy.php', // dev dependency of PHPUnit 'react/promise/src/functions_include.php', // added to phpstan-dist/bootstrap.php From 497618cba012be3317be969562d95f625dc669ec Mon Sep 17 00:00:00 2001 From: "ondrej@mirtes.cz" Date: Mon, 26 Oct 2026 11:00:37 +0000 Subject: [PATCH 02/11] Unmatched optional group followed by a matched one is '' in preg_match_all() with PREG_SET_ORDER Amp-Thread-ID: https://ampcode.com/threads/T-01a177b5-960f-71d2-a3e2-fa7649f39d2c --- src/Type/Php/RegexArrayShapeMatcher.php | 3 ++- .../Analyser/nsrt/preg_match_all_shapes.php | 17 +++++++++++++++++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/src/Type/Php/RegexArrayShapeMatcher.php b/src/Type/Php/RegexArrayShapeMatcher.php index b26810cd8a7..4e560251562 100644 --- a/src/Type/Php/RegexArrayShapeMatcher.php +++ b/src/Type/Php/RegexArrayShapeMatcher.php @@ -407,7 +407,8 @@ private function createGroupValueType(RegexCapturingGroup $captureGroup, Trinary $this->containsSetOrder($flags) && !$this->containsUnmatchedAsNull($flags, $matchesAll) && $captureGroup->isOptional() - && !$isTrailingOptional + // an unmatched group followed by a matched one is reported as '' + && !$isLastGroup ) ) { $groupValueType = $this->getValueType( diff --git a/tests/PHPStan/Analyser/nsrt/preg_match_all_shapes.php b/tests/PHPStan/Analyser/nsrt/preg_match_all_shapes.php index 243675faf58..2d26eb7f022 100644 --- a/tests/PHPStan/Analyser/nsrt/preg_match_all_shapes.php +++ b/tests/PHPStan/Analyser/nsrt/preg_match_all_shapes.php @@ -198,3 +198,20 @@ function bug11661(): void { assertType("list", $results); } + +function setOrderOptionalGroupFollowedByMatchedGroup(string $s): void { + // from php-src ext/pcre/tests/bug61780_1.phpt + if (preg_match_all('/(4)?(2)?\d/', $s, $matches, PREG_SET_ORDER)) { + assertType("non-empty-list", $matches); + } + + // from php-src ext/pcre/tests/bug61780_2.phpt + if (preg_match_all('/(?4)?(?2)?\d/', $s, $matches, PREG_SET_ORDER)) { + assertType("non-empty-list", $matches); + } + + // from php-src ext/pcre/tests/preg_replace_callback_flags.phpt + if (preg_match_all('/(a)|(b)/', $s, $matches, PREG_SET_ORDER)) { + assertType("non-empty-list", $matches); + } +} From 5aeee3b5c6f9f7fb745e7be58579ffcd03df2acc Mon Sep 17 00:00:00 2001 From: "ondrej@mirtes.cz" Date: Mon, 26 Oct 2026 11:01:12 +0000 Subject: [PATCH 03/11] MARK in preg_match_all() with PREG_PATTERN_ORDER is an array of marks keyed by match index Amp-Thread-ID: https://ampcode.com/threads/T-01a177b5-960f-71d2-a3e2-fa7649f39d2c --- src/Type/Php/RegexArrayShapeMatcher.php | 11 ++++++++++- tests/PHPStan/Analyser/nsrt/preg_match_all_shapes.php | 11 +++++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/src/Type/Php/RegexArrayShapeMatcher.php b/src/Type/Php/RegexArrayShapeMatcher.php index 4e560251562..dfb2b0c6361 100644 --- a/src/Type/Php/RegexArrayShapeMatcher.php +++ b/src/Type/Php/RegexArrayShapeMatcher.php @@ -310,9 +310,18 @@ private function buildArrayType( foreach ($markVerbs as $mark) { $markTypes[] = new ConstantStringType($mark); } + $markType = TypeCombinator::union(...$markTypes); + if ($matchesAll && $this->containsPatternOrder($flags)) { + // marks are keyed by the index of the match that set them, matches without a mark are left out + $markType = new IntersectionType([ + new ArrayType(IntegerRangeType::createAllGreaterThanOrEqualTo(0), $markType), + new NonEmptyArrayType(), + ]); + } + $builder->setOffsetValueType( $this->getKeyType('MARK'), - TypeCombinator::union(...$markTypes), + $markType, true, ); } diff --git a/tests/PHPStan/Analyser/nsrt/preg_match_all_shapes.php b/tests/PHPStan/Analyser/nsrt/preg_match_all_shapes.php index 2d26eb7f022..cdda4c47dd3 100644 --- a/tests/PHPStan/Analyser/nsrt/preg_match_all_shapes.php +++ b/tests/PHPStan/Analyser/nsrt/preg_match_all_shapes.php @@ -215,3 +215,14 @@ function setOrderOptionalGroupFollowedByMatchedGroup(string $s): void { assertType("non-empty-list", $matches); } } + +function markVerbs(string $s): void { + // from PCRE2 testdata/testinput1 + if (preg_match_all('/A(*:A)B|XX(*:B)Y/', $s, $matches)) { + assertType("array{0: non-empty-list, MARK?: non-empty-array, 'A'|'B'>}", $matches); + } + + if (preg_match_all('/A(*:A)B|XX(*:B)Y/', $s, $matches, PREG_SET_ORDER)) { + assertType("non-empty-list", $matches); + } +} From 23baa7aeed37d4955dbf3c56ef079e8889a9dd17 Mon Sep 17 00:00:00 2001 From: "ondrej@mirtes.cz" Date: Mon, 26 Oct 2026 11:03:26 +0000 Subject: [PATCH 04/11] Treat regex assertions as zero-width and groups in negative assertions as never set Amp-Thread-ID: https://ampcode.com/threads/T-01a177b5-960f-71d2-a3e2-fa7649f39d2c --- src/Type/Regex/RegexGroupParser.php | 22 ++++++++++++++ .../Analyser/nsrt/preg_match_shapes.php | 29 +++++++++++++++++++ 2 files changed, 51 insertions(+) diff --git a/src/Type/Regex/RegexGroupParser.php b/src/Type/Regex/RegexGroupParser.php index 51fecf87e3f..b08bf6ffebe 100644 --- a/src/Type/Regex/RegexGroupParser.php +++ b/src/Type/Regex/RegexGroupParser.php @@ -49,6 +49,10 @@ final class RegexGroupParser // to avoid combinatorial explosion from nested optional/bounded quantifications private const LITERALS_LIMIT = 100; + private const NEGATIVE_ASSERTIONS = ['#negativelookahead', '#negativelookbehind']; + + private const ASSERTIONS = ['#lookahead', '#lookbehind', '#negativelookahead', '#negativelookbehind']; + private static ?RegexParser $parser = null; /** @var array */ @@ -268,6 +272,15 @@ private function walkRegexAst( true, ); $parentGroup = $group; + } elseif (in_array($ast->getId(), self::NEGATIVE_ASSERTIONS, true)) { + // capturing groups in a negative assertion are never set when the whole pattern matches + $group = new RegexNonCapturingGroup( + $alternation, + true, + $parentGroup, + false, + ); + $parentGroup = $group; } $inOptionalQuantification = false; @@ -478,6 +491,11 @@ private function walkGroupAst( RegexGroupWalkResult $walkResult, ): RegexGroupWalkResult { + if (in_array($ast->getId(), self::ASSERTIONS, true)) { + // assertions are zero-width, they do not add anything to the matched string + return $walkResult; + } + $children = $ast->getChildren(); $quantifiedLiterals = null; @@ -741,6 +759,10 @@ private function repeatLiterals(array $literals, int $min, int $max): ?array private function isMaybeEmptyNode(RegexNode $node, string $patternModifiers, bool &$isNonFalsy): bool { + if (in_array($node->getId(), self::ASSERTIONS, true)) { + return true; + } + if ($node->getId() === '#quantification') { [$min] = $this->getQuantificationRange($node); diff --git a/tests/PHPStan/Analyser/nsrt/preg_match_shapes.php b/tests/PHPStan/Analyser/nsrt/preg_match_shapes.php index 91620523cad..5e84fd14780 100644 --- a/tests/PHPStan/Analyser/nsrt/preg_match_shapes.php +++ b/tests/PHPStan/Analyser/nsrt/preg_match_shapes.php @@ -1088,3 +1088,32 @@ function testExtendedSyntaxEscapedHash(string $string): void { assertType('array{non-falsy-string, numeric-string, non-empty-string}', $matches); } } + +function testAssertionsAreZeroWidth(string $s): void { + // from php-src ext/pcre/tests/bug66121.phpt + if (preg_match('/(? Date: Mon, 26 Oct 2026 11:06:36 +0000 Subject: [PATCH 05/11] Do not infer preg_match() shapes for duplicate group names outside of separate alternation branches Amp-Thread-ID: https://ampcode.com/threads/T-01a177b5-960f-71d2-a3e2-fa7649f39d2c --- src/Type/Php/RegexArrayShapeMatcher.php | 10 ++++ src/Type/Regex/RegexGroupList.php | 48 +++++++++++++++++++ .../Analyser/nsrt/preg_match_shapes.php | 17 +++++++ 3 files changed, 75 insertions(+) diff --git a/src/Type/Php/RegexArrayShapeMatcher.php b/src/Type/Php/RegexArrayShapeMatcher.php index dfb2b0c6361..07286700fd9 100644 --- a/src/Type/Php/RegexArrayShapeMatcher.php +++ b/src/Type/Php/RegexArrayShapeMatcher.php @@ -149,6 +149,16 @@ private function matchRegex(string $regex, ?int $flags, TrinaryLogic $wasMatched $onlyTopLevelAlternation = $regexGroupList->getOnlyTopLevelAlternation(); $flags ??= 0; + if ($regexGroupList->hasDuplicateNamesOutsideOfAlternationBranches( + !$matchesAll && $onlyOptionalTopLevelGroup === null && !$this->containsUnmatchedAsNull($flags, $matchesAll) + ? $onlyTopLevelAlternation + : null, + )) { + // which of the groups sharing a name sets the named offset is only modelled + // for groups in different branches of a top level alternation + return null; + } + if ( !$matchesAll && $wasMatched->yes() diff --git a/src/Type/Regex/RegexGroupList.php b/src/Type/Regex/RegexGroupList.php index 539055c803b..6150d3e1712 100644 --- a/src/Type/Regex/RegexGroupList.php +++ b/src/Type/Regex/RegexGroupList.php @@ -8,6 +8,7 @@ use Override; use PHPStan\ShouldNotHappenException; use PHPStan\Type\Type; +use function array_key_exists; use function array_reverse; use function count; @@ -130,6 +131,53 @@ public function getOnlyOptionalTopLevelGroup(): ?RegexCapturingGroup return $group; } + /** + * Groups sharing a name (allowed by (?J)) are only understood + * when each of them is in a different branch of the given alternation. + */ + public function hasDuplicateNamesOutsideOfAlternationBranches(?RegexAlternation $alternation): bool + { + $groupIdsByName = []; + foreach ($this->groups as $captureGroup) { + if (!$captureGroup->isNamed()) { + continue; + } + + $groupIdsByName[$captureGroup->getName()][] = $captureGroup->getId(); + } + + $branchByGroupId = []; + if ($alternation !== null) { + foreach ($alternation->getGroupCombinations() as $branch => $groupIds) { + foreach ($groupIds as $groupId) { + $branchByGroupId[$groupId] = $branch; + } + } + } + + foreach ($groupIdsByName as $groupIds) { + if (count($groupIds) === 1) { + continue; + } + + $branches = []; + foreach ($groupIds as $groupId) { + if (!array_key_exists($groupId, $branchByGroupId)) { + return true; + } + + $branch = $branchByGroupId[$groupId]; + if (array_key_exists($branch, $branches)) { + return true; + } + + $branches[$branch] = true; + } + } + + return false; + } + public function getOnlyTopLevelAlternation(): ?RegexAlternation { $alternation = null; diff --git a/tests/PHPStan/Analyser/nsrt/preg_match_shapes.php b/tests/PHPStan/Analyser/nsrt/preg_match_shapes.php index 5e84fd14780..9edd58dc18c 100644 --- a/tests/PHPStan/Analyser/nsrt/preg_match_shapes.php +++ b/tests/PHPStan/Analyser/nsrt/preg_match_shapes.php @@ -1117,3 +1117,20 @@ function testGroupsInNegativeAssertionsAreNeverSet(string $s): void { assertType("array{0: non-falsy-string, 1?: 'c'|'d'}", $matches); } } + +function testDuplicateNamesInternalOption(string $s): void { + // from php-src ext/pcre/tests/bug79257.phpt + if (preg_match('/(?J)(?:(?foo)|(?bar))(?baz)/', $s, $matches)) { + assertType('array', $matches); + } + + // from php-src ext/pcre/tests/bug79257.phpt + if (preg_match('/(?J)(?:(?foo)|(?bar))/', $s, $matches, PREG_UNMATCHED_AS_NULL)) { + assertType('array', $matches); + } + + // from php-src ext/pcre/tests/bug79257.phpt + if (preg_match_all('/(?J)(?[ac])(?\d)|(?[b])/', $s, $matches, PREG_SET_ORDER)) { + assertType('list>', $matches); + } +} From edbfe6303709d32cb2e96ecdda96f2aee01ce54b Mon Sep 17 00:00:00 2001 From: "ondrej@mirtes.cz" Date: Mon, 26 Oct 2026 11:13:58 +0000 Subject: [PATCH 06/11] Quantified atom that can match empty string does not make the match non-empty Amp-Thread-ID: https://ampcode.com/threads/T-01a177b5-960f-71d2-a3e2-fa7649f39d2c --- src/Type/Regex/RegexGroupParser.php | 7 +++++-- .../PHPStan/Analyser/nsrt/preg_match_shapes.php | 17 +++++++++++++++++ 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/src/Type/Regex/RegexGroupParser.php b/src/Type/Regex/RegexGroupParser.php index b08bf6ffebe..4a184decf63 100644 --- a/src/Type/Regex/RegexGroupParser.php +++ b/src/Type/Regex/RegexGroupParser.php @@ -537,7 +537,9 @@ private function walkGroupAst( $walkResult = $walkResult->inOptionalQuantification(true); } - if (!$walkResult->isInOptionalQuantification()) { + // repeating an atom that might match an empty string might match an empty string too + $atomNonFalsy = false; + if (!$walkResult->isInOptionalQuantification() && !$this->isMaybeEmptyNode($children[0], $patternModifiers, $atomNonFalsy)) { if ($min >= 1) { $walkResult = $walkResult->nonEmpty(TrinaryLogic::createYes()); } @@ -767,7 +769,8 @@ private function isMaybeEmptyNode(RegexNode $node, string $patternModifiers, boo [$min] = $this->getQuantificationRange($node); if ($min > 0) { - return false; + $atomNonFalsy = false; + return $this->isMaybeEmptyNode($node->getChildren()[0], $patternModifiers, $atomNonFalsy); } if ($min === 0) { diff --git a/tests/PHPStan/Analyser/nsrt/preg_match_shapes.php b/tests/PHPStan/Analyser/nsrt/preg_match_shapes.php index 9edd58dc18c..ab633fa4662 100644 --- a/tests/PHPStan/Analyser/nsrt/preg_match_shapes.php +++ b/tests/PHPStan/Analyser/nsrt/preg_match_shapes.php @@ -1134,3 +1134,20 @@ function testDuplicateNamesInternalOption(string $s): void { assertType('list>', $matches); } } + +function testQuantifiedMaybeEmptyAtom(string $s): void { + // from PCRE2 testdata/testinput6 + if (preg_match('/(abc|)+/', $s, $matches)) { + assertType("array{string, string}", $matches); + } + + // from PCRE2 testdata/testinput6 + if (preg_match('/^(){3,5}/', $s, $matches)) { + assertType("array{string, string}", $matches); + } + + // from PCRE2 testdata/testinput1 + if (preg_match('/((a|)+)+Z/', $s, $matches)) { + assertType("array{non-falsy-string, string, string}", $matches); + } +} From f9e4768bde79ee20b605e690d34cc355a851d8fc Mon Sep 17 00:00:00 2001 From: "ondrej@mirtes.cz" Date: Mon, 26 Oct 2026 11:17:41 +0000 Subject: [PATCH 07/11] Parse backreferences and subroutine calls in regexes, do not infer shapes for patterns with subroutine calls Amp-Thread-ID: https://ampcode.com/threads/T-01a177b5-960f-71d2-a3e2-fa7649f39d2c --- build/regex-parser/RegexGrammar.pp3 | 19 +- src/Type/Regex/Parser/RegexParser.php | 311 ++++++++++++------ src/Type/Regex/RegexGroupParser.php | 24 ++ .../Analyser/nsrt/preg_match_shapes.php | 17 + 4 files changed, 263 insertions(+), 108 deletions(-) diff --git a/build/regex-parser/RegexGrammar.pp3 b/build/regex-parser/RegexGrammar.pp3 index 64adaf69a93..4b9f58b237a 100644 --- a/build/regex-parser/RegexGrammar.pp3 +++ b/build/regex-parser/RegexGrammar.pp3 @@ -84,6 +84,10 @@ // Marker verbs. %token T_MARKER \(\*:([^)]+)\) +// Backreferences and subroutine calls written as groups: (?P=name), (?R), (?1), (?-1), (?&name), (?P>name). +%token T_NAMED_BACKREFERENCE \(\?P=\w+\) +%token T_SUBROUTINE \(\?(?:R|[+-]?\d+|&\w+|P>\w+)\) + // Groups. %token T_NAMED_CAPTURING \(\?P?<(.+?)(? %token T_NON_CAPTURING \(\?: @@ -120,6 +124,10 @@ %token T_CHARACTER_TYPE \\(?:[CdDhHNRsSvVwWX]|[pP]{[^}]+}) %token T_ANCHOR \\[bBAZzG]|\^|\$ %token T_MATCH_POINT_RESET \\K +// Subroutine calls \g, \g'1' and backreferences \1, \g1, \g{-1}, \g{name}, \k, \k'name', \k{name}. +// Octal escapes like \012 are matched as dynamic characters above. +%token T_SUBROUTINE_G \\g(?:<[^>]+>|'[^']+') +%token T_BACKREFERENCE \\(?:[1-9]\d*|g[+-]?\d+|g\{[+-]?\w+\}|k<\w+>|k'\w+'|k\{\w+\}) %token T_LITERAL \\.|.|\n Expression -> { @@ -354,12 +362,21 @@ Capturing -> { ; Literal -> { - return new RegexNode('token', \strtolower(\substr($children->name, 2)), $children->value); + $names = [ + self::T_NAMED_BACKREFERENCE => 'backreference', + self::T_SUBROUTINE_G => 'subroutine', + ]; + + return new RegexNode('token', $names[$children->id] ?? \strtolower(\substr($children->name, 2)), $children->value); } : | | | | + | + | + | + | | ; diff --git a/src/Type/Regex/Parser/RegexParser.php b/src/Type/Regex/Parser/RegexParser.php index 715f82928c4..533b93dcd14 100644 --- a/src/Type/Regex/Parser/RegexParser.php +++ b/src/Type/Regex/Parser/RegexParser.php @@ -50,65 +50,73 @@ final class RegexParser implements \Phplrt\Contracts\Parser\ParserInterface /** @var int */ public const T_MARKER = 14; /** @var int */ - public const T_NAMED_CAPTURING = 15; + public const T_NAMED_BACKREFERENCE = 15; /** @var int */ - public const T_NON_CAPTURING = 16; + public const T_SUBROUTINE = 16; /** @var int */ - public const T_NON_CAPTURING_INTERNAL_OPTION = 17; + public const T_NAMED_CAPTURING = 17; /** @var int */ - public const T_NON_CAPTURING_RESET = 18; + public const T_NON_CAPTURING = 18; /** @var int */ - public const T_ATOMIC_GROUP = 19; + public const T_NON_CAPTURING_INTERNAL_OPTION = 19; /** @var int */ - public const T_CAPTURING = 20; + public const T_NON_CAPTURING_RESET = 20; /** @var int */ - public const T_CAPTURING_CLOSE = 21; + public const T_ATOMIC_GROUP = 21; /** @var int */ - public const T_ZERO_OR_ONE_POSSESSIVE = 22; + public const T_CAPTURING = 22; /** @var int */ - public const T_ZERO_OR_ONE_LAZY = 23; + public const T_CAPTURING_CLOSE = 23; /** @var int */ - public const T_ZERO_OR_ONE = 24; + public const T_ZERO_OR_ONE_POSSESSIVE = 24; /** @var int */ - public const T_ZERO_OR_MORE_POSSESSIVE = 25; + public const T_ZERO_OR_ONE_LAZY = 25; /** @var int */ - public const T_ZERO_OR_MORE_LAZY = 26; + public const T_ZERO_OR_ONE = 26; /** @var int */ - public const T_ZERO_OR_MORE = 27; + public const T_ZERO_OR_MORE_POSSESSIVE = 27; /** @var int */ - public const T_ONE_OR_MORE_POSSESSIVE = 28; + public const T_ZERO_OR_MORE_LAZY = 28; /** @var int */ - public const T_ONE_OR_MORE_LAZY = 29; + public const T_ZERO_OR_MORE = 29; /** @var int */ - public const T_ONE_OR_MORE = 30; + public const T_ONE_OR_MORE_POSSESSIVE = 30; /** @var int */ - public const T_EXACTLY_N = 31; + public const T_ONE_OR_MORE_LAZY = 31; /** @var int */ - public const T_N_TO_M_POSSESSIVE = 32; + public const T_ONE_OR_MORE = 32; /** @var int */ - public const T_N_TO_M_LAZY = 33; + public const T_EXACTLY_N = 33; /** @var int */ - public const T_N_TO_M = 34; + public const T_N_TO_M_POSSESSIVE = 34; /** @var int */ - public const T_N_OR_MORE_POSSESSIVE = 35; + public const T_N_TO_M_LAZY = 35; /** @var int */ - public const T_N_OR_MORE_LAZY = 36; + public const T_N_TO_M = 36; /** @var int */ - public const T_N_OR_MORE = 37; + public const T_N_OR_MORE_POSSESSIVE = 37; /** @var int */ - public const T_ALTERNATION = 38; + public const T_N_OR_MORE_LAZY = 38; /** @var int */ - public const T_CHARACTER = 39; + public const T_N_OR_MORE = 39; /** @var int */ - public const T_DYNAMIC_CHARACTER = 40; + public const T_ALTERNATION = 40; /** @var int */ - public const T_CHARACTER_TYPE = 41; + public const T_CHARACTER = 41; /** @var int */ - public const T_ANCHOR = 42; + public const T_DYNAMIC_CHARACTER = 42; /** @var int */ - public const T_MATCH_POINT_RESET = 43; + public const T_CHARACTER_TYPE = 43; /** @var int */ - public const T_LITERAL = 44; + public const T_ANCHOR = 44; + /** @var int */ + public const T_MATCH_POINT_RESET = 45; + /** @var int */ + public const T_SUBROUTINE_G = 46; + /** @var int */ + public const T_BACKREFERENCE = 47; + /** @var int */ + public const T_LITERAL = 48; /** @var array */ private const PARSER_EXPECTATIONS = [ @@ -127,6 +135,8 @@ final class RegexParser implements \Phplrt\Contracts\Parser\ParserInterface 'T_ASSERTION_REFERENCE', 'T_COMMENT', 'T_MARKER', + 'T_NAMED_BACKREFERENCE', + 'T_SUBROUTINE', 'T_NAMED_CAPTURING', 'T_NON_CAPTURING', 'T_NON_CAPTURING_INTERNAL_OPTION', @@ -156,6 +166,8 @@ final class RegexParser implements \Phplrt\Contracts\Parser\ParserInterface 'T_CHARACTER_TYPE', 'T_ANCHOR', 'T_MATCH_POINT_RESET', + 'T_SUBROUTINE_G', + 'T_BACKREFERENCE', 'T_LITERAL', '/[^\\s]++/', ]; @@ -218,6 +230,10 @@ final class RegexParser implements \Phplrt\Contracts\Parser\ParserInterface false, false, false, + false, + false, + false, + false, true, false, false, @@ -258,7 +274,11 @@ final class RegexParser implements \Phplrt\Contracts\Parser\ParserInterface true, true, true, - 39 => true, + true, + true, + 41 => true, + true, + true, true, true, true, @@ -287,7 +307,11 @@ final class RegexParser implements \Phplrt\Contracts\Parser\ParserInterface true, true, true, - 39 => true, + true, + true, + 41 => true, + true, + true, true, true, true, @@ -322,7 +346,7 @@ final class RegexParser implements \Phplrt\Contracts\Parser\ParserInterface 8 => true, ], [ - 21 => true, + 23 => true, ], [ true, @@ -337,7 +361,11 @@ final class RegexParser implements \Phplrt\Contracts\Parser\ParserInterface true, true, true, - 39 => true, + true, + true, + 41 => true, + true, + true, true, true, true, @@ -357,7 +385,11 @@ final class RegexParser implements \Phplrt\Contracts\Parser\ParserInterface true, true, true, - 39 => true, + true, + true, + 41 => true, + true, + true, true, true, true, @@ -385,7 +417,7 @@ final class RegexParser implements \Phplrt\Contracts\Parser\ParserInterface [ 13 => true, true, - true, + 17 => true, true, true, true, @@ -399,7 +431,7 @@ final class RegexParser implements \Phplrt\Contracts\Parser\ParserInterface 13 => true, ], [ - 15 => true, + 17 => true, true, true, true, @@ -407,19 +439,13 @@ final class RegexParser implements \Phplrt\Contracts\Parser\ParserInterface true, ], [ - 15 => true, + 17 => true, true, true, true, true, true, ], - [ - 15 => true, - ], - [ - 16 => true, - ], [ 17 => true, ], @@ -433,18 +459,22 @@ final class RegexParser implements \Phplrt\Contracts\Parser\ParserInterface 20 => true, ], [ - 39 => true, + 21 => true, + ], + [ + 22 => true, + ], + [ + 15 => true, + true, + 41 => true, + true, + true, true, true, true, true, true, - ], - [ - 39 => true, - ], - [ - 40 => true, ], [ 41 => true, @@ -458,9 +488,27 @@ final class RegexParser implements \Phplrt\Contracts\Parser\ParserInterface [ 44 => true, ], + [ + 45 => true, + ], + [ + 46 => true, + ], + [ + 47 => true, + ], + [ + 15 => true, + ], + [ + 16 => true, + ], + [ + 48 => true, + ], null, [ - 22 => true, + 24 => true, true, true, true, @@ -477,12 +525,6 @@ final class RegexParser implements \Phplrt\Contracts\Parser\ParserInterface true, true, ], - [ - 22 => true, - ], - [ - 23 => true, - ], [ 24 => true, ], @@ -525,6 +567,12 @@ final class RegexParser implements \Phplrt\Contracts\Parser\ParserInterface [ 37 => true, ], + [ + 38 => true, + ], + [ + 39 => true, + ], [ 9 => true, true, @@ -554,13 +602,13 @@ final class RegexParser implements \Phplrt\Contracts\Parser\ParserInterface ], null, [ - 38 => true, + 40 => true, ], [ - 38 => true, + 40 => true, ], [ - 38 => true, + 40 => true, ], ]; @@ -618,7 +666,19 @@ final class RegexParser implements \Phplrt\Contracts\Parser\ParserInterface [ 13, ], - 39 => [ + [ + 13, + ], + [ + 13, + ], + 41 => [ + 13, + ], + [ + 13, + ], + [ 13, ], [ @@ -637,16 +697,16 @@ final class RegexParser implements \Phplrt\Contracts\Parser\ParserInterface 13, ], 9 => [ - 56, + 60, ], 10 => [ - 56, + 60, ], 11 => [ - 56, + 60, ], 12 => [ - 56, + 60, ], ], 7 => [ @@ -682,7 +742,7 @@ final class RegexParser implements \Phplrt\Contracts\Parser\ParserInterface [ 20, ], - [ + 17 => [ 20, ], [ @@ -700,7 +760,19 @@ final class RegexParser implements \Phplrt\Contracts\Parser\ParserInterface [ 20, ], - 39 => [ + 15 => [ + 31, + ], + 16 => [ + 31, + ], + 41 => [ + 31, + ], + [ + 31, + ], + [ 31, ], [ @@ -740,7 +812,7 @@ final class RegexParser implements \Phplrt\Contracts\Parser\ParserInterface 13 => [ -23, ], - [ + 17 => [ 23, ], [ @@ -760,7 +832,7 @@ final class RegexParser implements \Phplrt\Contracts\Parser\ParserInterface ], ], 24 => [ - 15 => [ + 17 => [ -26, ], [ @@ -780,7 +852,7 @@ final class RegexParser implements \Phplrt\Contracts\Parser\ParserInterface ], ], 31 => [ - 39 => [ + 41 => [ -33, ], [ @@ -798,21 +870,21 @@ final class RegexParser implements \Phplrt\Contracts\Parser\ParserInterface [ -38, ], - ], - 39 => [ - 22 => [ - -41, - ], [ - -42, + -39, ], - [ - -43, + 15 => [ + -40, ], - [ - -44, + 16 => [ + -41, ], [ + -42, + ], + ], + 43 => [ + 24 => [ -45, ], [ @@ -848,19 +920,31 @@ final class RegexParser implements \Phplrt\Contracts\Parser\ParserInterface [ -56, ], - ], - 57 => [ - 9 => [ + [ + -57, + ], + [ + -58, + ], + [ -59, ], [ -60, ], + ], + 61 => [ + 9 => [ + -63, + ], [ - -61, + -64, ], [ - 61, + -65, + ], + [ + 65, ], ], ]; @@ -869,14 +953,14 @@ final class RegexParser implements \Phplrt\Contracts\Parser\ParserInterface private const PARSER_SEQUENCE_PREDICTION_TABLE = [ 1 => [ -4, - -65, + -69, ], 13 => [ 14, - -40, + -44, ], - 65 => [ - 66, + 69 => [ + 70, -4, ], ]; @@ -911,9 +995,9 @@ public function __construct() ); $this->lexer = new \Phplrt\Lexer\Lexer( - pattern: '/\\G(?|(?:(?:\\[\\^\\])(*MARK:0))|(?:(?:\\[\\])(*MARK:1))|(?:(?:\\[\\^)(*MARK:2))|(?:(?:\\[)(*MARK:3))|(?:(?:\\(\\?[imsxnJUX^]*-?[imsxnJUX^]+\\))(*MARK:4))|(?:(?:\\(\\?=)(*MARK:5))|(?:(?:\\(\\?!)(*MARK:6))|(?:(?:\\(\\?<=)(*MARK:7))|(?:(?:\\(\\?\\))(*MARK:9))|(?:(?:\\(\\?\\((\\d+)\\))(*MARK:10))|(?:(?:\\(\\?\\(([\\+\\-]\\d+)\\))(*MARK:11))|(?:(?:\\(\\?\\()(*MARK:12))|(?:(?:\\(\\?#(.*?)(?)(*MARK:15))|(?:(?:\\(\\?:)(*MARK:16))|(?:(?:\\(\\?[imsxnJUX^]*-?[imsxnJUX^]+:)(*MARK:17))|(?:(?:\\(\\?\\|)(*MARK:18))|(?:(?:\\(\\?>)(*MARK:19))|(?:(?:\\()(*MARK:20))|(?:(?:\\))(*MARK:21))|(?:(?:\\?\\+)(*MARK:22))|(?:(?:\\?\\?)(*MARK:23))|(?:(?:\\?)(*MARK:24))|(?:(?:\\*\\+)(*MARK:25))|(?:(?:\\*\\?)(*MARK:26))|(?:(?:\\*)(*MARK:27))|(?:(?:\\+\\+)(*MARK:28))|(?:(?:\\+\\?)(*MARK:29))|(?:(?:\\+)(*MARK:30))|(?:(?:\\{[0-9]+\\})(*MARK:31))|(?:(?:\\{[0-9]+,[0-9]+\\}\\+)(*MARK:32))|(?:(?:\\{[0-9]+,[0-9]+\\}\\?)(*MARK:33))|(?:(?:\\{[0-9]+,[0-9]+\\})(*MARK:34))|(?:(?:\\{[0-9]+,\\}\\+)(*MARK:35))|(?:(?:\\{[0-9]+,\\}\\?)(*MARK:36))|(?:(?:\\{[0-9]+,\\})(*MARK:37))|(?:(?:\\|)(*MARK:38))|(?:(?:\\\\(?:[aefnrt]|c[\\x00-\\x7f]))(*MARK:39))|(?:(?:\\\\(?:[0-7]{3}|x[0-9a-zA-Z]{2}|x{[0-9a-zA-Z]+}))(*MARK:40))|(?:(?:\\\\(?:[CdDhHNRsSvVwWX]|[pP]{[^}]+}))(*MARK:41))|(?:(?:\\\\[bBAZzG]|\\^|\\$)(*MARK:42))|(?:(?:\\\\K)(*MARK:43))|(?:(?:\\\\.|.|\\n)(*MARK:44))|(?:(?:[^\\s]++)(*MARK:45)))/Ssum', + pattern: '/\\G(?|(?:(?:\\[\\^\\])(*MARK:0))|(?:(?:\\[\\])(*MARK:1))|(?:(?:\\[\\^)(*MARK:2))|(?:(?:\\[)(*MARK:3))|(?:(?:\\(\\?[imsxnJUX^]*-?[imsxnJUX^]+\\))(*MARK:4))|(?:(?:\\(\\?=)(*MARK:5))|(?:(?:\\(\\?!)(*MARK:6))|(?:(?:\\(\\?<=)(*MARK:7))|(?:(?:\\(\\?\\))(*MARK:9))|(?:(?:\\(\\?\\((\\d+)\\))(*MARK:10))|(?:(?:\\(\\?\\(([\\+\\-]\\d+)\\))(*MARK:11))|(?:(?:\\(\\?\\()(*MARK:12))|(?:(?:\\(\\?#(.*?)(?\\w+)\\))(*MARK:16))|(?:(?:\\(\\?P?<(.+?)(?)(*MARK:17))|(?:(?:\\(\\?:)(*MARK:18))|(?:(?:\\(\\?[imsxnJUX^]*-?[imsxnJUX^]+:)(*MARK:19))|(?:(?:\\(\\?\\|)(*MARK:20))|(?:(?:\\(\\?>)(*MARK:21))|(?:(?:\\()(*MARK:22))|(?:(?:\\))(*MARK:23))|(?:(?:\\?\\+)(*MARK:24))|(?:(?:\\?\\?)(*MARK:25))|(?:(?:\\?)(*MARK:26))|(?:(?:\\*\\+)(*MARK:27))|(?:(?:\\*\\?)(*MARK:28))|(?:(?:\\*)(*MARK:29))|(?:(?:\\+\\+)(*MARK:30))|(?:(?:\\+\\?)(*MARK:31))|(?:(?:\\+)(*MARK:32))|(?:(?:\\{[0-9]+\\})(*MARK:33))|(?:(?:\\{[0-9]+,[0-9]+\\}\\+)(*MARK:34))|(?:(?:\\{[0-9]+,[0-9]+\\}\\?)(*MARK:35))|(?:(?:\\{[0-9]+,[0-9]+\\})(*MARK:36))|(?:(?:\\{[0-9]+,\\}\\+)(*MARK:37))|(?:(?:\\{[0-9]+,\\}\\?)(*MARK:38))|(?:(?:\\{[0-9]+,\\})(*MARK:39))|(?:(?:\\|)(*MARK:40))|(?:(?:\\\\(?:[aefnrt]|c[\\x00-\\x7f]))(*MARK:41))|(?:(?:\\\\(?:[0-7]{3}|x[0-9a-zA-Z]{2}|x{[0-9a-zA-Z]+}))(*MARK:42))|(?:(?:\\\\(?:[CdDhHNRsSvVwWX]|[pP]{[^}]+}))(*MARK:43))|(?:(?:\\\\[bBAZzG]|\\^|\\$)(*MARK:44))|(?:(?:\\\\K)(*MARK:45))|(?:(?:\\\\g(?:<[^>]+>|\'[^\']+\'))(*MARK:46))|(?:(?:\\\\(?:[1-9]\\d*|g[+-]?\\d+|g\\{[+-]?\\w+\\}|k<\\w+>|k\'\\w+\'|k\\{\\w+\\}))(*MARK:47))|(?:(?:\\\\.|.|\\n)(*MARK:48))|(?:(?:[^\\s]++)(*MARK:49)))/Ssum', channels: [ - 45 => 'Unknown', + 49 => 'Unknown', ], names: [ 'T_NEGATIVE_CLASS_FC', @@ -931,6 +1015,8 @@ public function __construct() 'T_ASSERTION_REFERENCE', 'T_COMMENT', 'T_MARKER', + 'T_NAMED_BACKREFERENCE', + 'T_SUBROUTINE', 'T_NAMED_CAPTURING', 'T_NON_CAPTURING', 'T_NON_CAPTURING_INTERNAL_OPTION', @@ -960,6 +1046,8 @@ public function __construct() 'T_CHARACTER_TYPE', 'T_ANCHOR', 'T_MATCH_POINT_RESET', + 'T_SUBROUTINE_G', + 'T_BACKREFERENCE', 'T_LITERAL', ], transitions: [ @@ -974,7 +1062,7 @@ public function __construct() 1, 13 => 1, 1, - 1, + 17 => 1, ], ); @@ -982,10 +1070,10 @@ public function __construct() lexer: $this->lexer, grammar: [ new \Phplrt\Parser\Grammar\Optional(1), - new \Phplrt\Parser\Grammar\Concatenation([2, 63]), + new \Phplrt\Parser\Grammar\Concatenation([2, 67]), new \Phplrt\Parser\Grammar\Optional(3), new \Phplrt\Parser\Grammar\Repetition(4, 1, \INF), - new \Phplrt\Parser\Grammar\Alternation([5, 6, 13, 56]), + new \Phplrt\Parser\Grammar\Alternation([5, 6, 13, 60]), new \Phplrt\Parser\Grammar\Lexeme(self::T_INTERNAL_OPTION, true), new \Phplrt\Parser\Grammar\Concatenation([7, 1, 12]), new \Phplrt\Parser\Grammar\Alternation([8, 9, 10, 11]), @@ -994,7 +1082,7 @@ public function __construct() new \Phplrt\Parser\Grammar\Lexeme(self::T_LOOKBEHIND, true), new \Phplrt\Parser\Grammar\Lexeme(self::T_NEGATIVE_LOOKBEHIND, true), new \Phplrt\Parser\Grammar\Lexeme(self::T_CAPTURING_CLOSE, false), - new \Phplrt\Parser\Grammar\Concatenation([14, 38]), + new \Phplrt\Parser\Grammar\Concatenation([14, 42]), new \Phplrt\Parser\Grammar\Alternation([15, 20, 31]), new \Phplrt\Parser\Grammar\Alternation([16, 17, 18, 19]), new \Phplrt\Parser\Grammar\Lexeme(self::T_NEGATIVE_CLASS_FC, true), @@ -1012,15 +1100,19 @@ public function __construct() new \Phplrt\Parser\Grammar\Lexeme(self::T_NON_CAPTURING_RESET, true), new \Phplrt\Parser\Grammar\Lexeme(self::T_ATOMIC_GROUP, true), new \Phplrt\Parser\Grammar\Lexeme(self::T_CAPTURING, true), - new \Phplrt\Parser\Grammar\Alternation([32, 33, 34, 35, 36, 37]), + new \Phplrt\Parser\Grammar\Alternation([32, 33, 34, 35, 36, 37, 38, 39, 40, 41]), new \Phplrt\Parser\Grammar\Lexeme(self::T_CHARACTER, true), new \Phplrt\Parser\Grammar\Lexeme(self::T_DYNAMIC_CHARACTER, true), new \Phplrt\Parser\Grammar\Lexeme(self::T_CHARACTER_TYPE, true), new \Phplrt\Parser\Grammar\Lexeme(self::T_ANCHOR, true), new \Phplrt\Parser\Grammar\Lexeme(self::T_MATCH_POINT_RESET, true), + new \Phplrt\Parser\Grammar\Lexeme(self::T_SUBROUTINE_G, true), + new \Phplrt\Parser\Grammar\Lexeme(self::T_BACKREFERENCE, true), + new \Phplrt\Parser\Grammar\Lexeme(self::T_NAMED_BACKREFERENCE, true), + new \Phplrt\Parser\Grammar\Lexeme(self::T_SUBROUTINE, true), new \Phplrt\Parser\Grammar\Lexeme(self::T_LITERAL, true), - new \Phplrt\Parser\Grammar\Optional(39), - new \Phplrt\Parser\Grammar\Alternation([40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55]), + new \Phplrt\Parser\Grammar\Optional(43), + new \Phplrt\Parser\Grammar\Alternation([44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59]), new \Phplrt\Parser\Grammar\Lexeme(self::T_ZERO_OR_ONE_POSSESSIVE, true), new \Phplrt\Parser\Grammar\Lexeme(self::T_ZERO_OR_ONE_LAZY, true), new \Phplrt\Parser\Grammar\Lexeme(self::T_ZERO_OR_ONE, true), @@ -1037,16 +1129,16 @@ public function __construct() new \Phplrt\Parser\Grammar\Lexeme(self::T_N_OR_MORE_POSSESSIVE, true), new \Phplrt\Parser\Grammar\Lexeme(self::T_N_OR_MORE_LAZY, true), new \Phplrt\Parser\Grammar\Lexeme(self::T_N_OR_MORE, true), - new \Phplrt\Parser\Grammar\Concatenation([57, 1, 12]), - new \Phplrt\Parser\Grammar\Alternation([58, 59, 60, 61]), + new \Phplrt\Parser\Grammar\Concatenation([61, 1, 12]), + new \Phplrt\Parser\Grammar\Alternation([62, 63, 64, 65]), new \Phplrt\Parser\Grammar\Lexeme(self::T_NAMED_REFERENCE, true), new \Phplrt\Parser\Grammar\Lexeme(self::T_ABSOLUTE_REFERENCE, true), new \Phplrt\Parser\Grammar\Lexeme(self::T_RELATIVE_REFERENCE, true), - new \Phplrt\Parser\Grammar\Concatenation([62, 1, 12]), + new \Phplrt\Parser\Grammar\Concatenation([66, 1, 12]), new \Phplrt\Parser\Grammar\Lexeme(self::T_ASSERTION_REFERENCE, true), - new \Phplrt\Parser\Grammar\Optional(64), - new \Phplrt\Parser\Grammar\Repetition(65, 1, \INF), - new \Phplrt\Parser\Grammar\Concatenation([66, 2]), + new \Phplrt\Parser\Grammar\Optional(68), + new \Phplrt\Parser\Grammar\Repetition(69, 1, \INF), + new \Phplrt\Parser\Grammar\Concatenation([70, 2]), new \Phplrt\Parser\Grammar\Lexeme(self::T_ALTERNATION, true), ], initial: 0, @@ -1060,7 +1152,7 @@ public function __construct() 15 => \Closure::fromCallable([self::class, 'reduceCharacterClass']), 20 => \Closure::fromCallable([self::class, 'reduceCapturing']), 31 => \Closure::fromCallable([self::class, 'reduceLiteral']), - 56 => \Closure::fromCallable([self::class, 'reduceCondition']), + 60 => \Closure::fromCallable([self::class, 'reduceCondition']), ], expectations: self::PARSER_EXPECTATIONS, kept: self::PARSER_KEPT_TABLE, @@ -1256,7 +1348,12 @@ private static function reduceCapturing(\Phplrt\Parser\Context $ctx, mixed $chil private static function reduceLiteral(\Phplrt\Parser\Context $ctx, mixed $children): mixed { - return new RegexNode('token', \strtolower(\substr($children->name, 2)), $children->value); + $names = [ + self::T_NAMED_BACKREFERENCE => 'backreference', + self::T_SUBROUTINE_G => 'subroutine', + ]; + + return new RegexNode('token', $names[$children->id] ?? \strtolower(\substr($children->name, 2)), $children->value); } private static function reduceCondition(\Phplrt\Parser\Context $ctx, mixed $children): mixed diff --git a/src/Type/Regex/RegexGroupParser.php b/src/Type/Regex/RegexGroupParser.php index 4a184decf63..8aad7d8995b 100644 --- a/src/Type/Regex/RegexGroupParser.php +++ b/src/Type/Regex/RegexGroupParser.php @@ -106,6 +106,10 @@ public function parseGroups(string $regex): ?RegexAstWalkResult if (!$ast instanceof RegexNode) { throw new ShouldNotHappenException(); } + if ($this->containsSubroutineCall($ast)) { + // a subroutine call matches what another group matches, but it does not set that group + return self::$parsedAst[$regex] = null; + } self::$parsedAst[$regex] = $ast; } @@ -157,6 +161,21 @@ private function createEmptyTokenRegexNode(): RegexNode return new RegexNode('token', 'literal', ''); } + private function containsSubroutineCall(RegexNode $ast): bool + { + if ($ast->getId() === 'token' && $ast->getValueToken() === 'subroutine') { + return true; + } + + foreach ($ast->getChildren() as $child) { + if ($this->containsSubroutineCall($child)) { + return true; + } + } + + return false; + } + private function updateAlternationAstRemoveVerticalBarsAndAddEmptyToken(RegexNode $ast): void { $children = $ast->getChildren(); @@ -571,6 +590,11 @@ private function walkGroupAst( $literalValue = $this->getLiteralValue($ast, $onlyLiterals, !$inClass, $patternModifiers, false); $walkResult = $walkResult->onlyLiterals($onlyLiterals); + if ($ast->getValueToken() === 'backreference') { + // matches what another group matched, the content is not known + $walkResult = $walkResult->numeric(TrinaryLogic::createNo()); + } + if ($literalValue !== null) { if (Strings::match($literalValue, '/^\d+$/') === null) { $walkResult = $walkResult->numeric(TrinaryLogic::createNo()); diff --git a/tests/PHPStan/Analyser/nsrt/preg_match_shapes.php b/tests/PHPStan/Analyser/nsrt/preg_match_shapes.php index ab633fa4662..edfac2677cf 100644 --- a/tests/PHPStan/Analyser/nsrt/preg_match_shapes.php +++ b/tests/PHPStan/Analyser/nsrt/preg_match_shapes.php @@ -1151,3 +1151,20 @@ function testQuantifiedMaybeEmptyAtom(string $s): void { assertType("array{non-falsy-string, string, string}", $matches); } } + +function testBackreferences(string $s): void { + // from PCRE2 testdata/testinput1 + if (preg_match('/([a-c]*)\1/', $s, $matches)) { + assertType("array{string, string}", $matches); + } + + // from PCRE2 testdata/testinput1 + if (preg_match('/(\2|a)(\1)/', $s, $matches)) { + assertType("array{string, string, string}", $matches); + } + + // from PCRE2 testdata/testinput2 + if (preg_match('/^(..)(\1{2,3})ab/', $s, $matches)) { + assertType("array{non-falsy-string, non-falsy-string, string}", $matches); + } +} From 18c759242042ee708da69dec369dc9c24186c7e1 Mon Sep 17 00:00:00 2001 From: "ondrej@mirtes.cz" Date: Mon, 26 Oct 2026 11:20:58 +0000 Subject: [PATCH 08/11] Do not infer preg_match() shapes for branch reset groups outside of the only top level alternation Amp-Thread-ID: https://ampcode.com/threads/T-01a177b5-960f-71d2-a3e2-fa7649f39d2c --- src/Type/Php/RegexArrayShapeMatcher.php | 15 +++++++++++++++ src/Type/Regex/RegexGroupList.php | 11 +++++++++++ .../Analyser/nsrt/preg_match_all_shapes.php | 7 +++++++ tests/PHPStan/Analyser/nsrt/preg_match_shapes.php | 7 +++++++ 4 files changed, 40 insertions(+) diff --git a/src/Type/Php/RegexArrayShapeMatcher.php b/src/Type/Php/RegexArrayShapeMatcher.php index 07286700fd9..958cc76bc86 100644 --- a/src/Type/Php/RegexArrayShapeMatcher.php +++ b/src/Type/Php/RegexArrayShapeMatcher.php @@ -159,6 +159,21 @@ private function matchRegex(string $regex, ?int $flags, TrinaryLogic $wasMatched return null; } + if ( + $regexGroupList->hasGroupResettingGroupCounter() + && ( + $matchesAll + || $onlyOptionalTopLevelGroup !== null + || $onlyTopLevelAlternation === null + || $wasMatched->no() + || $this->containsUnmatchedAsNull($flags, $matchesAll) + ) + ) { + // groups in a branch reset (?|...) share offsets, which is only modelled + // when its alternation is the only top level alternation + return null; + } + if ( !$matchesAll && $wasMatched->yes() diff --git a/src/Type/Regex/RegexGroupList.php b/src/Type/Regex/RegexGroupList.php index 6150d3e1712..37f4a483081 100644 --- a/src/Type/Regex/RegexGroupList.php +++ b/src/Type/Regex/RegexGroupList.php @@ -178,6 +178,17 @@ public function hasDuplicateNamesOutsideOfAlternationBranches(?RegexAlternation return false; } + public function hasGroupResettingGroupCounter(): bool + { + foreach ($this->groups as $captureGroup) { + if ($captureGroup->resetsGroupCounter()) { + return true; + } + } + + return false; + } + public function getOnlyTopLevelAlternation(): ?RegexAlternation { $alternation = null; diff --git a/tests/PHPStan/Analyser/nsrt/preg_match_all_shapes.php b/tests/PHPStan/Analyser/nsrt/preg_match_all_shapes.php index cdda4c47dd3..93099323c1b 100644 --- a/tests/PHPStan/Analyser/nsrt/preg_match_all_shapes.php +++ b/tests/PHPStan/Analyser/nsrt/preg_match_all_shapes.php @@ -226,3 +226,10 @@ function markVerbs(string $s): void { assertType("non-empty-list", $matches); } } + +function testBranchReset(string $s): void { + // from PCRE2 testdata/testinput2 + if (preg_match_all('/(?|(abc)|(xyz))/', $s, $matches)) { + assertType('array>', $matches); + } +} diff --git a/tests/PHPStan/Analyser/nsrt/preg_match_shapes.php b/tests/PHPStan/Analyser/nsrt/preg_match_shapes.php index edfac2677cf..ab1f91a9e51 100644 --- a/tests/PHPStan/Analyser/nsrt/preg_match_shapes.php +++ b/tests/PHPStan/Analyser/nsrt/preg_match_shapes.php @@ -1168,3 +1168,10 @@ function testBackreferences(string $s): void { assertType("array{non-falsy-string, non-falsy-string, string}", $matches); } } + +function testBranchResetNotInOnlyTopLevelAlternation(string $s): void { + // from PCRE2 testdata/testinput2 + if (preg_match('/(x)(?|(abc)|(xyz))(x)/', $s, $matches)) { + assertType('array', $matches); + } +} From 8aacef33a324baeca7f0195c87b81d73899bcc55 Mon Sep 17 00:00:00 2001 From: "ondrej@mirtes.cz" Date: Mon, 26 Oct 2026 11:23:02 +0000 Subject: [PATCH 09/11] Groups in conditional regex subpatterns are optional and conditional subpatterns might match an empty string Amp-Thread-ID: https://ampcode.com/threads/T-01a177b5-960f-71d2-a3e2-fa7649f39d2c --- src/Type/Regex/RegexGroupParser.php | 17 ++++++++++++++--- .../PHPStan/Analyser/nsrt/preg_match_shapes.php | 17 +++++++++++++++++ 2 files changed, 31 insertions(+), 3 deletions(-) diff --git a/src/Type/Regex/RegexGroupParser.php b/src/Type/Regex/RegexGroupParser.php index 8aad7d8995b..0aea8561d35 100644 --- a/src/Type/Regex/RegexGroupParser.php +++ b/src/Type/Regex/RegexGroupParser.php @@ -53,6 +53,8 @@ final class RegexGroupParser private const ASSERTIONS = ['#lookahead', '#lookbehind', '#negativelookahead', '#negativelookbehind']; + private const CONDITIONS = ['#namedcondition', '#absolutecondition', '#relativecondition', '#assertioncondition']; + private static ?RegexParser $parser = null; /** @var array */ @@ -291,8 +293,12 @@ private function walkRegexAst( true, ); $parentGroup = $group; - } elseif (in_array($ast->getId(), self::NEGATIVE_ASSERTIONS, true)) { - // capturing groups in a negative assertion are never set when the whole pattern matches + } elseif ( + in_array($ast->getId(), self::NEGATIVE_ASSERTIONS, true) + || in_array($ast->getId(), self::CONDITIONS, true) + ) { + // capturing groups in a negative assertion are never set when the whole pattern matches, + // capturing groups in a conditional subpattern are set only when its condition says so $group = new RegexNonCapturingGroup( $alternation, true, @@ -515,6 +521,11 @@ private function walkGroupAst( return $walkResult; } + if (in_array($ast->getId(), self::CONDITIONS, true)) { + // which branch of a conditional subpattern matches is not known, it might match an empty string + return $walkResult->onlyLiterals(null)->numeric(TrinaryLogic::createNo()); + } + $children = $ast->getChildren(); $quantifiedLiterals = null; @@ -785,7 +796,7 @@ private function repeatLiterals(array $literals, int $min, int $max): ?array private function isMaybeEmptyNode(RegexNode $node, string $patternModifiers, bool &$isNonFalsy): bool { - if (in_array($node->getId(), self::ASSERTIONS, true)) { + if (in_array($node->getId(), self::ASSERTIONS, true) || in_array($node->getId(), self::CONDITIONS, true)) { return true; } diff --git a/tests/PHPStan/Analyser/nsrt/preg_match_shapes.php b/tests/PHPStan/Analyser/nsrt/preg_match_shapes.php index ab1f91a9e51..d8a68124cc7 100644 --- a/tests/PHPStan/Analyser/nsrt/preg_match_shapes.php +++ b/tests/PHPStan/Analyser/nsrt/preg_match_shapes.php @@ -1175,3 +1175,20 @@ function testBranchResetNotInOnlyTopLevelAlternation(string $s): void { assertType('array', $matches); } } + +function testConditionalSubpatterns(string $s): void { + // from PCRE2 testdata/testinput1 + if (preg_match('/^(\()?blah(?(1)(\)))$/', $s, $matches)) { + assertType("list{0: non-falsy-string, 1?: ''|'(', 2?: ')'}", $matches); + } + + // from PCRE2 testdata/testinput2 + if (preg_match('/((?(-1)a))/', $s, $matches)) { + assertType('array{string, string}', $matches); + } + + // from PCRE2 testdata/testinput1 + if (preg_match('/(?(DEFINE)(?bar))(? Date: Mon, 26 Oct 2026 12:11:36 +0000 Subject: [PATCH 10/11] Exclude generated regex parser from typos check Amp-Thread-ID: https://ampcode.com/threads/T-01a177b5-960f-71d2-a3e2-fa7649f39d2c --- .typos.toml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.typos.toml b/.typos.toml index 42b8a797374..5c9b8f36841 100644 --- a/.typos.toml +++ b/.typos.toml @@ -1,6 +1,8 @@ [files] extend-exclude = [ ".git/", + # generated by `make regex-parser`, contains lexer regexes + "src/Type/Regex/Parser/RegexParser.php", ] ignore-hidden = false From d1fd53b256cac227962adec252dda23bf59c286b Mon Sep 17 00:00:00 2001 From: "ondrej@mirtes.cz" Date: Mon, 26 Oct 2026 12:34:17 +0000 Subject: [PATCH 11/11] Remove unreachable wasMatched check in branch reset guard Amp-Thread-ID: https://ampcode.com/threads/T-01a177b5-960f-71d2-a3e2-fa7649f39d2c --- src/Type/Php/RegexArrayShapeMatcher.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Type/Php/RegexArrayShapeMatcher.php b/src/Type/Php/RegexArrayShapeMatcher.php index 958cc76bc86..6d5bfbab630 100644 --- a/src/Type/Php/RegexArrayShapeMatcher.php +++ b/src/Type/Php/RegexArrayShapeMatcher.php @@ -165,7 +165,6 @@ private function matchRegex(string $regex, ?int $flags, TrinaryLogic $wasMatched $matchesAll || $onlyOptionalTopLevelGroup !== null || $onlyTopLevelAlternation === null - || $wasMatched->no() || $this->containsUnmatchedAsNull($flags, $matchesAll) ) ) {