diff --git a/.typos.toml b/.typos.toml index 5c9b8f36841..42b8a797374 100644 --- a/.typos.toml +++ b/.typos.toml @@ -1,8 +1,6 @@ [files] extend-exclude = [ ".git/", - # generated by `make regex-parser`, contains lexer regexes - "src/Type/Regex/Parser/RegexParser.php", ] ignore-hidden = false diff --git a/CLAUDE.md b/CLAUDE.md index 7766526269e..31861e9df02 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -187,14 +187,13 @@ Collects structural information about a class during AST traversal: properties, ### 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): +`RegexGroupParser` reads regex patterns to infer `preg_match()` array shapes (via `RegexArrayShapeMatcher`) and `IgnoredRegexValidator` uses the same reader to validate `ignoreErrors` patterns. Patterns are parsed by [PHPRegex](https://github.com/php-regex/php-regex), from the `php-regex/regex-parser-php74` package (its PHP 7.4 compatible build, so it works after the PHAR is downgraded): -- **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/`. +- **Reader**: `PHPRegexReader` parses a pattern for the analysed PHP version and the PCRE2 version it bundles (`pcreVersion` parameter, or `config.platform.lib-pcre` in composer.json, see `PcreVersion`). A pattern the analysed engine refuses, or that the library cannot read, gets no shape. +- **Shapes**: the type of each group comes from the library's capture shape analysis (`CaptureShapeAnalyzer`: participation, values, length, digit and truthiness facts, per-branch arms). `RegexGroupParser` only walks the tree to place each group: its parent group and the alternation branch it belongs to. +- Library exceptions implement `PHPRegex\Parser\Exception\ExceptionInterface`; callers treat them as "pattern not supported" and infer no shape. +- `RegexArrayShapeMatcherEngineTest` matches every constant pattern of the `preg_*` fixtures with the engine and checks that the inferred type accepts what the engine writes. +- After changing `RegexGroupParser` or `RegexArrayShapeMatcher`, 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 @@ -350,4 +349,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 -- `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`) +- `php-regex/regex-parser-php74` - PHP 7.4 compatible build of the PHPRegex parser, reads regex patterns for `preg_*()` shapes and `ignoreErrors` diff --git a/Makefile b/Makefile index 3cdbc82fcab..deab32ea712 100644 --- a/Makefile +++ b/Makefile @@ -177,14 +177,6 @@ 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/build/phpstan.neon b/build/phpstan.neon index e4c1650ff6a..d07ecd57ac5 100644 --- a/build/phpstan.neon +++ b/build/phpstan.neon @@ -39,9 +39,6 @@ parameters: - ../tests/PHPStan/Reflection/MixedTypeTest.php - ../tests/e2e/magic-setter/* - ../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 deleted file mode 100644 index 22d0d82f809..00000000000 --- a/build/regex-parser/.gitignore +++ /dev/null @@ -1 +0,0 @@ -vendor diff --git a/build/regex-parser/RegexGrammar.pp3 b/build/regex-parser/RegexGrammar.pp3 deleted file mode 100644 index 4b9f58b237a..00000000000 --- a/build/regex-parser/RegexGrammar.pp3 +++ /dev/null @@ -1,382 +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 -// -// 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 \(\?#(.*?)(?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 \(\?: -%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 -// 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 -> { - 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 -> { - $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/build/regex-parser/composer.json b/build/regex-parser/composer.json deleted file mode 100644 index 4432594d8fa..00000000000 --- a/build/regex-parser/composer.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "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 deleted file mode 100644 index 99c7fa52c60..00000000000 --- a/build/regex-parser/composer.lock +++ /dev/null @@ -1,2049 +0,0 @@ -{ - "_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 deleted file mode 100644 index 0f6dca9c2f5..00000000000 --- a/build/regex-parser/generate.php +++ /dev/null @@ -1,54 +0,0 @@ -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/composer.json b/composer.json index c9fa0baeb6e..dd3810b46cb 100644 --- a/composer.json +++ b/composer.json @@ -34,7 +34,7 @@ "ondrejmirtes/better-reflection": "6.75.0.4", "ondrejmirtes/composer-attribute-collector": "^1.2.0", "ondrejmirtes/php-merge": "^4.1", - "ondrejmirtes/phplrt-runtime": "4.2.4.1", + "php-regex/regex-parser-php74": "2.x-dev#6c1dcd832546ee6f5bc4d89a49e4eca505db9b5c", "phpstan/php-8-stubs": "0.4.43", "phpstan/phpdoc-parser": "2.3.8", "react/child-process": "^0.7", @@ -142,5 +142,10 @@ "prefer-stable": true, "bin": [ "bin/phpstan" - ] + ], + "repositories": [{ + "name": "phpregex", + "type": "vcs", + "url": "https://github.com/php-regex/regex-parser-php74" + }] } diff --git a/composer.lock b/composer.lock index ba65a898934..8103fcc845a 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": "f5148e68b5a12ba8eac518eba014d4a8", + "content-hash": "bf0325d3a5160295f58ef05bdc4c747a", "packages": [ { "name": "clue/ndjson-react", @@ -1536,74 +1536,76 @@ "time": "2025-12-09T08:32:19+00:00" }, { - "name": "ondrejmirtes/phplrt-runtime", - "version": "4.2.4.1", + "name": "php-regex/regex-parser-php74", + "version": "2.x-dev", "source": { "type": "git", - "url": "https://github.com/ondrejmirtes/phplrt-runtime.git", - "reference": "0a9b3058a8f857f5e2428a5b5040e29bce7017a0" + "url": "https://github.com/php-regex/regex-parser-php74.git", + "reference": "6c1dcd832546ee6f5bc4d89a49e4eca505db9b5c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/ondrejmirtes/phplrt-runtime/zipball/0a9b3058a8f857f5e2428a5b5040e29bce7017a0", - "reference": "0a9b3058a8f857f5e2428a5b5040e29bce7017a0", + "url": "https://api.github.com/repos/php-regex/regex-parser-php74/zipball/6c1dcd832546ee6f5bc4d89a49e4eca505db9b5c", + "reference": "6c1dcd832546ee6f5bc4d89a49e4eca505db9b5c", "shasum": "" }, "require": { - "ext-json": "*", - "ext-pcre": "*", "php": "^7.4 || ^8.0", - "symfony/polyfill-php80": "^1.23", - "symfony/polyfill-php81": "^1.27" + "symfony/polyfill-mbstring": "^1.31", + "symfony/polyfill-php80": "^1.31", + "symfony/polyfill-php81": "^1.31" }, - "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" + "suggest": { + "ext-intl": "To resolve \\N{name} escapes by Unicode name.", + "psr/cache": "To share the AST cache through a PSR-6 pool.", + "psr/simple-cache": "To share the AST cache through a PSR-16 cache." }, + "default-branch": true, "type": "library", "extra": { "branch-alias": { - "dev-main": "4.2.x-dev" + "dev-2.x": "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" + "PHPRegex\\Parser\\": "src/" } }, - "notification-url": "https://packagist.org/downloads/", "license": [ "MIT" ], "authors": [ { - "name": "Kirill Nesmeyanov", - "email": "nesk@xakep.ru" + "name": "Younes ENNAJI", + "email": "younes.ennaji.pro@gmail.com", + "homepage": "https://www.linkedin.com/in/yoeunes/", + "role": "Developer" } ], - "description": "phplrt runtime (lexer, parser, source, position and their contracts) downgraded to PHP 7.4", - "homepage": "https://github.com/phplrt/phplrt", + "description": "The PHPRegex parser built to run on hosts PHP 7.4-8.0. On PHP 8.1+, install php-regex/regex-parser instead.", + "homepage": "https://github.com/php-regex/regex-parser-php74", + "keywords": [ + "ast", + "lexer", + "parser", + "pcre", + "pcre2", + "regex", + "validation" + ], "support": { - "issues": "https://github.com/ondrejmirtes/phplrt-runtime/issues", - "source": "https://github.com/ondrejmirtes/phplrt-runtime/tree/4.2.4.1" + "issues": "https://github.com/php-regex/regex-parser-php74/issues", + "source": "https://github.com/php-regex/regex-parser-php74", + "docs": "https://php-regex.com/reference/api/" }, - "time": "2026-10-26T17:32:05+00:00" + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/yoeunes" + } + ], + "time": "2026-10-27T03:57:00+00:00" }, { "name": "phpstan/php-8-stubs", @@ -5931,7 +5933,8 @@ "aliases": [], "minimum-stability": "dev", "stability-flags": { - "jetbrains/phpstorm-stubs": 20 + "jetbrains/phpstorm-stubs": 20, + "php-regex/regex-parser-php74": 20 }, "prefer-stable": true, "prefer-lowest": false, diff --git a/conf/bleedingEdge.neon b/conf/bleedingEdge.neon index a14610a1aa4..9b94d83324a 100644 --- a/conf/bleedingEdge.neon +++ b/conf/bleedingEdge.neon @@ -33,3 +33,4 @@ parameters: backedEnumFromParameterType: true unusedVariable: true unusedParameters: true + checkRegexPatternForAnalysedPhp: true diff --git a/conf/config.neon b/conf/config.neon index f623528506a..478cb3413be 100644 --- a/conf/config.neon +++ b/conf/config.neon @@ -65,6 +65,7 @@ parameters: backedEnumFromParameterType: false unusedVariable: false unusedParameters: false + checkRegexPatternForAnalysedPhp: false fileExtensions: - php checkAdvancedIsset: false @@ -120,6 +121,7 @@ parameters: buffer: 134217728 # 128 MB loadLimit: 1.0 phpVersion: null + pcreVersion: null polluteScopeWithLoopInitialAssignments: true polluteScopeWithAlwaysIterableForeach: true polluteScopeWithBlock: true diff --git a/conf/parametersSchema.neon b/conf/parametersSchema.neon index 154054ccb1f..a4f1833c0a6 100644 --- a/conf/parametersSchema.neon +++ b/conf/parametersSchema.neon @@ -62,6 +62,7 @@ parametersSchema: backedEnumFromParameterType: bool() unusedVariable: bool() unusedParameters: bool() + checkRegexPatternForAnalysedPhp: bool() ]) fileExtensions: listOf(string()) checkAdvancedIsset: bool() @@ -128,6 +129,7 @@ parametersSchema: max: schema(int(), min(70100), max(80699)) ]) ), nullable()) + pcreVersion: schema(string(), pattern('^\d{2}\.\d{2}(-[^ ]+)?( .*)?$'), nullable()) polluteScopeWithLoopInitialAssignments: bool() polluteScopeWithAlwaysIterableForeach: bool() polluteScopeWithBlock: bool() diff --git a/phpcs.xml b/phpcs.xml index 98e0caae54d..5a9b9e83855 100644 --- a/phpcs.xml +++ b/phpcs.xml @@ -118,8 +118,6 @@ src/Type/TypeResult.php - build/regex-parser/vendor/* - src/Type/Regex/Parser/RegexParser.php compiler/tests/*/data/ tests/*/Fixture/ tests/*/cache/ diff --git a/poc/README.md b/poc/README.md new file mode 100644 index 00000000000..2267b91667b --- /dev/null +++ b/poc/README.md @@ -0,0 +1,66 @@ +# Checked against the engine + +Each pattern of four sets is matched with `preg_match()` and `preg_match_all()` on many subjects, with every flag combination, and the array the engine writes is checked against the type PHPStan infers for the call. A type the engine contradicts is one PHPStan says cannot hold that array. The same scripts run on an upstream `2.3.x` checkout, where patterns are read by the parser generated by phplrt, and on this branch, where they are read by PHPRegex. + +| | phplrt | PHPRegex | +|---|---|---| +| PCRE2 test suite (testdata/testinput*) (3,178 patterns): types the engine contradicts | 70 | 0 | +| php-src ext/pcre tests (205 patterns): types the engine contradicts | 3 | 0 | +| patterns from open-source projects (1,645 patterns): types the engine contradicts | 21 | 0 | +| generated edge cases (5,100 patterns): types the engine contradicts | 308 | 0 | +| PCRE2 test suite (testdata/testinput*): patterns with no `preg_match()` shape | 589 | 0 | +| php-src ext/pcre tests: patterns with no `preg_match()` shape | 8 | 0 | +| patterns from open-source projects: patterns with no `preg_match()` shape | 15 | 0 | +| generated edge cases: patterns with no `preg_match()` shape | 1265 | 0 | + +Every contradicted pattern has one witness in `poc/witnesses/`: the pattern, the call, the subject, what the engine writes, and both types. `php poc/bin/verify.php ` replays all of them: on upstream `2.3.x` every witness is refused, on this branch every witness is accepted. + +## What the generated parser reads differently + +Each contradicted pattern is counted once, under the first row it matches, in the order of the table. + +| what the generated parser reads differently | patterns | witness | the engine writes | phplrt | PHPRegex | +|---|---|---|---|---|---| +| A `(?#...)` comment is read as pattern syntax | 2 | `~L(?#(\|++ '\x9202', 1 => '\x92', 2 => '02', )` | `array{non-falsy-string, '-'\|'−', numeric-string}` | `array{non-falsy-string, '-'\|'\x88'\|'\x92'\|'\xe2', numeric-string}` | +| `(?n)` and `/n`: groups that do not capture are typed as captured | 50 | `/(?n)(a)(?b)/` on `'ab'` | `{"0":"ab","x":"b","1":"b"}` | `array{0: non-falsy-string, 1: 'a', x: 'b', 2: 'b'}` | `array{0: 'ab', x: 'b', 1: 'b'}` | +| An inline `(?x)` is not read: skipped whitespace is typed as text | 25 | `/(?x)( a )/` on `'a'` | `["a","a"]` | `array{non-falsy-string, ' a '}` | `array{'a', 'a'}` | +| An inline `(?i)`, `(?i:...)` or `(?^i)` is not read: a caseless match is typed by its spelling | 60 | `/(?i)(a)/` on `'A'` | `["A","A"]` | `array{non-falsy-string, 'a'}` | `array{non-falsy-string, non-falsy-string}` | +| `\d` and `[[:digit:]]` under `/u` or `(*UCP)` read Unicode digits, which are not numeric strings | 62 | `/(\d)/u` on `'٣'` | `["٣","٣"]` | `array{non-empty-string, numeric-string}` | `array{non-empty-string, non-empty-string}` | +| A value that can be `'0'` or `''` is typed `non-falsy-string` or `non-empty-string`: `\w`, `\S`, `.`, `\p{N}`, `\X`, `[[:alnum:]]`, negated classes, lookbehinds | 153 | `/\pN/` on `'0'` | `["0"]` | `array{non-falsy-string}` | `array{non-empty-string}` | + +## Reproducing + +Replay the witnesses, a few seconds each side: + +``` +git fetch https://github.com/phpstan/phpstan-src.git 2.3.x +git worktree add --detach ../phpstan-upstream FETCH_HEAD +composer install -d ../phpstan-upstream +php poc/bin/verify.php ../phpstan-upstream +php poc/bin/verify.php . +``` + +Measure a set again, a few minutes each side; once both sides are measured, `php poc/bin/report.php ` writes the witnesses and this README from `README.template.md`: + +``` +php poc/bin/measure.php ../phpstan-upstream poc/data/pcre2.json.gz /tmp/poc +php poc/bin/measure.php . poc/data/pcre2.json.gz /tmp/poc +php poc/bin/report.php /tmp/poc +``` + +The engine decides what a pattern matches, so the witnesses hold for the PCRE2 release they were recorded with; `verify.php` skips, and names, a witness the running engine writes differently. + +## The sets + +- `pcre2.json.gz`: every pattern of `testdata/testinput*` of PCRE2Project/pcre2 (commit be940c4) whose modifiers PHP has, with the subject lines that follow it. One pattern is left out: `\K` inside a lookbehind makes `preg_replace_callback()` of PHP fail on the negative length of the match. +- `php-src.json.gz`: every literal pattern of a `preg_*()` call in `ext/pcre/tests` of php/php-src (branch PHP-8.6, commit 02ddcc5ee2), with the string literals of the same test. +- `corpus.json.gz`: patterns found in open-source PHP projects, collected by PHPRegex for its linter. +- `adversarial.json.gz`: hand-written pattern bodies with each modifier and delimiter the engine accepts (`poc/bin/adversarial-patterns.php`). + +`poc/bin/extract-suites.php` writes the first two from checkouts of both projects. Subjects are added with `poc/bin/subjects.php`: samples written by the PHPRegex generator, variations of them, the literals of the pattern, random strings over its characters, and a fixed list of edge strings. + +Recorded with PHP 8.4.27 and PCRE2 10.49, `pcre.jit` off, against upstream `2.3.x` at 49861becc4f8. diff --git a/poc/README.template.md b/poc/README.template.md new file mode 100644 index 00000000000..46cd42cf7d2 --- /dev/null +++ b/poc/README.template.md @@ -0,0 +1,46 @@ +# Checked against the engine + +Each pattern of four sets is matched with `preg_match()` and `preg_match_all()` on many subjects, with every flag combination, and the array the engine writes is checked against the type PHPStan infers for the call. A type the engine contradicts is one PHPStan says cannot hold that array. The same scripts run on an upstream `2.3.x` checkout, where patterns are read by the parser generated by phplrt, and on this branch, where they are read by PHPRegex. + +{totals} + +Every contradicted pattern has one witness in `poc/witnesses/`: the pattern, the call, the subject, what the engine writes, and both types. `php poc/bin/verify.php ` replays all of them: on upstream `2.3.x` every witness is refused, on this branch every witness is accepted. + +## What the generated parser reads differently + +Each contradicted pattern is counted once, under the first row it matches, in the order of the table. + +{families} + +## Reproducing + +Replay the witnesses, a few seconds each side: + +``` +git fetch https://github.com/phpstan/phpstan-src.git 2.3.x +git worktree add --detach ../phpstan-upstream FETCH_HEAD +composer install -d ../phpstan-upstream +php poc/bin/verify.php ../phpstan-upstream +php poc/bin/verify.php . +``` + +Measure a set again, a few minutes each side; once both sides are measured, `php poc/bin/report.php ` writes the witnesses and this README from `README.template.md`: + +``` +php poc/bin/measure.php ../phpstan-upstream poc/data/pcre2.json.gz /tmp/poc +php poc/bin/measure.php . poc/data/pcre2.json.gz /tmp/poc +php poc/bin/report.php /tmp/poc +``` + +The engine decides what a pattern matches, so the witnesses hold for the PCRE2 release they were recorded with; `verify.php` skips, and names, a witness the running engine writes differently. + +## The sets + +- `pcre2.json.gz`: every pattern of `testdata/testinput*` of PCRE2Project/pcre2 (commit be940c4) whose modifiers PHP has, with the subject lines that follow it. One pattern is left out: `\K` inside a lookbehind makes `preg_replace_callback()` of PHP fail on the negative length of the match. +- `php-src.json.gz`: every literal pattern of a `preg_*()` call in `ext/pcre/tests` of php/php-src (branch PHP-8.6, commit 02ddcc5ee2), with the string literals of the same test. +- `corpus.json.gz`: patterns found in open-source PHP projects, collected by PHPRegex for its linter. +- `adversarial.json.gz`: hand-written pattern bodies with each modifier and delimiter the engine accepts (`poc/bin/adversarial-patterns.php`). + +`poc/bin/extract-suites.php` writes the first two from checkouts of both projects. Subjects are added with `poc/bin/subjects.php`: samples written by the PHPRegex generator, variations of them, the literals of the pattern, random strings over its characters, and a fixed list of edge strings. + +Recorded with PHP 8.4.27 and PCRE2 10.49, `pcre.jit` off, against upstream `2.3.x` at 49861becc4f8. diff --git a/poc/bin/adversarial-patterns.php b/poc/bin/adversarial-patterns.php new file mode 100644 index 00000000000..6f98a5deafb --- /dev/null +++ b/poc/bin/adversarial-patterns.php @@ -0,0 +1,66 @@ + + * Subjects are then added with poc/bin/subjects.php. + */ +$p = []; +$bodies = [ + // digits / UCP + '(\d)', '(\d+)', '(\d*)', '^(\d+)$', '(\d{2})', '([0-9]{2})', '([[:digit:]]+)', '(\p{Nd}+)', '(\p{N}+)', '([\d]+)', '([^\D]+)', '(\d)\s', '(-?\d+)', '(\d+\.\d+)', '(\d+(?:\.\d+)?)', '(0)', '(0+)', '(00)', '(0|1)', '([01])', '(\d)(\d)', '(\d+)|(x)', '(?\d+)', '(\w+)', '(\w)', '([[:alnum:]]+)', '([[:alpha:]])', '(\x30)', '(\x{30})', '(\060)', '(\N{U+0030})', + '(?i)(\d+)', '(\d+)(?i)', '(\d|a)', '(\d|٣)', '(\h)', '(\s)', '(\S)', '(\W)', '(\D)', '(\R)', '(\X)', '(\C)', '(.)', '(.+)', '(..)', '(\b\d+\b)', '([^a])', '([^\d])', '([^0-9]+)', '([\x00-\x7f])', '([\x{660}-\x{669}])', '([٠-٩])', + // case + '(ab|cd)', '(a)', '(é)', '(k)', '(K)', '(s)', '(ss)', '(ß)', '(i)', '(I)', '(ı)', '(st)', '(st)', '(Σ)', '(σ)', '(ς)', '(a|b)c', '([a-c])', '([k])', '([s])', '(abc)', '(?i:a)(b)', '(?i)(a)', '(?-i)(a)', '(?i:(a))', '(?^)(a)', '(?^i)(a)', '((?i)a)b', '(?:(?i)a|b)(c)', + '(\x{212A})', '(\x{17F})', + // \K and verbs + 'a\K(b)', '(a)\Kb', '(?:a\K)?(b)', '(a)(*ACCEPT)(b)', '(a)(?:(*ACCEPT))?(b)', '(?:a(*ACCEPT))?(b)(c)', '(a(*ACCEPT)b)c', '(*ACCEPT)(a)', '(a)|b(*ACCEPT)(c)', '(?:x(*ACCEPT)|y)(z)', '(*MARK:m)(a)', '(?:(*MARK:m)a|(*MARK:n)b)(c)', '(a)(*SKIP)(*FAIL)|(b)', '(a)(*COMMIT)b', '(a)(*PRUNE)b|(c)', '(*:x)(a)|(*:y)(b)', 'a(*THEN:t)b|(c)', '(*F)|(a)', '(?:(a)(*F)|(b))', + // conditionals + '(a)?(?(1)b|c)', '(a)?(?(1)(b)|(c))', '(a)?(?(1)(b))', '(?(?=a)(a)|(b))', '(?(?!a)(b)|(a))', '(?(?=(a))a|b)', '(?(?<=x)(a)|(b))', '(?(DEFINE)(?a))(b)', '(?(DEFINE)(a))(?1)', '(?(R)(a)|(b))', '(?(R1)a|(b))', '(?(VERSION>=10.0)(a)|(b))', '(?(VERSION>=99)(a)|(b))', '(?a)?(?()b|c)', '(?a)?(?(n)b|(c))', '(a)?(?(1)|(b))', '(?(1)(a)|(b))', '(?:(a)|b)(?(1)c|d)', + // branch reset + '(?|(a)|(b))', '(?|(a)|(b)(c))', '(?|(a)(b)|(c))', '(?|(?a)|(?b))', '(?|(a)|(?|(b)|(c)(d)))', '(?|(a)|b)(c)', '(?|a|(b))(c)', '(?|(a)|(b))?', '(?|(a)|(b))(?|(c)|(d))', '(x)(?|(a)|(b))(y)', '(?|(\d)|(a))', '(?|(0)|())', + // duplicate names + '(?J)(?a)|(?b)', '(?J)(?:(?a)|(?b))c', '(?J)(?a)?(?b)', '(?J)(?\d)|(?b)', '(?J)(?a)|(?c)|(?b)', + // named / (?n) + '(?n)(a)(?b)', '(?n)(a)', '(?n:(a))(b)', '(?-n)(a)', '(?a)(b)', '(?Pa)', "(?'x'a)", + // extended + "(a) # (b)\n(c)", '(a)\#(b)', '(a)[#](b)', "(a)(?#c)(b)", "(?x)(a) #c\n(b)", "(?x)( a )", "(?xx)([a b])", + // quoting + '(\Qa.b\E)', '\Q(a)\E', '(\Q0\E)', '(\Q\E)', '(\Qa)\E)', + // backrefs, subroutines + '(a)\1', '(a|b)\1', '(a)(\1)', '(?a)\k', '(a)\g{-1}', '(a)\g1', '(?:(a)|b)\1', '(a)?\1', '(a)(?1)', '(a|b)(?1)', '(?1)(a)', '(a(?R)?b)', '((?R)?a)', '(a)(?R)?', '(?a)(?&x)', '(?P>x)(?Pa)', '(a)(?-1)', '(\d)(?1)', '(?:(\d)|x)(?1)', '(a)\1(b)', + // lookarounds + '(?=(a))', '(?=(a))a', '(?=(\d))\w', '(?!(a))b', '(?<=(a))b', '(?(a)|b)', '(?>a|(b))c', '(?|(?>(a))|b)', '(*UTF)(\d)', '(*UCP)(\d)', '(*UTF)(*UCP)(\d+)', '(*UCP)(\w)', '(*UCP)([[:digit:]])', '(*UTF)(.)', '(*UTF8)(.)', '(*NO_START_OPT)(a)', '(*CR)(a)$', '(*LF)(a)$', '(*ANYCRLF)(a)$', '(*NOTEMPTY)(a?)', '(*LIMIT_MATCH=10)(a)', + '(?s)(.)', '(?m)^(a)$', '(?x)(\d +)', '(?U)(a+)', '(?U)(a+?)', '(?i)(k)', '(?r)(k)', '(?ir)(k)', '(?ia)(\d)', '(?aD)(\d)', '(?aP)(\w)', '(?aT)([[:digit:]])', + '(\N)', '(\N+)', '(\p{L})', '(\pL+)', '(\p{Xan})', '(\p{Xwd})', '(\p{Xps})', '(\p{Any})', '(\X)', '(\v)', +]; +$mods = ['', 'i', 'u', 'iu', 'm', 's', 'x', 'D', 'n', 'U', 'mD', 'su', 'xu', 'J']; +foreach ($bodies as $b) { + foreach ($mods as $m) { + $pat = '/' . str_replace('/', '\/', $b) . '/' . $m; + if (@preg_match($pat, '') === false) continue; + $p[$pat] = 'adv'; + } + foreach (['#', '~', '{'] as $d) { + $close = $d === '{' ? '}' : $d; + if (str_contains($b, $d) || str_contains($b, $close)) continue; + $pat = $d . $b . $close; + if (@preg_match($pat, '') !== false) $p[$pat] = 'adv'; + } +} +$list = []; +foreach ($p as $k => $v) $list[] = ['pattern' => (string)$k, 'src' => $v]; +file_put_contents($argv[1], json_encode($list, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE)); +echo count($list), "\n"; diff --git a/poc/bin/extract-suites.php b/poc/bin/extract-suites.php new file mode 100644 index 00000000000..08b0725e102 --- /dev/null +++ b/poc/bin/extract-suites.php @@ -0,0 +1,181 @@ + + * holds php-src/ext/pcre/tests (php/php-src, branch PHP-8.6) and + * pcre2/testdata (PCRE2Project/pcre2). + */ +require __DIR__ . '/../../vendor/autoload.php'; + +use PhpParser\Node\Expr\FuncCall; +use PhpParser\Node\Name; +use PhpParser\Node\Scalar\String_; +use PhpParser\NodeFinder; +use PhpParser\ParserFactory; + +[$self, $dir, $outPhp, $outPcre] = $argv; + +/** + * Patterns left out: a \K inside a lookbehind sets the start of the match + * past its end, and preg_replace_callback() of PHP fails on the negative + * length ("Possible integer overflow in memory allocation"). + */ +const LEFT_OUT = ['#a|(?(DEFINE)(?\Ka))(?<=(?&sneaky))b#']; + +/** @param array> $rows */ +function write(string $file, array $rows): void { + $out = []; + foreach ($rows as $pattern => $subjects) { + $pattern = (string) $pattern; + // a pattern JSON cannot hold, raw bytes past UTF-8, is left out + if (in_array($pattern, LEFT_OUT, true) || @preg_match($pattern, '') === false || !mb_check_encoding($pattern, 'UTF-8')) { + continue; + } + $subjects = array_values(array_unique($subjects)); + $out[] = ['pattern' => $pattern, 'subjects' => array_map('base64_encode', array_slice($subjects, 0, 200))]; + } + file_put_contents($file, json_encode($out, JSON_UNESCAPED_SLASHES)); + fwrite(STDERR, basename($file) . ': ' . count($out) . " patterns\n"); +} + +// --- php-src: every preg_* call whose pattern is a literal, with the literal subjects of the file +$parser = (new ParserFactory())->createForHostVersion(); +$finder = new NodeFinder(); +$php = []; +foreach (glob("$dir/php-src/ext/pcre/tests/*.phpt") as $file) { + $text = file_get_contents($file); + if (!preg_match('/--FILE--\R(.*?)(?=\R--[A-Z_]+--)/s', $text, $m)) { + continue; + } + try { + $ast = $parser->parse($m[1]) ?? []; + } catch (Throwable) { + continue; + } + $literals = array_map(static fn (String_ $s): string => $s->value, $finder->findInstanceOf($ast, String_::class)); + foreach ($finder->findInstanceOf($ast, FuncCall::class) as $call) { + if (!$call->name instanceof Name || !str_starts_with($call->name->toLowerString(), 'preg_')) { + continue; + } + $arg = $call->getArgs()[0]->value ?? null; + $patterns = []; + if ($arg instanceof String_) { + $patterns[] = $arg->value; + } elseif ($arg instanceof PhpParser\Node\Expr\Array_) { + foreach ($arg->items as $item) { + if ($item !== null && $item->value instanceof String_) { + $patterns[] = $item->value->value; + } + } + } + foreach ($patterns as $pattern) { + $php[$pattern] = [...($php[$pattern] ?? []), ...$literals]; + } + } +} +write($outPhp, $php); + +// --- PCRE2: each pattern with the subject lines that follow it +function unescape(string $s, bool $utf): string { + $map = ['a' => "\x07", 'b' => "\x08", 'e' => "\x1b", 'f' => "\f", 'n' => "\n", 'r' => "\r", 't' => "\t", 'v' => "\v"]; + $out = ''; + for ($i = 0, $n = strlen($s); $i < $n; $i++) { + $c = $s[$i]; + if ($c !== '\\' || $i + 1 >= $n) { + $out .= $c; + continue; + } + $d = $s[++$i]; + if (isset($map[$d])) { + $out .= $map[$d]; + } elseif ($d === 'x' && ($s[$i + 1] ?? '') === '{' && preg_match('/\G\{([0-9a-fA-F]+)\}/', $s, $mm, 0, $i + 1)) { + $cp = hexdec($mm[1]); + $out .= $utf ? (mb_chr((int) $cp, 'UTF-8') ?: '') : chr((int) $cp & 0xFF); + $i += strlen($mm[0]); + } elseif ($d === 'x' && preg_match('/\G[0-9a-fA-F]{1,2}/', $s, $mm, 0, $i + 1)) { + $out .= chr((int) hexdec($mm[0])); + $i += strlen($mm[0]); + } elseif ($d === 'o' && preg_match('/\G\{([0-7]+)\}/', $s, $mm, 0, $i + 1)) { + $cp = octdec($mm[1]); + $out .= $utf ? (mb_chr((int) $cp, 'UTF-8') ?: '') : chr((int) $cp & 0xFF); + $i += strlen($mm[0]); + } elseif (ctype_digit($d) && preg_match('/\G[0-7]{1,3}/', $s, $mm, 0, $i)) { + $out .= chr((int) octdec($mm[0]) & 0xFF); + $i += strlen($mm[0]) - 1; + } else { + $out .= $d; + } + } + + return $out; +} + +const FLAGS = ['i' => 'i', 'caseless' => 'i', 'm' => 'm', 'multiline' => 'm', 's' => 's', 'dotall' => 's', 'x' => 'x', 'extended' => 'x', + 'n' => 'n', 'no_auto_capture' => 'n', 'utf' => 'u', 'dupnames' => 'J', 'ungreedy' => 'U', 'dollar_endonly' => 'D', 'anchored' => 'A']; +// display-only or match-time modifiers of pcre2test that do not change what one match writes +const IGNORED = ['B', 'I', 'aftertext', 'allaftertext', 'g', 'altglobal', 'mark', 'info', 'jit', 'jitverify', 'fullbincode', 'bincode', + 'debug', 'memory', 'stackguard', 'allusedtext', 'startchar', 'hex', 'expand', 'locale', 'framesize', 'find_limits', 'push', 'pushcopy', + 'callout_info', 'tables', 'use_length', 'posix', 'perlcompat', 'no_start_optimize', 'no_dotstar_anchor', 'no_auto_possess', 'auto_callout', 'S']; + +$pcre = []; +foreach (glob("$dir/pcre2/testdata/testinput*") as $file) { + $lines = preg_split('/\R/', file_get_contents($file)) ?: []; + for ($i = 0, $n = count($lines); $i < $n; $i++) { + $line = $lines[$i]; + if ($line === '' || $line[0] === '#' || ctype_space($line[0]) || ctype_alnum($line[0]) || $line[0] === '\\') { + continue; + } + $delim = $line[0]; + $text = $line; + // a pattern may run over several lines until its closing delimiter + while (!preg_match('/^' . preg_quote($delim, '/') . '((?:\\\\.|[^\\\\])*?)' . preg_quote($delim, '/') . '([^' . preg_quote($delim, '/') . ']*)$/s', $text, $pm) && $i + 1 < $n) { + $text .= "\n" . $lines[++$i]; + } + if (!isset($pm[1])) { + continue; + } + [$body, $mods] = [$pm[1], trim($pm[2])]; + $flags = ''; + $usable = true; + foreach ($mods === '' ? [] : preg_split('/\s*,\s*/', $mods) as $mod) { + $name = explode('=', $mod)[0]; + if (isset(FLAGS[$name])) { + $flags .= FLAGS[$name]; + } elseif (in_array($name, IGNORED, true)) { + continue; + } elseif (preg_match('/^[imsxn]+$/', $name)) { + $flags .= $name; + } else { + $usable = false; + } + } + $subjects = []; + while ($i + 1 < $n && $lines[$i + 1] !== '' && ctype_space($lines[$i + 1][0])) { + $subject = ltrim($lines[++$i]); + if (str_starts_with($subject, '\\=')) { + continue; + } + if (str_contains($subject, '\\=')) { + continue; // per-subject modifiers change the match + } + $subjects[] = unescape($subject, str_contains($flags, 'u')); + } + if (!$usable || str_contains($body, '/')) { + // PHP delimits with "/" below: a pattern holding one keeps its own delimiter only when it is not "/" + if (!$usable) { + continue; + } + } + $phpDelim = str_contains($body, '#') ? '~' : '#'; + if (str_contains($body, $phpDelim)) { + continue; + } + $pattern = $phpDelim . $body . $phpDelim . count_chars($flags, 3); + $pcre[$pattern] = [...($pcre[$pattern] ?? []), ...$subjects]; + } +} +write($outPcre, $pcre); diff --git a/poc/bin/measure.php b/poc/bin/measure.php new file mode 100644 index 00000000000..796e4e658c6 --- /dev/null +++ b/poc/bin/measure.php @@ -0,0 +1,204 @@ + + * + * The checkout is either this branch (PHPRegex) or an upstream 2.3.x + * checkout (the parser generated by phplrt). Writes, in : + * - ..types.jsonl: the type inferred for each pattern and call, + * - ..refusals.jsonl: each engine result the type does not accept, + * with the key it fails on and why, + * - ..stats.json. + * + * Calls measured, each with every flag combination: preg_match(), + * preg_match_all() (matched, and maybe matched), and the subject preg_match() + * narrows. The pcre.jit setting is turned off so a JIT bug of the running + * PCRE2 cannot crash the run. + */ + +[$self, $root, $setFile, $outDir] = $argv + [null, null, null, null]; +if ($root === null || $setFile === null || $outDir === null) { + fwrite(STDERR, "usage: php poc/bin/measure.php \n"); + exit(2); +} + +require $root . '/vendor/autoload.php'; + +use PHPStan\Testing\PHPStanTestCase; +use PHPStan\TrinaryLogic; +use PHPStan\Type\Constant\ConstantIntegerType; +use PHPStan\Type\Constant\ConstantStringType; +use PHPStan\Type\ConstantTypeHelper; +use PHPStan\Type\Php\RegexArrayShapeMatcher; +use PHPStan\Type\Regex\RegexGroupParser; +use PHPStan\Type\Type; +use PHPStan\Type\VerbosityLevel; + +final class PocContainer extends PHPStanTestCase +{ + + public static function get(string $class): object + { + return self::getContainer()->getByType($class); + } + +} + +ini_set('pcre.jit', '0'); + +$side = class_exists('PHPStan\Type\Regex\PHPRegexReader') ? 'phpregex' : 'phplrt'; +$set = basename($setFile, '.json.gz'); +$rows = json_decode((string) file_get_contents('compress.zlib://' . $setFile), true); +@mkdir($outDir, 0777, true); + +/** @var RegexArrayShapeMatcher $matcher */ +$matcher = PocContainer::get(RegexArrayShapeMatcher::class); +/** @var RegexGroupParser $groupParser */ +$groupParser = PocContainer::get(RegexGroupParser::class); +$matchPatternType = new ReflectionMethod($matcher, 'matchPatternType'); + +$O = PREG_OFFSET_CAPTURE; +$N = PREG_UNMATCHED_AS_NULL; +$P = PREG_PATTERN_ORDER; +$S = PREG_SET_ORDER; +$calls = [ + ['match', null], ['match', $O], ['match', $N], ['match', $O | $N], + ['all', null], ['all', $S], ['all', $P | $O], ['all', $S | $O], ['all', $P | $N], ['all', $S | $N], ['all', $P | $O | $N], ['all', $S | $O | $N], + ['allmaybe', null], ['allmaybe', $S], ['allmaybe', $S | $O | $N], ['allmaybe', $P | $O | $N], +]; + +/** + * A type as PHPStan prints it, with the bytes past UTF-8 a class without u + * reads written \xHH, so the JSON keeps them. + */ +function poc_describe(?Type $type): ?string +{ + if ($type === null) { + return null; + } + $text = $type->describe(VerbosityLevel::precise()); + + return mb_check_encoding($text, 'UTF-8') ? $text : (string) preg_replace_callback('/[\x80-\xff]/', static fn (array $byte): string => sprintf('\\x%02x', ord($byte[0])), $text); +} + +$json = static fn (array $row): string => json_encode($row, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE) . "\n"; +$types = fopen("$outDir/$set.$side.types.jsonl", 'w'); +$refusals = fopen("$outDir/$set.$side.refusals.jsonl", 'w'); +$stats = ['patterns' => 0, 'shapes' => 0, 'noShape' => 0, 'checks' => 0, 'refusals' => 0]; + +/** + * Why the type does not accept the engine result: the first key of the + * closest array of the type that fails, and how. + * + * @param array $value + * @return array{key: int|string|null, reason: string} + */ +function poc_reason(Type $type, array $value): array +{ + $best = null; + foreach ($type->getConstantArrays() as $shape) { + $issues = []; + foreach ($value as $key => $item) { + $keyType = is_int($key) ? new ConstantIntegerType($key) : new ConstantStringType($key); + if ($shape->hasOffsetValueType($keyType)->no()) { + $issues[] = ['key' => $key, 'reason' => 'absent']; + continue; + } + $itemType = $shape->getOffsetValueType($keyType); + if (!$itemType->isSuperTypeOf(ConstantTypeHelper::getTypeFromValue($item))->yes()) { + $issues[] = ['key' => $key, 'reason' => poc_holds_falsy($item) && !$itemType->isSuperTypeOf(new ConstantStringType('0'))->yes() ? 'falsy' : 'value']; + } + } + foreach ($shape->getKeyTypes() as $i => $keyType) { + $key = $keyType->getValue(); + if (!array_key_exists($key, $value) && !in_array($i, $shape->getOptionalKeys(), true)) { + $issues[] = ['key' => $key, 'reason' => 'present']; + } + } + if ($issues !== [] && ($best === null || count($issues) < count($best))) { + $best = $issues; + } + } + + return $best[0] ?? ['key' => null, 'reason' => 'value']; +} + +/** + * Whether a value written for a key holds '0' or '', where a falsy + * string typed non-falsy or non-empty is the commonest refusal. + */ +function poc_holds_falsy(mixed $item): bool +{ + if (is_array($item)) { + foreach ($item as $part) { + if (poc_holds_falsy($part)) { + return true; + } + } + + return false; + } + + return $item === '0' || $item === ''; +} + +foreach ($rows as $row) { + $pattern = $row['pattern']; + if (@preg_match($pattern, '') === false) { + continue; + } + $stats['patterns']++; + $subjects = array_map('base64_decode', $row['subjects']); + + $walk = $groupParser->parseGroups($pattern); + $narrow = $walk === null ? null : ($side === 'phpregex' ? $walk->getSubjectNarrowType() : $walk->getSubjectBaseType()); + fwrite($types, $json(['p' => $pattern, 'c' => 'subject', 't' => poc_describe($narrow)])); + if ($narrow !== null) { + foreach ($subjects as $subject) { + if (@preg_match($pattern, $subject) !== 1) { + continue; + } + $stats['checks']++; + if (!$narrow->isSuperTypeOf(new ConstantStringType($subject))->yes()) { + $stats['refusals']++; + fwrite($refusals, $json(['p' => $pattern, 'c' => 'subject', 's' => base64_encode($subject), 'e' => base64_encode(serialize($subject)), 't' => poc_describe($narrow), 'key' => null, 'reason' => 'subject'])); + } + } + } + + foreach ($calls as [$call, $flags]) { + $all = $call !== 'match'; + $type = $matchPatternType->invoke($matcher, new ConstantStringType($pattern), $flags === null ? null : new ConstantIntegerType($flags), $call === 'allmaybe' ? TrinaryLogic::createMaybe() : TrinaryLogic::createYes(), $all); + $label = $call . ':' . ($flags ?? 'null'); + fwrite($types, $json(['p' => $pattern, 'c' => $label, 't' => poc_describe($type)])); + if ($type === null) { + $stats['noShape']++; + continue; + } + $stats['shapes']++; + $seen = []; + foreach ($subjects as $subject) { + $matches = []; + $result = $all ? @preg_match_all($pattern, $subject, $matches, $flags ?? $P) : @preg_match($pattern, $subject, $matches, $flags ?? 0); + if ($result === false || ($call !== 'allmaybe' && $result === 0)) { + continue; + } + $key = serialize($matches); + if (isset($seen[$key])) { + continue; + } + $seen[$key] = true; + $stats['checks']++; + if ($type->isSuperTypeOf(ConstantTypeHelper::getTypeFromValue($matches))->no()) { + $stats['refusals']++; + fwrite($refusals, $json(['p' => $pattern, 'c' => $label, 's' => base64_encode($subject), 'e' => base64_encode($key), 't' => poc_describe($type)] + poc_reason($type, $matches))); + } + } + } +} + +file_put_contents("$outDir/$set.$side.stats.json", json_encode($stats, JSON_PRETTY_PRINT) . "\n"); +fwrite(STDERR, "$set $side " . json_encode($stats) . "\n"); diff --git a/poc/bin/report.php b/poc/bin/report.php new file mode 100644 index 00000000000..4e85590cc13 --- /dev/null +++ b/poc/bin/report.php @@ -0,0 +1,224 @@ + + * + * For every pattern the phplrt side types against the engine, keeps one + * witness, the first call refused, preferring preg_match() without flags, + * and writes it with the PHPRegex type of the same call to + * poc/witnesses/.json and .md. Prints the totals as a Markdown table. + */ + +[$self, $outDir] = $argv + [null, null]; +if ($outDir === null) { + fwrite(STDERR, "usage: php poc/bin/report.php \n"); + exit(2); +} + +const SETS = [ + 'pcre2' => 'PCRE2 test suite (testdata/testinput*)', + 'php-src' => 'php-src ext/pcre tests', + 'corpus' => 'patterns from open-source projects', + 'adversarial' => 'generated edge cases', +]; + +const REASONS = [ + 'falsy' => "a value that can be '0' or '' is typed non-falsy or non-empty", + 'value' => 'a value outside the type of its key', + 'absent' => 'a key the engine writes is typed absent', + 'present' => 'a key the engine leaves out is typed present', + 'subject' => 'the subject is narrowed further than the match proves', +]; + +const FLAGS = [PREG_OFFSET_CAPTURE => 'PREG_OFFSET_CAPTURE', PREG_UNMATCHED_AS_NULL => 'PREG_UNMATCHED_AS_NULL', PREG_SET_ORDER => 'PREG_SET_ORDER']; + +/** + * @return list> + */ +function poc_lines(string $file): array +{ + if (!is_file($file)) { + return []; + } + + return array_map(static fn (string $line): array => json_decode($line, true), file($file, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) ?: []); +} + +function poc_call(string $label): string +{ + if ($label === 'subject') { + return 'subject of preg_match()'; + } + [$call, $flags] = explode(':', $label); + $names = []; + foreach (FLAGS as $flag => $name) { + if ($flags !== 'null' && ((int) $flags & $flag) !== 0) { + $names[] = $name; + } + } + $function = $call === 'match' ? 'preg_match()' : ($call === 'all' ? 'preg_match_all()' : 'preg_match_all(), maybe matched'); + + return $names === [] ? $function : $function . ', ' . implode(' | ', $names); +} + +function poc_cell(string $text): string +{ + // bytes past UTF-8, as a class without u reads them, are written \xHH + if (!mb_check_encoding($text, 'UTF-8')) { + $text = (string) preg_replace_callback('/[\x80-\xff]/', static fn (array $byte): string => sprintf('\\x%02x', ord($byte[0])), $text); + } + + return str_replace(['|', "\n"], ['\\|', '\\n'], $text); +} + +function poc_value(string $serialized): string +{ + $value = unserialize(base64_decode($serialized)); + + $json = json_encode($value, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE); + + return is_string($value) || $json === false ? str_replace("\n", ' ', var_export($value, true)) : $json; +} + +$totals = []; +foreach (SETS as $set => $label) { + $types = []; + foreach (poc_lines("$outDir/$set.phpregex.types.jsonl") as $row) { + $types[$row['p'] . "\0" . $row['c']] = $row['t']; + } + $noShape = ['phplrt' => [], 'phpregex' => []]; + $patterns = []; + foreach (['phplrt', 'phpregex'] as $side) { + foreach (poc_lines("$outDir/$set.$side.types.jsonl") as $row) { + $patterns[$row['p']] = true; + if ($row['c'] === 'match:null' && $row['t'] === null) { + $noShape[$side][$row['p']] = true; + } + } + } + + $witnesses = []; + foreach (poc_lines("$outDir/$set.phplrt.refusals.jsonl") as $row) { + $known = $witnesses[$row['p']] ?? null; + if ($known !== null && ($known['call'] === 'match:null' || $row['c'] !== 'match:null')) { + continue; + } + $witnesses[$row['p']] = [ + 'pattern' => $row['p'], + 'call' => $row['c'], + 'subject' => $row['s'], + 'engine' => $row['e'], + 'reason' => $row['reason'], + 'key' => $row['key'], + 'phplrt' => $row['t'], + 'phpregex' => $types[$row['p'] . "\0" . $row['c']] ?? null, + ]; + } + $witnesses = array_values($witnesses); + $refusedOnPhpregex = []; + foreach (poc_lines("$outDir/$set.phpregex.refusals.jsonl") as $row) { + $refusedOnPhpregex[$row['p']] = true; + } + + file_put_contents(__DIR__ . "/../witnesses/$set.json", json_encode($witnesses, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_INVALID_UTF8_SUBSTITUTE) . "\n"); + + $reasons = array_count_values(array_column($witnesses, 'reason')); + $md = "# $label\n\nOne witness for each of the " . count($witnesses) . " patterns whose inferred type the engine contradicts with the parser generated by phplrt. `php poc/bin/verify.php` replays them.\n\n"; + foreach (REASONS as $reason => $text) { + $md .= '- ' . $text . ': ' . ($reasons[$reason] ?? 0) . "\n"; + } + $md .= "\n| pattern | call | subject | the engine writes | phplrt | PHPRegex | why |\n|---|---|---|---|---|---|---|\n"; + foreach ($witnesses as $w) { + $md .= sprintf( + "| `%s` | %s | `%s` | `%s` | `%s` | `%s` | %s |\n", + poc_cell($w['pattern']), + poc_call($w['call']), + poc_cell(var_export(base64_decode($w['subject']), true)), + poc_cell(poc_value($w['engine'])), + poc_cell((string) $w['phplrt']), + poc_cell($w['phpregex'] ?? 'no shape'), + ($w['key'] === null ? '' : 'key `' . $w['key'] . '`: ') . REASONS[$w['reason']], + ); + } + $md .= "\n## Patterns the parser generated by phplrt infers no shape for\n\n" . count($noShape['phplrt']) . " patterns, `preg_match()` without flags.\n\n| pattern | PHPRegex |\n|---|---|\n"; + foreach (array_keys($noShape['phplrt']) as $pattern) { + $md .= sprintf("| `%s` | `%s` |\n", poc_cell($pattern), poc_cell($types[$pattern . "\0match:null"] ?? 'no shape')); + } + file_put_contents(__DIR__ . "/../witnesses/$set.md", $md); + + $totals[$set] = [ + 'patterns' => count($patterns), + 'phplrt' => count($witnesses), + 'phpregex' => count($refusedOnPhpregex), + 'phplrtNoShape' => count($noShape['phplrt']), + 'phpregexNoShape' => count($noShape['phpregex']), + ]; +} + +$table = "| | phplrt | PHPRegex |\n|---|---|---|\n"; +foreach ($totals as $set => $t) { + $table .= sprintf("| %s (%s patterns): types the engine contradicts | %d | %d |\n", SETS[$set], number_format($t['patterns']), $t['phplrt'], $t['phpregex']); +} +foreach ($totals as $set => $t) { + $table .= sprintf("| %s: patterns with no `preg_match()` shape | %d | %d |\n", SETS[$set], $t['phplrtNoShape'], $t['phpregexNoShape']); +} + +// Each contradicted pattern once, under the first family it matches, in this order. +$families = [ + ['a `(?#...)` comment is read as pattern syntax', static fn (array $w, string $p, string $m): bool => str_contains($p, '(?#')], + ['a lone `\\E` is read as a quantifiable atom', static fn (array $w, string $p, string $m): bool => preg_match('/\\\\E/', $p) === 1 && !str_contains($p, '\\Q')], + ['`\\Q...\\E` is read as pattern syntax', static fn (array $w, string $p, string $m): bool => str_contains($p, '\\Q')], + ['a `MARK` name under `/x` loses the `#comment` the engine keeps in it', static fn (array $w, string $p, string $m): bool => $w['key'] === 'MARK'], + ['a class without `/u` is read as characters where the engine reads bytes', static fn (array $w, string $p, string $m): bool => preg_match('/[\x80-\xff]/', $p) === 1 && !str_contains($m, 'u') && $w['reason'] === 'value'], + ['`(?n)` and `/n`: groups that do not capture are typed as captured', static fn (array $w, string $p, string $m): bool => str_contains($p, '(?n') || str_contains($m, 'n')], + ['an inline `(?x)` is not read: skipped whitespace is typed as text', static fn (array $w, string $p, string $m): bool => str_contains($p, '(?x)')], + ['an inline `(?i)`, `(?i:...)` or `(?^i)` is not read: a caseless match is typed by its spelling', static fn (array $w, string $p, string $m): bool => preg_match('/\(\?\^?i[):]/', $p) === 1 && $w['reason'] === 'value'], + ['`\\d` and `[[:digit:]]` under `/u` or `(*UCP)` read Unicode digits, which are not numeric strings', static fn (array $w, string $p, string $m): bool => $w['reason'] === 'value' && preg_match('/\\\\d|\[:digit:\]|\\\\p\{?N/', $p) === 1], + ["a value that can be `'0'` or `''` is typed `non-falsy-string` or `non-empty-string`: `\\w`, `\\S`, `.`, `\\p{N}`, `\\X`, `[[:alnum:]]`, negated classes, lookbehinds", static fn (array $w, string $p, string $m): bool => $w['reason'] === 'falsy'], +]; +$examples = ['/\pN/', '/(\d)/u', '/(?i)(a)/', '/(?x)( a )/', '/\Q(a)\E/', '/(?n)(a)(?b)/']; +$all = []; +foreach (array_keys(SETS) as $set) { + foreach (json_decode((string) file_get_contents(__DIR__ . "/../witnesses/$set.json"), true) as $w) { + $all[] = $w; + } +} +$used = []; +$familyTable = "| what the generated parser reads differently | patterns | witness | the engine writes | phplrt | PHPRegex |\n|---|---|---|---|---|---|\n"; +foreach ($families as [$label, $test]) { + $members = []; + foreach ($all as $i => $w) { + $modifiers = preg_match('/[a-zA-Z]*$/', $w['pattern'], $tail) === 1 ? $tail[0] : ''; + if (!isset($used[$i]) && $test($w, $w['pattern'], $modifiers)) { + $members[] = $w; + $used[$i] = true; + } + } + if ($members === []) { + continue; + } + $example = $members[0]; + foreach ($members as $w) { + if (in_array($w['pattern'], $examples, true)) { + $example = $w; + break; + } + } + $familyTable .= sprintf( + "| %s | %d | `%s` on `%s` | `%s` | `%s` | `%s` |\n", + ucfirst($label), + count($members), + poc_cell($example['pattern']), + poc_cell(var_export(base64_decode($example['subject']), true)), + poc_cell(poc_value($example['engine'])), + poc_cell((string) $example['phplrt']), + poc_cell($example['phpregex'] ?? 'no shape'), + ); +} + +$readme = (string) file_get_contents(__DIR__ . '/../README.template.md'); +file_put_contents(__DIR__ . '/../README.md', strtr($readme, ['{totals}' => rtrim($table), '{families}' => rtrim($familyTable)])); +echo $table, "\n", $familyTable; diff --git a/poc/bin/subjects.php b/poc/bin/subjects.php new file mode 100644 index 00000000000..40b51cf7bd4 --- /dev/null +++ b/poc/bin/subjects.php @@ -0,0 +1,59 @@ + php poc/bin/subjects.php + * It runs with the PHPRegex monorepo (https://github.com/php-regex/php-regex), + * whose generator is not part of the parser package this branch installs. + */ +require getenv('PHPREGEX_ROOT') . '/vendor/autoload.php'; +use PHPRegex\Generator\SampleGenerator; +ini_set('pcre.jit', '0'); +$in = json_decode(file_get_contents($argv[1]), true); +$fixed = ['', '0', '1', '00', '01', '10', 'a', 'A', 'b', 'c', 'x', 'y', 'z', 'ab', 'aa', 'abc', 'cd', "a\n", "\n", "a\nb", "\na", "\r\n", "a\r\n", "0\n", "1\n", ' ', ' 1', '1 ', '-1', '-', '1.5', '.5', '1.', '٣', '١٢', '0٣', '0', '12', '߀', '१', "\u{212A}", 'k', 'K', "\u{17F}", 's', 'S', 'ss', 'ß', 'ẞ', 'é', 'É', "e\u{301}", 'ı', 'İ', 'i', 'I', 'σ', 'Σ', 'ς', 'st', 'st', "\x00", "\xff", "a\xffb", "\xc3", '#', 'a#b', 'm', 'n', 'aab', 'aba', 'bab', 'xy', 'xyz', 'ba', 'bc', 'ac', 'abab', '123', '2026-10', '0.0', 'foo', 'bar', '_', '0a', 'a0', 'ab0', '٠', '9', '٩']; +$out = []; +$t0 = microtime(true); +foreach ($in as $i => $row) { + $pat = $row['pattern']; + $subs = []; + $gen = new SampleGenerator(3); + for ($s = 1; $s <= 10; $s++) { + $gen->setSeed(crc32($pat) + $s); + try { $subs[] = $gen->generate($pat); } catch (\Throwable $e) { break; } + } + $samples = $subs; + // mutations of generated samples + foreach ($samples as $smp) { + $subs[] = $smp . "\n"; + $subs[] = "\n" . $smp; + $subs[] = 'x' . $smp . 'x'; + $subs[] = substr($smp, 0, -1); + $subs[] = substr($smp, 1); + $subs[] = strtoupper($smp); + $subs[] = strtolower($smp); + $subs[] = strtr($smp, '0123456789', '0000000000'); + $subs[] = str_replace(['0','1','2','3','4','5','6','7','8','9'], ['٠','١','٢','٣','٤','٥','٦','٧','٨','٩'], $smp); + $subs[] = str_replace(['k','K','s','S'], ["\u{212A}", "\u{212A}", "\u{17F}", "\u{17F}"], $smp); + $subs[] = $smp . $smp; + } + // literals of the body + $body = $pat; + preg_match_all('/[A-Za-z0-9]+/', $body, $lm); + foreach (array_slice(array_unique($lm[0]), 0, 12) as $lit) { $subs[] = $lit; $subs[] = $lit . "\n"; } + // random over alphabet + $alpha = array_values(array_unique(array_merge(mb_str_split(preg_replace('/[\\\\()\[\]{}?*+|^$]/', '', $body) ?? '') ?: [], ['0', '1', 'a', 'b', ' ', "\n", '٣', 'K']))); + mt_srand(crc32($pat)); + for ($r = 0; $r < 25; $r++) { + $len = mt_rand(0, 8); $s = ''; + for ($k = 0; $k < $len; $k++) $s .= $alpha[mt_rand(0, count($alpha) - 1)]; + $subs[] = $s; + } + $subs = array_merge($subs, $fixed); + $subs = array_values(array_unique(array_map('strval', $subs))); + $out[] = ['pattern' => $pat, 'src' => $row['src'], 'samples' => count($samples), 'subjects' => array_map('base64_encode', $subs)]; + if ($i % 500 === 0) fwrite(STDERR, "$i " . round(microtime(true) - $t0) . "s\n"); +} +file_put_contents($argv[2], json_encode($out, JSON_UNESCAPED_SLASHES)); diff --git a/poc/bin/verify.php b/poc/bin/verify.php new file mode 100644 index 00000000000..1001ba7efdc --- /dev/null +++ b/poc/bin/verify.php @@ -0,0 +1,90 @@ + + * + * For each witness, the engine runs the call again and must write what the + * witness recorded; the type the checkout infers for the call then accepts + * the result or refuses it. On an upstream 2.3.x checkout every witness is + * refused; on this branch every witness is accepted. + */ + +[$self, $root] = $argv + [null, null]; +if ($root === null) { + fwrite(STDERR, "usage: php poc/bin/verify.php \n"); + exit(2); +} + +require $root . '/vendor/autoload.php'; + +use PHPStan\Testing\PHPStanTestCase; +use PHPStan\TrinaryLogic; +use PHPStan\Type\Constant\ConstantIntegerType; +use PHPStan\Type\Constant\ConstantStringType; +use PHPStan\Type\ConstantTypeHelper; +use PHPStan\Type\Php\RegexArrayShapeMatcher; +use PHPStan\Type\Regex\RegexGroupParser; + +final class PocContainer extends PHPStanTestCase +{ + + public static function get(string $class): object + { + return self::getContainer()->getByType($class); + } + +} + +ini_set('pcre.jit', '0'); + +$side = class_exists('PHPStan\Type\Regex\PHPRegexReader') ? 'PHPRegex' : 'phplrt'; +/** @var RegexArrayShapeMatcher $matcher */ +$matcher = PocContainer::get(RegexArrayShapeMatcher::class); +/** @var RegexGroupParser $groupParser */ +$groupParser = PocContainer::get(RegexGroupParser::class); +$matchPatternType = new ReflectionMethod($matcher, 'matchPatternType'); + +$engineDiffers = 0; +foreach (glob(__DIR__ . '/../witnesses/*.json') ?: [] as $file) { + $counts = ['accepted' => 0, 'refused' => 0, 'maybe' => 0, 'no shape' => 0]; + foreach (json_decode((string) file_get_contents($file), true) as $w) { + $pattern = $w['pattern']; + $subject = base64_decode($w['subject']); + if ($w['call'] === 'subject') { + $written = $subject; + $result = @preg_match($pattern, $subject); + $walk = $groupParser->parseGroups($pattern); + $type = $walk === null ? null : ($side === 'PHPRegex' ? $walk->getSubjectNarrowType() : $walk->getSubjectBaseType()); + $value = new ConstantStringType($subject); + } else { + [$call, $flags] = explode(':', $w['call']); + $flags = $flags === 'null' ? null : (int) $flags; + $written = []; + $result = $call === 'match' + ? @preg_match($pattern, $subject, $written, $flags ?? 0) + : @preg_match_all($pattern, $subject, $written, $flags ?? PREG_PATTERN_ORDER); + $type = $matchPatternType->invoke($matcher, new ConstantStringType($pattern), $flags === null ? null : new ConstantIntegerType($flags), $call === 'allmaybe' ? TrinaryLogic::createMaybe() : TrinaryLogic::createYes(), $call !== 'match'); + $value = ConstantTypeHelper::getTypeFromValue($written); + } + + if ($result === false || serialize($written) !== base64_decode($w['engine'])) { + $engineDiffers++; + fwrite(STDERR, sprintf("the engine of this PHP (PCRE2 %s) writes something else for %s on %s\n", PCRE_VERSION, $pattern, var_export($subject, true))); + continue; + } + + if ($type === null) { + $counts['no shape']++; + continue; + } + $verdict = $type->isSuperTypeOf($value); + $counts[$verdict->yes() ? 'accepted' : ($verdict->no() ? 'refused' : 'maybe')]++; + } + printf("%-12s %s: %s\n", basename($file, '.json'), $side, json_encode($counts)); +} + +if ($engineDiffers > 0) { + fwrite(STDERR, "$engineDiffers witnesses were recorded with another PCRE2 release and were skipped\n"); +} diff --git a/poc/data/adversarial.json.gz b/poc/data/adversarial.json.gz new file mode 100644 index 00000000000..950709899a1 Binary files /dev/null and b/poc/data/adversarial.json.gz differ diff --git a/poc/data/corpus.json.gz b/poc/data/corpus.json.gz new file mode 100644 index 00000000000..d916f7ad442 Binary files /dev/null and b/poc/data/corpus.json.gz differ diff --git a/poc/data/pcre2.json.gz b/poc/data/pcre2.json.gz new file mode 100644 index 00000000000..d29302d4a0c Binary files /dev/null and b/poc/data/pcre2.json.gz differ diff --git a/poc/data/php-src.json.gz b/poc/data/php-src.json.gz new file mode 100644 index 00000000000..b5fc4c4f070 Binary files /dev/null and b/poc/data/php-src.json.gz differ diff --git a/poc/witnesses/adversarial.json b/poc/witnesses/adversarial.json new file mode 100644 index 00000000000..ee832481c34 --- /dev/null +++ b/poc/witnesses/adversarial.json @@ -0,0 +1,3082 @@ +[ + { + "pattern": "/(\\d)/u", + "call": "match:null", + "subject": "2aM=", + "engine": "YToyOntpOjA7czoyOiLZoyI7aToxO3M6Mjoi2aMiO30=", + "reason": "value", + "key": 1, + "phplrt": "array{non-empty-string, numeric-string}", + "phpregex": "array{non-empty-string, non-empty-string}" + }, + { + "pattern": "/(\\d)/iu", + "call": "match:null", + "subject": "2ac=", + "engine": "YToyOntpOjA7czoyOiLZpyI7aToxO3M6Mjoi2aciO30=", + "reason": "value", + "key": 1, + "phplrt": "array{non-empty-string, numeric-string}", + "phpregex": "array{non-empty-string, non-empty-string}" + }, + { + "pattern": "/(\\d)/su", + "call": "match:null", + "subject": "2ag=", + "engine": "YToyOntpOjA7czoyOiLZqCI7aToxO3M6Mjoi2agiO30=", + "reason": "value", + "key": 1, + "phplrt": "array{non-empty-string, numeric-string}", + "phpregex": "array{non-empty-string, non-empty-string}" + }, + { + "pattern": "/(\\d)/xu", + "call": "match:null", + "subject": "2aY=", + "engine": "YToyOntpOjA7czoyOiLZpiI7aToxO3M6Mjoi2aYiO30=", + "reason": "value", + "key": 1, + "phplrt": "array{non-empty-string, numeric-string}", + "phpregex": "array{non-empty-string, non-empty-string}" + }, + { + "pattern": "/(\\d+)/u", + "call": "match:null", + "subject": "2aXZoA==", + "engine": "YToyOntpOjA7czo0OiLZpdmgIjtpOjE7czo0OiLZpdmgIjt9", + "reason": "value", + "key": 1, + "phplrt": "array{non-empty-string, numeric-string}", + "phpregex": "array{non-empty-string, non-empty-string}" + }, + { + "pattern": "/(\\d+)/iu", + "call": "match:null", + "subject": "2aA=", + "engine": "YToyOntpOjA7czoyOiLZoCI7aToxO3M6Mjoi2aAiO30=", + "reason": "value", + "key": 1, + "phplrt": "array{non-empty-string, numeric-string}", + "phpregex": "array{non-empty-string, non-empty-string}" + }, + { + "pattern": "/(\\d+)/su", + "call": "match:null", + "subject": "2aDZpA==", + "engine": "YToyOntpOjA7czo0OiLZoNmkIjtpOjE7czo0OiLZoNmkIjt9", + "reason": "value", + "key": 1, + "phplrt": "array{non-empty-string, numeric-string}", + "phpregex": "array{non-empty-string, non-empty-string}" + }, + { + "pattern": "/(\\d+)/xu", + "call": "match:null", + "subject": "2aM=", + "engine": "YToyOntpOjA7czoyOiLZoyI7aToxO3M6Mjoi2aMiO30=", + "reason": "value", + "key": 1, + "phplrt": "array{non-empty-string, numeric-string}", + "phpregex": "array{non-empty-string, non-empty-string}" + }, + { + "pattern": "/(\\d*)/u", + "call": "match:null", + "subject": "2aE=", + "engine": "YToyOntpOjA7czoyOiLZoSI7aToxO3M6Mjoi2aEiO30=", + "reason": "value", + "key": 1, + "phplrt": "array{string, ''|numeric-string}", + "phpregex": "array{string, string}" + }, + { + "pattern": "/(\\d*)/iu", + "call": "match:null", + "subject": "2aA=", + "engine": "YToyOntpOjA7czoyOiLZoCI7aToxO3M6Mjoi2aAiO30=", + "reason": "value", + "key": 1, + "phplrt": "array{string, ''|numeric-string}", + "phpregex": "array{string, string}" + }, + { + "pattern": "/(\\d*)/su", + "call": "match:null", + "subject": "2abZqQ==", + "engine": "YToyOntpOjA7czo0OiLZptmpIjtpOjE7czo0OiLZptmpIjt9", + "reason": "value", + "key": 1, + "phplrt": "array{string, ''|numeric-string}", + "phpregex": "array{string, string}" + }, + { + "pattern": "/(\\d*)/xu", + "call": "match:null", + "subject": "2ak=", + "engine": "YToyOntpOjA7czoyOiLZqSI7aToxO3M6Mjoi2akiO30=", + "reason": "value", + "key": 1, + "phplrt": "array{string, ''|numeric-string}", + "phpregex": "array{string, string}" + }, + { + "pattern": "/^(\\d+)$/u", + "call": "match:null", + "subject": "2abZoA==", + "engine": "YToyOntpOjA7czo0OiLZptmgIjtpOjE7czo0OiLZptmgIjt9", + "reason": "value", + "key": 1, + "phplrt": "array{non-empty-string, numeric-string}", + "phpregex": "array{non-empty-string, non-empty-string}" + }, + { + "pattern": "/^(\\d+)$/iu", + "call": "match:null", + "subject": "2aPZoNmo", + "engine": "YToyOntpOjA7czo2OiLZo9mg2agiO2k6MTtzOjY6Itmj2aDZqCI7fQ==", + "reason": "value", + "key": 1, + "phplrt": "array{non-empty-string, numeric-string}", + "phpregex": "array{non-empty-string, non-empty-string}" + }, + { + "pattern": "/^(\\d+)$/su", + "call": "match:null", + "subject": "2afZodmm", + "engine": "YToyOntpOjA7czo2OiLZp9mh2aYiO2k6MTtzOjY6Itmn2aHZpiI7fQ==", + "reason": "value", + "key": 1, + "phplrt": "array{non-empty-string, numeric-string}", + "phpregex": "array{non-empty-string, non-empty-string}" + }, + { + "pattern": "/^(\\d+)$/xu", + "call": "match:null", + "subject": "2abZqA==", + "engine": "YToyOntpOjA7czo0OiLZptmoIjtpOjE7czo0OiLZptmoIjt9", + "reason": "value", + "key": 1, + "phplrt": "array{non-empty-string, numeric-string}", + "phpregex": "array{non-empty-string, non-empty-string}" + }, + { + "pattern": "/(\\d{2})/u", + "call": "match:null", + "subject": "2aLZpA==", + "engine": "YToyOntpOjA7czo0OiLZotmkIjtpOjE7czo0OiLZotmkIjt9", + "reason": "value", + "key": 1, + "phplrt": "array{non-falsy-string, non-falsy-string&numeric-string}", + "phpregex": "array{non-falsy-string, non-falsy-string}" + }, + { + "pattern": "/(\\d{2})/iu", + "call": "match:null", + "subject": "2aTZpw==", + "engine": "YToyOntpOjA7czo0OiLZpNmnIjtpOjE7czo0OiLZpNmnIjt9", + "reason": "value", + "key": 1, + "phplrt": "array{non-falsy-string, non-falsy-string&numeric-string}", + "phpregex": "array{non-falsy-string, non-falsy-string}" + }, + { + "pattern": "/(\\d{2})/su", + "call": "match:null", + "subject": "2aDZpQ==", + "engine": "YToyOntpOjA7czo0OiLZoNmlIjtpOjE7czo0OiLZoNmlIjt9", + "reason": "value", + "key": 1, + "phplrt": "array{non-falsy-string, non-falsy-string&numeric-string}", + "phpregex": "array{non-falsy-string, non-falsy-string}" + }, + { + "pattern": "/(\\d{2})/xu", + "call": "match:null", + "subject": "2aHZog==", + "engine": "YToyOntpOjA7czo0OiLZodmiIjtpOjE7czo0OiLZodmiIjt9", + "reason": "value", + "key": 1, + "phplrt": "array{non-falsy-string, non-falsy-string&numeric-string}", + "phpregex": "array{non-falsy-string, non-falsy-string}" + }, + { + "pattern": "/([[:digit:]]+)/u", + "call": "match:null", + "subject": "2aPZozk=", + "engine": "YToyOntpOjA7czo1OiLZo9mjOSI7aToxO3M6NToi2aPZozkiO30=", + "reason": "value", + "key": 1, + "phplrt": "array{non-empty-string, numeric-string}", + "phpregex": "array{non-empty-string, non-empty-string}" + }, + { + "pattern": "/([[:digit:]]+)/iu", + "call": "match:null", + "subject": "77yQ77yQ", + "engine": "YToyOntpOjA7czo2OiLvvJDvvJAiO2k6MTtzOjY6Iu+8kO+8kCI7fQ==", + "reason": "value", + "key": 1, + "phplrt": "array{non-empty-string, numeric-string}", + "phpregex": "array{non-empty-string, non-empty-string}" + }, + { + "pattern": "/([[:digit:]]+)/su", + "call": "match:null", + "subject": "77yQMDM0", + "engine": "YToyOntpOjA7czo2OiLvvJAwMzQiO2k6MTtzOjY6Iu+8kDAzNCI7fQ==", + "reason": "value", + "key": 1, + "phplrt": "array{non-empty-string, numeric-string}", + "phpregex": "array{non-empty-string, non-empty-string}" + }, + { + "pattern": "/([[:digit:]]+)/xu", + "call": "match:null", + "subject": "2aM=", + "engine": "YToyOntpOjA7czoyOiLZoyI7aToxO3M6Mjoi2aMiO30=", + "reason": "value", + "key": 1, + "phplrt": "array{non-empty-string, numeric-string}", + "phpregex": "array{non-empty-string, non-empty-string}" + }, + { + "pattern": "/([\\d]+)/u", + "call": "match:null", + "subject": "2aDZodmg2aA=", + "engine": "YToyOntpOjA7czo4OiLZoNmh2aDZoCI7aToxO3M6ODoi2aDZodmg2aAiO30=", + "reason": "value", + "key": 1, + "phplrt": "array{non-empty-string, numeric-string}", + "phpregex": "array{non-empty-string, non-empty-string}" + }, + { + "pattern": "/([\\d]+)/iu", + "call": "match:null", + "subject": "2aHZotmg2ac=", + "engine": "YToyOntpOjA7czo4OiLZodmi2aDZpyI7aToxO3M6ODoi2aHZotmg2aciO30=", + "reason": "value", + "key": 1, + "phplrt": "array{non-empty-string, numeric-string}", + "phpregex": "array{non-empty-string, non-empty-string}" + }, + { + "pattern": "/([\\d]+)/su", + "call": "match:null", + "subject": "2aM=", + "engine": "YToyOntpOjA7czoyOiLZoyI7aToxO3M6Mjoi2aMiO30=", + "reason": "value", + "key": 1, + "phplrt": "array{non-empty-string, numeric-string}", + "phpregex": "array{non-empty-string, non-empty-string}" + }, + { + "pattern": "/([\\d]+)/xu", + "call": "match:null", + "subject": "2aA=", + "engine": "YToyOntpOjA7czoyOiLZoCI7aToxO3M6Mjoi2aAiO30=", + "reason": "value", + "key": 1, + "phplrt": "array{non-empty-string, numeric-string}", + "phpregex": "array{non-empty-string, non-empty-string}" + }, + { + "pattern": "/(\\d)\\s/u", + "call": "match:null", + "subject": "2aUJ", + "engine": "YToyOntpOjA7czozOiLZpQkiO2k6MTtzOjI6ItmlIjt9", + "reason": "value", + "key": 1, + "phplrt": "array{non-falsy-string, numeric-string}", + "phpregex": "array{non-falsy-string, non-empty-string}" + }, + { + "pattern": "/(\\d)\\s/iu", + "call": "match:null", + "subject": "2aUJ", + "engine": "YToyOntpOjA7czozOiLZpQkiO2k6MTtzOjI6ItmlIjt9", + "reason": "value", + "key": 1, + "phplrt": "array{non-falsy-string, numeric-string}", + "phpregex": "array{non-falsy-string, non-empty-string}" + }, + { + "pattern": "/(\\d)\\s/su", + "call": "match:null", + "subject": "2aMK", + "engine": "YToyOntpOjA7czozOiLZowoiO2k6MTtzOjI6ItmjIjt9", + "reason": "value", + "key": 1, + "phplrt": "array{non-falsy-string, numeric-string}", + "phpregex": "array{non-falsy-string, non-empty-string}" + }, + { + "pattern": "/(\\d)\\s/xu", + "call": "match:null", + "subject": "2aQJ", + "engine": "YToyOntpOjA7czozOiLZpAkiO2k6MTtzOjI6ItmkIjt9", + "reason": "value", + "key": 1, + "phplrt": "array{non-falsy-string, numeric-string}", + "phpregex": "array{non-falsy-string, non-empty-string}" + }, + { + "pattern": "/(\\d)(\\d)/u", + "call": "match:null", + "subject": "2ajZpg==", + "engine": "YTozOntpOjA7czo0OiLZqNmmIjtpOjE7czoyOiLZqCI7aToyO3M6Mjoi2aYiO30=", + "reason": "value", + "key": 1, + "phplrt": "array{non-falsy-string, numeric-string, numeric-string}", + "phpregex": "array{non-falsy-string, non-empty-string, non-empty-string}" + }, + { + "pattern": "/(\\d)(\\d)/iu", + "call": "match:null", + "subject": "2aLZow==", + "engine": "YTozOntpOjA7czo0OiLZotmjIjtpOjE7czoyOiLZoiI7aToyO3M6Mjoi2aMiO30=", + "reason": "value", + "key": 1, + "phplrt": "array{non-falsy-string, numeric-string, numeric-string}", + "phpregex": "array{non-falsy-string, non-empty-string, non-empty-string}" + }, + { + "pattern": "/(\\d)(\\d)/su", + "call": "match:null", + "subject": "2aTZoA==", + "engine": "YTozOntpOjA7czo0OiLZpNmgIjtpOjE7czoyOiLZpCI7aToyO3M6Mjoi2aAiO30=", + "reason": "value", + "key": 1, + "phplrt": "array{non-falsy-string, numeric-string, numeric-string}", + "phpregex": "array{non-falsy-string, non-empty-string, non-empty-string}" + }, + { + "pattern": "/(\\d)(\\d)/xu", + "call": "match:null", + "subject": "2anZpg==", + "engine": "YTozOntpOjA7czo0OiLZqdmmIjtpOjE7czoyOiLZqSI7aToyO3M6Mjoi2aYiO30=", + "reason": "value", + "key": 1, + "phplrt": "array{non-falsy-string, numeric-string, numeric-string}", + "phpregex": "array{non-falsy-string, non-empty-string, non-empty-string}" + }, + { + "pattern": "/(\\d+)|(x)/u", + "call": "match:null", + "subject": "2ac=", + "engine": "YToyOntpOjA7czoyOiLZpyI7aToxO3M6Mjoi2aciO30=", + "reason": "value", + "key": 1, + "phplrt": "array{non-empty-string, '', 'x'}|array{non-empty-string, numeric-string}", + "phpregex": "array{non-empty-string, '', 'x'}|array{non-empty-string, non-empty-string}" + }, + { + "pattern": "/(\\d+)|(x)/iu", + "call": "match:null", + "subject": "2aDZoNmp2ac=", + "engine": "YToyOntpOjA7czo4OiLZoNmg2anZpyI7aToxO3M6ODoi2aDZoNmp2aciO30=", + "reason": "value", + "key": 1, + "phplrt": "array{non-empty-string, '', non-empty-string}|array{non-empty-string, numeric-string}", + "phpregex": "array{non-empty-string, '', non-falsy-string}|array{non-empty-string, non-empty-string}" + }, + { + "pattern": "/(\\d+)|(x)/su", + "call": "match:null", + "subject": "2afZptmo", + "engine": "YToyOntpOjA7czo2OiLZp9mm2agiO2k6MTtzOjY6Itmn2abZqCI7fQ==", + "reason": "value", + "key": 1, + "phplrt": "array{non-empty-string, '', 'x'}|array{non-empty-string, numeric-string}", + "phpregex": "array{non-empty-string, '', 'x'}|array{non-empty-string, non-empty-string}" + }, + { + "pattern": "/(\\d+)|(x)/xu", + "call": "match:null", + "subject": "2aXZo9ml", + "engine": "YToyOntpOjA7czo2OiLZpdmj2aUiO2k6MTtzOjY6Itml2aPZpSI7fQ==", + "reason": "value", + "key": 1, + "phplrt": "array{non-empty-string, '', 'x'}|array{non-empty-string, numeric-string}", + "phpregex": "array{non-empty-string, '', 'x'}|array{non-empty-string, non-empty-string}" + }, + { + "pattern": "/(?\\d+)/u", + "call": "match:null", + "subject": "2aLZpdmj2ac=", + "engine": "YTozOntpOjA7czo4OiLZotml2aPZpyI7czoxOiJuIjtzOjg6Itmi2aXZo9mnIjtpOjE7czo4OiLZotml2aPZpyI7fQ==", + "reason": "value", + "key": "n", + "phplrt": "array{0: non-empty-string, n: numeric-string, 1: numeric-string}", + "phpregex": "array{0: non-empty-string, n: non-empty-string, 1: non-empty-string}" + }, + { + "pattern": "/(?\\d+)/iu", + "call": "match:null", + "subject": "2aLZqNmj", + "engine": "YTozOntpOjA7czo2OiLZotmo2aMiO3M6MToibiI7czo2OiLZotmo2aMiO2k6MTtzOjY6Itmi2ajZoyI7fQ==", + "reason": "value", + "key": "n", + "phplrt": "array{0: non-empty-string, n: numeric-string, 1: numeric-string}", + "phpregex": "array{0: non-empty-string, n: non-empty-string, 1: non-empty-string}" + }, + { + "pattern": "/(?\\d+)/su", + "call": "match:null", + "subject": "2aXZp9mh2ak=", + "engine": "YTozOntpOjA7czo4OiLZpdmn2aHZqSI7czoxOiJuIjtzOjg6Itml2afZodmpIjtpOjE7czo4OiLZpdmn2aHZqSI7fQ==", + "reason": "value", + "key": "n", + "phplrt": "array{0: non-empty-string, n: numeric-string, 1: numeric-string}", + "phpregex": "array{0: non-empty-string, n: non-empty-string, 1: non-empty-string}" + }, + { + "pattern": "/(?\\d+)/xu", + "call": "match:null", + "subject": "2aPZog==", + "engine": "YTozOntpOjA7czo0OiLZo9miIjtzOjE6Im4iO3M6NDoi2aPZoiI7aToxO3M6NDoi2aPZoiI7fQ==", + "reason": "value", + "key": "n", + "phplrt": "array{0: non-empty-string, n: numeric-string, 1: numeric-string}", + "phpregex": "array{0: non-empty-string, n: non-empty-string, 1: non-empty-string}" + }, + { + "pattern": "/(\\N{U+0030})/u", + "call": "match:null", + "subject": "MA==", + "engine": "YToyOntpOjA7czoxOiIwIjtpOjE7czoxOiIwIjt9", + "reason": "falsy", + "key": 0, + "phplrt": "array{non-falsy-string, non-falsy-string}", + "phpregex": "array{'0', '0'}" + }, + { + "pattern": "/(\\N{U+0030})/iu", + "call": "match:null", + "subject": "MA==", + "engine": "YToyOntpOjA7czoxOiIwIjtpOjE7czoxOiIwIjt9", + "reason": "falsy", + "key": 0, + "phplrt": "array{non-falsy-string, non-falsy-string}", + "phpregex": "array{'0', '0'}" + }, + { + "pattern": "/(\\N{U+0030})/su", + "call": "match:null", + "subject": "MA==", + "engine": "YToyOntpOjA7czoxOiIwIjtpOjE7czoxOiIwIjt9", + "reason": "falsy", + "key": 0, + "phplrt": "array{non-falsy-string, non-falsy-string}", + "phpregex": "array{'0', '0'}" + }, + { + "pattern": "/(\\N{U+0030})/xu", + "call": "match:null", + "subject": "MA==", + "engine": "YToyOntpOjA7czoxOiIwIjtpOjE7czoxOiIwIjt9", + "reason": "falsy", + "key": 0, + "phplrt": "array{non-falsy-string, non-falsy-string}", + "phpregex": "array{'0', '0'}" + }, + { + "pattern": "/(?i)(\\d+)/u", + "call": "match:null", + "subject": "2ajZqNmm2aE=", + "engine": "YToyOntpOjA7czo4OiLZqNmo2abZoSI7aToxO3M6ODoi2ajZqNmm2aEiO30=", + "reason": "value", + "key": 1, + "phplrt": "array{non-empty-string, numeric-string}", + "phpregex": "array{non-empty-string, non-empty-string}" + }, + { + "pattern": "/(?i)(\\d+)/iu", + "call": "match:null", + "subject": "2aLZoNmp2ag=", + "engine": "YToyOntpOjA7czo4OiLZotmg2anZqCI7aToxO3M6ODoi2aLZoNmp2agiO30=", + "reason": "value", + "key": 1, + "phplrt": "array{non-empty-string, numeric-string}", + "phpregex": "array{non-empty-string, non-empty-string}" + }, + { + "pattern": "/(?i)(\\d+)/su", + "call": "match:null", + "subject": "2aDZpg==", + "engine": "YToyOntpOjA7czo0OiLZoNmmIjtpOjE7czo0OiLZoNmmIjt9", + "reason": "value", + "key": 1, + "phplrt": "array{non-empty-string, numeric-string}", + "phpregex": "array{non-empty-string, non-empty-string}" + }, + { + "pattern": "/(?i)(\\d+)/xu", + "call": "match:null", + "subject": "2aLZodmp", + "engine": "YToyOntpOjA7czo2OiLZotmh2akiO2k6MTtzOjY6Itmi2aHZqSI7fQ==", + "reason": "value", + "key": 1, + "phplrt": "array{non-empty-string, numeric-string}", + "phpregex": "array{non-empty-string, non-empty-string}" + }, + { + "pattern": "/(\\d+)(?i)/u", + "call": "match:null", + "subject": "2aTZodmh2ac=", + "engine": "YToyOntpOjA7czo4OiLZpNmh2aHZpyI7aToxO3M6ODoi2aTZodmh2aciO30=", + "reason": "value", + "key": 1, + "phplrt": "array{non-empty-string, numeric-string}", + "phpregex": "array{non-empty-string, non-empty-string}" + }, + { + "pattern": "/(\\d+)(?i)/iu", + "call": "match:null", + "subject": "2aLZp9ml2ak=", + "engine": "YToyOntpOjA7czo4OiLZotmn2aXZqSI7aToxO3M6ODoi2aLZp9ml2akiO30=", + "reason": "value", + "key": 1, + "phplrt": "array{non-empty-string, numeric-string}", + "phpregex": "array{non-empty-string, non-empty-string}" + }, + { + "pattern": "/(\\d+)(?i)/su", + "call": "match:null", + "subject": "2ak=", + "engine": "YToyOntpOjA7czoyOiLZqSI7aToxO3M6Mjoi2akiO30=", + "reason": "value", + "key": 1, + "phplrt": "array{non-empty-string, numeric-string}", + "phpregex": "array{non-empty-string, non-empty-string}" + }, + { + "pattern": "/(\\d+)(?i)/xu", + "call": "match:null", + "subject": "2anZotmp", + "engine": "YToyOntpOjA7czo2OiLZqdmi2akiO2k6MTtzOjY6Itmp2aLZqSI7fQ==", + "reason": "value", + "key": 1, + "phplrt": "array{non-empty-string, numeric-string}", + "phpregex": "array{non-empty-string, non-empty-string}" + }, + { + "pattern": "/(\\d|a)/u", + "call": "match:null", + "subject": "2aA=", + "engine": "YToyOntpOjA7czoyOiLZoCI7aToxO3M6Mjoi2aAiO30=", + "reason": "value", + "key": 1, + "phplrt": "array{non-empty-string, 'a'|numeric-string}", + "phpregex": "array{non-empty-string, non-empty-string}" + }, + { + "pattern": "/(\\d|a)/su", + "call": "match:null", + "subject": "2aA=", + "engine": "YToyOntpOjA7czoyOiLZoCI7aToxO3M6Mjoi2aAiO30=", + "reason": "value", + "key": 1, + "phplrt": "array{non-empty-string, 'a'|numeric-string}", + "phpregex": "array{non-empty-string, non-empty-string}" + }, + { + "pattern": "/(\\d|a)/xu", + "call": "match:null", + "subject": "2aY=", + "engine": "YToyOntpOjA7czoyOiLZpiI7aToxO3M6Mjoi2aYiO30=", + "reason": "value", + "key": 1, + "phplrt": "array{non-empty-string, 'a'|numeric-string}", + "phpregex": "array{non-empty-string, non-empty-string}" + }, + { + "pattern": "/(\\d|٣)/u", + "call": "match:null", + "subject": "2aE=", + "engine": "YToyOntpOjA7czoyOiLZoSI7aToxO3M6Mjoi2aEiO30=", + "reason": "value", + "key": 1, + "phplrt": "array{non-empty-string, '٣'|numeric-string}", + "phpregex": "array{non-empty-string, non-empty-string}" + }, + { + "pattern": "/(\\d|٣)/su", + "call": "match:null", + "subject": "2ac=", + "engine": "YToyOntpOjA7czoyOiLZpyI7aToxO3M6Mjoi2aciO30=", + "reason": "value", + "key": 1, + "phplrt": "array{non-empty-string, '٣'|numeric-string}", + "phpregex": "array{non-empty-string, non-empty-string}" + }, + { + "pattern": "/(\\d|٣)/xu", + "call": "match:null", + "subject": "2ac=", + "engine": "YToyOntpOjA7czoyOiLZpyI7aToxO3M6Mjoi2aciO30=", + "reason": "value", + "key": 1, + "phplrt": "array{non-empty-string, '٣'|numeric-string}", + "phpregex": "array{non-empty-string, non-empty-string}" + }, + { + "pattern": "/(?i)(a)/", + "call": "match:null", + "subject": "QQ==", + "engine": "YToyOntpOjA7czoxOiJBIjtpOjE7czoxOiJBIjt9", + "reason": "value", + "key": 1, + "phplrt": "array{non-falsy-string, 'a'}", + "phpregex": "array{non-falsy-string, non-falsy-string}" + }, + { + "pattern": "/(?i)(a)/u", + "call": "match:null", + "subject": "QQ==", + "engine": "YToyOntpOjA7czoxOiJBIjtpOjE7czoxOiJBIjt9", + "reason": "value", + "key": 1, + "phplrt": "array{non-falsy-string, 'a'}", + "phpregex": "array{non-falsy-string, non-falsy-string}" + }, + { + "pattern": "/(?i)(a)/m", + "call": "match:null", + "subject": "QQ==", + "engine": "YToyOntpOjA7czoxOiJBIjtpOjE7czoxOiJBIjt9", + "reason": "value", + "key": 1, + "phplrt": "array{non-falsy-string, 'a'}", + "phpregex": "array{non-falsy-string, non-falsy-string}" + }, + { + "pattern": "/(?i)(a)/s", + "call": "match:null", + "subject": "QQ==", + "engine": "YToyOntpOjA7czoxOiJBIjtpOjE7czoxOiJBIjt9", + "reason": "value", + "key": 1, + "phplrt": "array{non-falsy-string, 'a'}", + "phpregex": "array{non-falsy-string, non-falsy-string}" + }, + { + "pattern": "/(?i)(a)/x", + "call": "match:null", + "subject": "QQ==", + "engine": "YToyOntpOjA7czoxOiJBIjtpOjE7czoxOiJBIjt9", + "reason": "value", + "key": 1, + "phplrt": "array{non-falsy-string, 'a'}", + "phpregex": "array{non-falsy-string, non-falsy-string}" + }, + { + "pattern": "/(?i)(a)/D", + "call": "match:null", + "subject": "QQ==", + "engine": "YToyOntpOjA7czoxOiJBIjtpOjE7czoxOiJBIjt9", + "reason": "value", + "key": 1, + "phplrt": "array{non-falsy-string, 'a'}", + "phpregex": "array{non-falsy-string, non-falsy-string}" + }, + { + "pattern": "/(?i)(a)/U", + "call": "match:null", + "subject": "QQ==", + "engine": "YToyOntpOjA7czoxOiJBIjtpOjE7czoxOiJBIjt9", + "reason": "value", + "key": 1, + "phplrt": "array{non-falsy-string, 'a'}", + "phpregex": "array{non-falsy-string, non-falsy-string}" + }, + { + "pattern": "/(?i)(a)/mD", + "call": "match:null", + "subject": "QQ==", + "engine": "YToyOntpOjA7czoxOiJBIjtpOjE7czoxOiJBIjt9", + "reason": "value", + "key": 1, + "phplrt": "array{non-falsy-string, 'a'}", + "phpregex": "array{non-falsy-string, non-falsy-string}" + }, + { + "pattern": "/(?i)(a)/su", + "call": "match:null", + "subject": "QQ==", + "engine": "YToyOntpOjA7czoxOiJBIjtpOjE7czoxOiJBIjt9", + "reason": "value", + "key": 1, + "phplrt": "array{non-falsy-string, 'a'}", + "phpregex": "array{non-falsy-string, non-falsy-string}" + }, + { + "pattern": "/(?i)(a)/xu", + "call": "match:null", + "subject": "QQ==", + "engine": "YToyOntpOjA7czoxOiJBIjtpOjE7czoxOiJBIjt9", + "reason": "value", + "key": 1, + "phplrt": "array{non-falsy-string, 'a'}", + "phpregex": "array{non-falsy-string, non-falsy-string}" + }, + { + "pattern": "#(?i)(a)#", + "call": "match:null", + "subject": "QQ==", + "engine": "YToyOntpOjA7czoxOiJBIjtpOjE7czoxOiJBIjt9", + "reason": "value", + "key": 1, + "phplrt": "array{non-falsy-string, 'a'}", + "phpregex": "array{non-falsy-string, non-falsy-string}" + }, + { + "pattern": "~(?i)(a)~", + "call": "match:null", + "subject": "QQ==", + "engine": "YToyOntpOjA7czoxOiJBIjtpOjE7czoxOiJBIjt9", + "reason": "value", + "key": 1, + "phplrt": "array{non-falsy-string, 'a'}", + "phpregex": "array{non-falsy-string, non-falsy-string}" + }, + { + "pattern": "{(?i)(a)}", + "call": "match:null", + "subject": "QQ==", + "engine": "YToyOntpOjA7czoxOiJBIjtpOjE7czoxOiJBIjt9", + "reason": "value", + "key": 1, + "phplrt": "array{non-falsy-string, 'a'}", + "phpregex": "array{non-falsy-string, non-falsy-string}" + }, + { + "pattern": "/(?i:(a))/", + "call": "match:null", + "subject": "QQ==", + "engine": "YToyOntpOjA7czoxOiJBIjtpOjE7czoxOiJBIjt9", + "reason": "value", + "key": 1, + "phplrt": "array{non-empty-string, 'a'}", + "phpregex": "array{non-falsy-string, non-falsy-string}" + }, + { + "pattern": "/(?i:(a))/u", + "call": "match:null", + "subject": "QQ==", + "engine": "YToyOntpOjA7czoxOiJBIjtpOjE7czoxOiJBIjt9", + "reason": "value", + "key": 1, + "phplrt": "array{non-empty-string, 'a'}", + "phpregex": "array{non-falsy-string, non-falsy-string}" + }, + { + "pattern": "/(?i:(a))/m", + "call": "match:null", + "subject": "QQ==", + "engine": "YToyOntpOjA7czoxOiJBIjtpOjE7czoxOiJBIjt9", + "reason": "value", + "key": 1, + "phplrt": "array{non-empty-string, 'a'}", + "phpregex": "array{non-falsy-string, non-falsy-string}" + }, + { + "pattern": "/(?i:(a))/s", + "call": "match:null", + "subject": "QQ==", + "engine": "YToyOntpOjA7czoxOiJBIjtpOjE7czoxOiJBIjt9", + "reason": "value", + "key": 1, + "phplrt": "array{non-empty-string, 'a'}", + "phpregex": "array{non-falsy-string, non-falsy-string}" + }, + { + "pattern": "/(?i:(a))/x", + "call": "match:null", + "subject": "QQ==", + "engine": "YToyOntpOjA7czoxOiJBIjtpOjE7czoxOiJBIjt9", + "reason": "value", + "key": 1, + "phplrt": "array{non-empty-string, 'a'}", + "phpregex": "array{non-falsy-string, non-falsy-string}" + }, + { + "pattern": "/(?i:(a))/D", + "call": "match:null", + "subject": "QQ==", + "engine": "YToyOntpOjA7czoxOiJBIjtpOjE7czoxOiJBIjt9", + "reason": "value", + "key": 1, + "phplrt": "array{non-empty-string, 'a'}", + "phpregex": "array{non-falsy-string, non-falsy-string}" + }, + { + "pattern": "/(?i:(a))/U", + "call": "match:null", + "subject": "QQ==", + "engine": "YToyOntpOjA7czoxOiJBIjtpOjE7czoxOiJBIjt9", + "reason": "value", + "key": 1, + "phplrt": "array{non-empty-string, 'a'}", + "phpregex": "array{non-falsy-string, non-falsy-string}" + }, + { + "pattern": "/(?i:(a))/mD", + "call": "match:null", + "subject": "QQ==", + "engine": "YToyOntpOjA7czoxOiJBIjtpOjE7czoxOiJBIjt9", + "reason": "value", + "key": 1, + "phplrt": "array{non-empty-string, 'a'}", + "phpregex": "array{non-falsy-string, non-falsy-string}" + }, + { + "pattern": "/(?i:(a))/su", + "call": "match:null", + "subject": "QQ==", + "engine": "YToyOntpOjA7czoxOiJBIjtpOjE7czoxOiJBIjt9", + "reason": "value", + "key": 1, + "phplrt": "array{non-empty-string, 'a'}", + "phpregex": "array{non-falsy-string, non-falsy-string}" + }, + { + "pattern": "/(?i:(a))/xu", + "call": "match:null", + "subject": "QQ==", + "engine": "YToyOntpOjA7czoxOiJBIjtpOjE7czoxOiJBIjt9", + "reason": "value", + "key": 1, + "phplrt": "array{non-empty-string, 'a'}", + "phpregex": "array{non-falsy-string, non-falsy-string}" + }, + { + "pattern": "#(?i:(a))#", + "call": "match:null", + "subject": "QQ==", + "engine": "YToyOntpOjA7czoxOiJBIjtpOjE7czoxOiJBIjt9", + "reason": "value", + "key": 1, + "phplrt": "array{non-empty-string, 'a'}", + "phpregex": "array{non-falsy-string, non-falsy-string}" + }, + { + "pattern": "~(?i:(a))~", + "call": "match:null", + "subject": "QQ==", + "engine": "YToyOntpOjA7czoxOiJBIjtpOjE7czoxOiJBIjt9", + "reason": "value", + "key": 1, + "phplrt": "array{non-empty-string, 'a'}", + "phpregex": "array{non-falsy-string, non-falsy-string}" + }, + { + "pattern": "{(?i:(a))}", + "call": "match:null", + "subject": "QQ==", + "engine": "YToyOntpOjA7czoxOiJBIjtpOjE7czoxOiJBIjt9", + "reason": "value", + "key": 1, + "phplrt": "array{non-empty-string, 'a'}", + "phpregex": "array{non-falsy-string, non-falsy-string}" + }, + { + "pattern": "/(?^i)(a)/", + "call": "match:null", + "subject": "QQ==", + "engine": "YToyOntpOjA7czoxOiJBIjtpOjE7czoxOiJBIjt9", + "reason": "value", + "key": 1, + "phplrt": "array{non-falsy-string, 'a'}", + "phpregex": "array{non-falsy-string, non-falsy-string}" + }, + { + "pattern": "/(?^i)(a)/u", + "call": "match:null", + "subject": "QQ==", + "engine": "YToyOntpOjA7czoxOiJBIjtpOjE7czoxOiJBIjt9", + "reason": "value", + "key": 1, + "phplrt": "array{non-falsy-string, 'a'}", + "phpregex": "array{non-falsy-string, non-falsy-string}" + }, + { + "pattern": "/(?^i)(a)/m", + "call": "match:null", + "subject": "QQ==", + "engine": "YToyOntpOjA7czoxOiJBIjtpOjE7czoxOiJBIjt9", + "reason": "value", + "key": 1, + "phplrt": "array{non-falsy-string, 'a'}", + "phpregex": "array{non-falsy-string, non-falsy-string}" + }, + { + "pattern": "/(?^i)(a)/s", + "call": "match:null", + "subject": "QQ==", + "engine": "YToyOntpOjA7czoxOiJBIjtpOjE7czoxOiJBIjt9", + "reason": "value", + "key": 1, + "phplrt": "array{non-falsy-string, 'a'}", + "phpregex": "array{non-falsy-string, non-falsy-string}" + }, + { + "pattern": "/(?^i)(a)/x", + "call": "match:null", + "subject": "QQ==", + "engine": "YToyOntpOjA7czoxOiJBIjtpOjE7czoxOiJBIjt9", + "reason": "value", + "key": 1, + "phplrt": "array{non-falsy-string, 'a'}", + "phpregex": "array{non-falsy-string, non-falsy-string}" + }, + { + "pattern": "/(?^i)(a)/D", + "call": "match:null", + "subject": "QQ==", + "engine": "YToyOntpOjA7czoxOiJBIjtpOjE7czoxOiJBIjt9", + "reason": "value", + "key": 1, + "phplrt": "array{non-falsy-string, 'a'}", + "phpregex": "array{non-falsy-string, non-falsy-string}" + }, + { + "pattern": "/(?^i)(a)/U", + "call": "match:null", + "subject": "QQ==", + "engine": "YToyOntpOjA7czoxOiJBIjtpOjE7czoxOiJBIjt9", + "reason": "value", + "key": 1, + "phplrt": "array{non-falsy-string, 'a'}", + "phpregex": "array{non-falsy-string, non-falsy-string}" + }, + { + "pattern": "/(?^i)(a)/mD", + "call": "match:null", + "subject": "QQ==", + "engine": "YToyOntpOjA7czoxOiJBIjtpOjE7czoxOiJBIjt9", + "reason": "value", + "key": 1, + "phplrt": "array{non-falsy-string, 'a'}", + "phpregex": "array{non-falsy-string, non-falsy-string}" + }, + { + "pattern": "/(?^i)(a)/su", + "call": "match:null", + "subject": "QQ==", + "engine": "YToyOntpOjA7czoxOiJBIjtpOjE7czoxOiJBIjt9", + "reason": "value", + "key": 1, + "phplrt": "array{non-falsy-string, 'a'}", + "phpregex": "array{non-falsy-string, non-falsy-string}" + }, + { + "pattern": "/(?^i)(a)/xu", + "call": "match:null", + "subject": "QQ==", + "engine": "YToyOntpOjA7czoxOiJBIjtpOjE7czoxOiJBIjt9", + "reason": "value", + "key": 1, + "phplrt": "array{non-falsy-string, 'a'}", + "phpregex": "array{non-falsy-string, non-falsy-string}" + }, + { + "pattern": "#(?^i)(a)#", + "call": "match:null", + "subject": "QQ==", + "engine": "YToyOntpOjA7czoxOiJBIjtpOjE7czoxOiJBIjt9", + "reason": "value", + "key": 1, + "phplrt": "array{non-falsy-string, 'a'}", + "phpregex": "array{non-falsy-string, non-falsy-string}" + }, + { + "pattern": "~(?^i)(a)~", + "call": "match:null", + "subject": "QQ==", + "engine": "YToyOntpOjA7czoxOiJBIjtpOjE7czoxOiJBIjt9", + "reason": "value", + "key": 1, + "phplrt": "array{non-falsy-string, 'a'}", + "phpregex": "array{non-falsy-string, non-falsy-string}" + }, + { + "pattern": "{(?^i)(a)}", + "call": "match:null", + "subject": "QQ==", + "engine": "YToyOntpOjA7czoxOiJBIjtpOjE7czoxOiJBIjt9", + "reason": "value", + "key": 1, + "phplrt": "array{non-falsy-string, 'a'}", + "phpregex": "array{non-falsy-string, non-falsy-string}" + }, + { + "pattern": "/(?J)(?\\d)|(?b)/u", + "call": "match:null", + "subject": "2aE=", + "engine": "YTozOntpOjA7czoyOiLZoSI7czoxOiJ4IjtzOjI6ItmhIjtpOjE7czoyOiLZoSI7fQ==", + "reason": "value", + "key": "x", + "phplrt": "array{0: non-empty-string, x: 'b', 1: '', 2: 'b'}|array{0: non-empty-string, x: numeric-string, 1: numeric-string}", + "phpregex": "array{0: non-empty-string, x: 'b', 1: '', 2: 'b'}|array{0: non-empty-string, x: non-empty-string, 1: non-empty-string}" + }, + { + "pattern": "/(?J)(?\\d)|(?b)/iu", + "call": "match:null", + "subject": "2aQ=", + "engine": "YTozOntpOjA7czoyOiLZpCI7czoxOiJ4IjtzOjI6ItmkIjtpOjE7czoyOiLZpCI7fQ==", + "reason": "value", + "key": "x", + "phplrt": "array{0: non-empty-string, x: non-empty-string, 1: '', 2: non-empty-string}|array{0: non-empty-string, x: numeric-string, 1: numeric-string}", + "phpregex": "array{0: non-empty-string, x: non-empty-string, 1: non-empty-string}|array{0: non-empty-string, x: non-falsy-string, 1: '', 2: non-falsy-string}" + }, + { + "pattern": "/(?J)(?\\d)|(?b)/su", + "call": "match:null", + "subject": "2aI=", + "engine": "YTozOntpOjA7czoyOiLZoiI7czoxOiJ4IjtzOjI6ItmiIjtpOjE7czoyOiLZoiI7fQ==", + "reason": "value", + "key": "x", + "phplrt": "array{0: non-empty-string, x: 'b', 1: '', 2: 'b'}|array{0: non-empty-string, x: numeric-string, 1: numeric-string}", + "phpregex": "array{0: non-empty-string, x: 'b', 1: '', 2: 'b'}|array{0: non-empty-string, x: non-empty-string, 1: non-empty-string}" + }, + { + "pattern": "/(?J)(?\\d)|(?b)/xu", + "call": "match:null", + "subject": "2aY=", + "engine": "YTozOntpOjA7czoyOiLZpiI7czoxOiJ4IjtzOjI6ItmmIjtpOjE7czoyOiLZpiI7fQ==", + "reason": "value", + "key": "x", + "phplrt": "array{0: non-empty-string, x: 'b', 1: '', 2: 'b'}|array{0: non-empty-string, x: numeric-string, 1: numeric-string}", + "phpregex": "array{0: non-empty-string, x: 'b', 1: '', 2: 'b'}|array{0: non-empty-string, x: non-empty-string, 1: non-empty-string}" + }, + { + "pattern": "/(?n)(a)(?b)/", + "call": "match:null", + "subject": "YWI=", + "engine": "YTozOntpOjA7czoyOiJhYiI7czoxOiJ4IjtzOjE6ImIiO2k6MTtzOjE6ImIiO30=", + "reason": "value", + "key": 1, + "phplrt": "array{0: non-falsy-string, 1: 'a', x: 'b', 2: 'b'}", + "phpregex": "array{0: 'ab', x: 'b', 1: 'b'}" + }, + { + "pattern": "/(?n)(a)(?b)/i", + "call": "match:null", + "subject": "YWI=", + "engine": "YTozOntpOjA7czoyOiJhYiI7czoxOiJ4IjtzOjE6ImIiO2k6MTtzOjE6ImIiO30=", + "reason": "present", + "key": 2, + "phplrt": "array{0: non-falsy-string, 1: non-empty-string, x: non-empty-string, 2: non-empty-string}", + "phpregex": "array{0: non-falsy-string, x: non-falsy-string, 1: non-falsy-string}" + }, + { + "pattern": "/(?n)(a)(?b)/u", + "call": "match:null", + "subject": "YWI=", + "engine": "YTozOntpOjA7czoyOiJhYiI7czoxOiJ4IjtzOjE6ImIiO2k6MTtzOjE6ImIiO30=", + "reason": "value", + "key": 1, + "phplrt": "array{0: non-falsy-string, 1: 'a', x: 'b', 2: 'b'}", + "phpregex": "array{0: 'ab', x: 'b', 1: 'b'}" + }, + { + "pattern": "/(?n)(a)(?b)/iu", + "call": "match:null", + "subject": "YWI=", + "engine": "YTozOntpOjA7czoyOiJhYiI7czoxOiJ4IjtzOjE6ImIiO2k6MTtzOjE6ImIiO30=", + "reason": "present", + "key": 2, + "phplrt": "array{0: non-falsy-string, 1: non-empty-string, x: non-empty-string, 2: non-empty-string}", + "phpregex": "array{0: non-falsy-string, x: non-falsy-string, 1: non-falsy-string}" + }, + { + "pattern": "/(?n)(a)(?b)/m", + "call": "match:null", + "subject": "YWI=", + "engine": "YTozOntpOjA7czoyOiJhYiI7czoxOiJ4IjtzOjE6ImIiO2k6MTtzOjE6ImIiO30=", + "reason": "value", + "key": 1, + "phplrt": "array{0: non-falsy-string, 1: 'a', x: 'b', 2: 'b'}", + "phpregex": "array{0: 'ab', x: 'b', 1: 'b'}" + }, + { + "pattern": "/(?n)(a)(?b)/s", + "call": "match:null", + "subject": "YWI=", + "engine": "YTozOntpOjA7czoyOiJhYiI7czoxOiJ4IjtzOjE6ImIiO2k6MTtzOjE6ImIiO30=", + "reason": "value", + "key": 1, + "phplrt": "array{0: non-falsy-string, 1: 'a', x: 'b', 2: 'b'}", + "phpregex": "array{0: 'ab', x: 'b', 1: 'b'}" + }, + { + "pattern": "/(?n)(a)(?b)/x", + "call": "match:null", + "subject": "YWI=", + "engine": "YTozOntpOjA7czoyOiJhYiI7czoxOiJ4IjtzOjE6ImIiO2k6MTtzOjE6ImIiO30=", + "reason": "value", + "key": 1, + "phplrt": "array{0: non-falsy-string, 1: 'a', x: 'b', 2: 'b'}", + "phpregex": "array{0: 'ab', x: 'b', 1: 'b'}" + }, + { + "pattern": "/(?n)(a)(?b)/D", + "call": "match:null", + "subject": "YWI=", + "engine": "YTozOntpOjA7czoyOiJhYiI7czoxOiJ4IjtzOjE6ImIiO2k6MTtzOjE6ImIiO30=", + "reason": "value", + "key": 1, + "phplrt": "array{0: non-falsy-string, 1: 'a', x: 'b', 2: 'b'}", + "phpregex": "array{0: 'ab', x: 'b', 1: 'b'}" + }, + { + "pattern": "/(?n)(a)(?b)/U", + "call": "match:null", + "subject": "YWI=", + "engine": "YTozOntpOjA7czoyOiJhYiI7czoxOiJ4IjtzOjE6ImIiO2k6MTtzOjE6ImIiO30=", + "reason": "value", + "key": 1, + "phplrt": "array{0: non-falsy-string, 1: 'a', x: 'b', 2: 'b'}", + "phpregex": "array{0: 'ab', x: 'b', 1: 'b'}" + }, + { + "pattern": "/(?n)(a)(?b)/mD", + "call": "match:null", + "subject": "YWI=", + "engine": "YTozOntpOjA7czoyOiJhYiI7czoxOiJ4IjtzOjE6ImIiO2k6MTtzOjE6ImIiO30=", + "reason": "value", + "key": 1, + "phplrt": "array{0: non-falsy-string, 1: 'a', x: 'b', 2: 'b'}", + "phpregex": "array{0: 'ab', x: 'b', 1: 'b'}" + }, + { + "pattern": "/(?n)(a)(?b)/su", + "call": "match:null", + "subject": "YWI=", + "engine": "YTozOntpOjA7czoyOiJhYiI7czoxOiJ4IjtzOjE6ImIiO2k6MTtzOjE6ImIiO30=", + "reason": "value", + "key": 1, + "phplrt": "array{0: non-falsy-string, 1: 'a', x: 'b', 2: 'b'}", + "phpregex": "array{0: 'ab', x: 'b', 1: 'b'}" + }, + { + "pattern": "/(?n)(a)(?b)/xu", + "call": "match:null", + "subject": "YWI=", + "engine": "YTozOntpOjA7czoyOiJhYiI7czoxOiJ4IjtzOjE6ImIiO2k6MTtzOjE6ImIiO30=", + "reason": "value", + "key": 1, + "phplrt": "array{0: non-falsy-string, 1: 'a', x: 'b', 2: 'b'}", + "phpregex": "array{0: 'ab', x: 'b', 1: 'b'}" + }, + { + "pattern": "#(?n)(a)(?b)#", + "call": "match:null", + "subject": "YWI=", + "engine": "YTozOntpOjA7czoyOiJhYiI7czoxOiJ4IjtzOjE6ImIiO2k6MTtzOjE6ImIiO30=", + "reason": "value", + "key": 1, + "phplrt": "array{0: non-falsy-string, 1: 'a', x: 'b', 2: 'b'}", + "phpregex": "array{0: 'ab', x: 'b', 1: 'b'}" + }, + { + "pattern": "~(?n)(a)(?b)~", + "call": "match:null", + "subject": "YWI=", + "engine": "YTozOntpOjA7czoyOiJhYiI7czoxOiJ4IjtzOjE6ImIiO2k6MTtzOjE6ImIiO30=", + "reason": "value", + "key": 1, + "phplrt": "array{0: non-falsy-string, 1: 'a', x: 'b', 2: 'b'}", + "phpregex": "array{0: 'ab', x: 'b', 1: 'b'}" + }, + { + "pattern": "{(?n)(a)(?b)}", + "call": "match:null", + "subject": "YWI=", + "engine": "YTozOntpOjA7czoyOiJhYiI7czoxOiJ4IjtzOjE6ImIiO2k6MTtzOjE6ImIiO30=", + "reason": "value", + "key": 1, + "phplrt": "array{0: non-falsy-string, 1: 'a', x: 'b', 2: 'b'}", + "phpregex": "array{0: 'ab', x: 'b', 1: 'b'}" + }, + { + "pattern": "/(?n)(a)/", + "call": "match:null", + "subject": "YQ==", + "engine": "YToxOntpOjA7czoxOiJhIjt9", + "reason": "present", + "key": 1, + "phplrt": "array{non-falsy-string, 'a'}", + "phpregex": "array{'a'}" + }, + { + "pattern": "/(?n)(a)/i", + "call": "match:null", + "subject": "YQ==", + "engine": "YToxOntpOjA7czoxOiJhIjt9", + "reason": "present", + "key": 1, + "phplrt": "array{non-falsy-string, non-empty-string}", + "phpregex": "array{non-falsy-string}" + }, + { + "pattern": "/(?n)(a)/u", + "call": "match:null", + "subject": "YQ==", + "engine": "YToxOntpOjA7czoxOiJhIjt9", + "reason": "present", + "key": 1, + "phplrt": "array{non-falsy-string, 'a'}", + "phpregex": "array{'a'}" + }, + { + "pattern": "/(?n)(a)/iu", + "call": "match:null", + "subject": "YQ==", + "engine": "YToxOntpOjA7czoxOiJhIjt9", + "reason": "present", + "key": 1, + "phplrt": "array{non-falsy-string, non-empty-string}", + "phpregex": "array{non-falsy-string}" + }, + { + "pattern": "/(?n)(a)/m", + "call": "match:null", + "subject": "YQ==", + "engine": "YToxOntpOjA7czoxOiJhIjt9", + "reason": "present", + "key": 1, + "phplrt": "array{non-falsy-string, 'a'}", + "phpregex": "array{'a'}" + }, + { + "pattern": "/(?n)(a)/s", + "call": "match:null", + "subject": "YQ==", + "engine": "YToxOntpOjA7czoxOiJhIjt9", + "reason": "present", + "key": 1, + "phplrt": "array{non-falsy-string, 'a'}", + "phpregex": "array{'a'}" + }, + { + "pattern": "/(?n)(a)/x", + "call": "match:null", + "subject": "YQ==", + "engine": "YToxOntpOjA7czoxOiJhIjt9", + "reason": "present", + "key": 1, + "phplrt": "array{non-falsy-string, 'a'}", + "phpregex": "array{'a'}" + }, + { + "pattern": "/(?n)(a)/D", + "call": "match:null", + "subject": "YQ==", + "engine": "YToxOntpOjA7czoxOiJhIjt9", + "reason": "present", + "key": 1, + "phplrt": "array{non-falsy-string, 'a'}", + "phpregex": "array{'a'}" + }, + { + "pattern": "/(?n)(a)/U", + "call": "match:null", + "subject": "YQ==", + "engine": "YToxOntpOjA7czoxOiJhIjt9", + "reason": "present", + "key": 1, + "phplrt": "array{non-falsy-string, 'a'}", + "phpregex": "array{'a'}" + }, + { + "pattern": "/(?n)(a)/mD", + "call": "match:null", + "subject": "YQ==", + "engine": "YToxOntpOjA7czoxOiJhIjt9", + "reason": "present", + "key": 1, + "phplrt": "array{non-falsy-string, 'a'}", + "phpregex": "array{'a'}" + }, + { + "pattern": "/(?n)(a)/su", + "call": "match:null", + "subject": "YQ==", + "engine": "YToxOntpOjA7czoxOiJhIjt9", + "reason": "present", + "key": 1, + "phplrt": "array{non-falsy-string, 'a'}", + "phpregex": "array{'a'}" + }, + { + "pattern": "/(?n)(a)/xu", + "call": "match:null", + "subject": "YQ==", + "engine": "YToxOntpOjA7czoxOiJhIjt9", + "reason": "present", + "key": 1, + "phplrt": "array{non-falsy-string, 'a'}", + "phpregex": "array{'a'}" + }, + { + "pattern": "#(?n)(a)#", + "call": "match:null", + "subject": "YQ==", + "engine": "YToxOntpOjA7czoxOiJhIjt9", + "reason": "present", + "key": 1, + "phplrt": "array{non-falsy-string, 'a'}", + "phpregex": "array{'a'}" + }, + { + "pattern": "~(?n)(a)~", + "call": "match:null", + "subject": "YQ==", + "engine": "YToxOntpOjA7czoxOiJhIjt9", + "reason": "present", + "key": 1, + "phplrt": "array{non-falsy-string, 'a'}", + "phpregex": "array{'a'}" + }, + { + "pattern": "{(?n)(a)}", + "call": "match:null", + "subject": "YQ==", + "engine": "YToxOntpOjA7czoxOiJhIjt9", + "reason": "present", + "key": 1, + "phplrt": "array{non-falsy-string, 'a'}", + "phpregex": "array{'a'}" + }, + { + "pattern": "/(?n:(a))(b)/", + "call": "match:null", + "subject": "YWI=", + "engine": "YToyOntpOjA7czoyOiJhYiI7aToxO3M6MToiYiI7fQ==", + "reason": "value", + "key": 1, + "phplrt": "array{non-falsy-string, 'a', 'b'}", + "phpregex": "array{'ab', 'b'}" + }, + { + "pattern": "/(?n:(a))(b)/i", + "call": "match:null", + "subject": "YWI=", + "engine": "YToyOntpOjA7czoyOiJhYiI7aToxO3M6MToiYiI7fQ==", + "reason": "present", + "key": 2, + "phplrt": "array{non-falsy-string, non-empty-string, non-empty-string}", + "phpregex": "array{non-falsy-string, non-falsy-string}" + }, + { + "pattern": "/(?n:(a))(b)/u", + "call": "match:null", + "subject": "YWI=", + "engine": "YToyOntpOjA7czoyOiJhYiI7aToxO3M6MToiYiI7fQ==", + "reason": "value", + "key": 1, + "phplrt": "array{non-falsy-string, 'a', 'b'}", + "phpregex": "array{'ab', 'b'}" + }, + { + "pattern": "/(?n:(a))(b)/iu", + "call": "match:null", + "subject": "YWI=", + "engine": "YToyOntpOjA7czoyOiJhYiI7aToxO3M6MToiYiI7fQ==", + "reason": "present", + "key": 2, + "phplrt": "array{non-falsy-string, non-empty-string, non-empty-string}", + "phpregex": "array{non-falsy-string, non-falsy-string}" + }, + { + "pattern": "/(?n:(a))(b)/m", + "call": "match:null", + "subject": "YWI=", + "engine": "YToyOntpOjA7czoyOiJhYiI7aToxO3M6MToiYiI7fQ==", + "reason": "value", + "key": 1, + "phplrt": "array{non-falsy-string, 'a', 'b'}", + "phpregex": "array{'ab', 'b'}" + }, + { + "pattern": "/(?n:(a))(b)/s", + "call": "match:null", + "subject": "YWI=", + "engine": "YToyOntpOjA7czoyOiJhYiI7aToxO3M6MToiYiI7fQ==", + "reason": "value", + "key": 1, + "phplrt": "array{non-falsy-string, 'a', 'b'}", + "phpregex": "array{'ab', 'b'}" + }, + { + "pattern": "/(?n:(a))(b)/x", + "call": "match:null", + "subject": "YWI=", + "engine": "YToyOntpOjA7czoyOiJhYiI7aToxO3M6MToiYiI7fQ==", + "reason": "value", + "key": 1, + "phplrt": "array{non-falsy-string, 'a', 'b'}", + "phpregex": "array{'ab', 'b'}" + }, + { + "pattern": "/(?n:(a))(b)/D", + "call": "match:null", + "subject": "YWI=", + "engine": "YToyOntpOjA7czoyOiJhYiI7aToxO3M6MToiYiI7fQ==", + "reason": "value", + "key": 1, + "phplrt": "array{non-falsy-string, 'a', 'b'}", + "phpregex": "array{'ab', 'b'}" + }, + { + "pattern": "/(?n:(a))(b)/U", + "call": "match:null", + "subject": "YWI=", + "engine": "YToyOntpOjA7czoyOiJhYiI7aToxO3M6MToiYiI7fQ==", + "reason": "value", + "key": 1, + "phplrt": "array{non-falsy-string, 'a', 'b'}", + "phpregex": "array{'ab', 'b'}" + }, + { + "pattern": "/(?n:(a))(b)/mD", + "call": "match:null", + "subject": "YWI=", + "engine": "YToyOntpOjA7czoyOiJhYiI7aToxO3M6MToiYiI7fQ==", + "reason": "value", + "key": 1, + "phplrt": "array{non-falsy-string, 'a', 'b'}", + "phpregex": "array{'ab', 'b'}" + }, + { + "pattern": "/(?n:(a))(b)/su", + "call": "match:null", + "subject": "YWI=", + "engine": "YToyOntpOjA7czoyOiJhYiI7aToxO3M6MToiYiI7fQ==", + "reason": "value", + "key": 1, + "phplrt": "array{non-falsy-string, 'a', 'b'}", + "phpregex": "array{'ab', 'b'}" + }, + { + "pattern": "/(?n:(a))(b)/xu", + "call": "match:null", + "subject": "YWI=", + "engine": "YToyOntpOjA7czoyOiJhYiI7aToxO3M6MToiYiI7fQ==", + "reason": "value", + "key": 1, + "phplrt": "array{non-falsy-string, 'a', 'b'}", + "phpregex": "array{'ab', 'b'}" + }, + { + "pattern": "#(?n:(a))(b)#", + "call": "match:null", + "subject": "YWI=", + "engine": "YToyOntpOjA7czoyOiJhYiI7aToxO3M6MToiYiI7fQ==", + "reason": "value", + "key": 1, + "phplrt": "array{non-falsy-string, 'a', 'b'}", + "phpregex": "array{'ab', 'b'}" + }, + { + "pattern": "~(?n:(a))(b)~", + "call": "match:null", + "subject": "YWI=", + "engine": "YToyOntpOjA7czoyOiJhYiI7aToxO3M6MToiYiI7fQ==", + "reason": "value", + "key": 1, + "phplrt": "array{non-falsy-string, 'a', 'b'}", + "phpregex": "array{'ab', 'b'}" + }, + { + "pattern": "{(?n:(a))(b)}", + "call": "match:null", + "subject": "YWI=", + "engine": "YToyOntpOjA7czoyOiJhYiI7aToxO3M6MToiYiI7fQ==", + "reason": "value", + "key": 1, + "phplrt": "array{non-falsy-string, 'a', 'b'}", + "phpregex": "array{'ab', 'b'}" + }, + { + "pattern": "/(?x)( a )/", + "call": "match:null", + "subject": "YQ==", + "engine": "YToyOntpOjA7czoxOiJhIjtpOjE7czoxOiJhIjt9", + "reason": "value", + "key": 1, + "phplrt": "array{non-falsy-string, ' a '}", + "phpregex": "array{'a', 'a'}" + }, + { + "pattern": "/(?x)( a )/u", + "call": "match:null", + "subject": "YQ==", + "engine": "YToyOntpOjA7czoxOiJhIjtpOjE7czoxOiJhIjt9", + "reason": "value", + "key": 1, + "phplrt": "array{non-falsy-string, ' a '}", + "phpregex": "array{'a', 'a'}" + }, + { + "pattern": "/(?x)( a )/m", + "call": "match:null", + "subject": "YQ==", + "engine": "YToyOntpOjA7czoxOiJhIjtpOjE7czoxOiJhIjt9", + "reason": "value", + "key": 1, + "phplrt": "array{non-falsy-string, ' a '}", + "phpregex": "array{'a', 'a'}" + }, + { + "pattern": "/(?x)( a )/s", + "call": "match:null", + "subject": "YQ==", + "engine": "YToyOntpOjA7czoxOiJhIjtpOjE7czoxOiJhIjt9", + "reason": "value", + "key": 1, + "phplrt": "array{non-falsy-string, ' a '}", + "phpregex": "array{'a', 'a'}" + }, + { + "pattern": "/(?x)( a )/D", + "call": "match:null", + "subject": "YQ==", + "engine": "YToyOntpOjA7czoxOiJhIjtpOjE7czoxOiJhIjt9", + "reason": "value", + "key": 1, + "phplrt": "array{non-falsy-string, ' a '}", + "phpregex": "array{'a', 'a'}" + }, + { + "pattern": "/(?x)( a )/U", + "call": "match:null", + "subject": "YQ==", + "engine": "YToyOntpOjA7czoxOiJhIjtpOjE7czoxOiJhIjt9", + "reason": "value", + "key": 1, + "phplrt": "array{non-falsy-string, ' a '}", + "phpregex": "array{'a', 'a'}" + }, + { + "pattern": "/(?x)( a )/mD", + "call": "match:null", + "subject": "YQ==", + "engine": "YToyOntpOjA7czoxOiJhIjtpOjE7czoxOiJhIjt9", + "reason": "value", + "key": 1, + "phplrt": "array{non-falsy-string, ' a '}", + "phpregex": "array{'a', 'a'}" + }, + { + "pattern": "/(?x)( a )/su", + "call": "match:null", + "subject": "YQ==", + "engine": "YToyOntpOjA7czoxOiJhIjtpOjE7czoxOiJhIjt9", + "reason": "value", + "key": 1, + "phplrt": "array{non-falsy-string, ' a '}", + "phpregex": "array{'a', 'a'}" + }, + { + "pattern": "#(?x)( a )#", + "call": "match:null", + "subject": "YQ==", + "engine": "YToyOntpOjA7czoxOiJhIjtpOjE7czoxOiJhIjt9", + "reason": "value", + "key": 1, + "phplrt": "array{non-falsy-string, ' a '}", + "phpregex": "array{'a', 'a'}" + }, + { + "pattern": "~(?x)( a )~", + "call": "match:null", + "subject": "YQ==", + "engine": "YToyOntpOjA7czoxOiJhIjtpOjE7czoxOiJhIjt9", + "reason": "value", + "key": 1, + "phplrt": "array{non-falsy-string, ' a '}", + "phpregex": "array{'a', 'a'}" + }, + { + "pattern": "{(?x)( a )}", + "call": "match:null", + "subject": "YQ==", + "engine": "YToyOntpOjA7czoxOiJhIjtpOjE7czoxOiJhIjt9", + "reason": "value", + "key": 1, + "phplrt": "array{non-falsy-string, ' a '}", + "phpregex": "array{'a', 'a'}" + }, + { + "pattern": "/\\Q(a)\\E/", + "call": "match:null", + "subject": "KGEp", + "engine": "YToxOntpOjA7czozOiIoYSkiO30=", + "reason": "present", + "key": 1, + "phplrt": "array{non-falsy-string, 'a'}", + "phpregex": "array{'(a)'}" + }, + { + "pattern": "/\\Q(a)\\E/i", + "call": "match:null", + "subject": "KGEp", + "engine": "YToxOntpOjA7czozOiIoYSkiO30=", + "reason": "present", + "key": 1, + "phplrt": "array{non-falsy-string, non-empty-string}", + "phpregex": "array{non-falsy-string}" + }, + { + "pattern": "/\\Q(a)\\E/u", + "call": "match:null", + "subject": "KGEp", + "engine": "YToxOntpOjA7czozOiIoYSkiO30=", + "reason": "present", + "key": 1, + "phplrt": "array{non-falsy-string, 'a'}", + "phpregex": "array{'(a)'}" + }, + { + "pattern": "/\\Q(a)\\E/iu", + "call": "match:null", + "subject": "KGEp", + "engine": "YToxOntpOjA7czozOiIoYSkiO30=", + "reason": "present", + "key": 1, + "phplrt": "array{non-falsy-string, non-empty-string}", + "phpregex": "array{non-falsy-string}" + }, + { + "pattern": "/\\Q(a)\\E/m", + "call": "match:null", + "subject": "KGEp", + "engine": "YToxOntpOjA7czozOiIoYSkiO30=", + "reason": "present", + "key": 1, + "phplrt": "array{non-falsy-string, 'a'}", + "phpregex": "array{'(a)'}" + }, + { + "pattern": "/\\Q(a)\\E/s", + "call": "match:null", + "subject": "KGEp", + "engine": "YToxOntpOjA7czozOiIoYSkiO30=", + "reason": "present", + "key": 1, + "phplrt": "array{non-falsy-string, 'a'}", + "phpregex": "array{'(a)'}" + }, + { + "pattern": "/\\Q(a)\\E/x", + "call": "match:null", + "subject": "KGEp", + "engine": "YToxOntpOjA7czozOiIoYSkiO30=", + "reason": "present", + "key": 1, + "phplrt": "array{non-falsy-string, 'a'}", + "phpregex": "array{'(a)'}" + }, + { + "pattern": "/\\Q(a)\\E/D", + "call": "match:null", + "subject": "KGEp", + "engine": "YToxOntpOjA7czozOiIoYSkiO30=", + "reason": "present", + "key": 1, + "phplrt": "array{non-falsy-string, 'a'}", + "phpregex": "array{'(a)'}" + }, + { + "pattern": "/\\Q(a)\\E/U", + "call": "match:null", + "subject": "KGEp", + "engine": "YToxOntpOjA7czozOiIoYSkiO30=", + "reason": "present", + "key": 1, + "phplrt": "array{non-falsy-string, 'a'}", + "phpregex": "array{'(a)'}" + }, + { + "pattern": "/\\Q(a)\\E/mD", + "call": "match:null", + "subject": "KGEp", + "engine": "YToxOntpOjA7czozOiIoYSkiO30=", + "reason": "present", + "key": 1, + "phplrt": "array{non-falsy-string, 'a'}", + "phpregex": "array{'(a)'}" + }, + { + "pattern": "/\\Q(a)\\E/su", + "call": "match:null", + "subject": "KGEp", + "engine": "YToxOntpOjA7czozOiIoYSkiO30=", + "reason": "present", + "key": 1, + "phplrt": "array{non-falsy-string, 'a'}", + "phpregex": "array{'(a)'}" + }, + { + "pattern": "/\\Q(a)\\E/xu", + "call": "match:null", + "subject": "KGEp", + "engine": "YToxOntpOjA7czozOiIoYSkiO30=", + "reason": "present", + "key": 1, + "phplrt": "array{non-falsy-string, 'a'}", + "phpregex": "array{'(a)'}" + }, + { + "pattern": "#\\Q(a)\\E#", + "call": "match:null", + "subject": "KGEp", + "engine": "YToxOntpOjA7czozOiIoYSkiO30=", + "reason": "present", + "key": 1, + "phplrt": "array{non-falsy-string, 'a'}", + "phpregex": "array{'(a)'}" + }, + { + "pattern": "~\\Q(a)\\E~", + "call": "match:null", + "subject": "KGEp", + "engine": "YToxOntpOjA7czozOiIoYSkiO30=", + "reason": "present", + "key": 1, + "phplrt": "array{non-falsy-string, 'a'}", + "phpregex": "array{'(a)'}" + }, + { + "pattern": "{\\Q(a)\\E}", + "call": "match:null", + "subject": "KGEp", + "engine": "YToxOntpOjA7czozOiIoYSkiO30=", + "reason": "present", + "key": 1, + "phplrt": "array{non-falsy-string, 'a'}", + "phpregex": "array{'(a)'}" + }, + { + "pattern": "/(\\Q0\\E)/", + "call": "match:null", + "subject": "MA==", + "engine": "YToyOntpOjA7czoxOiIwIjtpOjE7czoxOiIwIjt9", + "reason": "falsy", + "key": 0, + "phplrt": "array{non-falsy-string, 'Q0E'}", + "phpregex": "array{'0', '0'}" + }, + { + "pattern": "/(\\Q0\\E)/i", + "call": "match:null", + "subject": "MA==", + "engine": "YToyOntpOjA7czoxOiIwIjtpOjE7czoxOiIwIjt9", + "reason": "falsy", + "key": 0, + "phplrt": "array{non-falsy-string, non-falsy-string}", + "phpregex": "array{'0', '0'}" + }, + { + "pattern": "/(\\Q0\\E)/u", + "call": "match:null", + "subject": "MA==", + "engine": "YToyOntpOjA7czoxOiIwIjtpOjE7czoxOiIwIjt9", + "reason": "falsy", + "key": 0, + "phplrt": "array{non-falsy-string, 'Q0E'}", + "phpregex": "array{'0', '0'}" + }, + { + "pattern": "/(\\Q0\\E)/iu", + "call": "match:null", + "subject": "MA==", + "engine": "YToyOntpOjA7czoxOiIwIjtpOjE7czoxOiIwIjt9", + "reason": "falsy", + "key": 0, + "phplrt": "array{non-falsy-string, non-falsy-string}", + "phpregex": "array{'0', '0'}" + }, + { + "pattern": "/(\\Q0\\E)/m", + "call": "match:null", + "subject": "MA==", + "engine": "YToyOntpOjA7czoxOiIwIjtpOjE7czoxOiIwIjt9", + "reason": "falsy", + "key": 0, + "phplrt": "array{non-falsy-string, 'Q0E'}", + "phpregex": "array{'0', '0'}" + }, + { + "pattern": "/(\\Q0\\E)/s", + "call": "match:null", + "subject": "MA==", + "engine": "YToyOntpOjA7czoxOiIwIjtpOjE7czoxOiIwIjt9", + "reason": "falsy", + "key": 0, + "phplrt": "array{non-falsy-string, 'Q0E'}", + "phpregex": "array{'0', '0'}" + }, + { + "pattern": "/(\\Q0\\E)/x", + "call": "match:null", + "subject": "MA==", + "engine": "YToyOntpOjA7czoxOiIwIjtpOjE7czoxOiIwIjt9", + "reason": "falsy", + "key": 0, + "phplrt": "array{non-falsy-string, 'Q0E'}", + "phpregex": "array{'0', '0'}" + }, + { + "pattern": "/(\\Q0\\E)/D", + "call": "match:null", + "subject": "MA==", + "engine": "YToyOntpOjA7czoxOiIwIjtpOjE7czoxOiIwIjt9", + "reason": "falsy", + "key": 0, + "phplrt": "array{non-falsy-string, 'Q0E'}", + "phpregex": "array{'0', '0'}" + }, + { + "pattern": "/(\\Q0\\E)/n", + "call": "match:null", + "subject": "MA==", + "engine": "YToxOntpOjA7czoxOiIwIjt9", + "reason": "falsy", + "key": 0, + "phplrt": "array{non-falsy-string}", + "phpregex": "array{'0'}" + }, + { + "pattern": "/(\\Q0\\E)/U", + "call": "match:null", + "subject": "MA==", + "engine": "YToyOntpOjA7czoxOiIwIjtpOjE7czoxOiIwIjt9", + "reason": "falsy", + "key": 0, + "phplrt": "array{non-falsy-string, 'Q0E'}", + "phpregex": "array{'0', '0'}" + }, + { + "pattern": "/(\\Q0\\E)/mD", + "call": "match:null", + "subject": "MA==", + "engine": "YToyOntpOjA7czoxOiIwIjtpOjE7czoxOiIwIjt9", + "reason": "falsy", + "key": 0, + "phplrt": "array{non-falsy-string, 'Q0E'}", + "phpregex": "array{'0', '0'}" + }, + { + "pattern": "/(\\Q0\\E)/su", + "call": "match:null", + "subject": "MA==", + "engine": "YToyOntpOjA7czoxOiIwIjtpOjE7czoxOiIwIjt9", + "reason": "falsy", + "key": 0, + "phplrt": "array{non-falsy-string, 'Q0E'}", + "phpregex": "array{'0', '0'}" + }, + { + "pattern": "/(\\Q0\\E)/xu", + "call": "match:null", + "subject": "MA==", + "engine": "YToyOntpOjA7czoxOiIwIjtpOjE7czoxOiIwIjt9", + "reason": "falsy", + "key": 0, + "phplrt": "array{non-falsy-string, 'Q0E'}", + "phpregex": "array{'0', '0'}" + }, + { + "pattern": "#(\\Q0\\E)#", + "call": "match:null", + "subject": "MA==", + "engine": "YToyOntpOjA7czoxOiIwIjtpOjE7czoxOiIwIjt9", + "reason": "falsy", + "key": 0, + "phplrt": "array{non-falsy-string, 'Q0E'}", + "phpregex": "array{'0', '0'}" + }, + { + "pattern": "~(\\Q0\\E)~", + "call": "match:null", + "subject": "MA==", + "engine": "YToyOntpOjA7czoxOiIwIjtpOjE7czoxOiIwIjt9", + "reason": "falsy", + "key": 0, + "phplrt": "array{non-falsy-string, 'Q0E'}", + "phpregex": "array{'0', '0'}" + }, + { + "pattern": "{(\\Q0\\E)}", + "call": "match:null", + "subject": "MA==", + "engine": "YToyOntpOjA7czoxOiIwIjtpOjE7czoxOiIwIjt9", + "reason": "falsy", + "key": 0, + "phplrt": "array{non-falsy-string, 'Q0E'}", + "phpregex": "array{'0', '0'}" + }, + { + "pattern": "/(\\Q\\E)/", + "call": "match:null", + "subject": "", + "engine": "YToyOntpOjA7czowOiIiO2k6MTtzOjA6IiI7fQ==", + "reason": "falsy", + "key": 0, + "phplrt": "array{non-falsy-string, 'QE'}", + "phpregex": "array{'', ''}" + }, + { + "pattern": "/(\\Q\\E)/i", + "call": "match:null", + "subject": "", + "engine": "YToyOntpOjA7czowOiIiO2k6MTtzOjA6IiI7fQ==", + "reason": "falsy", + "key": 0, + "phplrt": "array{non-falsy-string, non-falsy-string}", + "phpregex": "array{'', ''}" + }, + { + "pattern": "/(\\Q\\E)/u", + "call": "match:null", + "subject": "", + "engine": "YToyOntpOjA7czowOiIiO2k6MTtzOjA6IiI7fQ==", + "reason": "falsy", + "key": 0, + "phplrt": "array{non-falsy-string, 'QE'}", + "phpregex": "array{'', ''}" + }, + { + "pattern": "/(\\Q\\E)/iu", + "call": "match:null", + "subject": "", + "engine": "YToyOntpOjA7czowOiIiO2k6MTtzOjA6IiI7fQ==", + "reason": "falsy", + "key": 0, + "phplrt": "array{non-falsy-string, non-falsy-string}", + "phpregex": "array{'', ''}" + }, + { + "pattern": "/(\\Q\\E)/m", + "call": "match:null", + "subject": "", + "engine": "YToyOntpOjA7czowOiIiO2k6MTtzOjA6IiI7fQ==", + "reason": "falsy", + "key": 0, + "phplrt": "array{non-falsy-string, 'QE'}", + "phpregex": "array{'', ''}" + }, + { + "pattern": "/(\\Q\\E)/s", + "call": "match:null", + "subject": "", + "engine": "YToyOntpOjA7czowOiIiO2k6MTtzOjA6IiI7fQ==", + "reason": "falsy", + "key": 0, + "phplrt": "array{non-falsy-string, 'QE'}", + "phpregex": "array{'', ''}" + }, + { + "pattern": "/(\\Q\\E)/x", + "call": "match:null", + "subject": "", + "engine": "YToyOntpOjA7czowOiIiO2k6MTtzOjA6IiI7fQ==", + "reason": "falsy", + "key": 0, + "phplrt": "array{non-falsy-string, 'QE'}", + "phpregex": "array{'', ''}" + }, + { + "pattern": "/(\\Q\\E)/D", + "call": "match:null", + "subject": "", + "engine": "YToyOntpOjA7czowOiIiO2k6MTtzOjA6IiI7fQ==", + "reason": "falsy", + "key": 0, + "phplrt": "array{non-falsy-string, 'QE'}", + "phpregex": "array{'', ''}" + }, + { + "pattern": "/(\\Q\\E)/n", + "call": "match:null", + "subject": "", + "engine": "YToxOntpOjA7czowOiIiO30=", + "reason": "falsy", + "key": 0, + "phplrt": "array{non-falsy-string}", + "phpregex": "array{''}" + }, + { + "pattern": "/(\\Q\\E)/U", + "call": "match:null", + "subject": "", + "engine": "YToyOntpOjA7czowOiIiO2k6MTtzOjA6IiI7fQ==", + "reason": "falsy", + "key": 0, + "phplrt": "array{non-falsy-string, 'QE'}", + "phpregex": "array{'', ''}" + }, + { + "pattern": "/(\\Q\\E)/mD", + "call": "match:null", + "subject": "", + "engine": "YToyOntpOjA7czowOiIiO2k6MTtzOjA6IiI7fQ==", + "reason": "falsy", + "key": 0, + "phplrt": "array{non-falsy-string, 'QE'}", + "phpregex": "array{'', ''}" + }, + { + "pattern": "/(\\Q\\E)/su", + "call": "match:null", + "subject": "", + "engine": "YToyOntpOjA7czowOiIiO2k6MTtzOjA6IiI7fQ==", + "reason": "falsy", + "key": 0, + "phplrt": "array{non-falsy-string, 'QE'}", + "phpregex": "array{'', ''}" + }, + { + "pattern": "/(\\Q\\E)/xu", + "call": "match:null", + "subject": "", + "engine": "YToyOntpOjA7czowOiIiO2k6MTtzOjA6IiI7fQ==", + "reason": "falsy", + "key": 0, + "phplrt": "array{non-falsy-string, 'QE'}", + "phpregex": "array{'', ''}" + }, + { + "pattern": "#(\\Q\\E)#", + "call": "match:null", + "subject": "", + "engine": "YToyOntpOjA7czowOiIiO2k6MTtzOjA6IiI7fQ==", + "reason": "falsy", + "key": 0, + "phplrt": "array{non-falsy-string, 'QE'}", + "phpregex": "array{'', ''}" + }, + { + "pattern": "~(\\Q\\E)~", + "call": "match:null", + "subject": "", + "engine": "YToyOntpOjA7czowOiIiO2k6MTtzOjA6IiI7fQ==", + "reason": "falsy", + "key": 0, + "phplrt": "array{non-falsy-string, 'QE'}", + "phpregex": "array{'', ''}" + }, + { + "pattern": "{(\\Q\\E)}", + "call": "match:null", + "subject": "", + "engine": "YToyOntpOjA7czowOiIiO2k6MTtzOjA6IiI7fQ==", + "reason": "falsy", + "key": 0, + "phplrt": "array{non-falsy-string, 'QE'}", + "phpregex": "array{'', ''}" + }, + { + "pattern": "/(?=(\\d))\\w/", + "call": "match:null", + "subject": "MA==", + "engine": "YToyOntpOjA7czoxOiIwIjtpOjE7czoxOiIwIjt9", + "reason": "falsy", + "key": 0, + "phplrt": "array{non-falsy-string, numeric-string}", + "phpregex": "array{non-empty-string, numeric-string}" + }, + { + "pattern": "/(?=(\\d))\\w/i", + "call": "match:null", + "subject": "MA==", + "engine": "YToyOntpOjA7czoxOiIwIjtpOjE7czoxOiIwIjt9", + "reason": "falsy", + "key": 0, + "phplrt": "array{non-falsy-string, numeric-string}", + "phpregex": "array{non-empty-string, numeric-string}" + }, + { + "pattern": "/(?=(\\d))\\w/u", + "call": "match:null", + "subject": "MA==", + "engine": "YToyOntpOjA7czoxOiIwIjtpOjE7czoxOiIwIjt9", + "reason": "falsy", + "key": 0, + "phplrt": "array{non-falsy-string, numeric-string}", + "phpregex": "array{non-empty-string, non-empty-string}" + }, + { + "pattern": "/(?=(\\d))\\w/iu", + "call": "match:null", + "subject": "MA==", + "engine": "YToyOntpOjA7czoxOiIwIjtpOjE7czoxOiIwIjt9", + "reason": "falsy", + "key": 0, + "phplrt": "array{non-falsy-string, numeric-string}", + "phpregex": "array{non-empty-string, non-empty-string}" + }, + { + "pattern": "/(?=(\\d))\\w/m", + "call": "match:null", + "subject": "MA==", + "engine": "YToyOntpOjA7czoxOiIwIjtpOjE7czoxOiIwIjt9", + "reason": "falsy", + "key": 0, + "phplrt": "array{non-falsy-string, numeric-string}", + "phpregex": "array{non-empty-string, numeric-string}" + }, + { + "pattern": "/(?=(\\d))\\w/s", + "call": "match:null", + "subject": "MA==", + "engine": "YToyOntpOjA7czoxOiIwIjtpOjE7czoxOiIwIjt9", + "reason": "falsy", + "key": 0, + "phplrt": "array{non-falsy-string, numeric-string}", + "phpregex": "array{non-empty-string, numeric-string}" + }, + { + "pattern": "/(?=(\\d))\\w/x", + "call": "match:null", + "subject": "MA==", + "engine": "YToyOntpOjA7czoxOiIwIjtpOjE7czoxOiIwIjt9", + "reason": "falsy", + "key": 0, + "phplrt": "array{non-falsy-string, numeric-string}", + "phpregex": "array{non-empty-string, numeric-string}" + }, + { + "pattern": "/(?=(\\d))\\w/D", + "call": "match:null", + "subject": "MA==", + "engine": "YToyOntpOjA7czoxOiIwIjtpOjE7czoxOiIwIjt9", + "reason": "falsy", + "key": 0, + "phplrt": "array{non-falsy-string, numeric-string}", + "phpregex": "array{non-empty-string, numeric-string}" + }, + { + "pattern": "/(?=(\\d))\\w/n", + "call": "match:null", + "subject": "MA==", + "engine": "YToxOntpOjA7czoxOiIwIjt9", + "reason": "falsy", + "key": 0, + "phplrt": "array{non-falsy-string}", + "phpregex": "array{non-empty-string}" + }, + { + "pattern": "/(?=(\\d))\\w/U", + "call": "match:null", + "subject": "MA==", + "engine": "YToyOntpOjA7czoxOiIwIjtpOjE7czoxOiIwIjt9", + "reason": "falsy", + "key": 0, + "phplrt": "array{non-falsy-string, numeric-string}", + "phpregex": "array{non-empty-string, numeric-string}" + }, + { + "pattern": "/(?=(\\d))\\w/mD", + "call": "match:null", + "subject": "MA==", + "engine": "YToyOntpOjA7czoxOiIwIjtpOjE7czoxOiIwIjt9", + "reason": "falsy", + "key": 0, + "phplrt": "array{non-falsy-string, numeric-string}", + "phpregex": "array{non-empty-string, numeric-string}" + }, + { + "pattern": "/(?=(\\d))\\w/su", + "call": "match:null", + "subject": "MA==", + "engine": "YToyOntpOjA7czoxOiIwIjtpOjE7czoxOiIwIjt9", + "reason": "falsy", + "key": 0, + "phplrt": "array{non-falsy-string, numeric-string}", + "phpregex": "array{non-empty-string, non-empty-string}" + }, + { + "pattern": "/(?=(\\d))\\w/xu", + "call": "match:null", + "subject": "MA==", + "engine": "YToyOntpOjA7czoxOiIwIjtpOjE7czoxOiIwIjt9", + "reason": "falsy", + "key": 0, + "phplrt": "array{non-falsy-string, numeric-string}", + "phpregex": "array{non-empty-string, non-empty-string}" + }, + { + "pattern": "#(?=(\\d))\\w#", + "call": "match:null", + "subject": "MA==", + "engine": "YToyOntpOjA7czoxOiIwIjtpOjE7czoxOiIwIjt9", + "reason": "falsy", + "key": 0, + "phplrt": "array{non-falsy-string, numeric-string}", + "phpregex": "array{non-empty-string, numeric-string}" + }, + { + "pattern": "~(?=(\\d))\\w~", + "call": "match:null", + "subject": "MA==", + "engine": "YToyOntpOjA7czoxOiIwIjtpOjE7czoxOiIwIjt9", + "reason": "falsy", + "key": 0, + "phplrt": "array{non-falsy-string, numeric-string}", + "phpregex": "array{non-empty-string, numeric-string}" + }, + { + "pattern": "{(?=(\\d))\\w}", + "call": "match:null", + "subject": "MA==", + "engine": "YToyOntpOjA7czoxOiIwIjtpOjE7czoxOiIwIjt9", + "reason": "falsy", + "key": 0, + "phplrt": "array{non-falsy-string, numeric-string}", + "phpregex": "array{non-empty-string, numeric-string}" + }, + { + "pattern": "/(.)$/", + "call": "match:null", + "subject": "MA==", + "engine": "YToyOntpOjA7czoxOiIwIjtpOjE7czoxOiIwIjt9", + "reason": "falsy", + "key": 0, + "phplrt": "array{non-falsy-string, non-empty-string}", + "phpregex": "array{non-empty-string, non-empty-string}" + }, + { + "pattern": "/(.)$/i", + "call": "match:null", + "subject": "MA==", + "engine": "YToyOntpOjA7czoxOiIwIjtpOjE7czoxOiIwIjt9", + "reason": "falsy", + "key": 0, + "phplrt": "array{non-falsy-string, non-empty-string}", + "phpregex": "array{non-empty-string, non-empty-string}" + }, + { + "pattern": "/(.)$/u", + "call": "match:null", + "subject": "MA==", + "engine": "YToyOntpOjA7czoxOiIwIjtpOjE7czoxOiIwIjt9", + "reason": "falsy", + "key": 0, + "phplrt": "array{non-falsy-string, non-empty-string}", + "phpregex": "array{non-empty-string, non-empty-string}" + }, + { + "pattern": "/(.)$/iu", + "call": "match:null", + "subject": "MA==", + "engine": "YToyOntpOjA7czoxOiIwIjtpOjE7czoxOiIwIjt9", + "reason": "falsy", + "key": 0, + "phplrt": "array{non-falsy-string, non-empty-string}", + "phpregex": "array{non-empty-string, non-empty-string}" + }, + { + "pattern": "/(.)$/m", + "call": "match:null", + "subject": "MA==", + "engine": "YToyOntpOjA7czoxOiIwIjtpOjE7czoxOiIwIjt9", + "reason": "falsy", + "key": 0, + "phplrt": "array{non-falsy-string, non-empty-string}", + "phpregex": "array{non-empty-string, non-empty-string}" + }, + { + "pattern": "/(.)$/s", + "call": "match:null", + "subject": "MdmjMNmjLzA=", + "engine": "YToyOntpOjA7czoxOiIwIjtpOjE7czoxOiIwIjt9", + "reason": "falsy", + "key": 0, + "phplrt": "array{non-falsy-string, non-empty-string}", + "phpregex": "array{non-empty-string, non-empty-string}" + }, + { + "pattern": "/(.)$/x", + "call": "match:null", + "subject": "MA==", + "engine": "YToyOntpOjA7czoxOiIwIjtpOjE7czoxOiIwIjt9", + "reason": "falsy", + "key": 0, + "phplrt": "array{non-falsy-string, non-empty-string}", + "phpregex": "array{non-empty-string, non-empty-string}" + }, + { + "pattern": "/(.)$/D", + "call": "match:null", + "subject": "MA==", + "engine": "YToyOntpOjA7czoxOiIwIjtpOjE7czoxOiIwIjt9", + "reason": "falsy", + "key": 0, + "phplrt": "array{non-falsy-string, non-empty-string}", + "phpregex": "array{non-empty-string, non-empty-string}" + }, + { + "pattern": "/(.)$/n", + "call": "match:null", + "subject": "MA==", + "engine": "YToxOntpOjA7czoxOiIwIjt9", + "reason": "falsy", + "key": 0, + "phplrt": "array{non-falsy-string}", + "phpregex": "array{non-empty-string}" + }, + { + "pattern": "/(.)$/U", + "call": "match:null", + "subject": "MA==", + "engine": "YToyOntpOjA7czoxOiIwIjtpOjE7czoxOiIwIjt9", + "reason": "falsy", + "key": 0, + "phplrt": "array{non-falsy-string, non-empty-string}", + "phpregex": "array{non-empty-string, non-empty-string}" + }, + { + "pattern": "/(.)$/mD", + "call": "match:null", + "subject": "MA==", + "engine": "YToyOntpOjA7czoxOiIwIjtpOjE7czoxOiIwIjt9", + "reason": "falsy", + "key": 0, + "phplrt": "array{non-falsy-string, non-empty-string}", + "phpregex": "array{non-empty-string, non-empty-string}" + }, + { + "pattern": "/(.)$/su", + "call": "match:null", + "subject": "MA==", + "engine": "YToyOntpOjA7czoxOiIwIjtpOjE7czoxOiIwIjt9", + "reason": "falsy", + "key": 0, + "phplrt": "array{non-falsy-string, non-empty-string}", + "phpregex": "array{non-empty-string, non-empty-string}" + }, + { + "pattern": "/(.)$/xu", + "call": "match:null", + "subject": "MA==", + "engine": "YToyOntpOjA7czoxOiIwIjtpOjE7czoxOiIwIjt9", + "reason": "falsy", + "key": 0, + "phplrt": "array{non-falsy-string, non-empty-string}", + "phpregex": "array{non-empty-string, non-empty-string}" + }, + { + "pattern": "#(.)$#", + "call": "match:null", + "subject": "MA==", + "engine": "YToyOntpOjA7czoxOiIwIjtpOjE7czoxOiIwIjt9", + "reason": "falsy", + "key": 0, + "phplrt": "array{non-falsy-string, non-empty-string}", + "phpregex": "array{non-empty-string, non-empty-string}" + }, + { + "pattern": "~(.)$~", + "call": "match:null", + "subject": "MA==", + "engine": "YToyOntpOjA7czoxOiIwIjtpOjE7czoxOiIwIjt9", + "reason": "falsy", + "key": 0, + "phplrt": "array{non-falsy-string, non-empty-string}", + "phpregex": "array{non-empty-string, non-empty-string}" + }, + { + "pattern": "{(.)$}", + "call": "match:null", + "subject": "MA==", + "engine": "YToyOntpOjA7czoxOiIwIjtpOjE7czoxOiIwIjt9", + "reason": "falsy", + "key": 0, + "phplrt": "array{non-falsy-string, non-empty-string}", + "phpregex": "array{non-empty-string, non-empty-string}" + }, + { + "pattern": "/^(\\d+)\\z/u", + "call": "match:null", + "subject": "2ag=", + "engine": "YToyOntpOjA7czoyOiLZqCI7aToxO3M6Mjoi2agiO30=", + "reason": "value", + "key": 1, + "phplrt": "array{non-empty-string, numeric-string}", + "phpregex": "array{non-empty-string, non-empty-string}" + }, + { + "pattern": "/^(\\d+)\\z/iu", + "call": "match:null", + "subject": "2aQ=", + "engine": "YToyOntpOjA7czoyOiLZpCI7aToxO3M6Mjoi2aQiO30=", + "reason": "value", + "key": 1, + "phplrt": "array{non-empty-string, numeric-string}", + "phpregex": "array{non-empty-string, non-empty-string}" + }, + { + "pattern": "/^(\\d+)\\z/su", + "call": "match:null", + "subject": "2afZow==", + "engine": "YToyOntpOjA7czo0OiLZp9mjIjtpOjE7czo0OiLZp9mjIjt9", + "reason": "value", + "key": 1, + "phplrt": "array{non-empty-string, numeric-string}", + "phpregex": "array{non-empty-string, non-empty-string}" + }, + { + "pattern": "/^(\\d+)\\z/xu", + "call": "match:null", + "subject": "2afZqNmn", + "engine": "YToyOntpOjA7czo2OiLZp9mo2aciO2k6MTtzOjY6Itmn2ajZpyI7fQ==", + "reason": "value", + "key": 1, + "phplrt": "array{non-empty-string, numeric-string}", + "phpregex": "array{non-empty-string, non-empty-string}" + }, + { + "pattern": "/^(.)/", + "call": "match:null", + "subject": "MA==", + "engine": "YToyOntpOjA7czoxOiIwIjtpOjE7czoxOiIwIjt9", + "reason": "falsy", + "key": 0, + "phplrt": "array{non-falsy-string, non-empty-string}", + "phpregex": "array{non-empty-string, non-empty-string}" + }, + { + "pattern": "/^(.)/i", + "call": "match:null", + "subject": "MA==", + "engine": "YToyOntpOjA7czoxOiIwIjtpOjE7czoxOiIwIjt9", + "reason": "falsy", + "key": 0, + "phplrt": "array{non-falsy-string, non-empty-string}", + "phpregex": "array{non-empty-string, non-empty-string}" + }, + { + "pattern": "/^(.)/u", + "call": "match:null", + "subject": "MA==", + "engine": "YToyOntpOjA7czoxOiIwIjtpOjE7czoxOiIwIjt9", + "reason": "falsy", + "key": 0, + "phplrt": "array{non-falsy-string, non-empty-string}", + "phpregex": "array{non-empty-string, non-empty-string}" + }, + { + "pattern": "/^(.)/iu", + "call": "match:null", + "subject": "MA==", + "engine": "YToyOntpOjA7czoxOiIwIjtpOjE7czoxOiIwIjt9", + "reason": "falsy", + "key": 0, + "phplrt": "array{non-falsy-string, non-empty-string}", + "phpregex": "array{non-empty-string, non-empty-string}" + }, + { + "pattern": "/^(.)/m", + "call": "match:null", + "subject": "MA==", + "engine": "YToyOntpOjA7czoxOiIwIjtpOjE7czoxOiIwIjt9", + "reason": "falsy", + "key": 0, + "phplrt": "array{non-falsy-string, non-empty-string}", + "phpregex": "array{non-empty-string, non-empty-string}" + }, + { + "pattern": "/^(.)/s", + "call": "match:null", + "subject": "MA==", + "engine": "YToyOntpOjA7czoxOiIwIjtpOjE7czoxOiIwIjt9", + "reason": "falsy", + "key": 0, + "phplrt": "array{non-falsy-string, non-empty-string}", + "phpregex": "array{non-empty-string, non-empty-string}" + }, + { + "pattern": "/^(.)/x", + "call": "match:null", + "subject": "MA==", + "engine": "YToyOntpOjA7czoxOiIwIjtpOjE7czoxOiIwIjt9", + "reason": "falsy", + "key": 0, + "phplrt": "array{non-falsy-string, non-empty-string}", + "phpregex": "array{non-empty-string, non-empty-string}" + }, + { + "pattern": "/^(.)/D", + "call": "match:null", + "subject": "MA==", + "engine": "YToyOntpOjA7czoxOiIwIjtpOjE7czoxOiIwIjt9", + "reason": "falsy", + "key": 0, + "phplrt": "array{non-falsy-string, non-empty-string}", + "phpregex": "array{non-empty-string, non-empty-string}" + }, + { + "pattern": "/^(.)/n", + "call": "match:null", + "subject": "MA==", + "engine": "YToxOntpOjA7czoxOiIwIjt9", + "reason": "falsy", + "key": 0, + "phplrt": "array{non-falsy-string}", + "phpregex": "array{non-empty-string}" + }, + { + "pattern": "/^(.)/U", + "call": "match:null", + "subject": "MA==", + "engine": "YToyOntpOjA7czoxOiIwIjtpOjE7czoxOiIwIjt9", + "reason": "falsy", + "key": 0, + "phplrt": "array{non-falsy-string, non-empty-string}", + "phpregex": "array{non-empty-string, non-empty-string}" + }, + { + "pattern": "/^(.)/mD", + "call": "match:null", + "subject": "MA==", + "engine": "YToyOntpOjA7czoxOiIwIjtpOjE7czoxOiIwIjt9", + "reason": "falsy", + "key": 0, + "phplrt": "array{non-falsy-string, non-empty-string}", + "phpregex": "array{non-empty-string, non-empty-string}" + }, + { + "pattern": "/^(.)/su", + "call": "match:null", + "subject": "MA==", + "engine": "YToyOntpOjA7czoxOiIwIjtpOjE7czoxOiIwIjt9", + "reason": "falsy", + "key": 0, + "phplrt": "array{non-falsy-string, non-empty-string}", + "phpregex": "array{non-empty-string, non-empty-string}" + }, + { + "pattern": "/^(.)/xu", + "call": "match:null", + "subject": "MA==", + "engine": "YToyOntpOjA7czoxOiIwIjtpOjE7czoxOiIwIjt9", + "reason": "falsy", + "key": 0, + "phplrt": "array{non-falsy-string, non-empty-string}", + "phpregex": "array{non-empty-string, non-empty-string}" + }, + { + "pattern": "#^(.)#", + "call": "match:null", + "subject": "MA==", + "engine": "YToyOntpOjA7czoxOiIwIjtpOjE7czoxOiIwIjt9", + "reason": "falsy", + "key": 0, + "phplrt": "array{non-falsy-string, non-empty-string}", + "phpregex": "array{non-empty-string, non-empty-string}" + }, + { + "pattern": "~^(.)~", + "call": "match:null", + "subject": "MA==", + "engine": "YToyOntpOjA7czoxOiIwIjtpOjE7czoxOiIwIjt9", + "reason": "falsy", + "key": 0, + "phplrt": "array{non-falsy-string, non-empty-string}", + "phpregex": "array{non-empty-string, non-empty-string}" + }, + { + "pattern": "{^(.)}", + "call": "match:null", + "subject": "MA==", + "engine": "YToyOntpOjA7czoxOiIwIjtpOjE7czoxOiIwIjt9", + "reason": "falsy", + "key": 0, + "phplrt": "array{non-falsy-string, non-empty-string}", + "phpregex": "array{non-empty-string, non-empty-string}" + }, + { + "pattern": "/(\\d)$/u", + "call": "match:null", + "subject": "2aY=", + "engine": "YToyOntpOjA7czoyOiLZpiI7aToxO3M6Mjoi2aYiO30=", + "reason": "value", + "key": 1, + "phplrt": "array{non-empty-string, numeric-string}", + "phpregex": "array{non-empty-string, non-empty-string}" + }, + { + "pattern": "/(\\d)$/iu", + "call": "match:null", + "subject": "2ag=", + "engine": "YToyOntpOjA7czoyOiLZqCI7aToxO3M6Mjoi2agiO30=", + "reason": "value", + "key": 1, + "phplrt": "array{non-empty-string, numeric-string}", + "phpregex": "array{non-empty-string, non-empty-string}" + }, + { + "pattern": "/(\\d)$/su", + "call": "match:null", + "subject": "2aQ=", + "engine": "YToyOntpOjA7czoyOiLZpCI7aToxO3M6Mjoi2aQiO30=", + "reason": "value", + "key": 1, + "phplrt": "array{non-empty-string, numeric-string}", + "phpregex": "array{non-empty-string, non-empty-string}" + }, + { + "pattern": "/(\\d)$/xu", + "call": "match:null", + "subject": "2aQ=", + "engine": "YToyOntpOjA7czoyOiLZpCI7aToxO3M6Mjoi2aQiO30=", + "reason": "value", + "key": 1, + "phplrt": "array{non-empty-string, numeric-string}", + "phpregex": "array{non-empty-string, non-empty-string}" + }, + { + "pattern": "/(?s)(.)/", + "call": "match:null", + "subject": "MA==", + "engine": "YToyOntpOjA7czoxOiIwIjtpOjE7czoxOiIwIjt9", + "reason": "falsy", + "key": 0, + "phplrt": "array{non-falsy-string, non-empty-string}", + "phpregex": "array{non-empty-string, non-empty-string}" + }, + { + "pattern": "/(?s)(.)/i", + "call": "match:null", + "subject": "MA==", + "engine": "YToyOntpOjA7czoxOiIwIjtpOjE7czoxOiIwIjt9", + "reason": "falsy", + "key": 0, + "phplrt": "array{non-falsy-string, non-empty-string}", + "phpregex": "array{non-empty-string, non-empty-string}" + }, + { + "pattern": "/(?s)(.)/u", + "call": "match:null", + "subject": "MA==", + "engine": "YToyOntpOjA7czoxOiIwIjtpOjE7czoxOiIwIjt9", + "reason": "falsy", + "key": 0, + "phplrt": "array{non-falsy-string, non-empty-string}", + "phpregex": "array{non-empty-string, non-empty-string}" + }, + { + "pattern": "/(?s)(.)/iu", + "call": "match:null", + "subject": "MA==", + "engine": "YToyOntpOjA7czoxOiIwIjtpOjE7czoxOiIwIjt9", + "reason": "falsy", + "key": 0, + "phplrt": "array{non-falsy-string, non-empty-string}", + "phpregex": "array{non-empty-string, non-empty-string}" + }, + { + "pattern": "/(?s)(.)/m", + "call": "match:null", + "subject": "MA==", + "engine": "YToyOntpOjA7czoxOiIwIjtpOjE7czoxOiIwIjt9", + "reason": "falsy", + "key": 0, + "phplrt": "array{non-falsy-string, non-empty-string}", + "phpregex": "array{non-empty-string, non-empty-string}" + }, + { + "pattern": "/(?s)(.)/s", + "call": "match:null", + "subject": "MA==", + "engine": "YToyOntpOjA7czoxOiIwIjtpOjE7czoxOiIwIjt9", + "reason": "falsy", + "key": 0, + "phplrt": "array{non-falsy-string, non-empty-string}", + "phpregex": "array{non-empty-string, non-empty-string}" + }, + { + "pattern": "/(?s)(.)/x", + "call": "match:null", + "subject": "MA==", + "engine": "YToyOntpOjA7czoxOiIwIjtpOjE7czoxOiIwIjt9", + "reason": "falsy", + "key": 0, + "phplrt": "array{non-falsy-string, non-empty-string}", + "phpregex": "array{non-empty-string, non-empty-string}" + }, + { + "pattern": "/(?s)(.)/D", + "call": "match:null", + "subject": "MA==", + "engine": "YToyOntpOjA7czoxOiIwIjtpOjE7czoxOiIwIjt9", + "reason": "falsy", + "key": 0, + "phplrt": "array{non-falsy-string, non-empty-string}", + "phpregex": "array{non-empty-string, non-empty-string}" + }, + { + "pattern": "/(?s)(.)/n", + "call": "match:null", + "subject": "MA==", + "engine": "YToxOntpOjA7czoxOiIwIjt9", + "reason": "falsy", + "key": 0, + "phplrt": "array{non-falsy-string}", + "phpregex": "array{non-empty-string}" + }, + { + "pattern": "/(?s)(.)/U", + "call": "match:null", + "subject": "MA==", + "engine": "YToyOntpOjA7czoxOiIwIjtpOjE7czoxOiIwIjt9", + "reason": "falsy", + "key": 0, + "phplrt": "array{non-falsy-string, non-empty-string}", + "phpregex": "array{non-empty-string, non-empty-string}" + }, + { + "pattern": "/(?s)(.)/mD", + "call": "match:null", + "subject": "MA==", + "engine": "YToyOntpOjA7czoxOiIwIjtpOjE7czoxOiIwIjt9", + "reason": "falsy", + "key": 0, + "phplrt": "array{non-falsy-string, non-empty-string}", + "phpregex": "array{non-empty-string, non-empty-string}" + }, + { + "pattern": "/(?s)(.)/su", + "call": "match:null", + "subject": "MA==", + "engine": "YToyOntpOjA7czoxOiIwIjtpOjE7czoxOiIwIjt9", + "reason": "falsy", + "key": 0, + "phplrt": "array{non-falsy-string, non-empty-string}", + "phpregex": "array{non-empty-string, non-empty-string}" + }, + { + "pattern": "/(?s)(.)/xu", + "call": "match:null", + "subject": "MA==", + "engine": "YToyOntpOjA7czoxOiIwIjtpOjE7czoxOiIwIjt9", + "reason": "falsy", + "key": 0, + "phplrt": "array{non-falsy-string, non-empty-string}", + "phpregex": "array{non-empty-string, non-empty-string}" + }, + { + "pattern": "#(?s)(.)#", + "call": "match:null", + "subject": "MA==", + "engine": "YToyOntpOjA7czoxOiIwIjtpOjE7czoxOiIwIjt9", + "reason": "falsy", + "key": 0, + "phplrt": "array{non-falsy-string, non-empty-string}", + "phpregex": "array{non-empty-string, non-empty-string}" + }, + { + "pattern": "~(?s)(.)~", + "call": "match:null", + "subject": "MA==", + "engine": "YToyOntpOjA7czoxOiIwIjtpOjE7czoxOiIwIjt9", + "reason": "falsy", + "key": 0, + "phplrt": "array{non-falsy-string, non-empty-string}", + "phpregex": "array{non-empty-string, non-empty-string}" + }, + { + "pattern": "{(?s)(.)}", + "call": "match:null", + "subject": "MA==", + "engine": "YToyOntpOjA7czoxOiIwIjtpOjE7czoxOiIwIjt9", + "reason": "falsy", + "key": 0, + "phplrt": "array{non-falsy-string, non-empty-string}", + "phpregex": "array{non-empty-string, non-empty-string}" + }, + { + "pattern": "/(?x)(\\d +)/", + "call": "match:null", + "subject": "MA==", + "engine": "YToyOntpOjA7czoxOiIwIjtpOjE7czoxOiIwIjt9", + "reason": "falsy", + "key": 0, + "phplrt": "array{non-falsy-string, non-falsy-string}", + "phpregex": "array{numeric-string, numeric-string}" + }, + { + "pattern": "/(?x)(\\d +)/i", + "call": "match:null", + "subject": "MA==", + "engine": "YToyOntpOjA7czoxOiIwIjtpOjE7czoxOiIwIjt9", + "reason": "falsy", + "key": 0, + "phplrt": "array{non-falsy-string, non-falsy-string}", + "phpregex": "array{numeric-string, numeric-string}" + }, + { + "pattern": "/(?x)(\\d +)/u", + "call": "match:null", + "subject": "MA==", + "engine": "YToyOntpOjA7czoxOiIwIjtpOjE7czoxOiIwIjt9", + "reason": "falsy", + "key": 0, + "phplrt": "array{non-falsy-string, non-falsy-string}", + "phpregex": "array{non-empty-string, non-empty-string}" + }, + { + "pattern": "/(?x)(\\d +)/iu", + "call": "match:null", + "subject": "MA==", + "engine": "YToyOntpOjA7czoxOiIwIjtpOjE7czoxOiIwIjt9", + "reason": "falsy", + "key": 0, + "phplrt": "array{non-falsy-string, non-falsy-string}", + "phpregex": "array{non-empty-string, non-empty-string}" + }, + { + "pattern": "/(?x)(\\d +)/m", + "call": "match:null", + "subject": "MA==", + "engine": "YToyOntpOjA7czoxOiIwIjtpOjE7czoxOiIwIjt9", + "reason": "falsy", + "key": 0, + "phplrt": "array{non-falsy-string, non-falsy-string}", + "phpregex": "array{numeric-string, numeric-string}" + }, + { + "pattern": "/(?x)(\\d +)/s", + "call": "match:null", + "subject": "MA==", + "engine": "YToyOntpOjA7czoxOiIwIjtpOjE7czoxOiIwIjt9", + "reason": "falsy", + "key": 0, + "phplrt": "array{non-falsy-string, non-falsy-string}", + "phpregex": "array{numeric-string, numeric-string}" + }, + { + "pattern": "/(?x)(\\d +)/D", + "call": "match:null", + "subject": "MA==", + "engine": "YToyOntpOjA7czoxOiIwIjtpOjE7czoxOiIwIjt9", + "reason": "falsy", + "key": 0, + "phplrt": "array{non-falsy-string, non-falsy-string}", + "phpregex": "array{numeric-string, numeric-string}" + }, + { + "pattern": "/(?x)(\\d +)/n", + "call": "match:null", + "subject": "MA==", + "engine": "YToxOntpOjA7czoxOiIwIjt9", + "reason": "falsy", + "key": 0, + "phplrt": "array{non-falsy-string}", + "phpregex": "array{numeric-string}" + }, + { + "pattern": "/(?x)(\\d +)/U", + "call": "match:null", + "subject": "MDE=", + "engine": "YToyOntpOjA7czoxOiIwIjtpOjE7czoxOiIwIjt9", + "reason": "falsy", + "key": 0, + "phplrt": "array{non-falsy-string, non-falsy-string}", + "phpregex": "array{numeric-string, numeric-string}" + }, + { + "pattern": "/(?x)(\\d +)/mD", + "call": "match:null", + "subject": "MA==", + "engine": "YToyOntpOjA7czoxOiIwIjtpOjE7czoxOiIwIjt9", + "reason": "falsy", + "key": 0, + "phplrt": "array{non-falsy-string, non-falsy-string}", + "phpregex": "array{numeric-string, numeric-string}" + }, + { + "pattern": "/(?x)(\\d +)/su", + "call": "match:null", + "subject": "MA==", + "engine": "YToyOntpOjA7czoxOiIwIjtpOjE7czoxOiIwIjt9", + "reason": "falsy", + "key": 0, + "phplrt": "array{non-falsy-string, non-falsy-string}", + "phpregex": "array{non-empty-string, non-empty-string}" + }, + { + "pattern": "/(?x)(\\d +)/xu", + "call": "match:null", + "subject": "2anZqA==", + "engine": "YToyOntpOjA7czo0OiLZqdmoIjtpOjE7czo0OiLZqdmoIjt9", + "reason": "value", + "key": 1, + "phplrt": "array{non-empty-string, numeric-string}", + "phpregex": "array{non-empty-string, non-empty-string}" + }, + { + "pattern": "#(?x)(\\d +)#", + "call": "match:null", + "subject": "MA==", + "engine": "YToyOntpOjA7czoxOiIwIjtpOjE7czoxOiIwIjt9", + "reason": "falsy", + "key": 0, + "phplrt": "array{non-falsy-string, non-falsy-string}", + "phpregex": "array{numeric-string, numeric-string}" + }, + { + "pattern": "~(?x)(\\d +)~", + "call": "match:null", + "subject": "MA==", + "engine": "YToyOntpOjA7czoxOiIwIjtpOjE7czoxOiIwIjt9", + "reason": "falsy", + "key": 0, + "phplrt": "array{non-falsy-string, non-falsy-string}", + "phpregex": "array{numeric-string, numeric-string}" + }, + { + "pattern": "{(?x)(\\d +)}", + "call": "match:null", + "subject": "YjDZozFL", + "engine": "YToyOntpOjA7czoxOiIwIjtpOjE7czoxOiIwIjt9", + "reason": "falsy", + "key": 0, + "phplrt": "array{non-falsy-string, non-falsy-string}", + "phpregex": "array{numeric-string, numeric-string}" + }, + { + "pattern": "/(?i)(k)/", + "call": "match:null", + "subject": "Sw==", + "engine": "YToyOntpOjA7czoxOiJLIjtpOjE7czoxOiJLIjt9", + "reason": "value", + "key": 1, + "phplrt": "array{non-falsy-string, 'k'}", + "phpregex": "array{non-falsy-string, non-falsy-string}" + }, + { + "pattern": "/(?i)(k)/u", + "call": "match:null", + "subject": "Sw==", + "engine": "YToyOntpOjA7czoxOiJLIjtpOjE7czoxOiJLIjt9", + "reason": "value", + "key": 1, + "phplrt": "array{non-falsy-string, 'k'}", + "phpregex": "array{non-falsy-string, non-falsy-string}" + }, + { + "pattern": "/(?i)(k)/m", + "call": "match:null", + "subject": "Sw==", + "engine": "YToyOntpOjA7czoxOiJLIjtpOjE7czoxOiJLIjt9", + "reason": "value", + "key": 1, + "phplrt": "array{non-falsy-string, 'k'}", + "phpregex": "array{non-falsy-string, non-falsy-string}" + }, + { + "pattern": "/(?i)(k)/s", + "call": "match:null", + "subject": "Sw==", + "engine": "YToyOntpOjA7czoxOiJLIjtpOjE7czoxOiJLIjt9", + "reason": "value", + "key": 1, + "phplrt": "array{non-falsy-string, 'k'}", + "phpregex": "array{non-falsy-string, non-falsy-string}" + }, + { + "pattern": "/(?i)(k)/x", + "call": "match:null", + "subject": "Sw==", + "engine": "YToyOntpOjA7czoxOiJLIjtpOjE7czoxOiJLIjt9", + "reason": "value", + "key": 1, + "phplrt": "array{non-falsy-string, 'k'}", + "phpregex": "array{non-falsy-string, non-falsy-string}" + }, + { + "pattern": "/(?i)(k)/D", + "call": "match:null", + "subject": "Sw==", + "engine": "YToyOntpOjA7czoxOiJLIjtpOjE7czoxOiJLIjt9", + "reason": "value", + "key": 1, + "phplrt": "array{non-falsy-string, 'k'}", + "phpregex": "array{non-falsy-string, non-falsy-string}" + }, + { + "pattern": "/(?i)(k)/U", + "call": "match:null", + "subject": "Sw==", + "engine": "YToyOntpOjA7czoxOiJLIjtpOjE7czoxOiJLIjt9", + "reason": "value", + "key": 1, + "phplrt": "array{non-falsy-string, 'k'}", + "phpregex": "array{non-falsy-string, non-falsy-string}" + }, + { + "pattern": "/(?i)(k)/mD", + "call": "match:null", + "subject": "Sw==", + "engine": "YToyOntpOjA7czoxOiJLIjtpOjE7czoxOiJLIjt9", + "reason": "value", + "key": 1, + "phplrt": "array{non-falsy-string, 'k'}", + "phpregex": "array{non-falsy-string, non-falsy-string}" + }, + { + "pattern": "/(?i)(k)/su", + "call": "match:null", + "subject": "Sw==", + "engine": "YToyOntpOjA7czoxOiJLIjtpOjE7czoxOiJLIjt9", + "reason": "value", + "key": 1, + "phplrt": "array{non-falsy-string, 'k'}", + "phpregex": "array{non-falsy-string, non-falsy-string}" + }, + { + "pattern": "/(?i)(k)/xu", + "call": "match:null", + "subject": "Sw==", + "engine": "YToyOntpOjA7czoxOiJLIjtpOjE7czoxOiJLIjt9", + "reason": "value", + "key": 1, + "phplrt": "array{non-falsy-string, 'k'}", + "phpregex": "array{non-falsy-string, non-falsy-string}" + }, + { + "pattern": "#(?i)(k)#", + "call": "match:null", + "subject": "Sw==", + "engine": "YToyOntpOjA7czoxOiJLIjtpOjE7czoxOiJLIjt9", + "reason": "value", + "key": 1, + "phplrt": "array{non-falsy-string, 'k'}", + "phpregex": "array{non-falsy-string, non-falsy-string}" + }, + { + "pattern": "~(?i)(k)~", + "call": "match:null", + "subject": "Sw==", + "engine": "YToyOntpOjA7czoxOiJLIjtpOjE7czoxOiJLIjt9", + "reason": "value", + "key": 1, + "phplrt": "array{non-falsy-string, 'k'}", + "phpregex": "array{non-falsy-string, non-falsy-string}" + }, + { + "pattern": "{(?i)(k)}", + "call": "match:null", + "subject": "Sw==", + "engine": "YToyOntpOjA7czoxOiJLIjtpOjE7czoxOiJLIjt9", + "reason": "value", + "key": 1, + "phplrt": "array{non-falsy-string, 'k'}", + "phpregex": "array{non-falsy-string, non-falsy-string}" + } +] diff --git a/poc/witnesses/adversarial.md b/poc/witnesses/adversarial.md new file mode 100644 index 00000000000..e6ded8c5f7a --- /dev/null +++ b/poc/witnesses/adversarial.md @@ -0,0 +1,1592 @@ +# generated edge cases + +One witness for each of the 308 patterns whose inferred type the engine contradicts with the parser generated by phplrt. `php poc/bin/verify.php` replays them. + +- a value that can be '0' or '' is typed non-falsy or non-empty: 114 +- a value outside the type of its key: 160 +- a key the engine writes is typed absent: 0 +- a key the engine leaves out is typed present: 34 +- the subject is narrowed further than the match proves: 0 + +| pattern | call | subject | the engine writes | phplrt | PHPRegex | why | +|---|---|---|---|---|---|---| +| `/(\d)/u` | preg_match() | `'٣'` | `["٣","٣"]` | `array{non-empty-string, numeric-string}` | `array{non-empty-string, non-empty-string}` | key `1`: a value outside the type of its key | +| `/(\d)/iu` | preg_match() | `'٧'` | `["٧","٧"]` | `array{non-empty-string, numeric-string}` | `array{non-empty-string, non-empty-string}` | key `1`: a value outside the type of its key | +| `/(\d)/su` | preg_match() | `'٨'` | `["٨","٨"]` | `array{non-empty-string, numeric-string}` | `array{non-empty-string, non-empty-string}` | key `1`: a value outside the type of its key | +| `/(\d)/xu` | preg_match() | `'٦'` | `["٦","٦"]` | `array{non-empty-string, numeric-string}` | `array{non-empty-string, non-empty-string}` | key `1`: a value outside the type of its key | +| `/(\d+)/u` | preg_match() | `'٥٠'` | `["٥٠","٥٠"]` | `array{non-empty-string, numeric-string}` | `array{non-empty-string, non-empty-string}` | key `1`: a value outside the type of its key | +| `/(\d+)/iu` | preg_match() | `'٠'` | `["٠","٠"]` | `array{non-empty-string, numeric-string}` | `array{non-empty-string, non-empty-string}` | key `1`: a value outside the type of its key | +| `/(\d+)/su` | preg_match() | `'٠٤'` | `["٠٤","٠٤"]` | `array{non-empty-string, numeric-string}` | `array{non-empty-string, non-empty-string}` | key `1`: a value outside the type of its key | +| `/(\d+)/xu` | preg_match() | `'٣'` | `["٣","٣"]` | `array{non-empty-string, numeric-string}` | `array{non-empty-string, non-empty-string}` | key `1`: a value outside the type of its key | +| `/(\d*)/u` | preg_match() | `'١'` | `["١","١"]` | `array{string, ''\|numeric-string}` | `array{string, string}` | key `1`: a value outside the type of its key | +| `/(\d*)/iu` | preg_match() | `'٠'` | `["٠","٠"]` | `array{string, ''\|numeric-string}` | `array{string, string}` | key `1`: a value outside the type of its key | +| `/(\d*)/su` | preg_match() | `'٦٩'` | `["٦٩","٦٩"]` | `array{string, ''\|numeric-string}` | `array{string, string}` | key `1`: a value outside the type of its key | +| `/(\d*)/xu` | preg_match() | `'٩'` | `["٩","٩"]` | `array{string, ''\|numeric-string}` | `array{string, string}` | key `1`: a value outside the type of its key | +| `/^(\d+)$/u` | preg_match() | `'٦٠'` | `["٦٠","٦٠"]` | `array{non-empty-string, numeric-string}` | `array{non-empty-string, non-empty-string}` | key `1`: a value outside the type of its key | +| `/^(\d+)$/iu` | preg_match() | `'٣٠٨'` | `["٣٠٨","٣٠٨"]` | `array{non-empty-string, numeric-string}` | `array{non-empty-string, non-empty-string}` | key `1`: a value outside the type of its key | +| `/^(\d+)$/su` | preg_match() | `'٧١٦'` | `["٧١٦","٧١٦"]` | `array{non-empty-string, numeric-string}` | `array{non-empty-string, non-empty-string}` | key `1`: a value outside the type of its key | +| `/^(\d+)$/xu` | preg_match() | `'٦٨'` | `["٦٨","٦٨"]` | `array{non-empty-string, numeric-string}` | `array{non-empty-string, non-empty-string}` | key `1`: a value outside the type of its key | +| `/(\d{2})/u` | preg_match() | `'٢٤'` | `["٢٤","٢٤"]` | `array{non-falsy-string, non-falsy-string&numeric-string}` | `array{non-falsy-string, non-falsy-string}` | key `1`: a value outside the type of its key | +| `/(\d{2})/iu` | preg_match() | `'٤٧'` | `["٤٧","٤٧"]` | `array{non-falsy-string, non-falsy-string&numeric-string}` | `array{non-falsy-string, non-falsy-string}` | key `1`: a value outside the type of its key | +| `/(\d{2})/su` | preg_match() | `'٠٥'` | `["٠٥","٠٥"]` | `array{non-falsy-string, non-falsy-string&numeric-string}` | `array{non-falsy-string, non-falsy-string}` | key `1`: a value outside the type of its key | +| `/(\d{2})/xu` | preg_match() | `'١٢'` | `["١٢","١٢"]` | `array{non-falsy-string, non-falsy-string&numeric-string}` | `array{non-falsy-string, non-falsy-string}` | key `1`: a value outside the type of its key | +| `/([[:digit:]]+)/u` | preg_match() | `'٣٣9'` | `["٣٣9","٣٣9"]` | `array{non-empty-string, numeric-string}` | `array{non-empty-string, non-empty-string}` | key `1`: a value outside the type of its key | +| `/([[:digit:]]+)/iu` | preg_match() | `'00'` | `["00","00"]` | `array{non-empty-string, numeric-string}` | `array{non-empty-string, non-empty-string}` | key `1`: a value outside the type of its key | +| `/([[:digit:]]+)/su` | preg_match() | `'0034'` | `["0034","0034"]` | `array{non-empty-string, numeric-string}` | `array{non-empty-string, non-empty-string}` | key `1`: a value outside the type of its key | +| `/([[:digit:]]+)/xu` | preg_match() | `'٣'` | `["٣","٣"]` | `array{non-empty-string, numeric-string}` | `array{non-empty-string, non-empty-string}` | key `1`: a value outside the type of its key | +| `/([\d]+)/u` | preg_match() | `'٠١٠٠'` | `["٠١٠٠","٠١٠٠"]` | `array{non-empty-string, numeric-string}` | `array{non-empty-string, non-empty-string}` | key `1`: a value outside the type of its key | +| `/([\d]+)/iu` | preg_match() | `'١٢٠٧'` | `["١٢٠٧","١٢٠٧"]` | `array{non-empty-string, numeric-string}` | `array{non-empty-string, non-empty-string}` | key `1`: a value outside the type of its key | +| `/([\d]+)/su` | preg_match() | `'٣'` | `["٣","٣"]` | `array{non-empty-string, numeric-string}` | `array{non-empty-string, non-empty-string}` | key `1`: a value outside the type of its key | +| `/([\d]+)/xu` | preg_match() | `'٠'` | `["٠","٠"]` | `array{non-empty-string, numeric-string}` | `array{non-empty-string, non-empty-string}` | key `1`: a value outside the type of its key | +| `/(\d)\s/u` | preg_match() | `'٥ '` | `["٥\t","٥"]` | `array{non-falsy-string, numeric-string}` | `array{non-falsy-string, non-empty-string}` | key `1`: a value outside the type of its key | +| `/(\d)\s/iu` | preg_match() | `'٥ '` | `["٥\t","٥"]` | `array{non-falsy-string, numeric-string}` | `array{non-falsy-string, non-empty-string}` | key `1`: a value outside the type of its key | +| `/(\d)\s/su` | preg_match() | `'٣\n'` | `["٣\n","٣"]` | `array{non-falsy-string, numeric-string}` | `array{non-falsy-string, non-empty-string}` | key `1`: a value outside the type of its key | +| `/(\d)\s/xu` | preg_match() | `'٤ '` | `["٤\t","٤"]` | `array{non-falsy-string, numeric-string}` | `array{non-falsy-string, non-empty-string}` | key `1`: a value outside the type of its key | +| `/(\d)(\d)/u` | preg_match() | `'٨٦'` | `["٨٦","٨","٦"]` | `array{non-falsy-string, numeric-string, numeric-string}` | `array{non-falsy-string, non-empty-string, non-empty-string}` | key `1`: a value outside the type of its key | +| `/(\d)(\d)/iu` | preg_match() | `'٢٣'` | `["٢٣","٢","٣"]` | `array{non-falsy-string, numeric-string, numeric-string}` | `array{non-falsy-string, non-empty-string, non-empty-string}` | key `1`: a value outside the type of its key | +| `/(\d)(\d)/su` | preg_match() | `'٤٠'` | `["٤٠","٤","٠"]` | `array{non-falsy-string, numeric-string, numeric-string}` | `array{non-falsy-string, non-empty-string, non-empty-string}` | key `1`: a value outside the type of its key | +| `/(\d)(\d)/xu` | preg_match() | `'٩٦'` | `["٩٦","٩","٦"]` | `array{non-falsy-string, numeric-string, numeric-string}` | `array{non-falsy-string, non-empty-string, non-empty-string}` | key `1`: a value outside the type of its key | +| `/(\d+)\|(x)/u` | preg_match() | `'٧'` | `["٧","٧"]` | `array{non-empty-string, '', 'x'}\|array{non-empty-string, numeric-string}` | `array{non-empty-string, '', 'x'}\|array{non-empty-string, non-empty-string}` | key `1`: a value outside the type of its key | +| `/(\d+)\|(x)/iu` | preg_match() | `'٠٠٩٧'` | `["٠٠٩٧","٠٠٩٧"]` | `array{non-empty-string, '', non-empty-string}\|array{non-empty-string, numeric-string}` | `array{non-empty-string, '', non-falsy-string}\|array{non-empty-string, non-empty-string}` | key `1`: a value outside the type of its key | +| `/(\d+)\|(x)/su` | preg_match() | `'٧٦٨'` | `["٧٦٨","٧٦٨"]` | `array{non-empty-string, '', 'x'}\|array{non-empty-string, numeric-string}` | `array{non-empty-string, '', 'x'}\|array{non-empty-string, non-empty-string}` | key `1`: a value outside the type of its key | +| `/(\d+)\|(x)/xu` | preg_match() | `'٥٣٥'` | `["٥٣٥","٥٣٥"]` | `array{non-empty-string, '', 'x'}\|array{non-empty-string, numeric-string}` | `array{non-empty-string, '', 'x'}\|array{non-empty-string, non-empty-string}` | key `1`: a value outside the type of its key | +| `/(?\d+)/u` | preg_match() | `'٢٥٣٧'` | `{"0":"٢٥٣٧","n":"٢٥٣٧","1":"٢٥٣٧"}` | `array{0: non-empty-string, n: numeric-string, 1: numeric-string}` | `array{0: non-empty-string, n: non-empty-string, 1: non-empty-string}` | key `n`: a value outside the type of its key | +| `/(?\d+)/iu` | preg_match() | `'٢٨٣'` | `{"0":"٢٨٣","n":"٢٨٣","1":"٢٨٣"}` | `array{0: non-empty-string, n: numeric-string, 1: numeric-string}` | `array{0: non-empty-string, n: non-empty-string, 1: non-empty-string}` | key `n`: a value outside the type of its key | +| `/(?\d+)/su` | preg_match() | `'٥٧١٩'` | `{"0":"٥٧١٩","n":"٥٧١٩","1":"٥٧١٩"}` | `array{0: non-empty-string, n: numeric-string, 1: numeric-string}` | `array{0: non-empty-string, n: non-empty-string, 1: non-empty-string}` | key `n`: a value outside the type of its key | +| `/(?\d+)/xu` | preg_match() | `'٣٢'` | `{"0":"٣٢","n":"٣٢","1":"٣٢"}` | `array{0: non-empty-string, n: numeric-string, 1: numeric-string}` | `array{0: non-empty-string, n: non-empty-string, 1: non-empty-string}` | key `n`: a value outside the type of its key | +| `/(\N{U+0030})/u` | preg_match() | `'0'` | `["0","0"]` | `array{non-falsy-string, non-falsy-string}` | `array{'0', '0'}` | key `0`: a value that can be '0' or '' is typed non-falsy or non-empty | +| `/(\N{U+0030})/iu` | preg_match() | `'0'` | `["0","0"]` | `array{non-falsy-string, non-falsy-string}` | `array{'0', '0'}` | key `0`: a value that can be '0' or '' is typed non-falsy or non-empty | +| `/(\N{U+0030})/su` | preg_match() | `'0'` | `["0","0"]` | `array{non-falsy-string, non-falsy-string}` | `array{'0', '0'}` | key `0`: a value that can be '0' or '' is typed non-falsy or non-empty | +| `/(\N{U+0030})/xu` | preg_match() | `'0'` | `["0","0"]` | `array{non-falsy-string, non-falsy-string}` | `array{'0', '0'}` | key `0`: a value that can be '0' or '' is typed non-falsy or non-empty | +| `/(?i)(\d+)/u` | preg_match() | `'٨٨٦١'` | `["٨٨٦١","٨٨٦١"]` | `array{non-empty-string, numeric-string}` | `array{non-empty-string, non-empty-string}` | key `1`: a value outside the type of its key | +| `/(?i)(\d+)/iu` | preg_match() | `'٢٠٩٨'` | `["٢٠٩٨","٢٠٩٨"]` | `array{non-empty-string, numeric-string}` | `array{non-empty-string, non-empty-string}` | key `1`: a value outside the type of its key | +| `/(?i)(\d+)/su` | preg_match() | `'٠٦'` | `["٠٦","٠٦"]` | `array{non-empty-string, numeric-string}` | `array{non-empty-string, non-empty-string}` | key `1`: a value outside the type of its key | +| `/(?i)(\d+)/xu` | preg_match() | `'٢١٩'` | `["٢١٩","٢١٩"]` | `array{non-empty-string, numeric-string}` | `array{non-empty-string, non-empty-string}` | key `1`: a value outside the type of its key | +| `/(\d+)(?i)/u` | preg_match() | `'٤١١٧'` | `["٤١١٧","٤١١٧"]` | `array{non-empty-string, numeric-string}` | `array{non-empty-string, non-empty-string}` | key `1`: a value outside the type of its key | +| `/(\d+)(?i)/iu` | preg_match() | `'٢٧٥٩'` | `["٢٧٥٩","٢٧٥٩"]` | `array{non-empty-string, numeric-string}` | `array{non-empty-string, non-empty-string}` | key `1`: a value outside the type of its key | +| `/(\d+)(?i)/su` | preg_match() | `'٩'` | `["٩","٩"]` | `array{non-empty-string, numeric-string}` | `array{non-empty-string, non-empty-string}` | key `1`: a value outside the type of its key | +| `/(\d+)(?i)/xu` | preg_match() | `'٩٢٩'` | `["٩٢٩","٩٢٩"]` | `array{non-empty-string, numeric-string}` | `array{non-empty-string, non-empty-string}` | key `1`: a value outside the type of its key | +| `/(\d\|a)/u` | preg_match() | `'٠'` | `["٠","٠"]` | `array{non-empty-string, 'a'\|numeric-string}` | `array{non-empty-string, non-empty-string}` | key `1`: a value outside the type of its key | +| `/(\d\|a)/su` | preg_match() | `'٠'` | `["٠","٠"]` | `array{non-empty-string, 'a'\|numeric-string}` | `array{non-empty-string, non-empty-string}` | key `1`: a value outside the type of its key | +| `/(\d\|a)/xu` | preg_match() | `'٦'` | `["٦","٦"]` | `array{non-empty-string, 'a'\|numeric-string}` | `array{non-empty-string, non-empty-string}` | key `1`: a value outside the type of its key | +| `/(\d\|٣)/u` | preg_match() | `'١'` | `["١","١"]` | `array{non-empty-string, '٣'\|numeric-string}` | `array{non-empty-string, non-empty-string}` | key `1`: a value outside the type of its key | +| `/(\d\|٣)/su` | preg_match() | `'٧'` | `["٧","٧"]` | `array{non-empty-string, '٣'\|numeric-string}` | `array{non-empty-string, non-empty-string}` | key `1`: a value outside the type of its key | +| `/(\d\|٣)/xu` | preg_match() | `'٧'` | `["٧","٧"]` | `array{non-empty-string, '٣'\|numeric-string}` | `array{non-empty-string, non-empty-string}` | key `1`: a value outside the type of its key | +| `/(?i)(a)/` | preg_match() | `'A'` | `["A","A"]` | `array{non-falsy-string, 'a'}` | `array{non-falsy-string, non-falsy-string}` | key `1`: a value outside the type of its key | +| `/(?i)(a)/u` | preg_match() | `'A'` | `["A","A"]` | `array{non-falsy-string, 'a'}` | `array{non-falsy-string, non-falsy-string}` | key `1`: a value outside the type of its key | +| `/(?i)(a)/m` | preg_match() | `'A'` | `["A","A"]` | `array{non-falsy-string, 'a'}` | `array{non-falsy-string, non-falsy-string}` | key `1`: a value outside the type of its key | +| `/(?i)(a)/s` | preg_match() | `'A'` | `["A","A"]` | `array{non-falsy-string, 'a'}` | `array{non-falsy-string, non-falsy-string}` | key `1`: a value outside the type of its key | +| `/(?i)(a)/x` | preg_match() | `'A'` | `["A","A"]` | `array{non-falsy-string, 'a'}` | `array{non-falsy-string, non-falsy-string}` | key `1`: a value outside the type of its key | +| `/(?i)(a)/D` | preg_match() | `'A'` | `["A","A"]` | `array{non-falsy-string, 'a'}` | `array{non-falsy-string, non-falsy-string}` | key `1`: a value outside the type of its key | +| `/(?i)(a)/U` | preg_match() | `'A'` | `["A","A"]` | `array{non-falsy-string, 'a'}` | `array{non-falsy-string, non-falsy-string}` | key `1`: a value outside the type of its key | +| `/(?i)(a)/mD` | preg_match() | `'A'` | `["A","A"]` | `array{non-falsy-string, 'a'}` | `array{non-falsy-string, non-falsy-string}` | key `1`: a value outside the type of its key | +| `/(?i)(a)/su` | preg_match() | `'A'` | `["A","A"]` | `array{non-falsy-string, 'a'}` | `array{non-falsy-string, non-falsy-string}` | key `1`: a value outside the type of its key | +| `/(?i)(a)/xu` | preg_match() | `'A'` | `["A","A"]` | `array{non-falsy-string, 'a'}` | `array{non-falsy-string, non-falsy-string}` | key `1`: a value outside the type of its key | +| `#(?i)(a)#` | preg_match() | `'A'` | `["A","A"]` | `array{non-falsy-string, 'a'}` | `array{non-falsy-string, non-falsy-string}` | key `1`: a value outside the type of its key | +| `~(?i)(a)~` | preg_match() | `'A'` | `["A","A"]` | `array{non-falsy-string, 'a'}` | `array{non-falsy-string, non-falsy-string}` | key `1`: a value outside the type of its key | +| `{(?i)(a)}` | preg_match() | `'A'` | `["A","A"]` | `array{non-falsy-string, 'a'}` | `array{non-falsy-string, non-falsy-string}` | key `1`: a value outside the type of its key | +| `/(?i:(a))/` | preg_match() | `'A'` | `["A","A"]` | `array{non-empty-string, 'a'}` | `array{non-falsy-string, non-falsy-string}` | key `1`: a value outside the type of its key | +| `/(?i:(a))/u` | preg_match() | `'A'` | `["A","A"]` | `array{non-empty-string, 'a'}` | `array{non-falsy-string, non-falsy-string}` | key `1`: a value outside the type of its key | +| `/(?i:(a))/m` | preg_match() | `'A'` | `["A","A"]` | `array{non-empty-string, 'a'}` | `array{non-falsy-string, non-falsy-string}` | key `1`: a value outside the type of its key | +| `/(?i:(a))/s` | preg_match() | `'A'` | `["A","A"]` | `array{non-empty-string, 'a'}` | `array{non-falsy-string, non-falsy-string}` | key `1`: a value outside the type of its key | +| `/(?i:(a))/x` | preg_match() | `'A'` | `["A","A"]` | `array{non-empty-string, 'a'}` | `array{non-falsy-string, non-falsy-string}` | key `1`: a value outside the type of its key | +| `/(?i:(a))/D` | preg_match() | `'A'` | `["A","A"]` | `array{non-empty-string, 'a'}` | `array{non-falsy-string, non-falsy-string}` | key `1`: a value outside the type of its key | +| `/(?i:(a))/U` | preg_match() | `'A'` | `["A","A"]` | `array{non-empty-string, 'a'}` | `array{non-falsy-string, non-falsy-string}` | key `1`: a value outside the type of its key | +| `/(?i:(a))/mD` | preg_match() | `'A'` | `["A","A"]` | `array{non-empty-string, 'a'}` | `array{non-falsy-string, non-falsy-string}` | key `1`: a value outside the type of its key | +| `/(?i:(a))/su` | preg_match() | `'A'` | `["A","A"]` | `array{non-empty-string, 'a'}` | `array{non-falsy-string, non-falsy-string}` | key `1`: a value outside the type of its key | +| `/(?i:(a))/xu` | preg_match() | `'A'` | `["A","A"]` | `array{non-empty-string, 'a'}` | `array{non-falsy-string, non-falsy-string}` | key `1`: a value outside the type of its key | +| `#(?i:(a))#` | preg_match() | `'A'` | `["A","A"]` | `array{non-empty-string, 'a'}` | `array{non-falsy-string, non-falsy-string}` | key `1`: a value outside the type of its key | +| `~(?i:(a))~` | preg_match() | `'A'` | `["A","A"]` | `array{non-empty-string, 'a'}` | `array{non-falsy-string, non-falsy-string}` | key `1`: a value outside the type of its key | +| `{(?i:(a))}` | preg_match() | `'A'` | `["A","A"]` | `array{non-empty-string, 'a'}` | `array{non-falsy-string, non-falsy-string}` | key `1`: a value outside the type of its key | +| `/(?^i)(a)/` | preg_match() | `'A'` | `["A","A"]` | `array{non-falsy-string, 'a'}` | `array{non-falsy-string, non-falsy-string}` | key `1`: a value outside the type of its key | +| `/(?^i)(a)/u` | preg_match() | `'A'` | `["A","A"]` | `array{non-falsy-string, 'a'}` | `array{non-falsy-string, non-falsy-string}` | key `1`: a value outside the type of its key | +| `/(?^i)(a)/m` | preg_match() | `'A'` | `["A","A"]` | `array{non-falsy-string, 'a'}` | `array{non-falsy-string, non-falsy-string}` | key `1`: a value outside the type of its key | +| `/(?^i)(a)/s` | preg_match() | `'A'` | `["A","A"]` | `array{non-falsy-string, 'a'}` | `array{non-falsy-string, non-falsy-string}` | key `1`: a value outside the type of its key | +| `/(?^i)(a)/x` | preg_match() | `'A'` | `["A","A"]` | `array{non-falsy-string, 'a'}` | `array{non-falsy-string, non-falsy-string}` | key `1`: a value outside the type of its key | +| `/(?^i)(a)/D` | preg_match() | `'A'` | `["A","A"]` | `array{non-falsy-string, 'a'}` | `array{non-falsy-string, non-falsy-string}` | key `1`: a value outside the type of its key | +| `/(?^i)(a)/U` | preg_match() | `'A'` | `["A","A"]` | `array{non-falsy-string, 'a'}` | `array{non-falsy-string, non-falsy-string}` | key `1`: a value outside the type of its key | +| `/(?^i)(a)/mD` | preg_match() | `'A'` | `["A","A"]` | `array{non-falsy-string, 'a'}` | `array{non-falsy-string, non-falsy-string}` | key `1`: a value outside the type of its key | +| `/(?^i)(a)/su` | preg_match() | `'A'` | `["A","A"]` | `array{non-falsy-string, 'a'}` | `array{non-falsy-string, non-falsy-string}` | key `1`: a value outside the type of its key | +| `/(?^i)(a)/xu` | preg_match() | `'A'` | `["A","A"]` | `array{non-falsy-string, 'a'}` | `array{non-falsy-string, non-falsy-string}` | key `1`: a value outside the type of its key | +| `#(?^i)(a)#` | preg_match() | `'A'` | `["A","A"]` | `array{non-falsy-string, 'a'}` | `array{non-falsy-string, non-falsy-string}` | key `1`: a value outside the type of its key | +| `~(?^i)(a)~` | preg_match() | `'A'` | `["A","A"]` | `array{non-falsy-string, 'a'}` | `array{non-falsy-string, non-falsy-string}` | key `1`: a value outside the type of its key | +| `{(?^i)(a)}` | preg_match() | `'A'` | `["A","A"]` | `array{non-falsy-string, 'a'}` | `array{non-falsy-string, non-falsy-string}` | key `1`: a value outside the type of its key | +| `/(?J)(?\d)\|(?b)/u` | preg_match() | `'١'` | `{"0":"١","x":"١","1":"١"}` | `array{0: non-empty-string, x: 'b', 1: '', 2: 'b'}\|array{0: non-empty-string, x: numeric-string, 1: numeric-string}` | `array{0: non-empty-string, x: 'b', 1: '', 2: 'b'}\|array{0: non-empty-string, x: non-empty-string, 1: non-empty-string}` | key `x`: a value outside the type of its key | +| `/(?J)(?\d)\|(?b)/iu` | preg_match() | `'٤'` | `{"0":"٤","x":"٤","1":"٤"}` | `array{0: non-empty-string, x: non-empty-string, 1: '', 2: non-empty-string}\|array{0: non-empty-string, x: numeric-string, 1: numeric-string}` | `array{0: non-empty-string, x: non-empty-string, 1: non-empty-string}\|array{0: non-empty-string, x: non-falsy-string, 1: '', 2: non-falsy-string}` | key `x`: a value outside the type of its key | +| `/(?J)(?\d)\|(?b)/su` | preg_match() | `'٢'` | `{"0":"٢","x":"٢","1":"٢"}` | `array{0: non-empty-string, x: 'b', 1: '', 2: 'b'}\|array{0: non-empty-string, x: numeric-string, 1: numeric-string}` | `array{0: non-empty-string, x: 'b', 1: '', 2: 'b'}\|array{0: non-empty-string, x: non-empty-string, 1: non-empty-string}` | key `x`: a value outside the type of its key | +| `/(?J)(?\d)\|(?b)/xu` | preg_match() | `'٦'` | `{"0":"٦","x":"٦","1":"٦"}` | `array{0: non-empty-string, x: 'b', 1: '', 2: 'b'}\|array{0: non-empty-string, x: numeric-string, 1: numeric-string}` | `array{0: non-empty-string, x: 'b', 1: '', 2: 'b'}\|array{0: non-empty-string, x: non-empty-string, 1: non-empty-string}` | key `x`: a value outside the type of its key | +| `/(?n)(a)(?b)/` | preg_match() | `'ab'` | `{"0":"ab","x":"b","1":"b"}` | `array{0: non-falsy-string, 1: 'a', x: 'b', 2: 'b'}` | `array{0: 'ab', x: 'b', 1: 'b'}` | key `1`: a value outside the type of its key | +| `/(?n)(a)(?b)/i` | preg_match() | `'ab'` | `{"0":"ab","x":"b","1":"b"}` | `array{0: non-falsy-string, 1: non-empty-string, x: non-empty-string, 2: non-empty-string}` | `array{0: non-falsy-string, x: non-falsy-string, 1: non-falsy-string}` | key `2`: a key the engine leaves out is typed present | +| `/(?n)(a)(?b)/u` | preg_match() | `'ab'` | `{"0":"ab","x":"b","1":"b"}` | `array{0: non-falsy-string, 1: 'a', x: 'b', 2: 'b'}` | `array{0: 'ab', x: 'b', 1: 'b'}` | key `1`: a value outside the type of its key | +| `/(?n)(a)(?b)/iu` | preg_match() | `'ab'` | `{"0":"ab","x":"b","1":"b"}` | `array{0: non-falsy-string, 1: non-empty-string, x: non-empty-string, 2: non-empty-string}` | `array{0: non-falsy-string, x: non-falsy-string, 1: non-falsy-string}` | key `2`: a key the engine leaves out is typed present | +| `/(?n)(a)(?b)/m` | preg_match() | `'ab'` | `{"0":"ab","x":"b","1":"b"}` | `array{0: non-falsy-string, 1: 'a', x: 'b', 2: 'b'}` | `array{0: 'ab', x: 'b', 1: 'b'}` | key `1`: a value outside the type of its key | +| `/(?n)(a)(?b)/s` | preg_match() | `'ab'` | `{"0":"ab","x":"b","1":"b"}` | `array{0: non-falsy-string, 1: 'a', x: 'b', 2: 'b'}` | `array{0: 'ab', x: 'b', 1: 'b'}` | key `1`: a value outside the type of its key | +| `/(?n)(a)(?b)/x` | preg_match() | `'ab'` | `{"0":"ab","x":"b","1":"b"}` | `array{0: non-falsy-string, 1: 'a', x: 'b', 2: 'b'}` | `array{0: 'ab', x: 'b', 1: 'b'}` | key `1`: a value outside the type of its key | +| `/(?n)(a)(?b)/D` | preg_match() | `'ab'` | `{"0":"ab","x":"b","1":"b"}` | `array{0: non-falsy-string, 1: 'a', x: 'b', 2: 'b'}` | `array{0: 'ab', x: 'b', 1: 'b'}` | key `1`: a value outside the type of its key | +| `/(?n)(a)(?b)/U` | preg_match() | `'ab'` | `{"0":"ab","x":"b","1":"b"}` | `array{0: non-falsy-string, 1: 'a', x: 'b', 2: 'b'}` | `array{0: 'ab', x: 'b', 1: 'b'}` | key `1`: a value outside the type of its key | +| `/(?n)(a)(?b)/mD` | preg_match() | `'ab'` | `{"0":"ab","x":"b","1":"b"}` | `array{0: non-falsy-string, 1: 'a', x: 'b', 2: 'b'}` | `array{0: 'ab', x: 'b', 1: 'b'}` | key `1`: a value outside the type of its key | +| `/(?n)(a)(?b)/su` | preg_match() | `'ab'` | `{"0":"ab","x":"b","1":"b"}` | `array{0: non-falsy-string, 1: 'a', x: 'b', 2: 'b'}` | `array{0: 'ab', x: 'b', 1: 'b'}` | key `1`: a value outside the type of its key | +| `/(?n)(a)(?b)/xu` | preg_match() | `'ab'` | `{"0":"ab","x":"b","1":"b"}` | `array{0: non-falsy-string, 1: 'a', x: 'b', 2: 'b'}` | `array{0: 'ab', x: 'b', 1: 'b'}` | key `1`: a value outside the type of its key | +| `#(?n)(a)(?b)#` | preg_match() | `'ab'` | `{"0":"ab","x":"b","1":"b"}` | `array{0: non-falsy-string, 1: 'a', x: 'b', 2: 'b'}` | `array{0: 'ab', x: 'b', 1: 'b'}` | key `1`: a value outside the type of its key | +| `~(?n)(a)(?b)~` | preg_match() | `'ab'` | `{"0":"ab","x":"b","1":"b"}` | `array{0: non-falsy-string, 1: 'a', x: 'b', 2: 'b'}` | `array{0: 'ab', x: 'b', 1: 'b'}` | key `1`: a value outside the type of its key | +| `{(?n)(a)(?b)}` | preg_match() | `'ab'` | `{"0":"ab","x":"b","1":"b"}` | `array{0: non-falsy-string, 1: 'a', x: 'b', 2: 'b'}` | `array{0: 'ab', x: 'b', 1: 'b'}` | key `1`: a value outside the type of its key | +| `/(?n)(a)/` | preg_match() | `'a'` | `["a"]` | `array{non-falsy-string, 'a'}` | `array{'a'}` | key `1`: a key the engine leaves out is typed present | +| `/(?n)(a)/i` | preg_match() | `'a'` | `["a"]` | `array{non-falsy-string, non-empty-string}` | `array{non-falsy-string}` | key `1`: a key the engine leaves out is typed present | +| `/(?n)(a)/u` | preg_match() | `'a'` | `["a"]` | `array{non-falsy-string, 'a'}` | `array{'a'}` | key `1`: a key the engine leaves out is typed present | +| `/(?n)(a)/iu` | preg_match() | `'a'` | `["a"]` | `array{non-falsy-string, non-empty-string}` | `array{non-falsy-string}` | key `1`: a key the engine leaves out is typed present | +| `/(?n)(a)/m` | preg_match() | `'a'` | `["a"]` | `array{non-falsy-string, 'a'}` | `array{'a'}` | key `1`: a key the engine leaves out is typed present | +| `/(?n)(a)/s` | preg_match() | `'a'` | `["a"]` | `array{non-falsy-string, 'a'}` | `array{'a'}` | key `1`: a key the engine leaves out is typed present | +| `/(?n)(a)/x` | preg_match() | `'a'` | `["a"]` | `array{non-falsy-string, 'a'}` | `array{'a'}` | key `1`: a key the engine leaves out is typed present | +| `/(?n)(a)/D` | preg_match() | `'a'` | `["a"]` | `array{non-falsy-string, 'a'}` | `array{'a'}` | key `1`: a key the engine leaves out is typed present | +| `/(?n)(a)/U` | preg_match() | `'a'` | `["a"]` | `array{non-falsy-string, 'a'}` | `array{'a'}` | key `1`: a key the engine leaves out is typed present | +| `/(?n)(a)/mD` | preg_match() | `'a'` | `["a"]` | `array{non-falsy-string, 'a'}` | `array{'a'}` | key `1`: a key the engine leaves out is typed present | +| `/(?n)(a)/su` | preg_match() | `'a'` | `["a"]` | `array{non-falsy-string, 'a'}` | `array{'a'}` | key `1`: a key the engine leaves out is typed present | +| `/(?n)(a)/xu` | preg_match() | `'a'` | `["a"]` | `array{non-falsy-string, 'a'}` | `array{'a'}` | key `1`: a key the engine leaves out is typed present | +| `#(?n)(a)#` | preg_match() | `'a'` | `["a"]` | `array{non-falsy-string, 'a'}` | `array{'a'}` | key `1`: a key the engine leaves out is typed present | +| `~(?n)(a)~` | preg_match() | `'a'` | `["a"]` | `array{non-falsy-string, 'a'}` | `array{'a'}` | key `1`: a key the engine leaves out is typed present | +| `{(?n)(a)}` | preg_match() | `'a'` | `["a"]` | `array{non-falsy-string, 'a'}` | `array{'a'}` | key `1`: a key the engine leaves out is typed present | +| `/(?n:(a))(b)/` | preg_match() | `'ab'` | `["ab","b"]` | `array{non-falsy-string, 'a', 'b'}` | `array{'ab', 'b'}` | key `1`: a value outside the type of its key | +| `/(?n:(a))(b)/i` | preg_match() | `'ab'` | `["ab","b"]` | `array{non-falsy-string, non-empty-string, non-empty-string}` | `array{non-falsy-string, non-falsy-string}` | key `2`: a key the engine leaves out is typed present | +| `/(?n:(a))(b)/u` | preg_match() | `'ab'` | `["ab","b"]` | `array{non-falsy-string, 'a', 'b'}` | `array{'ab', 'b'}` | key `1`: a value outside the type of its key | +| `/(?n:(a))(b)/iu` | preg_match() | `'ab'` | `["ab","b"]` | `array{non-falsy-string, non-empty-string, non-empty-string}` | `array{non-falsy-string, non-falsy-string}` | key `2`: a key the engine leaves out is typed present | +| `/(?n:(a))(b)/m` | preg_match() | `'ab'` | `["ab","b"]` | `array{non-falsy-string, 'a', 'b'}` | `array{'ab', 'b'}` | key `1`: a value outside the type of its key | +| `/(?n:(a))(b)/s` | preg_match() | `'ab'` | `["ab","b"]` | `array{non-falsy-string, 'a', 'b'}` | `array{'ab', 'b'}` | key `1`: a value outside the type of its key | +| `/(?n:(a))(b)/x` | preg_match() | `'ab'` | `["ab","b"]` | `array{non-falsy-string, 'a', 'b'}` | `array{'ab', 'b'}` | key `1`: a value outside the type of its key | +| `/(?n:(a))(b)/D` | preg_match() | `'ab'` | `["ab","b"]` | `array{non-falsy-string, 'a', 'b'}` | `array{'ab', 'b'}` | key `1`: a value outside the type of its key | +| `/(?n:(a))(b)/U` | preg_match() | `'ab'` | `["ab","b"]` | `array{non-falsy-string, 'a', 'b'}` | `array{'ab', 'b'}` | key `1`: a value outside the type of its key | +| `/(?n:(a))(b)/mD` | preg_match() | `'ab'` | `["ab","b"]` | `array{non-falsy-string, 'a', 'b'}` | `array{'ab', 'b'}` | key `1`: a value outside the type of its key | +| `/(?n:(a))(b)/su` | preg_match() | `'ab'` | `["ab","b"]` | `array{non-falsy-string, 'a', 'b'}` | `array{'ab', 'b'}` | key `1`: a value outside the type of its key | +| `/(?n:(a))(b)/xu` | preg_match() | `'ab'` | `["ab","b"]` | `array{non-falsy-string, 'a', 'b'}` | `array{'ab', 'b'}` | key `1`: a value outside the type of its key | +| `#(?n:(a))(b)#` | preg_match() | `'ab'` | `["ab","b"]` | `array{non-falsy-string, 'a', 'b'}` | `array{'ab', 'b'}` | key `1`: a value outside the type of its key | +| `~(?n:(a))(b)~` | preg_match() | `'ab'` | `["ab","b"]` | `array{non-falsy-string, 'a', 'b'}` | `array{'ab', 'b'}` | key `1`: a value outside the type of its key | +| `{(?n:(a))(b)}` | preg_match() | `'ab'` | `["ab","b"]` | `array{non-falsy-string, 'a', 'b'}` | `array{'ab', 'b'}` | key `1`: a value outside the type of its key | +| `/(?x)( a )/` | preg_match() | `'a'` | `["a","a"]` | `array{non-falsy-string, ' a '}` | `array{'a', 'a'}` | key `1`: a value outside the type of its key | +| `/(?x)( a )/u` | preg_match() | `'a'` | `["a","a"]` | `array{non-falsy-string, ' a '}` | `array{'a', 'a'}` | key `1`: a value outside the type of its key | +| `/(?x)( a )/m` | preg_match() | `'a'` | `["a","a"]` | `array{non-falsy-string, ' a '}` | `array{'a', 'a'}` | key `1`: a value outside the type of its key | +| `/(?x)( a )/s` | preg_match() | `'a'` | `["a","a"]` | `array{non-falsy-string, ' a '}` | `array{'a', 'a'}` | key `1`: a value outside the type of its key | +| `/(?x)( a )/D` | preg_match() | `'a'` | `["a","a"]` | `array{non-falsy-string, ' a '}` | `array{'a', 'a'}` | key `1`: a value outside the type of its key | +| `/(?x)( a )/U` | preg_match() | `'a'` | `["a","a"]` | `array{non-falsy-string, ' a '}` | `array{'a', 'a'}` | key `1`: a value outside the type of its key | +| `/(?x)( a )/mD` | preg_match() | `'a'` | `["a","a"]` | `array{non-falsy-string, ' a '}` | `array{'a', 'a'}` | key `1`: a value outside the type of its key | +| `/(?x)( a )/su` | preg_match() | `'a'` | `["a","a"]` | `array{non-falsy-string, ' a '}` | `array{'a', 'a'}` | key `1`: a value outside the type of its key | +| `#(?x)( a )#` | preg_match() | `'a'` | `["a","a"]` | `array{non-falsy-string, ' a '}` | `array{'a', 'a'}` | key `1`: a value outside the type of its key | +| `~(?x)( a )~` | preg_match() | `'a'` | `["a","a"]` | `array{non-falsy-string, ' a '}` | `array{'a', 'a'}` | key `1`: a value outside the type of its key | +| `{(?x)( a )}` | preg_match() | `'a'` | `["a","a"]` | `array{non-falsy-string, ' a '}` | `array{'a', 'a'}` | key `1`: a value outside the type of its key | +| `/\Q(a)\E/` | preg_match() | `'(a)'` | `["(a)"]` | `array{non-falsy-string, 'a'}` | `array{'(a)'}` | key `1`: a key the engine leaves out is typed present | +| `/\Q(a)\E/i` | preg_match() | `'(a)'` | `["(a)"]` | `array{non-falsy-string, non-empty-string}` | `array{non-falsy-string}` | key `1`: a key the engine leaves out is typed present | +| `/\Q(a)\E/u` | preg_match() | `'(a)'` | `["(a)"]` | `array{non-falsy-string, 'a'}` | `array{'(a)'}` | key `1`: a key the engine leaves out is typed present | +| `/\Q(a)\E/iu` | preg_match() | `'(a)'` | `["(a)"]` | `array{non-falsy-string, non-empty-string}` | `array{non-falsy-string}` | key `1`: a key the engine leaves out is typed present | +| `/\Q(a)\E/m` | preg_match() | `'(a)'` | `["(a)"]` | `array{non-falsy-string, 'a'}` | `array{'(a)'}` | key `1`: a key the engine leaves out is typed present | +| `/\Q(a)\E/s` | preg_match() | `'(a)'` | `["(a)"]` | `array{non-falsy-string, 'a'}` | `array{'(a)'}` | key `1`: a key the engine leaves out is typed present | +| `/\Q(a)\E/x` | preg_match() | `'(a)'` | `["(a)"]` | `array{non-falsy-string, 'a'}` | `array{'(a)'}` | key `1`: a key the engine leaves out is typed present | +| `/\Q(a)\E/D` | preg_match() | `'(a)'` | `["(a)"]` | `array{non-falsy-string, 'a'}` | `array{'(a)'}` | key `1`: a key the engine leaves out is typed present | +| `/\Q(a)\E/U` | preg_match() | `'(a)'` | `["(a)"]` | `array{non-falsy-string, 'a'}` | `array{'(a)'}` | key `1`: a key the engine leaves out is typed present | +| `/\Q(a)\E/mD` | preg_match() | `'(a)'` | `["(a)"]` | `array{non-falsy-string, 'a'}` | `array{'(a)'}` | key `1`: a key the engine leaves out is typed present | +| `/\Q(a)\E/su` | preg_match() | `'(a)'` | `["(a)"]` | `array{non-falsy-string, 'a'}` | `array{'(a)'}` | key `1`: a key the engine leaves out is typed present | +| `/\Q(a)\E/xu` | preg_match() | `'(a)'` | `["(a)"]` | `array{non-falsy-string, 'a'}` | `array{'(a)'}` | key `1`: a key the engine leaves out is typed present | +| `#\Q(a)\E#` | preg_match() | `'(a)'` | `["(a)"]` | `array{non-falsy-string, 'a'}` | `array{'(a)'}` | key `1`: a key the engine leaves out is typed present | +| `~\Q(a)\E~` | preg_match() | `'(a)'` | `["(a)"]` | `array{non-falsy-string, 'a'}` | `array{'(a)'}` | key `1`: a key the engine leaves out is typed present | +| `{\Q(a)\E}` | preg_match() | `'(a)'` | `["(a)"]` | `array{non-falsy-string, 'a'}` | `array{'(a)'}` | key `1`: a key the engine leaves out is typed present | +| `/(\Q0\E)/` | preg_match() | `'0'` | `["0","0"]` | `array{non-falsy-string, 'Q0E'}` | `array{'0', '0'}` | key `0`: a value that can be '0' or '' is typed non-falsy or non-empty | +| `/(\Q0\E)/i` | preg_match() | `'0'` | `["0","0"]` | `array{non-falsy-string, non-falsy-string}` | `array{'0', '0'}` | key `0`: a value that can be '0' or '' is typed non-falsy or non-empty | +| `/(\Q0\E)/u` | preg_match() | `'0'` | `["0","0"]` | `array{non-falsy-string, 'Q0E'}` | `array{'0', '0'}` | key `0`: a value that can be '0' or '' is typed non-falsy or non-empty | +| `/(\Q0\E)/iu` | preg_match() | `'0'` | `["0","0"]` | `array{non-falsy-string, non-falsy-string}` | `array{'0', '0'}` | key `0`: a value that can be '0' or '' is typed non-falsy or non-empty | +| `/(\Q0\E)/m` | preg_match() | `'0'` | `["0","0"]` | `array{non-falsy-string, 'Q0E'}` | `array{'0', '0'}` | key `0`: a value that can be '0' or '' is typed non-falsy or non-empty | +| `/(\Q0\E)/s` | preg_match() | `'0'` | `["0","0"]` | `array{non-falsy-string, 'Q0E'}` | `array{'0', '0'}` | key `0`: a value that can be '0' or '' is typed non-falsy or non-empty | +| `/(\Q0\E)/x` | preg_match() | `'0'` | `["0","0"]` | `array{non-falsy-string, 'Q0E'}` | `array{'0', '0'}` | key `0`: a value that can be '0' or '' is typed non-falsy or non-empty | +| `/(\Q0\E)/D` | preg_match() | `'0'` | `["0","0"]` | `array{non-falsy-string, 'Q0E'}` | `array{'0', '0'}` | key `0`: a value that can be '0' or '' is typed non-falsy or non-empty | +| `/(\Q0\E)/n` | preg_match() | `'0'` | `["0"]` | `array{non-falsy-string}` | `array{'0'}` | key `0`: a value that can be '0' or '' is typed non-falsy or non-empty | +| `/(\Q0\E)/U` | preg_match() | `'0'` | `["0","0"]` | `array{non-falsy-string, 'Q0E'}` | `array{'0', '0'}` | key `0`: a value that can be '0' or '' is typed non-falsy or non-empty | +| `/(\Q0\E)/mD` | preg_match() | `'0'` | `["0","0"]` | `array{non-falsy-string, 'Q0E'}` | `array{'0', '0'}` | key `0`: a value that can be '0' or '' is typed non-falsy or non-empty | +| `/(\Q0\E)/su` | preg_match() | `'0'` | `["0","0"]` | `array{non-falsy-string, 'Q0E'}` | `array{'0', '0'}` | key `0`: a value that can be '0' or '' is typed non-falsy or non-empty | +| `/(\Q0\E)/xu` | preg_match() | `'0'` | `["0","0"]` | `array{non-falsy-string, 'Q0E'}` | `array{'0', '0'}` | key `0`: a value that can be '0' or '' is typed non-falsy or non-empty | +| `#(\Q0\E)#` | preg_match() | `'0'` | `["0","0"]` | `array{non-falsy-string, 'Q0E'}` | `array{'0', '0'}` | key `0`: a value that can be '0' or '' is typed non-falsy or non-empty | +| `~(\Q0\E)~` | preg_match() | `'0'` | `["0","0"]` | `array{non-falsy-string, 'Q0E'}` | `array{'0', '0'}` | key `0`: a value that can be '0' or '' is typed non-falsy or non-empty | +| `{(\Q0\E)}` | preg_match() | `'0'` | `["0","0"]` | `array{non-falsy-string, 'Q0E'}` | `array{'0', '0'}` | key `0`: a value that can be '0' or '' is typed non-falsy or non-empty | +| `/(\Q\E)/` | preg_match() | `''` | `["",""]` | `array{non-falsy-string, 'QE'}` | `array{'', ''}` | key `0`: a value that can be '0' or '' is typed non-falsy or non-empty | +| `/(\Q\E)/i` | preg_match() | `''` | `["",""]` | `array{non-falsy-string, non-falsy-string}` | `array{'', ''}` | key `0`: a value that can be '0' or '' is typed non-falsy or non-empty | +| `/(\Q\E)/u` | preg_match() | `''` | `["",""]` | `array{non-falsy-string, 'QE'}` | `array{'', ''}` | key `0`: a value that can be '0' or '' is typed non-falsy or non-empty | +| `/(\Q\E)/iu` | preg_match() | `''` | `["",""]` | `array{non-falsy-string, non-falsy-string}` | `array{'', ''}` | key `0`: a value that can be '0' or '' is typed non-falsy or non-empty | +| `/(\Q\E)/m` | preg_match() | `''` | `["",""]` | `array{non-falsy-string, 'QE'}` | `array{'', ''}` | key `0`: a value that can be '0' or '' is typed non-falsy or non-empty | +| `/(\Q\E)/s` | preg_match() | `''` | `["",""]` | `array{non-falsy-string, 'QE'}` | `array{'', ''}` | key `0`: a value that can be '0' or '' is typed non-falsy or non-empty | +| `/(\Q\E)/x` | preg_match() | `''` | `["",""]` | `array{non-falsy-string, 'QE'}` | `array{'', ''}` | key `0`: a value that can be '0' or '' is typed non-falsy or non-empty | +| `/(\Q\E)/D` | preg_match() | `''` | `["",""]` | `array{non-falsy-string, 'QE'}` | `array{'', ''}` | key `0`: a value that can be '0' or '' is typed non-falsy or non-empty | +| `/(\Q\E)/n` | preg_match() | `''` | `[""]` | `array{non-falsy-string}` | `array{''}` | key `0`: a value that can be '0' or '' is typed non-falsy or non-empty | +| `/(\Q\E)/U` | preg_match() | `''` | `["",""]` | `array{non-falsy-string, 'QE'}` | `array{'', ''}` | key `0`: a value that can be '0' or '' is typed non-falsy or non-empty | +| `/(\Q\E)/mD` | preg_match() | `''` | `["",""]` | `array{non-falsy-string, 'QE'}` | `array{'', ''}` | key `0`: a value that can be '0' or '' is typed non-falsy or non-empty | +| `/(\Q\E)/su` | preg_match() | `''` | `["",""]` | `array{non-falsy-string, 'QE'}` | `array{'', ''}` | key `0`: a value that can be '0' or '' is typed non-falsy or non-empty | +| `/(\Q\E)/xu` | preg_match() | `''` | `["",""]` | `array{non-falsy-string, 'QE'}` | `array{'', ''}` | key `0`: a value that can be '0' or '' is typed non-falsy or non-empty | +| `#(\Q\E)#` | preg_match() | `''` | `["",""]` | `array{non-falsy-string, 'QE'}` | `array{'', ''}` | key `0`: a value that can be '0' or '' is typed non-falsy or non-empty | +| `~(\Q\E)~` | preg_match() | `''` | `["",""]` | `array{non-falsy-string, 'QE'}` | `array{'', ''}` | key `0`: a value that can be '0' or '' is typed non-falsy or non-empty | +| `{(\Q\E)}` | preg_match() | `''` | `["",""]` | `array{non-falsy-string, 'QE'}` | `array{'', ''}` | key `0`: a value that can be '0' or '' is typed non-falsy or non-empty | +| `/(?=(\d))\w/` | preg_match() | `'0'` | `["0","0"]` | `array{non-falsy-string, numeric-string}` | `array{non-empty-string, numeric-string}` | key `0`: a value that can be '0' or '' is typed non-falsy or non-empty | +| `/(?=(\d))\w/i` | preg_match() | `'0'` | `["0","0"]` | `array{non-falsy-string, numeric-string}` | `array{non-empty-string, numeric-string}` | key `0`: a value that can be '0' or '' is typed non-falsy or non-empty | +| `/(?=(\d))\w/u` | preg_match() | `'0'` | `["0","0"]` | `array{non-falsy-string, numeric-string}` | `array{non-empty-string, non-empty-string}` | key `0`: a value that can be '0' or '' is typed non-falsy or non-empty | +| `/(?=(\d))\w/iu` | preg_match() | `'0'` | `["0","0"]` | `array{non-falsy-string, numeric-string}` | `array{non-empty-string, non-empty-string}` | key `0`: a value that can be '0' or '' is typed non-falsy or non-empty | +| `/(?=(\d))\w/m` | preg_match() | `'0'` | `["0","0"]` | `array{non-falsy-string, numeric-string}` | `array{non-empty-string, numeric-string}` | key `0`: a value that can be '0' or '' is typed non-falsy or non-empty | +| `/(?=(\d))\w/s` | preg_match() | `'0'` | `["0","0"]` | `array{non-falsy-string, numeric-string}` | `array{non-empty-string, numeric-string}` | key `0`: a value that can be '0' or '' is typed non-falsy or non-empty | +| `/(?=(\d))\w/x` | preg_match() | `'0'` | `["0","0"]` | `array{non-falsy-string, numeric-string}` | `array{non-empty-string, numeric-string}` | key `0`: a value that can be '0' or '' is typed non-falsy or non-empty | +| `/(?=(\d))\w/D` | preg_match() | `'0'` | `["0","0"]` | `array{non-falsy-string, numeric-string}` | `array{non-empty-string, numeric-string}` | key `0`: a value that can be '0' or '' is typed non-falsy or non-empty | +| `/(?=(\d))\w/n` | preg_match() | `'0'` | `["0"]` | `array{non-falsy-string}` | `array{non-empty-string}` | key `0`: a value that can be '0' or '' is typed non-falsy or non-empty | +| `/(?=(\d))\w/U` | preg_match() | `'0'` | `["0","0"]` | `array{non-falsy-string, numeric-string}` | `array{non-empty-string, numeric-string}` | key `0`: a value that can be '0' or '' is typed non-falsy or non-empty | +| `/(?=(\d))\w/mD` | preg_match() | `'0'` | `["0","0"]` | `array{non-falsy-string, numeric-string}` | `array{non-empty-string, numeric-string}` | key `0`: a value that can be '0' or '' is typed non-falsy or non-empty | +| `/(?=(\d))\w/su` | preg_match() | `'0'` | `["0","0"]` | `array{non-falsy-string, numeric-string}` | `array{non-empty-string, non-empty-string}` | key `0`: a value that can be '0' or '' is typed non-falsy or non-empty | +| `/(?=(\d))\w/xu` | preg_match() | `'0'` | `["0","0"]` | `array{non-falsy-string, numeric-string}` | `array{non-empty-string, non-empty-string}` | key `0`: a value that can be '0' or '' is typed non-falsy or non-empty | +| `#(?=(\d))\w#` | preg_match() | `'0'` | `["0","0"]` | `array{non-falsy-string, numeric-string}` | `array{non-empty-string, numeric-string}` | key `0`: a value that can be '0' or '' is typed non-falsy or non-empty | +| `~(?=(\d))\w~` | preg_match() | `'0'` | `["0","0"]` | `array{non-falsy-string, numeric-string}` | `array{non-empty-string, numeric-string}` | key `0`: a value that can be '0' or '' is typed non-falsy or non-empty | +| `{(?=(\d))\w}` | preg_match() | `'0'` | `["0","0"]` | `array{non-falsy-string, numeric-string}` | `array{non-empty-string, numeric-string}` | key `0`: a value that can be '0' or '' is typed non-falsy or non-empty | +| `/(.)$/` | preg_match() | `'0'` | `["0","0"]` | `array{non-falsy-string, non-empty-string}` | `array{non-empty-string, non-empty-string}` | key `0`: a value that can be '0' or '' is typed non-falsy or non-empty | +| `/(.)$/i` | preg_match() | `'0'` | `["0","0"]` | `array{non-falsy-string, non-empty-string}` | `array{non-empty-string, non-empty-string}` | key `0`: a value that can be '0' or '' is typed non-falsy or non-empty | +| `/(.)$/u` | preg_match() | `'0'` | `["0","0"]` | `array{non-falsy-string, non-empty-string}` | `array{non-empty-string, non-empty-string}` | key `0`: a value that can be '0' or '' is typed non-falsy or non-empty | +| `/(.)$/iu` | preg_match() | `'0'` | `["0","0"]` | `array{non-falsy-string, non-empty-string}` | `array{non-empty-string, non-empty-string}` | key `0`: a value that can be '0' or '' is typed non-falsy or non-empty | +| `/(.)$/m` | preg_match() | `'0'` | `["0","0"]` | `array{non-falsy-string, non-empty-string}` | `array{non-empty-string, non-empty-string}` | key `0`: a value that can be '0' or '' is typed non-falsy or non-empty | +| `/(.)$/s` | preg_match() | `'1٣0٣/0'` | `["0","0"]` | `array{non-falsy-string, non-empty-string}` | `array{non-empty-string, non-empty-string}` | key `0`: a value that can be '0' or '' is typed non-falsy or non-empty | +| `/(.)$/x` | preg_match() | `'0'` | `["0","0"]` | `array{non-falsy-string, non-empty-string}` | `array{non-empty-string, non-empty-string}` | key `0`: a value that can be '0' or '' is typed non-falsy or non-empty | +| `/(.)$/D` | preg_match() | `'0'` | `["0","0"]` | `array{non-falsy-string, non-empty-string}` | `array{non-empty-string, non-empty-string}` | key `0`: a value that can be '0' or '' is typed non-falsy or non-empty | +| `/(.)$/n` | preg_match() | `'0'` | `["0"]` | `array{non-falsy-string}` | `array{non-empty-string}` | key `0`: a value that can be '0' or '' is typed non-falsy or non-empty | +| `/(.)$/U` | preg_match() | `'0'` | `["0","0"]` | `array{non-falsy-string, non-empty-string}` | `array{non-empty-string, non-empty-string}` | key `0`: a value that can be '0' or '' is typed non-falsy or non-empty | +| `/(.)$/mD` | preg_match() | `'0'` | `["0","0"]` | `array{non-falsy-string, non-empty-string}` | `array{non-empty-string, non-empty-string}` | key `0`: a value that can be '0' or '' is typed non-falsy or non-empty | +| `/(.)$/su` | preg_match() | `'0'` | `["0","0"]` | `array{non-falsy-string, non-empty-string}` | `array{non-empty-string, non-empty-string}` | key `0`: a value that can be '0' or '' is typed non-falsy or non-empty | +| `/(.)$/xu` | preg_match() | `'0'` | `["0","0"]` | `array{non-falsy-string, non-empty-string}` | `array{non-empty-string, non-empty-string}` | key `0`: a value that can be '0' or '' is typed non-falsy or non-empty | +| `#(.)$#` | preg_match() | `'0'` | `["0","0"]` | `array{non-falsy-string, non-empty-string}` | `array{non-empty-string, non-empty-string}` | key `0`: a value that can be '0' or '' is typed non-falsy or non-empty | +| `~(.)$~` | preg_match() | `'0'` | `["0","0"]` | `array{non-falsy-string, non-empty-string}` | `array{non-empty-string, non-empty-string}` | key `0`: a value that can be '0' or '' is typed non-falsy or non-empty | +| `{(.)$}` | preg_match() | `'0'` | `["0","0"]` | `array{non-falsy-string, non-empty-string}` | `array{non-empty-string, non-empty-string}` | key `0`: a value that can be '0' or '' is typed non-falsy or non-empty | +| `/^(\d+)\z/u` | preg_match() | `'٨'` | `["٨","٨"]` | `array{non-empty-string, numeric-string}` | `array{non-empty-string, non-empty-string}` | key `1`: a value outside the type of its key | +| `/^(\d+)\z/iu` | preg_match() | `'٤'` | `["٤","٤"]` | `array{non-empty-string, numeric-string}` | `array{non-empty-string, non-empty-string}` | key `1`: a value outside the type of its key | +| `/^(\d+)\z/su` | preg_match() | `'٧٣'` | `["٧٣","٧٣"]` | `array{non-empty-string, numeric-string}` | `array{non-empty-string, non-empty-string}` | key `1`: a value outside the type of its key | +| `/^(\d+)\z/xu` | preg_match() | `'٧٨٧'` | `["٧٨٧","٧٨٧"]` | `array{non-empty-string, numeric-string}` | `array{non-empty-string, non-empty-string}` | key `1`: a value outside the type of its key | +| `/^(.)/` | preg_match() | `'0'` | `["0","0"]` | `array{non-falsy-string, non-empty-string}` | `array{non-empty-string, non-empty-string}` | key `0`: a value that can be '0' or '' is typed non-falsy or non-empty | +| `/^(.)/i` | preg_match() | `'0'` | `["0","0"]` | `array{non-falsy-string, non-empty-string}` | `array{non-empty-string, non-empty-string}` | key `0`: a value that can be '0' or '' is typed non-falsy or non-empty | +| `/^(.)/u` | preg_match() | `'0'` | `["0","0"]` | `array{non-falsy-string, non-empty-string}` | `array{non-empty-string, non-empty-string}` | key `0`: a value that can be '0' or '' is typed non-falsy or non-empty | +| `/^(.)/iu` | preg_match() | `'0'` | `["0","0"]` | `array{non-falsy-string, non-empty-string}` | `array{non-empty-string, non-empty-string}` | key `0`: a value that can be '0' or '' is typed non-falsy or non-empty | +| `/^(.)/m` | preg_match() | `'0'` | `["0","0"]` | `array{non-falsy-string, non-empty-string}` | `array{non-empty-string, non-empty-string}` | key `0`: a value that can be '0' or '' is typed non-falsy or non-empty | +| `/^(.)/s` | preg_match() | `'0'` | `["0","0"]` | `array{non-falsy-string, non-empty-string}` | `array{non-empty-string, non-empty-string}` | key `0`: a value that can be '0' or '' is typed non-falsy or non-empty | +| `/^(.)/x` | preg_match() | `'0'` | `["0","0"]` | `array{non-falsy-string, non-empty-string}` | `array{non-empty-string, non-empty-string}` | key `0`: a value that can be '0' or '' is typed non-falsy or non-empty | +| `/^(.)/D` | preg_match() | `'0'` | `["0","0"]` | `array{non-falsy-string, non-empty-string}` | `array{non-empty-string, non-empty-string}` | key `0`: a value that can be '0' or '' is typed non-falsy or non-empty | +| `/^(.)/n` | preg_match() | `'0'` | `["0"]` | `array{non-falsy-string}` | `array{non-empty-string}` | key `0`: a value that can be '0' or '' is typed non-falsy or non-empty | +| `/^(.)/U` | preg_match() | `'0'` | `["0","0"]` | `array{non-falsy-string, non-empty-string}` | `array{non-empty-string, non-empty-string}` | key `0`: a value that can be '0' or '' is typed non-falsy or non-empty | +| `/^(.)/mD` | preg_match() | `'0'` | `["0","0"]` | `array{non-falsy-string, non-empty-string}` | `array{non-empty-string, non-empty-string}` | key `0`: a value that can be '0' or '' is typed non-falsy or non-empty | +| `/^(.)/su` | preg_match() | `'0'` | `["0","0"]` | `array{non-falsy-string, non-empty-string}` | `array{non-empty-string, non-empty-string}` | key `0`: a value that can be '0' or '' is typed non-falsy or non-empty | +| `/^(.)/xu` | preg_match() | `'0'` | `["0","0"]` | `array{non-falsy-string, non-empty-string}` | `array{non-empty-string, non-empty-string}` | key `0`: a value that can be '0' or '' is typed non-falsy or non-empty | +| `#^(.)#` | preg_match() | `'0'` | `["0","0"]` | `array{non-falsy-string, non-empty-string}` | `array{non-empty-string, non-empty-string}` | key `0`: a value that can be '0' or '' is typed non-falsy or non-empty | +| `~^(.)~` | preg_match() | `'0'` | `["0","0"]` | `array{non-falsy-string, non-empty-string}` | `array{non-empty-string, non-empty-string}` | key `0`: a value that can be '0' or '' is typed non-falsy or non-empty | +| `{^(.)}` | preg_match() | `'0'` | `["0","0"]` | `array{non-falsy-string, non-empty-string}` | `array{non-empty-string, non-empty-string}` | key `0`: a value that can be '0' or '' is typed non-falsy or non-empty | +| `/(\d)$/u` | preg_match() | `'٦'` | `["٦","٦"]` | `array{non-empty-string, numeric-string}` | `array{non-empty-string, non-empty-string}` | key `1`: a value outside the type of its key | +| `/(\d)$/iu` | preg_match() | `'٨'` | `["٨","٨"]` | `array{non-empty-string, numeric-string}` | `array{non-empty-string, non-empty-string}` | key `1`: a value outside the type of its key | +| `/(\d)$/su` | preg_match() | `'٤'` | `["٤","٤"]` | `array{non-empty-string, numeric-string}` | `array{non-empty-string, non-empty-string}` | key `1`: a value outside the type of its key | +| `/(\d)$/xu` | preg_match() | `'٤'` | `["٤","٤"]` | `array{non-empty-string, numeric-string}` | `array{non-empty-string, non-empty-string}` | key `1`: a value outside the type of its key | +| `/(?s)(.)/` | preg_match() | `'0'` | `["0","0"]` | `array{non-falsy-string, non-empty-string}` | `array{non-empty-string, non-empty-string}` | key `0`: a value that can be '0' or '' is typed non-falsy or non-empty | +| `/(?s)(.)/i` | preg_match() | `'0'` | `["0","0"]` | `array{non-falsy-string, non-empty-string}` | `array{non-empty-string, non-empty-string}` | key `0`: a value that can be '0' or '' is typed non-falsy or non-empty | +| `/(?s)(.)/u` | preg_match() | `'0'` | `["0","0"]` | `array{non-falsy-string, non-empty-string}` | `array{non-empty-string, non-empty-string}` | key `0`: a value that can be '0' or '' is typed non-falsy or non-empty | +| `/(?s)(.)/iu` | preg_match() | `'0'` | `["0","0"]` | `array{non-falsy-string, non-empty-string}` | `array{non-empty-string, non-empty-string}` | key `0`: a value that can be '0' or '' is typed non-falsy or non-empty | +| `/(?s)(.)/m` | preg_match() | `'0'` | `["0","0"]` | `array{non-falsy-string, non-empty-string}` | `array{non-empty-string, non-empty-string}` | key `0`: a value that can be '0' or '' is typed non-falsy or non-empty | +| `/(?s)(.)/s` | preg_match() | `'0'` | `["0","0"]` | `array{non-falsy-string, non-empty-string}` | `array{non-empty-string, non-empty-string}` | key `0`: a value that can be '0' or '' is typed non-falsy or non-empty | +| `/(?s)(.)/x` | preg_match() | `'0'` | `["0","0"]` | `array{non-falsy-string, non-empty-string}` | `array{non-empty-string, non-empty-string}` | key `0`: a value that can be '0' or '' is typed non-falsy or non-empty | +| `/(?s)(.)/D` | preg_match() | `'0'` | `["0","0"]` | `array{non-falsy-string, non-empty-string}` | `array{non-empty-string, non-empty-string}` | key `0`: a value that can be '0' or '' is typed non-falsy or non-empty | +| `/(?s)(.)/n` | preg_match() | `'0'` | `["0"]` | `array{non-falsy-string}` | `array{non-empty-string}` | key `0`: a value that can be '0' or '' is typed non-falsy or non-empty | +| `/(?s)(.)/U` | preg_match() | `'0'` | `["0","0"]` | `array{non-falsy-string, non-empty-string}` | `array{non-empty-string, non-empty-string}` | key `0`: a value that can be '0' or '' is typed non-falsy or non-empty | +| `/(?s)(.)/mD` | preg_match() | `'0'` | `["0","0"]` | `array{non-falsy-string, non-empty-string}` | `array{non-empty-string, non-empty-string}` | key `0`: a value that can be '0' or '' is typed non-falsy or non-empty | +| `/(?s)(.)/su` | preg_match() | `'0'` | `["0","0"]` | `array{non-falsy-string, non-empty-string}` | `array{non-empty-string, non-empty-string}` | key `0`: a value that can be '0' or '' is typed non-falsy or non-empty | +| `/(?s)(.)/xu` | preg_match() | `'0'` | `["0","0"]` | `array{non-falsy-string, non-empty-string}` | `array{non-empty-string, non-empty-string}` | key `0`: a value that can be '0' or '' is typed non-falsy or non-empty | +| `#(?s)(.)#` | preg_match() | `'0'` | `["0","0"]` | `array{non-falsy-string, non-empty-string}` | `array{non-empty-string, non-empty-string}` | key `0`: a value that can be '0' or '' is typed non-falsy or non-empty | +| `~(?s)(.)~` | preg_match() | `'0'` | `["0","0"]` | `array{non-falsy-string, non-empty-string}` | `array{non-empty-string, non-empty-string}` | key `0`: a value that can be '0' or '' is typed non-falsy or non-empty | +| `{(?s)(.)}` | preg_match() | `'0'` | `["0","0"]` | `array{non-falsy-string, non-empty-string}` | `array{non-empty-string, non-empty-string}` | key `0`: a value that can be '0' or '' is typed non-falsy or non-empty | +| `/(?x)(\d +)/` | preg_match() | `'0'` | `["0","0"]` | `array{non-falsy-string, non-falsy-string}` | `array{numeric-string, numeric-string}` | key `0`: a value that can be '0' or '' is typed non-falsy or non-empty | +| `/(?x)(\d +)/i` | preg_match() | `'0'` | `["0","0"]` | `array{non-falsy-string, non-falsy-string}` | `array{numeric-string, numeric-string}` | key `0`: a value that can be '0' or '' is typed non-falsy or non-empty | +| `/(?x)(\d +)/u` | preg_match() | `'0'` | `["0","0"]` | `array{non-falsy-string, non-falsy-string}` | `array{non-empty-string, non-empty-string}` | key `0`: a value that can be '0' or '' is typed non-falsy or non-empty | +| `/(?x)(\d +)/iu` | preg_match() | `'0'` | `["0","0"]` | `array{non-falsy-string, non-falsy-string}` | `array{non-empty-string, non-empty-string}` | key `0`: a value that can be '0' or '' is typed non-falsy or non-empty | +| `/(?x)(\d +)/m` | preg_match() | `'0'` | `["0","0"]` | `array{non-falsy-string, non-falsy-string}` | `array{numeric-string, numeric-string}` | key `0`: a value that can be '0' or '' is typed non-falsy or non-empty | +| `/(?x)(\d +)/s` | preg_match() | `'0'` | `["0","0"]` | `array{non-falsy-string, non-falsy-string}` | `array{numeric-string, numeric-string}` | key `0`: a value that can be '0' or '' is typed non-falsy or non-empty | +| `/(?x)(\d +)/D` | preg_match() | `'0'` | `["0","0"]` | `array{non-falsy-string, non-falsy-string}` | `array{numeric-string, numeric-string}` | key `0`: a value that can be '0' or '' is typed non-falsy or non-empty | +| `/(?x)(\d +)/n` | preg_match() | `'0'` | `["0"]` | `array{non-falsy-string}` | `array{numeric-string}` | key `0`: a value that can be '0' or '' is typed non-falsy or non-empty | +| `/(?x)(\d +)/U` | preg_match() | `'01'` | `["0","0"]` | `array{non-falsy-string, non-falsy-string}` | `array{numeric-string, numeric-string}` | key `0`: a value that can be '0' or '' is typed non-falsy or non-empty | +| `/(?x)(\d +)/mD` | preg_match() | `'0'` | `["0","0"]` | `array{non-falsy-string, non-falsy-string}` | `array{numeric-string, numeric-string}` | key `0`: a value that can be '0' or '' is typed non-falsy or non-empty | +| `/(?x)(\d +)/su` | preg_match() | `'0'` | `["0","0"]` | `array{non-falsy-string, non-falsy-string}` | `array{non-empty-string, non-empty-string}` | key `0`: a value that can be '0' or '' is typed non-falsy or non-empty | +| `/(?x)(\d +)/xu` | preg_match() | `'٩٨'` | `["٩٨","٩٨"]` | `array{non-empty-string, numeric-string}` | `array{non-empty-string, non-empty-string}` | key `1`: a value outside the type of its key | +| `#(?x)(\d +)#` | preg_match() | `'0'` | `["0","0"]` | `array{non-falsy-string, non-falsy-string}` | `array{numeric-string, numeric-string}` | key `0`: a value that can be '0' or '' is typed non-falsy or non-empty | +| `~(?x)(\d +)~` | preg_match() | `'0'` | `["0","0"]` | `array{non-falsy-string, non-falsy-string}` | `array{numeric-string, numeric-string}` | key `0`: a value that can be '0' or '' is typed non-falsy or non-empty | +| `{(?x)(\d +)}` | preg_match() | `'b0٣1K'` | `["0","0"]` | `array{non-falsy-string, non-falsy-string}` | `array{numeric-string, numeric-string}` | key `0`: a value that can be '0' or '' is typed non-falsy or non-empty | +| `/(?i)(k)/` | preg_match() | `'K'` | `["K","K"]` | `array{non-falsy-string, 'k'}` | `array{non-falsy-string, non-falsy-string}` | key `1`: a value outside the type of its key | +| `/(?i)(k)/u` | preg_match() | `'K'` | `["K","K"]` | `array{non-falsy-string, 'k'}` | `array{non-falsy-string, non-falsy-string}` | key `1`: a value outside the type of its key | +| `/(?i)(k)/m` | preg_match() | `'K'` | `["K","K"]` | `array{non-falsy-string, 'k'}` | `array{non-falsy-string, non-falsy-string}` | key `1`: a value outside the type of its key | +| `/(?i)(k)/s` | preg_match() | `'K'` | `["K","K"]` | `array{non-falsy-string, 'k'}` | `array{non-falsy-string, non-falsy-string}` | key `1`: a value outside the type of its key | +| `/(?i)(k)/x` | preg_match() | `'K'` | `["K","K"]` | `array{non-falsy-string, 'k'}` | `array{non-falsy-string, non-falsy-string}` | key `1`: a value outside the type of its key | +| `/(?i)(k)/D` | preg_match() | `'K'` | `["K","K"]` | `array{non-falsy-string, 'k'}` | `array{non-falsy-string, non-falsy-string}` | key `1`: a value outside the type of its key | +| `/(?i)(k)/U` | preg_match() | `'K'` | `["K","K"]` | `array{non-falsy-string, 'k'}` | `array{non-falsy-string, non-falsy-string}` | key `1`: a value outside the type of its key | +| `/(?i)(k)/mD` | preg_match() | `'K'` | `["K","K"]` | `array{non-falsy-string, 'k'}` | `array{non-falsy-string, non-falsy-string}` | key `1`: a value outside the type of its key | +| `/(?i)(k)/su` | preg_match() | `'K'` | `["K","K"]` | `array{non-falsy-string, 'k'}` | `array{non-falsy-string, non-falsy-string}` | key `1`: a value outside the type of its key | +| `/(?i)(k)/xu` | preg_match() | `'K'` | `["K","K"]` | `array{non-falsy-string, 'k'}` | `array{non-falsy-string, non-falsy-string}` | key `1`: a value outside the type of its key | +| `#(?i)(k)#` | preg_match() | `'K'` | `["K","K"]` | `array{non-falsy-string, 'k'}` | `array{non-falsy-string, non-falsy-string}` | key `1`: a value outside the type of its key | +| `~(?i)(k)~` | preg_match() | `'K'` | `["K","K"]` | `array{non-falsy-string, 'k'}` | `array{non-falsy-string, non-falsy-string}` | key `1`: a value outside the type of its key | +| `{(?i)(k)}` | preg_match() | `'K'` | `["K","K"]` | `array{non-falsy-string, 'k'}` | `array{non-falsy-string, non-falsy-string}` | key `1`: a value outside the type of its key | + +## Patterns the parser generated by phplrt infers no shape for + +1265 patterns, `preg_match()` without flags. + +| pattern | PHPRegex | +|---|---| +| `/(\d)/J` | `array{numeric-string, numeric-string}` | +| `/(\d+)/J` | `array{numeric-string, numeric-string}` | +| `/(\d*)/J` | `array{''\|numeric-string, ''\|numeric-string}` | +| `/^(\d+)$/J` | `array{numeric-string, numeric-string}` | +| `/(\d{2})/J` | `array{non-falsy-string&numeric-string, non-falsy-string&numeric-string}` | +| `/([0-9]{2})/J` | `array{non-falsy-string&numeric-string, non-falsy-string&numeric-string}` | +| `/([[:digit:]]+)/J` | `array{numeric-string, numeric-string}` | +| `/(\p{Nd}+)/J` | `array{non-empty-string, non-empty-string}` | +| `/(\p{N}+)/J` | `array{non-empty-string, non-empty-string}` | +| `/([\d]+)/J` | `array{numeric-string, numeric-string}` | +| `/([^\D]+)/J` | `array{non-empty-string, non-empty-string}` | +| `/(\d)\s/J` | `array{non-falsy-string, numeric-string}` | +| `/(-?\d+)/J` | `array{non-empty-string, non-empty-string}` | +| `/(\d+\.\d+)/J` | `array{non-falsy-string, non-falsy-string}` | +| `/(\d+(?:\.\d+)?)/J` | `array{non-empty-string, non-empty-string}` | +| `/(0)/J` | `array{'0', '0'}` | +| `/(0+)/J` | `array{numeric-string, numeric-string}` | +| `/(00)/J` | `array{'00', '00'}` | +| `/(0\|1)/J` | `array{'0'\|'1', '0'\|'1'}` | +| `/([01])/J` | `array{'0'\|'1', '0'\|'1'}` | +| `/(\d)(\d)/J` | `array{non-falsy-string&numeric-string, numeric-string, numeric-string}` | +| `/(\d+)\|(x)/J` | `array{'x'\|numeric-string, '', 'x'}\|array{'x'\|numeric-string, numeric-string}` | +| `/(?\d+)/J` | `array{0: numeric-string, n: numeric-string, 1: numeric-string}` | +| `/(\w+)/J` | `array{non-empty-string, non-empty-string}` | +| `/(\w)/J` | `array{non-empty-string, non-empty-string}` | +| `/([[:alnum:]]+)/J` | `array{non-empty-string, non-empty-string}` | +| `/([[:alpha:]])/J` | `array{non-falsy-string, non-falsy-string}` | +| `/(\x30)/J` | `array{'0', '0'}` | +| `/(\x{30})/J` | `array{'0', '0'}` | +| `/(\060)/J` | `array{'0', '0'}` | +| `/(?i)(\d+)/J` | `array{numeric-string, numeric-string}` | +| `/(\d+)(?i)/J` | `array{numeric-string, numeric-string}` | +| `/(\d\|a)/J` | `array{'a'\|numeric-string, 'a'\|numeric-string}` | +| `/(\d\|٣)/J` | `array{'٣'\|numeric-string, '٣'\|numeric-string}` | +| `/(\h)/J` | `array{non-falsy-string, non-falsy-string}` | +| `/(\s)/J` | `array{non-falsy-string, non-falsy-string}` | +| `/(\S)/J` | `array{non-empty-string, non-empty-string}` | +| `/(\W)/J` | `array{non-falsy-string, non-falsy-string}` | +| `/(\D)/J` | `array{non-falsy-string, non-falsy-string}` | +| `/(\R)/J` | `array{non-falsy-string, non-falsy-string}` | +| `/(\X)/J` | `array{non-empty-string, non-empty-string}` | +| `/(\C)/J` | `array{non-empty-string, non-empty-string}` | +| `/(.)/J` | `array{non-empty-string, non-empty-string}` | +| `/(.+)/J` | `array{non-empty-string, non-empty-string}` | +| `/(..)/J` | `array{non-falsy-string, non-falsy-string}` | +| `/(\b\d+\b)/J` | `array{numeric-string, numeric-string}` | +| `/([^a])/J` | `array{non-empty-string, non-empty-string}` | +| `/([^\d])/J` | `array{non-falsy-string, non-falsy-string}` | +| `/([^0-9]+)/J` | `array{non-empty-string, non-empty-string}` | +| `/([\x00-\x7f])/J` | `array{non-empty-string, non-empty-string}` | +| `/([٠-٩])/J` | `array{non-empty-string, non-empty-string}` | +| `/(ab\|cd)/J` | `array{'ab'\|'cd', 'ab'\|'cd'}` | +| `/(a)/J` | `array{'a', 'a'}` | +| `/(é)/J` | `array{'é', 'é'}` | +| `/(k)/J` | `array{'k', 'k'}` | +| `/(K)/J` | `array{'K', 'K'}` | +| `/(s)/J` | `array{'s', 's'}` | +| `/(ss)/J` | `array{'ss', 'ss'}` | +| `/(ß)/J` | `array{'ß', 'ß'}` | +| `/(i)/J` | `array{'i', 'i'}` | +| `/(I)/J` | `array{'I', 'I'}` | +| `/(ı)/J` | `array{'ı', 'ı'}` | +| `/(st)/J` | `array{'st', 'st'}` | +| `/(st)/J` | `array{'st', 'st'}` | +| `/(Σ)/J` | `array{'Σ', 'Σ'}` | +| `/(σ)/J` | `array{'σ', 'σ'}` | +| `/(ς)/J` | `array{'ς', 'ς'}` | +| `/(a\|b)c/J` | `array{'ac'\|'bc', 'a'\|'b'}` | +| `/([a-c])/J` | `array{'a'\|'b'\|'c', 'a'\|'b'\|'c'}` | +| `/([k])/J` | `array{'k', 'k'}` | +| `/([s])/J` | `array{'s', 's'}` | +| `/(abc)/J` | `array{'abc', 'abc'}` | +| `/(?i:a)(b)/J` | `array{non-falsy-string, 'b'}` | +| `/(?i)(a)/J` | `array{non-falsy-string, non-falsy-string}` | +| `/(?-i)(a)/J` | `array{'a', 'a'}` | +| `/(?i:(a))/J` | `array{non-falsy-string, non-falsy-string}` | +| `/(?^)(a)/J` | `array{'a', 'a'}` | +| `/(?^i)(a)/J` | `array{non-falsy-string, non-falsy-string}` | +| `/((?i)a)b/J` | `array{non-falsy-string, non-falsy-string}` | +| `/(?:(?i)a\|b)(c)/J` | `array{non-falsy-string, 'c'}` | +| `/a\K(b)/J` | `array{string, 'b'}` | +| `/(a)\Kb/J` | `array{string, 'a'}` | +| `/(?:a\K)?(b)/J` | `array{string, 'b'}` | +| `/(a)(*ACCEPT)(b)/` | `array{0: string, 1: string, 2?: string}` | +| `/(a)(*ACCEPT)(b)/i` | `array{0: string, 1: string, 2?: string}` | +| `/(a)(*ACCEPT)(b)/u` | `array{0: string, 1: string, 2?: string}` | +| `/(a)(*ACCEPT)(b)/iu` | `array{0: string, 1: string, 2?: string}` | +| `/(a)(*ACCEPT)(b)/m` | `array{0: string, 1: string, 2?: string}` | +| `/(a)(*ACCEPT)(b)/s` | `array{0: string, 1: string, 2?: string}` | +| `/(a)(*ACCEPT)(b)/x` | `array{0: string, 1: string, 2?: string}` | +| `/(a)(*ACCEPT)(b)/D` | `array{0: string, 1: string, 2?: string}` | +| `/(a)(*ACCEPT)(b)/n` | `array{string}` | +| `/(a)(*ACCEPT)(b)/U` | `array{0: string, 1: string, 2?: string}` | +| `/(a)(*ACCEPT)(b)/mD` | `array{0: string, 1: string, 2?: string}` | +| `/(a)(*ACCEPT)(b)/su` | `array{0: string, 1: string, 2?: string}` | +| `/(a)(*ACCEPT)(b)/xu` | `array{0: string, 1: string, 2?: string}` | +| `/(a)(*ACCEPT)(b)/J` | `array{0: string, 1: string, 2?: string}` | +| `#(a)(*ACCEPT)(b)#` | `array{0: string, 1: string, 2?: string}` | +| `~(a)(*ACCEPT)(b)~` | `array{0: string, 1: string, 2?: string}` | +| `{(a)(*ACCEPT)(b)}` | `array{0: string, 1: string, 2?: string}` | +| `/(a)(?:(*ACCEPT))?(b)/` | `array{0: string, 1: string, 2?: string}` | +| `/(a)(?:(*ACCEPT))?(b)/i` | `array{0: string, 1: string, 2?: string}` | +| `/(a)(?:(*ACCEPT))?(b)/u` | `array{0: string, 1: string, 2?: string}` | +| `/(a)(?:(*ACCEPT))?(b)/iu` | `array{0: string, 1: string, 2?: string}` | +| `/(a)(?:(*ACCEPT))?(b)/m` | `array{0: string, 1: string, 2?: string}` | +| `/(a)(?:(*ACCEPT))?(b)/s` | `array{0: string, 1: string, 2?: string}` | +| `/(a)(?:(*ACCEPT))?(b)/x` | `array{0: string, 1: string, 2?: string}` | +| `/(a)(?:(*ACCEPT))?(b)/D` | `array{0: string, 1: string, 2?: string}` | +| `/(a)(?:(*ACCEPT))?(b)/n` | `array{string}` | +| `/(a)(?:(*ACCEPT))?(b)/U` | `array{0: string, 1: string, 2?: string}` | +| `/(a)(?:(*ACCEPT))?(b)/mD` | `array{0: string, 1: string, 2?: string}` | +| `/(a)(?:(*ACCEPT))?(b)/su` | `array{0: string, 1: string, 2?: string}` | +| `/(a)(?:(*ACCEPT))?(b)/xu` | `array{0: string, 1: string, 2?: string}` | +| `/(a)(?:(*ACCEPT))?(b)/J` | `array{0: string, 1: string, 2?: string}` | +| `#(a)(?:(*ACCEPT))?(b)#` | `array{0: string, 1: string, 2?: string}` | +| `~(a)(?:(*ACCEPT))?(b)~` | `array{0: string, 1: string, 2?: string}` | +| `{(a)(?:(*ACCEPT))?(b)}` | `array{0: string, 1: string, 2?: string}` | +| `/(?:a(*ACCEPT))?(b)(c)/` | `list{0: string, 1?: string, 2?: string}` | +| `/(?:a(*ACCEPT))?(b)(c)/i` | `list{0: string, 1?: string, 2?: string}` | +| `/(?:a(*ACCEPT))?(b)(c)/u` | `list{0: string, 1?: string, 2?: string}` | +| `/(?:a(*ACCEPT))?(b)(c)/iu` | `list{0: string, 1?: string, 2?: string}` | +| `/(?:a(*ACCEPT))?(b)(c)/m` | `list{0: string, 1?: string, 2?: string}` | +| `/(?:a(*ACCEPT))?(b)(c)/s` | `list{0: string, 1?: string, 2?: string}` | +| `/(?:a(*ACCEPT))?(b)(c)/x` | `list{0: string, 1?: string, 2?: string}` | +| `/(?:a(*ACCEPT))?(b)(c)/D` | `list{0: string, 1?: string, 2?: string}` | +| `/(?:a(*ACCEPT))?(b)(c)/n` | `array{string}` | +| `/(?:a(*ACCEPT))?(b)(c)/U` | `list{0: string, 1?: string, 2?: string}` | +| `/(?:a(*ACCEPT))?(b)(c)/mD` | `list{0: string, 1?: string, 2?: string}` | +| `/(?:a(*ACCEPT))?(b)(c)/su` | `list{0: string, 1?: string, 2?: string}` | +| `/(?:a(*ACCEPT))?(b)(c)/xu` | `list{0: string, 1?: string, 2?: string}` | +| `/(?:a(*ACCEPT))?(b)(c)/J` | `list{0: string, 1?: string, 2?: string}` | +| `#(?:a(*ACCEPT))?(b)(c)#` | `list{0: string, 1?: string, 2?: string}` | +| `~(?:a(*ACCEPT))?(b)(c)~` | `list{0: string, 1?: string, 2?: string}` | +| `{(?:a(*ACCEPT))?(b)(c)}` | `list{0: string, 1?: string, 2?: string}` | +| `/(a(*ACCEPT)b)c/` | `array{string, string}` | +| `/(a(*ACCEPT)b)c/i` | `array{string, string}` | +| `/(a(*ACCEPT)b)c/u` | `array{string, string}` | +| `/(a(*ACCEPT)b)c/iu` | `array{string, string}` | +| `/(a(*ACCEPT)b)c/m` | `array{string, string}` | +| `/(a(*ACCEPT)b)c/s` | `array{string, string}` | +| `/(a(*ACCEPT)b)c/x` | `array{string, string}` | +| `/(a(*ACCEPT)b)c/D` | `array{string, string}` | +| `/(a(*ACCEPT)b)c/n` | `array{string}` | +| `/(a(*ACCEPT)b)c/U` | `array{string, string}` | +| `/(a(*ACCEPT)b)c/mD` | `array{string, string}` | +| `/(a(*ACCEPT)b)c/su` | `array{string, string}` | +| `/(a(*ACCEPT)b)c/xu` | `array{string, string}` | +| `/(a(*ACCEPT)b)c/J` | `array{string, string}` | +| `#(a(*ACCEPT)b)c#` | `array{string, string}` | +| `~(a(*ACCEPT)b)c~` | `array{string, string}` | +| `{(a(*ACCEPT)b)c}` | `array{string, string}` | +| `/(*ACCEPT)(a)/` | `array{0: string, 1?: string}` | +| `/(*ACCEPT)(a)/i` | `array{0: string, 1?: string}` | +| `/(*ACCEPT)(a)/u` | `array{0: string, 1?: string}` | +| `/(*ACCEPT)(a)/iu` | `array{0: string, 1?: string}` | +| `/(*ACCEPT)(a)/m` | `array{0: string, 1?: string}` | +| `/(*ACCEPT)(a)/s` | `array{0: string, 1?: string}` | +| `/(*ACCEPT)(a)/x` | `array{0: string, 1?: string}` | +| `/(*ACCEPT)(a)/D` | `array{0: string, 1?: string}` | +| `/(*ACCEPT)(a)/n` | `array{string}` | +| `/(*ACCEPT)(a)/U` | `array{0: string, 1?: string}` | +| `/(*ACCEPT)(a)/mD` | `array{0: string, 1?: string}` | +| `/(*ACCEPT)(a)/su` | `array{0: string, 1?: string}` | +| `/(*ACCEPT)(a)/xu` | `array{0: string, 1?: string}` | +| `/(*ACCEPT)(a)/J` | `array{0: string, 1?: string}` | +| `#(*ACCEPT)(a)#` | `array{0: string, 1?: string}` | +| `~(*ACCEPT)(a)~` | `array{0: string, 1?: string}` | +| `{(*ACCEPT)(a)}` | `array{0: string, 1?: string}` | +| `/(a)\|b(*ACCEPT)(c)/` | `list{0: string, 1?: string, 2?: string}` | +| `/(a)\|b(*ACCEPT)(c)/i` | `list{0: string, 1?: string, 2?: string}` | +| `/(a)\|b(*ACCEPT)(c)/u` | `list{0: string, 1?: string, 2?: string}` | +| `/(a)\|b(*ACCEPT)(c)/iu` | `list{0: string, 1?: string, 2?: string}` | +| `/(a)\|b(*ACCEPT)(c)/m` | `list{0: string, 1?: string, 2?: string}` | +| `/(a)\|b(*ACCEPT)(c)/s` | `list{0: string, 1?: string, 2?: string}` | +| `/(a)\|b(*ACCEPT)(c)/x` | `list{0: string, 1?: string, 2?: string}` | +| `/(a)\|b(*ACCEPT)(c)/D` | `list{0: string, 1?: string, 2?: string}` | +| `/(a)\|b(*ACCEPT)(c)/n` | `array{string}` | +| `/(a)\|b(*ACCEPT)(c)/U` | `list{0: string, 1?: string, 2?: string}` | +| `/(a)\|b(*ACCEPT)(c)/mD` | `list{0: string, 1?: string, 2?: string}` | +| `/(a)\|b(*ACCEPT)(c)/su` | `list{0: string, 1?: string, 2?: string}` | +| `/(a)\|b(*ACCEPT)(c)/xu` | `list{0: string, 1?: string, 2?: string}` | +| `/(a)\|b(*ACCEPT)(c)/J` | `list{0: string, 1?: string, 2?: string}` | +| `#(a)\|b(*ACCEPT)(c)#` | `list{0: string, 1?: string, 2?: string}` | +| `~(a)\|b(*ACCEPT)(c)~` | `list{0: string, 1?: string, 2?: string}` | +| `{(a)\|b(*ACCEPT)(c)}` | `list{0: string, 1?: string, 2?: string}` | +| `/(?:x(*ACCEPT)\|y)(z)/` | `array{0: string, 1?: string}` | +| `/(?:x(*ACCEPT)\|y)(z)/i` | `array{0: string, 1?: string}` | +| `/(?:x(*ACCEPT)\|y)(z)/u` | `array{0: string, 1?: string}` | +| `/(?:x(*ACCEPT)\|y)(z)/iu` | `array{0: string, 1?: string}` | +| `/(?:x(*ACCEPT)\|y)(z)/m` | `array{0: string, 1?: string}` | +| `/(?:x(*ACCEPT)\|y)(z)/s` | `array{0: string, 1?: string}` | +| `/(?:x(*ACCEPT)\|y)(z)/x` | `array{0: string, 1?: string}` | +| `/(?:x(*ACCEPT)\|y)(z)/D` | `array{0: string, 1?: string}` | +| `/(?:x(*ACCEPT)\|y)(z)/n` | `array{string}` | +| `/(?:x(*ACCEPT)\|y)(z)/U` | `array{0: string, 1?: string}` | +| `/(?:x(*ACCEPT)\|y)(z)/mD` | `array{0: string, 1?: string}` | +| `/(?:x(*ACCEPT)\|y)(z)/su` | `array{0: string, 1?: string}` | +| `/(?:x(*ACCEPT)\|y)(z)/xu` | `array{0: string, 1?: string}` | +| `/(?:x(*ACCEPT)\|y)(z)/J` | `array{0: string, 1?: string}` | +| `#(?:x(*ACCEPT)\|y)(z)#` | `array{0: string, 1?: string}` | +| `~(?:x(*ACCEPT)\|y)(z)~` | `array{0: string, 1?: string}` | +| `{(?:x(*ACCEPT)\|y)(z)}` | `array{0: string, 1?: string}` | +| `/(*MARK:m)(a)/` | `array{0: 'a', 1: 'a', MARK?: 'm'}` | +| `/(*MARK:m)(a)/i` | `array{0: non-falsy-string, 1: non-falsy-string, MARK?: 'm'}` | +| `/(*MARK:m)(a)/u` | `array{0: 'a', 1: 'a', MARK?: 'm'}` | +| `/(*MARK:m)(a)/iu` | `array{0: non-falsy-string, 1: non-falsy-string, MARK?: 'm'}` | +| `/(*MARK:m)(a)/m` | `array{0: 'a', 1: 'a', MARK?: 'm'}` | +| `/(*MARK:m)(a)/s` | `array{0: 'a', 1: 'a', MARK?: 'm'}` | +| `/(*MARK:m)(a)/x` | `array{0: 'a', 1: 'a', MARK?: 'm'}` | +| `/(*MARK:m)(a)/D` | `array{0: 'a', 1: 'a', MARK?: 'm'}` | +| `/(*MARK:m)(a)/n` | `array{0: 'a', MARK?: 'm'}` | +| `/(*MARK:m)(a)/U` | `array{0: 'a', 1: 'a', MARK?: 'm'}` | +| `/(*MARK:m)(a)/mD` | `array{0: 'a', 1: 'a', MARK?: 'm'}` | +| `/(*MARK:m)(a)/su` | `array{0: 'a', 1: 'a', MARK?: 'm'}` | +| `/(*MARK:m)(a)/xu` | `array{0: 'a', 1: 'a', MARK?: 'm'}` | +| `/(*MARK:m)(a)/J` | `array{0: 'a', 1: 'a', MARK?: 'm'}` | +| `#(*MARK:m)(a)#` | `array{0: 'a', 1: 'a', MARK?: 'm'}` | +| `~(*MARK:m)(a)~` | `array{0: 'a', 1: 'a', MARK?: 'm'}` | +| `{(*MARK:m)(a)}` | `array{0: 'a', 1: 'a', MARK?: 'm'}` | +| `/(?:(*MARK:m)a\|(*MARK:n)b)(c)/` | `array{0: 'ac'\|'bc', 1: 'c', MARK?: 'm'\|'n'}` | +| `/(?:(*MARK:m)a\|(*MARK:n)b)(c)/i` | `array{0: non-falsy-string, 1: non-falsy-string, MARK?: 'm'\|'n'}` | +| `/(?:(*MARK:m)a\|(*MARK:n)b)(c)/u` | `array{0: 'ac'\|'bc', 1: 'c', MARK?: 'm'\|'n'}` | +| `/(?:(*MARK:m)a\|(*MARK:n)b)(c)/iu` | `array{0: non-falsy-string, 1: non-falsy-string, MARK?: 'm'\|'n'}` | +| `/(?:(*MARK:m)a\|(*MARK:n)b)(c)/m` | `array{0: 'ac'\|'bc', 1: 'c', MARK?: 'm'\|'n'}` | +| `/(?:(*MARK:m)a\|(*MARK:n)b)(c)/s` | `array{0: 'ac'\|'bc', 1: 'c', MARK?: 'm'\|'n'}` | +| `/(?:(*MARK:m)a\|(*MARK:n)b)(c)/x` | `array{0: 'ac'\|'bc', 1: 'c', MARK?: 'm'\|'n'}` | +| `/(?:(*MARK:m)a\|(*MARK:n)b)(c)/D` | `array{0: 'ac'\|'bc', 1: 'c', MARK?: 'm'\|'n'}` | +| `/(?:(*MARK:m)a\|(*MARK:n)b)(c)/n` | `array{0: 'ac'\|'bc', MARK?: 'm'\|'n'}` | +| `/(?:(*MARK:m)a\|(*MARK:n)b)(c)/U` | `array{0: 'ac'\|'bc', 1: 'c', MARK?: 'm'\|'n'}` | +| `/(?:(*MARK:m)a\|(*MARK:n)b)(c)/mD` | `array{0: 'ac'\|'bc', 1: 'c', MARK?: 'm'\|'n'}` | +| `/(?:(*MARK:m)a\|(*MARK:n)b)(c)/su` | `array{0: 'ac'\|'bc', 1: 'c', MARK?: 'm'\|'n'}` | +| `/(?:(*MARK:m)a\|(*MARK:n)b)(c)/xu` | `array{0: 'ac'\|'bc', 1: 'c', MARK?: 'm'\|'n'}` | +| `/(?:(*MARK:m)a\|(*MARK:n)b)(c)/J` | `array{0: 'ac'\|'bc', 1: 'c', MARK?: 'm'\|'n'}` | +| `#(?:(*MARK:m)a\|(*MARK:n)b)(c)#` | `array{0: 'ac'\|'bc', 1: 'c', MARK?: 'm'\|'n'}` | +| `~(?:(*MARK:m)a\|(*MARK:n)b)(c)~` | `array{0: 'ac'\|'bc', 1: 'c', MARK?: 'm'\|'n'}` | +| `{(?:(*MARK:m)a\|(*MARK:n)b)(c)}` | `array{0: 'ac'\|'bc', 1: 'c', MARK?: 'm'\|'n'}` | +| `/(a)(*SKIP)(*FAIL)\|(b)/` | `array{'a'\|'b', '', 'b'}\|array{'a'\|'b', 'a'}` | +| `/(a)(*SKIP)(*FAIL)\|(b)/i` | `array{non-falsy-string, '', non-falsy-string}\|array{non-falsy-string, non-falsy-string}` | +| `/(a)(*SKIP)(*FAIL)\|(b)/u` | `array{'a'\|'b', '', 'b'}\|array{'a'\|'b', 'a'}` | +| `/(a)(*SKIP)(*FAIL)\|(b)/iu` | `array{non-falsy-string, '', non-falsy-string}\|array{non-falsy-string, non-falsy-string}` | +| `/(a)(*SKIP)(*FAIL)\|(b)/m` | `array{'a'\|'b', '', 'b'}\|array{'a'\|'b', 'a'}` | +| `/(a)(*SKIP)(*FAIL)\|(b)/s` | `array{'a'\|'b', '', 'b'}\|array{'a'\|'b', 'a'}` | +| `/(a)(*SKIP)(*FAIL)\|(b)/x` | `array{'a'\|'b', '', 'b'}\|array{'a'\|'b', 'a'}` | +| `/(a)(*SKIP)(*FAIL)\|(b)/D` | `array{'a'\|'b', '', 'b'}\|array{'a'\|'b', 'a'}` | +| `/(a)(*SKIP)(*FAIL)\|(b)/n` | `array{'a'\|'b'}` | +| `/(a)(*SKIP)(*FAIL)\|(b)/U` | `array{'a'\|'b', '', 'b'}\|array{'a'\|'b', 'a'}` | +| `/(a)(*SKIP)(*FAIL)\|(b)/mD` | `array{'a'\|'b', '', 'b'}\|array{'a'\|'b', 'a'}` | +| `/(a)(*SKIP)(*FAIL)\|(b)/su` | `array{'a'\|'b', '', 'b'}\|array{'a'\|'b', 'a'}` | +| `/(a)(*SKIP)(*FAIL)\|(b)/xu` | `array{'a'\|'b', '', 'b'}\|array{'a'\|'b', 'a'}` | +| `/(a)(*SKIP)(*FAIL)\|(b)/J` | `array{'a'\|'b', '', 'b'}\|array{'a'\|'b', 'a'}` | +| `#(a)(*SKIP)(*FAIL)\|(b)#` | `array{'a'\|'b', '', 'b'}\|array{'a'\|'b', 'a'}` | +| `~(a)(*SKIP)(*FAIL)\|(b)~` | `array{'a'\|'b', '', 'b'}\|array{'a'\|'b', 'a'}` | +| `{(a)(*SKIP)(*FAIL)\|(b)}` | `array{'a'\|'b', '', 'b'}\|array{'a'\|'b', 'a'}` | +| `/(a)(*COMMIT)b/` | `array{'ab', 'a'}` | +| `/(a)(*COMMIT)b/i` | `array{non-falsy-string, non-falsy-string}` | +| `/(a)(*COMMIT)b/u` | `array{'ab', 'a'}` | +| `/(a)(*COMMIT)b/iu` | `array{non-falsy-string, non-falsy-string}` | +| `/(a)(*COMMIT)b/m` | `array{'ab', 'a'}` | +| `/(a)(*COMMIT)b/s` | `array{'ab', 'a'}` | +| `/(a)(*COMMIT)b/x` | `array{'ab', 'a'}` | +| `/(a)(*COMMIT)b/D` | `array{'ab', 'a'}` | +| `/(a)(*COMMIT)b/n` | `array{'ab'}` | +| `/(a)(*COMMIT)b/U` | `array{'ab', 'a'}` | +| `/(a)(*COMMIT)b/mD` | `array{'ab', 'a'}` | +| `/(a)(*COMMIT)b/su` | `array{'ab', 'a'}` | +| `/(a)(*COMMIT)b/xu` | `array{'ab', 'a'}` | +| `/(a)(*COMMIT)b/J` | `array{'ab', 'a'}` | +| `#(a)(*COMMIT)b#` | `array{'ab', 'a'}` | +| `~(a)(*COMMIT)b~` | `array{'ab', 'a'}` | +| `{(a)(*COMMIT)b}` | `array{'ab', 'a'}` | +| `/(a)(*PRUNE)b\|(c)/` | `array{'ab'\|'c', '', 'c'}\|array{'ab'\|'c', 'a'}` | +| `/(a)(*PRUNE)b\|(c)/i` | `array{non-falsy-string, '', non-falsy-string}\|array{non-falsy-string, non-falsy-string}` | +| `/(a)(*PRUNE)b\|(c)/u` | `array{'ab'\|'c', '', 'c'}\|array{'ab'\|'c', 'a'}` | +| `/(a)(*PRUNE)b\|(c)/iu` | `array{non-falsy-string, '', non-falsy-string}\|array{non-falsy-string, non-falsy-string}` | +| `/(a)(*PRUNE)b\|(c)/m` | `array{'ab'\|'c', '', 'c'}\|array{'ab'\|'c', 'a'}` | +| `/(a)(*PRUNE)b\|(c)/s` | `array{'ab'\|'c', '', 'c'}\|array{'ab'\|'c', 'a'}` | +| `/(a)(*PRUNE)b\|(c)/x` | `array{'ab'\|'c', '', 'c'}\|array{'ab'\|'c', 'a'}` | +| `/(a)(*PRUNE)b\|(c)/D` | `array{'ab'\|'c', '', 'c'}\|array{'ab'\|'c', 'a'}` | +| `/(a)(*PRUNE)b\|(c)/n` | `array{'ab'\|'c'}` | +| `/(a)(*PRUNE)b\|(c)/U` | `array{'ab'\|'c', '', 'c'}\|array{'ab'\|'c', 'a'}` | +| `/(a)(*PRUNE)b\|(c)/mD` | `array{'ab'\|'c', '', 'c'}\|array{'ab'\|'c', 'a'}` | +| `/(a)(*PRUNE)b\|(c)/su` | `array{'ab'\|'c', '', 'c'}\|array{'ab'\|'c', 'a'}` | +| `/(a)(*PRUNE)b\|(c)/xu` | `array{'ab'\|'c', '', 'c'}\|array{'ab'\|'c', 'a'}` | +| `/(a)(*PRUNE)b\|(c)/J` | `array{'ab'\|'c', '', 'c'}\|array{'ab'\|'c', 'a'}` | +| `#(a)(*PRUNE)b\|(c)#` | `array{'ab'\|'c', '', 'c'}\|array{'ab'\|'c', 'a'}` | +| `~(a)(*PRUNE)b\|(c)~` | `array{'ab'\|'c', '', 'c'}\|array{'ab'\|'c', 'a'}` | +| `{(a)(*PRUNE)b\|(c)}` | `array{'ab'\|'c', '', 'c'}\|array{'ab'\|'c', 'a'}` | +| `/(*:x)(a)\|(*:y)(b)/J` | `array{0: 'a'\|'b', 1: '', 2: 'b', MARK?: 'x'\|'y'}\|array{0: 'a'\|'b', 1: 'a', MARK?: 'x'\|'y'}` | +| `/a(*THEN:t)b\|(c)/` | `array{0: 'ab'\|'c', 1?: 'c', MARK?: 't'}` | +| `/a(*THEN:t)b\|(c)/i` | `array{0: non-falsy-string, 1?: non-falsy-string, MARK?: 't'}` | +| `/a(*THEN:t)b\|(c)/u` | `array{0: 'ab'\|'c', 1?: 'c', MARK?: 't'}` | +| `/a(*THEN:t)b\|(c)/iu` | `array{0: non-falsy-string, 1?: non-falsy-string, MARK?: 't'}` | +| `/a(*THEN:t)b\|(c)/m` | `array{0: 'ab'\|'c', 1?: 'c', MARK?: 't'}` | +| `/a(*THEN:t)b\|(c)/s` | `array{0: 'ab'\|'c', 1?: 'c', MARK?: 't'}` | +| `/a(*THEN:t)b\|(c)/x` | `array{0: 'ab'\|'c', 1?: 'c', MARK?: 't'}` | +| `/a(*THEN:t)b\|(c)/D` | `array{0: 'ab'\|'c', 1?: 'c', MARK?: 't'}` | +| `/a(*THEN:t)b\|(c)/n` | `array{0: 'ab'\|'c', MARK?: 't'}` | +| `/a(*THEN:t)b\|(c)/U` | `array{0: 'ab'\|'c', 1?: 'c', MARK?: 't'}` | +| `/a(*THEN:t)b\|(c)/mD` | `array{0: 'ab'\|'c', 1?: 'c', MARK?: 't'}` | +| `/a(*THEN:t)b\|(c)/su` | `array{0: 'ab'\|'c', 1?: 'c', MARK?: 't'}` | +| `/a(*THEN:t)b\|(c)/xu` | `array{0: 'ab'\|'c', 1?: 'c', MARK?: 't'}` | +| `/a(*THEN:t)b\|(c)/J` | `array{0: 'ab'\|'c', 1?: 'c', MARK?: 't'}` | +| `#a(*THEN:t)b\|(c)#` | `array{0: 'ab'\|'c', 1?: 'c', MARK?: 't'}` | +| `~a(*THEN:t)b\|(c)~` | `array{0: 'ab'\|'c', 1?: 'c', MARK?: 't'}` | +| `{a(*THEN:t)b\|(c)}` | `array{0: 'ab'\|'c', 1?: 'c', MARK?: 't'}` | +| `/(*F)\|(a)/` | `array{0: ''\|'a', 1?: 'a'}` | +| `/(*F)\|(a)/i` | `array{0: string, 1?: non-falsy-string}` | +| `/(*F)\|(a)/u` | `array{0: ''\|'a', 1?: 'a'}` | +| `/(*F)\|(a)/iu` | `array{0: string, 1?: non-falsy-string}` | +| `/(*F)\|(a)/m` | `array{0: ''\|'a', 1?: 'a'}` | +| `/(*F)\|(a)/s` | `array{0: ''\|'a', 1?: 'a'}` | +| `/(*F)\|(a)/x` | `array{0: ''\|'a', 1?: 'a'}` | +| `/(*F)\|(a)/D` | `array{0: ''\|'a', 1?: 'a'}` | +| `/(*F)\|(a)/n` | `array{''\|'a'}` | +| `/(*F)\|(a)/U` | `array{0: ''\|'a', 1?: 'a'}` | +| `/(*F)\|(a)/mD` | `array{0: ''\|'a', 1?: 'a'}` | +| `/(*F)\|(a)/su` | `array{0: ''\|'a', 1?: 'a'}` | +| `/(*F)\|(a)/xu` | `array{0: ''\|'a', 1?: 'a'}` | +| `/(*F)\|(a)/J` | `array{0: ''\|'a', 1?: 'a'}` | +| `#(*F)\|(a)#` | `array{0: ''\|'a', 1?: 'a'}` | +| `~(*F)\|(a)~` | `array{0: ''\|'a', 1?: 'a'}` | +| `{(*F)\|(a)}` | `array{0: ''\|'a', 1?: 'a'}` | +| `/(?:(a)(*F)\|(b))/` | `array{'a'\|'b', '', 'b'}\|array{'a'\|'b', 'a'}` | +| `/(?:(a)(*F)\|(b))/i` | `array{non-falsy-string, '', non-falsy-string}\|array{non-falsy-string, non-falsy-string}` | +| `/(?:(a)(*F)\|(b))/u` | `array{'a'\|'b', '', 'b'}\|array{'a'\|'b', 'a'}` | +| `/(?:(a)(*F)\|(b))/iu` | `array{non-falsy-string, '', non-falsy-string}\|array{non-falsy-string, non-falsy-string}` | +| `/(?:(a)(*F)\|(b))/m` | `array{'a'\|'b', '', 'b'}\|array{'a'\|'b', 'a'}` | +| `/(?:(a)(*F)\|(b))/s` | `array{'a'\|'b', '', 'b'}\|array{'a'\|'b', 'a'}` | +| `/(?:(a)(*F)\|(b))/x` | `array{'a'\|'b', '', 'b'}\|array{'a'\|'b', 'a'}` | +| `/(?:(a)(*F)\|(b))/D` | `array{'a'\|'b', '', 'b'}\|array{'a'\|'b', 'a'}` | +| `/(?:(a)(*F)\|(b))/n` | `array{'a'\|'b'}` | +| `/(?:(a)(*F)\|(b))/U` | `array{'a'\|'b', '', 'b'}\|array{'a'\|'b', 'a'}` | +| `/(?:(a)(*F)\|(b))/mD` | `array{'a'\|'b', '', 'b'}\|array{'a'\|'b', 'a'}` | +| `/(?:(a)(*F)\|(b))/su` | `array{'a'\|'b', '', 'b'}\|array{'a'\|'b', 'a'}` | +| `/(?:(a)(*F)\|(b))/xu` | `array{'a'\|'b', '', 'b'}\|array{'a'\|'b', 'a'}` | +| `/(?:(a)(*F)\|(b))/J` | `array{'a'\|'b', '', 'b'}\|array{'a'\|'b', 'a'}` | +| `#(?:(a)(*F)\|(b))#` | `array{'a'\|'b', '', 'b'}\|array{'a'\|'b', 'a'}` | +| `~(?:(a)(*F)\|(b))~` | `array{'a'\|'b', '', 'b'}\|array{'a'\|'b', 'a'}` | +| `{(?:(a)(*F)\|(b))}` | `array{'a'\|'b', '', 'b'}\|array{'a'\|'b', 'a'}` | +| `/(a)?(?(1)b\|c)/J` | `array{0: non-empty-string, 1?: 'a'}` | +| `/(a)?(?(1)(b)\|(c))/J` | `array{non-empty-string, ''\|'a', '', 'c'}\|array{non-empty-string, ''\|'a', 'b'}` | +| `/(a)?(?(1)(b))/J` | `list{0: string, 1?: ''\|'a', 2?: 'b'}` | +| `/(?(?=a)(a)\|(b))/` | `array{non-empty-string, '', 'b'}\|array{non-empty-string, 'a'}` | +| `/(?(?=a)(a)\|(b))/i` | `array{non-empty-string, '', non-falsy-string}\|array{non-empty-string, non-falsy-string}` | +| `/(?(?=a)(a)\|(b))/u` | `array{non-empty-string, '', 'b'}\|array{non-empty-string, 'a'}` | +| `/(?(?=a)(a)\|(b))/iu` | `array{non-empty-string, '', non-falsy-string}\|array{non-empty-string, non-falsy-string}` | +| `/(?(?=a)(a)\|(b))/m` | `array{non-empty-string, '', 'b'}\|array{non-empty-string, 'a'}` | +| `/(?(?=a)(a)\|(b))/s` | `array{non-empty-string, '', 'b'}\|array{non-empty-string, 'a'}` | +| `/(?(?=a)(a)\|(b))/x` | `array{non-empty-string, '', 'b'}\|array{non-empty-string, 'a'}` | +| `/(?(?=a)(a)\|(b))/D` | `array{non-empty-string, '', 'b'}\|array{non-empty-string, 'a'}` | +| `/(?(?=a)(a)\|(b))/n` | `array{non-empty-string}` | +| `/(?(?=a)(a)\|(b))/U` | `array{non-empty-string, '', 'b'}\|array{non-empty-string, 'a'}` | +| `/(?(?=a)(a)\|(b))/mD` | `array{non-empty-string, '', 'b'}\|array{non-empty-string, 'a'}` | +| `/(?(?=a)(a)\|(b))/su` | `array{non-empty-string, '', 'b'}\|array{non-empty-string, 'a'}` | +| `/(?(?=a)(a)\|(b))/xu` | `array{non-empty-string, '', 'b'}\|array{non-empty-string, 'a'}` | +| `/(?(?=a)(a)\|(b))/J` | `array{non-empty-string, '', 'b'}\|array{non-empty-string, 'a'}` | +| `#(?(?=a)(a)\|(b))#` | `array{non-empty-string, '', 'b'}\|array{non-empty-string, 'a'}` | +| `~(?(?=a)(a)\|(b))~` | `array{non-empty-string, '', 'b'}\|array{non-empty-string, 'a'}` | +| `{(?(?=a)(a)\|(b))}` | `array{non-empty-string, '', 'b'}\|array{non-empty-string, 'a'}` | +| `/(?(?!a)(b)\|(a))/` | `array{non-empty-string, '', 'a'}\|array{non-empty-string, 'b'}` | +| `/(?(?!a)(b)\|(a))/i` | `array{non-empty-string, '', non-falsy-string}\|array{non-empty-string, non-falsy-string}` | +| `/(?(?!a)(b)\|(a))/u` | `array{non-empty-string, '', 'a'}\|array{non-empty-string, 'b'}` | +| `/(?(?!a)(b)\|(a))/iu` | `array{non-empty-string, '', non-falsy-string}\|array{non-empty-string, non-falsy-string}` | +| `/(?(?!a)(b)\|(a))/m` | `array{non-empty-string, '', 'a'}\|array{non-empty-string, 'b'}` | +| `/(?(?!a)(b)\|(a))/s` | `array{non-empty-string, '', 'a'}\|array{non-empty-string, 'b'}` | +| `/(?(?!a)(b)\|(a))/x` | `array{non-empty-string, '', 'a'}\|array{non-empty-string, 'b'}` | +| `/(?(?!a)(b)\|(a))/D` | `array{non-empty-string, '', 'a'}\|array{non-empty-string, 'b'}` | +| `/(?(?!a)(b)\|(a))/n` | `array{non-empty-string}` | +| `/(?(?!a)(b)\|(a))/U` | `array{non-empty-string, '', 'a'}\|array{non-empty-string, 'b'}` | +| `/(?(?!a)(b)\|(a))/mD` | `array{non-empty-string, '', 'a'}\|array{non-empty-string, 'b'}` | +| `/(?(?!a)(b)\|(a))/su` | `array{non-empty-string, '', 'a'}\|array{non-empty-string, 'b'}` | +| `/(?(?!a)(b)\|(a))/xu` | `array{non-empty-string, '', 'a'}\|array{non-empty-string, 'b'}` | +| `/(?(?!a)(b)\|(a))/J` | `array{non-empty-string, '', 'a'}\|array{non-empty-string, 'b'}` | +| `#(?(?!a)(b)\|(a))#` | `array{non-empty-string, '', 'a'}\|array{non-empty-string, 'b'}` | +| `~(?(?!a)(b)\|(a))~` | `array{non-empty-string, '', 'a'}\|array{non-empty-string, 'b'}` | +| `{(?(?!a)(b)\|(a))}` | `array{non-empty-string, '', 'a'}\|array{non-empty-string, 'b'}` | +| `/(?(?=(a))a\|b)/` | `array{0: non-empty-string, 1?: 'a'}` | +| `/(?(?=(a))a\|b)/i` | `array{0: non-empty-string, 1?: non-falsy-string}` | +| `/(?(?=(a))a\|b)/u` | `array{0: non-empty-string, 1?: 'a'}` | +| `/(?(?=(a))a\|b)/iu` | `array{0: non-empty-string, 1?: non-falsy-string}` | +| `/(?(?=(a))a\|b)/m` | `array{0: non-empty-string, 1?: 'a'}` | +| `/(?(?=(a))a\|b)/s` | `array{0: non-empty-string, 1?: 'a'}` | +| `/(?(?=(a))a\|b)/x` | `array{0: non-empty-string, 1?: 'a'}` | +| `/(?(?=(a))a\|b)/D` | `array{0: non-empty-string, 1?: 'a'}` | +| `/(?(?=(a))a\|b)/n` | `array{non-empty-string}` | +| `/(?(?=(a))a\|b)/U` | `array{0: non-empty-string, 1?: 'a'}` | +| `/(?(?=(a))a\|b)/mD` | `array{0: non-empty-string, 1?: 'a'}` | +| `/(?(?=(a))a\|b)/su` | `array{0: non-empty-string, 1?: 'a'}` | +| `/(?(?=(a))a\|b)/xu` | `array{0: non-empty-string, 1?: 'a'}` | +| `/(?(?=(a))a\|b)/J` | `array{0: non-empty-string, 1?: 'a'}` | +| `#(?(?=(a))a\|b)#` | `array{0: non-empty-string, 1?: 'a'}` | +| `~(?(?=(a))a\|b)~` | `array{0: non-empty-string, 1?: 'a'}` | +| `{(?(?=(a))a\|b)}` | `array{0: non-empty-string, 1?: 'a'}` | +| `/(?(?<=x)(a)\|(b))/` | `array{non-empty-string, '', 'b'}\|array{non-empty-string, 'a'}` | +| `/(?(?<=x)(a)\|(b))/i` | `array{non-empty-string, '', non-falsy-string}\|array{non-empty-string, non-falsy-string}` | +| `/(?(?<=x)(a)\|(b))/u` | `array{non-empty-string, '', 'b'}\|array{non-empty-string, 'a'}` | +| `/(?(?<=x)(a)\|(b))/iu` | `array{non-empty-string, '', non-falsy-string}\|array{non-empty-string, non-falsy-string}` | +| `/(?(?<=x)(a)\|(b))/m` | `array{non-empty-string, '', 'b'}\|array{non-empty-string, 'a'}` | +| `/(?(?<=x)(a)\|(b))/s` | `array{non-empty-string, '', 'b'}\|array{non-empty-string, 'a'}` | +| `/(?(?<=x)(a)\|(b))/x` | `array{non-empty-string, '', 'b'}\|array{non-empty-string, 'a'}` | +| `/(?(?<=x)(a)\|(b))/D` | `array{non-empty-string, '', 'b'}\|array{non-empty-string, 'a'}` | +| `/(?(?<=x)(a)\|(b))/n` | `array{non-empty-string}` | +| `/(?(?<=x)(a)\|(b))/U` | `array{non-empty-string, '', 'b'}\|array{non-empty-string, 'a'}` | +| `/(?(?<=x)(a)\|(b))/mD` | `array{non-empty-string, '', 'b'}\|array{non-empty-string, 'a'}` | +| `/(?(?<=x)(a)\|(b))/su` | `array{non-empty-string, '', 'b'}\|array{non-empty-string, 'a'}` | +| `/(?(?<=x)(a)\|(b))/xu` | `array{non-empty-string, '', 'b'}\|array{non-empty-string, 'a'}` | +| `/(?(?<=x)(a)\|(b))/J` | `array{non-empty-string, '', 'b'}\|array{non-empty-string, 'a'}` | +| `#(?(?<=x)(a)\|(b))#` | `array{non-empty-string, '', 'b'}\|array{non-empty-string, 'a'}` | +| `~(?(?<=x)(a)\|(b))~` | `array{non-empty-string, '', 'b'}\|array{non-empty-string, 'a'}` | +| `{(?(?<=x)(a)\|(b))}` | `array{non-empty-string, '', 'b'}\|array{non-empty-string, 'a'}` | +| `/(?(DEFINE)(?a))(b)/J` | `array{0: 'b', d: '', 1: '', 2: 'b'}` | +| `/(?(DEFINE)(a))(?1)/` | `array{'a'}` | +| `/(?(DEFINE)(a))(?1)/i` | `array{non-falsy-string}` | +| `/(?(DEFINE)(a))(?1)/u` | `array{'a'}` | +| `/(?(DEFINE)(a))(?1)/iu` | `array{non-falsy-string}` | +| `/(?(DEFINE)(a))(?1)/m` | `array{'a'}` | +| `/(?(DEFINE)(a))(?1)/s` | `array{'a'}` | +| `/(?(DEFINE)(a))(?1)/x` | `array{'a'}` | +| `/(?(DEFINE)(a))(?1)/D` | `array{'a'}` | +| `/(?(DEFINE)(a))(?1)/U` | `array{'a'}` | +| `/(?(DEFINE)(a))(?1)/mD` | `array{'a'}` | +| `/(?(DEFINE)(a))(?1)/su` | `array{'a'}` | +| `/(?(DEFINE)(a))(?1)/xu` | `array{'a'}` | +| `/(?(DEFINE)(a))(?1)/J` | `array{'a'}` | +| `#(?(DEFINE)(a))(?1)#` | `array{'a'}` | +| `~(?(DEFINE)(a))(?1)~` | `array{'a'}` | +| `{(?(DEFINE)(a))(?1)}` | `array{'a'}` | +| `/(?(R)(a)\|(b))/J` | `array{non-empty-string, '', 'b'}\|array{non-empty-string, 'a'}` | +| `/(?(R1)a\|(b))/J` | `array{0: non-empty-string, 1?: 'b'}` | +| `/(?(VERSION>=10.0)(a)\|(b))/J` | `array{non-empty-string, '', 'b'}\|array{non-empty-string, 'a'}` | +| `/(?(VERSION>=99)(a)\|(b))/J` | `array{non-empty-string, '', 'b'}\|array{non-empty-string, 'a'}` | +| `/(?a)?(?()b\|c)/J` | `array{0: non-empty-string, n: 'a', 1: 'a'}\|array{non-empty-string}` | +| `/(?a)?(?(n)b\|(c))/J` | `array{0: non-empty-string, n?: ''\|'a', 1?: ''\|'a', 2?: 'c'}` | +| `/(a)?(?(1)\|(b))/J` | `list{0: string, 1?: ''\|'a', 2?: 'b'}` | +| `/(?(1)(a)\|(b))/J` | `array{non-empty-string, '', 'b'}\|array{non-empty-string, 'a'}` | +| `/(?:(a)\|b)(?(1)c\|d)/J` | `array{0: non-falsy-string, 1?: 'a'}` | +| `/(?\|(a)\|(b))/J` | `array{'a'\|'b', 'a'\|'b'}` | +| `/(?\|(a)\|(b)(c))/J` | `array{0: 'a'\|'bc', 1: 'a'\|'b', 2?: 'c'}` | +| `/(?\|(a)(b)\|(c))/J` | `array{0: 'ab'\|'c', 1: 'a'\|'c', 2?: 'b'}` | +| `/(?\|(?a)\|(?b))/J` | `array{0: 'a'\|'b', x: 'a'\|'b', 1: 'a'\|'b'}` | +| `/(?\|(a)\|(?\|(b)\|(c)(d)))/` | `list{0: 'a'\|'b'\|'cd', 1?: 'a'\|'b'\|'c', 2?: 'd'}` | +| `/(?\|(a)\|(?\|(b)\|(c)(d)))/i` | `list{0: non-falsy-string, 1?: non-falsy-string, 2?: non-falsy-string}` | +| `/(?\|(a)\|(?\|(b)\|(c)(d)))/u` | `list{0: 'a'\|'b'\|'cd', 1?: 'a'\|'b'\|'c', 2?: 'd'}` | +| `/(?\|(a)\|(?\|(b)\|(c)(d)))/iu` | `list{0: non-falsy-string, 1?: non-falsy-string, 2?: non-falsy-string}` | +| `/(?\|(a)\|(?\|(b)\|(c)(d)))/m` | `list{0: 'a'\|'b'\|'cd', 1?: 'a'\|'b'\|'c', 2?: 'd'}` | +| `/(?\|(a)\|(?\|(b)\|(c)(d)))/s` | `list{0: 'a'\|'b'\|'cd', 1?: 'a'\|'b'\|'c', 2?: 'd'}` | +| `/(?\|(a)\|(?\|(b)\|(c)(d)))/x` | `list{0: 'a'\|'b'\|'cd', 1?: 'a'\|'b'\|'c', 2?: 'd'}` | +| `/(?\|(a)\|(?\|(b)\|(c)(d)))/D` | `list{0: 'a'\|'b'\|'cd', 1?: 'a'\|'b'\|'c', 2?: 'd'}` | +| `/(?\|(a)\|(?\|(b)\|(c)(d)))/U` | `list{0: 'a'\|'b'\|'cd', 1?: 'a'\|'b'\|'c', 2?: 'd'}` | +| `/(?\|(a)\|(?\|(b)\|(c)(d)))/mD` | `list{0: 'a'\|'b'\|'cd', 1?: 'a'\|'b'\|'c', 2?: 'd'}` | +| `/(?\|(a)\|(?\|(b)\|(c)(d)))/su` | `list{0: 'a'\|'b'\|'cd', 1?: 'a'\|'b'\|'c', 2?: 'd'}` | +| `/(?\|(a)\|(?\|(b)\|(c)(d)))/xu` | `list{0: 'a'\|'b'\|'cd', 1?: 'a'\|'b'\|'c', 2?: 'd'}` | +| `/(?\|(a)\|(?\|(b)\|(c)(d)))/J` | `list{0: 'a'\|'b'\|'cd', 1?: 'a'\|'b'\|'c', 2?: 'd'}` | +| `#(?\|(a)\|(?\|(b)\|(c)(d)))#` | `list{0: 'a'\|'b'\|'cd', 1?: 'a'\|'b'\|'c', 2?: 'd'}` | +| `~(?\|(a)\|(?\|(b)\|(c)(d)))~` | `list{0: 'a'\|'b'\|'cd', 1?: 'a'\|'b'\|'c', 2?: 'd'}` | +| `{(?\|(a)\|(?\|(b)\|(c)(d)))}` | `list{0: 'a'\|'b'\|'cd', 1?: 'a'\|'b'\|'c', 2?: 'd'}` | +| `/(?\|(a)\|b)(c)/` | `array{'ac'\|'bc', ''\|'a', 'c'}` | +| `/(?\|(a)\|b)(c)/i` | `array{non-falsy-string, string, non-falsy-string}` | +| `/(?\|(a)\|b)(c)/u` | `array{'ac'\|'bc', ''\|'a', 'c'}` | +| `/(?\|(a)\|b)(c)/iu` | `array{non-falsy-string, string, non-falsy-string}` | +| `/(?\|(a)\|b)(c)/m` | `array{'ac'\|'bc', ''\|'a', 'c'}` | +| `/(?\|(a)\|b)(c)/s` | `array{'ac'\|'bc', ''\|'a', 'c'}` | +| `/(?\|(a)\|b)(c)/x` | `array{'ac'\|'bc', ''\|'a', 'c'}` | +| `/(?\|(a)\|b)(c)/D` | `array{'ac'\|'bc', ''\|'a', 'c'}` | +| `/(?\|(a)\|b)(c)/U` | `array{'ac'\|'bc', ''\|'a', 'c'}` | +| `/(?\|(a)\|b)(c)/mD` | `array{'ac'\|'bc', ''\|'a', 'c'}` | +| `/(?\|(a)\|b)(c)/su` | `array{'ac'\|'bc', ''\|'a', 'c'}` | +| `/(?\|(a)\|b)(c)/xu` | `array{'ac'\|'bc', ''\|'a', 'c'}` | +| `/(?\|(a)\|b)(c)/J` | `array{'ac'\|'bc', ''\|'a', 'c'}` | +| `#(?\|(a)\|b)(c)#` | `array{'ac'\|'bc', ''\|'a', 'c'}` | +| `~(?\|(a)\|b)(c)~` | `array{'ac'\|'bc', ''\|'a', 'c'}` | +| `{(?\|(a)\|b)(c)}` | `array{'ac'\|'bc', ''\|'a', 'c'}` | +| `/(?\|a\|(b))(c)/` | `array{'ac'\|'bc', ''\|'b', 'c'}` | +| `/(?\|a\|(b))(c)/i` | `array{non-falsy-string, string, non-falsy-string}` | +| `/(?\|a\|(b))(c)/u` | `array{'ac'\|'bc', ''\|'b', 'c'}` | +| `/(?\|a\|(b))(c)/iu` | `array{non-falsy-string, string, non-falsy-string}` | +| `/(?\|a\|(b))(c)/m` | `array{'ac'\|'bc', ''\|'b', 'c'}` | +| `/(?\|a\|(b))(c)/s` | `array{'ac'\|'bc', ''\|'b', 'c'}` | +| `/(?\|a\|(b))(c)/x` | `array{'ac'\|'bc', ''\|'b', 'c'}` | +| `/(?\|a\|(b))(c)/D` | `array{'ac'\|'bc', ''\|'b', 'c'}` | +| `/(?\|a\|(b))(c)/U` | `array{'ac'\|'bc', ''\|'b', 'c'}` | +| `/(?\|a\|(b))(c)/mD` | `array{'ac'\|'bc', ''\|'b', 'c'}` | +| `/(?\|a\|(b))(c)/su` | `array{'ac'\|'bc', ''\|'b', 'c'}` | +| `/(?\|a\|(b))(c)/xu` | `array{'ac'\|'bc', ''\|'b', 'c'}` | +| `/(?\|a\|(b))(c)/J` | `array{'ac'\|'bc', ''\|'b', 'c'}` | +| `#(?\|a\|(b))(c)#` | `array{'ac'\|'bc', ''\|'b', 'c'}` | +| `~(?\|a\|(b))(c)~` | `array{'ac'\|'bc', ''\|'b', 'c'}` | +| `{(?\|a\|(b))(c)}` | `array{'ac'\|'bc', ''\|'b', 'c'}` | +| `/(?\|(a)\|(b))?/J` | `array{0: ''\|'a'\|'b', 1?: 'a'\|'b'}` | +| `/(?\|(a)\|(b))(?\|(c)\|(d))/` | `array{'ac'\|'ad'\|'bc'\|'bd', 'a'\|'b', 'c'\|'d'}` | +| `/(?\|(a)\|(b))(?\|(c)\|(d))/i` | `array{non-falsy-string, non-falsy-string, non-falsy-string}` | +| `/(?\|(a)\|(b))(?\|(c)\|(d))/u` | `array{'ac'\|'ad'\|'bc'\|'bd', 'a'\|'b', 'c'\|'d'}` | +| `/(?\|(a)\|(b))(?\|(c)\|(d))/iu` | `array{non-falsy-string, non-falsy-string, non-falsy-string}` | +| `/(?\|(a)\|(b))(?\|(c)\|(d))/m` | `array{'ac'\|'ad'\|'bc'\|'bd', 'a'\|'b', 'c'\|'d'}` | +| `/(?\|(a)\|(b))(?\|(c)\|(d))/s` | `array{'ac'\|'ad'\|'bc'\|'bd', 'a'\|'b', 'c'\|'d'}` | +| `/(?\|(a)\|(b))(?\|(c)\|(d))/x` | `array{'ac'\|'ad'\|'bc'\|'bd', 'a'\|'b', 'c'\|'d'}` | +| `/(?\|(a)\|(b))(?\|(c)\|(d))/D` | `array{'ac'\|'ad'\|'bc'\|'bd', 'a'\|'b', 'c'\|'d'}` | +| `/(?\|(a)\|(b))(?\|(c)\|(d))/U` | `array{'ac'\|'ad'\|'bc'\|'bd', 'a'\|'b', 'c'\|'d'}` | +| `/(?\|(a)\|(b))(?\|(c)\|(d))/mD` | `array{'ac'\|'ad'\|'bc'\|'bd', 'a'\|'b', 'c'\|'d'}` | +| `/(?\|(a)\|(b))(?\|(c)\|(d))/su` | `array{'ac'\|'ad'\|'bc'\|'bd', 'a'\|'b', 'c'\|'d'}` | +| `/(?\|(a)\|(b))(?\|(c)\|(d))/xu` | `array{'ac'\|'ad'\|'bc'\|'bd', 'a'\|'b', 'c'\|'d'}` | +| `/(?\|(a)\|(b))(?\|(c)\|(d))/J` | `array{'ac'\|'ad'\|'bc'\|'bd', 'a'\|'b', 'c'\|'d'}` | +| `#(?\|(a)\|(b))(?\|(c)\|(d))#` | `array{'ac'\|'ad'\|'bc'\|'bd', 'a'\|'b', 'c'\|'d'}` | +| `~(?\|(a)\|(b))(?\|(c)\|(d))~` | `array{'ac'\|'ad'\|'bc'\|'bd', 'a'\|'b', 'c'\|'d'}` | +| `{(?\|(a)\|(b))(?\|(c)\|(d))}` | `array{'ac'\|'ad'\|'bc'\|'bd', 'a'\|'b', 'c'\|'d'}` | +| `/(x)(?\|(a)\|(b))(y)/` | `array{'xay'\|'xby', 'x', 'a'\|'b', 'y'}` | +| `/(x)(?\|(a)\|(b))(y)/i` | `array{non-falsy-string, non-falsy-string, non-falsy-string, non-falsy-string}` | +| `/(x)(?\|(a)\|(b))(y)/u` | `array{'xay'\|'xby', 'x', 'a'\|'b', 'y'}` | +| `/(x)(?\|(a)\|(b))(y)/iu` | `array{non-falsy-string, non-falsy-string, non-falsy-string, non-falsy-string}` | +| `/(x)(?\|(a)\|(b))(y)/m` | `array{'xay'\|'xby', 'x', 'a'\|'b', 'y'}` | +| `/(x)(?\|(a)\|(b))(y)/s` | `array{'xay'\|'xby', 'x', 'a'\|'b', 'y'}` | +| `/(x)(?\|(a)\|(b))(y)/x` | `array{'xay'\|'xby', 'x', 'a'\|'b', 'y'}` | +| `/(x)(?\|(a)\|(b))(y)/D` | `array{'xay'\|'xby', 'x', 'a'\|'b', 'y'}` | +| `/(x)(?\|(a)\|(b))(y)/U` | `array{'xay'\|'xby', 'x', 'a'\|'b', 'y'}` | +| `/(x)(?\|(a)\|(b))(y)/mD` | `array{'xay'\|'xby', 'x', 'a'\|'b', 'y'}` | +| `/(x)(?\|(a)\|(b))(y)/su` | `array{'xay'\|'xby', 'x', 'a'\|'b', 'y'}` | +| `/(x)(?\|(a)\|(b))(y)/xu` | `array{'xay'\|'xby', 'x', 'a'\|'b', 'y'}` | +| `/(x)(?\|(a)\|(b))(y)/J` | `array{'xay'\|'xby', 'x', 'a'\|'b', 'y'}` | +| `#(x)(?\|(a)\|(b))(y)#` | `array{'xay'\|'xby', 'x', 'a'\|'b', 'y'}` | +| `~(x)(?\|(a)\|(b))(y)~` | `array{'xay'\|'xby', 'x', 'a'\|'b', 'y'}` | +| `{(x)(?\|(a)\|(b))(y)}` | `array{'xay'\|'xby', 'x', 'a'\|'b', 'y'}` | +| `/(?\|(\d)\|(a))/J` | `array{'a'\|numeric-string, 'a'\|numeric-string}` | +| `/(?\|(0)\|())/J` | `array{''\|'0', ''\|'0'}` | +| `/(?J)(?a)\|(?b)/J` | `array{0: 'a'\|'b', x: 'a', 1: 'a'}\|array{0: 'a'\|'b', x: 'b', 1: '', 2: 'b'}` | +| `/(?J)(?:(?a)\|(?b))c/J` | `array{0: 'ac'\|'bc', x: 'a', 1: 'a'}\|array{0: 'ac'\|'bc', x: 'b', 1: '', 2: 'b'}` | +| `/(?J)(?a)?(?b)/` | `array{0: 'ab'\|'b', x: ''\|'a'\|'b', 1: ''\|'a', 2: 'b'}` | +| `/(?J)(?a)?(?b)/i` | `array{0: non-falsy-string, x: string, 1: string, 2: non-falsy-string}` | +| `/(?J)(?a)?(?b)/u` | `array{0: 'ab'\|'b', x: ''\|'a'\|'b', 1: ''\|'a', 2: 'b'}` | +| `/(?J)(?a)?(?b)/iu` | `array{0: non-falsy-string, x: string, 1: string, 2: non-falsy-string}` | +| `/(?J)(?a)?(?b)/m` | `array{0: 'ab'\|'b', x: ''\|'a'\|'b', 1: ''\|'a', 2: 'b'}` | +| `/(?J)(?a)?(?b)/s` | `array{0: 'ab'\|'b', x: ''\|'a'\|'b', 1: ''\|'a', 2: 'b'}` | +| `/(?J)(?a)?(?b)/x` | `array{0: 'ab'\|'b', x: ''\|'a'\|'b', 1: ''\|'a', 2: 'b'}` | +| `/(?J)(?a)?(?b)/D` | `array{0: 'ab'\|'b', x: ''\|'a'\|'b', 1: ''\|'a', 2: 'b'}` | +| `/(?J)(?a)?(?b)/n` | `array{0: 'ab'\|'b', x: ''\|'a'\|'b', 1: ''\|'a', 2: 'b'}` | +| `/(?J)(?a)?(?b)/U` | `array{0: 'ab'\|'b', x: ''\|'a'\|'b', 1: ''\|'a', 2: 'b'}` | +| `/(?J)(?a)?(?b)/mD` | `array{0: 'ab'\|'b', x: ''\|'a'\|'b', 1: ''\|'a', 2: 'b'}` | +| `/(?J)(?a)?(?b)/su` | `array{0: 'ab'\|'b', x: ''\|'a'\|'b', 1: ''\|'a', 2: 'b'}` | +| `/(?J)(?a)?(?b)/xu` | `array{0: 'ab'\|'b', x: ''\|'a'\|'b', 1: ''\|'a', 2: 'b'}` | +| `/(?J)(?a)?(?b)/J` | `array{0: 'ab'\|'b', x: ''\|'a'\|'b', 1: ''\|'a', 2: 'b'}` | +| `#(?J)(?a)?(?b)#` | `array{0: 'ab'\|'b', x: ''\|'a'\|'b', 1: ''\|'a', 2: 'b'}` | +| `~(?J)(?a)?(?b)~` | `array{0: 'ab'\|'b', x: ''\|'a'\|'b', 1: ''\|'a', 2: 'b'}` | +| `{(?J)(?a)?(?b)}` | `array{0: 'ab'\|'b', x: ''\|'a'\|'b', 1: ''\|'a', 2: 'b'}` | +| `/(?J)(?\d)\|(?b)/J` | `array{0: 'b'\|numeric-string, x: 'b', 1: '', 2: 'b'}\|array{0: 'b'\|numeric-string, x: numeric-string, 1: numeric-string}` | +| `/(?J)(?a)\|(?c)\|(?b)/J` | `array{0: 'a'\|'b'\|'c', x: '', 1: '', y: 'c', 2: 'c'}\|array{0: 'a'\|'b'\|'c', x: 'a', 1: 'a'}\|array{0: 'a'\|'b'\|'c', x: 'b', 1: '', y: '', 2: '', 3: 'b'}` | +| `/(?n)(a)(?b)/J` | `array{0: 'ab', x: 'b', 1: 'b'}` | +| `/(?n)(a)/J` | `array{'a'}` | +| `/(?n:(a))(b)/J` | `array{'ab', 'b'}` | +| `/(?-n)(a)/J` | `array{'a', 'a'}` | +| `/(?a)(b)/J` | `array{0: 'ab', x: 'a', 1: 'a', 2: 'b'}` | +| `/(?Pa)/J` | `array{0: 'a', x: 'a', 1: 'a'}` | +| `/(?'x'a)/` | `array{0: 'a', x: 'a', 1: 'a'}` | +| `/(?'x'a)/i` | `array{0: non-falsy-string, x: non-falsy-string, 1: non-falsy-string}` | +| `/(?'x'a)/u` | `array{0: 'a', x: 'a', 1: 'a'}` | +| `/(?'x'a)/iu` | `array{0: non-falsy-string, x: non-falsy-string, 1: non-falsy-string}` | +| `/(?'x'a)/m` | `array{0: 'a', x: 'a', 1: 'a'}` | +| `/(?'x'a)/s` | `array{0: 'a', x: 'a', 1: 'a'}` | +| `/(?'x'a)/x` | `array{0: 'a', x: 'a', 1: 'a'}` | +| `/(?'x'a)/D` | `array{0: 'a', x: 'a', 1: 'a'}` | +| `/(?'x'a)/n` | `array{0: 'a', x: 'a', 1: 'a'}` | +| `/(?'x'a)/U` | `array{0: 'a', x: 'a', 1: 'a'}` | +| `/(?'x'a)/mD` | `array{0: 'a', x: 'a', 1: 'a'}` | +| `/(?'x'a)/su` | `array{0: 'a', x: 'a', 1: 'a'}` | +| `/(?'x'a)/xu` | `array{0: 'a', x: 'a', 1: 'a'}` | +| `/(?'x'a)/J` | `array{0: 'a', x: 'a', 1: 'a'}` | +| `#(?'x'a)#` | `array{0: 'a', x: 'a', 1: 'a'}` | +| `~(?'x'a)~` | `array{0: 'a', x: 'a', 1: 'a'}` | +| `{(?'x'a)}` | `array{0: 'a', x: 'a', 1: 'a'}` | +| `/(a) # (b)\n(c)/J` | `array{"a # b\nc", 'a', 'b', 'c'}` | +| `/(a)\#(b)/J` | `array{'a#b', 'a', 'b'}` | +| `/(a)[#](b)/J` | `array{'a#b', 'a', 'b'}` | +| `/(a)(?#c)(b)/J` | `array{'ab', 'a', 'b'}` | +| `/(?x)(a) #c\n(b)/J` | `array{'ab', 'a', 'b'}` | +| `/(?x)( a )/J` | `array{'a', 'a'}` | +| `/(?xx)([a b])/J` | `array{'a'\|'b', 'a'\|'b'}` | +| `/(\Qa.b\E)/J` | `array{'a.b', 'a.b'}` | +| `/\Q(a)\E/J` | `array{'(a)'}` | +| `/(\Q0\E)/J` | `array{'0', '0'}` | +| `/(\Q\E)/J` | `array{'', ''}` | +| `/(\Qa)\E)/` | `array{'a)', 'a)'}` | +| `/(\Qa)\E)/i` | `array{non-falsy-string, non-falsy-string}` | +| `/(\Qa)\E)/u` | `array{'a)', 'a)'}` | +| `/(\Qa)\E)/iu` | `array{non-falsy-string, non-falsy-string}` | +| `/(\Qa)\E)/m` | `array{'a)', 'a)'}` | +| `/(\Qa)\E)/s` | `array{'a)', 'a)'}` | +| `/(\Qa)\E)/x` | `array{'a)', 'a)'}` | +| `/(\Qa)\E)/D` | `array{'a)', 'a)'}` | +| `/(\Qa)\E)/n` | `array{'a)'}` | +| `/(\Qa)\E)/U` | `array{'a)', 'a)'}` | +| `/(\Qa)\E)/mD` | `array{'a)', 'a)'}` | +| `/(\Qa)\E)/su` | `array{'a)', 'a)'}` | +| `/(\Qa)\E)/xu` | `array{'a)', 'a)'}` | +| `/(\Qa)\E)/J` | `array{'a)', 'a)'}` | +| `#(\Qa)\E)#` | `array{'a)', 'a)'}` | +| `~(\Qa)\E)~` | `array{'a)', 'a)'}` | +| `{(\Qa)\E)}` | `array{'a)', 'a)'}` | +| `/(a)\1/J` | `array{non-falsy-string, 'a'}` | +| `/(a\|b)\1/J` | `array{non-falsy-string, 'a'\|'b'}` | +| `/(a)(\1)/J` | `array{non-falsy-string, 'a', string}` | +| `/(?a)\k/J` | `array{0: non-falsy-string, x: 'a', 1: 'a'}` | +| `/(a)\g{-1}/J` | `array{non-falsy-string, 'a'}` | +| `/(a)\g1/J` | `array{non-falsy-string, 'a'}` | +| `/(?:(a)\|b)\1/J` | `array{0: non-falsy-string, 1?: 'a'}` | +| `/(a)?\1/J` | `array{0: string, 1?: 'a'}` | +| `/(a)(?1)/` | `array{'aa', 'a'}` | +| `/(a)(?1)/i` | `array{non-falsy-string, non-falsy-string}` | +| `/(a)(?1)/u` | `array{'aa', 'a'}` | +| `/(a)(?1)/iu` | `array{non-falsy-string, non-falsy-string}` | +| `/(a)(?1)/m` | `array{'aa', 'a'}` | +| `/(a)(?1)/s` | `array{'aa', 'a'}` | +| `/(a)(?1)/x` | `array{'aa', 'a'}` | +| `/(a)(?1)/D` | `array{'aa', 'a'}` | +| `/(a)(?1)/U` | `array{'aa', 'a'}` | +| `/(a)(?1)/mD` | `array{'aa', 'a'}` | +| `/(a)(?1)/su` | `array{'aa', 'a'}` | +| `/(a)(?1)/xu` | `array{'aa', 'a'}` | +| `/(a)(?1)/J` | `array{'aa', 'a'}` | +| `#(a)(?1)#` | `array{'aa', 'a'}` | +| `~(a)(?1)~` | `array{'aa', 'a'}` | +| `{(a)(?1)}` | `array{'aa', 'a'}` | +| `/(a\|b)(?1)/` | `array{'aa'\|'ab'\|'ba'\|'bb', 'a'\|'b'}` | +| `/(a\|b)(?1)/i` | `array{non-falsy-string, non-falsy-string}` | +| `/(a\|b)(?1)/u` | `array{'aa'\|'ab'\|'ba'\|'bb', 'a'\|'b'}` | +| `/(a\|b)(?1)/iu` | `array{non-falsy-string, non-falsy-string}` | +| `/(a\|b)(?1)/m` | `array{'aa'\|'ab'\|'ba'\|'bb', 'a'\|'b'}` | +| `/(a\|b)(?1)/s` | `array{'aa'\|'ab'\|'ba'\|'bb', 'a'\|'b'}` | +| `/(a\|b)(?1)/x` | `array{'aa'\|'ab'\|'ba'\|'bb', 'a'\|'b'}` | +| `/(a\|b)(?1)/D` | `array{'aa'\|'ab'\|'ba'\|'bb', 'a'\|'b'}` | +| `/(a\|b)(?1)/U` | `array{'aa'\|'ab'\|'ba'\|'bb', 'a'\|'b'}` | +| `/(a\|b)(?1)/mD` | `array{'aa'\|'ab'\|'ba'\|'bb', 'a'\|'b'}` | +| `/(a\|b)(?1)/su` | `array{'aa'\|'ab'\|'ba'\|'bb', 'a'\|'b'}` | +| `/(a\|b)(?1)/xu` | `array{'aa'\|'ab'\|'ba'\|'bb', 'a'\|'b'}` | +| `/(a\|b)(?1)/J` | `array{'aa'\|'ab'\|'ba'\|'bb', 'a'\|'b'}` | +| `#(a\|b)(?1)#` | `array{'aa'\|'ab'\|'ba'\|'bb', 'a'\|'b'}` | +| `~(a\|b)(?1)~` | `array{'aa'\|'ab'\|'ba'\|'bb', 'a'\|'b'}` | +| `{(a\|b)(?1)}` | `array{'aa'\|'ab'\|'ba'\|'bb', 'a'\|'b'}` | +| `/(?1)(a)/` | `array{'aa', 'a'}` | +| `/(?1)(a)/i` | `array{non-falsy-string, non-falsy-string}` | +| `/(?1)(a)/u` | `array{'aa', 'a'}` | +| `/(?1)(a)/iu` | `array{non-falsy-string, non-falsy-string}` | +| `/(?1)(a)/m` | `array{'aa', 'a'}` | +| `/(?1)(a)/s` | `array{'aa', 'a'}` | +| `/(?1)(a)/x` | `array{'aa', 'a'}` | +| `/(?1)(a)/D` | `array{'aa', 'a'}` | +| `/(?1)(a)/U` | `array{'aa', 'a'}` | +| `/(?1)(a)/mD` | `array{'aa', 'a'}` | +| `/(?1)(a)/su` | `array{'aa', 'a'}` | +| `/(?1)(a)/xu` | `array{'aa', 'a'}` | +| `/(?1)(a)/J` | `array{'aa', 'a'}` | +| `#(?1)(a)#` | `array{'aa', 'a'}` | +| `~(?1)(a)~` | `array{'aa', 'a'}` | +| `{(?1)(a)}` | `array{'aa', 'a'}` | +| `/(a(?R)?b)/` | `array{non-falsy-string, non-falsy-string}` | +| `/(a(?R)?b)/i` | `array{non-falsy-string, non-falsy-string}` | +| `/(a(?R)?b)/u` | `array{non-falsy-string, non-falsy-string}` | +| `/(a(?R)?b)/iu` | `array{non-falsy-string, non-falsy-string}` | +| `/(a(?R)?b)/m` | `array{non-falsy-string, non-falsy-string}` | +| `/(a(?R)?b)/s` | `array{non-falsy-string, non-falsy-string}` | +| `/(a(?R)?b)/x` | `array{non-falsy-string, non-falsy-string}` | +| `/(a(?R)?b)/D` | `array{non-falsy-string, non-falsy-string}` | +| `/(a(?R)?b)/n` | `array{non-falsy-string}` | +| `/(a(?R)?b)/U` | `array{non-falsy-string, non-falsy-string}` | +| `/(a(?R)?b)/mD` | `array{non-falsy-string, non-falsy-string}` | +| `/(a(?R)?b)/su` | `array{non-falsy-string, non-falsy-string}` | +| `/(a(?R)?b)/xu` | `array{non-falsy-string, non-falsy-string}` | +| `/(a(?R)?b)/J` | `array{non-falsy-string, non-falsy-string}` | +| `#(a(?R)?b)#` | `array{non-falsy-string, non-falsy-string}` | +| `~(a(?R)?b)~` | `array{non-falsy-string, non-falsy-string}` | +| `{(a(?R)?b)}` | `array{non-falsy-string, non-falsy-string}` | +| `/((?R)?a)/` | `array{non-falsy-string, non-falsy-string}` | +| `/((?R)?a)/i` | `array{non-falsy-string, non-falsy-string}` | +| `/((?R)?a)/u` | `array{non-falsy-string, non-falsy-string}` | +| `/((?R)?a)/iu` | `array{non-falsy-string, non-falsy-string}` | +| `/((?R)?a)/m` | `array{non-falsy-string, non-falsy-string}` | +| `/((?R)?a)/s` | `array{non-falsy-string, non-falsy-string}` | +| `/((?R)?a)/x` | `array{non-falsy-string, non-falsy-string}` | +| `/((?R)?a)/D` | `array{non-falsy-string, non-falsy-string}` | +| `/((?R)?a)/n` | `array{non-falsy-string}` | +| `/((?R)?a)/U` | `array{non-falsy-string, non-falsy-string}` | +| `/((?R)?a)/mD` | `array{non-falsy-string, non-falsy-string}` | +| `/((?R)?a)/su` | `array{non-falsy-string, non-falsy-string}` | +| `/((?R)?a)/xu` | `array{non-falsy-string, non-falsy-string}` | +| `/((?R)?a)/J` | `array{non-falsy-string, non-falsy-string}` | +| `#((?R)?a)#` | `array{non-falsy-string, non-falsy-string}` | +| `~((?R)?a)~` | `array{non-falsy-string, non-falsy-string}` | +| `{((?R)?a)}` | `array{non-falsy-string, non-falsy-string}` | +| `/(a)(?R)?/` | `array{non-falsy-string, 'a'}` | +| `/(a)(?R)?/i` | `array{non-falsy-string, non-falsy-string}` | +| `/(a)(?R)?/u` | `array{non-falsy-string, 'a'}` | +| `/(a)(?R)?/iu` | `array{non-falsy-string, non-falsy-string}` | +| `/(a)(?R)?/m` | `array{non-falsy-string, 'a'}` | +| `/(a)(?R)?/s` | `array{non-falsy-string, 'a'}` | +| `/(a)(?R)?/x` | `array{non-falsy-string, 'a'}` | +| `/(a)(?R)?/D` | `array{non-falsy-string, 'a'}` | +| `/(a)(?R)?/n` | `array{non-falsy-string}` | +| `/(a)(?R)?/U` | `array{non-falsy-string, 'a'}` | +| `/(a)(?R)?/mD` | `array{non-falsy-string, 'a'}` | +| `/(a)(?R)?/su` | `array{non-falsy-string, 'a'}` | +| `/(a)(?R)?/xu` | `array{non-falsy-string, 'a'}` | +| `/(a)(?R)?/J` | `array{non-falsy-string, 'a'}` | +| `#(a)(?R)?#` | `array{non-falsy-string, 'a'}` | +| `~(a)(?R)?~` | `array{non-falsy-string, 'a'}` | +| `{(a)(?R)?}` | `array{non-falsy-string, 'a'}` | +| `/(?a)(?&x)/` | `array{0: 'aa', x: 'a', 1: 'a'}` | +| `/(?a)(?&x)/i` | `array{0: non-falsy-string, x: non-falsy-string, 1: non-falsy-string}` | +| `/(?a)(?&x)/u` | `array{0: 'aa', x: 'a', 1: 'a'}` | +| `/(?a)(?&x)/iu` | `array{0: non-falsy-string, x: non-falsy-string, 1: non-falsy-string}` | +| `/(?a)(?&x)/m` | `array{0: 'aa', x: 'a', 1: 'a'}` | +| `/(?a)(?&x)/s` | `array{0: 'aa', x: 'a', 1: 'a'}` | +| `/(?a)(?&x)/x` | `array{0: 'aa', x: 'a', 1: 'a'}` | +| `/(?a)(?&x)/D` | `array{0: 'aa', x: 'a', 1: 'a'}` | +| `/(?a)(?&x)/n` | `array{0: 'aa', x: 'a', 1: 'a'}` | +| `/(?a)(?&x)/U` | `array{0: 'aa', x: 'a', 1: 'a'}` | +| `/(?a)(?&x)/mD` | `array{0: 'aa', x: 'a', 1: 'a'}` | +| `/(?a)(?&x)/su` | `array{0: 'aa', x: 'a', 1: 'a'}` | +| `/(?a)(?&x)/xu` | `array{0: 'aa', x: 'a', 1: 'a'}` | +| `/(?a)(?&x)/J` | `array{0: 'aa', x: 'a', 1: 'a'}` | +| `#(?a)(?&x)#` | `array{0: 'aa', x: 'a', 1: 'a'}` | +| `~(?a)(?&x)~` | `array{0: 'aa', x: 'a', 1: 'a'}` | +| `{(?a)(?&x)}` | `array{0: 'aa', x: 'a', 1: 'a'}` | +| `/(?P>x)(?Pa)/` | `array{0: 'aa', x: 'a', 1: 'a'}` | +| `/(?P>x)(?Pa)/i` | `array{0: non-falsy-string, x: non-falsy-string, 1: non-falsy-string}` | +| `/(?P>x)(?Pa)/u` | `array{0: 'aa', x: 'a', 1: 'a'}` | +| `/(?P>x)(?Pa)/iu` | `array{0: non-falsy-string, x: non-falsy-string, 1: non-falsy-string}` | +| `/(?P>x)(?Pa)/m` | `array{0: 'aa', x: 'a', 1: 'a'}` | +| `/(?P>x)(?Pa)/s` | `array{0: 'aa', x: 'a', 1: 'a'}` | +| `/(?P>x)(?Pa)/x` | `array{0: 'aa', x: 'a', 1: 'a'}` | +| `/(?P>x)(?Pa)/D` | `array{0: 'aa', x: 'a', 1: 'a'}` | +| `/(?P>x)(?Pa)/n` | `array{0: 'aa', x: 'a', 1: 'a'}` | +| `/(?P>x)(?Pa)/U` | `array{0: 'aa', x: 'a', 1: 'a'}` | +| `/(?P>x)(?Pa)/mD` | `array{0: 'aa', x: 'a', 1: 'a'}` | +| `/(?P>x)(?Pa)/su` | `array{0: 'aa', x: 'a', 1: 'a'}` | +| `/(?P>x)(?Pa)/xu` | `array{0: 'aa', x: 'a', 1: 'a'}` | +| `/(?P>x)(?Pa)/J` | `array{0: 'aa', x: 'a', 1: 'a'}` | +| `#(?P>x)(?Pa)#` | `array{0: 'aa', x: 'a', 1: 'a'}` | +| `~(?P>x)(?Pa)~` | `array{0: 'aa', x: 'a', 1: 'a'}` | +| `{(?P>x)(?Pa)}` | `array{0: 'aa', x: 'a', 1: 'a'}` | +| `/(a)(?-1)/` | `array{'aa', 'a'}` | +| `/(a)(?-1)/i` | `array{non-falsy-string, non-falsy-string}` | +| `/(a)(?-1)/u` | `array{'aa', 'a'}` | +| `/(a)(?-1)/iu` | `array{non-falsy-string, non-falsy-string}` | +| `/(a)(?-1)/m` | `array{'aa', 'a'}` | +| `/(a)(?-1)/s` | `array{'aa', 'a'}` | +| `/(a)(?-1)/x` | `array{'aa', 'a'}` | +| `/(a)(?-1)/D` | `array{'aa', 'a'}` | +| `/(a)(?-1)/U` | `array{'aa', 'a'}` | +| `/(a)(?-1)/mD` | `array{'aa', 'a'}` | +| `/(a)(?-1)/su` | `array{'aa', 'a'}` | +| `/(a)(?-1)/xu` | `array{'aa', 'a'}` | +| `/(a)(?-1)/J` | `array{'aa', 'a'}` | +| `#(a)(?-1)#` | `array{'aa', 'a'}` | +| `~(a)(?-1)~` | `array{'aa', 'a'}` | +| `{(a)(?-1)}` | `array{'aa', 'a'}` | +| `/(\d)(?1)/` | `array{non-falsy-string&numeric-string, numeric-string}` | +| `/(\d)(?1)/i` | `array{non-falsy-string&numeric-string, numeric-string}` | +| `/(\d)(?1)/u` | `array{non-falsy-string, non-empty-string}` | +| `/(\d)(?1)/iu` | `array{non-falsy-string, non-empty-string}` | +| `/(\d)(?1)/m` | `array{non-falsy-string&numeric-string, numeric-string}` | +| `/(\d)(?1)/s` | `array{non-falsy-string&numeric-string, numeric-string}` | +| `/(\d)(?1)/x` | `array{non-falsy-string&numeric-string, numeric-string}` | +| `/(\d)(?1)/D` | `array{non-falsy-string&numeric-string, numeric-string}` | +| `/(\d)(?1)/U` | `array{non-falsy-string&numeric-string, numeric-string}` | +| `/(\d)(?1)/mD` | `array{non-falsy-string&numeric-string, numeric-string}` | +| `/(\d)(?1)/su` | `array{non-falsy-string, non-empty-string}` | +| `/(\d)(?1)/xu` | `array{non-falsy-string, non-empty-string}` | +| `/(\d)(?1)/J` | `array{non-falsy-string&numeric-string, numeric-string}` | +| `#(\d)(?1)#` | `array{non-falsy-string&numeric-string, numeric-string}` | +| `~(\d)(?1)~` | `array{non-falsy-string&numeric-string, numeric-string}` | +| `{(\d)(?1)}` | `array{non-falsy-string&numeric-string, numeric-string}` | +| `/(?:(\d)\|x)(?1)/` | `array{0: lowercase-string&non-falsy-string, 1?: numeric-string}` | +| `/(?:(\d)\|x)(?1)/i` | `array{0: non-falsy-string, 1?: numeric-string}` | +| `/(?:(\d)\|x)(?1)/u` | `array{0: lowercase-string&non-falsy-string, 1?: non-empty-string}` | +| `/(?:(\d)\|x)(?1)/iu` | `array{0: non-falsy-string, 1?: non-empty-string}` | +| `/(?:(\d)\|x)(?1)/m` | `array{0: lowercase-string&non-falsy-string, 1?: numeric-string}` | +| `/(?:(\d)\|x)(?1)/s` | `array{0: lowercase-string&non-falsy-string, 1?: numeric-string}` | +| `/(?:(\d)\|x)(?1)/x` | `array{0: lowercase-string&non-falsy-string, 1?: numeric-string}` | +| `/(?:(\d)\|x)(?1)/D` | `array{0: lowercase-string&non-falsy-string, 1?: numeric-string}` | +| `/(?:(\d)\|x)(?1)/U` | `array{0: lowercase-string&non-falsy-string, 1?: numeric-string}` | +| `/(?:(\d)\|x)(?1)/mD` | `array{0: lowercase-string&non-falsy-string, 1?: numeric-string}` | +| `/(?:(\d)\|x)(?1)/su` | `array{0: lowercase-string&non-falsy-string, 1?: non-empty-string}` | +| `/(?:(\d)\|x)(?1)/xu` | `array{0: lowercase-string&non-falsy-string, 1?: non-empty-string}` | +| `/(?:(\d)\|x)(?1)/J` | `array{0: lowercase-string&non-falsy-string, 1?: numeric-string}` | +| `#(?:(\d)\|x)(?1)#` | `array{0: lowercase-string&non-falsy-string, 1?: numeric-string}` | +| `~(?:(\d)\|x)(?1)~` | `array{0: lowercase-string&non-falsy-string, 1?: numeric-string}` | +| `{(?:(\d)\|x)(?1)}` | `array{0: lowercase-string&non-falsy-string, 1?: numeric-string}` | +| `/(a)\1(b)/J` | `array{non-falsy-string, 'a', 'b'}` | +| `/(?=(a))/J` | `array{'', 'a'}` | +| `/(?=(a))a/J` | `array{'a', 'a'}` | +| `/(?=(\d))\w/J` | `array{non-empty-string, numeric-string}` | +| `/(?!(a))b/J` | `array{'b'}` | +| `/(?<=(a))b/J` | `array{'b', 'a'}` | +| `/(?(a)\|b)/J` | `array{0: 'a'\|'b', 1?: 'a'}` | +| `/(?>a\|(b))c/J` | `array{0: 'ac'\|'bc', 1?: 'b'}` | +| `/(?\|(?>(a))\|b)/` | `array{0: 'a'\|'b', 1?: 'a'}` | +| `/(?\|(?>(a))\|b)/i` | `array{0: non-falsy-string, 1?: non-falsy-string}` | +| `/(?\|(?>(a))\|b)/u` | `array{0: 'a'\|'b', 1?: 'a'}` | +| `/(?\|(?>(a))\|b)/iu` | `array{0: non-falsy-string, 1?: non-falsy-string}` | +| `/(?\|(?>(a))\|b)/m` | `array{0: 'a'\|'b', 1?: 'a'}` | +| `/(?\|(?>(a))\|b)/s` | `array{0: 'a'\|'b', 1?: 'a'}` | +| `/(?\|(?>(a))\|b)/x` | `array{0: 'a'\|'b', 1?: 'a'}` | +| `/(?\|(?>(a))\|b)/D` | `array{0: 'a'\|'b', 1?: 'a'}` | +| `/(?\|(?>(a))\|b)/U` | `array{0: 'a'\|'b', 1?: 'a'}` | +| `/(?\|(?>(a))\|b)/mD` | `array{0: 'a'\|'b', 1?: 'a'}` | +| `/(?\|(?>(a))\|b)/su` | `array{0: 'a'\|'b', 1?: 'a'}` | +| `/(?\|(?>(a))\|b)/xu` | `array{0: 'a'\|'b', 1?: 'a'}` | +| `/(?\|(?>(a))\|b)/J` | `array{0: 'a'\|'b', 1?: 'a'}` | +| `#(?\|(?>(a))\|b)#` | `array{0: 'a'\|'b', 1?: 'a'}` | +| `~(?\|(?>(a))\|b)~` | `array{0: 'a'\|'b', 1?: 'a'}` | +| `{(?\|(?>(a))\|b)}` | `array{0: 'a'\|'b', 1?: 'a'}` | +| `/(*UTF)(\d)/` | `array{numeric-string, numeric-string}` | +| `/(*UTF)(\d)/i` | `array{numeric-string, numeric-string}` | +| `/(*UTF)(\d)/u` | `array{non-empty-string, non-empty-string}` | +| `/(*UTF)(\d)/iu` | `array{non-empty-string, non-empty-string}` | +| `/(*UTF)(\d)/m` | `array{numeric-string, numeric-string}` | +| `/(*UTF)(\d)/s` | `array{numeric-string, numeric-string}` | +| `/(*UTF)(\d)/x` | `array{numeric-string, numeric-string}` | +| `/(*UTF)(\d)/D` | `array{numeric-string, numeric-string}` | +| `/(*UTF)(\d)/n` | `array{numeric-string}` | +| `/(*UTF)(\d)/U` | `array{numeric-string, numeric-string}` | +| `/(*UTF)(\d)/mD` | `array{numeric-string, numeric-string}` | +| `/(*UTF)(\d)/su` | `array{non-empty-string, non-empty-string}` | +| `/(*UTF)(\d)/xu` | `array{non-empty-string, non-empty-string}` | +| `/(*UTF)(\d)/J` | `array{numeric-string, numeric-string}` | +| `#(*UTF)(\d)#` | `array{numeric-string, numeric-string}` | +| `~(*UTF)(\d)~` | `array{numeric-string, numeric-string}` | +| `{(*UTF)(\d)}` | `array{numeric-string, numeric-string}` | +| `/(*UCP)(\d)/` | `array{non-empty-string, non-empty-string}` | +| `/(*UCP)(\d)/i` | `array{non-empty-string, non-empty-string}` | +| `/(*UCP)(\d)/u` | `array{non-empty-string, non-empty-string}` | +| `/(*UCP)(\d)/iu` | `array{non-empty-string, non-empty-string}` | +| `/(*UCP)(\d)/m` | `array{non-empty-string, non-empty-string}` | +| `/(*UCP)(\d)/s` | `array{non-empty-string, non-empty-string}` | +| `/(*UCP)(\d)/x` | `array{non-empty-string, non-empty-string}` | +| `/(*UCP)(\d)/D` | `array{non-empty-string, non-empty-string}` | +| `/(*UCP)(\d)/n` | `array{non-empty-string}` | +| `/(*UCP)(\d)/U` | `array{non-empty-string, non-empty-string}` | +| `/(*UCP)(\d)/mD` | `array{non-empty-string, non-empty-string}` | +| `/(*UCP)(\d)/su` | `array{non-empty-string, non-empty-string}` | +| `/(*UCP)(\d)/xu` | `array{non-empty-string, non-empty-string}` | +| `/(*UCP)(\d)/J` | `array{non-empty-string, non-empty-string}` | +| `#(*UCP)(\d)#` | `array{non-empty-string, non-empty-string}` | +| `~(*UCP)(\d)~` | `array{non-empty-string, non-empty-string}` | +| `{(*UCP)(\d)}` | `array{non-empty-string, non-empty-string}` | +| `/(*UTF)(*UCP)(\d+)/` | `array{non-empty-string, non-empty-string}` | +| `/(*UTF)(*UCP)(\d+)/i` | `array{non-empty-string, non-empty-string}` | +| `/(*UTF)(*UCP)(\d+)/u` | `array{non-empty-string, non-empty-string}` | +| `/(*UTF)(*UCP)(\d+)/iu` | `array{non-empty-string, non-empty-string}` | +| `/(*UTF)(*UCP)(\d+)/m` | `array{non-empty-string, non-empty-string}` | +| `/(*UTF)(*UCP)(\d+)/s` | `array{non-empty-string, non-empty-string}` | +| `/(*UTF)(*UCP)(\d+)/x` | `array{non-empty-string, non-empty-string}` | +| `/(*UTF)(*UCP)(\d+)/D` | `array{non-empty-string, non-empty-string}` | +| `/(*UTF)(*UCP)(\d+)/n` | `array{non-empty-string}` | +| `/(*UTF)(*UCP)(\d+)/U` | `array{non-empty-string, non-empty-string}` | +| `/(*UTF)(*UCP)(\d+)/mD` | `array{non-empty-string, non-empty-string}` | +| `/(*UTF)(*UCP)(\d+)/su` | `array{non-empty-string, non-empty-string}` | +| `/(*UTF)(*UCP)(\d+)/xu` | `array{non-empty-string, non-empty-string}` | +| `/(*UTF)(*UCP)(\d+)/J` | `array{non-empty-string, non-empty-string}` | +| `#(*UTF)(*UCP)(\d+)#` | `array{non-empty-string, non-empty-string}` | +| `~(*UTF)(*UCP)(\d+)~` | `array{non-empty-string, non-empty-string}` | +| `{(*UTF)(*UCP)(\d+)}` | `array{non-empty-string, non-empty-string}` | +| `/(*UCP)(\w)/` | `array{non-empty-string, non-empty-string}` | +| `/(*UCP)(\w)/i` | `array{non-empty-string, non-empty-string}` | +| `/(*UCP)(\w)/u` | `array{non-empty-string, non-empty-string}` | +| `/(*UCP)(\w)/iu` | `array{non-empty-string, non-empty-string}` | +| `/(*UCP)(\w)/m` | `array{non-empty-string, non-empty-string}` | +| `/(*UCP)(\w)/s` | `array{non-empty-string, non-empty-string}` | +| `/(*UCP)(\w)/x` | `array{non-empty-string, non-empty-string}` | +| `/(*UCP)(\w)/D` | `array{non-empty-string, non-empty-string}` | +| `/(*UCP)(\w)/n` | `array{non-empty-string}` | +| `/(*UCP)(\w)/U` | `array{non-empty-string, non-empty-string}` | +| `/(*UCP)(\w)/mD` | `array{non-empty-string, non-empty-string}` | +| `/(*UCP)(\w)/su` | `array{non-empty-string, non-empty-string}` | +| `/(*UCP)(\w)/xu` | `array{non-empty-string, non-empty-string}` | +| `/(*UCP)(\w)/J` | `array{non-empty-string, non-empty-string}` | +| `#(*UCP)(\w)#` | `array{non-empty-string, non-empty-string}` | +| `~(*UCP)(\w)~` | `array{non-empty-string, non-empty-string}` | +| `{(*UCP)(\w)}` | `array{non-empty-string, non-empty-string}` | +| `/(*UCP)([[:digit:]])/` | `array{non-empty-string, non-empty-string}` | +| `/(*UCP)([[:digit:]])/i` | `array{non-empty-string, non-empty-string}` | +| `/(*UCP)([[:digit:]])/u` | `array{non-empty-string, non-empty-string}` | +| `/(*UCP)([[:digit:]])/iu` | `array{non-empty-string, non-empty-string}` | +| `/(*UCP)([[:digit:]])/m` | `array{non-empty-string, non-empty-string}` | +| `/(*UCP)([[:digit:]])/s` | `array{non-empty-string, non-empty-string}` | +| `/(*UCP)([[:digit:]])/x` | `array{non-empty-string, non-empty-string}` | +| `/(*UCP)([[:digit:]])/D` | `array{non-empty-string, non-empty-string}` | +| `/(*UCP)([[:digit:]])/n` | `array{non-empty-string}` | +| `/(*UCP)([[:digit:]])/U` | `array{non-empty-string, non-empty-string}` | +| `/(*UCP)([[:digit:]])/mD` | `array{non-empty-string, non-empty-string}` | +| `/(*UCP)([[:digit:]])/su` | `array{non-empty-string, non-empty-string}` | +| `/(*UCP)([[:digit:]])/xu` | `array{non-empty-string, non-empty-string}` | +| `/(*UCP)([[:digit:]])/J` | `array{non-empty-string, non-empty-string}` | +| `#(*UCP)([[:digit:]])#` | `array{non-empty-string, non-empty-string}` | +| `~(*UCP)([[:digit:]])~` | `array{non-empty-string, non-empty-string}` | +| `{(*UCP)([[:digit:]])}` | `array{non-empty-string, non-empty-string}` | +| `/(*UTF)(.)/` | `array{non-empty-string, non-empty-string}` | +| `/(*UTF)(.)/i` | `array{non-empty-string, non-empty-string}` | +| `/(*UTF)(.)/u` | `array{non-empty-string, non-empty-string}` | +| `/(*UTF)(.)/iu` | `array{non-empty-string, non-empty-string}` | +| `/(*UTF)(.)/m` | `array{non-empty-string, non-empty-string}` | +| `/(*UTF)(.)/s` | `array{non-empty-string, non-empty-string}` | +| `/(*UTF)(.)/x` | `array{non-empty-string, non-empty-string}` | +| `/(*UTF)(.)/D` | `array{non-empty-string, non-empty-string}` | +| `/(*UTF)(.)/n` | `array{non-empty-string}` | +| `/(*UTF)(.)/U` | `array{non-empty-string, non-empty-string}` | +| `/(*UTF)(.)/mD` | `array{non-empty-string, non-empty-string}` | +| `/(*UTF)(.)/su` | `array{non-empty-string, non-empty-string}` | +| `/(*UTF)(.)/xu` | `array{non-empty-string, non-empty-string}` | +| `/(*UTF)(.)/J` | `array{non-empty-string, non-empty-string}` | +| `#(*UTF)(.)#` | `array{non-empty-string, non-empty-string}` | +| `~(*UTF)(.)~` | `array{non-empty-string, non-empty-string}` | +| `{(*UTF)(.)}` | `array{non-empty-string, non-empty-string}` | +| `/(*UTF8)(.)/` | `array{non-empty-string, non-empty-string}` | +| `/(*UTF8)(.)/i` | `array{non-empty-string, non-empty-string}` | +| `/(*UTF8)(.)/u` | `array{non-empty-string, non-empty-string}` | +| `/(*UTF8)(.)/iu` | `array{non-empty-string, non-empty-string}` | +| `/(*UTF8)(.)/m` | `array{non-empty-string, non-empty-string}` | +| `/(*UTF8)(.)/s` | `array{non-empty-string, non-empty-string}` | +| `/(*UTF8)(.)/x` | `array{non-empty-string, non-empty-string}` | +| `/(*UTF8)(.)/D` | `array{non-empty-string, non-empty-string}` | +| `/(*UTF8)(.)/n` | `array{non-empty-string}` | +| `/(*UTF8)(.)/U` | `array{non-empty-string, non-empty-string}` | +| `/(*UTF8)(.)/mD` | `array{non-empty-string, non-empty-string}` | +| `/(*UTF8)(.)/su` | `array{non-empty-string, non-empty-string}` | +| `/(*UTF8)(.)/xu` | `array{non-empty-string, non-empty-string}` | +| `/(*UTF8)(.)/J` | `array{non-empty-string, non-empty-string}` | +| `#(*UTF8)(.)#` | `array{non-empty-string, non-empty-string}` | +| `~(*UTF8)(.)~` | `array{non-empty-string, non-empty-string}` | +| `{(*UTF8)(.)}` | `array{non-empty-string, non-empty-string}` | +| `/(*NO_START_OPT)(a)/` | `array{'a', 'a'}` | +| `/(*NO_START_OPT)(a)/i` | `array{non-falsy-string, non-falsy-string}` | +| `/(*NO_START_OPT)(a)/u` | `array{'a', 'a'}` | +| `/(*NO_START_OPT)(a)/iu` | `array{non-falsy-string, non-falsy-string}` | +| `/(*NO_START_OPT)(a)/m` | `array{'a', 'a'}` | +| `/(*NO_START_OPT)(a)/s` | `array{'a', 'a'}` | +| `/(*NO_START_OPT)(a)/x` | `array{'a', 'a'}` | +| `/(*NO_START_OPT)(a)/D` | `array{'a', 'a'}` | +| `/(*NO_START_OPT)(a)/n` | `array{'a'}` | +| `/(*NO_START_OPT)(a)/U` | `array{'a', 'a'}` | +| `/(*NO_START_OPT)(a)/mD` | `array{'a', 'a'}` | +| `/(*NO_START_OPT)(a)/su` | `array{'a', 'a'}` | +| `/(*NO_START_OPT)(a)/xu` | `array{'a', 'a'}` | +| `/(*NO_START_OPT)(a)/J` | `array{'a', 'a'}` | +| `#(*NO_START_OPT)(a)#` | `array{'a', 'a'}` | +| `~(*NO_START_OPT)(a)~` | `array{'a', 'a'}` | +| `{(*NO_START_OPT)(a)}` | `array{'a', 'a'}` | +| `/(*CR)(a)$/` | `array{'a', 'a'}` | +| `/(*CR)(a)$/i` | `array{non-falsy-string, non-falsy-string}` | +| `/(*CR)(a)$/u` | `array{'a', 'a'}` | +| `/(*CR)(a)$/iu` | `array{non-falsy-string, non-falsy-string}` | +| `/(*CR)(a)$/m` | `array{'a', 'a'}` | +| `/(*CR)(a)$/s` | `array{'a', 'a'}` | +| `/(*CR)(a)$/x` | `array{'a', 'a'}` | +| `/(*CR)(a)$/D` | `array{'a', 'a'}` | +| `/(*CR)(a)$/n` | `array{'a'}` | +| `/(*CR)(a)$/U` | `array{'a', 'a'}` | +| `/(*CR)(a)$/mD` | `array{'a', 'a'}` | +| `/(*CR)(a)$/su` | `array{'a', 'a'}` | +| `/(*CR)(a)$/xu` | `array{'a', 'a'}` | +| `/(*CR)(a)$/J` | `array{'a', 'a'}` | +| `#(*CR)(a)$#` | `array{'a', 'a'}` | +| `~(*CR)(a)$~` | `array{'a', 'a'}` | +| `{(*CR)(a)$}` | `array{'a', 'a'}` | +| `/(*LF)(a)$/` | `array{'a', 'a'}` | +| `/(*LF)(a)$/i` | `array{non-falsy-string, non-falsy-string}` | +| `/(*LF)(a)$/u` | `array{'a', 'a'}` | +| `/(*LF)(a)$/iu` | `array{non-falsy-string, non-falsy-string}` | +| `/(*LF)(a)$/m` | `array{'a', 'a'}` | +| `/(*LF)(a)$/s` | `array{'a', 'a'}` | +| `/(*LF)(a)$/x` | `array{'a', 'a'}` | +| `/(*LF)(a)$/D` | `array{'a', 'a'}` | +| `/(*LF)(a)$/n` | `array{'a'}` | +| `/(*LF)(a)$/U` | `array{'a', 'a'}` | +| `/(*LF)(a)$/mD` | `array{'a', 'a'}` | +| `/(*LF)(a)$/su` | `array{'a', 'a'}` | +| `/(*LF)(a)$/xu` | `array{'a', 'a'}` | +| `/(*LF)(a)$/J` | `array{'a', 'a'}` | +| `#(*LF)(a)$#` | `array{'a', 'a'}` | +| `~(*LF)(a)$~` | `array{'a', 'a'}` | +| `{(*LF)(a)$}` | `array{'a', 'a'}` | +| `/(*ANYCRLF)(a)$/` | `array{'a', 'a'}` | +| `/(*ANYCRLF)(a)$/i` | `array{non-falsy-string, non-falsy-string}` | +| `/(*ANYCRLF)(a)$/u` | `array{'a', 'a'}` | +| `/(*ANYCRLF)(a)$/iu` | `array{non-falsy-string, non-falsy-string}` | +| `/(*ANYCRLF)(a)$/m` | `array{'a', 'a'}` | +| `/(*ANYCRLF)(a)$/s` | `array{'a', 'a'}` | +| `/(*ANYCRLF)(a)$/x` | `array{'a', 'a'}` | +| `/(*ANYCRLF)(a)$/D` | `array{'a', 'a'}` | +| `/(*ANYCRLF)(a)$/n` | `array{'a'}` | +| `/(*ANYCRLF)(a)$/U` | `array{'a', 'a'}` | +| `/(*ANYCRLF)(a)$/mD` | `array{'a', 'a'}` | +| `/(*ANYCRLF)(a)$/su` | `array{'a', 'a'}` | +| `/(*ANYCRLF)(a)$/xu` | `array{'a', 'a'}` | +| `/(*ANYCRLF)(a)$/J` | `array{'a', 'a'}` | +| `#(*ANYCRLF)(a)$#` | `array{'a', 'a'}` | +| `~(*ANYCRLF)(a)$~` | `array{'a', 'a'}` | +| `{(*ANYCRLF)(a)$}` | `array{'a', 'a'}` | +| `/(*NOTEMPTY)(a?)/` | `array{''\|'a', ''\|'a'}` | +| `/(*NOTEMPTY)(a?)/i` | `array{string, string}` | +| `/(*NOTEMPTY)(a?)/u` | `array{''\|'a', ''\|'a'}` | +| `/(*NOTEMPTY)(a?)/iu` | `array{string, string}` | +| `/(*NOTEMPTY)(a?)/m` | `array{''\|'a', ''\|'a'}` | +| `/(*NOTEMPTY)(a?)/s` | `array{''\|'a', ''\|'a'}` | +| `/(*NOTEMPTY)(a?)/x` | `array{''\|'a', ''\|'a'}` | +| `/(*NOTEMPTY)(a?)/D` | `array{''\|'a', ''\|'a'}` | +| `/(*NOTEMPTY)(a?)/n` | `array{''\|'a'}` | +| `/(*NOTEMPTY)(a?)/U` | `array{''\|'a', ''\|'a'}` | +| `/(*NOTEMPTY)(a?)/mD` | `array{''\|'a', ''\|'a'}` | +| `/(*NOTEMPTY)(a?)/su` | `array{''\|'a', ''\|'a'}` | +| `/(*NOTEMPTY)(a?)/xu` | `array{''\|'a', ''\|'a'}` | +| `/(*NOTEMPTY)(a?)/J` | `array{''\|'a', ''\|'a'}` | +| `#(*NOTEMPTY)(a?)#` | `array{''\|'a', ''\|'a'}` | +| `~(*NOTEMPTY)(a?)~` | `array{''\|'a', ''\|'a'}` | +| `{(*NOTEMPTY)(a?)}` | `array{''\|'a', ''\|'a'}` | +| `/(*LIMIT_MATCH=10)(a)/` | `array{lowercase-string&non-falsy-string, 'a'}` | +| `/(*LIMIT_MATCH=10)(a)/i` | `array{non-falsy-string, non-falsy-string}` | +| `/(*LIMIT_MATCH=10)(a)/u` | `array{lowercase-string&non-falsy-string, 'a'}` | +| `/(*LIMIT_MATCH=10)(a)/iu` | `array{non-falsy-string, non-falsy-string}` | +| `/(*LIMIT_MATCH=10)(a)/m` | `array{lowercase-string&non-falsy-string, 'a'}` | +| `/(*LIMIT_MATCH=10)(a)/s` | `array{lowercase-string&non-falsy-string, 'a'}` | +| `/(*LIMIT_MATCH=10)(a)/x` | `array{lowercase-string&non-falsy-string, 'a'}` | +| `/(*LIMIT_MATCH=10)(a)/D` | `array{lowercase-string&non-falsy-string, 'a'}` | +| `/(*LIMIT_MATCH=10)(a)/n` | `array{lowercase-string&non-falsy-string}` | +| `/(*LIMIT_MATCH=10)(a)/U` | `array{lowercase-string&non-falsy-string, 'a'}` | +| `/(*LIMIT_MATCH=10)(a)/mD` | `array{lowercase-string&non-falsy-string, 'a'}` | +| `/(*LIMIT_MATCH=10)(a)/su` | `array{lowercase-string&non-falsy-string, 'a'}` | +| `/(*LIMIT_MATCH=10)(a)/xu` | `array{lowercase-string&non-falsy-string, 'a'}` | +| `/(*LIMIT_MATCH=10)(a)/J` | `array{lowercase-string&non-falsy-string, 'a'}` | +| `#(*LIMIT_MATCH=10)(a)#` | `array{lowercase-string&non-falsy-string, 'a'}` | +| `~(*LIMIT_MATCH=10)(a)~` | `array{lowercase-string&non-falsy-string, 'a'}` | +| `{(*LIMIT_MATCH=10)(a)}` | `array{lowercase-string&non-falsy-string, 'a'}` | +| `/(?s)(.)/J` | `array{non-empty-string, non-empty-string}` | +| `/(?m)^(a)$/J` | `array{'a', 'a'}` | +| `/(?x)(\d +)/J` | `array{numeric-string, numeric-string}` | +| `/(?U)(a+)/J` | `array{lowercase-string&non-falsy-string, lowercase-string&non-falsy-string}` | +| `/(?U)(a+?)/J` | `array{lowercase-string&non-falsy-string, lowercase-string&non-falsy-string}` | +| `/(?i)(k)/J` | `array{non-falsy-string, non-falsy-string}` | +| `/(?r)(k)/` | `array{'k', 'k'}` | +| `/(?r)(k)/i` | `array{non-falsy-string, non-falsy-string}` | +| `/(?r)(k)/u` | `array{'k', 'k'}` | +| `/(?r)(k)/iu` | `array{non-falsy-string, non-falsy-string}` | +| `/(?r)(k)/m` | `array{'k', 'k'}` | +| `/(?r)(k)/s` | `array{'k', 'k'}` | +| `/(?r)(k)/x` | `array{'k', 'k'}` | +| `/(?r)(k)/D` | `array{'k', 'k'}` | +| `/(?r)(k)/n` | `array{'k'}` | +| `/(?r)(k)/U` | `array{'k', 'k'}` | +| `/(?r)(k)/mD` | `array{'k', 'k'}` | +| `/(?r)(k)/su` | `array{'k', 'k'}` | +| `/(?r)(k)/xu` | `array{'k', 'k'}` | +| `/(?r)(k)/J` | `array{'k', 'k'}` | +| `#(?r)(k)#` | `array{'k', 'k'}` | +| `~(?r)(k)~` | `array{'k', 'k'}` | +| `{(?r)(k)}` | `array{'k', 'k'}` | +| `/(?ir)(k)/` | `array{non-falsy-string, non-falsy-string}` | +| `/(?ir)(k)/i` | `array{non-falsy-string, non-falsy-string}` | +| `/(?ir)(k)/u` | `array{non-falsy-string, non-falsy-string}` | +| `/(?ir)(k)/iu` | `array{non-falsy-string, non-falsy-string}` | +| `/(?ir)(k)/m` | `array{non-falsy-string, non-falsy-string}` | +| `/(?ir)(k)/s` | `array{non-falsy-string, non-falsy-string}` | +| `/(?ir)(k)/x` | `array{non-falsy-string, non-falsy-string}` | +| `/(?ir)(k)/D` | `array{non-falsy-string, non-falsy-string}` | +| `/(?ir)(k)/n` | `array{non-falsy-string}` | +| `/(?ir)(k)/U` | `array{non-falsy-string, non-falsy-string}` | +| `/(?ir)(k)/mD` | `array{non-falsy-string, non-falsy-string}` | +| `/(?ir)(k)/su` | `array{non-falsy-string, non-falsy-string}` | +| `/(?ir)(k)/xu` | `array{non-falsy-string, non-falsy-string}` | +| `/(?ir)(k)/J` | `array{non-falsy-string, non-falsy-string}` | +| `#(?ir)(k)#` | `array{non-falsy-string, non-falsy-string}` | +| `~(?ir)(k)~` | `array{non-falsy-string, non-falsy-string}` | +| `{(?ir)(k)}` | `array{non-falsy-string, non-falsy-string}` | +| `/(?ia)(\d)/` | `array{numeric-string, numeric-string}` | +| `/(?ia)(\d)/i` | `array{numeric-string, numeric-string}` | +| `/(?ia)(\d)/u` | `array{non-empty-string, non-empty-string}` | +| `/(?ia)(\d)/iu` | `array{non-empty-string, non-empty-string}` | +| `/(?ia)(\d)/m` | `array{numeric-string, numeric-string}` | +| `/(?ia)(\d)/s` | `array{numeric-string, numeric-string}` | +| `/(?ia)(\d)/x` | `array{numeric-string, numeric-string}` | +| `/(?ia)(\d)/D` | `array{numeric-string, numeric-string}` | +| `/(?ia)(\d)/n` | `array{numeric-string}` | +| `/(?ia)(\d)/U` | `array{numeric-string, numeric-string}` | +| `/(?ia)(\d)/mD` | `array{numeric-string, numeric-string}` | +| `/(?ia)(\d)/su` | `array{non-empty-string, non-empty-string}` | +| `/(?ia)(\d)/xu` | `array{non-empty-string, non-empty-string}` | +| `/(?ia)(\d)/J` | `array{numeric-string, numeric-string}` | +| `#(?ia)(\d)#` | `array{numeric-string, numeric-string}` | +| `~(?ia)(\d)~` | `array{numeric-string, numeric-string}` | +| `{(?ia)(\d)}` | `array{numeric-string, numeric-string}` | +| `/(?aD)(\d)/` | `array{numeric-string, numeric-string}` | +| `/(?aD)(\d)/i` | `array{numeric-string, numeric-string}` | +| `/(?aD)(\d)/u` | `array{non-empty-string, non-empty-string}` | +| `/(?aD)(\d)/iu` | `array{non-empty-string, non-empty-string}` | +| `/(?aD)(\d)/m` | `array{numeric-string, numeric-string}` | +| `/(?aD)(\d)/s` | `array{numeric-string, numeric-string}` | +| `/(?aD)(\d)/x` | `array{numeric-string, numeric-string}` | +| `/(?aD)(\d)/D` | `array{numeric-string, numeric-string}` | +| `/(?aD)(\d)/n` | `array{numeric-string}` | +| `/(?aD)(\d)/U` | `array{numeric-string, numeric-string}` | +| `/(?aD)(\d)/mD` | `array{numeric-string, numeric-string}` | +| `/(?aD)(\d)/su` | `array{non-empty-string, non-empty-string}` | +| `/(?aD)(\d)/xu` | `array{non-empty-string, non-empty-string}` | +| `/(?aD)(\d)/J` | `array{numeric-string, numeric-string}` | +| `#(?aD)(\d)#` | `array{numeric-string, numeric-string}` | +| `~(?aD)(\d)~` | `array{numeric-string, numeric-string}` | +| `{(?aD)(\d)}` | `array{numeric-string, numeric-string}` | +| `/(?aP)(\w)/` | `array{non-empty-string, non-empty-string}` | +| `/(?aP)(\w)/i` | `array{non-empty-string, non-empty-string}` | +| `/(?aP)(\w)/u` | `array{non-empty-string, non-empty-string}` | +| `/(?aP)(\w)/iu` | `array{non-empty-string, non-empty-string}` | +| `/(?aP)(\w)/m` | `array{non-empty-string, non-empty-string}` | +| `/(?aP)(\w)/s` | `array{non-empty-string, non-empty-string}` | +| `/(?aP)(\w)/x` | `array{non-empty-string, non-empty-string}` | +| `/(?aP)(\w)/D` | `array{non-empty-string, non-empty-string}` | +| `/(?aP)(\w)/n` | `array{non-empty-string}` | +| `/(?aP)(\w)/U` | `array{non-empty-string, non-empty-string}` | +| `/(?aP)(\w)/mD` | `array{non-empty-string, non-empty-string}` | +| `/(?aP)(\w)/su` | `array{non-empty-string, non-empty-string}` | +| `/(?aP)(\w)/xu` | `array{non-empty-string, non-empty-string}` | +| `/(?aP)(\w)/J` | `array{non-empty-string, non-empty-string}` | +| `#(?aP)(\w)#` | `array{non-empty-string, non-empty-string}` | +| `~(?aP)(\w)~` | `array{non-empty-string, non-empty-string}` | +| `{(?aP)(\w)}` | `array{non-empty-string, non-empty-string}` | +| `/(?aT)([[:digit:]])/` | `array{numeric-string, numeric-string}` | +| `/(?aT)([[:digit:]])/i` | `array{numeric-string, numeric-string}` | +| `/(?aT)([[:digit:]])/u` | `array{non-empty-string, non-empty-string}` | +| `/(?aT)([[:digit:]])/iu` | `array{non-empty-string, non-empty-string}` | +| `/(?aT)([[:digit:]])/m` | `array{numeric-string, numeric-string}` | +| `/(?aT)([[:digit:]])/s` | `array{numeric-string, numeric-string}` | +| `/(?aT)([[:digit:]])/x` | `array{numeric-string, numeric-string}` | +| `/(?aT)([[:digit:]])/D` | `array{numeric-string, numeric-string}` | +| `/(?aT)([[:digit:]])/n` | `array{numeric-string}` | +| `/(?aT)([[:digit:]])/U` | `array{numeric-string, numeric-string}` | +| `/(?aT)([[:digit:]])/mD` | `array{numeric-string, numeric-string}` | +| `/(?aT)([[:digit:]])/su` | `array{non-empty-string, non-empty-string}` | +| `/(?aT)([[:digit:]])/xu` | `array{non-empty-string, non-empty-string}` | +| `/(?aT)([[:digit:]])/J` | `array{numeric-string, numeric-string}` | +| `#(?aT)([[:digit:]])#` | `array{numeric-string, numeric-string}` | +| `~(?aT)([[:digit:]])~` | `array{numeric-string, numeric-string}` | +| `{(?aT)([[:digit:]])}` | `array{numeric-string, numeric-string}` | +| `/(\N)/J` | `array{non-empty-string, non-empty-string}` | +| `/(\N+)/J` | `array{non-empty-string, non-empty-string}` | +| `/(\p{L})/J` | `array{non-empty-string, non-empty-string}` | +| `/(\pL+)/J` | `array{non-empty-string, non-empty-string}` | +| `/(\p{Xan})/J` | `array{non-empty-string, non-empty-string}` | +| `/(\p{Xwd})/J` | `array{non-empty-string, non-empty-string}` | +| `/(\p{Xps})/J` | `array{non-empty-string, non-empty-string}` | +| `/(\p{Any})/J` | `array{non-empty-string, non-empty-string}` | +| `/(\v)/J` | `array{non-falsy-string, non-falsy-string}` | diff --git a/poc/witnesses/corpus.json b/poc/witnesses/corpus.json new file mode 100644 index 00000000000..602813d16cc --- /dev/null +++ b/poc/witnesses/corpus.json @@ -0,0 +1,212 @@ +[ + { + "pattern": "/^(?=.{1,253}$)([a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?)(\\.[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?)*$/i", + "call": "match:null", + "subject": "MC5hLnp6LWktMC0wMC0wMHNoeS0wMC0wMC0tLS16bC4waDBnLTAtbTAwMC0tczAwLWEwZHk=", + "engine": "YTo1OntpOjA7czo1MzoiMC5hLnp6LWktMC0wMC0wMHNoeS0wMC0wMC0tLS16bC4waDBnLTAtbTAwMC0tczAwLWEwZHkiO2k6MTtzOjE6IjAiO2k6MjtzOjA6IiI7aTozO3M6MjI6Ii4waDBnLTAtbTAwMC0tczAwLWEwZHkiO2k6NDtzOjIwOiJoMGctMC1tMDAwLS1zMDAtYTBkeSI7fQ==", + "reason": "falsy", + "key": 1, + "phplrt": "list{0: non-falsy-string, 1: non-falsy-string, 2?: string, 3?: string, 4?: non-falsy-string}", + "phpregex": "list{0: non-empty-string, 1: non-empty-string, 2?: string, 3?: string, 4?: non-empty-string}" + }, + { + "pattern": "~^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*\\.[a-z]{2,}$~i", + "call": "match:null", + "subject": "MGpyci4wLmYwLTAuMDAuZXVn", + "engine": "YTo0OntpOjA7czoxODoiMGpyci4wLmYwLTAuMDAuZXVnIjtpOjE7czozOiJqcnIiO2k6MjtzOjM6Ii4wMCI7aTozO3M6MToiMCI7fQ==", + "reason": "falsy", + "key": 3, + "phplrt": "list{0: non-falsy-string, 1?: string, 2?: string, 3?: non-falsy-string}", + "phpregex": "list{0: non-falsy-string, 1?: string, 2?: string, 3?: non-empty-string}" + }, + { + "pattern": "/^[a-z]([a-zA-Z0-9_-]*[a-zA-Z0-9])*:[a-z]([a-zA-Z0-9_-]*[a-zA-Z0-9])*$/", + "call": "match:null", + "subject": "Y3QwMDA6aTA=", + "engine": "YTozOntpOjA7czo4OiJjdDAwMDppMCI7aToxO3M6NDoidDAwMCI7aToyO3M6MToiMCI7fQ==", + "reason": "falsy", + "key": 2, + "phplrt": "list{0: non-falsy-string, 1?: string, 2?: non-falsy-string}", + "phpregex": "list{0: non-falsy-string, 1?: string, 2?: non-empty-string}" + }, + { + "pattern": "/^(((?!-))(xn--|_)?[a-z0-9-]{0,61}[a-z0-9]{1,1}\\.)*(xn--)?([a-z0-9][a-z0-9\\-]{0,60}|[a-z0-9-]{1,30}\\.[a-z]{2,})$/", + "call": "match:null", + "subject": "MA==", + "engine": "YTo2OntpOjA7czoxOiIwIjtpOjE7czowOiIiO2k6MjtzOjA6IiI7aTozO3M6MDoiIjtpOjQ7czowOiIiO2k6NTtzOjE6IjAiO30=", + "reason": "falsy", + "key": 5, + "phplrt": "array{non-empty-string, string, string, string, ''|'xn--', non-falsy-string}", + "phpregex": "array{lowercase-string&non-empty-string, lowercase-string, '', ''|'_'|'xn--', ''|'xn--', lowercase-string&non-empty-string}" + }, + { + "pattern": "/^([a-z\\d](-*[a-z\\d])*)(\\.([a-z\\d](-*[a-z\\d])*))*\\.?$/i", + "call": "match:null", + "subject": "aC0tMC0wLmYtbG0tYy4wLS0tMC0tMDA=", + "engine": "YTo2OntpOjA7czoyMzoiaC0tMC0wLmYtbG0tYy4wLS0tMC0tMDAiO2k6MTtzOjY6ImgtLTAtMCI7aToyO3M6MjoiLTAiO2k6MztzOjEwOiIuMC0tLTAtLTAwIjtpOjQ7czo5OiIwLS0tMC0tMDAiO2k6NTtzOjE6IjAiO30=", + "reason": "falsy", + "key": 5, + "phplrt": "list{0: non-falsy-string, 1: non-falsy-string, 2?: string, 3?: string, 4?: string, 5?: non-falsy-string}", + "phpregex": "list{0: non-empty-string, 1: non-empty-string, 2?: string, 3?: string, 4?: string, 5?: non-empty-string}" + }, + { + "pattern": "#(?:[^\\p{L}\\p{N}*\"()]|^)([+\\-|]?(?:[\\p{L}\\p{N}*\"()]+'?)*[\\p{L}\\p{N}*\"()])(?:[^\\p{L}\\p{N}*\"()]|$)#u", + "call": "match:null", + "subject": "MDotTjAjTiM=", + "engine": "YToyOntpOjA7czoyOiIwOiI7aToxO3M6MToiMCI7fQ==", + "reason": "falsy", + "key": 1, + "phplrt": "array{non-falsy-string, non-falsy-string}", + "phpregex": "array{non-empty-string, non-empty-string}" + }, + { + "pattern": "#(?:[^\\p{L}\\p{N}*]|^)([+\\-|]?(?:[\\p{L}\\p{N}*]+'?)*[\\p{L}\\p{N}*])(?:[^\\p{L}\\p{N}*]|$)#u", + "call": "match:null", + "subject": "MA==", + "engine": "YToyOntpOjA7czoxOiIwIjtpOjE7czoxOiIwIjt9", + "reason": "falsy", + "key": 0, + "phplrt": "array{non-falsy-string, non-falsy-string}", + "phpregex": "array{non-empty-string, non-empty-string}" + }, + { + "pattern": "#^([\\w\\-_]\\.?){2,}$#is", + "call": "match:null", + "subject": "MDA=", + "engine": "YToyOntpOjA7czoyOiIwMCI7aToxO3M6MToiMCI7fQ==", + "reason": "falsy", + "key": 1, + "phplrt": "array{non-falsy-string, non-falsy-string}", + "phpregex": "array{non-falsy-string, non-empty-string}" + }, + { + "pattern": "~^\\w(?:(?!\\/\\.{0,2}\\/)[\\w\\/\\-\\.])*$~", + "call": "match:null", + "subject": "MA==", + "engine": "YToxOntpOjA7czoxOiIwIjt9", + "reason": "falsy", + "key": 0, + "phplrt": "array{non-falsy-string}", + "phpregex": "array{non-empty-string}" + }, + { + "pattern": "/^\\s*(\\w+)\\s*:\\s*(\\S.*),?$/Uis", + "call": "match:null", + "subject": "CSAKWgoJOiAKCTAs", + "engine": "YTozOntpOjA7czoxMjoiCSAKWgoJOiAKCTAsIjtpOjE7czoxOiJaIjtpOjI7czoxOiIwIjt9", + "reason": "falsy", + "key": 2, + "phplrt": "array{non-falsy-string, non-empty-string, non-falsy-string}", + "phpregex": "array{non-falsy-string, non-empty-string, non-empty-string}" + }, + { + "pattern": "/\\pN/", + "call": "match:null", + "subject": "MA==", + "engine": "YToxOntpOjA7czoxOiIwIjt9", + "reason": "falsy", + "key": 0, + "phplrt": "array{non-falsy-string}", + "phpregex": "array{non-empty-string}" + }, + { + "pattern": "!'?\\b\\w(\\w|')*\\b!", + "call": "match:null", + "subject": "CjAKMWEhIQ==", + "engine": "YToxOntpOjA7czoxOiIwIjt9", + "reason": "falsy", + "key": 0, + "phplrt": "array{0: non-falsy-string, 1?: non-empty-string}", + "phpregex": "array{0: non-empty-string, 1?: non-empty-string}" + }, + { + "pattern": "/CHARSET=([A-Z]+([A-Z0-9]-?)*):/", + "call": "match:null", + "subject": "Q0hBUlNFVD1HRUkwMDo=", + "engine": "YTozOntpOjA7czoxNDoiQ0hBUlNFVD1HRUkwMDoiO2k6MTtzOjU6IkdFSTAwIjtpOjI7czoxOiIwIjt9", + "reason": "falsy", + "key": 2, + "phplrt": "array{0: non-falsy-string, 1: non-empty-string, 2?: non-falsy-string}", + "phpregex": "array{0: non-falsy-string&uppercase-string, 1: non-falsy-string&uppercase-string, 2?: non-empty-string&uppercase-string}" + }, + { + "pattern": "#^[ \\t*]*+([^\\s@].*)#mi", + "call": "match:null", + "subject": "MA==", + "engine": "YToyOntpOjA7czoxOiIwIjtpOjE7czoxOiIwIjt9", + "reason": "falsy", + "key": 0, + "phplrt": "array{non-falsy-string, non-falsy-string}", + "phpregex": "array{non-empty-string, non-empty-string}" + }, + { + "pattern": "!([-−])(\\d+)!", + "call": "match:null", + "subject": "4oiSMDI=", + "engine": "YTozOntpOjA7czozOiKSMDIiO2k6MTtzOjE6IpIiO2k6MjtzOjI6IjAyIjt9", + "reason": "value", + "key": 1, + "phplrt": "array{non-falsy-string, '-'|'−', numeric-string}", + "phpregex": "array{non-falsy-string, '-'|'\\x88'|'\\x92'|'\\xe2', numeric-string}" + }, + { + "pattern": "~^((?!oProxyCommand).)*$~i", + "call": "match:null", + "subject": "MDA=", + "engine": "YToyOntpOjA7czoyOiIwMCI7aToxO3M6MToiMCI7fQ==", + "reason": "falsy", + "key": 1, + "phplrt": "array{0: string, 1?: non-falsy-string}", + "phpregex": "array{0: string, 1?: non-empty-string}" + }, + { + "pattern": "/^([a-z\\d](-*[a-z\\d])*)(\\.([a-z\\d](-*[a-z\\d])*))*$/i", + "call": "match:null", + "subject": "MC4wLngtMC0tLTAuMDA=", + "engine": "YTo2OntpOjA7czoxNDoiMC4wLngtMC0tLTAuMDAiO2k6MTtzOjE6IjAiO2k6MjtzOjA6IiI7aTozO3M6MzoiLjAwIjtpOjQ7czoyOiIwMCI7aTo1O3M6MToiMCI7fQ==", + "reason": "falsy", + "key": 1, + "phplrt": "list{0: non-falsy-string, 1: non-falsy-string, 2?: string, 3?: string, 4?: string, 5?: non-falsy-string}", + "phpregex": "list{0: non-empty-string, 1: non-empty-string, 2?: string, 3?: string, 4?: string, 5?: non-empty-string}" + }, + { + "pattern": "/^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\\-]*[a-zA-Z0-9])\\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\\-]*[A-Za-z0-9])$/", + "call": "match:null", + "subject": "MA==", + "engine": "YTo0OntpOjA7czoxOiIwIjtpOjE7czowOiIiO2k6MjtzOjA6IiI7aTozO3M6MToiMCI7fQ==", + "reason": "falsy", + "key": 0, + "phplrt": "array{non-falsy-string, string, string, non-empty-string}", + "phpregex": "array{non-empty-string, string, string, non-empty-string}" + }, + { + "pattern": "/^\\w[-\\w_]*$/", + "call": "match:null", + "subject": "MA==", + "engine": "YToxOntpOjA7czoxOiIwIjt9", + "reason": "falsy", + "key": 0, + "phplrt": "array{non-falsy-string}", + "phpregex": "array{non-empty-string}" + }, + { + "pattern": "/(^|_|\\.)+(.)/", + "call": "match:null", + "subject": "MDAv2aMwLjA=", + "engine": "YTozOntpOjA7czoxOiIwIjtpOjE7czowOiIiO2k6MjtzOjE6IjAiO30=", + "reason": "falsy", + "key": 0, + "phplrt": "array{non-falsy-string, string, non-empty-string}", + "phpregex": "array{non-empty-string, ''|'.'|'_', non-empty-string}" + }, + { + "pattern": "/(^|_|\\.)++(.)/", + "call": "match:null", + "subject": "MDEx", + "engine": "YTozOntpOjA7czoxOiIwIjtpOjE7czowOiIiO2k6MjtzOjE6IjAiO30=", + "reason": "falsy", + "key": 0, + "phplrt": "array{non-falsy-string, string, non-empty-string}", + "phpregex": "array{non-empty-string, ''|'.'|'_', non-empty-string}" + } +] diff --git a/poc/witnesses/corpus.md b/poc/witnesses/corpus.md new file mode 100644 index 00000000000..fab37b26cfe --- /dev/null +++ b/poc/witnesses/corpus.md @@ -0,0 +1,55 @@ +# patterns from open-source projects + +One witness for each of the 21 patterns whose inferred type the engine contradicts with the parser generated by phplrt. `php poc/bin/verify.php` replays them. + +- a value that can be '0' or '' is typed non-falsy or non-empty: 20 +- a value outside the type of its key: 1 +- a key the engine writes is typed absent: 0 +- a key the engine leaves out is typed present: 0 +- the subject is narrowed further than the match proves: 0 + +| pattern | call | subject | the engine writes | phplrt | PHPRegex | why | +|---|---|---|---|---|---|---| +| `/^(?=.{1,253}$)([a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?)(\.[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?)*$/i` | preg_match() | `'0.a.zz-i-0-00-00shy-00-00----zl.0h0g-0-m000--s00-a0dy'` | `["0.a.zz-i-0-00-00shy-00-00----zl.0h0g-0-m000--s00-a0dy","0","",".0h0g-0-m000--s00-a0dy","h0g-0-m000--s00-a0dy"]` | `list{0: non-falsy-string, 1: non-falsy-string, 2?: string, 3?: string, 4?: non-falsy-string}` | `list{0: non-empty-string, 1: non-empty-string, 2?: string, 3?: string, 4?: non-empty-string}` | key `1`: a value that can be '0' or '' is typed non-falsy or non-empty | +| `~^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*\.[a-z]{2,}$~i` | preg_match() | `'0jrr.0.f0-0.00.eug'` | `["0jrr.0.f0-0.00.eug","jrr",".00","0"]` | `list{0: non-falsy-string, 1?: string, 2?: string, 3?: non-falsy-string}` | `list{0: non-falsy-string, 1?: string, 2?: string, 3?: non-empty-string}` | key `3`: a value that can be '0' or '' is typed non-falsy or non-empty | +| `/^[a-z]([a-zA-Z0-9_-]*[a-zA-Z0-9])*:[a-z]([a-zA-Z0-9_-]*[a-zA-Z0-9])*$/` | preg_match() | `'ct000:i0'` | `["ct000:i0","t000","0"]` | `list{0: non-falsy-string, 1?: string, 2?: non-falsy-string}` | `list{0: non-falsy-string, 1?: string, 2?: non-empty-string}` | key `2`: a value that can be '0' or '' is typed non-falsy or non-empty | +| `/^(((?!-))(xn--\|_)?[a-z0-9-]{0,61}[a-z0-9]{1,1}\.)*(xn--)?([a-z0-9][a-z0-9\-]{0,60}\|[a-z0-9-]{1,30}\.[a-z]{2,})$/` | preg_match() | `'0'` | `["0","","","","","0"]` | `array{non-empty-string, string, string, string, ''\|'xn--', non-falsy-string}` | `array{lowercase-string&non-empty-string, lowercase-string, '', ''\|'_'\|'xn--', ''\|'xn--', lowercase-string&non-empty-string}` | key `5`: a value that can be '0' or '' is typed non-falsy or non-empty | +| `/^([a-z\d](-*[a-z\d])*)(\.([a-z\d](-*[a-z\d])*))*\.?$/i` | preg_match() | `'h--0-0.f-lm-c.0---0--00'` | `["h--0-0.f-lm-c.0---0--00","h--0-0","-0",".0---0--00","0---0--00","0"]` | `list{0: non-falsy-string, 1: non-falsy-string, 2?: string, 3?: string, 4?: string, 5?: non-falsy-string}` | `list{0: non-empty-string, 1: non-empty-string, 2?: string, 3?: string, 4?: string, 5?: non-empty-string}` | key `5`: a value that can be '0' or '' is typed non-falsy or non-empty | +| `#(?:[^\p{L}\p{N}*"()]\|^)([+\-\|]?(?:[\p{L}\p{N}*"()]+'?)*[\p{L}\p{N}*"()])(?:[^\p{L}\p{N}*"()]\|$)#u` | preg_match() | `'0:-N0#N#'` | `["0:","0"]` | `array{non-falsy-string, non-falsy-string}` | `array{non-empty-string, non-empty-string}` | key `1`: a value that can be '0' or '' is typed non-falsy or non-empty | +| `#(?:[^\p{L}\p{N}*]\|^)([+\-\|]?(?:[\p{L}\p{N}*]+'?)*[\p{L}\p{N}*])(?:[^\p{L}\p{N}*]\|$)#u` | preg_match() | `'0'` | `["0","0"]` | `array{non-falsy-string, non-falsy-string}` | `array{non-empty-string, non-empty-string}` | key `0`: a value that can be '0' or '' is typed non-falsy or non-empty | +| `#^([\w\-_]\.?){2,}$#is` | preg_match() | `'00'` | `["00","0"]` | `array{non-falsy-string, non-falsy-string}` | `array{non-falsy-string, non-empty-string}` | key `1`: a value that can be '0' or '' is typed non-falsy or non-empty | +| `~^\w(?:(?!\/\.{0,2}\/)[\w\/\-\.])*$~` | preg_match() | `'0'` | `["0"]` | `array{non-falsy-string}` | `array{non-empty-string}` | key `0`: a value that can be '0' or '' is typed non-falsy or non-empty | +| `/^\s*(\w+)\s*:\s*(\S.*),?$/Uis` | preg_match() | `' \nZ\n : \n 0,'` | `["\t \nZ\n\t: \n\t0,","Z","0"]` | `array{non-falsy-string, non-empty-string, non-falsy-string}` | `array{non-falsy-string, non-empty-string, non-empty-string}` | key `2`: a value that can be '0' or '' is typed non-falsy or non-empty | +| `/\pN/` | preg_match() | `'0'` | `["0"]` | `array{non-falsy-string}` | `array{non-empty-string}` | key `0`: a value that can be '0' or '' is typed non-falsy or non-empty | +| `!'?\b\w(\w\|')*\b!` | preg_match() | `'\n0\n1a!!'` | `["0"]` | `array{0: non-falsy-string, 1?: non-empty-string}` | `array{0: non-empty-string, 1?: non-empty-string}` | key `0`: a value that can be '0' or '' is typed non-falsy or non-empty | +| `/CHARSET=([A-Z]+([A-Z0-9]-?)*):/` | preg_match() | `'CHARSET=GEI00:'` | `["CHARSET=GEI00:","GEI00","0"]` | `array{0: non-falsy-string, 1: non-empty-string, 2?: non-falsy-string}` | `array{0: non-falsy-string&uppercase-string, 1: non-falsy-string&uppercase-string, 2?: non-empty-string&uppercase-string}` | key `2`: a value that can be '0' or '' is typed non-falsy or non-empty | +| `#^[ \t*]*+([^\s@].*)#mi` | preg_match() | `'0'` | `["0","0"]` | `array{non-falsy-string, non-falsy-string}` | `array{non-empty-string, non-empty-string}` | key `0`: a value that can be '0' or '' is typed non-falsy or non-empty | +| `!([-−])(\d+)!` | preg_match() | `'−02'` | `array ( 0 => '\x9202', 1 => '\x92', 2 => '02', )` | `array{non-falsy-string, '-'\|'−', numeric-string}` | `array{non-falsy-string, '-'\|'\x88'\|'\x92'\|'\xe2', numeric-string}` | key `1`: a value outside the type of its key | +| `~^((?!oProxyCommand).)*$~i` | preg_match() | `'00'` | `["00","0"]` | `array{0: string, 1?: non-falsy-string}` | `array{0: string, 1?: non-empty-string}` | key `1`: a value that can be '0' or '' is typed non-falsy or non-empty | +| `/^([a-z\d](-*[a-z\d])*)(\.([a-z\d](-*[a-z\d])*))*$/i` | preg_match() | `'0.0.x-0---0.00'` | `["0.0.x-0---0.00","0","",".00","00","0"]` | `list{0: non-falsy-string, 1: non-falsy-string, 2?: string, 3?: string, 4?: string, 5?: non-falsy-string}` | `list{0: non-empty-string, 1: non-empty-string, 2?: string, 3?: string, 4?: string, 5?: non-empty-string}` | key `1`: a value that can be '0' or '' is typed non-falsy or non-empty | +| `/^(([a-zA-Z0-9]\|[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]\|[A-Za-z0-9][A-Za-z0-9\-]*[A-Za-z0-9])$/` | preg_match() | `'0'` | `["0","","","0"]` | `array{non-falsy-string, string, string, non-empty-string}` | `array{non-empty-string, string, string, non-empty-string}` | key `0`: a value that can be '0' or '' is typed non-falsy or non-empty | +| `/^\w[-\w_]*$/` | preg_match() | `'0'` | `["0"]` | `array{non-falsy-string}` | `array{non-empty-string}` | key `0`: a value that can be '0' or '' is typed non-falsy or non-empty | +| `/(^\|_\|\.)+(.)/` | preg_match() | `'00/٣0.0'` | `["0","","0"]` | `array{non-falsy-string, string, non-empty-string}` | `array{non-empty-string, ''\|'.'\|'_', non-empty-string}` | key `0`: a value that can be '0' or '' is typed non-falsy or non-empty | +| `/(^\|_\|\.)++(.)/` | preg_match() | `'011'` | `["0","","0"]` | `array{non-falsy-string, string, non-empty-string}` | `array{non-empty-string, ''\|'.'\|'_', non-empty-string}` | key `0`: a value that can be '0' or '' is typed non-falsy or non-empty | + +## Patterns the parser generated by phplrt infers no shape for + +15 patterns, `preg_match()` without flags. + +| pattern | PHPRegex | +|---|---| +| `/(@[^{]+{(?:[^{}]*\|(?R))*})/` | `array{non-falsy-string, non-falsy-string}` | +| `/^(?!(?>(?1)"?(?>\\[ -~]\|[^"])"?(?1)){255,})(?!(?>(?1)"?(?>\\[ -~]\|[^"])"?(?1)){65,}@)((?>(?>(?>((?>(?>(?>\x0D\x0A)?[\t ])+\|(?>[\t ]*\x0D\x0A)?[\t ]+)?)(\((?>(?2)(?>[\x01-\x08\x0B\x0C\x0E-'*-\[\]-\x7F]\|\\[\x00-\x7F]\|(?3)))*(?2)\)))+(?2))\|(?2))?)([!#-'*+\/-9=?^-~-]+\|"(?>(?2)(?>[\x01-\x08\x0B\x0C\x0E-!#-\[\]-\x7F]\|\\[\x00-\x7F]))*(?2)")(?>(?1)\.(?1)(?4))*(?1)@(?!(?1)[a-z0-9-]{64,})(?1)(?>([a-z0-9](?>[a-z0-9-]*[a-z0-9])?)(?>(?1)\.(?!(?1)[a-z0-9-]{64,})(?1)(?5)){0,126}\|\[(?:(?>IPv6:(?>([a-f0-9]{1,4})(?>:(?6)){7}\|(?!(?:.*[a-f0-9][:\]]){8,})((?6)(?>:(?6)){0,6})?::(?7)?))\|(?>(?>IPv6:(?>(?6)(?>:(?6)){5}:\|(?!(?:.*[a-f0-9]:){6,})(?8)?::(?>((?6)(?>:(?6)){0,4}):)?))?(25[0-5]\|2[0-4][0-9]\|1[0-9]{2}\|[1-9]?[0-9])(?>\.(?9)){3}))\])(?1)$/isD` | `list{0: non-falsy-string, 1: string, 2: string, 3: string, 4: non-empty-string, 5?: string, 6?: string, 7?: string, 8?: string, 9?: numeric-string}` | +| `#(?"'\[\]()]+\|\[(?1)*+\]\|\((?1)*+\))+(?[^()]+)\|(?R))*\)/` | `array{0: non-falsy-string, 1?: string}` | +| `/\n (?(DEFINE)(?P[0-9]+(_[0-9]+)*))\n (?:\.(?&LNUM)\|(?&LNUM)(?:\.(?!\.)(?&LNUM)?)?)(?:[eE][+-]?(?&LNUM))?/Ax` | `array{non-falsy-string}` | +| `{\(([^()]\|(?R))*\)}A` | `array{0: non-falsy-string, 1?: string}` | +| `{\(([^()]*+\|(?R))*\)(?: *: *[^ ]++)?\|<([^<>]*+\|(?R))*>\|\{([^{}]*+\|(?R))*\}}` | `list{0: non-falsy-string, 1?: string, 2?: string, 3?: string}` | +| `/\(\?(?:=\|<=\|!\|[^()]+) \| (?3) )* \))?/x` | `list{0: non-falsy-string, 1: non-empty-string, 2: string, 3?: non-falsy-string, 4?: string}` | +| `/@(class)(\( ( (?>[^()]+) \| (?2) )* \))/x` | `array{0: non-falsy-string, 1: 'class', 2: non-falsy-string, 3?: string}` | +| `/@(style)(\( ( (?>[^()]+) \| (?2) )* \))/x` | `array{0: non-falsy-string, 1: 'style', 2: non-falsy-string, 3?: string}` | diff --git a/poc/witnesses/pcre2.json b/poc/witnesses/pcre2.json new file mode 100644 index 00000000000..4acb11687bd --- /dev/null +++ b/poc/witnesses/pcre2.json @@ -0,0 +1,702 @@ +[ + { + "pattern": "#^[^ab\\]cde]#", + "call": "match:null", + "subject": "MCMjI2FiSw==", + "engine": "YToxOntpOjA7czoxOiIwIjt9", + "reason": "falsy", + "key": 0, + "phplrt": "array{non-falsy-string}", + "phpregex": "array{non-empty-string}" + }, + { + "pattern": "#^[^]cde]#", + "call": "match:null", + "subject": "MA==", + "engine": "YToxOntpOjA7czoxOiIwIjt9", + "reason": "falsy", + "key": 0, + "phplrt": "array{non-falsy-string}", + "phpregex": "array{non-empty-string}" + }, + { + "pattern": "#^a*\\w#", + "call": "match:null", + "subject": "MA==", + "engine": "YToxOntpOjA7czoxOiIwIjt9", + "reason": "falsy", + "key": 0, + "phplrt": "array{non-falsy-string}", + "phpregex": "array{non-empty-string}" + }, + { + "pattern": "#^a*?\\w#", + "call": "match:null", + "subject": "MA==", + "engine": "YToxOntpOjA7czoxOiIwIjt9", + "reason": "falsy", + "key": 0, + "phplrt": "array{non-falsy-string}", + "phpregex": "array{non-empty-string}" + }, + { + "pattern": "#(?<=(?<=(?<=a)b)c).#", + "call": "match:null", + "subject": "YWJjMA==", + "engine": "YToxOntpOjA7czoxOiIwIjt9", + "reason": "falsy", + "key": 0, + "phplrt": "array{non-falsy-string}", + "phpregex": "array{non-empty-string}" + }, + { + "pattern": "#(?<=ab(cd(?<=...))).#", + "call": "match:null", + "subject": "YWJjZDA=", + "engine": "YToyOntpOjA7czoxOiIwIjtpOjE7czoyOiJjZCI7fQ==", + "reason": "falsy", + "key": 0, + "phplrt": "array{non-falsy-string, 'cd'}", + "phpregex": "array{non-empty-string, 'cd'}" + }, + { + "pattern": "#.c*#", + "call": "match:null", + "subject": "MA==", + "engine": "YToxOntpOjA7czoxOiIwIjt9", + "reason": "falsy", + "key": 0, + "phplrt": "array{non-falsy-string}", + "phpregex": "array{non-empty-string}" + }, + { + "pattern": "#(?(1)abz)(.)#", + "call": "match:null", + "subject": "MA==", + "engine": "YToyOntpOjA7czoxOiIwIjtpOjE7czoxOiIwIjt9", + "reason": "falsy", + "key": 0, + "phplrt": "array{non-falsy-string, non-empty-string}", + "phpregex": "array{non-empty-string, non-empty-string}" + }, + { + "pattern": "#a*\\S#", + "call": "match:null", + "subject": "MA==", + "engine": "YToxOntpOjA7czoxOiIwIjt9", + "reason": "falsy", + "key": 0, + "phplrt": "array{non-falsy-string}", + "phpregex": "array{non-empty-string}" + }, + { + "pattern": "# *\\S#", + "call": "match:null", + "subject": "MA==", + "engine": "YToxOntpOjA7czoxOiIwIjt9", + "reason": "falsy", + "key": 0, + "phplrt": "array{non-falsy-string}", + "phpregex": "array{non-empty-string}" + }, + { + "pattern": "#a*\\w#", + "call": "match:null", + "subject": "MA==", + "engine": "YToxOntpOjA7czoxOiIwIjt9", + "reason": "falsy", + "key": 0, + "phplrt": "array{non-falsy-string}", + "phpregex": "array{non-empty-string}" + }, + { + "pattern": "#=*\\w#", + "call": "match:null", + "subject": "MDBhMSAKYTE=", + "engine": "YToxOntpOjA7czoxOiIwIjt9", + "reason": "falsy", + "key": 0, + "phplrt": "array{non-falsy-string}", + "phpregex": "array{non-empty-string}" + }, + { + "pattern": "#\\d*\\S#", + "call": "match:null", + "subject": "MA==", + "engine": "YToxOntpOjA7czoxOiIwIjt9", + "reason": "falsy", + "key": 0, + "phplrt": "array{non-falsy-string}", + "phpregex": "array{non-empty-string}" + }, + { + "pattern": "#\\d*\\w#", + "call": "match:null", + "subject": "MA==", + "engine": "YToxOntpOjA7czoxOiIwIjt9", + "reason": "falsy", + "key": 0, + "phplrt": "array{non-falsy-string}", + "phpregex": "array{non-empty-string}" + }, + { + "pattern": "#\\D*\\S#", + "call": "match:null", + "subject": "MA==", + "engine": "YToxOntpOjA7czoxOiIwIjt9", + "reason": "falsy", + "key": 0, + "phplrt": "array{non-falsy-string}", + "phpregex": "array{non-empty-string}" + }, + { + "pattern": "#\\D*\\w#", + "call": "match:null", + "subject": "MA==", + "engine": "YToxOntpOjA7czoxOiIwIjt9", + "reason": "falsy", + "key": 0, + "phplrt": "array{non-falsy-string}", + "phpregex": "array{non-empty-string}" + }, + { + "pattern": "#\\s*\\S#", + "call": "match:null", + "subject": "MA==", + "engine": "YToxOntpOjA7czoxOiIwIjt9", + "reason": "falsy", + "key": 0, + "phplrt": "array{non-falsy-string}", + "phpregex": "array{non-empty-string}" + }, + { + "pattern": "#\\s*\\w#", + "call": "match:null", + "subject": "MGI=", + "engine": "YToxOntpOjA7czoxOiIwIjt9", + "reason": "falsy", + "key": 0, + "phplrt": "array{non-falsy-string}", + "phpregex": "array{non-empty-string}" + }, + { + "pattern": "#\\S*\\S#", + "call": "match:null", + "subject": "MA==", + "engine": "YToxOntpOjA7czoxOiIwIjt9", + "reason": "falsy", + "key": 0, + "phplrt": "array{non-falsy-string}", + "phpregex": "array{non-empty-string}" + }, + { + "pattern": "#\\S*\\w#", + "call": "match:null", + "subject": "MA==", + "engine": "YToxOntpOjA7czoxOiIwIjt9", + "reason": "falsy", + "key": 0, + "phplrt": "array{non-falsy-string}", + "phpregex": "array{non-empty-string}" + }, + { + "pattern": "#\\w*\\S#", + "call": "match:null", + "subject": "MA==", + "engine": "YToxOntpOjA7czoxOiIwIjt9", + "reason": "falsy", + "key": 0, + "phplrt": "array{non-falsy-string}", + "phpregex": "array{non-empty-string}" + }, + { + "pattern": "#\\w*\\w#", + "call": "match:null", + "subject": "MA==", + "engine": "YToxOntpOjA7czoxOiIwIjt9", + "reason": "falsy", + "key": 0, + "phplrt": "array{non-falsy-string}", + "phpregex": "array{non-empty-string}" + }, + { + "pattern": "#\\W*\\S#", + "call": "match:null", + "subject": "MGHZow==", + "engine": "YToxOntpOjA7czoxOiIwIjt9", + "reason": "falsy", + "key": 0, + "phplrt": "array{non-falsy-string}", + "phpregex": "array{non-empty-string}" + }, + { + "pattern": "#\\W*\\w#", + "call": "match:null", + "subject": "MA==", + "engine": "YToxOntpOjA7czoxOiIwIjt9", + "reason": "falsy", + "key": 0, + "phplrt": "array{non-falsy-string}", + "phpregex": "array{non-empty-string}" + }, + { + "pattern": "#a*[^a]#", + "call": "match:null", + "subject": "MA==", + "engine": "YToxOntpOjA7czoxOiIwIjt9", + "reason": "falsy", + "key": 0, + "phplrt": "array{non-falsy-string}", + "phpregex": "array{non-empty-string}" + }, + { + "pattern": "# *\\H#", + "call": "match:null", + "subject": "MA==", + "engine": "YToxOntpOjA7czoxOiIwIjt9", + "reason": "falsy", + "key": 0, + "phplrt": "array{non-falsy-string}", + "phpregex": "array{non-empty-string}" + }, + { + "pattern": "#\\x0b*\\V#", + "call": "match:null", + "subject": "MA==", + "engine": "YToxOntpOjA7czoxOiIwIjt9", + "reason": "falsy", + "key": 0, + "phplrt": "array{non-falsy-string}", + "phpregex": "array{non-empty-string}" + }, + { + "pattern": "#(\\R*)(.)#s", + "call": "match:null", + "subject": "MA==", + "engine": "YTozOntpOjA7czoxOiIwIjtpOjE7czowOiIiO2k6MjtzOjE6IjAiO30=", + "reason": "falsy", + "key": 0, + "phplrt": "array{non-falsy-string, string, non-empty-string}", + "phpregex": "array{non-empty-string, string, non-empty-string}" + }, + { + "pattern": "#(\\R)*(.)#s", + "call": "match:null", + "subject": "MA==", + "engine": "YTozOntpOjA7czoxOiIwIjtpOjE7czowOiIiO2k6MjtzOjE6IjAiO30=", + "reason": "falsy", + "key": 0, + "phplrt": "array{non-falsy-string, string, non-empty-string}", + "phpregex": "array{non-empty-string, string, non-empty-string}" + }, + { + "pattern": "#((?>\\r\\n|\\n|\\x0b|\\f|\\r|\\x85)*)(.)#s", + "call": "match:null", + "subject": "MCA1MWIuSzg=", + "engine": "YTozOntpOjA7czoxOiIwIjtpOjE7czowOiIiO2k6MjtzOjE6IjAiO30=", + "reason": "falsy", + "key": 0, + "phplrt": "array{non-falsy-string, string, non-empty-string}", + "phpregex": "array{non-empty-string, string, non-empty-string}" + }, + { + "pattern": "#^(a)*+(\\w)#", + "call": "match:null", + "subject": "MHd3", + "engine": "YTozOntpOjA7czoxOiIwIjtpOjE7czowOiIiO2k6MjtzOjE6IjAiO30=", + "reason": "falsy", + "key": 0, + "phplrt": "array{non-falsy-string, string, non-empty-string}", + "phpregex": "array{non-empty-string, ''|'a', non-empty-string}" + }, + { + "pattern": "#^(?:a)*+(\\w)#", + "call": "match:null", + "subject": "MHdiCks6OjE=", + "engine": "YToyOntpOjA7czoxOiIwIjtpOjE7czoxOiIwIjt9", + "reason": "falsy", + "key": 0, + "phplrt": "array{non-falsy-string, non-empty-string}", + "phpregex": "array{non-empty-string, non-empty-string}" + }, + { + "pattern": "~(*: A \\ and #comment\n \\ B)x~x", + "call": "match:null", + "subject": "eA==", + "engine": "YToyOntpOjA7czoxOiJ4IjtzOjQ6Ik1BUksiO3M6MjY6IiBBIFwgYW5kICNjb21tZW50CiAgICAgXCBCIjt9", + "reason": "value", + "key": "MARK", + "phplrt": "array{0: non-falsy-string, MARK?: \" A \\\\ and \\n \\\\ B\"}", + "phpregex": "array{0: 'x', MARK?: \" A \\\\ and #comment\\n \\\\ B\"}" + }, + { + "pattern": "~L(?#(|++|b?).#", + "call": "match:null", + "subject": "MA==", + "engine": "YToxOntpOjA7czoxOiIwIjt9", + "reason": "falsy", + "key": 0, + "phplrt": "array{non-falsy-string}", + "phpregex": "array{non-empty-string}" + }, + { + "pattern": "#.x*#", + "call": "match:null", + "subject": "MA==", + "engine": "YToxOntpOjA7czoxOiIwIjt9", + "reason": "falsy", + "key": 0, + "phplrt": "array{non-falsy-string}", + "phpregex": "array{non-empty-string}" + }, + { + "pattern": "#^\\p{sc=Common}#u", + "call": "match:null", + "subject": "MA==", + "engine": "YToxOntpOjA7czoxOiIwIjt9", + "reason": "falsy", + "key": 0, + "phplrt": "array{non-falsy-string}", + "phpregex": "array{non-empty-string}" + }, + { + "pattern": "#^\\p{Script=Zyyy}#u", + "call": "match:null", + "subject": "MA==", + "engine": "YToxOntpOjA7czoxOiIwIjt9", + "reason": "falsy", + "key": 0, + "phplrt": "array{non-falsy-string}", + "phpregex": "array{non-empty-string}" + }, + { + "pattern": "#^\\p{Common}#u", + "call": "match:null", + "subject": "MA==", + "engine": "YToxOntpOjA7czoxOiIwIjt9", + "reason": "falsy", + "key": 0, + "phplrt": "array{non-falsy-string}", + "phpregex": "array{non-empty-string}" + }, + { + "pattern": "#a*\\x{100}*\\w#u", + "call": "match:null", + "subject": "MAp3MHc=", + "engine": "YToxOntpOjA7czoxOiIwIjt9", + "reason": "falsy", + "key": 0, + "phplrt": "array{non-falsy-string}", + "phpregex": "array{non-empty-string}" + }, + { + "pattern": "#^\\PC#u", + "call": "match:null", + "subject": "MA==", + "engine": "YToxOntpOjA7czoxOiIwIjt9", + "reason": "falsy", + "key": 0, + "phplrt": "array{non-falsy-string}", + "phpregex": "array{non-empty-string}" + }, + { + "pattern": "#^\\PL#u", + "call": "match:null", + "subject": "MA==", + "engine": "YToxOntpOjA7czoxOiIwIjt9", + "reason": "falsy", + "key": 0, + "phplrt": "array{non-falsy-string}", + "phpregex": "array{non-empty-string}" + }, + { + "pattern": "#^\\PM#u", + "call": "match:null", + "subject": "MA==", + "engine": "YToxOntpOjA7czoxOiIwIjt9", + "reason": "falsy", + "key": 0, + "phplrt": "array{non-falsy-string}", + "phpregex": "array{non-empty-string}" + }, + { + "pattern": "#^\\PP#u", + "call": "match:null", + "subject": "MA==", + "engine": "YToxOntpOjA7czoxOiIwIjt9", + "reason": "falsy", + "key": 0, + "phplrt": "array{non-falsy-string}", + "phpregex": "array{non-empty-string}" + }, + { + "pattern": "#^\\PS#u", + "call": "match:null", + "subject": "MA==", + "engine": "YToxOntpOjA7czoxOiIwIjt9", + "reason": "falsy", + "key": 0, + "phplrt": "array{non-falsy-string}", + "phpregex": "array{non-empty-string}" + }, + { + "pattern": "#^\\PZ#u", + "call": "match:null", + "subject": "MA==", + "engine": "YToxOntpOjA7czoxOiIwIjt9", + "reason": "falsy", + "key": 0, + "phplrt": "array{non-falsy-string}", + "phpregex": "array{non-empty-string}" + }, + { + "pattern": "#^(\\X*)(.)#u", + "call": "match:null", + "subject": "MA==", + "engine": "YTozOntpOjA7czoxOiIwIjtpOjE7czowOiIiO2k6MjtzOjE6IjAiO30=", + "reason": "falsy", + "key": 0, + "phplrt": "array{non-falsy-string, string, non-empty-string}", + "phpregex": "array{non-empty-string, string, non-empty-string}" + }, + { + "pattern": "#^(\\X*?)(.)#u", + "call": "match:null", + "subject": "MC4=", + "engine": "YTozOntpOjA7czoxOiIwIjtpOjE7czowOiIiO2k6MjtzOjE6IjAiO30=", + "reason": "falsy", + "key": 0, + "phplrt": "array{non-falsy-string, string, non-empty-string}", + "phpregex": "array{non-empty-string, string, non-empty-string}" + }, + { + "pattern": "#^\\X#u", + "call": "match:null", + "subject": "MHUgdWIKS2E=", + "engine": "YToxOntpOjA7czoxOiIwIjt9", + "reason": "falsy", + "key": 0, + "phplrt": "array{non-falsy-string}", + "phpregex": "array{non-empty-string}" + }, + { + "pattern": "#^[\\P{Yi}]#u", + "call": "match:null", + "subject": "MA==", + "engine": "YToxOntpOjA7czoxOiIwIjt9", + "reason": "falsy", + "key": 0, + "phplrt": "array{non-falsy-string}", + "phpregex": "array{non-empty-string}" + }, + { + "pattern": "#\\X*(.)#u", + "call": "match:null", + "subject": "MA==", + "engine": "YToyOntpOjA7czoxOiIwIjtpOjE7czoxOiIwIjt9", + "reason": "falsy", + "key": 0, + "phplrt": "array{non-falsy-string, non-empty-string}", + "phpregex": "array{non-empty-string, non-empty-string}" + }, + { + "pattern": "#(?<=\\d{2,3}|áBC).#u", + "call": "match:null", + "subject": "MDAwYw==", + "engine": "YToxOntpOjA7czoxOiIwIjt9", + "reason": "falsy", + "key": 0, + "phplrt": "array{non-falsy-string}", + "phpregex": "array{non-empty-string}" + }, + { + "pattern": "#(\\R*)(.)#su", + "call": "match:null", + "subject": "MA==", + "engine": "YTozOntpOjA7czoxOiIwIjtpOjE7czowOiIiO2k6MjtzOjE6IjAiO30=", + "reason": "falsy", + "key": 0, + "phplrt": "array{non-falsy-string, string, non-empty-string}", + "phpregex": "array{non-empty-string, string, non-empty-string}" + }, + { + "pattern": "#(\\R)*(.)#su", + "call": "match:null", + "subject": "MA==", + "engine": "YTozOntpOjA7czoxOiIwIjtpOjE7czowOiIiO2k6MjtzOjE6IjAiO30=", + "reason": "falsy", + "key": 0, + "phplrt": "array{non-falsy-string, string, non-empty-string}", + "phpregex": "array{non-empty-string, string, non-empty-string}" + }, + { + "pattern": "#^\\p{Xan}#u", + "call": "match:null", + "subject": "MA==", + "engine": "YToxOntpOjA7czoxOiIwIjt9", + "reason": "falsy", + "key": 0, + "phplrt": "array{non-falsy-string}", + "phpregex": "array{non-empty-string}" + }, + { + "pattern": "#^[\\p{Xan}]#u", + "call": "match:null", + "subject": "MA==", + "engine": "YToxOntpOjA7czoxOiIwIjt9", + "reason": "falsy", + "key": 0, + "phplrt": "array{non-falsy-string}", + "phpregex": "array{non-empty-string}" + }, + { + "pattern": "#^\\p{Xwd}#u", + "call": "match:null", + "subject": "MA==", + "engine": "YToxOntpOjA7czoxOiIwIjt9", + "reason": "falsy", + "key": 0, + "phplrt": "array{non-falsy-string}", + "phpregex": "array{non-empty-string}" + }, + { + "pattern": "#^[\\p{Xwd}]#u", + "call": "match:null", + "subject": "MA==", + "engine": "YToxOntpOjA7czoxOiIwIjt9", + "reason": "falsy", + "key": 0, + "phplrt": "array{non-falsy-string}", + "phpregex": "array{non-empty-string}" + }, + { + "pattern": "#(\\X*)(.)#su", + "call": "match:null", + "subject": "MA==", + "engine": "YTozOntpOjA7czoxOiIwIjtpOjE7czowOiIiO2k6MjtzOjE6IjAiO30=", + "reason": "falsy", + "key": 0, + "phplrt": "array{non-falsy-string, string, non-empty-string}", + "phpregex": "array{non-empty-string, string, non-empty-string}" + }, + { + "pattern": "#^\\P{Xuc}#u", + "call": "match:null", + "subject": "MA==", + "engine": "YToxOntpOjA7czoxOiIwIjt9", + "reason": "falsy", + "key": 0, + "phplrt": "array{non-falsy-string}", + "phpregex": "array{non-empty-string}" + }, + { + "pattern": "#^[\\P{Xuc}]#u", + "call": "match:null", + "subject": "MA==", + "engine": "YToxOntpOjA7czoxOiIwIjt9", + "reason": "falsy", + "key": 0, + "phplrt": "array{non-falsy-string}", + "phpregex": "array{non-empty-string}" + }, + { + "pattern": "#\\pN+\\z#", + "call": "match:null", + "subject": "MA==", + "engine": "YToxOntpOjA7czoxOiIwIjt9", + "reason": "falsy", + "key": 0, + "phplrt": "array{non-falsy-string}", + "phpregex": "array{non-empty-string}" + }, + { + "pattern": "#\\pN+#", + "call": "match:null", + "subject": "MA==", + "engine": "YToxOntpOjA7czoxOiIwIjt9", + "reason": "falsy", + "key": 0, + "phplrt": "array{non-falsy-string}", + "phpregex": "array{non-empty-string}" + }, + { + "pattern": "~L(?#(|++\r\n\|\n\|\x0b\|\f\|\r\|\x85)*)(.)#s` | preg_match() | `'0 51b.K8'` | `["0","","0"]` | `array{non-falsy-string, string, non-empty-string}` | `array{non-empty-string, string, non-empty-string}` | key `0`: a value that can be '0' or '' is typed non-falsy or non-empty | +| `#^(a)*+(\w)#` | preg_match() | `'0ww'` | `["0","","0"]` | `array{non-falsy-string, string, non-empty-string}` | `array{non-empty-string, ''\|'a', non-empty-string}` | key `0`: a value that can be '0' or '' is typed non-falsy or non-empty | +| `#^(?:a)*+(\w)#` | preg_match() | `'0wb\nK::1'` | `["0","0"]` | `array{non-falsy-string, non-empty-string}` | `array{non-empty-string, non-empty-string}` | key `0`: a value that can be '0' or '' is typed non-falsy or non-empty | +| `~(*: A \ and #comment\n \ B)x~x` | preg_match() | `'x'` | `{"0":"x","MARK":" A \\ and #comment\n \\ B"}` | `array{0: non-falsy-string, MARK?: " A \\ and \n \\ B"}` | `array{0: 'x', MARK?: " A \\ and #comment\n \\ B"}` | key `MARK`: a value outside the type of its key | +| `~L(?#(\|++\|b?).#` | preg_match() | `'0'` | `["0"]` | `array{non-falsy-string}` | `array{non-empty-string}` | key `0`: a value that can be '0' or '' is typed non-falsy or non-empty | +| `#.x*#` | preg_match() | `'0'` | `["0"]` | `array{non-falsy-string}` | `array{non-empty-string}` | key `0`: a value that can be '0' or '' is typed non-falsy or non-empty | +| `#^\p{sc=Common}#u` | preg_match() | `'0'` | `["0"]` | `array{non-falsy-string}` | `array{non-empty-string}` | key `0`: a value that can be '0' or '' is typed non-falsy or non-empty | +| `#^\p{Script=Zyyy}#u` | preg_match() | `'0'` | `["0"]` | `array{non-falsy-string}` | `array{non-empty-string}` | key `0`: a value that can be '0' or '' is typed non-falsy or non-empty | +| `#^\p{Common}#u` | preg_match() | `'0'` | `["0"]` | `array{non-falsy-string}` | `array{non-empty-string}` | key `0`: a value that can be '0' or '' is typed non-falsy or non-empty | +| `#a*\x{100}*\w#u` | preg_match() | `'0\nw0w'` | `["0"]` | `array{non-falsy-string}` | `array{non-empty-string}` | key `0`: a value that can be '0' or '' is typed non-falsy or non-empty | +| `#^\PC#u` | preg_match() | `'0'` | `["0"]` | `array{non-falsy-string}` | `array{non-empty-string}` | key `0`: a value that can be '0' or '' is typed non-falsy or non-empty | +| `#^\PL#u` | preg_match() | `'0'` | `["0"]` | `array{non-falsy-string}` | `array{non-empty-string}` | key `0`: a value that can be '0' or '' is typed non-falsy or non-empty | +| `#^\PM#u` | preg_match() | `'0'` | `["0"]` | `array{non-falsy-string}` | `array{non-empty-string}` | key `0`: a value that can be '0' or '' is typed non-falsy or non-empty | +| `#^\PP#u` | preg_match() | `'0'` | `["0"]` | `array{non-falsy-string}` | `array{non-empty-string}` | key `0`: a value that can be '0' or '' is typed non-falsy or non-empty | +| `#^\PS#u` | preg_match() | `'0'` | `["0"]` | `array{non-falsy-string}` | `array{non-empty-string}` | key `0`: a value that can be '0' or '' is typed non-falsy or non-empty | +| `#^\PZ#u` | preg_match() | `'0'` | `["0"]` | `array{non-falsy-string}` | `array{non-empty-string}` | key `0`: a value that can be '0' or '' is typed non-falsy or non-empty | +| `#^(\X*)(.)#u` | preg_match() | `'0'` | `["0","","0"]` | `array{non-falsy-string, string, non-empty-string}` | `array{non-empty-string, string, non-empty-string}` | key `0`: a value that can be '0' or '' is typed non-falsy or non-empty | +| `#^(\X*?)(.)#u` | preg_match() | `'0.'` | `["0","","0"]` | `array{non-falsy-string, string, non-empty-string}` | `array{non-empty-string, string, non-empty-string}` | key `0`: a value that can be '0' or '' is typed non-falsy or non-empty | +| `#^\X#u` | preg_match() | `'0u ub\nKa'` | `["0"]` | `array{non-falsy-string}` | `array{non-empty-string}` | key `0`: a value that can be '0' or '' is typed non-falsy or non-empty | +| `#^[\P{Yi}]#u` | preg_match() | `'0'` | `["0"]` | `array{non-falsy-string}` | `array{non-empty-string}` | key `0`: a value that can be '0' or '' is typed non-falsy or non-empty | +| `#\X*(.)#u` | preg_match() | `'0'` | `["0","0"]` | `array{non-falsy-string, non-empty-string}` | `array{non-empty-string, non-empty-string}` | key `0`: a value that can be '0' or '' is typed non-falsy or non-empty | +| `#(?<=\d{2,3}\|áBC).#u` | preg_match() | `'000c'` | `["0"]` | `array{non-falsy-string}` | `array{non-empty-string}` | key `0`: a value that can be '0' or '' is typed non-falsy or non-empty | +| `#(\R*)(.)#su` | preg_match() | `'0'` | `["0","","0"]` | `array{non-falsy-string, string, non-empty-string}` | `array{non-empty-string, string, non-empty-string}` | key `0`: a value that can be '0' or '' is typed non-falsy or non-empty | +| `#(\R)*(.)#su` | preg_match() | `'0'` | `["0","","0"]` | `array{non-falsy-string, string, non-empty-string}` | `array{non-empty-string, string, non-empty-string}` | key `0`: a value that can be '0' or '' is typed non-falsy or non-empty | +| `#^\p{Xan}#u` | preg_match() | `'0'` | `["0"]` | `array{non-falsy-string}` | `array{non-empty-string}` | key `0`: a value that can be '0' or '' is typed non-falsy or non-empty | +| `#^[\p{Xan}]#u` | preg_match() | `'0'` | `["0"]` | `array{non-falsy-string}` | `array{non-empty-string}` | key `0`: a value that can be '0' or '' is typed non-falsy or non-empty | +| `#^\p{Xwd}#u` | preg_match() | `'0'` | `["0"]` | `array{non-falsy-string}` | `array{non-empty-string}` | key `0`: a value that can be '0' or '' is typed non-falsy or non-empty | +| `#^[\p{Xwd}]#u` | preg_match() | `'0'` | `["0"]` | `array{non-falsy-string}` | `array{non-empty-string}` | key `0`: a value that can be '0' or '' is typed non-falsy or non-empty | +| `#(\X*)(.)#su` | preg_match() | `'0'` | `["0","","0"]` | `array{non-falsy-string, string, non-empty-string}` | `array{non-empty-string, string, non-empty-string}` | key `0`: a value that can be '0' or '' is typed non-falsy or non-empty | +| `#^\P{Xuc}#u` | preg_match() | `'0'` | `["0"]` | `array{non-falsy-string}` | `array{non-empty-string}` | key `0`: a value that can be '0' or '' is typed non-falsy or non-empty | +| `#^[\P{Xuc}]#u` | preg_match() | `'0'` | `["0"]` | `array{non-falsy-string}` | `array{non-empty-string}` | key `0`: a value that can be '0' or '' is typed non-falsy or non-empty | +| `#\pN+\z#` | preg_match() | `'0'` | `["0"]` | `array{non-falsy-string}` | `array{non-empty-string}` | key `0`: a value that can be '0' or '' is typed non-falsy or non-empty | +| `#\pN+#` | preg_match() | `'0'` | `["0"]` | `array{non-falsy-string}` | `array{non-empty-string}` | key `0`: a value that can be '0' or '' is typed non-falsy or non-empty | +| `~L(?#(\|++abc>([^()]\|\((?1)*\))*\d+)(,(?P>elem))*)\]#` | `array{0: non-falsy-string, 1: non-empty-string, elem: numeric-string, 2: numeric-string, 3?: non-falsy-string}` | +| `#\[((?P\d+)(,(?P>elem))*)?\]#` | `array{0: non-falsy-string, 1: non-empty-string, elem: numeric-string, 2: numeric-string, 3?: non-falsy-string}\|array{non-falsy-string}` | +| `#[ab]{1}+#` | `array{'a'\|'b'}` | +| `#()(?1){1}#` | `array{'', ''}` | +| `#()(?1)#` | `array{'', ''}` | +| `#[^()]*(?:\((?R)\)[^()]*)*#` | `array{string}` | +| `#[^()]*(?:\((?>(?R))\)[^()]*)*#` | `array{string}` | +| `#[^()]*(?:\((?R)\))*[^()]*#` | `array{string}` | +| `#(?:\((?R)\))*[^()]*#` | `array{string}` | +| `#(?:\((?R)\))\|[^()]*#` | `array{string}` | +| `#^((?Pa1)\|(?Pa2)b)#J` | `array{0: 'a1'\|'a2b', 1: 'a1'\|'a2b', A: 'a1', 2: 'a1'}\|array{0: 'a1'\|'a2b', 1: 'a1'\|'a2b', A: 'a2', 2: '', 3: 'a2'}` | +| `#(?\|(?)(?)(?)\|(?)(?)(?))#J` | `array{0: '', a: '', 1: '', b: '', 2: '', 3: ''}` | +| `#^(?Pa)(?Pb)#J` | `array{0: 'ab', A: 'a'\|'b', 1: 'a', 2: 'b'}` | +| `#^(?Pa)(?Pb)\|cd#J` | `array{'ab'\|'cd'}\|array{0: 'ab'\|'cd', A: 'a'\|'b', 1: 'a', 2: 'b'}` | +| `#^(?Pa)(?Pb)\|cd(?Pef)(?Pgh)#J` | `array{0: 'ab'\|'cdefgh', A: 'a'\|'b', 1: 'a', 2: 'b'}\|array{0: 'ab'\|'cdefgh', A: 'ef'\|'gh', 1: '', 2: '', 3: 'ef', 4: 'gh'}` | +| `#(?J)^((?Pa1)\|(?Pa2)b)#` | `array{0: 'a1'\|'a2b', 1: 'a1'\|'a2b', A: 'a1', 2: 'a1'}\|array{0: 'a1'\|'a2b', 1: 'a1'\|'a2b', A: 'a2', 2: '', 3: 'a2'}` | +| `#^(?Pa) (?J:(?Pb)(?Pc)) (?Pd)#` | `array{0: 'a bc d', A: 'a', 1: 'a', B: 'b'\|'c', 2: 'b', 3: 'c', C: 'd', 4: 'd'}` | +| `#(?P25[0-5]\|2[0-4]\d\|[01]?\d?\d)(?:\.(?P>B)){3}#` | `array{0: non-falsy-string, B: numeric-string, 1: numeric-string}` | +| `#(?x)(?'xyz'y)#` | `array{0: 'xy', abc: 'x', 1: 'x', xyz: 'y', 2: 'y'}` | +| `#^(?P>abc)(?x\|y)#` | `array{0: 'xx'\|'xy'\|'yx'\|'yy', abc: 'x'\|'y', 1: 'x'\|'y'}` | +| `#^(?P>abc)(?Px\|y)#` | `array{0: 'xx'\|'xy'\|'yx'\|'yy', abc: 'x'\|'y', 1: 'x'\|'y'}` | +| `#^(?1)(abc)#` | `array{'abcabc', 'abc'}` | +| `#^(?P>abc)[()](?)#` | `array{0: '('\|')', abc: '', 1: ''}` | +| `#^(?P>abc)\Q()\E(?)#` | `array{0: '()', abc: '', 1: ''}` | +| `#^(?P>abc)[a\Q(]\E(](?)#` | `array{0: '('\|']'\|'a', abc: '', 1: ''}` | +| `#(?=(?'abc'\w+))\k:#` | `array{0: non-falsy-string, abc: non-empty-string, 1: non-empty-string}` | +| `#(?'abc'a\|b)(?d\|e)\k{2}#J` | `array{0: non-falsy-string, abc: 'a'\|'b'\|'d'\|'e', 1: 'a'\|'b', 2: 'd'\|'e'}` | +| `#(?'abc'a\|b)(?d\|e)(?&abc){2}#J` | `array{0: 'adaa'\|'adab'\|'adba'\|'adbb'\|'aeaa'\|'aeab'\|'aeba'\|'aebb'\|'bdaa'\|'bdab'\|'bdba'\|'bdbb'\|'beaa'\|'beab'\|'beba'\|'bebb', abc: 'a'\|'b'\|'d'\|'e', 1: 'a'\|'b', 2: 'd'\|'e'}` | +| `#(?&abc)X(?P)#` | `array{0: 'PXP', abc: 'P', 1: 'P'}` | +| `#(?1)X(?P)#` | `array{0: 'PXP', abc: 'P', 1: 'P'}` | +| `#(?:a(?&abc)b)*(?x)#` | `array{0: lowercase-string&non-falsy-string, abc: 'x', 1: 'x'}` | +| `#(?:a(?&abc)b){1,5}(?x)#` | `array{0: 'axbaxbaxbaxbaxbx'\|'axbaxbaxbaxbx'\|'axbaxbaxbx'\|'axbaxbx'\|'axbx', abc: 'x', 1: 'x'}` | +| `#(?:a(?&abc)b){2,5}(?x)#` | `array{0: 'axbaxbaxbaxbaxbx'\|'axbaxbaxbaxbx'\|'axbaxbaxbx'\|'axbaxbx', abc: 'x', 1: 'x'}` | +| `#(?:a(?&abc)b){2,}(?x)#` | `array{0: lowercase-string&non-falsy-string, abc: 'x', 1: 'x'}` | +| `#(abc)(?i:(?1))#` | `array{'abcabc', 'abc'}` | +| `#(abc)(?:(?i)(?1))#` | `array{'abcabc', 'abc'}` | +| `#(ab\|c)(?-1)#` | `array{'abab'\|'abc'\|'cab'\|'cc', 'ab'\|'c'}` | +| `#xy(?+1)(abc)#` | `array{'xyabcabc', 'abc'}` | +| `#(x)(?\|(abc)\|(xyz))(x)#` | `array{'xabcx'\|'xxyzx', 'x', 'abc'\|'xyz', 'x'}` | +| `#(x)(?\|(abc)(pqr)\|(xyz))(x)#` | `array{'xabcpqrx'\|'xxyzx', 'x', ''\|'abc'\|'xyz', ''\|'pqr', 'x'}` | +| `#A(*PRUNE)B(*SKIP)C(*THEN)D(*COMMIT)E(*F)F(*FAIL)G(?!)H(*ACCEPT)I#` | `array{string}` | +| `#^a+(*FAIL)#` | `array{lowercase-string&non-falsy-string}` | +| `#a+b?c+(*FAIL)#` | `array{lowercase-string&non-falsy-string}` | +| `#a+b?(*PRUNE)c+(*FAIL)#` | `array{lowercase-string&non-falsy-string}` | +| `#a+b?(*COMMIT)c+(*FAIL)#` | `array{lowercase-string&non-falsy-string}` | +| `#a+b?(*SKIP)c+(*FAIL)#` | `array{lowercase-string&non-falsy-string}` | +| `#a+b?(*THEN)c+(*FAIL)#` | `array{lowercase-string&non-falsy-string}` | +| `#(*CR)a.b#` | `array{non-falsy-string}` | +| `#(*CRLF)a.b#` | `array{non-falsy-string}` | +| `#(*ANY).*#` | `array{string}` | +| `#(*ANYCRLF).*#` | `array{string}` | +| `#(*CRLF).*#` | `array{string}` | +| `#(*NUL)^.*#` | `array{string}` | +| `#(*NUL)^.*#s` | `array{string}` | +| `#(*NUL)^X\NY#` | `array{non-falsy-string}` | +| `#(*BSR_ANYCRLF)a\Rb#` | `array{lowercase-string&non-falsy-string}` | +| `#(*BSR_UNICODE)a\Rb#` | `array{lowercase-string&non-falsy-string}` | +| `#(*BSR_ANYCRLF)(*CRLF)a\Rb#` | `array{lowercase-string&non-falsy-string}` | +| `#(*CRLF)(*BSR_UNICODE)a\Rb#` | `array{lowercase-string&non-falsy-string}` | +| `#(*CRLF)(*BSR_ANYCRLF)(*CR)ab#` | `array{'ab'}` | +| `#^(?+1)(?x\|y){0}z#` | `array{'xz'\|'yz'}` | +| `#(?1)[]a()b](abc)#` | `array{'abc(abc'\|'abc)abc'\|'abc]abc'\|'abcaabc'\|'abcbabc', 'abc'}` | +| `#(?1)[^]a()b](abc)#` | `array{non-falsy-string, 'abc'}` | +| `#(?2)[]a()b](abc)(xyz)#` | `array{'xyz(abcxyz'\|'xyz)abcxyz'\|'xyz]abcxyz'\|'xyzaabcxyz'\|'xyzbabcxyz', 'abc', 'xyz'}` | +| `#(?!)?a#` | `array{'a'}` | +| `#(?(?=.*b)b\|^)#` | `array{string}` | +| `#(?(?=b).*b\|^d)#` | `array{non-empty-string}` | +| `#(?(?=.*b).*b\|^d)#` | `array{non-empty-string}` | +| `#(*NO_START_OPT)xyz#` | `array{'xyz'}` | +| `#(*NO_AUTO_POSSESS)a+b#` | `array{lowercase-string&non-falsy-string}` | +| `#^"((?(?=[a])[^"])\|b)*"$#` | `array{0: non-falsy-string, 1?: string}` | +| `#^X(?&N)(a)(?\|(b)\|(q))(c)(d)(?Y)#` | `array{0: 'XYabcdY'\|'XYaqcdY', 1: 'a', 2: 'b'\|'q', 3: 'c', 4: 'd', N: 'Y', 5: 'Y'}` | +| `#Z(*F)#` | `array{'Z'}` | +| `#(?&word)(?&element)(?(DEFINE)(?<[^m][^>]>[^<])(?\w*+))#` | `array{non-falsy-string}` | +| `#(?&word)(?&element)(?(DEFINE)(?<[^\d][^>]>[^<])(?\w*+))#` | `array{non-falsy-string}` | +| `#(ab)(x(y)z(cd(*ACCEPT)))pq#` | `array{string, string, string, string, string}` | +| `#^(?a\|b\gc)#` | `array{0: non-falsy-string, name: non-falsy-string, 1: non-falsy-string}` | +| `#^(?a\|b\g'name'c)#` | `array{0: non-falsy-string, name: non-falsy-string, 1: non-falsy-string}` | +| `#^(a\|b\g<1>c)#` | `array{non-falsy-string, non-falsy-string}` | +| `#^(a\|b\g'1'c)#` | `array{non-falsy-string, non-falsy-string}` | +| `#^(a\|b\g'-1'c)#` | `array{non-falsy-string, non-falsy-string}` | +| `#(^(a\|b\g<-1>c))#` | `array{non-falsy-string, non-falsy-string, non-falsy-string}` | +| `#(?-i:\g)(?i:(?a))#` | `array{0: non-falsy-string, name: non-falsy-string, 1: non-falsy-string}` | +| `#(?i:\g)(?-i:(?a))#` | `array{0: 'aa', name: 'a', 1: 'a'}` | +| `#(?-i:\g<+1>)(?i:(a))#` | `array{non-falsy-string, non-falsy-string}` | +| `~(?=(?(?#simplesyntax)\$(?[a-zA-Z_\x{7f}-\x{ff}][a-zA-Z0-9_\x{7f}-\x{ff}]*)(?:\[(?[a-zA-Z0-9_\x{7f}-\x{ff}]+\|\$\g)\]\|->\g(\(.*?\))?)?\|(?#simple syntax withbraces)\$\{(?:\g(?\[(?:\g\|'(?:\\.\|[^'\\])*'\|"(?:\g\|\\.\|[^"\\])*")\])?\|\g\|\$\{\g\})\}\|(?#complexsyntax)\{(?\$(?\g(\g*\|\(.*?\))?)(?:->\g)*\|\$\g\|\$\{\g\})\}))\{~` | `array{0: '{', regex: non-falsy-string, 1: non-falsy-string, name?: string, 2?: string, index?: string, 3?: string, 4?: string, indices?: string, 5?: string, complex?: string, 6?: string, segment?: string, 7?: string, 8?: string}` | +| `#(?a\|b\|c)\g*#` | `array{0: lowercase-string&non-falsy-string, n: 'a'\|'b'\|'c', 1: 'a'\|'b'\|'c'}` | +| `#^X(?7)(a)(?\|(b)\|(q)(r)(s))(c)(d)(Y)#` | `array{'XYabcdY'\|'XYaqrscdY', 'a', ''\|'b'\|'q', ''\|'r', ''\|'s', 'c', 'd', 'Y'}` | +| `#(?<=b(?1)\|zzz)(a)#` | `array{'a', 'a'}` | +| `#(a\|bc)(?1)#` | `array{'aa'\|'abc'\|'bca'\|'bcbc', 'a'\|'bc'}` | +| `#(?:a(?[0-5])\|b(?[4-7]))c(?()d\|e)#J` | `array{0: non-falsy-string, digit: '0'\|'1'\|'2'\|'3'\|'4'\|'5', 1: '0'\|'1'\|'2'\|'3'\|'4'\|'5'}\|array{0: non-falsy-string, digit: '4'\|'5'\|'6'\|'7', 1: '', 2: '4'\|'5'\|'6'\|'7'}` | +| `#^(?&t)*+(?(DEFINE)(?.))$#` | `array{string}` | +| `#^(?&t)*(?(DEFINE)(?.))$#` | `array{string}` | +| `#(*COMMIT:X)#` | `array{0: '', MARK?: 'X'}` | +| `#A(*COMMIT)B#` | `array{'AB'}` | +| `#A(*THEN)B\|A(*THEN)C#` | `array{'AB'\|'AC'}` | +| `#A(*PRUNE)B\|A(*PRUNE)C#` | `array{'AB'\|'AC'}` | +| `#(*COMMIT)ABC#` | `array{'ABC'}` | +| `#(?>.(*ACCEPT))*?5#` | `array{string}` | +| `#(.(*ACCEPT))*?5#` | `array{0: string, 1?: string}` | +| `#(.(*ACCEPT))5#` | `array{string, string}` | +| `#(.(*ACCEPT))*5#` | `array{0: string, 1?: string}` | +| `#(?1)(?:(b(*ACCEPT))){0}#` | `array{string}` | +| `#(?1)(?:(b(*ACCEPT))){0}c#` | `array{string}` | +| `#(?1)(?:((*ACCEPT))){0}c#` | `array{string}` | +| `#^.*?(?(?=a)a\|b(*THEN)c)#` | `array{non-empty-string}` | +| `#^.*?(?(?=a)a\|bc)#` | `array{non-empty-string}` | +| `#^.*?(?(?=a)a(*THEN)b\|c)#` | `array{non-empty-string}` | +| `#^.*?(?(?=a)a(*THEN)b)c#` | `array{non-falsy-string}` | +| `#^.*?(a(*THEN)b)c#` | `array{non-falsy-string, 'ab'}` | +| `#^.*?(a(*THEN)b\|z)c#` | `array{non-falsy-string, 'ab'\|'z'}` | +| `#^.*?(z\|a(*THEN)b)c#` | `array{non-falsy-string, 'ab'\|'z'}` | +| `#(*MARK:A)(*SKIP:B)(C\|X)#` | `array{0: 'C'\|'X', 1: 'C'\|'X', MARK?: 'A'}` | +| `#(*:A)A+(*SKIP:A)(B\|Z)#` | `array{0: non-falsy-string&uppercase-string, 1: 'B'\|'Z', MARK?: 'A'}` | +| `#(?=a*(*ACCEPT)b)c#` | `array{string}` | +| `#(?1)c(?(DEFINE)((*ACCEPT)b))#` | `array{string}` | +| `#(?>(*ACCEPT)b)c#` | `array{string}` | +| `#(?(DEFINE)(a(?2)\|b)(b(?1)\|a))(?:(?1)\|(?2))#` | `array{string}` | +| `#(a(?2)\|b)(b(?1)\|a)(?:(?1)\|(?2))#` | `array{non-falsy-string, non-falsy-string, non-falsy-string}` | +| `#(a(?2)\|b)(b(?1)\|a)(?1)(?2)#` | `array{non-falsy-string, non-falsy-string, non-falsy-string}` | +| `#(abc)(?1)#` | `array{'abcabc', 'abc'}` | +| `#(?=abc){3}abc#` | `array{'abc'}` | +| `#(?=abc)+abc#` | `array{lowercase-string&non-falsy-string}` | +| `#(?=abc)++abc#` | `array{lowercase-string&non-falsy-string}` | +| `#(?=abc){0}xyz#` | `array{'xyz'}` | +| `#(?=(a))?.#` | `array{0: non-empty-string, 1?: 'a'}` | +| `#(?=(a))??.#` | `array{0: non-empty-string, 1?: 'a'}` | +| `#^(?=(a)){0}b(?1)#` | `array{'ba'}` | +| `#(?(DEFINE)(a))?b(?1)#` | `array{'ba'}` | +| `#^(?=(?1))?[az]([abc])d#` | `array{'aad'\|'abd'\|'acd'\|'zad'\|'zbd'\|'zcd', 'a'\|'b'\|'c'}` | +| `#^(?!a){0}\w+#` | `array{non-empty-string}` | +| `#(?<=(abc))?xyz#` | `array{0: 'xyz', 1?: 'abc'}` | +| `#.(*F)#` | `array{non-empty-string}` | +| `#a(*SKIP)c\|b(*ACCEPT)\|#` | `array{string}` | +| `#a(*SKIP)c\|b(*ACCEPT)cd(*ACCEPT)\|x#` | `array{string}` | +| `#a*(*ACCEPT)b#` | `array{string}` | +| `#(*ACCEPT)a#` | `array{string}` | +| `#z(*ACCEPT)a#` | `array{string}` | +| `#(.)(\1\|a(?2))#` | `array{non-empty-string, non-empty-string, string}` | +| `#\1\|(.)(?R)\1#` | `array{0: string, 1?: non-empty-string}` | +| `#(.)((?(1)c\|a)\|a(?2))#` | `array{non-falsy-string, non-empty-string, non-empty-string}` | +| `#(?(?=c)c\|d)++Y#` | `array{non-falsy-string}` | +| `#(?(?=c)c\|d)*+Y#` | `array{non-falsy-string}` | +| `#(a)(?2){0,1999}?(b)#` | `array{lowercase-string&non-falsy-string, 'a', 'b'}` | +| `#(a)(?(DEFINE)(b))(?2){0,1999}?(?2)#` | `array{lowercase-string&non-falsy-string, 'a'}` | +| `#AB(C(D))(E(F))?(?(?=\2)(?=\4))#` | `array{non-falsy-string, 'CD', 'D', 'EF', 'F'}\|array{non-falsy-string, 'CD', 'D'}` | +| `#.*?a(*PRUNE)b#` | `array{non-falsy-string}` | +| `#.*?a(*SKIP)b#` | `array{non-falsy-string}` | +| `#(?:(a)+(?C1)bb\|aa(?C2)b)#` | `array{0: lowercase-string&non-falsy-string, 1?: 'a'}` | +| `#(?:(a)++(?C1)bb\|aa(?C2)b)#` | `array{0: lowercase-string&non-falsy-string, 1?: 'a'}` | +| `#(?:(?>(a))(?C1)bb\|aa(?C2)b)#` | `array{0: 'aab'\|'abb', 1?: 'a'}` | +| `#(?:(?1)(?C1)x\|ab(?C2))((a)){0}#` | `array{'ab'\|'ax'}` | +| `#(?1)(?C1)((a)(?C2)){0}#` | `array{'a'}` | +| `#(?:(a)+(?C1)bb\|aa(?C2)b)++#` | `array{0: lowercase-string&non-falsy-string, 1?: 'a'}` | +| `#^(?(?=a)aa\|bb)#` | `array{non-falsy-string}` | +| `#(?C1)^(?C2)(?(?C99)(?=(?C3)a(?C4))(?C5)a(?C6)a(?C7)\|(?C8)b(?C9)b(?C10))(?C11)#` | `array{non-falsy-string}` | +| `#aaaaa(*COMMIT)(*PRUNE)b\|a+c#` | `array{lowercase-string&non-falsy-string}` | +| `#(?!a(*COMMIT)b)ac\|ad#` | `array{'ac'\|'ad'}` | +| `#^(?!a(*THEN)b\|ac)..#` | `array{non-falsy-string}` | +| `#^(?=a(*THEN)b\|ac)#` | `array{''}` | +| `#\A.*?(?:a\|b(*THEN)c)#` | `array{non-falsy-string}` | +| `#\A.*?(?:a\|b(*THEN)c)++#` | `array{non-falsy-string}` | +| `#\A.*?(?:a\|b(*THEN)c\|d)#` | `array{non-falsy-string}` | +| `#(?:(a(*MARK:X)a+(*SKIP:X)b)){0}(?:(?1)\|aac)#` | `array{0: lowercase-string&non-falsy-string, MARK?: 'X'}` | +| `#\A.*?(a\|b(*THEN)c)#` | `array{non-falsy-string, 'a'\|'bc'}` | +| `#^(A(*THEN)B\|A(*THEN)D)#` | `array{'AB'\|'AD', 'AB'\|'AD'}` | +| `#(?!b(*THEN)a)bn\|bnn#` | `array{'bn'\|'bnn'}` | +| `#(?(?=b(*SKIP)a)bn\|bnn)#` | `array{non-falsy-string}` | +| `#(?=b(*THEN)a\|)bn\|bnn#` | `array{'bn'\|'bnn'}` | +| `#(?\|(?f)\|(?b))#J` | `array{0: 'b'\|'f', n: 'b'\|'f', 1: 'b'\|'f'}` | +| `#(?:b(?R)a\|a+)#` | `array{non-falsy-string}` | +| `#(?:b(?0)a\|a+)#` | `array{non-falsy-string}` | +| `#(?b(?&r)a\|x)a+#` | `array{0: non-falsy-string, r: non-falsy-string, 1: non-falsy-string}` | +| `#(?:b(?R)a\|a+b)#` | `array{non-falsy-string}` | +| `#(?:b(?R)a\|[ac]+)#` | `array{non-falsy-string}` | +| `#a{4}+#` | `array{'aaaa'}` | +| `#[A-[!xxx!]]+#` | `array{non-falsy-string}` | +| `#(((a\2)\|(a*)\g<-1>))*a?#` | `array{string, lowercase-string\|non-falsy-string, lowercase-string\|non-falsy-string, non-falsy-string, lowercase-string}\|array{string}` | +| `#^(?(?=abc)\w{3}:\|\d\d)$#` | `array{non-falsy-string}` | +| `#(?(?=ab)ab)#` | `array{string}` | +| `#(?(R)a+\|(?R)b)#` | `array{non-empty-string}` | +| `#(*NOTEMPTY)a*?b*?#` | `array{lowercase-string}` | +| `#(*NOTEMPTY_ATSTART)a*?b*?#` | `array{lowercase-string}` | +| `#(?(VERSION=8)yes\|no){3}#` | `array{non-falsy-string}` | +| `#((?=(?(?=(?(?=(?(?=()))))))))#` | `array{0: '', 1: '', 2?: ''}` | +| `#(?(?=)==)(((((((((?=)))))))))#` | `array{string, '', '', '', '', '', '', '', ''}` | +| `#(?a)\|(?b)#J` | `array{0: 'a'\|'b', A: 'a', 1: 'a'}\|array{0: 'a'\|'b', A: 'b', 1: '', 2: 'b'}` | +| `#(*NO_DOTSTAR_ANCHOR)(?s).*\d#` | `array{non-empty-string}` | +| `#^(?)(?:(?a)\|b)(?()A\|B)#J` | `array{0: non-falsy-string, AA: ''\|'a', 1: '', 2?: 'a'}` | +| `#^(?:(?X)\|)(?:(?a)\|b)\k{AA}#J` | `array{0: non-falsy-string, AA?: ''\|'a'\|'X', 1?: ''\|'X', 2?: 'a'}` | +| `#(?(?J)(?1(111111)11\|)1\|1\|)(?()1)#` | `array{0: string, N111: ''\|'1'\|'111111111'\|'1111111111', 1: ''\|'1'\|'1111111111', 2?: ''\|'111111111', 3?: '111111'}` | +| `#(?(?J)(?))(?-J)\k#` | `array{0: string, N: '', 1: '', 2: ''}` | +| `#(?(?=0)(?=00)?00765)#` | `array{string}` | +| `#(?(?=0)(?=00)?00765\|(?!3).56)#` | `array{non-falsy-string}` | +| `#((?2){0,1999}())?#` | `array{'', '', ''}\|array{''}` | +| `#((?+1)(\1))#` | `array{string, string, string}` | +| `#a(?C"a)b""c")#` | `array{'a'}` | +| `#ab(?C" any text with spaces ")cde#` | `array{'abcde'}` | +| `#^a(b)c(?C1)def#` | `array{'abcdef', 'b'}` | +| `#^a(b)c(?C"AB")def#` | `array{'abcdef', 'b'}` | +| `#^a(b)c(?C{AB})def#` | `array{'abcdef', 'b'}` | +| `~(?C`a``b`)(?C'a''b')(?C"a""b")(?C^a^^b^)(?C%a%%b%)(?C#a##b#)(?C$a$$b$)(?C{a}}b})~` | `array{''}` | +| `#(?:a(?C`code`)){3}#` | `array{'aaa'}` | +| `#^(?(?C25)(?=abc)abcd\|xyz)#` | `array{non-falsy-string}` | +| `#^(?(?C$abc$)(?=abc)abcd\|xyz)#` | `array{non-falsy-string}` | +| `#^ab(?C'first')cd(?C"second")ef#` | `array{'abcdef'}` | +| `#(?:a(?C`code`)){3}X#` | `array{'aaaX'}` | +| `#(?(?!)^)#` | `array{string}` | +| `#(?(?!)a\|b)#` | `array{non-empty-string}` | +| `#(*NO_JIT)((?2)+)((?1)){#` | `array{non-falsy-string, string, string}` | +| `#X((?2)()*+){2}+#` | `array{0: non-falsy-string&uppercase-string, 1: ''\|numeric-string, 2?: ''}` | +| `#X((?2)()*+){2}#` | `array{0: non-falsy-string&uppercase-string, 1: ''\|numeric-string, 2?: ''}` | +| `~(?x:((?'a')) # comment (with parentheses) and \| vertical\n(?-x:#not a comment (?'b')) # this is a comment ()\n(?'c')) # not a comment (?'d')~` | `array{0: '#not a comment # not a comment ', 1: '', a: '', 2: '', b: '', 3: '', c: '', 4: '', d: '', 5: ''}` | +| `#(?\|(?'a')(2)(?'b')\|(?'a')(?'a')(3))#J` | `array{0: '2'\|'3', a: ''\|'2', 1: '', 2: ''\|'2', b: ''\|'3', 3: ''\|'3'}` | +| `#.((?2)(?R)\|\1\|$)()#` | `array{non-empty-string, string, ''}` | +| `#.((?3)(?R)()(?2)\|\1\|$)()#` | `array{non-empty-string, string, '', ''}` | +| `#(((?(R)){0,2}) (?'x'((?'R')((?'R')))))#J` | `array{0: non-falsy-string, 1: non-falsy-string, 2: string, x: '', 3: '', 4: '', R: '', 5: '', 6: '', 7: ''}` | +| `#(((?(X)){0,2}) (?'x'((?'X')((?'X')))))#J` | `array{0: non-falsy-string, 1: non-falsy-string, 2: string, x: '', 3: '', 4: '', X: '', 5: '', 6: '', 7: ''}` | +| `#(((?(R)){0,2}) (?'x'((?'X')((?'R')))))#` | `array{0: non-falsy-string, 1: non-falsy-string, 2: string, x: '', 3: '', 4: '', X: '', 5: '', 6: '', R: '', 7: ''}` | +| `#(?J)(?'d'(?'d'\g{d}))#` | `array{0: string, d: string, 1: string, 2: string}` | +| `#(?J:(?\|(?'R')(\k'R')\|((?'R'))))#` | `array{0: string, R: string, 1: '', 2: string}` | +| `#^(?:(?(1)x\|)+)+$()#` | `array{string, ''}` | +| `#(?(R))*+#` | `array{string}` | +| `~((?x)(?#))#(?'abc')~` | `array{0: '#', 1: '', abc: '', 2: ''}` | +| `#[[:\\](?'abc')[a:]#` | `array{0: '::'\|':a'\|'[:'\|'[a'\|'\\:'\|'\\a', abc: '', 1: ''}` | +| `#()(?(R)0)*+#` | `array{string, ''}` | +| `#(?(?C{\Q})(?!(?'abc')))#` | `array{0: string, abc: '', 1: ''}\|array{string}` | +| `#(?1){3918}(((((0(\k'R'))))(?J)(?'R'(?'R'\3){99})))#` | `array{0: non-falsy-string, 1: non-empty-string, 2: non-empty-string, 3: non-empty-string, 4: non-empty-string, 5: non-empty-string, 6: string, R: string, 7: string, 8: string}` | +| `#(?\|(aaa)\|(b))(?1)#` | `array{'aaaaaa'\|'baaa', 'aaa'\|'b'}` | +| `#(?\|(?'a'aaa)\|(?'a'b))\k'a'#` | `array{0: non-falsy-string, a: 'aaa'\|'b', 1: 'aaa'\|'b'}` | +| `#(?\|(?'a'aaa)\|(?'a'b))(?'a'cccc)\k'a'#J` | `array{0: non-falsy-string, a: 'aaa'\|'b'\|'cccc', 1: 'aaa'\|'b', 2: 'cccc'}` | +| `#.*?a(*PRUNE)b#s` | `array{non-falsy-string}` | +| `#^a(*PRUNE)b#s` | `array{'ab'}` | +| `#0(?0)\|(1)(*THEN)(*SKIP:0)(*FAIL)#` | `array{0: non-empty-string, 1?: '1'}` | +| `#((*MARK:A))++a(*SKIP:B)b#` | `array{0: lowercase-string&non-falsy-string, 1: '', MARK?: 'A'}` | +| `#(^aa(*MARK:X)a(*SKIP:X)c\|(?!^)a+b)#` | `array{0: lowercase-string&non-falsy-string, 1: lowercase-string&non-falsy-string, MARK?: 'X'}` | +| `#(^aa(*MARK:X)a(*SKIP:Y)c\|(?!^)a+b)#` | `array{0: lowercase-string&non-falsy-string, 1: lowercase-string&non-falsy-string, MARK?: 'X'}` | +| `#(^aa(*MARK:Y)a(*SKIP:X)c\|(?!^)a+b)#` | `array{0: lowercase-string&non-falsy-string, 1: lowercase-string&non-falsy-string, MARK?: 'Y'}` | +| `#(^aa(*MARK:X)a(*SKIP:XX)c\|(?!^)a+b)#` | `array{0: lowercase-string&non-falsy-string, 1: lowercase-string&non-falsy-string, MARK?: 'X'}` | +| `#(^aa(*MARK:XX)a(*SKIP:X)c\|(?!^)a+b)#` | `array{0: lowercase-string&non-falsy-string, 1: lowercase-string&non-falsy-string, MARK?: 'XX'}` | +| `#(?'abcdefghijklmnopqrstuvwxyzABCDEFGabcdefghijklmnopqrstuvwxyzABCDEabcdefghijklmnopqrstuvwxyzABCDEabcdefghijklmnopqrstuvwxyzABCDEFG'justright)#` | `array{0: 'justright', abcdefghijklmnopqrstuvwxyzABCDEFGabcdefghijklmnopqrstuvwxyzABCDEabcdefghijklmnopqrstuvwxyzABCDEabcdefghijklmnopqrstuvwxyzABCDEFG: 'justright', 1: 'justright'}` | +| `#\n(?-x):?#x` | `array{''\|':'}` | +| `#(*MARK:A\x00b)#` | `array{0: '', MARK?: 'A\\x00b'}` | +| `#"(*MARK:A" 00 "b)"#` | `array{0: '""', MARK?: 'A" 00 "b'}` | +| `#(?<=((?C)0))#` | `array{'', '0'}` | +| `#ab(?C1)c#` | `array{'abc'}` | +| `#'ab(?C1)c'#` | `array{'\'abc\''}` | +| `#(?<=(?1))((?s))#A` | `array{'', ''}` | +| `#((?(?C'')\Q\E(?!((?(?C'')(?!X=X));=)r*X=X));=)#` | `array{0: non-falsy-string, 1: non-falsy-string, 2?: non-falsy-string}` | +| `#(?a)(?b)\g{b}\g{a}\g{a}\g{a}\g{a}(?xx)(?zz)#J` | `array{0: non-falsy-string, a: 'a'\|'xx', 1: 'a', b: 'b'\|'zz', 2: 'b', 3: 'xx', 4: 'zz'}` | +| `#(?1)(A(*COMMIT)\|B)D#` | `array{'AAD'\|'ABD'\|'BAD'\|'BBD', 'A'\|'B'}` | +| `#(?1){2}(a)#` | `array{'aaa', 'a'}` | +| `#(?1){2,4}(a)#` | `array{'aaa'\|'aaaa'\|'aaaaa', 'a'}` | +| `#^(?:\n(?:A\| (?:B\|B(*ACCEPT)) (?<=(.)) D)\n(Z)\n)+$#x` | `list{0: string, 1?: string, 2?: string}` | +| `#(a(?C1)(b)(c)d)+#` | `array{lowercase-string&non-falsy-string, 'abcd', 'b', 'c'}` | +| `#(a(*MARK:m)(*ACCEPT)){0}(?1)#` | `array{0: string, MARK?: 'm'}` | +| `#\d{0,3}(*:abc)(?C1)xxx#` | `array{0: lowercase-string&non-falsy-string, MARK?: 'abc'}` | +| `#(*CRLF)^X.+?Z#` | `array{non-falsy-string}` | +| `#(*BSR_ANYCRLF)^X\R+?Z#` | `array{non-falsy-string&uppercase-string}` | +| `#^(X(*THEN)Y\|AB){0}(?1)#` | `array{'AB'\|'XY'}` | +| `#^(?!A(?C1)B)C#` | `array{'C'}` | +| `#^(?(?!A(?C1)B)C)#` | `array{string}` | +| `#(*NO_JIT)(a+)b#` | `array{lowercase-string&non-falsy-string, lowercase-string&non-falsy-string}` | +| `#(*NO_JIT)a+(?C'XXX')b#` | `array{lowercase-string&non-falsy-string}` | +| `#(?(DEFINE)(?a?))^(?&optional_a)a$#` | `array{'a'\|'aa'}` | +| `#(?(DEFINE)(?a?)X)^(?&optional_a)a$#` | `array{'a'\|'aa'}` | +| `#^(a?)b(?1)a#` | `array{'aba'\|'abaa'\|'ba'\|'baa', ''\|'a'}` | +| `#^(a?)+b(?1)a#` | `array{lowercase-string&non-falsy-string, ''\|'a'}` | +| `#^(a?)++b(?1)a#` | `array{lowercase-string&non-falsy-string, ''\|'a'}` | +| `#(?<=(?=.){4,5}x)#` | `array{''}` | +| `#a(*PRUNE:X)bc\|qq#` | `array{0: 'abc'\|'qq', MARK?: 'X'}` | +| `#a(*THEN:X)bc\|qq#` | `array{0: 'abc'\|'qq', MARK?: 'X'}` | +| `#(?(?=^))b#` | `array{non-falsy-string}` | +| `#(?(?=^)\|)b#` | `array{non-falsy-string}` | +| `#(?(?=^)\|^)b#` | `array{non-falsy-string}` | +| `#(?(1)^())+b#` | `array{0: non-falsy-string, 1?: ''}` | +| `#(?(1)^()\|^)+b#` | `array{0: non-falsy-string, 1?: ''}` | +| `#(?(1)^()\|^)*b#` | `array{0: non-falsy-string, 1?: ''}` | +| `#(?(1)^a()\|^a)+b#` | `array{0: non-falsy-string, 1?: ''}` | +| `#(?(1)^\|^(a))+b#` | `array{0: non-falsy-string, 1?: 'a'}` | +| `#(?(1)^a()\|^a)*b#` | `array{0: non-falsy-string, 1?: ''}` | +| `#(*COMMIT:XX)^abc#` | `array{0: 'abc', MARK?: 'XX'}` | +| `#(*ACCEPT:XX)^abc#` | `array{0: string, MARK?: 'XX'}` | +| `#a(?:(*ACCEPT))??bc#` | `array{string}` | +| `#a(*ACCEPT)??bc#` | `array{string}` | +| `#a(*ACCEPT:XX)??bc#` | `array{0: string, MARK?: 'XX'}` | +| `#(?\|(a)\|(bcde))(c)\2#` | `array{non-falsy-string, 'a'\|'bcde', 'c'}` | +| `#(?\|(a)\|(bcde))(c)\1#` | `array{non-falsy-string, 'a'\|'bcde', 'c'}` | +| `#(?\|(?'A'a)\|(?'A'bcde))(?'B'c)\k'B'(?'A')#J` | `array{0: non-falsy-string, A: ''\|'a'\|'bcde', 1: 'a'\|'bcde', B: 'c', 2: 'c', 3: ''}` | +| `#(?\|(?'A'a)\|(?'A'bcde))(?'B'c)\k'A'(?'A')#J` | `array{0: non-falsy-string, A: ''\|'a'\|'bcde', 1: 'a'\|'bcde', B: 'c', 2: 'c', 3: ''}` | +| `#A(?:(*ACCEPT))?B#` | `array{string}` | +| `#(A(*ACCEPT)??B)C#` | `array{string, string}` | +| `#\A(*pla:.*\b(\w++))(?>.*?\b\1\b){3}#` | `array{string, non-empty-string}` | +| `#\A(*napla:.*\b(\w++))(?>.*?\b\1\b){3}#` | `array{string, non-empty-string}` | +| `#\A(?*.*\b(\w++))(?>.*?\b\1\b){3}#` | `array{string, non-empty-string}` | +| `#(*plb:(.)..\|(.)...)(\1\|\2)#` | `array{string, string, string, string}` | +| `#(*naplb:(.)..\|(.)...)(\1\|\2)#` | `array{string, string, string, string}` | +| `#(?<*(.)..\|(.)...)(\1\|\2)#` | `array{string, string, string, string}` | +| `#(*non_atomic_positive_lookahead:ab)#` | `array{''}` | +| `#(*non_atomic_positive_lookbehind:ab)#` | `array{''}` | +| `#(*pla:ab+)#` | `array{''}` | +| `#(*napla:ab+)#` | `array{''}` | +| `#(*napla:)+#` | `array{''\|numeric-string}` | +| `#(*naplb:)+#` | `array{''\|numeric-string}` | +| `#(*napla:^x\|^y)#` | `array{''}` | +| `#(*napla:abc\|abd)#` | `array{''}` | +| `#(*napla:a\|(.)(*ACCEPT)zz)\1..#` | `array{0: string, 1?: string}` | +| `#(*napla:a(*ACCEPT)zz\|(.))\1..#` | `array{0: string, 1?: string}` | +| `#(*napla:a\|(*COMMIT)(.))\1\1#` | `array{0: string, 1?: non-empty-string}` | +| `#(*napla:a\|(.))\1\1#` | `array{0: string, 1?: non-empty-string}` | +| `#(*naplb:ab?c\|PQ)...#` | `array{non-falsy-string}` | +| `#^(?\|(\*)(*napla:\S*_(\2?+.+))\|(\w)(?=\S*_(\2?+\1)))+_\2$#` | `array{non-falsy-string, non-empty-string, string}` | +| `#^(?\|(\*)(*napla:\S*_(\3?+.+))\|(\w)(?=\S*_((\2?+\1))))+_\2$#` | `array{0: non-falsy-string, 1: non-empty-string, 2: string, 3?: string}` | +| `#^((\1+)(?C)\|\d)+133X$#` | `array{0: non-falsy-string, 1: string, 2?: string}` | +| `#"(*MARK:>" 00 "<).."#` | `array{0: non-falsy-string, MARK?: '>" 00 "<'}` | +| `#"(*MARK:>" 00 "<).(?C1)."#` | `array{0: non-falsy-string, MARK?: '>" 00 "<'}` | +| `#(?=.{10}(?1))x(\K){0}#` | `array{string}` | +| `#(?=.{10}(.))(*scs:(1)(?2))x(\K){0}#` | `array{string, non-empty-string}` | +| `#(?=.{5}(?1))\d*(\K){0}#` | `array{string}` | +| `#(?>this line\s*((?R)\|)\K)#` | `array{string, string}` | +| `#(?>this line\s*((?R)\|)(*MARK:A))#` | `array{0: non-falsy-string, 1: string, MARK?: 'A'}` | +| `#(?(?a)\|(?Pb))(?P=same))+#J` | `array{0: non-falsy-string, same?: ''\|'a'\|'b', 1?: ''\|'a', 2?: 'b'}` | +| `#(*ACCEPT)+#` | `array{string}` | +| `#a?(?=b(*COMMIT)c\|)d#` | `array{'ad'\|'d'}` | +| `#(?=b(*COMMIT)c\|)d#` | `array{'d'}` | +| `#(*MARK:a/y_)#` | `array{0: '', MARK?: 'a/y_'}` | +| `#(a)(*scs:(1)b)*c#` | `array{lowercase-string&non-falsy-string, 'a'}` | +| `#(a)(*scs:(1)b)*?c#` | `array{lowercase-string&non-falsy-string, 'a'}` | +| `#(a)(*scs:(1)b)*+c#` | `array{lowercase-string&non-falsy-string, 'a'}` | +| `#(a)(*scs:(1)b)+c#` | `array{lowercase-string&non-falsy-string, 'a'}` | +| `#(a)(*scs:(1)b)+?c#` | `array{lowercase-string&non-falsy-string, 'a'}` | +| `#(a)(*scs:(1)b)++c#` | `array{lowercase-string&non-falsy-string, 'a'}` | +| `#(a)(*scs:(1)b)?c#` | `array{'ac', 'a'}` | +| `#(a)(*scs:(1)b)??c#` | `array{'ac', 'a'}` | +| `#(a)(*scs:(1)b)?+c#` | `array{'ac', 'a'}` | +| `#(a)(*scs:(1)b){3}c#` | `array{'ac', 'a'}` | +| `#(a)(*scs:(1)b){3,5}?c#` | `array{'ac', 'a'}` | +| `#(a)(*scs:(1)b){3,}+c#` | `array{lowercase-string&non-falsy-string, 'a'}` | +| `#([a-z]++)(*scs:(1)(stx)\|(ne))(.)#` | `array{non-falsy-string, lowercase-string&non-falsy-string, ''\|'stx', ''\|'ne', non-empty-string}` | +| `~(?[a-z]++)##(*scan_substring:('XX').*(..)$)\2~` | `array{0: non-falsy-string, XX: lowercase-string&non-falsy-string, 1: lowercase-string&non-falsy-string, 2?: non-falsy-string}` | +| `~([a-z])([a-z]++)(#+)(*scs:(2)(ab.))~` | `array{0: lowercase-string&non-falsy-string, 1: 'a'\|'b'\|'c'\|'d'\|'e'\|'f'\|'g'\|'h'\|'i'\|'j'\|'k'\|'l'\|'m'\|'n'\|'o'\|'p'\|'q'\|'r'\|'s'\|'t'\|'u'\|'v'\|'w'\|'x'\|'y'\|'z', 2: lowercase-string&non-falsy-string, 3: non-falsy-string, 4?: non-falsy-string}` | +| `#(?:(?[a-z]++)\|(?[0-9]++)\|$)(*scan_substring:('YYY')((?.).*\k$))#J` | `array{0: lowercase-string, YYY: lowercase-string&non-falsy-string, 1: lowercase-string&non-falsy-string, 2?: '', 3?: string, START?: non-empty-string, 4?: non-empty-string}\|array{0: lowercase-string, YYY: numeric-string, 1: '', 2: numeric-string, 3?: string, START?: non-empty-string, 4?: non-empty-string}\|array{0: lowercase-string, YYY?: '', 1?: '', 2?: '', 3?: string, START?: non-empty-string, 4?: non-empty-string}` | +| `~([a-zA-Z]+)(*scs:(1).*?(?[A-Z]+)(*scan_substring:('ABC').*(.)\3))#+~` | `array{0: non-falsy-string, 1: non-falsy-string, ABC?: uppercase-string, 2?: uppercase-string, 3?: non-empty-string}` | +| `#([a-zA-Z]+)(*scs:(1)(xy\|ab(*ACCEPT)cd))#` | `array{0: string, 1: string, 2?: string}` | +| `#(?[a-zA-Z]+)(*scs:('AA')(ab(*ACCEPT)cd\|xy))#` | `array{0: string, AA: string, 1: string, 2?: string}` | +| `~([a-z]++)##(*scs:(1)(abc))?!~` | `array{0: lowercase-string&non-falsy-string, 1: lowercase-string&non-falsy-string, 2?: 'abc'}` | +| `~([a-z]++)##(*scs:(1)(abc))??(?(2)!\|:)~` | `array{0: non-falsy-string, 1: lowercase-string&non-falsy-string, 2?: 'abc'}` | +| `~([a-z]++)##(*scs:(1)(abc)\|xyz){8}(?(2)!\|:)~` | `array{0: non-falsy-string, 1: lowercase-string&non-falsy-string, 2?: 'abc'}` | +| `~[A-Z]{3}([A-Z]++)#(*scs:(1)(?<=BC)XY)#~` | `array{non-falsy-string&uppercase-string, non-falsy-string&uppercase-string}` | +| `#()(\w++)=(*scs:(2)(?=abc))(\w++)#` | `array{non-falsy-string, '', non-empty-string, non-empty-string}` | +| `#(\d++)(*scs:(1)\d+\z)(\w+)#` | `array{non-falsy-string, numeric-string, non-empty-string}` | +| `#(\d++)(*scs:(1)\d+\Z)(\w+)#` | `array{non-falsy-string, numeric-string, non-empty-string}` | +| `#(\d++)(*scs:(1)\d+$)(\w+)#` | `array{non-falsy-string, numeric-string, non-empty-string}` | +| `#([a-z]{2})[a-z](*scs:(1)(.*?))\2$#` | `array{0: non-falsy-string, 1: lowercase-string&non-falsy-string, 2?: string}` | +| `~^(([a-z]([a-z]*+))(*scs:(2).(?=(?1)\|$)\3)\|#){5}~` | `list{0: lowercase-string&non-falsy-string, 1: lowercase-string&non-falsy-string, 2?: lowercase-string, 3?: lowercase-string}` | +| `#(*scs:(1)a)(a)\|x#` | `array{0: 'a'\|'x', 1?: 'a'}` | +| `#(*scs:()a)(?a)(?b)(?c)(?d)\|x#J` | `array{'abcd'\|'x'}\|array{0: 'abcd'\|'x', GOOD_NAME: 'a'\|'b'\|'c'\|'d', 1: 'a', 2: 'b', 3: 'c', 4: 'd'}` | +| `#(*scs:(1)a)?(a)#` | `array{'a', 'a'}` | +| `#(*scs:(1)a)??(a)#` | `array{'a', 'a'}` | +| `#x(?\|(*scs:(1)(?<=(.)))\|()){8}#` | `array{0: 'x', 1?: string}` | +| `#(a)(b)(*scs:(2)(*scs:(1)a(*PRUNE)x)).+\|(.+)#` | `array{non-empty-string, '', '', non-empty-string}\|array{non-empty-string, 'a', 'b'}` | +| `#(a)(b)(*scs:(2)(*scs:(1)a(*PRUNE:markstr)x)).+\|(.+)#` | `array{0: non-empty-string, 1: '', 2: '', 3: non-empty-string, MARK?: 'markstr'}\|array{0: non-empty-string, 1: 'a', 2: 'b', MARK?: 'markstr'}` | +| `#(a)(b)(*scs:(2)(*scs:(1)a(*PRUNE:markstr))).+\|(.+)#` | `array{0: non-empty-string, 1: '', 2: '', 3: non-empty-string, MARK?: 'markstr'}\|array{0: non-empty-string, 1: 'a', 2: 'b', MARK?: 'markstr'}` | +| `#(a)(b)(*scs:(2)(*scs:(1)a(*COMMIT)x)).+\|(.+)#` | `array{non-empty-string, '', '', non-empty-string}\|array{non-empty-string, 'a', 'b'}` | +| `#(a)(b)(*scs:(2)(*scs:(1)a(*COMMIT:markstr)x)).+\|(.+)#` | `array{0: non-empty-string, 1: '', 2: '', 3: non-empty-string, MARK?: 'markstr'}\|array{0: non-empty-string, 1: 'a', 2: 'b', MARK?: 'markstr'}` | +| `#(a)(b)(*scs:(2)(*scs:(1)a(*COMMIT:markstr))).+\|(.+)#` | `array{0: non-empty-string, 1: '', 2: '', 3: non-empty-string, MARK?: 'markstr'}\|array{0: non-empty-string, 1: 'a', 2: 'b', MARK?: 'markstr'}` | +| `#(abc)(def)(*scs:(1)(*scs:(2)de(*SKIP)x)).+\|(.+)#` | `array{non-empty-string, '', '', non-empty-string}\|array{non-empty-string, 'abc', 'def'}` | +| `#(abc)(def)(*scs:(2)(*scs:(1)(*SKIP)x)).+\|(.+)#` | `array{non-empty-string, '', '', non-empty-string}\|array{non-empty-string, 'abc', 'def'}` | +| `#(?<=(abc))(def)(*scs:(2)(*scs:(1)(*SKIP)x)).+\|(ef.+)#` | `array{non-falsy-string, '', '', non-falsy-string}\|array{non-falsy-string, 'abc', 'def'}` | +| `#(abc)(def)(*scs:(2)(?:(*scs:(1)abc(*SKIP:notfound)x\|abcd\|(abc)))).+#` | `array{0: non-falsy-string, 1: 'abc', 2: 'def', 3?: 'abc'}` | +| `#(abc)(def)(*MARK:markstr)(*scs:(2)(?:(*scs:(1)abc(*SKIP:markstr)x))).+\|(.+)#` | `array{0: non-empty-string, 1: '', 2: '', 3: non-empty-string, MARK?: 'markstr'}\|array{0: non-empty-string, 1: 'abc', 2: 'def', MARK?: 'markstr'}` | +| `~^([a-z]++)(?:((?6))\|((?7))\|((?8))\|(#))(?(DEFINE)((*scs:(1)abc(*PRUNE)d))((*scs:(1)abc(*COMMIT)e))((*scs:(1)abc(*SKIP)f)))~` | `list{0: lowercase-string&non-falsy-string, 1: lowercase-string&non-falsy-string, 2: '', 3?: '', 4?: '', 5?: '#'}` | +| `#\b(\w++)(*scs:(1)^)#` | `array{non-empty-string, non-empty-string}` | +| `#(?:(?'A'a)\|(?b))(*scs:('A')b)c#J` | `array{0: 'ac'\|'bc', A: 'a', 1: 'a'}\|array{0: 'ac'\|'bc', A: 'b', 1: '', 2: 'b'}` | +| `#(xyz)(abc)(*scs:(-1)abc)(*scs:(-2)\1)#` | `array{'xyzabc', 'xyz', 'abc'}` | +| `~^([a-z]++)#(*scs:(1)a\|ab\|abc\|abcd\|abcde\|abcdef\|(abcdefg))\2~` | `array{0: non-falsy-string, 1: lowercase-string&non-falsy-string, 2?: 'abcdefg'}` | +| `#^([a-z]++)(*scs:(1)(a+)(*THEN)b\|(a+)(*THEN)c\|(aa))#` | `list{0: lowercase-string&non-falsy-string, 1: lowercase-string&non-falsy-string, 2?: lowercase-string, 3?: lowercase-string, 4?: 'aa'}` | +| `#^([a-z]++)(*scs:(1)((a+)(*THEN)b)\|(a+)(*THEN)c\|(aa))#` | `list{0: lowercase-string&non-falsy-string, 1: lowercase-string&non-falsy-string, 2?: lowercase-string, 3?: lowercase-string, 4?: lowercase-string, 5?: 'aa'}` | +| `#^([a-z]++)(*scs:(1)((a+)(*THEN)b))?#` | `list{0: lowercase-string&non-falsy-string, 1: lowercase-string&non-falsy-string, 2?: lowercase-string, 3?: lowercase-string&non-falsy-string}` | +| `#^([a-z]++)(*scs:(1)(abc\|(a+)(*THEN)b))?#` | `list{0: lowercase-string&non-falsy-string, 1: lowercase-string&non-falsy-string, 2?: lowercase-string, 3?: lowercase-string&non-falsy-string}` | +| `~^(?:(.){20,30}#\|([a-z]++)(*scs:(1)(a+)(*THEN)b){20,30}#\|(.){20,30}!)~` | `array{non-falsy-string, '', '', '', non-empty-string}\|array{non-falsy-string, '', lowercase-string&non-falsy-string, lowercase-string&non-falsy-string}\|array{non-falsy-string, non-empty-string}` | +| `#(?:(abc)\|(?def)\|ghi)(*scs:(1,'PP').(.))#` | `array{0: 'abc'\|'def'\|'ghi', 1: '', PP: 'def', 2: 'def', 3?: non-empty-string}\|array{0: 'abc'\|'def'\|'ghi', 1?: ''\|'abc', PP?: '', 2?: '', 3?: non-empty-string}` | +| `#(?:(?abc)\|(?def)\|(ghi)\|(?'NN'jkl)\|mno)(*scs:('MM',3,).(.))#J` | `array{0: 'abc'\|'def'\|'ghi'\|'jkl'\|'mno', MM: '', 1: '', 2: '', 3: '', NN: 'jkl', 4: 'jkl', 5?: non-empty-string}\|array{0: 'abc'\|'def'\|'ghi'\|'jkl'\|'mno', MM: 'abc', 1: 'abc', 2?: '', 3?: '', NN?: '', 4?: '', 5?: non-empty-string}\|array{0: 'abc'\|'def'\|'ghi'\|'jkl'\|'mno', MM: 'def', 1: '', 2: 'def', 3?: '', NN?: '', 4?: '', 5?: non-empty-string}\|array{0: 'abc'\|'def'\|'ghi'\|'jkl'\|'mno', MM?: '', 1?: '', 2?: '', 3?: ''\|'ghi', NN?: '', 4?: '', 5?: non-empty-string}` | +| `#f(?:(*scs:(+1,+2)(?<=(.)))\|()){16}#` | `list{0: 'f', 1?: string, 2?: ''}` | +| `#(?a)(*scan_substring:(1,'AA',1,)a)b#` | `array{0: 'ab', AA: 'a', 1: 'a'}` | +| `#()()()(?<=ab(*scs:(1,2,3))cd)xyz#` | `array{'xyz', '', '', ''}` | +| `#()()()(?<=ab(*ACCEPT)(*scs:(1,2,3))cd\|efg)xyz#` | `array{string, string, string, string}` | +| `#(a)(*scs:(1)a(*ACCEPT))bbb#` | `array{string, string}` | +| `#(a)(b+)(*scs:(1)a(*ACCEPT))(\2)#` | `array{0: string, 1: string, 2: string, 3?: string}` | +| `#(a)(b)(c)(d)(*scs:(4,3,1,2,2,1,3,3,4,4)x)#` | `array{'abcd', 'a', 'b', 'c', 'd'}` | +| `#(?a)(?b)(?c)(d)(*scs:(2,3,1,1,,)abc)#J` | `array{0: 'abcd', n: 'a'\|'b'\|'c', 1: 'a', 2: 'b', 3: 'c', 4: 'd'}` | +| `#(?a)(?b)(?c)(d)(*scs:(2,1,1,,)abc)#J` | `array{0: 'abcd', n: 'a'\|'b'\|'c', 1: 'a', 2: 'b', 3: 'c', 4: 'd'}` | +| `#(?a)(?b)(?c)(d)(?e)(?f)(*scs:(,5,3,2,1,4,1,4,,6,,)abc)#J` | `array{0: 'abcdef', n: 'a'\|'b'\|'c', 1: 'a', 2: 'b', 3: 'c', 4: 'd', m: 'e'\|'f', 5: 'e', 6: 'f'}` | +| `#a(?C)b#` | `array{'ab'}` | +| `#(?[\n+[a]&\n+(\n+[a]&\n+(\n+[a]&\n+(\n+[a]&\n+(\n+[a]&\n+(\n+[a]&\n+(\n+[a]&\n+(\n+[a]&\n+(\n+[a]&\n+(\n+[a]&\n+(\n+[a]&\n+(\n+[a]&\n+(\n+[a]&\n+(\n+[a]&\n+(\n&\n))))))))))))))])#` | `array{non-empty-string}` | +| `#(?[\d-[1]]).(?.))(?1('id'))(.)#` | `array{0: non-falsy-string, 1: non-falsy-string, 2: non-empty-string, id: non-empty-string, 3: non-empty-string, 4: non-empty-string}` | +| `#^(?1(3,2,2,3,2))(?1(3))(?(DEFINE)(a(.)b(.)c))#` | `array{non-falsy-string, '', non-empty-string, non-empty-string}\|array{non-falsy-string}` | +| `~(?1(2))#(?1(3))#(?1(4))#(?(DEFINE)((.)\2(.)\3(.)\4))~` | `array{non-falsy-string, '', non-empty-string, non-empty-string, non-empty-string}\|array{non-falsy-string}` | +| `~^(?1(1,2)){2,4}(?(DEFINE)((..)#))xx#~` | `array{non-falsy-string, non-falsy-string, non-falsy-string}\|array{non-falsy-string}` | +| `~^(?1(1,2)){2,}+(?(DEFINE)((..)#))!~` | `array{non-falsy-string, non-falsy-string, non-falsy-string}\|array{non-falsy-string}` | +| `#^(?1)(?(DEFINE)(<(?2(3,4))><\4\3>)((..)(..)))#` | `array{non-falsy-string}` | +| `~(?1)#(?(DEFINE)(a(.(*ACCEPT).)b))~` | `array{string}` | +| `#^(?>(?1()))!(?(DEFINE)((?..)(?..)))\|abcde#J` | `array{0: non-falsy-string, 1: '', n: non-falsy-string, 2: non-falsy-string, 3: non-falsy-string}\|array{non-falsy-string}` | +| `#<(?:[^<>]*?(?:(AB)[^<>]*\|)(?:\|(?R(1))))+>#` | `array{0: non-falsy-string, 1?: 'AB'}` | +| `~(?:(?1())#){4}(?(DEFINE)((?()\2)(?.{3})))$~` | `array{0: non-falsy-string, 1: '', prefix: non-falsy-string, 2: non-falsy-string}\|array{non-falsy-string}` | +| `~ # Define the routine "weekendday" which matches Saturday or Sunday, and\n # returns the Sat/Sun prefix as \k.\n (?(DEFINE)(?(?\|(?Sat)urday\|(?Sun)day)))\n # Call the routine. Matches "Saturday,Sat" or "Sunday,Sun".\n (?&weekendday()),\k ~x` | `array{0: non-falsy-string, weekendday: '', 1: '', short: 'Sat'\|'Sun', 2: 'Sat'\|'Sun'}\|array{non-falsy-string}` | +| `#()()()(?2(2,3,2,3,2))#` | `array{'', '', '', ''}` | +| `#(?(R)(Sat)urday\|(?R(1)),\1)#` | `array{0: non-empty-string, 1?: 'Sat'}` | +| `#(?(DEFINE)((Sat)urday))(?1(2)),\2#` | `array{non-falsy-string, '', 'Sat'}\|array{non-falsy-string}` | +| `#(?(DEFINE)((Sat)urday))(?-2(-1)),\2#` | `array{non-falsy-string, '', 'Sat'}\|array{non-falsy-string}` | +| `#(?+1(+2)),\2(?(DEFINE)((Sat)urday))#` | `array{non-falsy-string, '', 'Sat'}\|array{non-falsy-string}` | +| `#(?(DEFINE)(?(?Sat)urday))(?&fn('ret')),\k#` | `array{0: non-falsy-string, fn: '', 1: '', ret: 'Sat', 2: 'Sat'}\|array{non-falsy-string}` | +| `#(?(DEFINE)(?(?Sat)urday))(?P>fn()),\k#` | `array{0: non-falsy-string, fn: '', 1: '', ret: 'Sat', 2: 'Sat'}\|array{non-falsy-string}` | +| `#(?(DEFINE)((Sat)urday))(?1),\2#` | `array{non-falsy-string}` | +| `#(?(DEFINE)((Sat)(urday)))(?1(2,3)),\2,\3#` | `array{non-falsy-string, '', 'Sat', 'urday'}\|array{non-falsy-string}` | +| `#(?C1)A\K(?C2)B(?C3)C#` | `array{string}` | +| `#A\K(?C"after")BC#` | `array{string}` | +| `#A\K(?C1)B\K(?C2)C#` | `array{string}` | +| `#A(?:B\K(?C1)D\|(?C2)B)(?C3)C#` | `array{string}` | +| `#A\K(?(?C1)(?=B)B\|D)C#` | `array{string}` | +| `#(?(DEFINE)(?A\K(?C1)B))(?&x)(?C2)C#` | `array{string}` | +| `#A\K(?C1)B#` | `array{string}` | +| `#(?C1)A\K(?C2)B#` | `array{string}` | +| `#(?C1)A(?C2)B(?C3)C#` | `array{'ABC'}` | +| `#(?(?&NAME_PAT))\s+(?(?&ADDRESS_PAT))\n (?(DEFINE)\n (?[a-z]+)\n (?\d+)\n )#x` | `array{0: lowercase-string&non-falsy-string, NAME: lowercase-string&non-falsy-string, 1: lowercase-string&non-falsy-string, ADDR: numeric-string, 2: numeric-string}` | +| `#^(?:((.)(?1)\2\|)\|((.)(?3)\4\|.))$#i` | `list{0: string, 1: string, 2?: string, 3?: non-empty-string, 4?: non-empty-string}` | +| `#(?:(?foo)\|(?bar))\k#J` | `array{0: non-falsy-string, n: 'bar', 1: '', 2: 'bar'}\|array{0: non-falsy-string, n: 'foo', 1: 'foo'}` | +| `#a[--0]z#` | `array{'a-z'\|'a.z'\|'a/z'\|'a0z'}` | +| `#[-----]#` | `array{'-'}` | +| `#[------]#` | `array{'-'}` | +| `#[!------]#` | `array{'!'\|'"'\|'#'\|'$'\|'%'\|'&'\|'\''\|'('\|')'\|'*'\|'+'\|','\|'-'}` | +| `#^[\pL](abc)(?1)#` | `array{non-falsy-string, 'abc'}` | +| `#(?(?=.*b)(?=.*b)\pL\|.*c)#` | `array{non-empty-string}` | +| `#^(*script_run:.{4})#u` | `array{non-falsy-string}` | +| `#^(*sr:.{4}\|..)#u` | `array{non-falsy-string}` | +| `#^(*atomic_script_run:.{4}\|..)#u` | `array{non-falsy-string}` | +| `#^(*asr:.*)#u` | `array{string}` | +| `#^(?>(*sr:.*))#u` | `array{string}` | +| `#^(*sr:.*)#u` | `array{string}` | +| `#^(*sr:\x{2e80}*)#u` | `array{string}` | +| `#^(*sr:\x{2e80}*)\x{2e80}#u` | `array{non-falsy-string}` | +| `#^(*sr:.*)Test#u` | `array{non-falsy-string}` | +| `#^(*sr:(.{2})){2}#u` | `array{non-falsy-string, non-falsy-string}` | +| `#^(*sr:\S*)#u` | `array{string}` | +| `#(?:]]X#` | `array{'X'}` | +| `#^(?1)\p{Nd}{3}(a)#` | `array{lowercase-string&non-falsy-string, 'a'}` | +| `#\p{Nd}{0,3}[\pL](*:abc)(?C1)xxx#` | `array{0: non-falsy-string, MARK?: 'abc'}` | +| `#(*CRLF)^X.+?Z#u` | `array{non-falsy-string}` | +| `#(*BSR_ANYCRLF)^X\R+?Z#u` | `array{non-falsy-string&uppercase-string}` | +| `#^(\p{Adlam}+)(\p{Bhaiksuki}+)(\p{Marchen}+)(\p{Newa}+)(\p{Osage}+)\n (\p{Tangut}+)(\p{Masaram_Gondi}+)(\p{Nushu}+)(\p{Soyombo}+)\n (\p{Zanabazar_Square}+)#ux` | `array{non-falsy-string, non-empty-string, non-empty-string, non-empty-string, non-empty-string, non-empty-string, non-empty-string, non-empty-string, non-empty-string, non-empty-string, non-empty-string}` | +| `#^(?:(\X)(?C))+$#u` | `array{non-empty-string, non-empty-string}` | +| `#^(\p{Dogra}+)(\p{Gunjala_Gondi}+)(\p{Hanifi_Rohingya}+)(\p{Makasar}+)\n (\p{Medefaidrin}+)(\p{Old_Sogdian}+)(\p{Sogdian}+)#ux` | `array{non-falsy-string, non-empty-string, non-empty-string, non-empty-string, non-empty-string, non-empty-string, non-empty-string, non-empty-string}` | +| `#^(*sr:.*(*ACCEPT))#u` | `array{string}` | +| `#^(*sr:.{4})#u` | `array{non-falsy-string}` | +| `#s(?r)s(?-r)s(?r:s)s#iu` | `array{non-falsy-string}` | +| `#(.) (?r:\1)#iu` | `array{non-falsy-string, non-empty-string}` | +| `#(?:(?ss)\|(?kk)) \k#Jiu` | `array{0: non-falsy-string, A: non-falsy-string, 1: '', 2: non-falsy-string}\|array{0: non-falsy-string, A: non-falsy-string, 1: non-falsy-string}` | +| `#(?:(?s)\|(?k)) \k{3,}!#Jiu` | `array{0: non-falsy-string, A: non-falsy-string, 1: '', 2: non-falsy-string}\|array{0: non-falsy-string, A: non-falsy-string, 1: non-falsy-string}` | +| `#(*TURKISH_CASING)(.) \1#iu` | `array{non-falsy-string, non-empty-string}` | +| `#(*UCP)(*UTF)[[:alnum:]](?aP:[[:alnum:]])[[:alnum:]]#` | `array{non-falsy-string}` | +| `#1(abc\|xyz)2(?1)3#` | `array{'1abc2abc3'\|'1abc2xyz3'\|'1xyz2abc3'\|'1xyz2xyz3', 'abc'\|'xyz'}` | +| `#^(a*\w\|ab)=(?1)#` | `array{non-falsy-string, non-empty-string}` | +| `#^(?(?!abc)\d\d\|\w{3}:)$#` | `array{non-falsy-string}` | +| `#abc(?C1)xyz#` | `array{'abcxyz'}` | +| `#(?[[\p{Ll}]+[\p{Nd}]])#` | `array{non-empty-string}` | +| `#(?[[\p{Ll}Z]&[\p{Lu}a]])#` | `array{non-empty-string}` | +| `#(a(?1)b)#` | `array{non-falsy-string, non-falsy-string}` | +| `#(a(?1)+b)#` | `array{non-falsy-string, non-falsy-string}` | +| `#(?Pa)...(?P=a)bbb(?P>a)d#` | `array{0: non-falsy-string, a: 'a', 1: 'a'}` | +| `#abc(?C255)de(?C)f#` | `array{'abcdef'}` | +| `#abc(d\|e)(*THEN)x(123(*THEN)4\|567(b\|q)(*THEN)xx)#` | `array{0: 'abcdx1234'\|'abcdx567bxx'\|'abcdx567qxx'\|'abcex1234'\|'abcex567bxx'\|'abcex567qxx', 1: 'd'\|'e', 2: '1234'\|'567bxx'\|'567qxx', 3?: 'b'\|'q'}` | +| `~(?1)(?#?'){2}(a)~` | `array{'aaa', 'a'}` | +| `#(?1)()((((((\1++))\x85)+)\|))#` | `array{string, '', string, string, non-falsy-string, non-falsy-string, string, string}\|array{string, '', string, string}` | +| `#(?(1)(?1)){8,}+()#` | `array{string, ''}` | +| `#(?(1)\|a(?1)b){2,}+()#` | `array{string, ''}` | diff --git a/poc/witnesses/php-src.json b/poc/witnesses/php-src.json new file mode 100644 index 00000000000..696f28202db --- /dev/null +++ b/poc/witnesses/php-src.json @@ -0,0 +1,32 @@ +[ + { + "pattern": "/.*\\p{N}/", + "call": "match:null", + "subject": "MA==", + "engine": "YToxOntpOjA7czoxOiIwIjt9", + "reason": "falsy", + "key": 0, + "phplrt": "array{non-falsy-string}", + "phpregex": "array{non-empty-string}" + }, + { + "pattern": "/\\G\\w/u", + "call": "match:null", + "subject": "MA==", + "engine": "YToxOntpOjA7czoxOiIwIjt9", + "reason": "falsy", + "key": 0, + "phplrt": "array{non-falsy-string}", + "phpregex": "array{non-empty-string}" + }, + { + "pattern": "~\\A.~", + "call": "match:null", + "subject": "MA==", + "engine": "YToxOntpOjA7czoxOiIwIjt9", + "reason": "falsy", + "key": 0, + "phplrt": "array{non-falsy-string}", + "phpregex": "array{non-empty-string}" + } +] diff --git a/poc/witnesses/php-src.md b/poc/witnesses/php-src.md new file mode 100644 index 00000000000..a78dafb6705 --- /dev/null +++ b/poc/witnesses/php-src.md @@ -0,0 +1,30 @@ +# php-src ext/pcre tests + +One witness for each of the 3 patterns whose inferred type the engine contradicts with the parser generated by phplrt. `php poc/bin/verify.php` replays them. + +- a value that can be '0' or '' is typed non-falsy or non-empty: 3 +- a value outside the type of its key: 0 +- a key the engine writes is typed absent: 0 +- a key the engine leaves out is typed present: 0 +- the subject is narrowed further than the match proves: 0 + +| pattern | call | subject | the engine writes | phplrt | PHPRegex | why | +|---|---|---|---|---|---|---| +| `/.*\p{N}/` | preg_match() | `'0'` | `["0"]` | `array{non-falsy-string}` | `array{non-empty-string}` | key `0`: a value that can be '0' or '' is typed non-falsy or non-empty | +| `/\G\w/u` | preg_match() | `'0'` | `["0"]` | `array{non-falsy-string}` | `array{non-empty-string}` | key `0`: a value that can be '0' or '' is typed non-falsy or non-empty | +| `~\A.~` | preg_match() | `'0'` | `["0"]` | `array{non-falsy-string}` | `array{non-empty-string}` | key `0`: a value that can be '0' or '' is typed non-falsy or non-empty | + +## Patterns the parser generated by phplrt infers no shape for + +8 patterns, `preg_match()` without flags. + +| pattern | PHPRegex | +|---|---| +| `#\[(right)\](((?R)\|[^[]+?\|\[)*)\[/\1\]#siU` | `array{0: non-falsy-string, 1: non-falsy-string, 2: string, 3?: string}` | +| `/((?1)?z)/` | `array{non-falsy-string, non-falsy-string}` | +| `/(?J)(?:(?foo)\|(?bar))(?baz)/` | `array{0: 'barbaz'\|'foobaz', g: 'bar', 1: '', 2: 'bar', h: 'baz', 3: 'baz'}\|array{0: 'barbaz'\|'foobaz', g: 'foo', 1: 'foo', 2: '', h: 'baz', 3: 'baz'}` | +| `/(?P[abc])(*MARK:value)/` | `array{0: 'a'\|'b'\|'c', MARK: 'a'\|'b'\|'c'\|'value', 1: 'a'\|'b'\|'c'}` | +| `/(*NO_JIT)^(A{1,2}B)+$$/` | `array{non-falsy-string&uppercase-string, 'AAB'\|'AB'}` | +| `~(*NO_JIT)(a)*~` | `array{0: lowercase-string, 1?: 'a'}` | +| `/s(?r)s(?-r)s(?r:s)s/iu` | `array{non-falsy-string}` | +| `/(?[ac])(?\d)\|(?[b])/J` | `array{0: lowercase-string&non-falsy-string, chr: 'a'\|'c', 1: 'a'\|'c', num: numeric-string, 2: numeric-string}\|array{0: lowercase-string&non-falsy-string, chr: 'b', 1: '', num: '', 2: '', 3: 'b'}` | diff --git a/src/Analyser/ArgumentsHandler.php b/src/Analyser/ArgumentsHandler.php index 4e84ba02c59..dae56478e02 100644 --- a/src/Analyser/ArgumentsHandler.php +++ b/src/Analyser/ArgumentsHandler.php @@ -625,6 +625,15 @@ public function processArgs( } $argContext = $context->enterDeep(); if (!$arg->unpack && $arg->value instanceof Expr\Array_) { + if ($parameter !== null) { + // an extension can type the closures of an array literal by describing the whole array + $overwritingParameterType = $this->getParameterTypeFromParameterClosureTypeExtension($callLike, $calleeReflection, $parameter, $scopeToPass); + if ($overwritingParameterType !== null) { + $parameterType = $overwritingParameterType; + $parameterNativeType = $this->getParameterTypeFromParameterClosureTypeExtension($callLike, $calleeReflection, $parameter, $scopeToPass->doNotTreatPhpDocTypesAsCertain()) ?? $parameterNativeType; + $passedToTypeResolver = null; + } + } $argContext = $argContext->enterPassedToType($parameterType, $parameterNativeType); if ($passedToTypeResolver !== null) { $argContext = $argContext->enterPassedToTypeResolver($passedToTypeResolver); diff --git a/src/Analyser/ResultCache/ResultCacheManager.php b/src/Analyser/ResultCache/ResultCacheManager.php index 72c1e6a005a..d7f38ab111c 100644 --- a/src/Analyser/ResultCache/ResultCacheManager.php +++ b/src/Analyser/ResultCache/ResultCacheManager.php @@ -27,6 +27,7 @@ use PHPStan\Internal\ArrayHelper; use PHPStan\Internal\ComposerHelper; use PHPStan\Php\ComposerPhpVersionFactory; +use PHPStan\Php\PcreVersion; use PHPStan\Php\PhpVersion; use PHPStan\PhpDoc\StubFilesProvider; use PHPStan\Reflection\BetterReflection\SourceStubber\ExtensionVersionProvider; @@ -230,6 +231,7 @@ public function __construct( #[AutowiredParameter(ref: '%rootDir%')] private string $anchorDirectory, private PhpVersion $phpVersion, + private PcreVersion $pcreVersion, private ComposerPhpVersionFactory $composerPhpVersionFactory, private FileStatSignatures $fileStatSignatures, private ValueDependencyCollector $valueDependencyCollector, @@ -2771,6 +2773,9 @@ private function getMeta(array $allAnalysedFiles, ?array $projectConfigArray): a // below come from its require section, which is not in composer.lock's content hash either, // so a composer update need not move anything else in this metadata. 'phpVersionForAnalysis' => [$this->phpVersion->getVersionId(), $this->phpVersion->getSource()], + // regex shapes depend on the PCRE2 version, which can come from the pcreVersion + // parameter or from config.platform.lib-pcre in composer.json + 'pcreVersionForAnalysis' => [$this->pcreVersion->getVersionId(), $this->pcreVersion->getSource()], 'composerPhpVersionRange' => [ $composerMinPhpVersion !== null ? $composerMinPhpVersion->getVersionId() : null, $composerMaxPhpVersion !== null ? $composerMaxPhpVersion->getVersionId() : null, diff --git a/src/Command/IgnoredRegexValidator.php b/src/Command/IgnoredRegexValidator.php index cfcfb22524c..80884e94581 100644 --- a/src/Command/IgnoredRegexValidator.php +++ b/src/Command/IgnoredRegexValidator.php @@ -3,25 +3,23 @@ namespace PHPStan\Command; use Nette\Utils\Strings; -use Phplrt\Contracts\Parser\Exception\ParserExceptionInterface; -use Phplrt\Source\StringSource; +use PHPRegex\Parser\Exception\ExceptionInterface; +use PHPRegex\Parser\Node\AlternationNode; +use PHPRegex\Parser\Node\AnchorNode; +use PHPRegex\Parser\Node\NodeInterface; +use PHPRegex\Parser\Node\SequenceNode; 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\Regex\PHPRegexReader; use PHPStan\Type\VerbosityLevel; use function count; -use function strrpos; use function substr; final class IgnoredRegexValidator { public function __construct( - private RegexParser $parser, + private PHPRegexReader $reader, private TypeStringResolver $typeStringResolver, ) { @@ -29,27 +27,24 @@ public function __construct( public function validate(string $regex): IgnoredRegexValidatorResult { - $regex = $this->removeDelimiters($regex); + ['body' => $regex, 'modifiers' => $modifiers, 'delimiter' => $delimiter] = $this->reader->split($regex); try { - $ast = $this->parser->parse(StringSource::createFromString($regex)); - } catch (ParserExceptionInterface | RegexSyntaxException) { + $root = $this->reader->read($regex, $modifiers, $delimiter); + } catch (ExceptionInterface) { return new IgnoredRegexValidatorResult(ignoredTypes: [], anchorsInTheMiddle: false, allErrorsIgnored: false); } - if (!$ast instanceof RegexNode) { - throw new ShouldNotHappenException(); - } - if (Strings::match($regex, '~(?getIgnoredTypes($ast), - anchorsInTheMiddle: $this->hasAnchorsInTheMiddle($ast), + ignoredTypes: $this->getIgnoredTypes($root->pattern, $root->source ?? $regex), + anchorsInTheMiddle: $this->hasAnchorsInTheMiddle($root->pattern), allErrorsIgnored: false, ); } @@ -57,20 +52,15 @@ public function validate(string $regex): IgnoredRegexValidatorResult /** * @return array */ - private function getIgnoredTypes(RegexNode $ast): array + private function getIgnoredTypes(NodeInterface $ast, string $source): array { - $alternation = $ast->getChildren()[0] ?? null; - if ($alternation === null) { - return []; - } - - if ($alternation->getId() !== '#alternation') { + if (!$ast instanceof AlternationNode) { return []; } $types = []; - foreach ($alternation->getChildren() as $child) { - $text = $this->getText($child); + foreach ($ast->alternatives as $alternative) { + $text = $this->getText($alternative, $source); if ($text === null) { continue; } @@ -86,7 +76,7 @@ private function getIgnoredTypes(RegexNode $ast): array continue; } - if ($type instanceof ObjectType) { + if ($type->isObject()->yes()) { continue; } @@ -102,27 +92,15 @@ private function getIgnoredTypes(RegexNode $ast): array return $types; } - private function removeDelimiters(string $regex): string - { - $delimiter = substr($regex, 0, 1); - $endDelimiterPosition = strrpos($regex, $delimiter); - if ($endDelimiterPosition === false) { - throw new ShouldNotHappenException(); - } - - return substr($regex, 1, $endDelimiterPosition - 1); - } - - private function getText(RegexNode $treeNode): ?string + /** + * Returns the text of a branch as written in the pattern, escapes included. + */ + private function getText(NodeInterface $node, string $source): ?string { - if ($treeNode->getId() === 'token') { - return $treeNode->getValueValue(); - } - - if ($treeNode->getId() === '#concatenation') { + if ($node instanceof SequenceNode) { $fullText = ''; - foreach ($treeNode->getChildren() as $child) { - $text = $this->getText($child); + foreach ($node->children as $child) { + $text = $this->getText($child, $source); if ($text === null) { continue; } @@ -137,20 +115,26 @@ private function getText(RegexNode $treeNode): ?string return $fullText; } - return null; + if ($node->getChildren() !== []) { + return null; + } + + return substr($source, $node->getStartPosition(), $node->getEndPosition() - $node->getStartPosition()); } - private function hasAnchorsInTheMiddle(RegexNode $ast): bool + private function hasAnchorsInTheMiddle(NodeInterface $ast): bool { - if ($ast->getId() === 'token') { - return $ast->getValueToken() === 'anchor' && $ast->getValueValue() === '$'; + if ($ast instanceof AnchorNode) { + return $ast->value === '$'; } - $childrenCount = count($ast->getChildren()); - foreach ($ast->getChildren() as $i => $child) { + + $children = $ast instanceof SequenceNode ? $ast->children : $ast->getChildren(); + $childrenCount = count($children); + foreach ($children as $i => $child) { $has = $this->hasAnchorsInTheMiddle($child); if ( $has - && ($ast->getId() !== '#concatenation' || $i !== $childrenCount - 1) + && (!$ast instanceof SequenceNode || $i !== $childrenCount - 1) ) { return true; } diff --git a/src/DependencyInjection/ValidateIgnoredErrorsExtension.php b/src/DependencyInjection/ValidateIgnoredErrorsExtension.php index 4cfdb396799..0027faad722 100644 --- a/src/DependencyInjection/ValidateIgnoredErrorsExtension.php +++ b/src/DependencyInjection/ValidateIgnoredErrorsExtension.php @@ -12,6 +12,7 @@ use PHPStan\File\FileExcluder; use PHPStan\Php\ComposerPhpVersionFactory; use PHPStan\Php\ConfiguredPhpVersionRangeHelper; +use PHPStan\Php\PcreVersionFactory; use PHPStan\Php\PhpVersion; use PHPStan\PhpDoc\DirectTypeNodeResolverExtensionRegistryProvider; use PHPStan\PhpDoc\TypeNodeResolver; @@ -31,7 +32,8 @@ use PHPStan\Type\DirectTypeAliasResolverProvider; use PHPStan\Type\ObjectType; use PHPStan\Type\OperatorTypeSpecifyingExtensionRegistry; -use PHPStan\Type\Regex\Parser\RegexParser; +use PHPStan\Type\Regex\PHPRegexReader; +use PHPStan\Type\Regex\RegexExpressionHelper; use PHPStan\Type\Type; use PHPStan\Type\TypeAliasResolver; use PHPStan\Type\UnaryOperatorTypeSpecifyingExtensionRegistry; @@ -67,7 +69,6 @@ public function loadConfiguration(): void return; } - $parser = new RegexParser(); $reflectionProvider = new DummyReflectionProvider(); $reflectionProviderProvider = new DirectReflectionProviderProvider($reflectionProvider); @@ -89,10 +90,16 @@ public function loadConfiguration(): void try { $composerPhpVersionFactory = new ComposerPhpVersionFactory([]); $constantResolver = new ConstantResolver($reflectionProviderProvider, [], new ConfiguredPhpVersionRangeHelper(null, $composerPhpVersionFactory), container: null); + $initializerExprTypeResolver = new InitializerExprTypeResolver($constantResolver, $reflectionProviderProvider, new PhpVersion(PHP_VERSION_ID), new OperatorTypeSpecifyingExtensionRegistry(new DirectExtensionsCollection([])), new UnaryOperatorTypeSpecifyingExtensionRegistry(new DirectExtensionsCollection([])), new OversizedArrayBuilder(), true); $phpDocParserConfig = new ParserConfig([]); $ignoredRegexValidator = new IgnoredRegexValidator( - $parser, + // ignoreErrors patterns are matched by the running PHP + new PHPRegexReader( + new PhpVersion(PHP_VERSION_ID), + (new PcreVersionFactory(null, null))->create(), + new RegexExpressionHelper($initializerExprTypeResolver), + ), new TypeStringResolver( new Lexer($phpDocParserConfig), new TypeParser($phpDocParserConfig, new ConstExprParser($phpDocParserConfig)), @@ -122,7 +129,7 @@ public function resolveTypeAlias(string $aliasName, NameScope $nameScope): ?Type }), $constantResolver, - new InitializerExprTypeResolver($constantResolver, $reflectionProviderProvider, new PhpVersion(PHP_VERSION_ID), new OperatorTypeSpecifyingExtensionRegistry(new DirectExtensionsCollection([])), new UnaryOperatorTypeSpecifyingExtensionRegistry(new DirectExtensionsCollection([])), new OversizedArrayBuilder(), true), + $initializerExprTypeResolver, reportUnsafeArrayStringKeyCasting: null, ), ), diff --git a/src/Php/PcreVersion.php b/src/Php/PcreVersion.php new file mode 100644 index 00000000000..0a25be57b66 --- /dev/null +++ b/src/Php/PcreVersion.php @@ -0,0 +1,96 @@ +source; + } + + public function getSourceLabel(): string + { + switch ($this->source) { + case self::SOURCE_RUNTIME: + return 'runtime'; + case self::SOURCE_CONFIG: + return 'config'; + case self::SOURCE_COMPOSER_PLATFORM_PCRE: + return 'config.platform.lib-pcre in composer.json'; + } + + return 'unknown'; + } + + public function getVersionId(): int + { + return $this->versionId; + } + + public function getVersionString(): string + { + return sprintf('%d.%02d', intdiv($this->versionId, 100), $this->versionId % 100); + } + + public function atLeast(string $release): bool + { + return $this->versionId >= self::fromString($release)->getVersionId(); + } + +} diff --git a/src/Php/PcreVersionFactory.php b/src/Php/PcreVersionFactory.php new file mode 100644 index 00000000000..661d876594b --- /dev/null +++ b/src/Php/PcreVersionFactory.php @@ -0,0 +1,57 @@ +version !== null) { + try { + return PcreVersion::fromString($this->version, PcreVersion::SOURCE_CONFIG); + } catch (InvalidArgumentException $exception) { + throw new InvalidArgumentException(sprintf( + 'Invalid pcreVersion configuration: %s Fix the "pcreVersion" parameter, or remove it to read the running engine.', + $exception->getMessage(), + ), previous: $exception); + } + } + + if ($this->composerPcreVersion !== null) { + try { + return PcreVersion::fromString($this->composerPcreVersion, PcreVersion::SOURCE_COMPOSER_PLATFORM_PCRE); + } catch (InvalidArgumentException $exception) { + throw new InvalidArgumentException(sprintf( + 'Invalid composer platform package: %s Fix "config.platform.lib-pcre" in composer.json, or remove it to read the running engine.', + $exception->getMessage(), + ), previous: $exception); + } + } + + try { + return PcreVersion::fromRuntime(); + } catch (InvalidArgumentException) { + // unknown PCRE_VERSION format + return PcreVersion::fromString( + PcreTarget::bundledWith(PHP_VERSION_ID)->pcreVersion, + PcreVersion::SOURCE_RUNTIME, + ); + } + } + +} diff --git a/src/Php/PcreVersionFactoryFactory.php b/src/Php/PcreVersionFactoryFactory.php new file mode 100644 index 00000000000..d6f84558a56 --- /dev/null +++ b/src/Php/PcreVersionFactoryFactory.php @@ -0,0 +1,51 @@ +composerAutoloaderProjectPaths) > 0) { + $composerJsonPath = end($this->composerAutoloaderProjectPaths); + $composer = ComposerHelper::getComposerConfig($composerJsonPath); + if ($composer !== null) { + $platformVersion = $composer['config']['platform']['lib-pcre'] ?? null; + if (is_string($platformVersion)) { + $composerPcreVersion = $platformVersion; + } + } + } + + $version = null; + + if (is_string($this->pcreVersion)) { + $version = $this->pcreVersion; + } + + return new PcreVersionFactory($version, $composerPcreVersion); + } + +} diff --git a/src/Rules/Regexp/RegularExpressionPatternRule.php b/src/Rules/Regexp/RegularExpressionPatternRule.php index b3a1f98d5d7..da1ad49b124 100644 --- a/src/Rules/Regexp/RegularExpressionPatternRule.php +++ b/src/Rules/Regexp/RegularExpressionPatternRule.php @@ -7,9 +7,11 @@ use PhpParser\Node; use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\AutowiredParameter; use PHPStan\DependencyInjection\RegisteredRule; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; +use PHPStan\Type\Regex\PHPRegexReader; use PHPStan\Type\Regex\RegexExpressionHelper; use function in_array; use function sprintf; @@ -25,6 +27,9 @@ final class RegularExpressionPatternRule implements Rule public function __construct( private RegexExpressionHelper $regexExpressionHelper, + private PHPRegexReader $reader, + #[AutowiredParameter(ref: '%featureToggles.checkRegexPatternForAnalysedPhp%')] + private bool $checkRegexPatternForAnalysedPhp, ) { } @@ -128,7 +133,12 @@ private function validatePattern(string $pattern): ?string return $e->getMessage(); } - return null; + if (!$this->checkRegexPatternForAnalysedPhp) { + return null; + } + + // the pattern compiles on the running PHP, but can still be invalid for the analysed PHP version and its PCRE2 version + return $this->reader->refusalOfExpression($pattern); } } diff --git a/src/Type/Php/PregMatchParameterOutTypeExtension.php b/src/Type/Php/PregMatchParameterOutTypeExtension.php index 58135a1dcb0..fd8393f2881 100644 --- a/src/Type/Php/PregMatchParameterOutTypeExtension.php +++ b/src/Type/Php/PregMatchParameterOutTypeExtension.php @@ -51,7 +51,7 @@ public function getParameterOutTypeFromFunctionCall(FunctionReflection $function } if ($functionReflection->getName() === 'preg_match') { - $matchedType = $this->regexShapeMatcher->matchExpr($patternArg->value, $flagsType, TrinaryLogic::createYes(), $scope); + $matchedType = $this->regexShapeMatcher->matchExpr($patternArg->value, $flagsType, TrinaryLogic::createYes(), $scope, $this->regexShapeMatcher->matchesFromOffset($funcCall, $scope)); if ($matchedType === null) { return null; } diff --git a/src/Type/Php/PregMatchTypeSpecifyingExtension.php b/src/Type/Php/PregMatchTypeSpecifyingExtension.php index 5f13ed60161..edb4daafcc3 100644 --- a/src/Type/Php/PregMatchTypeSpecifyingExtension.php +++ b/src/Type/Php/PregMatchTypeSpecifyingExtension.php @@ -13,7 +13,10 @@ use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Reflection\FunctionReflection; use PHPStan\TrinaryLogic; +use PHPStan\Type\Constant\ConstantStringType; use PHPStan\Type\FunctionTypeSpecifyingExtension; +use PHPStan\Type\StringType; +use PHPStan\Type\TypeCombinator; use function in_array; use function strtolower; @@ -55,14 +58,25 @@ public function specifyTypes(FunctionReflection $functionReflection, FuncCall $n if ( $subjectArg !== null && $context->true() - && $scope->getType($subjectArg->value)->isString()->yes() + && !$scope->getType($subjectArg->value)->isString()->no() && !$this->isSubExprOfMatchesArg($subjectArg->value, $matchesArg !== null ? $matchesArg->value : null) ) { - $subjectType = $this->regexShapeMatcher->matchSubjectExpr($patternArg->value, $scope); - if ($subjectType !== null) { + $narrowType = $this->regexShapeMatcher->matchSubjectExpr($patternArg->value, $scope); + if ($narrowType !== null) { + // non-string parts of the subject are kept: an int subject is matched as its digits + $subjectType = $scope->getType($subjectArg->value); + $besideString = TypeCombinator::remove($subjectType, new StringType()); + // null is matched as '', so it is removed when the pattern cannot match '' + if ($narrowType->isSuperTypeOf(new ConstantStringType(''))->no()) { + $besideString = TypeCombinator::removeNull($besideString); + } + $narrowType = TypeCombinator::intersect( + $subjectType, + TypeCombinator::union($narrowType, $besideString), + ); $subjectTypes = $this->typeSpecifier->create( $subjectArg->value, - $subjectType, + $narrowType, $context, $scope, )->setRootExpr($node); @@ -87,7 +101,7 @@ public function specifyTypes(FunctionReflection $functionReflection, FuncCall $n } if ($functionReflection->getName() === 'preg_match') { - $matchedType = $this->regexShapeMatcher->matchExpr($patternArg->value, $flagsType, $wasMatched, $scope); + $matchedType = $this->regexShapeMatcher->matchExpr($patternArg->value, $flagsType, $wasMatched, $scope, $this->regexShapeMatcher->matchesFromOffset($node, $scope)); } else { $matchedType = $this->regexShapeMatcher->matchAllExpr($patternArg->value, $flagsType, $wasMatched, $scope); } diff --git a/src/Type/Php/PregReplaceCallbackArrayClosureTypeExtension.php b/src/Type/Php/PregReplaceCallbackArrayClosureTypeExtension.php new file mode 100644 index 00000000000..08a68a4bc0c --- /dev/null +++ b/src/Type/Php/PregReplaceCallbackArrayClosureTypeExtension.php @@ -0,0 +1,90 @@ + callback literal, + * each from the shape of its own pattern. + */ +#[AutowiredService] +final class PregReplaceCallbackArrayClosureTypeExtension implements FunctionParameterClosureTypeExtension +{ + + public function __construct( + private RegexArrayShapeMatcher $regexShapeMatcher, + ) + { + } + + public function isFunctionSupported(FunctionReflection $functionReflection, ParameterReflection $parameter): bool + { + return $functionReflection->getName() === 'preg_replace_callback_array' && $parameter->getName() === 'pattern'; + } + + public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, ParameterReflection $parameter, Scope $scope): ?Type + { + $args = $functionCall->getArgs(); + $patternArg = $args[0] ?? null; + $flagsArg = $args[4] ?? null; + + if ($patternArg === null || !$patternArg->value instanceof Array_) { + return null; + } + + $flagsType = null; + if ($flagsArg !== null) { + $flagsType = $scope->getType($flagsArg->value); + } + + $builder = ConstantArrayTypeBuilder::createEmpty(); + $builder->disableClosureDegradation(); + foreach ($patternArg->value->items as $item) { + // a callback whose pattern is not known could get the shape of another one + if ($item->key === null || $item->unpack) { + return null; + } + + $keyType = $scope->getType($item->key); + if (count($keyType->getConstantStrings()) !== 1) { + return null; + } + + $matchesType = $this->regexShapeMatcher->matchExpr($item->key, $flagsType, TrinaryLogic::createYes(), $scope, true); + if ($matchesType === null) { + $builder->setOffsetValueType($keyType, $parameter->getType()->getIterableValueType()); + continue; + } + + $builder->setOffsetValueType($keyType, new ClosureType( + [ + new NativeParameterReflection('matches', false, $matchesType, PassedByReference::createNo(), false, null), + ], + new StringType(), + )); + } + + $arrayType = $builder->getArray(); + if (!$arrayType->isConstantArray()->yes()) { + return null; + } + + return $arrayType; + } + +} diff --git a/src/Type/Php/PregReplaceCallbackClosureTypeExtension.php b/src/Type/Php/PregReplaceCallbackClosureTypeExtension.php index 1be24ff645f..05550643268 100644 --- a/src/Type/Php/PregReplaceCallbackClosureTypeExtension.php +++ b/src/Type/Php/PregReplaceCallbackClosureTypeExtension.php @@ -46,7 +46,7 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, $flagsType = $scope->getType($flagsArg->value); } - $matchesType = $this->regexShapeMatcher->matchExpr($patternArg->value, $flagsType, TrinaryLogic::createYes(), $scope); + $matchesType = $this->regexShapeMatcher->matchExpr($patternArg->value, $flagsType, TrinaryLogic::createYes(), $scope, true); if ($matchesType === null) { return null; } diff --git a/src/Type/Php/PregSplitDynamicReturnTypeExtension.php b/src/Type/Php/PregSplitDynamicReturnTypeExtension.php index fe582a38585..b476163938e 100644 --- a/src/Type/Php/PregSplitDynamicReturnTypeExtension.php +++ b/src/Type/Php/PregSplitDynamicReturnTypeExtension.php @@ -4,6 +4,7 @@ use Nette\Utils\RegexpException; use Nette\Utils\Strings; +use PhpParser\Node\Arg; use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; use PHPStan\DependencyInjection\AutowiredService; @@ -152,6 +153,11 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, return new UnionType([$returnListType, new ConstantBooleanType(false)]); } + // limit or flags is not a constant: the result is still a list, not only false + if ($limits === [] || $flags === []) { + return $this->genericReturnType($subjectType, $capturesOffset, $scope); + } + $resultTypes = []; foreach ($patternConstantTypes as $patternConstantType) { foreach ($subjectConstantTypes as $subjectConstantType) { @@ -170,6 +176,49 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, return TypeCombinator::union(...$resultTypes); } + private function genericReturnType(Type $subjectType, ?Arg $capturesOffset, Scope $scope): Type + { + $returnStringType = new StringType(); + + $arrayTypeBuilder = ConstantArrayTypeBuilder::createEmpty(); + $arrayTypeBuilder->setOffsetValueType( + new ConstantIntegerType(0), + $returnStringType, + ); + $arrayTypeBuilder->setOffsetValueType( + new ConstantIntegerType(1), + IntegerRangeType::fromInterval(0, null), + ); + $capturedArrayType = $arrayTypeBuilder->getArray(); + + $returnInternalValueType = $returnStringType; + if ($capturesOffset !== null) { + $flagState = $this->bitwiseFlagAnalyser->bitwiseOrContainsConstant($capturesOffset->value, $scope, 'PREG_SPLIT_OFFSET_CAPTURE'); + if ($flagState->yes()) { + $capturedArrayListType = new IntersectionType([ + new ArrayType(IntegerRangeType::createAllGreaterThanOrEqualTo(0), $capturedArrayType), + new AccessoryArrayListType(), + ]); + + if ($subjectType->isNonEmptyString()->yes()) { + $capturedArrayListType = TypeCombinator::intersect($capturedArrayListType, new NonEmptyArrayType()); + } + + return new UnionType([$capturedArrayListType, new ConstantBooleanType(false)]); + } + if ($flagState->maybe()) { + $returnInternalValueType = new UnionType([new StringType(), $capturedArrayType]); + } + } + + $returnListType = new IntersectionType([new ArrayType(IntegerRangeType::createAllGreaterThanOrEqualTo(0), $returnInternalValueType), new AccessoryArrayListType()]); + if ($subjectType->isNonEmptyString()->yes()) { + $returnListType = new IntersectionType([new NonEmptyArrayType(), ...$returnListType->getTypes()]); + } + + return new UnionType([$returnListType, new ConstantBooleanType(false)]); + } + /** * @param ConstantStringType[] $patternConstantArray * @param ConstantStringType[] $subjectConstantArray diff --git a/src/Type/Php/RegexArrayShapeMatcher.php b/src/Type/Php/RegexArrayShapeMatcher.php index 6d5bfbab630..58d90b39c0c 100644 --- a/src/Type/Php/RegexArrayShapeMatcher.php +++ b/src/Type/Php/RegexArrayShapeMatcher.php @@ -3,6 +3,7 @@ namespace PHPStan\Type\Php; use PhpParser\Node\Expr; +use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Php\PhpVersion; @@ -24,6 +25,7 @@ use PHPStan\Type\StringType; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; +use function array_reverse; use function count; use function in_array; use function is_string; @@ -57,9 +59,26 @@ public function matchAllExpr(Expr $patternExpr, ?Type $flagsType, TrinaryLogic $ return $this->matchPatternType($this->getPatternType($patternExpr, $scope), $flagsType, $wasMatched, true); } - public function matchExpr(Expr $patternExpr, ?Type $flagsType, TrinaryLogic $wasMatched, Scope $scope): ?Type + /** + * Whether a preg_match() call starts its match from an offset: the match is then one of a scan, + * and a zero-width first branch like ^ no longer wins it. + */ + public function matchesFromOffset(FuncCall $call, Scope $scope): bool { - return $this->matchPatternType($this->getPatternType($patternExpr, $scope), $flagsType, $wasMatched, false); + $offsetArg = $call->getArgs()[4] ?? null; + if ($offsetArg === null) { + return false; + } + + return !(new ConstantIntegerType(0))->isSuperTypeOf($scope->getType($offsetArg->value))->yes(); + } + + /** + * @param bool $inScan the match is one of a scan, e.g. a preg_replace_callback() callback or a preg_match() from an offset + */ + public function matchExpr(Expr $patternExpr, ?Type $flagsType, TrinaryLogic $wasMatched, Scope $scope, bool $inScan = false): ?Type + { + return $this->matchPatternType($this->getPatternType($patternExpr, $scope), $flagsType, $wasMatched, false, $inScan); } public function matchSubjectExpr(Expr $patternExpr, Scope $scope): ?Type @@ -77,13 +96,13 @@ public function matchSubjectExpr(Expr $patternExpr, Scope $scope): ?Type return null; } - $subjectTypes[] = $astWalkResult->getSubjectBaseType(); + $subjectTypes[] = $astWalkResult->getSubjectNarrowType(); } return TypeCombinator::union(...$subjectTypes); } - private function matchPatternType(Type $patternType, ?Type $flagsType, TrinaryLogic $wasMatched, bool $matchesAll): ?Type + private function matchPatternType(Type $patternType, ?Type $flagsType, TrinaryLogic $wasMatched, bool $matchesAll, bool $inScan = false): ?Type { if ($wasMatched->no()) { return ConstantArrayTypeBuilder::createEmpty()->getArray(); @@ -111,7 +130,7 @@ private function matchPatternType(Type $patternType, ?Type $flagsType, TrinaryLo $matchedTypes = []; foreach ($constantStrings as $constantString) { - $matched = $this->matchRegex($constantString->getValue(), $flags, $wasMatched, $matchesAll); + $matched = $this->matchRegex($constantString->getValue(), $flags, $wasMatched, $matchesAll, $inScan); if ($matched === null) { return null; } @@ -129,18 +148,18 @@ private function matchPatternType(Type $patternType, ?Type $flagsType, TrinaryLo /** * @param int-mask|null $flags */ - private function matchRegex(string $regex, ?int $flags, TrinaryLogic $wasMatched, bool $matchesAll): ?Type + private function matchRegex(string $regex, ?int $flags, TrinaryLogic $wasMatched, bool $matchesAll, bool $inScan): ?Type { $astWalkResult = $this->regexGroupParser->parseGroups($regex); if ($astWalkResult === null) { // regex could not be parsed by RegexParser return null; } - $groupList = $astWalkResult->getCapturingGroups(); + $groupList = $this->resolveNeverMatchedGroups($astWalkResult->getCapturingGroups(), $flags ?? 0, $matchesAll); $markVerbs = $astWalkResult->getMarkVerbs(); $subjectBaseType = new StringType(); if ($wasMatched->yes()) { - $subjectBaseType = $astWalkResult->getSubjectBaseType(); + $subjectBaseType = $inScan ? $astWalkResult->getScanBaseType() : $astWalkResult->getSubjectBaseType(); } $regexGroupList = new RegexGroupList($groupList); @@ -149,30 +168,6 @@ 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 ( - $regexGroupList->hasGroupResettingGroupCounter() - && ( - $matchesAll - || $onlyOptionalTopLevelGroup !== null - || $onlyTopLevelAlternation === null - || $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() @@ -266,6 +261,37 @@ private function matchRegex(string $regex, ?int $flags, TrinaryLogic $wasMatched return TypeCombinator::union(...$combiTypes); } + if (!$matchesAll && !$wasMatched->no() && $astWalkResult->getCases() !== []) { + // the union of the cases the pattern splits into, e.g. an optional group with nested groups + // is either set with all of them or skipped with all of them + $combiTypes = []; + foreach ($astWalkResult->getCases() as $participations) { + $caseGroups = []; + foreach ($astWalkResult->getCapturingGroups() as $id => $group) { + $set = $participations[$group->getId()] ?? null; + if ($set === true) { + $group = $group->forceNonOptional(); + } elseif ($set === false) { + $group = $group->forceNeverMatched(); + } + $caseGroups[$id] = $group; + } + $caseList = new RegexGroupList($this->resolveNeverMatchedGroups($caseGroups, $flags, $matchesAll)); + + $combiTypes[] = $this->buildArrayType( + $subjectBaseType, + $caseList, + $wasMatched, + $caseList->countTrailingOptionals(), + $flags, + $markVerbs, + $matchesAll, + ); + } + + return TypeCombinator::union(...$combiTypes); + } + // the general case, which should work in all cases but does not yield the most // precise result possible in some cases return $this->buildArrayType( @@ -303,20 +329,50 @@ private function buildArrayType( ); $countGroups = count($captureGroups); + $composed = []; + $names = []; $i = 0; foreach ($captureGroups as $captureGroup) { $isTrailingOptional = $i >= $countGroups - $trailingOptionals; $isLastGroup = $i === $countGroups - 1; $groupValueType = $this->createGroupValueType($captureGroup, $wasMatched, $flags, $isTrailingOptional, $isLastGroup, $matchesAll); $optional = $this->isGroupOptional($captureGroup, $wasMatched, $flags, $isTrailingOptional, $matchesAll); + $composed[$i] = [$captureGroup, $groupValueType, $optional]; if ($captureGroup->isNamed()) { $forceList = false; + // several groups can share a name (J modifier, or a branch reset): + // the key gets the union of the groups that can be set in this arm + $forced = $captureGroup->getForceType(); + $unmatched = $this->getUnmatchedGroupType($flags); + $live = $forced === null || $forced->equals($unmatched) === false; + // when no group of the name can be set in this arm, the key is the first group's own key + $names[$captureGroup->getName()] ??= ['type' => null, 'optional' => true, 'firstType' => $groupValueType, 'firstOptional' => $optional]; + if ($live) { + $names[$captureGroup->getName()]['type'] = $names[$captureGroup->getName()]['type'] === null + ? $groupValueType + : TypeCombinator::union($names[$captureGroup->getName()]['type'], $groupValueType); + if (!$optional) { + $names[$captureGroup->getName()]['optional'] = false; + } + } + } + + $i++; + } + $resolvedNames = []; + foreach ($names as $name => $entry) { + $resolvedNames[$name] = $entry['type'] !== null + ? ['type' => $entry['type'], 'optional' => $entry['optional']] + : ['type' => $entry['firstType'], 'optional' => $entry['firstOptional']]; + } + foreach ($composed as $i => [$captureGroup, $groupValueType, $optional]) { + if ($captureGroup->isNamed()) { $builder->setOffsetValueType( $this->getKeyType($captureGroup->getName()), - $groupValueType, - $optional, + $resolvedNames[$captureGroup->getName()]['type'], + $resolvedNames[$captureGroup->getName()]['optional'], ); } @@ -325,8 +381,6 @@ private function buildArrayType( $groupValueType, $optional, ); - - $i++; } if (count($markVerbs) > 0) { @@ -366,6 +420,36 @@ private function buildArrayType( return $builder->getArray(); } + /** + * A group the engine never sets holds the unmatched value, and has no key + * when no group after it can be set: /(?(DEFINE)(a))b/ writes ['b']. + * + * @param array $groupList + * @return array + */ + private function resolveNeverMatchedGroups(array $groupList, int $flags, bool $matchesAll): array + { + $keyAlwaysWritten = $this->containsUnmatchedAsNull($flags, $matchesAll) + || ($matchesAll && $this->containsPatternOrder($flags)); + + $trailing = true; + foreach (array_reverse($groupList, true) as $id => $group) { + if (!$group->isNeverMatched()) { + $trailing = false; + continue; + } + + if ($trailing && !$keyAlwaysWritten) { + unset($groupList[$id]); + continue; + } + + $groupList[$id] = $group->forceType($this->getUnmatchedGroupType($flags)); + } + + return $groupList; + } + private function isSubjectOptional(TrinaryLogic $wasMatched, bool $matchesAll): bool { if ($matchesAll) { @@ -380,10 +464,11 @@ private function isSubjectOptional(TrinaryLogic $wasMatched, bool $matchesAll): */ private function createSubjectValueType(Type $baseType, int $flags, bool $matchesAll, TrinaryLogic $wasMatched): Type { - $subjectValueType = TypeCombinator::removeNull($this->getValueType($baseType, $flags, $matchesAll)); + // $matches[0] is always set on a match, its offset is never -1 + $subjectValueType = $this->wrapOffset($baseType, $flags, false); if ($matchesAll) { - $subjectValueType = TypeCombinator::removeNull($this->getValueType(new StringType(), $flags, $matchesAll)); + $subjectValueType = $this->wrapOffset(new StringType(), $flags, false); if ($this->containsPatternOrder($flags)) { $accessoryTypes = [ @@ -428,6 +513,8 @@ private function isGroupOptional(RegexCapturingGroup $captureGroup, TrinaryLogic private function createGroupValueType(RegexCapturingGroup $captureGroup, TrinaryLogic $wasMatched, int $flags, bool $isTrailingOptional, bool $isLastGroup, bool $matchesAll): Type { + $unmatchedAsNull = $this->containsUnmatchedAsNull($flags, $matchesAll); + if ($matchesAll) { if ( ( @@ -444,20 +531,19 @@ private function createGroupValueType(RegexCapturingGroup $captureGroup, Trinary && !$isLastGroup ) ) { - $groupValueType = $this->getValueType( - TypeCombinator::union($captureGroup->getType(), new ConstantStringType('')), - $flags, - $matchesAll, - ); - $groupValueType = TypeCombinator::removeNull($groupValueType); + $valueType = TypeCombinator::union($captureGroup->getType(), new ConstantStringType('')); + $offsetMayBeMinusOne = true; } else { - $groupValueType = $this->getValueType($captureGroup->getType(), $flags, $matchesAll); + $valueType = $this->addUnmatchedNull($captureGroup->getType(), $flags, $matchesAll); + $offsetMayBeMinusOne = $this->canBeUnmatchedAndPresent($captureGroup, $flags, $matchesAll); } if (!$isTrailingOptional && $this->containsUnmatchedAsNull($flags, $matchesAll) && !$captureGroup->isOptional()) { - $groupValueType = TypeCombinator::removeNull($groupValueType); + $valueType = TypeCombinator::removeNull($valueType); } + $groupValueType = $this->wrapOffset($valueType, $flags, $unmatchedAsNull ? TypeCombinator::containsNull($valueType) : $offsetMayBeMinusOne); + if ($this->containsPatternOrder($flags)) { $accessoryTypes = [new ArrayType(IntegerRangeType::createAllGreaterThanOrEqualTo(0), $groupValueType), new AccessoryArrayListType()]; if ($wasMatched->yes()) { @@ -471,22 +557,21 @@ private function createGroupValueType(RegexCapturingGroup $captureGroup, Trinary } if (!$isLastGroup && !$this->containsUnmatchedAsNull($flags, $matchesAll) && $captureGroup->isOptional()) { - $groupValueType = $this->getValueType( - TypeCombinator::union($captureGroup->getType(), $this->getUnmatchedGroupType($flags)), - $flags, - $matchesAll, - ); + $valueType = TypeCombinator::union($captureGroup->getType(), $this->getUnmatchedGroupType($flags)); + $offsetMayBeMinusOne = true; } else { - $groupValueType = $this->getValueType($captureGroup->getType(), $flags, $matchesAll); + $valueType = $this->addUnmatchedNull($captureGroup->getType(), $flags, $matchesAll); + $offsetMayBeMinusOne = $this->canBeUnmatchedAndPresent($captureGroup, $flags, $matchesAll); } if ($wasMatched->yes()) { if (!$isTrailingOptional && $this->containsUnmatchedAsNull($flags, $matchesAll) && !$captureGroup->isOptional()) { - $groupValueType = TypeCombinator::removeNull($groupValueType); + $valueType = TypeCombinator::removeNull($valueType); } } - return $groupValueType; + // with PREG_UNMATCHED_AS_NULL an unmatched group is [null, -1] + return $this->wrapOffset($valueType, $flags, $unmatchedAsNull ? TypeCombinator::containsNull($valueType) : $offsetMayBeMinusOne); } private function containsOffsetCapture(int $flags): bool @@ -540,32 +625,41 @@ private function getKeyType(int|string $key): Type return new ConstantIntegerType($key); } - private function getValueType(Type $baseType, int $flags, bool $matchesAll): Type + private function canBeUnmatchedAndPresent(RegexCapturingGroup $captureGroup, int $flags, bool $matchesAll): bool { - $valueType = $baseType; + if ($captureGroup->getType()->isSuperTypeOf($this->getUnmatchedGroupType($flags))->yes()) { + return true; + } + + return $this->containsUnmatchedAsNull($flags, $matchesAll) && $captureGroup->isOptional(); + } - // unmatched groups return -1 as offset - $offsetType = IntegerRangeType::fromInterval(-1, null); + private function addUnmatchedNull(Type $valueType, int $flags, bool $matchesAll): Type + { if ($this->containsUnmatchedAsNull($flags, $matchesAll)) { - $valueType = TypeCombinator::addNull($valueType); + return TypeCombinator::addNull($valueType); } - if ($this->containsOffsetCapture($flags)) { - $builder = ConstantArrayTypeBuilder::createEmpty(); - - $builder->setOffsetValueType( - new ConstantIntegerType(0), - $valueType, - ); - $builder->setOffsetValueType( - new ConstantIntegerType(1), - $offsetType, - ); + return $valueType; + } - return $builder->getArray(); + /** + * With PREG_OFFSET_CAPTURE, the value and its offset; an unmatched group returns -1 as offset. + */ + private function wrapOffset(Type $valueType, int $flags, bool $offsetMayBeMinusOne): Type + { + if (!$this->containsOffsetCapture($flags)) { + return $valueType; } - return $valueType; + $builder = ConstantArrayTypeBuilder::createEmpty(); + $builder->setOffsetValueType(new ConstantIntegerType(0), $valueType); + $builder->setOffsetValueType( + new ConstantIntegerType(1), + $offsetMayBeMinusOne ? IntegerRangeType::fromInterval(-1, null) : IntegerRangeType::fromInterval(0, null), + ); + + return $builder->getArray(); } private function getPatternType(Expr $patternExpr, Scope $scope): Type diff --git a/src/Type/Php/ReplaceFunctionsDynamicReturnTypeExtension.php b/src/Type/Php/ReplaceFunctionsDynamicReturnTypeExtension.php index 996f8dfe195..24d97d66450 100644 --- a/src/Type/Php/ReplaceFunctionsDynamicReturnTypeExtension.php +++ b/src/Type/Php/ReplaceFunctionsDynamicReturnTypeExtension.php @@ -2,17 +2,21 @@ namespace PHPStan\Type\Php; +use PhpParser\Node\Expr; use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Reflection\FunctionReflection; use PHPStan\Reflection\ParametersAcceptorSelector; use PHPStan\ShouldNotHappenException; +use PHPStan\TrinaryLogic; use PHPStan\Type\Accessory\AccessoryLowercaseStringType; use PHPStan\Type\Accessory\AccessoryNonEmptyStringType; use PHPStan\Type\Accessory\AccessoryNonFalsyStringType; use PHPStan\Type\Accessory\AccessoryUppercaseStringType; use PHPStan\Type\ArrayType; +use PHPStan\Type\Constant\ConstantIntegerType; +use PHPStan\Type\Constant\ConstantStringType; use PHPStan\Type\DynamicFunctionReturnTypeExtension; use PHPStan\Type\IntersectionType; use PHPStan\Type\MixedType; @@ -23,6 +27,10 @@ use function array_key_exists; use function count; use function in_array; +use function preg_match_all; +use function strlen; +use const PREG_SET_ORDER; +use const PREG_UNMATCHED_AS_NULL; #[AutowiredService] final class ReplaceFunctionsDynamicReturnTypeExtension implements DynamicFunctionReturnTypeExtension @@ -46,6 +54,12 @@ final class ReplaceFunctionsDynamicReturnTypeExtension implements DynamicFunctio 'strtr' => 2, ]; + public function __construct( + private RegexArrayShapeMatcher $regexShapeMatcher, + ) + { + } + public function isFunctionSupported(FunctionReflection $functionReflection): bool { return array_key_exists($functionReflection->getName(), self::FUNCTIONS_SUBJECT_POSITION); @@ -108,6 +122,11 @@ private function getPreliminarilyResolvedTypeFromFunctionCall( } } + $substitutedReplaceType = null; + if ($replaceArgumentType !== null && $functionReflection->getName() === 'preg_replace') { + $substitutedReplaceType = $this->getPregReplacementType($args[0]->value, $replaceArgumentType, $scope); + } + $result = []; if ($subjectArgumentType->isString()->yes()) { @@ -116,7 +135,7 @@ private function getPreliminarilyResolvedTypeFromFunctionCall( $stringArgumentType = TypeCombinator::intersect(new StringType(), $subjectArgumentType); } if ($stringArgumentType->isString()->yes()) { - $result[] = $this->getReplaceType($stringArgumentType, $replaceArgumentType); + $result[] = $this->getReplaceType($stringArgumentType, $replaceArgumentType, $substitutedReplaceType); } if ($subjectArgumentType->isArray()->yes()) { @@ -132,7 +151,7 @@ private function getPreliminarilyResolvedTypeFromFunctionCall( ); $mapped = $arrayArgumentType->mapValueType( - fn (Type $value): Type => $this->getReplaceType($value, $replaceArgumentType), + fn (Type $value): Type => $this->getReplaceType($value, $replaceArgumentType, $substitutedReplaceType), ); if ($keyShouldBeOptional) { $mapped = $mapped->makeAllArrayKeysOptional(); @@ -147,16 +166,18 @@ private function getPreliminarilyResolvedTypeFromFunctionCall( private function getReplaceType( Type $subjectArgumentType, ?Type $replaceArgumentType, + ?Type $substitutedReplaceType, ): Type { if ($replaceArgumentType === null) { return new StringType(); } + $substitutedReplaceType ??= $replaceArgumentType; $accessories = []; - if ($subjectArgumentType->isNonFalsyString()->yes() && $replaceArgumentType->isNonFalsyString()->yes()) { + if ($subjectArgumentType->isNonFalsyString()->yes() && $substitutedReplaceType->isNonFalsyString()->yes()) { $accessories[] = new AccessoryNonFalsyStringType(); - } elseif ($subjectArgumentType->isNonEmptyString()->yes() && $replaceArgumentType->isNonEmptyString()->yes()) { + } elseif ($subjectArgumentType->isNonEmptyString()->yes() && $substitutedReplaceType->isNonEmptyString()->yes()) { $accessories[] = new AccessoryNonEmptyStringType(); } @@ -176,6 +197,77 @@ private function getReplaceType( return new StringType(); } + /** + * The text a preg_replace() replacement writes once its references to the groups are + * substituted - a reference to a group that can be unset or empty writes nothing. + */ + private function getPregReplacementType(Expr $patternExpr, Type $replaceType, Scope $scope): Type + { + $constantStrings = $replaceType->getConstantStrings(); + if (count($constantStrings) === 0) { + return new StringType(); + } + + $matchesType = $this->regexShapeMatcher->matchExpr($patternExpr, null, TrinaryLogic::createYes(), $scope, true); + $types = []; + foreach ($constantStrings as $constantString) { + preg_match_all('/\$\{(\d\d?)\}|[\\\\$](\d\d?)|\\\\([\\\\$])|(.)/s', $constantString->getValue(), $tokens, PREG_SET_ORDER | PREG_UNMATCHED_AS_NULL); + $text = ''; + $referenceTypes = []; + foreach ($tokens as [, $bracedReference, $reference, $escaped, $char]) { + if ($bracedReference === null && $reference === null) { + $text .= $escaped ?? $char; + continue; + } + + $referenceTypes[] = $this->getGroupType($matchesType, (int) ($bracedReference ?? $reference)); + } + + $minLength = strlen($text); + $isNonFalsy = (new ConstantStringType($text))->isNonFalsyString()->yes(); + foreach ($referenceTypes as $referenceType) { + if ($referenceType->isNonFalsyString()->yes()) { + $isNonFalsy = true; + } + if (!$referenceType->isNonEmptyString()->yes()) { + continue; + } + + $minLength++; + } + + if ($isNonFalsy || $minLength >= 2) { + $types[] = new IntersectionType([new StringType(), new AccessoryNonFalsyStringType()]); + } elseif ($minLength === 1) { + $types[] = new IntersectionType([new StringType(), new AccessoryNonEmptyStringType()]); + } else { + $types[] = new StringType(); + } + } + + return TypeCombinator::union(...$types); + } + + private function getGroupType(?Type $matchesType, int $group): Type + { + if ($matchesType === null) { + return new StringType(); + } + + $offsetType = new ConstantIntegerType($group); + $hasOffset = $matchesType->hasOffsetValueType($offsetType); + if ($hasOffset->no()) { + return new ConstantStringType(''); + } + + $groupType = $matchesType->getOffsetValueType($offsetType); + if ($hasOffset->yes()) { + return $groupType; + } + + return TypeCombinator::union($groupType, new ConstantStringType('')); + } + private function getSubjectType( FunctionReflection $functionReflection, FuncCall $functionCall, diff --git a/src/Type/Regex/PHPRegexReader.php b/src/Type/Regex/PHPRegexReader.php new file mode 100644 index 00000000000..a8711037a36 --- /dev/null +++ b/src/Type/Regex/PHPRegexReader.php @@ -0,0 +1,167 @@ +getParser()->parsePattern($rawRegex, $modifiers, $delimiter); + } + + /** + * Returns null when the pattern is invalid for the analysed PCRE2 version, + * or when the parser cannot read it. + */ + public function readExpression(string $regex): ?RegexNode + { + if (trim($regex) === '') { + return null; + } + + ['body' => $rawRegex, 'modifiers' => $modifiers, 'delimiter' => $delimiter] = $this->split($regex); + + if ($this->refusal($rawRegex, $modifiers, $delimiter) !== null) { + return null; + } + + try { + return $this->read($rawRegex, $modifiers, $delimiter); + } catch (ExceptionInterface) { + return null; + } + } + + /** + * Returns the error the analysed PCRE2 version reports for the pattern, or null. + */ + public function refusalOfExpression(string $pattern): ?string + { + if (trim($pattern) === '') { + return null; + } + + ['body' => $rawRegex, 'modifiers' => $modifiers, 'delimiter' => $delimiter] = $this->split($pattern); + + return $this->refusal($rawRegex, $modifiers, $delimiter); + } + + /** + * @return array{body: string, modifiers: string, delimiter: string} + */ + public function split(string $regex): array + { + $trimmed = trim($regex); + + return [ + 'body' => $this->regexExpressionHelper->removeDelimitersAndModifiers($regex), + 'modifiers' => $this->regexExpressionHelper->getPatternModifiers($regex) ?? '', + 'delimiter' => $trimmed === '' ? '/' : $trimmed[0], + ]; + } + + /** + * PCRE2 version the patterns are parsed for, e.g. "10.44". + */ + public function judgedPcreVersion(): string + { + return $this->getParser()->target()->pcreVersion; + } + + private function refusal(string $rawRegex, string $modifiers, string $delimiter): ?string + { + try { + $result = $this->getParser()->validate($this->delimited($rawRegex, $modifiers, $delimiter)); + } catch (ExceptionInterface) { + // failure of the parser itself, not of the pattern + return null; + } + + if ($result->isValid) { + return null; + } + + // the parser hit one of its own limits, the pattern may still be valid + if ($result->category === ValidationErrorCategory::Limit()) { + return null; + } + + if ($result->offset !== null && $result->error !== null) { + return sprintf('%s at offset %d', rtrim($result->error, '.'), $result->offset); + } + + return $result->error; + } + + private function delimited(string $rawRegex, string $modifiers, string $delimiter): string + { + static $pairs = ['(' => ')', '[' => ']', '{' => '}', '<' => '>']; + + return $delimiter . $rawRegex . ($pairs[$delimiter] ?? $delimiter) . $modifiers; + } + + private function getParser(): RegexParser + { + return $this->parser ??= RegexParser::create($this->getParserOptions()); + } + + /** + * A PCRE2 version set in the configuration or in composer.json is used as is. + * The PCRE2 version of the running PHP is used only when the analysed PHP + * is the same minor version; otherwise the parser uses the PCRE2 version + * bundled with the analysed PHP version. + * + * @return array{php_version: int, pcre_version?: string} + */ + private function getParserOptions(): array + { + $phpVersionId = $this->phpVersion->getVersionId(); + $options = ['php_version' => $phpVersionId]; + + $isConfigured = in_array($this->pcreVersion->getSource(), [PcreVersion::SOURCE_CONFIG, PcreVersion::SOURCE_COMPOSER_PLATFORM_PCRE], true); + $isRuntimeOfSameMinor = $this->pcreVersion->getSource() === PcreVersion::SOURCE_RUNTIME + && intdiv($phpVersionId, 100) === intdiv(PHP_VERSION_ID, 100); + + if ($isConfigured || $isRuntimeOfSameMinor) { + $options['pcre_version'] = $this->pcreVersion->getVersionString(); + } + + return $options; + } + +} diff --git a/src/Type/Regex/Parser/RegexNode.php b/src/Type/Regex/Parser/RegexNode.php deleted file mode 100644 index a1e7fff28dd..00000000000 --- a/src/Type/Regex/Parser/RegexNode.php +++ /dev/null @@ -1,65 +0,0 @@ - $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 deleted file mode 100644 index 533b93dcd14..00000000000 --- a/src/Type/Regex/Parser/RegexParser.php +++ /dev/null @@ -1,1376 +0,0 @@ - - * - * @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_BACKREFERENCE = 15; - /** @var int */ - public const T_SUBROUTINE = 16; - /** @var int */ - public const T_NAMED_CAPTURING = 17; - /** @var int */ - public const T_NON_CAPTURING = 18; - /** @var int */ - public const T_NON_CAPTURING_INTERNAL_OPTION = 19; - /** @var int */ - public const T_NON_CAPTURING_RESET = 20; - /** @var int */ - public const T_ATOMIC_GROUP = 21; - /** @var int */ - public const T_CAPTURING = 22; - /** @var int */ - public const T_CAPTURING_CLOSE = 23; - /** @var int */ - public const T_ZERO_OR_ONE_POSSESSIVE = 24; - /** @var int */ - public const T_ZERO_OR_ONE_LAZY = 25; - /** @var int */ - public const T_ZERO_OR_ONE = 26; - /** @var int */ - public const T_ZERO_OR_MORE_POSSESSIVE = 27; - /** @var int */ - public const T_ZERO_OR_MORE_LAZY = 28; - /** @var int */ - public const T_ZERO_OR_MORE = 29; - /** @var int */ - public const T_ONE_OR_MORE_POSSESSIVE = 30; - /** @var int */ - public const T_ONE_OR_MORE_LAZY = 31; - /** @var int */ - public const T_ONE_OR_MORE = 32; - /** @var int */ - public const T_EXACTLY_N = 33; - /** @var int */ - public const T_N_TO_M_POSSESSIVE = 34; - /** @var int */ - public const T_N_TO_M_LAZY = 35; - /** @var int */ - public const T_N_TO_M = 36; - /** @var int */ - public const T_N_OR_MORE_POSSESSIVE = 37; - /** @var int */ - public const T_N_OR_MORE_LAZY = 38; - /** @var int */ - public const T_N_OR_MORE = 39; - /** @var int */ - public const T_ALTERNATION = 40; - /** @var int */ - public const T_CHARACTER = 41; - /** @var int */ - public const T_DYNAMIC_CHARACTER = 42; - /** @var int */ - public const T_CHARACTER_TYPE = 43; - /** @var int */ - 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 = [ - '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_BACKREFERENCE', - 'T_SUBROUTINE', - '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_SUBROUTINE_G', - 'T_BACKREFERENCE', - '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, - 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, - true, - true, - 41 => 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, - true, - true, - true, - true, - 41 => true, - true, - 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, - ], - [ - 23 => true, - ], - [ - true, - true, - true, - true, - 13 => true, - true, - true, - true, - true, - true, - true, - true, - true, - true, - 41 => true, - true, - true, - true, - true, - true, - true, - true, - ], - [ - true, - true, - true, - true, - 13 => true, - true, - true, - true, - true, - true, - true, - true, - true, - true, - 41 => true, - true, - true, - true, - true, - true, - true, - true, - ], - [ - true, - true, - true, - true, - ], - [ - true, - ], - [ - 1 => true, - ], - [ - 2 => true, - ], - [ - 3 => true, - ], - [ - 13 => true, - true, - 17 => true, - true, - true, - true, - true, - true, - ], - [ - 14 => true, - ], - [ - 13 => true, - ], - [ - 17 => true, - true, - true, - true, - true, - true, - ], - [ - 17 => true, - true, - true, - true, - true, - true, - ], - [ - 17 => true, - ], - [ - 18 => true, - ], - [ - 19 => true, - ], - [ - 20 => true, - ], - [ - 21 => true, - ], - [ - 22 => true, - ], - [ - 15 => true, - true, - 41 => true, - true, - true, - true, - true, - true, - true, - true, - ], - [ - 41 => true, - ], - [ - 42 => true, - ], - [ - 43 => true, - ], - [ - 44 => true, - ], - [ - 45 => true, - ], - [ - 46 => true, - ], - [ - 47 => true, - ], - [ - 15 => true, - ], - [ - 16 => true, - ], - [ - 48 => true, - ], - null, - [ - 24 => true, - true, - true, - true, - true, - true, - true, - true, - true, - true, - true, - true, - true, - true, - true, - 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, - ], - [ - 38 => true, - ], - [ - 39 => true, - ], - [ - 9 => true, - true, - true, - true, - ], - [ - 9 => true, - true, - true, - true, - ], - [ - 9 => true, - ], - [ - 10 => true, - ], - [ - 11 => true, - ], - [ - 12 => true, - ], - [ - 12 => true, - ], - null, - [ - 40 => true, - ], - [ - 40 => true, - ], - [ - 40 => 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, - ], - [ - 13, - ], - [ - 13, - ], - 41 => [ - 13, - ], - [ - 13, - ], - [ - 13, - ], - [ - 13, - ], - [ - 13, - ], - [ - 13, - ], - [ - 13, - ], - [ - 13, - ], - 9 => [ - 60, - ], - 10 => [ - 60, - ], - 11 => [ - 60, - ], - 12 => [ - 60, - ], - ], - 7 => [ - 5 => [ - -9, - ], - [ - -10, - ], - [ - -11, - ], - [ - -12, - ], - ], - 14 => [ - [ - 15, - ], - [ - 15, - ], - [ - 15, - ], - [ - 15, - ], - 13 => [ - 20, - ], - [ - 20, - ], - 17 => [ - 20, - ], - [ - 20, - ], - [ - 20, - ], - [ - 20, - ], - [ - 20, - ], - [ - 20, - ], - 15 => [ - 31, - ], - 16 => [ - 31, - ], - 41 => [ - 31, - ], - [ - 31, - ], - [ - 31, - ], - [ - 31, - ], - [ - 31, - ], - [ - 31, - ], - [ - 31, - ], - [ - 31, - ], - ], - [ - [ - -17, - ], - [ - -18, - ], - [ - -19, - ], - [ - -20, - ], - ], - 20 => [ - 14 => [ - -22, - ], - 13 => [ - -23, - ], - 17 => [ - 23, - ], - [ - 23, - ], - [ - 23, - ], - [ - 23, - ], - [ - 23, - ], - [ - 23, - ], - ], - 24 => [ - 17 => [ - -26, - ], - [ - -27, - ], - [ - -28, - ], - [ - -29, - ], - [ - -30, - ], - [ - -31, - ], - ], - 31 => [ - 41 => [ - -33, - ], - [ - -34, - ], - [ - -35, - ], - [ - -36, - ], - [ - -37, - ], - [ - -38, - ], - [ - -39, - ], - 15 => [ - -40, - ], - 16 => [ - -41, - ], - [ - -42, - ], - ], - 43 => [ - 24 => [ - -45, - ], - [ - -46, - ], - [ - -47, - ], - [ - -48, - ], - [ - -49, - ], - [ - -50, - ], - [ - -51, - ], - [ - -52, - ], - [ - -53, - ], - [ - -54, - ], - [ - -55, - ], - [ - -56, - ], - [ - -57, - ], - [ - -58, - ], - [ - -59, - ], - [ - -60, - ], - ], - 61 => [ - 9 => [ - -63, - ], - [ - -64, - ], - [ - -65, - ], - [ - 65, - ], - ], - ]; - - /** @var array> */ - private const PARSER_SEQUENCE_PREDICTION_TABLE = [ - 1 => [ - -4, - -69, - ], - 13 => [ - 14, - -44, - ], - 69 => [ - 70, - -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))|(?:(?:\\(\\?#(.*?)(?\\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: [ - 49 => '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_BACKREFERENCE', - 'T_SUBROUTINE', - '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_SUBROUTINE_G', - 'T_BACKREFERENCE', - 'T_LITERAL', - ], - transitions: [ - 0 => $state_class, - 1 => $state_class, - 2 => $state_class, - 3 => $state_class, - ], - subgroups: [ - 9 => 1, - 1, - 1, - 13 => 1, - 1, - 17 => 1, - ], - ); - - $this->parser = new \Phplrt\Parser\Parser( - lexer: $this->lexer, - grammar: [ - new \Phplrt\Parser\Grammar\Optional(1), - 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, 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]), - 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, 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), - 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, 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(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), - 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([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([66, 1, 12]), - new \Phplrt\Parser\Grammar\Lexeme(self::T_ASSERTION_REFERENCE, true), - 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, - 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']), - 60 => \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 - { - $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 - { - $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 deleted file mode 100644 index be3012dccd4..00000000000 --- a/src/Type/Regex/Parser/RegexSyntaxException.php +++ /dev/null @@ -1,11 +0,0 @@ - $capturingGroups * @param list $markVerbs + * @param Type $subjectBaseType type of the whole match, $matches[0] + * @param Type $subjectNarrowType type the subject is narrowed to when the pattern matches + * @param Type|null $scanBaseType type of the whole match when the match is one of a scan, + * e.g. in a preg_replace_callback() callback, null when it is the same + * @param list> $cases cases whose union covers every match: the groups each case + * sets (true) or never sets (false), a group left out may be unset */ public function __construct( private int $alternationId, - private int $captureGroupId, private array $capturingGroups, private array $markVerbs, private Type $subjectBaseType, + private Type $subjectNarrowType, + private ?Type $scanBaseType = null, + private array $cases = [], ) { } @@ -27,11 +35,10 @@ public static function createEmpty(): self { return new self( -1, - // use different start-index for groups to make it easier to distinguish groupids from other ids - 100, [], [], new StringType(), + new StringType(), ); } @@ -39,21 +46,12 @@ public function nextAlternationId(): self { return new self( $this->alternationId + 1, - $this->captureGroupId, - $this->capturingGroups, - $this->markVerbs, - $this->subjectBaseType, - ); - } - - public function nextCaptureGroupId(): self - { - return new self( - $this->alternationId, - $this->captureGroupId + 1, $this->capturingGroups, $this->markVerbs, $this->subjectBaseType, + $this->subjectNarrowType, + $this->scanBaseType, + $this->cases, ); } @@ -64,10 +62,12 @@ public function addCapturingGroup(RegexCapturingGroup $group): self return new self( $this->alternationId, - $this->captureGroupId, $capturingGroups, $this->markVerbs, $this->subjectBaseType, + $this->subjectNarrowType, + $this->scanBaseType, + $this->cases, ); } @@ -78,10 +78,12 @@ public function markVerb(string $markVerb): self return new self( $this->alternationId, - $this->captureGroupId, $this->capturingGroups, $verbs, $this->subjectBaseType, + $this->subjectNarrowType, + $this->scanBaseType, + $this->cases, ); } @@ -89,21 +91,31 @@ public function withSubjectBaseType(Type $subjectBaseType): self { return new self( $this->alternationId, - $this->captureGroupId, $this->capturingGroups, $this->markVerbs, $subjectBaseType, + $this->subjectNarrowType, + $this->scanBaseType, + $this->cases, ); } - public function getAlternationId(): int + public function withSubjectNarrowType(Type $subjectNarrowType): self { - return $this->alternationId; + return new self( + $this->alternationId, + $this->capturingGroups, + $this->markVerbs, + $this->subjectBaseType, + $subjectNarrowType, + $this->scanBaseType, + $this->cases, + ); } - public function getCaptureGroupId(): int + public function getAlternationId(): int { - return $this->captureGroupId; + return $this->alternationId; } /** @@ -127,4 +139,51 @@ public function getSubjectBaseType(): Type return $this->subjectBaseType; } + public function withScanBaseType(?Type $scanBaseType): self + { + return new self( + $this->alternationId, + $this->capturingGroups, + $this->markVerbs, + $this->subjectBaseType, + $this->subjectNarrowType, + $scanBaseType, + $this->cases, + ); + } + + public function getScanBaseType(): Type + { + return $this->scanBaseType ?? $this->subjectBaseType; + } + + public function getSubjectNarrowType(): Type + { + return $this->subjectNarrowType; + } + + /** + * @param list> $cases + */ + public function withCases(array $cases): self + { + return new self( + $this->alternationId, + $this->capturingGroups, + $this->markVerbs, + $this->subjectBaseType, + $this->subjectNarrowType, + $this->scanBaseType, + $cases, + ); + } + + /** + * @return list> + */ + public function getCases(): array + { + return $this->cases; + } + } diff --git a/src/Type/Regex/RegexCapturingGroup.php b/src/Type/Regex/RegexCapturingGroup.php index b33d620db48..f9be3decf86 100644 --- a/src/Type/Regex/RegexCapturingGroup.php +++ b/src/Type/Regex/RegexCapturingGroup.php @@ -17,6 +17,7 @@ public function __construct( private readonly bool $forceNonOptional = false, private readonly ?Type $forceType = null, private readonly bool $forceAlternationBranchMatched = false, + private readonly bool $neverMatched = false, ) { } @@ -38,6 +39,7 @@ public function forceNonOptional(): self true, $this->forceType, $this->forceAlternationBranchMatched, + $this->neverMatched, ); } @@ -57,6 +59,23 @@ public function forceAlternationBranchMatched(): self $this->forceNonOptional, $this->forceType, true, + $this->neverMatched, + ); + } + + public function forceNeverMatched(): self + { + return new self( + $this->id, + $this->name, + $this->alternation, + $this->inOptionalQuantification, + $this->parent, + $this->type, + $this->forceNonOptional, + $this->forceType, + $this->forceAlternationBranchMatched, + true, ); } @@ -70,8 +89,9 @@ public function forceType(Type $type): self $this->parent, $type, $this->forceNonOptional, - $this->forceType, + $type, $this->forceAlternationBranchMatched, + $this->neverMatched, ); } @@ -87,6 +107,7 @@ public function withParent(RegexCapturingGroup|RegexNonCapturingGroup $parent): $this->forceNonOptional, $this->forceType, $this->forceAlternationBranchMatched, + $this->neverMatched, ); } @@ -188,6 +209,19 @@ public function getName(): ?string return $this->name; } + /** + * The engine never sets it, e.g. a group inside (?(DEFINE)...). + */ + public function isNeverMatched(): bool + { + return $this->neverMatched; + } + + public function getForceType(): ?Type + { + return $this->forceType; + } + public function getType(): Type { if ($this->forceType !== null) { diff --git a/src/Type/Regex/RegexGroupList.php b/src/Type/Regex/RegexGroupList.php index 37f4a483081..539055c803b 100644 --- a/src/Type/Regex/RegexGroupList.php +++ b/src/Type/Regex/RegexGroupList.php @@ -8,7 +8,6 @@ use Override; use PHPStan\ShouldNotHappenException; use PHPStan\Type\Type; -use function array_key_exists; use function array_reverse; use function count; @@ -131,64 +130,6 @@ 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 hasGroupResettingGroupCounter(): bool - { - foreach ($this->groups as $captureGroup) { - if ($captureGroup->resetsGroupCounter()) { - return true; - } - } - - return false; - } - public function getOnlyTopLevelAlternation(): ?RegexAlternation { $alternation = null; diff --git a/src/Type/Regex/RegexGroupParser.php b/src/Type/Regex/RegexGroupParser.php index 0aea8561d35..4cccbfbeaa1 100644 --- a/src/Type/Regex/RegexGroupParser.php +++ b/src/Type/Regex/RegexGroupParser.php @@ -2,949 +2,466 @@ namespace PHPStan\Type\Regex; -use Nette\Utils\RegexpException; -use Nette\Utils\Strings; -use Phplrt\Contracts\Parser\Exception\ParserExceptionInterface; -use Phplrt\Source\StringSource; +use PHPRegex\Parser\Analysis\CaptureGroupShape; +use PHPRegex\Parser\Analysis\CaptureShape; +use PHPRegex\Parser\Analysis\CaptureShapeAnalyzer; +use PHPRegex\Parser\Analysis\Participation; +use PHPRegex\Parser\Node\AlternationNode; +use PHPRegex\Parser\Node\ConditionalNode; +use PHPRegex\Parser\Node\DefineNode; +use PHPRegex\Parser\Node\GroupNode; +use PHPRegex\Parser\Node\GroupType; +use PHPRegex\Parser\Node\KeepNode; +use PHPRegex\Parser\Node\NodeInterface; +use PHPRegex\Parser\Node\QuantifierBounds; +use PHPRegex\Parser\Node\QuantifierNode; use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Php\PhpVersion; -use PHPStan\ShouldNotHappenException; -use PHPStan\TrinaryLogic; +use PHPStan\Type\Accessory\AccessoryDecimalIntegerStringType; +use PHPStan\Type\Accessory\AccessoryLowercaseStringType; use PHPStan\Type\Accessory\AccessoryNonEmptyStringType; use PHPStan\Type\Accessory\AccessoryNonFalsyStringType; use PHPStan\Type\Accessory\AccessoryNumericStringType; +use PHPStan\Type\Accessory\AccessoryUppercaseStringType; use PHPStan\Type\Constant\ConstantStringType; use PHPStan\Type\IntersectionType; -use PHPStan\Type\Regex\Parser\RegexNode; -use PHPStan\Type\Regex\Parser\RegexParser; -use PHPStan\Type\Regex\Parser\RegexSyntaxException; use PHPStan\Type\StringType; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; use PHPStan\Type\UnionType; +use function array_filter; use function array_key_exists; -use function array_pop; -use function array_values; +use function array_unique; use function count; -use function in_array; -use function is_int; -use function preg_replace; -use function rtrim; -use function sscanf; -use function str_contains; -use function str_replace; -use function strlen; -use function substr; -use function trim; - +use function max; +use function spl_object_id; + +/** + * Builds the capturing group list RegexArrayShapeMatcher turns into $matches types. + * + * The value of each group comes from PHPRegex's CaptureShapeAnalyzer. This class + * only walks the tree to find where each group sits: its parent group, and the + * alternation branch it belongs to. + */ #[AutowiredService] final class RegexGroupParser { - private const NOT_SUPPORTED_MODIFIERS = [ - 'J', // rare modifier too complicated to support - ]; - - // upper bound on the number of constant string literals enumerated from a group, - // 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 const CONDITIONS = ['#namedcondition', '#absolutecondition', '#relativecondition', '#assertioncondition']; - - private static ?RegexParser $parser = null; - - /** @var array */ - private static array $parsedAst = []; + /** @var array */ + private static array $parsedShapes = []; public function __construct( private PhpVersion $phpVersion, - private RegexExpressionHelper $regexExpressionHelper, + private PHPRegexReader $reader, ) { } public function parseGroups(string $regex): ?RegexAstWalkResult { - self::$parser ??= new RegexParser(); - - if (array_key_exists($regex, self::$parsedAst)) { - $ast = self::$parsedAst[$regex]; - if ($ast === null) { - return null; - } - - $modifiers = $this->regexExpressionHelper->getPatternModifiers($regex) ?? ''; - } else { - try { - Strings::match('', $regex); - } catch (RegexpException) { - // pattern is invalid, so let the RegularExpressionPatternRule report it - return self::$parsedAst[$regex] = null; - } - - $modifiers = $this->regexExpressionHelper->getPatternModifiers($regex) ?? ''; - foreach (self::NOT_SUPPORTED_MODIFIERS as $notSupportedModifier) { - if (str_contains($modifiers, $notSupportedModifier)) { - return self::$parsedAst[$regex] = null; - } - } - - if (str_contains($modifiers, 'x')) { - // in freespacing mode the # character starts a comment and runs until the end of the line - // but \# is an escaped literal hash, and (?#...) is an inline comment - neither starts a line comment - $regex = preg_replace('/(?phpVersion->getVersionId() . "\n" . $this->reader->judgedPcreVersion() . "\n" . $regex; - $rawRegex = $this->regexExpressionHelper->removeDelimitersAndModifiers($regex); - try { - $ast = self::$parser->parse(StringSource::createFromString($rawRegex)); - } catch (ParserExceptionInterface | RegexSyntaxException) { - return self::$parsedAst[$regex] = null; - } - 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; - } - - $this->updateAlternationAstRemoveVerticalBarsAndAddEmptyToken($ast); - $this->updateCapturingAstAddEmptyToken($ast); - - $captureOnlyNamed = false; - if ($this->phpVersion->supportsPregCaptureOnlyNamedGroups()) { - $captureOnlyNamed = str_contains($modifiers, 'n'); + if (array_key_exists($cacheKey, self::$parsedShapes)) { + return self::$parsedShapes[$cacheKey]; } - $astWalkResult = $this->walkRegexAst( - $ast, - null, - 0, - false, - null, - $captureOnlyNamed, - false, - $modifiers, - RegexAstWalkResult::createEmpty(), - ); - - $subjectAsGroupResult = $this->walkGroupAst( - $ast, - false, - $modifiers, - RegexGroupWalkResult::createEmpty(), - ); - - if (!$subjectAsGroupResult->mightContainEmptyStringLiteral() && !$this->containsEscapeK($ast)) { - // we could handle numeric-string, in case we know the regex is delimited by ^ and $ - if ($subjectAsGroupResult->isNonFalsy()->yes()) { - $astWalkResult = $astWalkResult->withSubjectBaseType( - new IntersectionType([new StringType(), new AccessoryNonFalsyStringType()]), - ); - } elseif ($subjectAsGroupResult->isNonEmpty()->yes()) { - $astWalkResult = $astWalkResult->withSubjectBaseType( - new IntersectionType([new StringType(), new AccessoryNonEmptyStringType()]), - ); - } + $ast = $this->reader->readExpression($regex); + if ($ast === null) { + return self::$parsedShapes[$cacheKey] = null; } - return $astWalkResult; - } + $shape = (new CaptureShapeAnalyzer())->analyze($ast); - private function createEmptyTokenRegexNode(): RegexNode - { - return new RegexNode('token', 'literal', ''); + return self::$parsedShapes[$cacheKey] = $this->buildWalkResult($shape, $ast->pattern); } - private function containsSubroutineCall(RegexNode $ast): bool + private function buildWalkResult(CaptureShape $shape, NodeInterface $pattern): RegexAstWalkResult { - if ($ast->getId() === 'token' && $ast->getValueToken() === 'subroutine') { - return true; - } + $topology = $this->collectTopology($pattern, null, null, 0, ['alternationNodes' => [], 'groups' => [], 'nonCapturing' => [], 'order' => [], 'nextNumber' => 1]); - foreach ($ast->getChildren() as $child) { - if ($this->containsSubroutineCall($child)) { - return true; - } - } - - return false; - } - - private function updateAlternationAstRemoveVerticalBarsAndAddEmptyToken(RegexNode $ast): void - { - $children = $ast->getChildren(); - - foreach ($children as $i => $child) { - $this->updateAlternationAstRemoveVerticalBarsAndAddEmptyToken($child); - - if ($ast->getId() !== '#alternation' || $child->getValueToken() !== 'alternation') { - continue; - } - - unset($children[$i]); - - if ($i !== 0 - && isset($children[$i + 1]) - && $children[$i + 1]->getValueToken() !== 'alternation') { - continue; - } - - $children[$i] = $this->createEmptyTokenRegexNode(); - } - - $ast->setChildren(array_values($children)); - } - - private function updateCapturingAstAddEmptyToken(RegexNode $ast): void - { - foreach ($ast->getChildren() as $child) { - $this->updateCapturingAstAddEmptyToken($child); - } - - if ($ast->getId() !== '#capturing' || $ast->getChildren() !== []) { - return; - } - - $ast->setChildren([new RegexNode('#alternation', children: [$this->createEmptyTokenRegexNode()])]); - } - - private function containsEscapeK(RegexNode $ast): bool - { - if ($ast->getId() === 'token' && $ast->getValueToken() === 'match_point_reset') { - return true; - } - - foreach ($ast->getChildren() as $child) { - if ($this->containsEscapeK($child)) { - return true; - } + $walkResult = RegexAstWalkResult::createEmpty(); + foreach ($shape->marks as $mark) { + $walkResult = $walkResult->markVerb($mark); } - return false; - } - - private function walkRegexAst( - RegexNode $ast, - ?RegexAlternation $alternation, - int $combinationIndex, - bool $inOptionalQuantification, - RegexCapturingGroup|RegexNonCapturingGroup|null $parentGroup, - bool $captureOnlyNamed, - bool $repeatedMoreThanOnce, - string $patternModifiers, - RegexAstWalkResult $astWalkResult, - ): RegexAstWalkResult - { - $group = null; - if ($ast->getId() === '#capturing') { - $astWalkResult = $astWalkResult->nextCaptureGroupId(); - - $group = new RegexCapturingGroup( - $astWalkResult->getCaptureGroupId(), - null, - $alternation, - $inOptionalQuantification, - $parentGroup, - $this->createGroupType( - $ast, - $this->allowConstantTypes($patternModifiers, $repeatedMoreThanOnce, $parentGroup), - $patternModifiers, - ), - ); - $parentGroup = $group; - } elseif ($ast->getId() === '#namedcapturing') { - $astWalkResult = $astWalkResult->nextCaptureGroupId(); - - $name = $ast->getChild(0)->getValueValue(); - $group = new RegexCapturingGroup( - $astWalkResult->getCaptureGroupId(), - $name, - $alternation, - $inOptionalQuantification, - $parentGroup, - $this->createGroupType( - $ast, - $this->allowConstantTypes($patternModifiers, $repeatedMoreThanOnce, $parentGroup), - $patternModifiers, - ), - ); - $parentGroup = $group; - } elseif ($ast->getId() === '#noncapturing') { - $group = new RegexNonCapturingGroup( - $alternation, - $inOptionalQuantification, - $parentGroup, - false, - ); - $parentGroup = $group; - } elseif ($ast->getId() === '#noncapturingreset') { - $group = new RegexNonCapturingGroup( - $alternation, - $inOptionalQuantification, - $parentGroup, - true, - ); - $parentGroup = $group; - } 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, - $parentGroup, - false, + $alternations = []; + $branchCounts = []; + foreach ($topology['alternationNodes'] as $alternationNode) { + $branchCounts[spl_object_id($alternationNode['node'])] = $alternationNode['branches']; + $walkResult = $walkResult->nextAlternationId(); + $alternations[spl_object_id($alternationNode['node'])] = new RegexAlternation( + $walkResult->getAlternationId(), + $alternationNode['branches'], ); - $parentGroup = $group; - } - - $inOptionalQuantification = false; - if ($ast->getId() === '#quantification') { - [$min, $max] = $this->getQuantificationRange($ast); - - if ($min === 0) { - $inOptionalQuantification = true; - } - - if ($max === null || $max > 1) { - $repeatedMoreThanOnce = true; - } - } - - if ($ast->getId() === '#alternation') { - $astWalkResult = $astWalkResult->nextAlternationId(); - $alternation = new RegexAlternation($astWalkResult->getAlternationId(), count($ast->getChildren())); - } - - if ($ast->getId() === '#mark') { - return $astWalkResult->markVerb($ast->getChild(0)->getValueValue()); - } - - if ( - $group instanceof RegexCapturingGroup && - (!$captureOnlyNamed || $group->isNamed()) - ) { - $astWalkResult = $astWalkResult->addCapturingGroup($group); - - if ($alternation !== null) { - $alternation->pushGroup($combinationIndex, $group); - } } - foreach ($ast->getChildren() as $child) { - $astWalkResult = $this->walkRegexAst( - $child, - $alternation, - $combinationIndex, - $inOptionalQuantification, - $parentGroup, - $captureOnlyNamed, - $repeatedMoreThanOnce, - $patternModifiers, - $astWalkResult, - ); - - if ($ast->getId() !== '#alternation') { + $everyBranchAlternations = []; + foreach ($topology['groups'] as $entry) { + if ($entry['alternation'] === null) { continue; } - $combinationIndex++; - } - - return $astWalkResult; - } - - private function allowConstantTypes( - string $patternModifiers, - bool $repeatedMoreThanOnce, - RegexCapturingGroup|RegexNonCapturingGroup|null $parentGroup, - ): bool - { - if (str_contains($patternModifiers, 'i')) { - // if caseless, we don't use constant types - // because it likely yields too many combinations - return false; - } - - if ($repeatedMoreThanOnce) { - return false; - } - - if ($parentGroup !== null && $parentGroup->resetsGroupCounter()) { - return false; + $id = spl_object_id($entry['alternation']); + $inEveryBranch = count(array_unique($entry['branches'])) === $branchCounts[$id]; + $everyBranchAlternations[$id] = ($everyBranchAlternations[$id] ?? true) && $inEveryBranch; } + $everyBranchAlternations = array_filter($everyBranchAlternations); - return true; - } - - /** @return array{?int, ?int} */ - private function getQuantificationRange(RegexNode $node): array - { - if ($node->getId() !== '#quantification') { - throw new ShouldNotHappenException(); + $groupShapes = []; + foreach ($shape->groups as $groupShape) { + $groupShapes[$groupShape->number] = $groupShape; } - $min = null; - $max = null; - - $children = $node->getChildren(); - $lastChild = $children[count($children) - 1]; - - // normalize away possessive and lazy quantifier-modifiers - $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)) { - throw new ShouldNotHappenException(); - } - - $min = $n; - $max = $m; - } elseif ($token === 'n_or_more') { - if (sscanf($value, '{%d,}', $n) !== 1 || !is_int($n)) { - throw new ShouldNotHappenException(); - } - - $min = $n; - } elseif ($token === 'exactly_n') { - if (sscanf($value, '{%d}', $n) !== 1 || !is_int($n)) { - throw new ShouldNotHappenException(); + // groups are created in tree order, so a parent group, capturing or not, exists before its children + $objects = []; + $capturingGroups = []; + foreach ($topology['order'] as $item) { + if ($item['kind'] === 'nonCapturing') { + $nonCapturing = $topology['nonCapturing'][$item['node']]; + $objects[$item['node']] = new RegexNonCapturingGroup( + $nonCapturing['alternation'] !== null ? ($alternations[spl_object_id($nonCapturing['alternation'])] ?? null) : null, + $nonCapturing['inOptional'], + $nonCapturing['parent'] !== null ? ($objects[$nonCapturing['parent']] ?? null) : null, + $nonCapturing['resets'], + ); + continue; } - $min = $n; - $max = $n; - } elseif ($token === 'zero_or_one') { - $min = 0; - $max = 1; - } elseif ($token === 'zero_or_more') { - $min = 0; - } elseif ($token === 'one_or_more') { - $min = 1; - } - - return [$min, $max]; - } - - private function createGroupType(RegexNode $group, bool $maybeConstant, string $patternModifiers): Type - { - $rootAlternation = $this->getRootAlternation($group); - if ($rootAlternation !== null) { - $types = []; - foreach ($rootAlternation->getChildren() as $alternative) { - $types[] = $this->createGroupType($alternative, $maybeConstant, $patternModifiers); - } - - return TypeCombinator::union(...$types); - } - - $walkResult = $this->walkGroupAst( - $group, - false, - $patternModifiers, - RegexGroupWalkResult::createEmpty(), - ); - - if ($maybeConstant && $walkResult->getOnlyLiterals() !== null && $walkResult->getOnlyLiterals() !== []) { - $result = []; - foreach ($walkResult->getOnlyLiterals() as $literal) { - $result[] = new ConstantStringType($literal); + $number = $item['number']; + // in a branch reset, several nodes share one group number + if (isset($capturingGroups[$number])) { + $objects[$item['node']] = $capturingGroups[$number]; + continue; } - return TypeCombinator::union(...$result); - } - if ($walkResult->isNumeric()->yes()) { - if ($walkResult->isNonFalsy()->yes()) { - return new IntersectionType([ - new StringType(), - new AccessoryNumericStringType(), - new AccessoryNonFalsyStringType(), - ]); + $entry = $topology['groups'][$number]; + $groupShape = $groupShapes[$number] ?? null; + if ($groupShape === null) { + continue; } - $result = new IntersectionType([new StringType(), new AccessoryNumericStringType()]); - if (!$walkResult->isNonEmpty()->yes()) { - return new UnionType([new ConstantStringType(''), $result]); + // a branch reset whose groups all capture in every branch sets them all, whichever branch matches + if ($entry['alternation'] !== null && isset($everyBranchAlternations[spl_object_id($entry['alternation'])])) { + $entry['alternation'] = null; } - return $result; - } elseif ($walkResult->isNonFalsy()->yes()) { - return new IntersectionType([new StringType(), new AccessoryNonFalsyStringType()]); - } elseif ($walkResult->isNonEmpty()->yes()) { - return new IntersectionType([new StringType(), new AccessoryNonEmptyStringType()]); - } - - return new StringType(); - } - - private function getRootAlternation(RegexNode $group): ?RegexNode - { - if ( - $group->getId() === '#capturing' - && count($group->getChildren()) === 1 - && $group->getChild(0)->getId() === '#alternation' - ) { - return $group->getChild(0); - } - - // 1st token within a named capturing group is a token holding the group-name - if ( - $group->getId() === '#namedcapturing' - && count($group->getChildren()) === 2 - && $group->getChild(1)->getId() === '#alternation' - ) { - return $group->getChild(1); - } - - return null; - } - private function walkGroupAst( - RegexNode $ast, - bool $inClass, - string $patternModifiers, - 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; - } - - 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()); - } + // e.g. a group inside (?(DEFINE)...): it is numbered but never set + $neverMatched = $groupShape->participation === Participation::Never(); + $type = $neverMatched + ? new ConstantStringType('') + : $this->typeOf($groupShape); - $children = $ast->getChildren(); - $quantifiedLiterals = null; - - if ( - $ast->getId() === '#concatenation' - && count($children) > 0 - && !$walkResult->isInOptionalQuantification() - ) { - $meaningfulTokens = 0; - foreach ($children as $child) { - $nonFalsy = false; - if ($this->isMaybeEmptyNode($child, $patternModifiers, $nonFalsy)) { - continue; - } - - $meaningfulTokens++; - - if (!$nonFalsy) { - continue; - } + $parent = $item['parent'] !== null ? ($objects[$item['parent']] ?? null) : null; + $inOptional = $entry['inOptional']; - // a single token non-falsy on its own - $walkResult = $walkResult->nonFalsy(TrinaryLogic::createYes()); - break; + // the analysis can know that a group may be unset for reasons the tree + // walk does not see, e.g. a (*ACCEPT) before it or a negative lookaround around it + $unsetUnseen = $neverMatched + || ( + $groupShape->participation === Participation::MayBeUnset() + && !$inOptional + && ($parent === null || !$parent->isOptional()) + && ($entry['alternation'] === null || $this->mayBeUnsetInItsBranch($shape, $number)) + ); + if ($unsetUnseen) { + $inOptional = true; } - if ($meaningfulTokens > 0) { - $walkResult = $walkResult->nonEmpty(TrinaryLogic::createYes()); - - // two non-empty tokens concatenated results in a non-falsy string - if ($meaningfulTokens > 1) { - $walkResult = $walkResult->nonFalsy(TrinaryLogic::createYes()); - } - } - } elseif ($ast->getId() === '#quantification') { - [$min, $max] = $this->getQuantificationRange($ast); + $group = new RegexCapturingGroup( + $groupShape->number, + $groupShape->name, + $entry['alternation'] !== null ? $alternations[spl_object_id($entry['alternation'])] ?? null : null, + $inOptional, + $parent, + $type, + neverMatched: $neverMatched, + ); + $objects[$item['node']] = $group; + $capturingGroups[$number] = $group; - if ($min === 0) { - $walkResult = $walkResult->inOptionalQuantification(true); - } + $walkResult = $walkResult->addCapturingGroup($group); - // 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()); - } - if ($min >= 2) { - $walkResult = $walkResult->nonFalsy(TrinaryLogic::createYes()); - } + if ($entry['alternation'] === null) { + continue; } - // "a?" yields 'a'|'', "a{1,2}" yields 'a'|'aa', etc. so a bounded quantification - // over constant literals can be combined with the surrounding literals - $quantifiedLiterals = $this->getQuantifiedLiterals($ast, $min, $max, $inClass, $patternModifiers, $walkResult); - - $walkResult = $walkResult->onlyLiterals(null); - } elseif ($ast->getId() === '#class' && $walkResult->getOnlyLiterals() !== null) { - $inClass = true; - - $newLiterals = []; - foreach ($children as $child) { - $oldLiterals = $walkResult->getOnlyLiterals(); - - $this->getLiteralValue($child, $oldLiterals, true, $patternModifiers, true); - foreach ($oldLiterals ?? [] as $oldLiteral) { - $newLiterals[] = $oldLiteral; - } - } - $walkResult = $walkResult->onlyLiterals($newLiterals); - } elseif ($ast->getId() === 'token') { - $onlyLiterals = $walkResult->getOnlyLiterals(); - $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()); + $alternationObject = $alternations[spl_object_id($entry['alternation'])] ?? null; + if ($alternationObject === null) { + continue; } - if ($literalValue !== null) { - if (Strings::match($literalValue, '/^\d+$/') === null) { - $walkResult = $walkResult->numeric(TrinaryLogic::createNo()); - } elseif ($walkResult->isNumeric()->maybe()) { - $walkResult = $walkResult->numeric(TrinaryLogic::createYes()); - } - - if (!$walkResult->isInOptionalQuantification() && $literalValue !== '') { - $walkResult = $walkResult->nonEmpty(TrinaryLogic::createYes()); - } + foreach ($entry['branches'] as $branch) { + $alternationObject->pushGroup($branch, $group); } - } elseif (!in_array($ast->getId(), ['#capturing', '#namedcapturing', '#alternation'], true)) { - $walkResult = $walkResult->onlyLiterals(null); } - if ($ast->getId() === '#alternation') { - if (count($children) === 0) { - return $walkResult; - } - - // literals accumulated before the alternation form a common prefix that has to be - // combined with every branch, e.g. "a(b|c)" yields "ab"|"ac", not "b"|"c" - $prefixLiterals = $walkResult->getOnlyLiterals(); - $newLiterals = []; - $nonEmpty = TrinaryLogic::createYes(); - $nonFalsy = TrinaryLogic::createYes(); - $numeric = TrinaryLogic::createYes(); - foreach ($children as $child) { - $childResult = $this->walkGroupAst( - $child, - $inClass, - $patternModifiers, - $walkResult->onlyLiterals([]) - ->nonEmpty(TrinaryLogic::createMaybe()) - ->nonFalsy(TrinaryLogic::createMaybe()) - ->numeric(TrinaryLogic::createMaybe()), - ); - - $nonEmpty = $nonEmpty->and($childResult->isNonEmpty()); - $nonFalsy = $nonFalsy->and($childResult->isNonFalsy()); - $numeric = $numeric->and($childResult->isNumeric()); - - if ($newLiterals === null) { - continue; - } - - $childLiterals = $childResult->getOnlyLiterals(); - if ($prefixLiterals === null || $childLiterals === null || count($childLiterals) === 0) { - $newLiterals = null; - continue; - } + $whole = $shape->whole; + $matchType = $this->containsEscapeK($pattern) + ? new StringType() + : $this->typeOf($whole); - foreach ($childLiterals as $childLiteral) { - if ($prefixLiterals === []) { - $newLiterals[] = $childLiteral; - } else { - foreach ($prefixLiterals as $prefixLiteral) { - $newLiterals[] = $prefixLiteral . $childLiteral; - } - } + // a pattern pinned at both ends tells what the whole subject holds, a final newline + // included where $ lets one through; otherwise the subject contains the match, + // so it is non-empty or non-falsy when the match is + if ($shape->subject !== null) { + $subjectNarrowType = $this->typeOf($shape->subject); + } elseif ($whole->nonFalsy) { + $subjectNarrowType = new IntersectionType([new StringType(), new AccessoryNonFalsyStringType()]); + } elseif ($whole->minLength >= 1) { + $subjectNarrowType = new IntersectionType([new StringType(), new AccessoryNonEmptyStringType()]); + } else { + $subjectNarrowType = new StringType(); + } + + // a scan resumes after an empty match, where a zero-width first branch like ^ no longer matches + $scanWhole = $shape->scanWhole; + $scanType = $scanWhole === null || $this->containsEscapeK($pattern) + ? null + : $this->typeOf($scanWhole); + + // the analysis splits a pattern into cases whose union covers every match, + // e.g. an optional group that runs with its nested groups or is skipped with them + $cases = []; + foreach ($shape->cases as $case) { + $participations = []; + foreach ($case->groups as $groupShape) { + if ($groupShape->participation === Participation::Always()) { + $participations[$groupShape->number] = true; + } elseif ($groupShape->participation === Participation::Never()) { + $participations[$groupShape->number] = false; } } - - return $walkResult - ->onlyLiterals($newLiterals) - ->nonEmpty($walkResult->isNonEmpty()->or($nonEmpty)) - ->nonFalsy($walkResult->isNonFalsy()->or($nonFalsy)) - ->numeric($walkResult->isNumeric()->and($numeric)); - } - - // [^0-9] should not parse as numeric-string, and [^list-everything-but-numbers] is technically - // doable but really silly compared to just \d so we can safely assume the string is not numeric - // for negative classes - if ($ast->getId() === '#negativeclass') { - $walkResult = $walkResult->numeric(TrinaryLogic::createNo()); - } - - foreach ($children as $child) { - $walkResult = $this->walkGroupAst( - $child, - $inClass, - $patternModifiers, - $walkResult, - ); - } - - if ($ast->getId() === '#quantification') { - // the bottom walk above nulls the literals via the quantifier token, - // so restore the literals enumerated up-front for bounded quantifications - $walkResult = $walkResult->onlyLiterals($quantifiedLiterals); + $cases[] = $participations; } - return $walkResult; + return $walkResult + ->withSubjectBaseType($matchType) + ->withSubjectNarrowType($subjectNarrowType) + ->withScanBaseType($scanType) + ->withCases($cases); } /** - * Enumerate the constant strings a bounded quantification (like "a?", "a{2}", "a{1,3}") - * produces, combined with the literals accumulated so far. Returns null when the result - * cannot be enumerated (unbounded quantifier, non-literal atom, or too many combinations). - * - * @return array|null + * Whether a group of an alternation may be unset even when its branch matches, + * e.g. (c) in /(a)|b(*ACCEPT)(c)/. Each case of the shape covers the matches of one branch. */ - private function getQuantifiedLiterals(RegexNode $ast, ?int $min, ?int $max, bool $inClass, string $patternModifiers, RegexGroupWalkResult $walkResult): ?array + private function mayBeUnsetInItsBranch(CaptureShape $shape, int $number): bool { - $prefixLiterals = $walkResult->getOnlyLiterals(); - if ($prefixLiterals === null || $min === null || $max === null) { - return null; - } - - // walk the quantified atom standalone (everything but the trailing quantifier token); - // the atom itself is not optional, so reset the flag to let concatenations accumulate literals - $atomChildren = $ast->getChildren(); - array_pop($atomChildren); - - $atomResult = $walkResult->onlyLiterals([])->inOptionalQuantification(false); - foreach ($atomChildren as $atomChild) { - $atomResult = $this->walkGroupAst($atomChild, $inClass, $patternModifiers, $atomResult); - } - - $atomLiterals = $atomResult->getOnlyLiterals(); - if ($atomLiterals === null) { - return null; - } - - $repeatedLiterals = $this->repeatLiterals($atomLiterals, $min, $max); - if ($repeatedLiterals === null) { - return null; - } - - $newLiterals = []; - foreach ($repeatedLiterals as $repeatedLiteral) { - if ($prefixLiterals === []) { - $newLiterals[] = $repeatedLiteral; - } else { - foreach ($prefixLiterals as $prefixLiteral) { - $newLiterals[] = $prefixLiteral . $repeatedLiteral; - } - } - - if (count($newLiterals) > self::LITERALS_LIMIT) { - return null; + foreach ($shape->cases as $case) { + if (isset($case->groups[$number]) && $case->groups[$number]->participation === Participation::MayBeUnset()) { + return true; } } - return $newLiterals; + return false; } /** - * @param array $literals - * @return array|null + * Numbers the capturing groups the way PCRE does, and records for each one + * its parent group, the alternation above it and its branch in that alternation. + * + * @param array{alternationNodes: list, groups: array, inOptional: bool}>, nonCapturing: array, order: list, nextNumber: int} $topology + * @return array{alternationNodes: list, groups: array, inOptional: bool}>, nonCapturing: array, order: list, nextNumber: int} */ - private function repeatLiterals(array $literals, int $min, int $max): ?array + private function collectTopology( + NodeInterface $node, + ?int $parentId, + ?NodeInterface $alternation, + int $branch, + array $topology, + bool $inOptionalQuantification = false, + bool $inRepetition = false, + ): array { - $collected = []; - if ($min === 0) { - $collected[''] = ''; - } - - $current = ['']; - for ($k = 1; $k <= $max; $k++) { - $next = []; - foreach ($current as $prefix) { - foreach ($literals as $literal) { - $next[] = $prefix . $literal; - } - - if (count($next) > self::LITERALS_LIMIT) { - return null; + if ($node instanceof AlternationNode) { + // a repeated alternation sets the groups of several branches: /(?:(a)|(b))+/ writes ['ab', 'a', 'b'] + if ($inRepetition) { + foreach ($node->alternatives as $alternative) { + $topology = $this->collectTopology($alternative, $parentId, null, 0, $topology, false, true); } - } - $current = $next; - if ($k < $min) { - continue; + return $topology; } - foreach ($current as $value) { - $collected[$value] = $value; + $topology['alternationNodes'][] = ['node' => $node, 'branches' => count($node->alternatives)]; + foreach ($node->alternatives as $i => $alternative) { + $topology = $this->collectTopology($alternative, $parentId, $node, $i, $topology); } - if (count($collected) > self::LITERALS_LIMIT) { - return null; - } + return $topology; } - return array_values($collected); - } + if ($node instanceof DefineNode) { + // groups inside (?(DEFINE)...) are never set; an optional parent makes them optional + $topology['nonCapturing'][spl_object_id($node)] = [ + 'parent' => $parentId, + 'alternation' => $alternation, + 'inOptional' => true, + 'resets' => false, + ]; + $topology['order'][] = ['kind' => 'nonCapturing', 'node' => spl_object_id($node)]; - private function isMaybeEmptyNode(RegexNode $node, string $patternModifiers, bool &$isNonFalsy): bool - { - if (in_array($node->getId(), self::ASSERTIONS, true) || in_array($node->getId(), self::CONDITIONS, true)) { - return true; + return $this->collectTopology($node->content, spl_object_id($node), null, 0, $topology, false, $inRepetition); } - if ($node->getId() === '#quantification') { - [$min] = $this->getQuantificationRange($node); - - if ($min > 0) { - $atomNonFalsy = false; - return $this->isMaybeEmptyNode($node->getChildren()[0], $patternModifiers, $atomNonFalsy); - } + if ($node instanceof ConditionalNode && !$inRepetition) { + // groups in a lookaround condition are numbered first; only one of the + // two branches runs, so they are read like an alternation + $topology['alternationNodes'][] = ['node' => $node, 'branches' => 2]; + $topology = $this->collectTopology($node->condition, $parentId, null, 0, $topology); + $topology = $this->collectTopology($node->yes, $parentId, $node, 0, $topology); - if ($min === 0) { - return true; - } + return $this->collectTopology($node->no, $parentId, $node, 1, $topology); } - $literal = $this->getLiteralValue($node, $onlyLiterals, false, $patternModifiers, false); - if ($literal !== null) { - if ($literal !== '' && $literal !== '0') { - $isNonFalsy = true; - } - return $literal === ''; - } + // only the node directly under the quantifier is marked optional, + // nested groups get it through their parent + if ($node instanceof QuantifierNode) { + $bounds = QuantifierBounds::parse($node->quantifier); - // an alternation is maybe-empty if any of its branches is maybe-empty, - // and non-falsy only if every branch is non-falsy - if ($node->getId() === '#alternation') { - $maybeEmpty = false; - $allNonFalsy = true; - foreach ($node->getChildren() as $child) { - $childNonFalsy = false; - if ($this->isMaybeEmptyNode($child, $patternModifiers, $childNonFalsy)) { - $maybeEmpty = true; - } - $allNonFalsy = $allNonFalsy && $childNonFalsy; - } + return $this->collectTopology( + $node->node, + $parentId, + $alternation, + $branch, + $topology, + $bounds !== null && $bounds->min === 0, + $inRepetition || $bounds === null || $bounds->max === null || $bounds->max > 1, + ); + } - if ($allNonFalsy) { - $isNonFalsy = true; + if ($node instanceof GroupNode && $node->type === GroupType::BranchReset()) { + // (?|...): every branch numbers its groups from the same number, + // and the group after it continues after the longest branch + $topology['nonCapturing'][spl_object_id($node)] = [ + 'parent' => $parentId, + 'alternation' => $alternation, + 'inOptional' => $inOptionalQuantification, + 'resets' => true, + ]; + $topology['order'][] = ['kind' => 'nonCapturing', 'node' => spl_object_id($node)]; + $base = $topology['nextNumber']; + $end = $base; + $branchAlternation = $node->child instanceof AlternationNode && !$inRepetition ? $node->child : null; + if ($branchAlternation !== null) { + $topology['alternationNodes'][] = ['node' => $branchAlternation, 'branches' => count($branchAlternation->alternatives)]; + $branches = $branchAlternation->alternatives; + } else { + $branches = $node->child instanceof AlternationNode ? $node->child->alternatives : [$node->child]; + } + foreach ($branches as $i => $branch) { + $topology['nextNumber'] = $base; + $topology = $this->collectTopology($branch, spl_object_id($node), $branchAlternation, $branchAlternation === null ? 0 : $i, $topology, $inOptionalQuantification, $inRepetition); + $end = max($end, $topology['nextNumber']); + } + $topology['nextNumber'] = $end; + + return $topology; + } + + if ($node instanceof GroupNode) { + if ($node->type === GroupType::Capturing() || $node->type === GroupType::Named()) { + $number = $topology['nextNumber']; + $topology['nextNumber'] = $number + 1; + $entry = $topology['groups'][$number] ?? null; + $topology['groups'][$number] = [ + 'node' => $node, + 'parent' => $parentId, + 'alternation' => $alternation, + // in a branch reset, one number can be captured in several branches + 'branches' => [...($entry['branches'] ?? []), $branch], + 'inOptional' => $inOptionalQuantification, + ]; + $topology['order'][] = ['kind' => 'capturing', 'node' => spl_object_id($node), 'number' => $number, 'parent' => $parentId]; + $parentId = spl_object_id($node); + } else { + $topology['nonCapturing'][spl_object_id($node)] = [ + 'parent' => $parentId, + 'alternation' => $alternation, + 'inOptional' => $inOptionalQuantification, + 'resets' => false, + ]; + $topology['order'][] = ['kind' => 'nonCapturing', 'node' => spl_object_id($node)]; + $parentId = spl_object_id($node); } - - return $maybeEmpty; } foreach ($node->getChildren() as $child) { - if (!$this->isMaybeEmptyNode($child, $patternModifiers, $isNonFalsy)) { - return false; - } + $topology = $this->collectTopology($child, $parentId, $alternation, $branch, $topology, false, $inRepetition); } - return true; + return $topology; } /** - * @param array|null $onlyLiterals + * A group that reads either constants or digits, like (a|\d), keeps each arm: 'a'|numeric-string. */ - private function getLiteralValue(RegexNode $node, ?array &$onlyLiterals, bool $appendLiterals, string $patternModifiers, bool $inCharacterClass): ?string + private function typeOf(CaptureGroupShape $shape): Type { - if ($node->getId() !== 'token') { - return null; + if ($shape->arms === []) { + return $this->stringTypeOf($shape); } - // token is the token name from grammar without the namespace so literal and class:literal are both called literal here - $token = $node->getValueToken(); - $value = $node->getValueValue(); - - if ( - in_array($token, [ - 'literal', - // literal "-" in front/back of a character class like '[-a-z]' or '[abc-]', not forming a range - 'range', - // literal "[" or "]" inside character classes '[[]' or '[]]' - 'class_', '_class', - ], true) - ) { - if (str_contains($patternModifiers, 'x') && trim($value) === '') { - return null; - } + $types = []; + foreach ($shape->arms as $arm) { + $types[] = $this->stringTypeOf($arm); + } - $isEscaped = false; - if (strlen($value) > 1 && $value[0] === '\\') { - $value = substr($value, 1) ?: ''; - $isEscaped = true; - } + return TypeCombinator::union(...$types); + } - if ( - $appendLiterals - && $onlyLiterals !== null - ) { - if ( - in_array($value, ['.'], true) - && !($isEscaped || $inCharacterClass) - ) { - $onlyLiterals = null; - } else { - if ($onlyLiterals === []) { - $onlyLiterals = [$value]; - } else { - foreach ($onlyLiterals as &$literal) { - $literal .= $value; - } - } - } + private function stringTypeOf(CaptureGroupShape $shape): Type + { + if ($shape->values !== null && $shape->values !== []) { + $types = []; + foreach ($shape->values as $value) { + $types[] = new ConstantStringType($value); } - return $value; + return TypeCombinator::union(...$types); } - if (!in_array($token, ['capturing_name'], true)) { - $onlyLiterals = null; + $accessories = []; + if ($shape->decimalInt) { + $accessories[] = new AccessoryDecimalIntegerStringType(); + } elseif ($shape->digitsOnly) { + $accessories[] = new AccessoryNumericStringType(); + } + if ($shape->nonFalsy) { + $accessories[] = new AccessoryNonFalsyStringType(); + } elseif ($shape->minLength >= 1 && !$shape->digitsOnly && !$shape->decimalInt) { + $accessories[] = new AccessoryNonEmptyStringType(); + } + // a case is kept when it alone holds: a value with no letter, digits among them, is both + if (!$shape->digitsOnly && !$shape->decimalInt && $shape->lowercase !== $shape->uppercase) { + $accessories[] = $shape->lowercase ? new AccessoryLowercaseStringType() : new AccessoryUppercaseStringType(); } - // character escape sequences, just return a fixed string - if (in_array($token, ['character', 'dynamic_character', 'character_type'], true)) { - if ($token === 'character_type' && $value === '\d') { - return '0'; - } + $type = $accessories === [] ? new StringType() : new IntersectionType([new StringType(), ...$accessories]); - return $value; + // a run of digits of any length is digits or the empty string + if ($shape->digitsOnly && !$shape->decimalInt && $shape->minLength === 0) { + return new UnionType([new ConstantStringType(''), $type]); } - // [:digit:] and the like, more support coming later - if ($token === 'posix_class') { - if ($value === '[:digit:]') { - return '0'; - } - if (in_array($value, ['[:alpha:]', '[:alnum:]', '[:upper:]', '[:lower:]', '[:word:]', '[:ascii:]', '[:print:]', '[:xdigit:]', '[:graph:]'], true)) { - return 'a'; - } - if ($value === '[:blank:]') { - return " \t"; - } - if ($value === '[:cntrl:]') { - return "\x00\x1F"; - } - if ($value === '[:space:]') { - return " \t\r\n\v\f"; - } - if ($value === '[:punct:]') { - return '!"#$%&\'()*+,\-./:;<=>?@[\]^_`{|}~'; - } + return $type; + } + + private function containsEscapeK(NodeInterface $node): bool + { + if ($node instanceof KeepNode) { + return true; } - if (in_array($token, ['anchor', 'match_point_reset'], true)) { - return ''; + foreach ($node->getChildren() as $child) { + if ($this->containsEscapeK($child)) { + return true; + } } - return null; + return false; } } diff --git a/src/Type/Regex/RegexGroupWalkResult.php b/src/Type/Regex/RegexGroupWalkResult.php deleted file mode 100644 index 9169af89ba5..00000000000 --- a/src/Type/Regex/RegexGroupWalkResult.php +++ /dev/null @@ -1,135 +0,0 @@ -|null $onlyLiterals - */ - public function __construct( - private bool $inOptionalQuantification, - private ?array $onlyLiterals, - private TrinaryLogic $isNonEmpty, - private TrinaryLogic $isNonFalsy, - private TrinaryLogic $isNumeric, - ) - { - } - - public static function createEmpty(): self - { - return new self( - false, - [], - TrinaryLogic::createMaybe(), - TrinaryLogic::createMaybe(), - TrinaryLogic::createMaybe(), - ); - } - - public function inOptionalQuantification(bool $inOptionalQuantification): self - { - return new self( - $inOptionalQuantification, - $this->onlyLiterals, - $this->isNonEmpty, - $this->isNonFalsy, - $this->isNumeric, - ); - } - - /** - * @param array|null $onlyLiterals - */ - public function onlyLiterals(?array $onlyLiterals): self - { - return new self( - $this->inOptionalQuantification, - $onlyLiterals, - $this->isNonEmpty, - $this->isNonFalsy, - $this->isNumeric, - ); - } - - public function nonEmpty(TrinaryLogic $nonEmpty): self - { - return new self( - $this->inOptionalQuantification, - $this->onlyLiterals, - $nonEmpty, - $this->isNonFalsy, - $this->isNumeric, - ); - } - - public function nonFalsy(TrinaryLogic $nonFalsy): self - { - return new self( - $this->inOptionalQuantification, - $this->onlyLiterals, - $this->isNonEmpty, - $nonFalsy, - $this->isNumeric, - ); - } - - public function numeric(TrinaryLogic $numeric): self - { - return new self( - $this->inOptionalQuantification, - $this->onlyLiterals, - $this->isNonEmpty, - $this->isNonFalsy, - $numeric, - ); - } - - public function isInOptionalQuantification(): bool - { - return $this->inOptionalQuantification; - } - - /** - * @return array|null - */ - public function getOnlyLiterals(): ?array - { - return $this->onlyLiterals; - } - - public function mightContainEmptyStringLiteral(): bool - { - if ($this->onlyLiterals === null) { - return false; - } - foreach ($this->onlyLiterals as $onlyLiteral) { - if ($onlyLiteral === '') { - return true; - } - } - - return false; - } - - public function isNonEmpty(): TrinaryLogic - { - return $this->isNonEmpty; - } - - public function isNonFalsy(): TrinaryLogic - { - return $this->isNonFalsy; - } - - public function isNumeric(): TrinaryLogic - { - return $this->isNumeric; - } - -} diff --git a/stubs/core.stub b/stubs/core.stub index 32d59f2b10d..3729f344dae 100644 --- a/stubs/core.stub +++ b/stubs/core.stub @@ -288,7 +288,17 @@ function preg_match($pattern, $subject, &$matches = [], int $flags = 0, int $off /** * @param string|string[] $pattern - * @param callable(string[]):string $callback + * @param ( + * $flags is 256 + * ? callable(array):string + * : ($flags is 512 + * ? callable(array):string + * : ($flags is 768 + * ? callable(array):string + * : callable(string[]):string + * ) + * ) + * ) $callback * @param string|array $subject * @param int $count * @param-out 0|positive-int $count diff --git a/tests/PHPStan/Analyser/data/bug-11292.php b/tests/PHPStan/Analyser/data/bug-11292.php index 7d317e9e6cd..ec8f8fdede2 100644 --- a/tests/PHPStan/Analyser/data/bug-11292.php +++ b/tests/PHPStan/Analyser/data/bug-11292.php @@ -8,6 +8,6 @@ function (string $s): void { $nonUrl = "[^-_#$+.!*%'(),;/?:@~=&a-zA-Z0-9\x7f-\xff]"; $pattern = '{\\b(https?://)?(?:([^]\\\\\\x00-\\x20\\"(),:-<>[\\x7f-\\xff]{1,64})(:[^]\\\\\\x00-\\x20\\"(),:-<>[\\x7f-\\xff]{1,64})?@)?((?:[-a-zA-Z0-9\\x7f-\\xff]{1,63}\\.)+[a-zA-Z\\x7f-\\xff][-a-zA-Z0-9\\x7f-\\xff]{1,62})((:[0-9]{1,5})?(/[!$-/0-9:;=@_~\':;!a-zA-Z\\x7f-\\xff]*?)?(\\?[!$-/0-9:;=@_\':;!a-zA-Z\\x7f-\\xff]+?)?(#[!$-/0-9?:;=@_\':;!a-zA-Z\\x7f-\\xff]+?)?)(?=[)\'?.!,;:]*(' . $nonUrl . '|$))}'; if (preg_match($pattern, $s, $matches, PREG_OFFSET_CAPTURE, 0)) { - assertType('array}>', $matches); + assertType("array{array{non-falsy-string, int<0, max>}, array{''|'http://'|'https://', int<-1, max>}, array{string, int<-1, max>}, array{string, int<-1, max>}, array{non-falsy-string, int<0, max>}, array{string, int<-1, max>}, array{string, int<-1, max>}, array{string, int<-1, max>}, array{string, int<-1, max>}, array{string, int<-1, max>}, array{string, int<-1, max>}}", $matches); } }; diff --git a/tests/PHPStan/Analyser/data/bug-15297-php73.php b/tests/PHPStan/Analyser/data/bug-15297-php73.php index 22a6693f67a..2796193fb60 100644 --- a/tests/PHPStan/Analyser/data/bug-15297-php73.php +++ b/tests/PHPStan/Analyser/data/bug-15297-php73.php @@ -12,16 +12,16 @@ (?:-(?P[a-z]+))? )$/ix"; if (preg_match($pattern, "foobar", $matches, \PREG_UNMATCHED_AS_NULL)) { - assertType("array{0: non-falsy-string, foo1: non-falsy-string, 1: non-falsy-string, bar1?: numeric-string, 2?: numeric-string}|array{0: non-falsy-string, foo1: null, 1: null, bar1: null, 2: null, foo2: non-falsy-string, 3: non-falsy-string, bar2?: non-empty-string, 4?: non-empty-string}", $matches); + assertType('array{0: non-falsy-string, foo1: non-falsy-string, 1: non-falsy-string, bar1?: numeric-string, 2?: numeric-string}|array{0: non-falsy-string, foo1: null, 1: null, bar1: null, 2: null, foo2: non-falsy-string, 3: non-falsy-string, bar2?: non-falsy-string, 4?: non-falsy-string}', $matches); $foo = $matches["foo1"] ?? $matches["foo2"] ?? ""; $bar = $matches["bar1"] ?? $matches["bar2"] ?? ""; } function (string $s): void { if (preg_match('/(a)?(b)?(c)/', $s, $matches, \PREG_UNMATCHED_AS_NULL)) { - assertType("array{non-falsy-string, 'a'|null, 'b'|null, 'c'}", $matches); + assertType("array{'abc'|'ac'|'bc'|'c', 'a'|null, 'b'|null, 'c'}", $matches); } if (preg_match('/(a)(b)?(c)?/', $s, $matches, \PREG_UNMATCHED_AS_NULL)) { - assertType("list{0: non-falsy-string, 1: 'a', 2?: 'b'|null, 3?: 'c'}", $matches); + assertType("list{0: 'a'|'ab'|'abc'|'ac', 1: 'a', 2?: 'b'|null, 3?: 'c'}", $matches); } }; diff --git a/tests/PHPStan/Analyser/data/param-out.php b/tests/PHPStan/Analyser/data/param-out.php index ebd668effd4..e1dee20eb37 100644 --- a/tests/PHPStan/Analyser/data/param-out.php +++ b/tests/PHPStan/Analyser/data/param-out.php @@ -494,7 +494,7 @@ function fooMatch(string $input): void { assertType('list', $matches); preg_match('/@[a-z\d](?:[a-z\d]|-(?=[a-z\d])){0,38}(?!\w)/', $input, $matches, PREG_UNMATCHED_AS_NULL); - assertType("array{}|array{non-falsy-string}", $matches); + assertType("array{}|array{lowercase-string&non-falsy-string}", $matches); } function testMatch() { diff --git a/tests/PHPStan/Analyser/nsrt/bug-11293.php b/tests/PHPStan/Analyser/nsrt/bug-11293.php index 49f8d008a6e..e5ff64e29df 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-11293.php +++ b/tests/PHPStan/Analyser/nsrt/bug-11293.php @@ -9,54 +9,54 @@ class HelloWorld public function sayHello(string $s): void { if (preg_match('/data-(\d{6})\.json$/', $s, $matches) > 0) { - assertType('array{non-falsy-string, non-falsy-string&numeric-string}', $matches); + assertType('array{lowercase-string&non-falsy-string, non-falsy-string&numeric-string}', $matches); } } public function sayHello2(string $s): void { if (preg_match('/data-(\d{6})\.json$/', $s, $matches) === 1) { - assertType('array{non-falsy-string, non-falsy-string&numeric-string}', $matches); + assertType('array{lowercase-string&non-falsy-string, non-falsy-string&numeric-string}', $matches); } } public function sayHello3(string $s): void { if (preg_match('/data-(\d{6})\.json$/', $s, $matches) >= 1) { - assertType('array{non-falsy-string, non-falsy-string&numeric-string}', $matches); + assertType('array{lowercase-string&non-falsy-string, non-falsy-string&numeric-string}', $matches); } } public function sayHello4(string $s): void { if (preg_match('/data-(\d{6})\.json$/', $s, $matches) <= 0) { - assertType('array{}|array{non-falsy-string, non-falsy-string&numeric-string}', $matches); + assertType('array{}|array{lowercase-string&non-falsy-string, non-falsy-string&numeric-string}', $matches); return; } - assertType('array{non-falsy-string, non-falsy-string&numeric-string}', $matches); + assertType('array{lowercase-string&non-falsy-string, non-falsy-string&numeric-string}', $matches); } public function sayHello5(string $s): void { if (preg_match('/data-(\d{6})\.json$/', $s, $matches) < 1) { - assertType('array{}|array{non-falsy-string, non-falsy-string&numeric-string}', $matches); + assertType('array{}|array{lowercase-string&non-falsy-string, non-falsy-string&numeric-string}', $matches); return; } - assertType('array{non-falsy-string, non-falsy-string&numeric-string}', $matches); + assertType('array{lowercase-string&non-falsy-string, non-falsy-string&numeric-string}', $matches); } public function sayHello6(string $s): void { if (1 > preg_match('/data-(\d{6})\.json$/', $s, $matches)) { - assertType('array{}|array{non-falsy-string, non-falsy-string&numeric-string}', $matches); + assertType('array{}|array{lowercase-string&non-falsy-string, non-falsy-string&numeric-string}', $matches); return; } - assertType('array{non-falsy-string, non-falsy-string&numeric-string}', $matches); + assertType('array{lowercase-string&non-falsy-string, non-falsy-string&numeric-string}', $matches); } } diff --git a/tests/PHPStan/Analyser/nsrt/bug-11311.php b/tests/PHPStan/Analyser/nsrt/bug-11311.php index 79fbf3aca44..a4ed71d8de5 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-11311.php +++ b/tests/PHPStan/Analyser/nsrt/bug-11311.php @@ -14,9 +14,9 @@ function doFoo(string $s) { function doUnmatchedAsNull(string $s): void { if (preg_match('/(foo)?(bar)?(baz)?/', $s, $matches, PREG_UNMATCHED_AS_NULL)) { - assertType("array{string, 'foo'|null, 'bar'|null, 'baz'|null}", $matches); + assertType("array{''|'bar'|'barbaz'|'baz'|'foo'|'foobar'|'foobarbaz'|'foobaz', 'foo'|null, 'bar'|null, 'baz'|null}", $matches); } - assertType("array{}|array{string, 'foo'|null, 'bar'|null, 'baz'|null}", $matches); + assertType("array{}|array{''|'bar'|'barbaz'|'baz'|'foo'|'foobar'|'foobarbaz'|'foobaz', 'foo'|null, 'bar'|null, 'baz'|null}", $matches); } // see https://3v4l.org/VeDob @@ -63,7 +63,7 @@ function bug11331c(string $url):void { ([^/]+?) (?:\.git|/)? $}x', $url, $matches, PREG_UNMATCHED_AS_NULL)) { - assertType('array{non-falsy-string, non-empty-string|null, non-empty-string|null, non-empty-string, non-empty-string}', $matches); + assertType('array{non-falsy-string, non-empty-string, null, non-empty-string, non-empty-string}|array{non-falsy-string, null, non-empty-string, non-empty-string, non-empty-string}', $matches); } } @@ -85,56 +85,56 @@ function (string $size): void { if (preg_match('/ab(\d){2,4}xx([0-9])?e?/', $size, $matches, PREG_UNMATCHED_AS_NULL) !== 1) { throw new InvalidArgumentException(sprintf('Invalid size "%s"', $size)); } - assertType('array{non-falsy-string, numeric-string, numeric-string|null}', $matches); + assertType('array{lowercase-string&non-falsy-string, numeric-string, \'0\'|\'1\'|\'2\'|\'3\'|\'4\'|\'5\'|\'6\'|\'7\'|\'8\'|\'9\'|null}', $matches); }; function (string $size): void { if (preg_match('/a(\dAB){2}b(\d){2,4}([1-5])([1-5a-z])e?/', $size, $matches, PREG_UNMATCHED_AS_NULL) !== 1) { throw new InvalidArgumentException(sprintf('Invalid size "%s"', $size)); } - assertType('array{non-falsy-string, non-falsy-string, numeric-string, numeric-string, non-empty-string}', $matches); + assertType('array{non-falsy-string, non-falsy-string&uppercase-string, numeric-string, \'1\'|\'2\'|\'3\'|\'4\'|\'5\', \'1\'|\'2\'|\'3\'|\'4\'|\'5\'|\'a\'|\'b\'|\'c\'|\'d\'|\'e\'|\'f\'|\'g\'|\'h\'|\'i\'|\'j\'|\'k\'|\'l\'|\'m\'|\'n\'|\'o\'|\'p\'|\'q\'|\'r\'|\'s\'|\'t\'|\'u\'|\'v\'|\'w\'|\'x\'|\'y\'|\'z\'}', $matches); }; function (string $size): void { if (preg_match('/ab(ab(\d)){2,4}xx([0-9][a-c])?e?/', $size, $matches, PREG_UNMATCHED_AS_NULL) !== 1) { throw new InvalidArgumentException(sprintf('Invalid size "%s"', $size)); } - assertType('array{non-falsy-string, non-falsy-string, numeric-string, non-falsy-string|null}', $matches); + assertType('array{lowercase-string&non-falsy-string, lowercase-string&non-falsy-string, numeric-string, \'0a\'|\'0b\'|\'0c\'|\'1a\'|\'1b\'|\'1c\'|\'2a\'|\'2b\'|\'2c\'|\'3a\'|\'3b\'|\'3c\'|\'4a\'|\'4b\'|\'4c\'|\'5a\'|\'5b\'|\'5c\'|\'6a\'|\'6b\'|\'6c\'|\'7a\'|\'7b\'|\'7c\'|\'8a\'|\'8b\'|\'8c\'|\'9a\'|\'9b\'|\'9c\'|null}', $matches); }; function (string $size): void { if (preg_match('/ab(\d+)e(\d?)/', $size, $matches, PREG_UNMATCHED_AS_NULL) !== 1) { throw new InvalidArgumentException(sprintf('Invalid size "%s"', $size)); } - assertType("array{non-falsy-string, numeric-string, ''|numeric-string}", $matches); + assertType('array{lowercase-string&non-falsy-string, numeric-string, \'\'|numeric-string}', $matches); }; function (string $size): void { if (preg_match('/ab(?P\d+)e?/', $size, $matches, PREG_UNMATCHED_AS_NULL) !== 1) { throw new InvalidArgumentException(sprintf('Invalid size "%s"', $size)); } - assertType('array{0: non-falsy-string, num: numeric-string, 1: numeric-string}', $matches); + assertType('array{0: lowercase-string&non-falsy-string, num: numeric-string, 1: numeric-string}', $matches); }; function (string $size): void { if (preg_match('/ab(\d\d)/', $size, $matches, PREG_UNMATCHED_AS_NULL) !== 1) { throw new InvalidArgumentException(sprintf('Invalid size "%s"', $size)); } - assertType('array{non-falsy-string, non-falsy-string&numeric-string}', $matches); + assertType('array{lowercase-string&non-falsy-string, non-falsy-string&numeric-string}', $matches); }; function (string $size): void { if (preg_match('/ab(\d+\s)e?/', $size, $matches, PREG_UNMATCHED_AS_NULL) !== 1) { throw new InvalidArgumentException(sprintf('Invalid size "%s"', $size)); } - assertType('array{non-falsy-string, non-falsy-string}', $matches); + assertType('array{lowercase-string&non-falsy-string, non-falsy-string}', $matches); }; function (string $size): void { if (preg_match('/ab(\s)e?/', $size, $matches, PREG_UNMATCHED_AS_NULL) !== 1) { throw new InvalidArgumentException(sprintf('Invalid size "%s"', $size)); } - assertType('array{non-falsy-string, non-empty-string}', $matches); + assertType('array{lowercase-string&non-falsy-string, non-falsy-string}', $matches); }; function (string $size): void { @@ -162,18 +162,18 @@ function (string $size): void { if (preg_match('/ab(\d+\d?)e?/', $size, $matches, PREG_UNMATCHED_AS_NULL) !== 1) { throw new InvalidArgumentException(sprintf('Invalid size "%s"', $size)); } - assertType('array{non-falsy-string, numeric-string}', $matches); + assertType('array{lowercase-string&non-falsy-string, numeric-string}', $matches); }; function (string $s): void { if (preg_match('/Price: ([2-5])/i', $s, $matches, PREG_UNMATCHED_AS_NULL)) { - assertType('array{non-falsy-string, numeric-string}', $matches); + assertType('array{non-falsy-string, \'2\'|\'3\'|\'4\'|\'5\'}', $matches); } }; function (string $s): void { if (preg_match('/Price: ([2-5A-Z])/i', $s, $matches, PREG_UNMATCHED_AS_NULL)) { - assertType('array{non-falsy-string, non-empty-string}', $matches); + assertType('array{non-falsy-string, non-falsy-string}', $matches); } }; @@ -191,36 +191,36 @@ function (string $s): void { function (string $s): void { preg_match('/%a(\d*)/', $s, $matches, PREG_UNMATCHED_AS_NULL); - assertType("array{}|array{non-falsy-string, ''|numeric-string}", $matches); + assertType('array{}|array{lowercase-string&non-falsy-string, \'\'|numeric-string}', $matches); }; function (string $s): void { preg_match('/%a(\d*)?/', $s, $matches, PREG_UNMATCHED_AS_NULL); - assertType("array{}|array{non-falsy-string, ''|numeric-string|null}", $matches); + assertType('array{}|array{lowercase-string&non-falsy-string, \'\'|numeric-string|null}', $matches); }; function (string $s): void { if (preg_match('~a|(\d)|(\s)~', $s, $matches, PREG_UNMATCHED_AS_NULL)) { - assertType("array{non-empty-string, numeric-string|null, non-empty-string|null}", $matches); + assertType('array{non-falsy-string|numeric-string, numeric-string|null, non-falsy-string|null}', $matches); } else { assertType("array{}", $matches); } - assertType("array{}|array{non-empty-string, numeric-string|null, non-empty-string|null}", $matches); + assertType('array{}|array{non-falsy-string|numeric-string, numeric-string|null, non-falsy-string|null}', $matches); }; function (string $s): void { if (preg_match('~a|(\d)|(\s)~', $s, $matches, PREG_UNMATCHED_AS_NULL|PREG_OFFSET_CAPTURE) === 1) { - assertType("array{array{non-empty-string|null, int<-1, max>}, array{numeric-string|null, int<-1, max>}, array{non-empty-string|null, int<-1, max>}}", $matches); + assertType('array{array{non-falsy-string|numeric-string, int<0, max>}, array{numeric-string|null, int<-1, max>}, array{non-falsy-string|null, int<-1, max>}}', $matches); } }; function (string $s): void { if (preg_match('~a|((u)x)|((v)y)~', $s, $matches, PREG_UNMATCHED_AS_NULL) === 1) { - assertType("array{non-empty-string, 'ux'|null, 'u'|null, 'vy'|null, 'v'|null}", $matches); + assertType("array{'a'|'ux'|'vy', 'ux'|null, 'u'|null, 'vy'|null, 'v'|null}", $matches); } }; function (string $s): void { preg_match('~a|(\d)|(\s)~', $s, $matches, PREG_UNMATCHED_AS_NULL); - assertType("array{}|array{non-empty-string, numeric-string|null, non-empty-string|null}", $matches); + assertType('array{}|array{non-falsy-string|numeric-string, numeric-string|null, non-falsy-string|null}', $matches); }; diff --git a/tests/PHPStan/Analyser/nsrt/bug-11438.php b/tests/PHPStan/Analyser/nsrt/bug-11438.php new file mode 100644 index 00000000000..c1be97c9141 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-11438.php @@ -0,0 +1,65 @@ += 8.0 + +namespace Bug11438; + +use function PHPStan\Testing\assertType; + +function (string $subject): void { + preg_replace_callback_array( + [ + '~A([a]+)~i' => function ($match) { + assertType('array{non-falsy-string, non-falsy-string}', $match); + return ''; + }, + '~B([b]+)?~i' => function ($match) { + assertType('array{0: non-falsy-string, 1?: non-falsy-string}', $match); + return ''; + }, + '/(foo)?(bar)?(baz)?/' => function ($match) { + assertType("list{0: ''|'bar'|'barbaz'|'baz'|'foo'|'foobar'|'foobarbaz'|'foobaz', 1?: ''|'foo', 2?: ''|'bar', 3?: 'baz'}", $match); + return ''; + }, + ], + $subject, + ); +}; + +function (string $subject): void { + preg_replace_callback_array( + [ + '/(\d+)-(x)?/' => fn ($match) => assertType('array{lowercase-string&non-falsy-string, numeric-string, \'x\'|null}', $match), + ], + $subject, + -1, + $count, + PREG_UNMATCHED_AS_NULL, + ); + + preg_replace_callback_array( + [ + '/(\d+)-(x)?/' => fn ($match) => assertType('array{0: array{lowercase-string&non-falsy-string, int<0, max>}, 1: array{numeric-string, int<0, max>}, 2?: array{\'x\', int<0, max>}}', $match), + ], + $subject, + flags: PREG_OFFSET_CAPTURE, + ); +}; + +function (string $subject, string $pattern, int $flags): void { + preg_replace_callback_array( + [ + '/(a)/' => fn ($match) => assertType('mixed', $match), + $pattern => fn ($match) => assertType('mixed', $match), + ], + $subject, + ); + + preg_replace_callback_array( + [ + '/(a)/' => fn ($match) => assertType('mixed', $match), + ], + $subject, + -1, + $count, + $flags, + ); +}; diff --git a/tests/PHPStan/Analyser/nsrt/bug-11602.php b/tests/PHPStan/Analyser/nsrt/bug-11602.php new file mode 100644 index 00000000000..51235af83d7 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-11602.php @@ -0,0 +1,25 @@ +a)(?:&(?(?&type)))?~', $str, $matches) !== 1) { + return; + } + + assertType("array{0: 'a'|'a&a', type: 'a', 1: 'a', type2?: 'a', 2?: 'a'}", $matches); + } + + public function typeConstTopLevel(string $str): void + { + if (preg_match('~a~', $str, $matches) !== 1) { + return; + } + + assertType("array{'a'}", $matches); + } + + public function typeWithCommentWithSpecialCharacters(string $str): void + { + if (preg_match(<<<'EOD' + ~(?:(?x) + a # https://3v4l.org/igvia !"#$%&'()*+,-./:;<=>?@[\]^_`{|}\~ + )~ + EOD, $str, $matches) !== 1) { + return; + } + + assertType("array{'a'}", $matches); + } + +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-12126.php b/tests/PHPStan/Analyser/nsrt/bug-12126.php index c494d8d60df..65edb804609 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-12126.php +++ b/tests/PHPStan/Analyser/nsrt/bug-12126.php @@ -17,20 +17,17 @@ public function sayHello(): void return; } - assertType('list>', $matches); - // could be assertType("list", $matches); + assertType('non-empty-list', $matches); if (!preg_match_all($regex, $options[$key], $matches, PREG_PATTERN_ORDER)) { return; } - assertType('array>', $matches); - // could be assertType("array{0: list, test: list<'foo'|'test'>, 1: list<'test'|''>, 2: list<''|'foo'>}", $matches); + assertType('array{0: non-empty-list, test: non-empty-list<\'\'|\'foo\'|\'test\'>, 1: non-empty-list<\'\'|\'test\'>, 2: non-empty-list<\'\'|\'foo\'>}', $matches); if (!preg_match($regex, $options[$key], $matches)) { return; } - assertType('array', $matches); - // could be assertType("array{0: list, test: 'foo', 1: '', 2: 'foo'}|array{0: list, test: 'test', 1: 'test', 2: ''}", $matches); - } + assertType('array{0: \'footest\'|\'testfoo\', test: \'foo\', 1: \'\', 2: \'foo\'}|array{0: \'footest\'|\'testfoo\', test: \'test\', 1: \'test\'}', $matches); + } } diff --git a/tests/PHPStan/Analyser/nsrt/bug-12210.php b/tests/PHPStan/Analyser/nsrt/bug-12210.php index 460ef458062..b30b50be1b8 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-12210.php +++ b/tests/PHPStan/Analyser/nsrt/bug-12210.php @@ -8,7 +8,7 @@ function bug12210a(string $text): void { assert(preg_match('(((sum|min|max)))', $text, $match) === 1); - assertType("array{non-falsy-string, 'max'|'min'|'sum', 'max'|'min'|'sum'}", $match); + assertType("array{'max'|'min'|'sum', 'max'|'min'|'sum', 'max'|'min'|'sum'}", $match); } function bug12210b(string $text): void { diff --git a/tests/PHPStan/Analyser/nsrt/bug-12242.php b/tests/PHPStan/Analyser/nsrt/bug-12242.php index d9335610d38..e11035865ad 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-12242.php +++ b/tests/PHPStan/Analyser/nsrt/bug-12242.php @@ -27,7 +27,9 @@ function bar(string $str): void (\w*) # extra description (UNSIGNED, CHARACTER SET, ...) [3] $/x'; if (preg_match($regexp, $str, $matches)) { - assertType('array{non-falsy-string, non-empty-string, string, string}', $matches); + // under /x the whitespace is ignored, so "0" matches: non-falsy was an + // artifact of the whitespace the old grammar read as literal characters + assertType('array{non-empty-string, non-empty-string, string, string}', $matches); } } diff --git a/tests/PHPStan/Analyser/nsrt/bug-12997.php b/tests/PHPStan/Analyser/nsrt/bug-12997.php new file mode 100644 index 00000000000..574f656af58 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-12997.php @@ -0,0 +1,48 @@ + $array + */ + public function sayHello($nonES, $nonFalsy, $replacement, array $array): void + { + $stripped = preg_replace('{.*]*>(?:\s*\s*)?<\/?script[^>]*>.*}is', '$1', $nonES); + assertType('string|null', $stripped); + + assertType('non-empty-string|null', preg_replace('/a/', 'b', $nonES)); + + // the group can be unset or missing + assertType('string|null', preg_replace('/a(b)?/', '$1', $nonES)); + assertType('string|null', preg_replace('/a(b)?/', '${1}', $nonES)); + assertType('string|null', preg_replace('/a(b)?/', '\1', $nonES)); + assertType('string|null', preg_replace('/a/', '$2', $nonES)); + assertType('array', preg_replace('/a(b)?/', '$1', $array)); + + // the group always matches some text + assertType('non-empty-string|null', preg_replace('/(a+)/', '$1', $nonES)); + assertType('non-empty-string|null', preg_replace('/a/', '$0', $nonES)); + + // the text around the reference + assertType('non-empty-string|null', preg_replace('/a(b)?/', '<$1>', $nonES)); + assertType('non-falsy-string|null', preg_replace('/a(b)?/', '<$1>', $nonFalsy)); + assertType('non-empty-string|null', preg_replace('/a(b)?/', '0$1', $nonFalsy)); + + // escaped and incomplete references are text + assertType('non-empty-string|null', preg_replace('/a/', '\$1', $nonES)); + assertType('non-falsy-string|null', preg_replace('/a/', '\\\\$1', $nonFalsy)); + assertType('non-empty-string|null', preg_replace('/a/', '${1', $nonES)); + + // a replacement not known can hold a reference + assertType('string|null', preg_replace('/a(b)?/', $replacement, $nonES)); + } + +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-14314.php b/tests/PHPStan/Analyser/nsrt/bug-14314.php index ea451aa59e8..28725f8064e 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-14314.php +++ b/tests/PHPStan/Analyser/nsrt/bug-14314.php @@ -8,9 +8,9 @@ function () { preg_match('/^(.)$/', '', $matches) || preg_match('/^(.)(.)(.)$/', '', $matches); - assertType('array{}|array{non-falsy-string, non-empty-string, non-empty-string, non-empty-string}|array{non-falsy-string, non-empty-string}', $matches); + assertType('array{}|array{non-empty-string, non-empty-string}|array{non-falsy-string, non-empty-string, non-empty-string, non-empty-string}', $matches); if (count($matches) === 2) { - assertType('array{non-falsy-string, non-empty-string}', $matches); + assertType('array{non-empty-string, non-empty-string}', $matches); return; } assertType('array{}|array{non-falsy-string, non-empty-string, non-empty-string, non-empty-string}', $matches); @@ -86,9 +86,9 @@ public function testIntRangeWithUnionAndEmpty(array $arr, int $twoToFour): void // Test: sequential count checks preserve narrowing correctly function () { preg_match('/^(.)$/', '', $m) || preg_match('/^(.)(.)(.)$/', '', $m) || preg_match('/^(.)(.)(.)(.)(.)(.)$/', '', $m); - assertType('array{}|array{non-falsy-string, non-empty-string, non-empty-string, non-empty-string, non-empty-string, non-empty-string, non-empty-string}|array{non-falsy-string, non-empty-string, non-empty-string, non-empty-string}|array{non-falsy-string, non-empty-string}', $m); + assertType('array{}|array{non-empty-string, non-empty-string}|array{non-falsy-string, non-empty-string, non-empty-string, non-empty-string, non-empty-string, non-empty-string, non-empty-string}|array{non-falsy-string, non-empty-string, non-empty-string, non-empty-string}', $m); if (count($m) === 2) { - assertType('array{non-falsy-string, non-empty-string}', $m); + assertType('array{non-empty-string, non-empty-string}', $m); return; } assertType('array{}|array{non-falsy-string, non-empty-string, non-empty-string, non-empty-string, non-empty-string, non-empty-string, non-empty-string}|array{non-falsy-string, non-empty-string, non-empty-string, non-empty-string}', $m); @@ -108,7 +108,7 @@ function (int $x) { if ($x > 0) { assertType('int<1, max>', $x); if (count($matches) === 2) { - assertType('array{non-falsy-string, non-empty-string}', $matches); + assertType('array{non-empty-string, non-empty-string}', $matches); assertType('int<1, max>', $x); return; } diff --git a/tests/PHPStan/Analyser/nsrt/bug-14575.php b/tests/PHPStan/Analyser/nsrt/bug-14575.php index 6564cac8b15..efc79e3a5fa 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-14575.php +++ b/tests/PHPStan/Analyser/nsrt/bug-14575.php @@ -7,81 +7,81 @@ function doFoo(string $string): void { // Anchors in alternation should not make the match non-empty if (preg_match('(foo|$)', $string, $match)) { - assertType('array{string}', $match); + assertType('array{\'\'|\'foo\'}', $match); } if (preg_match('(^|foo)', $string, $match)) { - assertType('array{string}', $match); + assertType('array{\'\'}', $match); } if (preg_match('(\b|foo)', $string, $match)) { - assertType('array{string}', $match); + assertType('array{\'\'}', $match); } if (preg_match('/foo|$/', $string, $match)) { - assertType('array{string}', $match); + assertType('array{\'\'|\'foo\'}', $match); } if (preg_match('/^|bar/', $string, $match)) { - assertType('array{string}', $match); + assertType('array{\'\'}', $match); } // Anchor in alternation within capturing group if (preg_match('/(foo|$)/', $string, $match)) { - assertType('array{string, string}', $match); + assertType('array{\'\'|\'foo\', \'\'|\'foo\'}', $match); } if (preg_match('/(^|bar)/', $string, $match)) { - assertType('array{string, string}', $match); + assertType('array{\'\', \'\'|\'bar\'}', $match); } // Anchor in alternation does not affect parent concatenation if (preg_match('/^abc(def|$)/', $string, $match)) { - assertType("array{non-falsy-string, string}", $match); + assertType("array{'abc'|'abcdef', ''|'def'}", $match); } // All non-empty alternatives should still produce non-empty/non-falsy if (preg_match('/foo|bar/', $string, $match)) { - assertType('array{non-falsy-string}', $match); + assertType('array{\'bar\'|\'foo\'}', $match); } if (preg_match('/foo/', $string, $match)) { - assertType('array{non-falsy-string}', $match); + assertType('array{\'foo\'}', $match); } // Anchor alone if (preg_match('/^$/', $string, $match)) { - assertType('array{string}', $match); + assertType('array{\'\'}', $match); } // Three-way alternation with anchor if (preg_match('/foo|bar|$/', $string, $match)) { - assertType('array{string}', $match); + assertType('array{\'\'|\'bar\'|\'foo\'}', $match); } // All alternatives are single chars (non-falsy not determinable from single tokens) if (preg_match('/a|b/', $string, $match)) { - assertType('array{non-empty-string}', $match); + assertType('array{\'a\'|\'b\'}', $match); } // Empty alternation branches if (preg_match('/(|)/', $string, $match)) { - assertType("array{string, ''}", $match); + assertType("array{'', ''}", $match); } if (preg_match('/(foo|)/', $string, $match)) { - assertType("array{string, ''|'foo'}", $match); + assertType("array{''|'foo', ''|'foo'}", $match); } if (preg_match('/(|bar)/', $string, $match)) { - assertType("array{string, ''|'bar'}", $match); + assertType("array{''|'bar', ''|'bar'}", $match); } if (preg_match('/|/', $string, $match)) { - assertType('array{string}', $match); + assertType('array{\'\'}', $match); } if (preg_match('/foo|/', $string, $match)) { - assertType('array{string}', $match); + assertType('array{\'\'|\'foo\'}', $match); } } diff --git a/tests/PHPStan/Analyser/nsrt/bug-14710.php b/tests/PHPStan/Analyser/nsrt/bug-14710.php index 711580f5365..38eb4214ffc 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-14710.php +++ b/tests/PHPStan/Analyser/nsrt/bug-14710.php @@ -6,13 +6,13 @@ function ternaryPregMatch(string $x): void { (preg_match('/^(a|b|c)$/', $x)) ? - assertType('non-falsy-string', $x) + assertType('\'a\'|"a\\n"|\'b\'|"b\\n"|\'c\'|"c\\n"', $x) : assertType('string', $x); } function ifPregMatch(string $x): void { if (preg_match('/^(a|b|c)$/', $x)) { - assertType('non-falsy-string', $x); + assertType('\'a\'|"a\\n"|\'b\'|"b\\n"|\'c\'|"c\\n"', $x); } else { assertType('string', $x); } @@ -20,13 +20,13 @@ function ifPregMatch(string $x): void { function ternaryPregMatchWithMatches(string $x): void { (preg_match('/^(a|b|c)$/', $x, $matches)) ? - assertType('non-falsy-string', $x) + assertType('\'a\'|"a\\n"|\'b\'|"b\\n"|\'c\'|"c\\n"', $x) : assertType('string', $x); } function ifPregMatchWithMatches(string $x): void { if (preg_match('/^(a|b|c)$/', $x, $matches)) { - assertType('non-falsy-string', $x); + assertType('\'a\'|"a\\n"|\'b\'|"b\\n"|\'c\'|"c\\n"', $x); } else { assertType('string', $x); } @@ -40,13 +40,13 @@ function pregMatchNonEmpty(string $x): void { function pregMatchNoNarrow(string $x): void { if (preg_match('/^$/', $x)) { - assertType('string', $x); + assertType('\'\'|"\\n"', $x); } } function pregMatchAll(string $x): void { if (preg_match_all('/^(a|b|c)$/', $x)) { - assertType('non-falsy-string', $x); + assertType('\'a\'|"a\\n"|\'b\'|"b\\n"|\'c\'|"c\\n"', $x); } else { assertType('string', $x); } @@ -56,13 +56,13 @@ function negatedPregMatch(string $x): void { if (!preg_match('/^(a|b|c)$/', $x)) { assertType('string', $x); } else { - assertType('non-falsy-string', $x); + assertType('\'a\'|"a\\n"|\'b\'|"b\\n"|\'c\'|"c\\n"', $x); } } function pregMatchCompare(string $x): void { if (preg_match('/^(a|b|c)$/', $x) === 1) { - assertType('non-falsy-string', $x); + assertType('\'a\'|"a\\n"|\'b\'|"b\\n"|\'c\'|"c\\n"', $x); } } @@ -89,22 +89,23 @@ function pregMatchWithNonConstantPattern(string $pattern, string $x): void { function pregMatchSubjectSharesVarWithMatches(): void { $matches = ['', '', 'foo']; if (preg_match('/^(a|b|c)$/', $matches[2], $matches)) { - assertType("array{non-falsy-string, 'a'|'b'|'c'}", $matches); + assertType("array{'a'|'b'|'c', 'a'|'b'|'c'}", $matches); } } function pregMatchNullableSubject(?string $x): void { // a null subject is coerced to '' which cannot match a non-empty pattern, so null is removed if (preg_match('/^(a|b|c)$/', $x)) { - assertType('string|null', $x); // could be non-falsy-string + assertType('\'a\'|"a\\n"|\'b\'|"b\\n"|\'c\'|"c\\n"', $x); } else { assertType('string|null', $x); } } function pregMatchIntStringSubject(int|string $x): void { - // an int subject can be coerced to a matching string, so narrowing it away would be unsound + // the int the engine reads as digits never matches, but the variable + // stays an int where the string arm matched: only the string narrows if (preg_match('/^(a|b|c)$/', $x)) { - assertType('int|string', $x); + assertType('\'a\'|"a\\n"|\'b\'|"b\\n"|\'c\'|"c\\n"|int', $x); } } diff --git a/tests/PHPStan/Analyser/nsrt/bug-14784.php b/tests/PHPStan/Analyser/nsrt/bug-14784.php index db3529c6457..b9be4c96642 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-14784.php +++ b/tests/PHPStan/Analyser/nsrt/bug-14784.php @@ -16,7 +16,7 @@ function doFoo(string $str): void } if (preg_match('/^(?:0|123)$/', $str)) { - assertType('non-empty-string', $str); + assertType('\'0\'|"0\\n"|\'123\'|"123\\n"', $str); } if (preg_match('/^(?:0|[1-9][0-9]*)$/', $str)) { @@ -28,10 +28,10 @@ function doFoo(string $str): void } if (preg_match('/^(?:0|abc)$/', $str)) { - assertType('non-empty-string', $str); + assertType('\'0\'|"0\\n"|\'abc\'|"abc\\n"', $str); } if (preg_match('/^(?:2|abc)$/', $str)) { - assertType('non-falsy-string', $str); + assertType('\'2\'|"2\\n"|\'abc\'|"abc\\n"', $str); } if (preg_match('/^(0|[1-9][0-9]*)$/', $str, $matches)) { diff --git a/tests/PHPStan/Analyser/nsrt/bug-14809.php b/tests/PHPStan/Analyser/nsrt/bug-14809.php index b809a18dc59..6b96d6ceba4 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-14809.php +++ b/tests/PHPStan/Analyser/nsrt/bug-14809.php @@ -22,18 +22,18 @@ function doFoo(string $s): void // guard: an alternation whose branches are all non-empty stays non-falsy if (preg_match('~(a(b|c))~', $s, $m)) { - assertType("array{non-falsy-string, 'ab'|'ac', 'b'|'c'}", $m); + assertType("array{'ab'|'ac', 'ab'|'ac', 'b'|'c'}", $m); } if (preg_match('~((a|b)(b|c))~', $s, $m)) { - assertType("array{non-falsy-string, 'ab'|'ac'|'bb'|'bc', 'a'|'b', 'b'|'c'}", $m); + assertType("array{'ab'|'ac'|'bb'|'bc', 'ab'|'ac'|'bb'|'bc', 'a'|'b', 'b'|'c'}", $m); } if (preg_match('~((b|c)d)~', $s, $m)) { - assertType("array{non-falsy-string, 'bd'|'cd', 'b'|'c'}", $m); + assertType("array{'bd'|'cd', 'bd'|'cd', 'b'|'c'}", $m); } if (preg_match('~(a(b|c)d)~', $s, $m)) { - assertType("array{non-falsy-string, 'abd'|'acd', 'b'|'c'}", $m); + assertType("array{'abd'|'acd', 'abd'|'acd', 'b'|'c'}", $m); } } diff --git a/tests/PHPStan/Analyser/nsrt/bug-14820.php b/tests/PHPStan/Analyser/nsrt/bug-14820.php index 68436e9d868..48f3070b447 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-14820.php +++ b/tests/PHPStan/Analyser/nsrt/bug-14820.php @@ -9,50 +9,50 @@ function doFoo(string $s): void // a single optional literal yields the literal or the empty string, // combined with the surrounding literals if (preg_match('~(ab?)~', $s, $m)) { - assertType("array{non-falsy-string, 'a'|'ab'}", $m); + assertType("array{'a'|'ab', 'a'|'ab'}", $m); } if (preg_match('~(ab?c)~', $s, $m)) { - assertType("array{non-falsy-string, 'abc'|'ac'}", $m); + assertType("array{'abc'|'ac', 'abc'|'ac'}", $m); } // optional in front if (preg_match('~(a?bc)~', $s, $m)) { - assertType("array{non-falsy-string, 'abc'|'bc'}", $m); + assertType("array{'abc'|'bc', 'abc'|'bc'}", $m); } // two optionals combine into the full cross-product if (preg_match('~(a?b?)~', $s, $m)) { - assertType("array{string, ''|'a'|'ab'|'b'}", $m); + assertType("array{''|'a'|'ab'|'b', ''|'a'|'ab'|'b'}", $m); } // optional over a (sub) group of literals if (preg_match('~(a(bc)?d)~', $s, $m)) { - assertType("array{0: non-falsy-string, 1: 'abcd'|'ad', 2?: 'bc'}", $m); + assertType("array{0: 'abcd'|'ad', 1: 'abcd'|'ad', 2?: 'bc'}", $m); } // optional over an alternation if (preg_match('~(a(b|c)?d)~', $s, $m)) { - assertType("array{0: non-falsy-string, 1: 'abd'|'acd'|'ad', 2?: 'b'|'c'}", $m); + assertType("array{0: 'abd'|'acd'|'ad', 1: 'abd'|'acd'|'ad', 2?: 'b'|'c'}", $m); } // the classic colour/color example if (preg_match('~(colou?r)~', $s, $m)) { - assertType("array{non-falsy-string, 'color'|'colour'}", $m); + assertType("array{'color'|'colour', 'color'|'colour'}", $m); } // exactly-n repetition of a literal if (preg_match('~(ab{2}c)~', $s, $m)) { - assertType("array{non-falsy-string, 'abbc'}", $m); + assertType("array{'abbc', 'abbc'}", $m); } // n-to-m repetition of a literal if (preg_match('~(ab{1,2}c)~', $s, $m)) { - assertType("array{non-falsy-string, 'abbc'|'abc'}", $m); + assertType("array{'abbc'|'abc', 'abbc'|'abc'}", $m); } // unbounded repetition stays non-constant if (preg_match('~(ab*c)~', $s, $m)) { - assertType('array{non-falsy-string, non-falsy-string}', $m); + assertType('array{lowercase-string&non-falsy-string, lowercase-string&non-falsy-string}', $m); } } diff --git a/tests/PHPStan/Analyser/nsrt/bug-14909.php b/tests/PHPStan/Analyser/nsrt/bug-14909.php new file mode 100644 index 00000000000..f22c362e12f --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-14909.php @@ -0,0 +1,18 @@ +[a-z]+))? )$/ix"; if (preg_match($pattern, $s, $matches)) { - assertType("array{0: non-falsy-string, foo1: '', 1: '', bar1: '', 2: '', foo2: non-falsy-string, 3: non-falsy-string, bar2?: non-empty-string, 4?: non-empty-string}|array{0: non-falsy-string, foo1: non-falsy-string, 1: non-falsy-string, bar1?: numeric-string, 2?: numeric-string}", $matches); + assertType("array{0: non-falsy-string, foo1: '', 1: '', bar1: '', 2: '', foo2: non-falsy-string, 3: non-falsy-string, bar2?: non-falsy-string, 4?: non-falsy-string}|array{0: non-falsy-string, foo1: non-falsy-string, 1: non-falsy-string, bar1?: numeric-string, 2?: numeric-string}", $matches); } } function doBar(string $s): void { if (preg_match('/^(?:(?:(a))?(b)|(c))$/', $s, $matches)) { - assertType("array{non-falsy-string, '', '', 'c'}|list{0: non-falsy-string, 1?: ''|'a', 2: 'b'}", $matches); + assertType("array{'ab'|'b'|'c', '', '', 'c'}|list{0: 'ab'|'b'|'c', 1?: ''|'a', 2: 'b'}", $matches); } } diff --git a/tests/PHPStan/Analyser/nsrt/bug-6859.php b/tests/PHPStan/Analyser/nsrt/bug-6859.php index 56cd257c5e8..dc8949633c4 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-6859.php +++ b/tests/PHPStan/Analyser/nsrt/bug-6859.php @@ -18,7 +18,7 @@ public function keys($body) fn ($key) => preg_match("/^somePattern[0-9]+$/", $key) ); - assertType('array, (int|string)>', $someKeys); + assertType('array, (int|non-falsy-string)>', $someKeys); if (count($someKeys) > 0) { return 1; diff --git a/tests/PHPStan/Analyser/nsrt/bug11384.php b/tests/PHPStan/Analyser/nsrt/bug11384.php index 709f2986351..b8dc7a856da 100644 --- a/tests/PHPStan/Analyser/nsrt/bug11384.php +++ b/tests/PHPStan/Analyser/nsrt/bug11384.php @@ -14,7 +14,7 @@ class HelloWorld public function sayHello(string $s): void { if (preg_match('{(' . Bar::VAL . ')}', $s, $m)) { - assertType("array{non-empty-string, '3'}", $m); + assertType("array{'3', '3'}", $m); } } } diff --git a/tests/PHPStan/Analyser/nsrt/by-ref-writeback-native-type.php b/tests/PHPStan/Analyser/nsrt/by-ref-writeback-native-type.php index 6ebab6d0b0d..f67699e18ee 100644 --- a/tests/PHPStan/Analyser/nsrt/by-ref-writeback-native-type.php +++ b/tests/PHPStan/Analyser/nsrt/by-ref-writeback-native-type.php @@ -11,8 +11,8 @@ function offsetCapture(string $s): void return; } - assertType("array{0: array{non-falsy-string, int<-1, max>}, 1: array{'a', int<-1, max>}, 2?: array{'b', int<-1, max>}}", $m); - assertNativeType("array{0: array{non-falsy-string, int<-1, max>}, 1: array{'a', int<-1, max>}, 2?: array{'b', int<-1, max>}}", $m); + assertType("array{0: array{'a'|'ab', int<0, max>}, 1: array{'a', int<0, max>}, 2?: array{'b', int<0, max>}}", $m); + assertNativeType("array{0: array{'a'|'ab', int<0, max>}, 1: array{'a', int<0, max>}, 2?: array{'b', int<0, max>}}", $m); } function noFlags(string $s): void @@ -21,8 +21,8 @@ function noFlags(string $s): void return; } - assertType("array{0: non-falsy-string, 1: 'a', 2?: 'b'}", $m); - assertNativeType("array{0: non-falsy-string, 1: 'a', 2?: 'b'}", $m); + assertType("array{0: 'a'|'ab', 1: 'a', 2?: 'b'}", $m); + assertNativeType("array{0: 'a'|'ab', 1: 'a', 2?: 'b'}", $m); } /** @param-out string $v */ diff --git a/tests/PHPStan/Analyser/nsrt/count-type.php b/tests/PHPStan/Analyser/nsrt/count-type.php index 8ebe0c445eb..ee9334e82b7 100644 --- a/tests/PHPStan/Analyser/nsrt/count-type.php +++ b/tests/PHPStan/Analyser/nsrt/count-type.php @@ -94,7 +94,7 @@ public function constantArrayWhichCanBecomeList(string $h): void return; } - assertType('array{non-falsy-string, non-empty-string}', $matches); + assertType('array{non-falsy-string, lowercase-string&non-empty-string}', $matches); } } diff --git a/tests/PHPStan/Analyser/nsrt/equality-narrowing-new-world.php b/tests/PHPStan/Analyser/nsrt/equality-narrowing-new-world.php index 29914710ad1..61413977db4 100644 --- a/tests/PHPStan/Analyser/nsrt/equality-narrowing-new-world.php +++ b/tests/PHPStan/Analyser/nsrt/equality-narrowing-new-world.php @@ -284,13 +284,13 @@ public function gettypeNarrowing($is, $m): void public function pregMatchNarrowing(string $s): void { if (preg_match('/^a(b)c$/', $s, $matches) === 1) { - assertType("array{non-falsy-string, 'b'}", $matches); + assertType("array{'abc', 'b'}", $matches); } if (1 === preg_match('/^a(b)c$/', $s, $matches2)) { - assertType("array{non-falsy-string, 'b'}", $matches2); + assertType("array{'abc', 'b'}", $matches2); } if (preg_match('/^a(b)c$/', $s, $matches3) === 0) { - assertType("array{}|array{non-falsy-string, 'b'}", $matches3); + assertType("array{}|array{'abc', 'b'}", $matches3); } } diff --git a/tests/PHPStan/Analyser/nsrt/non-empty-string-replace-functions.php b/tests/PHPStan/Analyser/nsrt/non-empty-string-replace-functions.php index a774b4eba44..f13d68e4540 100644 --- a/tests/PHPStan/Analyser/nsrt/non-empty-string-replace-functions.php +++ b/tests/PHPStan/Analyser/nsrt/non-empty-string-replace-functions.php @@ -18,7 +18,7 @@ public function replace(string $search, string $replacement, string $subject){ assertType('non-empty-string', str_replace($search, $replacement, $subject)); assertType('non-empty-string', str_ireplace($search, $replacement, $subject)); - assertType('non-empty-string|null', preg_replace($search, $replacement, $subject)); + assertType('string|null', preg_replace($search, $replacement, $subject)); assertType('non-empty-string', substr_replace($subject, $replacement, 1)); assertType('non-empty-string', substr_replace($subject, $replacement, -1)); diff --git a/tests/PHPStan/Analyser/nsrt/phpregex-participation.php b/tests/PHPStan/Analyser/nsrt/phpregex-participation.php new file mode 100644 index 00000000000..b77fa748bbb --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/phpregex-participation.php @@ -0,0 +1,44 @@ += 8.0 + +namespace PHPRegexParticipation; + +use function PHPStan\Testing\assertType; + +function acceptVerb(string $s): void { + // preg_match('/(a)(*ACCEPT)(b)/', 'a') writes ['a','a'] only: the cut + // leaves group 2 unset + if (preg_match('/(a)(*ACCEPT)(b)/', $s, $m)) { + assertType('array{0: string, 1: string, 2?: string}', $m); + } +} + +function assertionCondition(string $s): void { + // preg_match('/(?(?=(a))b|(c))/', 'c') writes ['c','','c']: the + // condition group sits in the array, empty, when the branch fails + if (preg_match('/(?(?=(a))b|(c))/', $s, $m)) { + // the yes branch sets group 1, the no branch writes it empty and sets group 2 + assertType("array{non-empty-string, '', 'c'}|array{non-empty-string, 'a'}", $m); + } +} + +function assertionConditionAsNull(string $s): void { + if (preg_match('/(?(?=(a))b|(c))/', $s, $m, PREG_UNMATCHED_AS_NULL)) { + assertType("array{non-empty-string, 'a', null}|array{non-empty-string, null, 'c'}", $m); + } +} + +function unmatchedOffsets(string $s): void { + // preg_match('/(a)(b)*/', 'a', PREG_UNMATCHED_AS_NULL | PREG_OFFSET_CAPTURE) + // writes group 2 as [null, -1] + if (preg_match('/(a)(b)*/', $s, $m, PREG_UNMATCHED_AS_NULL | PREG_OFFSET_CAPTURE)) { + assertType('array{array{lowercase-string&non-falsy-string, int<0, max>}, array{\'a\', int<0, max>}, array{\'b\'|null, int<-1, max>}}', $m); + } +} + +function nestedOptional(string $s): void { + // isset($m[1]) proves $m[2]: the engine writes both or neither + if (preg_match('/(a(b))?/', $s, $m) === 1 && isset($m[1])) { + \PHPStan\dumpType($m); + echo $m[2]; + } +} diff --git a/tests/PHPStan/Analyser/nsrt/preg_match_all_shapes.php b/tests/PHPStan/Analyser/nsrt/preg_match_all_shapes.php index 93099323c1b..f74271031cf 100644 --- a/tests/PHPStan/Analyser/nsrt/preg_match_all_shapes.php +++ b/tests/PHPStan/Analyser/nsrt/preg_match_all_shapes.php @@ -112,25 +112,25 @@ function (string $size): void { function (string $size): void { if (preg_match_all('/ab(?P\d+)(?Pab)?/', $size, $matches, PREG_SET_ORDER|PREG_OFFSET_CAPTURE)) { - assertType("non-empty-list}, num: array{numeric-string, int<-1, max>}, 1: array{numeric-string, int<-1, max>}, suffix?: array{'ab', int<-1, max>}, 2?: array{'ab', int<-1, max>}}>", $matches); + assertType("non-empty-list}, num: array{numeric-string, int<0, max>}, 1: array{numeric-string, int<0, max>}, suffix?: array{'ab', int<0, max>}, 2?: array{'ab', int<0, max>}}>", $matches); } }; function (string $size): void { if (preg_match_all('/ab(?P\d+)(?Pab)?/', $size, $matches, PREG_PATTERN_ORDER|PREG_OFFSET_CAPTURE)) { - assertType("array{0: non-empty-list}>, num: non-empty-list}>, 1: non-empty-list}>, suffix: non-empty-list}>, 2: non-empty-list}>}", $matches); + assertType("array{0: non-empty-list}>, num: non-empty-list}>, 1: non-empty-list}>, suffix: non-empty-list}>, 2: non-empty-list}>}", $matches); } }; function (string $size): void { if (preg_match_all('/ab(?P\d+)(?Pab)?/', $size, $matches, PREG_UNMATCHED_AS_NULL|PREG_SET_ORDER|PREG_OFFSET_CAPTURE)) { - assertType("non-empty-list}, num: array{numeric-string|null, int<-1, max>}, 1: array{numeric-string|null, int<-1, max>}, suffix: array{'ab'|null, int<-1, max>}, 2: array{'ab'|null, int<-1, max>}}>", $matches); + assertType("non-empty-list}, num: array{numeric-string, int<0, max>}, 1: array{numeric-string, int<0, max>}, suffix: array{'ab'|null, int<-1, max>}, 2: array{'ab'|null, int<-1, max>}}>", $matches); } }; function (string $size): void { if (preg_match_all('/ab(?P\d+)(?Pab)?/', $size, $matches, PREG_UNMATCHED_AS_NULL|PREG_PATTERN_ORDER|PREG_OFFSET_CAPTURE)) { - assertType("array{0: non-empty-list}>, num: non-empty-list}>, 1: non-empty-list}>, suffix: non-empty-list}>, 2: non-empty-list}>}", $matches); + assertType("array{0: non-empty-list}>, num: non-empty-list}>, 1: non-empty-list}>, suffix: non-empty-list}>, 2: non-empty-list}>}", $matches); } }; @@ -142,7 +142,7 @@ public function sayHello(string $content): void return; } - assertType('array{list}>}', $matches); + assertType('array{list}>}', $matches); } public function sayFoo(string $content): void @@ -165,13 +165,13 @@ public function sayBar(string $content): void function doFoobar(string $s): void { if (preg_match_all('/(foo)?(bar)?(baz)?/', $s, $matches, PREG_OFFSET_CAPTURE)) { - assertType("array{non-empty-list}>, non-empty-list}>, non-empty-list}>, non-empty-list}>}", $matches); + assertType("array{non-empty-list}>, non-empty-list}>, non-empty-list}>, non-empty-list}>}", $matches); } } function doFoobarNull(string $s): void { if (preg_match_all('/(foo)?(bar)?(baz)?/', $s, $matches, PREG_OFFSET_CAPTURE|PREG_UNMATCHED_AS_NULL)) { - assertType("array{non-empty-list}>, non-empty-list}>, non-empty-list}>, non-empty-list}>}", $matches); + assertType("array{non-empty-list}>, non-empty-list}>, non-empty-list}>, non-empty-list}>}", $matches); } } } @@ -230,6 +230,6 @@ function markVerbs(string $s): void { function testBranchReset(string $s): void { // from PCRE2 testdata/testinput2 if (preg_match_all('/(?|(abc)|(xyz))/', $s, $matches)) { - assertType('array>', $matches); + assertType("array{non-empty-list, non-empty-list<'abc'|'xyz'>}", $matches); } } diff --git a/tests/PHPStan/Analyser/nsrt/preg_match_shapes.php b/tests/PHPStan/Analyser/nsrt/preg_match_shapes.php index d8a68124cc7..e789f9347c7 100644 --- a/tests/PHPStan/Analyser/nsrt/preg_match_shapes.php +++ b/tests/PHPStan/Analyser/nsrt/preg_match_shapes.php @@ -19,19 +19,19 @@ function doMatch(string $s): void { assertType("array{}|array{non-falsy-string, '£'|'€'}", $matches); if (preg_match('/Price: (£|€)(\d+)/i', $s, $matches)) { - assertType('array{non-falsy-string, non-empty-string, numeric-string}', $matches); + assertType('array{non-falsy-string, \'£\'|\'€\', numeric-string}', $matches); } - assertType('array{}|array{non-falsy-string, non-empty-string, numeric-string}', $matches); + assertType('array{}|array{non-falsy-string, \'£\'|\'€\', numeric-string}', $matches); if (preg_match(' /Price: (£|€)\d+/ i u', $s, $matches)) { - assertType('array{non-falsy-string, non-empty-string}', $matches); + assertType('array{non-falsy-string, non-falsy-string}', $matches); } - assertType('array{}|array{non-falsy-string, non-empty-string}', $matches); + assertType('array{}|array{non-falsy-string, non-falsy-string}', $matches); if (preg_match('(Price: (£|€))i', $s, $matches)) { - assertType('array{non-falsy-string, non-empty-string}', $matches); + assertType('array{non-falsy-string, \'£\'|\'€\'}', $matches); } - assertType('array{}|array{non-falsy-string, non-empty-string}', $matches); + assertType('array{}|array{non-falsy-string, \'£\'|\'€\'}', $matches); if (preg_match('_foo(.)\_i_i', $s, $matches)) { assertType('array{non-falsy-string, non-empty-string}', $matches); @@ -39,54 +39,54 @@ function doMatch(string $s): void { assertType('array{}|array{non-falsy-string, non-empty-string}', $matches); if (preg_match('/(a)(b)*(c)(d)*/', $s, $matches)) { - assertType("array{0: non-falsy-string, 1: 'a', 2: string, 3: 'c', 4?: non-empty-string}", $matches); + assertType('array{0: lowercase-string&non-falsy-string, 1: \'a\', 2: \'\'|\'b\', 3: \'c\', 4?: \'d\'}', $matches); } - assertType("array{}|array{0: non-falsy-string, 1: 'a', 2: string, 3: 'c', 4?: non-empty-string}", $matches); + assertType('array{}|array{0: lowercase-string&non-falsy-string, 1: \'a\', 2: \'\'|\'b\', 3: \'c\', 4?: \'d\'}', $matches); if (preg_match('/(a)(?b)*(c)(d)*/', $s, $matches)) { - assertType("array{0: non-falsy-string, 1: 'a', name: string, 2: string, 3: 'c', 4?: non-empty-string}", $matches); + assertType('array{0: lowercase-string&non-falsy-string, 1: \'a\', name: \'\'|\'b\', 2: \'\'|\'b\', 3: \'c\', 4?: \'d\'}', $matches); } - assertType("array{}|array{0: non-falsy-string, 1: 'a', name: string, 2: string, 3: 'c', 4?: non-empty-string}", $matches); + assertType('array{}|array{0: lowercase-string&non-falsy-string, 1: \'a\', name: \'\'|\'b\', 2: \'\'|\'b\', 3: \'c\', 4?: \'d\'}', $matches); if (preg_match('/(a)(b)*(c)(?d)*/', $s, $matches)) { - assertType("array{0: non-falsy-string, 1: 'a', 2: string, 3: 'c', name?: non-empty-string, 4?: non-empty-string}", $matches); + assertType('array{0: lowercase-string&non-falsy-string, 1: \'a\', 2: \'\'|\'b\', 3: \'c\', name?: \'d\', 4?: \'d\'}', $matches); } - assertType("array{}|array{0: non-falsy-string, 1: 'a', 2: string, 3: 'c', name?: non-empty-string, 4?: non-empty-string}", $matches); + assertType('array{}|array{0: lowercase-string&non-falsy-string, 1: \'a\', 2: \'\'|\'b\', 3: \'c\', name?: \'d\', 4?: \'d\'}', $matches); if (preg_match('/(a|b)|(?:c)/', $s, $matches)) { - assertType("array{0: non-empty-string, 1?: 'a'|'b'}", $matches); + assertType("array{0: 'a'|'b'|'c', 1?: 'a'|'b'}", $matches); } - assertType("array{}|array{0: non-empty-string, 1?: 'a'|'b'}", $matches); + assertType("array{}|array{0: 'a'|'b'|'c', 1?: 'a'|'b'}", $matches); if (preg_match('/(foo)(bar)(baz)+/', $s, $matches)) { - assertType("array{non-falsy-string, 'foo', 'bar', non-falsy-string}", $matches); + assertType('array{lowercase-string&non-falsy-string, \'foo\', \'bar\', \'baz\'}', $matches); } - assertType("array{}|array{non-falsy-string, 'foo', 'bar', non-falsy-string}", $matches); + assertType('array{}|array{lowercase-string&non-falsy-string, \'foo\', \'bar\', \'baz\'}', $matches); if (preg_match('/(foo)(bar)(baz)*/', $s, $matches)) { - assertType("array{0: non-falsy-string, 1: 'foo', 2: 'bar', 3?: non-falsy-string}", $matches); + assertType('array{0: lowercase-string&non-falsy-string, 1: \'foo\', 2: \'bar\', 3?: \'baz\'}', $matches); } - assertType("array{}|array{0: non-falsy-string, 1: 'foo', 2: 'bar', 3?: non-falsy-string}", $matches); + assertType('array{}|array{0: lowercase-string&non-falsy-string, 1: \'foo\', 2: \'bar\', 3?: \'baz\'}', $matches); if (preg_match('/(foo)(bar)(baz)?/', $s, $matches)) { - assertType("array{0: non-falsy-string, 1: 'foo', 2: 'bar', 3?: 'baz'}", $matches); + assertType("array{0: 'foobar'|'foobarbaz', 1: 'foo', 2: 'bar', 3?: 'baz'}", $matches); } - assertType("array{}|array{0: non-falsy-string, 1: 'foo', 2: 'bar', 3?: 'baz'}", $matches); + assertType("array{}|array{0: 'foobar'|'foobarbaz', 1: 'foo', 2: 'bar', 3?: 'baz'}", $matches); if (preg_match('/(foo)(bar)(baz){0,3}/', $s, $matches)) { - assertType("array{0: non-falsy-string, 1: 'foo', 2: 'bar', 3?: non-falsy-string}", $matches); + assertType("array{0: 'foobar'|'foobarbaz'|'foobarbazbaz'|'foobarbazbazbaz', 1: 'foo', 2: 'bar', 3?: 'baz'}", $matches); } - assertType("array{}|array{0: non-falsy-string, 1: 'foo', 2: 'bar', 3?: non-falsy-string}", $matches); + assertType("array{}|array{0: 'foobar'|'foobarbaz'|'foobarbazbaz'|'foobarbazbazbaz', 1: 'foo', 2: 'bar', 3?: 'baz'}", $matches); if (preg_match('/(foo)(bar)(baz){2,3}/', $s, $matches)) { - assertType("array{non-falsy-string, 'foo', 'bar', non-falsy-string}", $matches); + assertType("array{'foobarbazbaz'|'foobarbazbazbaz', 'foo', 'bar', 'baz'}", $matches); } - assertType("array{}|array{non-falsy-string, 'foo', 'bar', non-falsy-string}", $matches); + assertType("array{}|array{'foobarbazbaz'|'foobarbazbazbaz', 'foo', 'bar', 'baz'}", $matches); if (preg_match('/(foo)(bar)(baz){2}/', $s, $matches)) { - assertType("array{non-falsy-string, 'foo', 'bar', non-falsy-string}", $matches); + assertType("array{'foobarbazbaz', 'foo', 'bar', 'baz'}", $matches); } - assertType("array{}|array{non-falsy-string, 'foo', 'bar', non-falsy-string}", $matches); + assertType("array{}|array{'foobarbazbaz', 'foo', 'bar', 'baz'}", $matches); } function doNonCapturingGroup(string $s): void { @@ -115,9 +115,9 @@ function doNamedSubpattern(string $s): void { function doOffsetCapture(string $s): void { if (preg_match('/(foo)(bar)(baz)/', $s, $matches, PREG_OFFSET_CAPTURE)) { - assertType("array{array{non-falsy-string, int<-1, max>}, array{'foo', int<-1, max>}, array{'bar', int<-1, max>}, array{'baz', int<-1, max>}}", $matches); + assertType("array{array{'foobarbaz', int<0, max>}, array{'foo', int<0, max>}, array{'bar', int<0, max>}, array{'baz', int<0, max>}}", $matches); } - assertType("array{}|array{array{non-falsy-string, int<-1, max>}, array{'foo', int<-1, max>}, array{'bar', int<-1, max>}, array{'baz', int<-1, max>}}", $matches); + assertType("array{}|array{array{'foobarbaz', int<0, max>}, array{'foo', int<0, max>}, array{'bar', int<0, max>}, array{'baz', int<0, max>}}", $matches); } function doUnknownFlags(string $s, int $flags): void { @@ -129,16 +129,16 @@ function doUnknownFlags(string $s, int $flags): void { function doMultipleAlternativeCaptureGroupsWithSameNameWithModifier(string $s): void { if (preg_match('/(?J)(?[a-z]+)|(?[0-9]+)/', $s, $matches)) { - assertType("array{0: non-empty-string, Foo: non-empty-string, 1: non-empty-string}|array{0: non-empty-string, Foo: numeric-string, 1: '', 2: numeric-string}", $matches); + assertType('array{0: (lowercase-string&non-falsy-string)|numeric-string, Foo: lowercase-string&non-falsy-string, 1: lowercase-string&non-falsy-string}|array{0: (lowercase-string&non-falsy-string)|numeric-string, Foo: numeric-string, 1: \'\', 2: numeric-string}', $matches); } - assertType("array{}|array{0: non-empty-string, Foo: non-empty-string, 1: non-empty-string}|array{0: non-empty-string, Foo: numeric-string, 1: '', 2: numeric-string}", $matches); + assertType('array{}|array{0: (lowercase-string&non-falsy-string)|numeric-string, Foo: lowercase-string&non-falsy-string, 1: lowercase-string&non-falsy-string}|array{0: (lowercase-string&non-falsy-string)|numeric-string, Foo: numeric-string, 1: \'\', 2: numeric-string}', $matches); } function doMultipleConsecutiveCaptureGroupsWithSameNameWithModifier(string $s): void { if (preg_match('/(?J)(?[a-z]+)|(?[0-9]+)/', $s, $matches)) { - assertType("array{0: non-empty-string, Foo: non-empty-string, 1: non-empty-string}|array{0: non-empty-string, Foo: numeric-string, 1: '', 2: numeric-string}", $matches); + assertType('array{0: (lowercase-string&non-falsy-string)|numeric-string, Foo: lowercase-string&non-falsy-string, 1: lowercase-string&non-falsy-string}|array{0: (lowercase-string&non-falsy-string)|numeric-string, Foo: numeric-string, 1: \'\', 2: numeric-string}', $matches); } - assertType("array{}|array{0: non-empty-string, Foo: non-empty-string, 1: non-empty-string}|array{0: non-empty-string, Foo: numeric-string, 1: '', 2: numeric-string}", $matches); + assertType('array{}|array{0: (lowercase-string&non-falsy-string)|numeric-string, Foo: lowercase-string&non-falsy-string, 1: lowercase-string&non-falsy-string}|array{0: (lowercase-string&non-falsy-string)|numeric-string, Foo: numeric-string, 1: \'\', 2: numeric-string}', $matches); } // https://github.com/hoaproject/Regex/issues/31 @@ -157,9 +157,9 @@ function hoaBug31(string $s): void { // https://github.com/phpstan/phpstan/issues/10855#issuecomment-2044323638 function testHoaUnsupportedRegexSyntax(string $s): void { if (preg_match('#\QPHPDoc type array of property App\Log::$fillable is not covariant with PHPDoc type array of overridden property Illuminate\Database\E\\\\\QEloquent\Model::$fillable.\E#', $s, $matches)) { - assertType('array{non-falsy-string}', $matches); + assertType('array{\'PHPDoc type array of property App\\\\Log::$fillable is not covariant with PHPDoc type array of overridden property Illuminate\\\\Database\\\\Eloquent\\\\Model::$fillable.\'}', $matches); } - assertType('array{}|array{non-falsy-string}', $matches); + assertType('array{}|array{\'PHPDoc type array of property App\\\\Log::$fillable is not covariant with PHPDoc type array of overridden property Illuminate\\\\Database\\\\Eloquent\\\\Model::$fillable.\'}', $matches); } function testPregMatchSimpleCondition(string $value): void { @@ -233,13 +233,13 @@ function testUnionPattern(string $s): void function doFoo(string $row): void { if (preg_match('~^(a(b))$~', $row, $matches) === 1) { - assertType("array{non-falsy-string, 'ab', 'b'}", $matches); + assertType("array{'ab', 'ab', 'b'}", $matches); } if (preg_match('~^(a(b)?)$~', $row, $matches) === 1) { - assertType("array{0: non-falsy-string, 1: 'a'|'ab', 2?: 'b'}", $matches); + assertType("array{0: 'a'|'ab', 1: 'a'|'ab', 2?: 'b'}", $matches); } if (preg_match('~^(a(b)?)?$~', $row, $matches) === 1) { - assertType("list{0: string, 1?: 'a'|'ab', 2?: 'b'}", $matches); + assertType("list{0: ''|'a'|'ab', 1?: 'a'|'ab', 2?: 'b'}", $matches); } } @@ -249,7 +249,7 @@ function doFoo2(string $row): void return; } - assertType("array{0: non-falsy-string, 1: string, branchCode: ''|numeric-string, 2: ''|numeric-string, accountNumber: numeric-string, 3: numeric-string, bankCode: non-falsy-string&numeric-string, 4: non-falsy-string&numeric-string}", $matches); + assertType("array{0: non-falsy-string, 1: '', branchCode: '', 2: '', accountNumber: numeric-string, 3: numeric-string, bankCode: non-falsy-string&numeric-string, 4: non-falsy-string&numeric-string}|array{0: non-falsy-string, 1: non-falsy-string, branchCode: numeric-string, 2: numeric-string, accountNumber: numeric-string, 3: numeric-string, bankCode: non-falsy-string&numeric-string, 4: non-falsy-string&numeric-string}", $matches); } function doFoo3(string $row): void @@ -265,63 +265,63 @@ function (string $size): void { if (preg_match('~^a\.b(c(\d+)(\d+)(\s+))?d~', $size, $matches) !== 1) { throw new InvalidArgumentException(sprintf('Invalid size "%s"', $size)); } - assertType('array{non-falsy-string, non-falsy-string, numeric-string, numeric-string, non-empty-string}|array{non-falsy-string}', $matches); + assertType('array{lowercase-string&non-falsy-string, lowercase-string&non-falsy-string, numeric-string, numeric-string, non-falsy-string}|array{lowercase-string&non-falsy-string}', $matches); }; function (string $size): void { if (preg_match('~^a\.b(c(\d+))?d~', $size, $matches) !== 1) { throw new InvalidArgumentException(sprintf('Invalid size "%s"', $size)); } - assertType('array{non-falsy-string, non-falsy-string, numeric-string}|array{non-falsy-string}', $matches); + assertType('array{lowercase-string&non-falsy-string, lowercase-string&non-falsy-string, numeric-string}|array{lowercase-string&non-falsy-string}', $matches); }; function (string $size): void { if (preg_match('~^a\.b(c(\d+)?)d~', $size, $matches) !== 1) { throw new InvalidArgumentException(sprintf('Invalid size "%s"', $size)); } - assertType('array{0: non-falsy-string, 1: non-falsy-string, 2?: numeric-string}', $matches); + assertType('array{0: lowercase-string&non-falsy-string, 1: lowercase-string&non-falsy-string, 2?: numeric-string}', $matches); }; function (string $size): void { if (preg_match('~^a\.b(c(\d+)?)?d~', $size, $matches) !== 1) { throw new InvalidArgumentException(sprintf('Invalid size "%s"', $size)); } - assertType('list{0: non-falsy-string, 1?: non-falsy-string, 2?: numeric-string}', $matches); + assertType('list{0: lowercase-string&non-falsy-string, 1?: lowercase-string&non-falsy-string, 2?: numeric-string}', $matches); }; function (string $size): void { if (preg_match('~^a\.b(c(\d+))d~', $size, $matches) !== 1) { throw new InvalidArgumentException(sprintf('Invalid size "%s"', $size)); } - assertType('array{non-falsy-string, non-falsy-string, numeric-string}', $matches); + assertType('array{lowercase-string&non-falsy-string, lowercase-string&non-falsy-string, numeric-string}', $matches); }; function (string $size): void { if (preg_match('~^a\.(b)?(c)?d~', $size, $matches) !== 1) { throw new InvalidArgumentException(sprintf('Invalid size "%s"', $size)); } - assertType("list{0: non-falsy-string, 1?: ''|'b', 2?: 'c'}", $matches); + assertType("list{0: 'a.bcd'|'a.bd'|'a.cd'|'a.d', 1?: ''|'b', 2?: 'c'}", $matches); }; function (string $size): void { if (preg_match('~^(?:(\\d+)x(\\d+)|(\\d+)|x(\\d+))$~', $size, $matches) !== 1) { throw new InvalidArgumentException(sprintf('Invalid size "%s"', $size)); } - assertType("array{non-empty-string, '', '', '', numeric-string}|array{non-empty-string, '', '', numeric-string}|array{non-empty-string, numeric-string, numeric-string}", $matches); + assertType('array{lowercase-string&non-empty-string, \'\', \'\', \'\', numeric-string}|array{lowercase-string&non-empty-string, \'\', \'\', numeric-string}|array{lowercase-string&non-empty-string, numeric-string, numeric-string}', $matches); }; function (string $size): void { if (preg_match('~^(?:(\\d+)x(\\d+)|(\\d+)|x(\\d+))?$~', $size, $matches) !== 1) { throw new InvalidArgumentException(sprintf('Invalid size "%s"', $size)); } - assertType("array{string, '', '', '', numeric-string}|array{string, '', '', numeric-string}|array{string, numeric-string, numeric-string}|array{string}", $matches); + assertType('array{lowercase-string, \'\', \'\', \'\', numeric-string}|array{lowercase-string, \'\', \'\', numeric-string}|array{lowercase-string, numeric-string, numeric-string}|array{lowercase-string}', $matches); }; function (string $size): void { if (preg_match('~\{(?:(include)\\s+(?:[$]?\\w+(?a|bc?)/', $s, $matches)) { - assertType("array{0: non-falsy-string, named: 'a'|'b'|'bc', 1: 'a'|'b'|'bc'}", $matches); + assertType("array{0: 'Price: a'|'Price: b'|'Price: bc', named: 'a'|'b'|'bc', 1: 'a'|'b'|'bc'}", $matches); } }; function (string $s): void { if (preg_match('/Price: (a|0c?)/', $s, $matches)) { - assertType("array{non-falsy-string, '0'|'0c'|'a'}", $matches); + assertType("array{'Price: 0'|'Price: 0c'|'Price: a', '0'|'0c'|'a'}", $matches); } }; @@ -642,19 +642,19 @@ function (string $s): void { function (string $s): void { if (preg_match('/Price: (a|0)/', $s, $matches)) { - assertType("array{non-falsy-string, '0'|'a'}", $matches); + assertType("array{'Price: 0'|'Price: a', '0'|'a'}", $matches); } }; function (string $s): void { if (preg_match('/Price: (aa|0)/', $s, $matches)) { - assertType("array{non-falsy-string, '0'|'aa'}", $matches); + assertType("array{'Price: 0'|'Price: aa', '0'|'aa'}", $matches); } }; function (string $s): void { if (preg_match('/( \d+ )/x', $s, $matches)) { - assertType('array{non-empty-string, numeric-string}', $matches); + assertType('array{numeric-string, numeric-string}', $matches); } }; @@ -679,13 +679,13 @@ function (string $s): void { function (string $value): void { if (preg_match('/^(x)*$/', $value, $matches, PREG_OFFSET_CAPTURE)) { - assertType("array{0: array{string, int<-1, max>}, 1?: array{non-empty-string, int<-1, max>}}", $matches); + assertType('array{0: array{lowercase-string, int<0, max>}, 1?: array{\'x\', int<0, max>}}', $matches); } }; function (string $value): void { if (preg_match('/^(?:(x)|(y))*$/', $value, $matches, PREG_OFFSET_CAPTURE)) { - assertType("array{0: array{string, int<-1, max>}, 1?: array{non-empty-string, int<-1, max>}}|array{array{string, int<-1, max>}, array{'', int<-1, max>}, array{non-empty-string, int<-1, max>}}", $matches); + assertType('list{0: array{lowercase-string, int<0, max>}, 1?: array{\'\'|\'x\', int<-1, max>}, 2?: array{\'y\', int<0, max>}}', $matches); } }; @@ -712,25 +712,25 @@ static public function sayHello(string $source): void function (string $s): void { if (preg_match('~a|(\d)|(\s)~', $s, $matches) === 1) { - assertType("array{0: non-empty-string, 1?: numeric-string}|array{non-empty-string, '', non-empty-string}", $matches); + assertType("array{0: non-falsy-string|numeric-string, 1?: numeric-string}|array{non-falsy-string|numeric-string, '', non-falsy-string}", $matches); } }; function (string $s): void { if (preg_match('~a|((u)x)|((v)y)~', $s, $matches) === 1) { - assertType("array{non-empty-string, '', '', 'vy', 'v'}|array{non-empty-string, 'ux', 'u'}|array{non-empty-string}", $matches); + assertType("array{'a'|'ux'|'vy', '', '', 'vy', 'v'}|array{'a'|'ux'|'vy', 'ux', 'u'}|array{'a'|'ux'|'vy'}", $matches); } }; function (string $s): void { if (preg_match('~a|(\d)|(\s)~', $s, $matches, PREG_OFFSET_CAPTURE) === 1) { - assertType("array{0: array{non-empty-string, int<-1, max>}, 1?: array{numeric-string, int<-1, max>}}|array{array{non-empty-string, int<-1, max>}, array{'', int<-1, max>}, array{non-empty-string, int<-1, max>}}", $matches); + assertType("array{0: array{non-falsy-string|numeric-string, int<0, max>}, 1?: array{numeric-string, int<0, max>}}|array{array{non-falsy-string|numeric-string, int<0, max>}, array{'', int<-1, max>}, array{non-falsy-string, int<0, max>}}", $matches); } }; function (string $s): void { preg_match('~a|(\d)|(\s)~', $s, $matches); - assertType("array{}|array{0: non-empty-string, 1?: numeric-string}|array{non-empty-string, '', non-empty-string}", $matches); + assertType("array{}|array{0: non-falsy-string|numeric-string, 1?: numeric-string}|array{non-falsy-string|numeric-string, '', non-falsy-string}", $matches); }; function bug11490 (string $expression): void { @@ -753,7 +753,7 @@ function bug11622 (string $expression): void { $matches = []; if (preg_match('/^abc(def|$)/', $expression, $matches) === 1) { - assertType("array{non-falsy-string, string}", $matches); + assertType("array{'abc'|'abcdef', ''|'def'}", $matches); } } @@ -762,23 +762,23 @@ function bug11604 (string $string): void { return; } - assertType("list{0: non-falsy-string, 1?: ''|'XX', 2?: 'YY'}", $matches); + assertType("array{'XX'|'YYZZ'|'ZZ', 'XX'}|list{0: 'XX'|'YYZZ'|'ZZ', 1?: '', 2?: 'YY'}", $matches); // could be array{string, '', 'YY'}|array{string, 'XX'}|array{string} } function bug11604b (string $string): void { if (preg_match('/(XX)|(YY)?(ZZ)/', $string, $matches)) { - assertType("list{0: non-falsy-string, 1?: ''|'XX', 2?: ''|'YY', 3?: 'ZZ'}", $matches); + assertType("array{'XX'|'YYZZ'|'ZZ', '', ''|'YY', 'ZZ'}|array{'XX'|'YYZZ'|'ZZ', 'XX'}", $matches); } } function testLtrimDelimiter (string $string): void { if (preg_match(' /(x)/', $string, $matches)) { - assertType("array{non-empty-string, 'x'}", $matches); + assertType("array{'x', 'x'}", $matches); } if (preg_match(' /(x)/', $string, $matches)) { - assertType("array{non-empty-string, 'x'}", $matches); + assertType("array{'x', 'x'}", $matches); } } @@ -786,19 +786,19 @@ function testUnescapeBackslash (string $string): void { if (preg_match(<<<'EOD' ~(\[)~ EOD, $string, $matches)) { - assertType("array{non-empty-string, '['}", $matches); + assertType("array{'[', '['}", $matches); } if (preg_match(<<<'EOD' ~(\d)~ EOD, $string, $matches)) { - assertType("array{non-empty-string, numeric-string}", $matches); + assertType("array{numeric-string, numeric-string}", $matches); } if (preg_match(<<<'EOD' ~(\\d)~ EOD, $string, $matches)) { - assertType("array{non-falsy-string, '\\\d'}", $matches); + assertType("array{'\\\\d', '\\\\d'}", $matches); } if (preg_match(<<<'EOD' @@ -810,7 +810,7 @@ function testUnescapeBackslash (string $string): void { if (preg_match(<<<'EOD' ~(\\\\d)~ EOD, $string, $matches)) { - assertType("array{non-falsy-string, '\\\\\\\d'}", $matches); + assertType("array{'\\\\\\\\d', '\\\\\\\\d'}", $matches); } } @@ -818,128 +818,128 @@ function testEscapedDelimiter (string $string): void { if (preg_match(<<<'EOD' /(\/)/ EOD, $string, $matches)) { - assertType("array{non-empty-string, '/'}", $matches); + assertType("array{'/', '/'}", $matches); } if (preg_match(<<<'EOD' ~(\~)~ EOD, $string, $matches)) { - assertType("array{non-empty-string, '~'}", $matches); + assertType("array{'~', '~'}", $matches); } if (preg_match(<<<'EOD' ~(\[2])~ EOD, $string, $matches)) { - assertType("array{non-falsy-string, '[2]'}", $matches); + assertType("array{'[2]', '[2]'}", $matches); } if (preg_match(<<<'EOD' [(\[2\])] EOD, $string, $matches)) { - assertType("array{non-falsy-string, '[2]'}", $matches); + assertType("array{'[2]', '[2]'}", $matches); } if (preg_match(<<<'EOD' ~(\{2})~ EOD, $string, $matches)) { - assertType("array{non-falsy-string, '{2}'}", $matches); + assertType("array{'{2}', '{2}'}", $matches); } if (preg_match(<<<'EOD' {(\{2\})} EOD, $string, $matches)) { - assertType("array{non-falsy-string, '{2}'}", $matches); + assertType("array{'{2}', '{2}'}", $matches); } if (preg_match(<<<'EOD' ~([a\]])~ EOD, $string, $matches)) { - assertType("array{non-empty-string, ']'|'a'}", $matches); + assertType("array{']'|'a', ']'|'a'}", $matches); } if (preg_match(<<<'EOD' ~([a[])~ EOD, $string, $matches)) { - assertType("array{non-empty-string, '['|'a'}", $matches); + assertType("array{'['|'a', '['|'a'}", $matches); } if (preg_match(<<<'EOD' ~([a\]b])~ EOD, $string, $matches)) { - assertType("array{non-empty-string, ']'|'a'|'b'}", $matches); + assertType("array{']'|'a'|'b', ']'|'a'|'b'}", $matches); } if (preg_match(<<<'EOD' ~([a[b])~ EOD, $string, $matches)) { - assertType("array{non-empty-string, '['|'a'|'b'}", $matches); + assertType("array{'['|'a'|'b', '['|'a'|'b'}", $matches); } if (preg_match(<<<'EOD' ~([a\[b])~ EOD, $string, $matches)) { - assertType("array{non-empty-string, '['|'a'|'b'}", $matches); + assertType("array{'['|'a'|'b', '['|'a'|'b'}", $matches); } if (preg_match(<<<'EOD' [([a\[b])] EOD, $string, $matches)) { - assertType("array{non-empty-string, '['|'a'|'b'}", $matches); + assertType("array{'['|'a'|'b', '['|'a'|'b'}", $matches); } if (preg_match(<<<'EOD' {(x\\\{)|(y\\\\\})} EOD, $string, $matches)) { - assertType("array{non-falsy-string, '', 'y\\\\\\\}'}|array{non-falsy-string, 'x\\\{'}", $matches); + assertType("array{'x\\\\{'|'y\\\\\\\\}', '', 'y\\\\\\\\}'}|array{'x\\\\{'|'y\\\\\\\\}', 'x\\\\{'}", $matches); } } function bugUnescapedDashAfterRange (string $string): void { if (preg_match('/([0-1-y])/', $string, $matches)) { - assertType("array{non-empty-string, non-empty-string}", $matches); + assertType("array{'-'|'0'|'1'|'y', '-'|'0'|'1'|'y'}", $matches); } } function bugEmptySubexpression (string $string): void { if (preg_match('//', $string, $matches)) { - assertType("array{string}", $matches); // could be array{''} + assertType("array{''}", $matches); } if (preg_match('/()/', $string, $matches)) { - assertType("array{string, ''}", $matches); // could be array{'', ''} + assertType("array{'', ''}", $matches); } if (preg_match('/|/', $string, $matches)) { - assertType("array{string}", $matches); // could be array{''} + assertType("array{''}", $matches); } if (preg_match('~|(a)~', $string, $matches)) { - assertType("array{0: string, 1?: 'a'}", $matches); + assertType("array{0: ''|'a', 1?: 'a'}", $matches); } if (preg_match('~(a)|~', $string, $matches)) { - assertType("array{0: string, 1?: 'a'}", $matches); + assertType("array{0: ''|'a', 1?: 'a'}", $matches); } if (preg_match('~(a)||(b)~', $string, $matches)) { - assertType("array{0: string, 1?: 'a'}|array{string, '', 'b'}", $matches); + assertType("array{''|'a'|'b', '', 'b'}|array{0: ''|'a'|'b', 1?: 'a'}", $matches); } if (preg_match('~(|(a))~', $string, $matches)) { - assertType("array{0: string, 1: ''|'a', 2?: 'a'}", $matches); + assertType("array{0: ''|'a', 1: ''|'a', 2?: 'a'}", $matches); } if (preg_match('~((a)|)~', $string, $matches)) { - assertType("array{0: string, 1: ''|'a', 2?: 'a'}", $matches); + assertType("array{0: ''|'a', 1: ''|'a', 2?: 'a'}", $matches); } if (preg_match('~((a)||(b))~', $string, $matches)) { - assertType("list{0: string, 1: ''|'a'|'b', 2?: ''|'a', 3?: 'b'}", $matches); + assertType("array{''|'a'|'b', ''|'a'|'b', '', 'b'}|array{0: ''|'a'|'b', 1: ''|'a'|'b', 2?: 'a'}", $matches); } if (preg_match('~((a)|()|(b))~', $string, $matches)) { - assertType("list{0: string, 1: ''|'a'|'b', 2?: ''|'a', 3?: '', 4?: 'b'}", $matches); + assertType("array{''|'a'|'b', ''|'a'|'b', 'a'}|array{0: ''|'a'|'b', 1: ''|'a'|'b', 2: '', 3: '', 4?: 'b'}", $matches); } } @@ -948,51 +948,51 @@ function bug11744(string $string): void if (!preg_match('~^((/[a-z]+)?)~', $string, $matches)) { return; } - assertType('array{0: string, 1: string, 2?: non-falsy-string}', $matches); + assertType('array{0: lowercase-string, 1: lowercase-string, 2?: lowercase-string&non-falsy-string}', $matches); if (!preg_match('~^((/[a-z]+)?.*)~', $string, $matches)) { return; } - assertType('array{0: string, 1: string, 2?: non-falsy-string}', $matches); + assertType('array{0: string, 1: string, 2?: lowercase-string&non-falsy-string}', $matches); if (!preg_match('~^((/[a-z]+)?.+)~', $string, $matches)) { return; } - assertType('array{0: non-empty-string, 1: non-empty-string, 2?: non-falsy-string}', $matches); + assertType('array{0: non-empty-string, 1: non-empty-string, 2?: lowercase-string&non-falsy-string}', $matches); } function bug12749(string $str): void { if (preg_match('/[A-Z]/', $str, $match)) { - assertType('array{non-empty-string}', $match); // could be non-falsy-string + assertType('array{\'A\'|\'B\'|\'C\'|\'D\'|\'E\'|\'F\'|\'G\'|\'H\'|\'I\'|\'J\'|\'K\'|\'L\'|\'M\'|\'N\'|\'O\'|\'P\'|\'Q\'|\'R\'|\'S\'|\'T\'|\'U\'|\'V\'|\'W\'|\'X\'|\'Y\'|\'Z\'}', $match); // the wish granted and exceeded: the constants enumerate } } function bug12749a(string $str): void { if (preg_match('/[A-Z]{2,}/', $str, $match)) { - assertType('array{non-falsy-string}', $match); + assertType('array{non-falsy-string&uppercase-string}', $match); } } function bug12749b(string $str): void { if (preg_match('/[0-9][A-Z]/', $str, $match)) { - assertType('array{non-falsy-string}', $match); + assertType('array{non-falsy-string&uppercase-string}', $match); } } function bug12749c(string $str): void { if (preg_match('/[0-9][A-Z]?/', $str, $match)) { - assertType('array{non-empty-string}', $match); + assertType('array{non-empty-string&uppercase-string}', $match); } } function bug12749d(string $str): void { if (preg_match('/[0-9]?[A-Z]/', $str, $match)) { - assertType('array{non-falsy-string}', $match); + assertType('array{non-falsy-string&uppercase-string}', $match); } } @@ -1000,33 +1000,33 @@ function bug12749e(string $str): void { // no ^ $ delims, therefore can be anything which contains a number if (preg_match('/[0-9]/', $str, $match)) { - assertType('array{non-empty-string}', $match); + assertType('array{\'0\'|\'1\'|\'2\'|\'3\'|\'4\'|\'5\'|\'6\'|\'7\'|\'8\'|\'9\'}', $match); } } function bug12749f(string $str): void { if (preg_match('/^[0-9]$/', $str, $match)) { - assertType('array{non-empty-string}', $match); // could be numeric-string + assertType('array{\'0\'|\'1\'|\'2\'|\'3\'|\'4\'|\'5\'|\'6\'|\'7\'|\'8\'|\'9\'}', $match); // the wish granted and exceeded: the digits enumerate } } function bug12397(string $string): void { $m = preg_match('#\b([A-Z]{2,})-(\d+)#', $string, $match); - assertType("array{}|array{non-falsy-string, non-falsy-string, numeric-string}", $match); + assertType('array{}|array{non-falsy-string&uppercase-string, non-falsy-string&uppercase-string, numeric-string}', $match); } function bug12792(string $string): void { if (preg_match('~a\Kb~', $string, $match) === 1) { - assertType('array{string}', $match); // could be array{'b'} + assertType('array{string}', $match); } if (preg_match('~a\K~', $string, $match) === 1) { - assertType('array{string}', $match); // could be array{''} + assertType('array{string}', $match); } if (preg_match('~a\K.+~', $string, $match) === 1) { - assertType('array{string}', $match); // could be array{non-empty-string} + assertType('array{string}', $match); } if (preg_match('~a\K.*~', $string, $match) === 1) { @@ -1034,7 +1034,7 @@ function bug12792(string $string): void { } if (preg_match('~a\K(.+)~', $string, $match) === 1) { - assertType('array{string, non-empty-string}', $match); // could be array{non-empty-string, non-empty-string} + assertType('array{string, non-empty-string}', $match); } if (preg_match('~a\K(.*)~', $string, $match) === 1) { @@ -1042,7 +1042,7 @@ function bug12792(string $string): void { } if (preg_match('~a\K(.+?)~', $string, $match) === 1) { - assertType('array{string, non-empty-string}', $match); // could be array{non-empty-string, non-empty-string} + assertType('array{string, non-empty-string}', $match); } if (preg_match('~a\K(.*?)~', $string, $match) === 1) { @@ -1050,34 +1050,34 @@ function bug12792(string $string): void { } if (preg_match('~a\K(?=.+)~', $string, $match) === 1) { - assertType('array{string}', $match); // could be array{''} + assertType('array{string}', $match); } if (preg_match('~a\K(?=.*)~', $string, $match) === 1) { - assertType('array{string}', $match); // could be array{''} + assertType('array{string}', $match); } if (preg_match('~a(?:x\Kb|c)~', $string, $match) === 1) { - assertType('array{string}', $match); // could be array{'ac'|'b'} + assertType('array{string}', $match); } if (preg_match('~a(?:c|x\Kb)~', $string, $match) === 1) { - assertType('array{string}', $match); // could be array{'ac'|'b'} + assertType('array{string}', $match); } if (preg_match('~a(y|(?:x\Kb|c))d~', $string, $match) === 1) { - assertType('array{string, non-empty-string}', $match); // could be array{'acd'|'ayd'|'bd', 'c'|'xb'|'y'} + assertType('array{string, \'c\'|\'xb\'|\'y\'}', $match); } if (preg_match('~a((?:c|x\Kb)|y)d~', $string, $match) === 1) { - assertType('array{string, non-empty-string}', $match); // could be array{'acd'|'ayd'|'bd', 'c'|'xb'|'y'} + assertType('array{string, \'c\'|\'xb\'|\'y\'}', $match); } } function testExtendedSyntaxEscapedHash(string $string): void { // \# in extended mode should be treated as literal hash, not comment if (preg_match('/^ ([\#.]) $/x', $string, $matches)) { - assertType("array{non-falsy-string, '#'|'.'}", $matches); + assertType("array{'#'|'.', '#'|'.'}", $matches); } // Real comment vs escaped hash @@ -1085,77 +1085,166 @@ function testExtendedSyntaxEscapedHash(string $string): void { (\d+) # this is a comment ([\#ab]+) # hash in character class (escaped) /x', $string, $matches)) { - assertType('array{non-falsy-string, numeric-string, non-empty-string}', $matches); + assertType('array{lowercase-string&non-falsy-string, numeric-string, lowercase-string&non-falsy-string}', $matches); + } +} + +function nestedCapturingParent(string $s): void { + if (preg_match('/(x(?:(b)))?c/', $s, $matches)) { + assertType("array{'c'|'xbc', 'xb', 'b'}|array{'c'|'xbc'}", $matches); + } +} + +function branchResetInEveryBranch(string $s): void { + if (preg_match('/(?|(a)|(b))(c)?/', $s, $matches)) { + assertType("array{0: 'a'|'ac'|'b'|'bc', 1: 'a'|'b', 2?: 'c'}", $matches); + } + if (preg_match('/(?|(a)(x)|(b)(y))/', $s, $matches)) { + assertType("array{'ax'|'by', 'a'|'b', 'x'|'y'}", $matches); + } +} + +function unmatchedOffsetOfOnlyOptionalGroup(string $s): void { + // preg_match('/(a)?/', '', $m, PREG_OFFSET_CAPTURE | PREG_UNMATCHED_AS_NULL) writes group 1 as [null, -1] + if (preg_match('/(a)?/', $s, $matches, PREG_OFFSET_CAPTURE | PREG_UNMATCHED_AS_NULL)) { + assertType("array{array{''|'a', int<0, max>}, array{'a'|null, int<-1, max>}}", $matches); + } +} + +function unmatchedNameWithOffsets(string $s): void { + // preg_match('/(?x)|(?y)/', 'y', $m, PREG_OFFSET_CAPTURE) writes 'a' as ['', -1] + if (preg_match('/(?x)|(?y)/', $s, $matches, PREG_OFFSET_CAPTURE)) { + assertType("array{0: array{'x'|'y', int<0, max>}, a: array{'', int<-1, max>}, 1: array{'', int<-1, max>}, b: array{'y', int<0, max>}, 2: array{'y', int<0, max>}}|array{0: array{'x'|'y', int<0, max>}, a: array{'x', int<0, max>}, 1: array{'x', int<0, max>}}", $matches); + } +} + +function caretBranchBeforeAFailingTail(string $s): void { + // preg_match('/(^|a)b/', 'ab') sets group 1 to 'a': the caret branch loses when 'b' fails after it + if (preg_match('/(^|a)b/', $s, $matches)) { + assertType("array{'ab'|'b', ''|'a'}", $matches); + } +} + +function caretBranchFromAnOffset(string $s, int $offset): void { + if (preg_match('/^|bar/', $s, $matches)) { + assertType("array{''}", $matches); + } + // preg_match('/^|bar/', 'xbar', $m, 0, 1) sets $m[0] to 'bar': the caret does not match at an offset + if (preg_match('/^|bar/', $s, $matches, 0, 1)) { + assertType("array{''|'bar'}", $matches); + } + if (preg_match('/^|bar/', $s, $matches, 0, $offset)) { + assertType("array{''|'bar'}", $matches); + } +} + +function groupsUnsetAfterAccept(string $s): void { + // preg_match('/(?:a(*ACCEPT))?(b)(c)/', 'a') writes ['a']: both groups after the cut are left out + if (preg_match('/(?:a(*ACCEPT))?(b)(c)/', $s, $matches)) { + assertType('list{0: string, 1?: string, 2?: string}', $matches); + } + if (preg_match('/(?:a(*ACCEPT))?(b)(c)/', $s, $matches, PREG_UNMATCHED_AS_NULL)) { + assertType('array{string, string|null, string|null}', $matches); + } + // preg_match('/(a)|b(*ACCEPT)(c)/', 'b') writes ['b']: the branch is cut before its group + if (preg_match('/(a)|b(*ACCEPT)(c)/', $s, $matches)) { + assertType('list{0: string, 1?: string, 2?: string}', $matches); + } +} + +function repeatedAlternationKeepsTheGroupsOfEarlierRounds(string $s): void { + // preg_match('/(?:(a)|(b))+/', 'ab') writes ['ab', 'a', 'b']: a later round does not unset the groups of an earlier one + if (preg_match('/(?:(a)|(b))+/', $s, $matches)) { + assertType('list{0: lowercase-string&non-falsy-string, 1?: \'\'|\'a\', 2?: \'b\'}', $matches); + } + if (preg_match('/(?:x(a)|y(b)){2}/', $s, $matches)) { + assertType("list{0: 'xaxa'|'xayb'|'ybxa'|'ybyb', 1?: ''|'a', 2?: 'b'}", $matches); + } +} + +function inlineCaselessOptionKeepsItsScope(string $s): void { + // preg_match('/(?i:a)(b)/', 'aB') is 0: the option holds inside its group only + if (preg_match('/(?i:a)(b)/', $s, $matches)) { + assertType("array{non-falsy-string, 'b'}", $matches); + } + // preg_match('/((?i)a)(b)/', 'aB') is 0: a bare option holds to the end of its group + if (preg_match('/((?i)a)(b)/', $s, $matches)) { + assertType("array{non-falsy-string, non-falsy-string, 'b'}", $matches); } } function testAssertionsAreZeroWidth(string $s): void { // from php-src ext/pcre/tests/bug66121.phpt if (preg_match('/(?foo)|(?bar))(?baz)/', $s, $matches)) { - assertType('array', $matches); + assertType("array{0: 'barbaz'|'foobaz', g: 'bar', 1: '', 2: 'bar', h: 'baz', 3: 'baz'}|array{0: 'barbaz'|'foobaz', g: 'foo', 1: 'foo', 2: '', h: 'baz', 3: 'baz'}", $matches); } // from php-src ext/pcre/tests/bug79257.phpt if (preg_match('/(?J)(?:(?foo)|(?bar))/', $s, $matches, PREG_UNMATCHED_AS_NULL)) { - assertType('array', $matches); + assertType("array{0: 'bar'|'foo', g: 'bar'|'foo'|null, 1: 'foo'|null, 2: 'bar'|null}", $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); + assertType("non-empty-list", $matches); } } function testQuantifiedMaybeEmptyAtom(string $s): void { // from PCRE2 testdata/testinput6 if (preg_match('/(abc|)+/', $s, $matches)) { - assertType("array{string, string}", $matches); + assertType('array{lowercase-string, \'\'|\'abc\'}', $matches); } // from PCRE2 testdata/testinput6 if (preg_match('/^(){3,5}/', $s, $matches)) { - assertType("array{string, string}", $matches); + assertType("array{'', ''}", $matches); } // from PCRE2 testdata/testinput1 if (preg_match('/((a|)+)+Z/', $s, $matches)) { - assertType("array{non-falsy-string, string, string}", $matches); + assertType('array{non-falsy-string, lowercase-string, \'\'|\'a\'}', $matches); } } function testBackreferences(string $s): void { // from PCRE2 testdata/testinput1 if (preg_match('/([a-c]*)\1/', $s, $matches)) { - assertType("array{string, string}", $matches); + assertType('array{string, lowercase-string}', $matches); } // from PCRE2 testdata/testinput1 @@ -1172,7 +1261,7 @@ function testBackreferences(string $s): void { function testBranchResetNotInOnlyTopLevelAlternation(string $s): void { // from PCRE2 testdata/testinput2 if (preg_match('/(x)(?|(abc)|(xyz))(x)/', $s, $matches)) { - assertType('array', $matches); + assertType("array{'xabcx'|'xxyzx', 'x', 'abc'|'xyz', 'x'}", $matches); } } @@ -1189,6 +1278,14 @@ function testConditionalSubpatterns(string $s): void { // from PCRE2 testdata/testinput1 if (preg_match('/(?(DEFINE)(?bar))(?bar))(?bar))(x)?y/', $s, $matches)) { + assertType("array{0: 'xy'|'y', foo?: '', 1?: '', 2?: 'x'}", $matches); } } diff --git a/tests/PHPStan/Analyser/nsrt/preg_match_shapes_php80.php b/tests/PHPStan/Analyser/nsrt/preg_match_shapes_php80.php index 4620565210c..235ab6c3d2f 100644 --- a/tests/PHPStan/Analyser/nsrt/preg_match_shapes_php80.php +++ b/tests/PHPStan/Analyser/nsrt/preg_match_shapes_php80.php @@ -7,15 +7,15 @@ function doOffsetCaptureWithUnmatchedNull(string $s): void { // see https://3v4l.org/07rBO#v8.2.9 if (preg_match('/(foo)(bar)(baz)/', $s, $matches, PREG_OFFSET_CAPTURE|PREG_UNMATCHED_AS_NULL)) { - assertType("array{array{non-falsy-string|null, int<-1, max>}, array{'foo'|null, int<-1, max>}, array{'bar'|null, int<-1, max>}, array{'baz'|null, int<-1, max>}}", $matches); + assertType("array{array{'foobarbaz', int<0, max>}, array{'foo', int<0, max>}, array{'bar', int<0, max>}, array{'baz', int<0, max>}}", $matches); } - assertType("array{}|array{array{non-falsy-string|null, int<-1, max>}, array{'foo'|null, int<-1, max>}, array{'bar'|null, int<-1, max>}, array{'baz'|null, int<-1, max>}}", $matches); + assertType("array{}|array{array{'foobarbaz', int<0, max>}, array{'foo', int<0, max>}, array{'bar', int<0, max>}, array{'baz', int<0, max>}}", $matches); } function doNonAutoCapturingModifier(string $s): void { if (preg_match('/(?n)(\d+)/', $s, $matches)) { - // should be assertType('array{string}', $matches); - assertType('array{non-empty-string, numeric-string}', $matches); + // (?n) turns capturing off, so only the full match remains + assertType('array{numeric-string}', $matches); } - assertType('array{}|array{non-empty-string, numeric-string}', $matches); + assertType('array{}|array{numeric-string}', $matches); } diff --git a/tests/PHPStan/Analyser/nsrt/preg_match_shapes_php82.php b/tests/PHPStan/Analyser/nsrt/preg_match_shapes_php82.php index 1b5dc597b7f..6cb56342565 100644 --- a/tests/PHPStan/Analyser/nsrt/preg_match_shapes_php82.php +++ b/tests/PHPStan/Analyser/nsrt/preg_match_shapes_php82.php @@ -8,14 +8,14 @@ // https://php.watch/versions/8.2/preg-n-no-capture-modifier function doNonAutoCapturingFlag(string $s): void { if (preg_match('/(\d+)/n', $s, $matches)) { - assertType('array{non-empty-string}', $matches); + assertType('array{numeric-string}', $matches); } - assertType('array{}|array{non-empty-string}', $matches); + assertType('array{}|array{numeric-string}', $matches); if (preg_match('/(\d+)(?P\d+)/n', $s, $matches)) { - assertType('array{0: non-falsy-string, num: numeric-string, 1: numeric-string}', $matches); + assertType('array{0: non-falsy-string&numeric-string, num: numeric-string, 1: numeric-string}', $matches); } - assertType('array{}|array{0: non-falsy-string, num: numeric-string, 1: numeric-string}', $matches); + assertType('array{}|array{0: non-falsy-string&numeric-string, num: numeric-string, 1: numeric-string}', $matches); if (preg_match('/(\w)-(?P\d+)-(\w)/n', $s, $matches)) { assertType('array{0: non-falsy-string, num: numeric-string, 1: numeric-string}', $matches); @@ -26,21 +26,21 @@ function doNonAutoCapturingFlag(string $s): void { // delimiter variants, see https://www.php.net/manual/en/regexp.reference.delimiters.php function (string $s): void { if (preg_match('{(\d+)(?P\d+)}n', $s, $matches)) { - assertType('array{0: non-falsy-string, num: numeric-string, 1: numeric-string}', $matches); + assertType('array{0: non-falsy-string&numeric-string, num: numeric-string, 1: numeric-string}', $matches); } }; function (string $s): void { if (preg_match('<(\d+)(?P\d+)>n', $s, $matches)) { - assertType('array{0: non-falsy-string, num: numeric-string, 1: numeric-string}', $matches); + assertType('array{0: non-falsy-string&numeric-string, num: numeric-string, 1: numeric-string}', $matches); } }; function (string $s): void { if (preg_match('((\d+)(?P\d+))n', $s, $matches)) { - assertType('array{0: non-falsy-string, num: numeric-string, 1: numeric-string}', $matches); + assertType('array{0: non-falsy-string&numeric-string, num: numeric-string, 1: numeric-string}', $matches); } }; function (string $s): void { if (preg_match('[(\d+)(?P\d+)]n', $s, $matches)) { - assertType('array{0: non-falsy-string, num: numeric-string, 1: numeric-string}', $matches); + assertType('array{0: non-falsy-string&numeric-string, num: numeric-string, 1: numeric-string}', $matches); } }; diff --git a/tests/PHPStan/Analyser/nsrt/preg_replace_callback_shapes.php b/tests/PHPStan/Analyser/nsrt/preg_replace_callback_shapes.php index 9f8c608d471..ccbcf6812eb 100644 --- a/tests/PHPStan/Analyser/nsrt/preg_replace_callback_shapes.php +++ b/tests/PHPStan/Analyser/nsrt/preg_replace_callback_shapes.php @@ -9,7 +9,7 @@ function (string $s): void { preg_replace_callback( '/(foo)?(bar)?(baz)?/', function ($matches) { - assertType("array{string, 'foo'|null, 'bar'|null, 'baz'|null}", $matches); + assertType("array{''|'bar'|'barbaz'|'baz'|'foo'|'foobar'|'foobarbaz'|'foobaz', 'foo'|null, 'bar'|null, 'baz'|null}", $matches); return ''; }, $s, @@ -23,7 +23,7 @@ function (string $s): void { preg_replace_callback( '/(foo)?(bar)?(baz)?/', function ($matches) { - assertType("list{0: array{string, int<-1, max>}, 1?: array{''|'foo', int<-1, max>}, 2?: array{''|'bar', int<-1, max>}, 3?: array{'baz', int<-1, max>}}", $matches); + assertType("list{0: array{''|'bar'|'barbaz'|'baz'|'foo'|'foobar'|'foobarbaz'|'foobaz', int<0, max>}, 1?: array{''|'foo', int<-1, max>}, 2?: array{''|'bar', int<-1, max>}, 3?: array{'baz', int<0, max>}}", $matches); return ''; }, $s, @@ -37,7 +37,7 @@ function (string $s): void { preg_replace_callback( '/(foo)?(bar)?(baz)?/', function ($matches) { - assertType("array{array{string|null, int<-1, max>}, array{'foo'|null, int<-1, max>}, array{'bar'|null, int<-1, max>}, array{'baz'|null, int<-1, max>}}", $matches); + assertType("array{array{''|'bar'|'barbaz'|'baz'|'foo'|'foobar'|'foobarbaz'|'foobaz', int<0, max>}, array{'foo'|null, int<-1, max>}, array{'bar'|null, int<-1, max>}, array{'baz'|null, int<-1, max>}}", $matches); return ''; }, $s, @@ -62,8 +62,8 @@ function bug14904(string $string): void { preg_replace_callback( '/a|(?b)/', function (array $match): string { - assertType("array{0: non-empty-string, b: 'b'|null, 1: 'b'|null}", $match); - assertNativeType("array{0: non-empty-string, b: 'b'|null, 1: 'b'|null}", $match); + assertType("array{0: 'a'|'b', b: 'b'|null, 1: 'b'|null}", $match); + assertNativeType("array{0: 'a'|'b', b: 'b'|null, 1: 'b'|null}", $match); return ''; }, $string, @@ -72,3 +72,15 @@ function (array $match): string { PREG_UNMATCHED_AS_NULL ); } + +function caretBranchInACallback(string $s): void { + // a callback runs for every match of the scan: preg_replace_callback('/^|bar/', ..., 'xbar') sees '' then 'bar' + preg_replace_callback( + '/^|bar/', + function ($matches) { + assertType("array{''|'bar'}", $matches); + return ''; + }, + $s, + ); +} diff --git a/tests/PHPStan/Analyser/nsrt/preg_split.php b/tests/PHPStan/Analyser/nsrt/preg_split.php index 16e917d3e9a..6bca2c6ba47 100644 --- a/tests/PHPStan/Analyser/nsrt/preg_split.php +++ b/tests/PHPStan/Analyser/nsrt/preg_split.php @@ -43,6 +43,11 @@ public function doWithVariables(string $pattern, string $subject, int $offset, i assertType("array{'1', '2', '3'}|array{'1-2-3'}|false", preg_split('/-/', '1-2-3', $this->generate())); assertType("array{'1', '2', '3'}|array{'1-2-3'}|false", preg_split('/-/', '1-2-3', $this->generate(), $this->generate())); + // a limit or a flags value no constant names is no failed call: + // the engine reads the flags it knows and splits all the same + assertType('non-empty-list|false', preg_split('/-/', '1-2-3', $offset)); + assertType('non-empty-list}|string>|false', preg_split('/-/', '1-2-3', $offset, $flags)); + assertType('list}|string>|false', preg_split($pattern, $subject, $offset, $flags)); assertType('list}|string>|false', preg_split("//", $subject, $offset, $flags)); diff --git a/tests/PHPStan/Command/IgnoredRegexValidatorTest.php b/tests/PHPStan/Command/IgnoredRegexValidatorTest.php index 71c58e04a42..27dd3ace360 100644 --- a/tests/PHPStan/Command/IgnoredRegexValidatorTest.php +++ b/tests/PHPStan/Command/IgnoredRegexValidatorTest.php @@ -2,10 +2,14 @@ namespace PHPStan\Command; +use PHPStan\Php\PcreVersion; +use PHPStan\Php\PhpVersion; use PHPStan\PhpDoc\TypeStringResolver; use PHPStan\Testing\PHPStanTestCase; -use PHPStan\Type\Regex\Parser\RegexParser; +use PHPStan\Type\Regex\PHPRegexReader; +use PHPStan\Type\Regex\RegexExpressionHelper; use PHPUnit\Framework\Attributes\DataProvider; +use const PHP_VERSION_ID; class IgnoredRegexValidatorTest extends PHPStanTestCase { @@ -162,7 +166,10 @@ public function testValidate( bool $expectAllErrorsIgnored, ): void { - $validator = new IgnoredRegexValidator(new RegexParser(), self::getContainer()->getByType(TypeStringResolver::class)); + $validator = new IgnoredRegexValidator( + new PHPRegexReader(new PhpVersion(PHP_VERSION_ID), PcreVersion::fromRuntime(), self::getContainer()->getByType(RegexExpressionHelper::class)), + self::getContainer()->getByType(TypeStringResolver::class), + ); $result = $validator->validate($regex); $this->assertSame($expectedTypes, $result->getIgnoredTypes()); diff --git a/tests/PHPStan/Rules/Arrays/NonexistentOffsetInArrayDimFetchRuleTest.php b/tests/PHPStan/Rules/Arrays/NonexistentOffsetInArrayDimFetchRuleTest.php index 0724cb09adf..fac16672030 100644 --- a/tests/PHPStan/Rules/Arrays/NonexistentOffsetInArrayDimFetchRuleTest.php +++ b/tests/PHPStan/Rules/Arrays/NonexistentOffsetInArrayDimFetchRuleTest.php @@ -804,7 +804,7 @@ public function testBug11655(): void { $this->analyse([__DIR__ . '/data/bug-11655.php'], [ [ - "Offset 3 does not exist on array{non-falsy-string, 'x', array{non-falsy-string, 'x'}}.", + "Offset 3 does not exist on array{'ax', 'x', array{'cx', 'x'}}.", 15, ], ]); diff --git a/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php b/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php index 67b5837fe6d..62bb76a087e 100644 --- a/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php +++ b/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php @@ -799,6 +799,36 @@ public function testPregReplaceCallback(): void ]); } + #[RequiresPhp('>= 8.1.0')] + public function testBug10396(): void + { + $this->analyse([__DIR__ . '/data/bug-10396.php'], [ + [ + 'Parameter #2 $callback of function preg_replace_callback expects callable(array}>): string, Closure(array): string given.', + 46, + ], + [ + 'Parameter #2 $callback of function preg_replace_callback expects callable(array): string, Closure(array): string given.', + 47, + 'Type array of parameter #1 $match of passed callable needs to be same or wider than parameter type array of accepting callable.', + ], + [ + 'Parameter #2 $callback of function preg_replace_callback expects callable(array}>): string, Closure(array): string given.', + 48, + "• Offset 0 (string) does not accept type null. +• Type array of parameter #1 \$match of passed callable needs to be same or wider than parameter type array}> of accepting callable.", + ], + [ + 'Parameter #2 $callback of function preg_replace_callback expects callable(array): string, Closure(array): string given.', + 49, + ], + [ + 'Parameter #2 $callback of function preg_replace_callback expects callable(array): string, Closure(array): string given.', + 50, + ], + ]); + } + public function testMbEregReplaceCallback(): void { $this->analyse([__DIR__ . '/data/mb_ereg_replace_callback.php'], [ @@ -3009,11 +3039,11 @@ public function testConstantParameterCheck(): void 110, ], [ - 'Parameter #2 $callback of function preg_replace_callback expects callable(array): string, Closure(mixed): array{non-falsy-string, int<-1, max>} given.', + 'Parameter #2 $callback of function preg_replace_callback expects callable(array}>): string, Closure(mixed): array{\'foo\', int<0, max>} given.', 113, ], [ - 'Parameter #2 $callback of function preg_replace_callback expects callable(array): string, Closure(mixed): array{non-falsy-string|null, int<-1, max>} given.', + 'Parameter #2 $callback of function preg_replace_callback expects callable(array}>): string, Closure(mixed): array{\'foo\', int<0, max>} given.', 116, ], [ diff --git a/tests/PHPStan/Rules/Functions/data/bug-10396.php b/tests/PHPStan/Rules/Functions/data/bug-10396.php new file mode 100644 index 00000000000..d8d5826c118 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/bug-10396.php @@ -0,0 +1,51 @@ += 8.1 declare(strict_types = 1); + +namespace Bug10396; + +function test(): string|null { + $flags = \PREG_OFFSET_CAPTURE | \PREG_UNMATCHED_AS_NULL; + return preg_replace_callback('/bar/', callback(...), 'foobar', -1, $count, $flags); +} + +/** + * @param array $match + * @return string + */ +function callback(array $match): string { + return (string) $match[0][1]; +} + +/** + * @param array $match + */ +function offsetCallback(array $match): string { + return $match[0][0]; +} + +/** + * @param array $match + */ +function nullableCallback(array $match): string { + return (string) $match[0]; +} + +/** + * @param array $match + */ +function stringCallback(array $match): string { + return $match[0]; +} + +function flags(string $s, int $flags): void { + preg_replace_callback('/bar/', offsetCallback(...), $s, -1, $count, PREG_OFFSET_CAPTURE); + preg_replace_callback('/bar/', nullableCallback(...), $s, -1, $count, PREG_UNMATCHED_AS_NULL); + preg_replace_callback('/bar/', nullableCallback(...), $s); + preg_replace_callback('/bar/', stringCallback(...), $s); + preg_replace_callback('/bar/', stringCallback(...), $s, -1, $count, $flags); + + preg_replace_callback('/bar/', stringCallback(...), $s, -1, $count, PREG_OFFSET_CAPTURE); + preg_replace_callback('/bar/', stringCallback(...), $s, -1, $count, PREG_UNMATCHED_AS_NULL); + preg_replace_callback('/bar/', offsetCallback(...), $s, -1, $count, PREG_OFFSET_CAPTURE | PREG_UNMATCHED_AS_NULL); + preg_replace_callback('/bar/', offsetCallback(...), $s); + preg_replace_callback('/bar/', callback(...), $s, flags: PREG_UNMATCHED_AS_NULL); +} diff --git a/tests/PHPStan/Rules/Regexp/RegularExpressionPatternRuleAnalysedPhpTest.php b/tests/PHPStan/Rules/Regexp/RegularExpressionPatternRuleAnalysedPhpTest.php new file mode 100644 index 00000000000..62b38e6691e --- /dev/null +++ b/tests/PHPStan/Rules/Regexp/RegularExpressionPatternRuleAnalysedPhpTest.php @@ -0,0 +1,54 @@ + + */ +class RegularExpressionPatternRuleAnalysedPhpTest extends RuleTestCase +{ + + private bool $checkRegexPatternForAnalysedPhp = true; + + protected function getRule(): Rule + { + return new RegularExpressionPatternRule( + self::getContainer()->getByType(RegexExpressionHelper::class), + self::getContainer()->getByType(PHPRegexReader::class), + $this->checkRegexPatternForAnalysedPhp, + ); + } + + #[RequiresPhp('>= 8.4')] + public function testPatternInvalidForAnalysedPhp(): void + { + // spaces inside \x{} compile on the PCRE2 bundled with PHP 8.4, not on the one PHP 8.2 bundles + $this->analyse([__DIR__ . '/data/regex-pattern-analysed-php.php'], [ + [ + 'Regex pattern is invalid: Spaces inside \x{} need PCRE2 10.43, which PHP bundles from 8.4 at offset 5', + 6, + ], + ]); + } + + #[RequiresPhp('>= 8.4')] + public function testPatternInvalidForAnalysedPhpWithoutToggle(): void + { + $this->checkRegexPatternForAnalysedPhp = false; + $this->analyse([__DIR__ . '/data/regex-pattern-analysed-php.php'], []); + } + + public static function getAdditionalConfigFiles(): array + { + return [ + __DIR__ . '/data/analysed-php-82.neon', + ]; + } + +} diff --git a/tests/PHPStan/Rules/Regexp/RegularExpressionPatternRuleTest.php b/tests/PHPStan/Rules/Regexp/RegularExpressionPatternRuleTest.php index 00f0db3ce34..afa913a5980 100644 --- a/tests/PHPStan/Rules/Regexp/RegularExpressionPatternRuleTest.php +++ b/tests/PHPStan/Rules/Regexp/RegularExpressionPatternRuleTest.php @@ -4,6 +4,7 @@ use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; +use PHPStan\Type\Regex\PHPRegexReader; use PHPStan\Type\Regex\RegexExpressionHelper; use PHPUnit\Framework\Attributes\DataProvider; use function sprintf; @@ -19,6 +20,8 @@ protected function getRule(): Rule { return new RegularExpressionPatternRule( self::getContainer()->getByType(RegexExpressionHelper::class), + self::getContainer()->getByType(PHPRegexReader::class), + true, ); } diff --git a/tests/PHPStan/Rules/Regexp/data/analysed-php-82.neon b/tests/PHPStan/Rules/Regexp/data/analysed-php-82.neon new file mode 100644 index 00000000000..636d772d2c7 --- /dev/null +++ b/tests/PHPStan/Rules/Regexp/data/analysed-php-82.neon @@ -0,0 +1,2 @@ +parameters: + phpVersion: 80200 diff --git a/tests/PHPStan/Rules/Regexp/data/regex-pattern-analysed-php.php b/tests/PHPStan/Rules/Regexp/data/regex-pattern-analysed-php.php new file mode 100644 index 00000000000..c9b9a42c5ea --- /dev/null +++ b/tests/PHPStan/Rules/Regexp/data/regex-pattern-analysed-php.php @@ -0,0 +1,8 @@ +analyse([__DIR__ . '/data/bug-15297.php'], [ [ - 'Offset \'foo2\' on array{0: non-falsy-string, foo1: null, 1: null, bar1: null, 2: null, foo2: non-falsy-string, 3: non-falsy-string, bar2?: non-empty-string, ...} on left side of ?? always exists and is not nullable.', + 'Offset \'foo2\' on array{0: non-falsy-string, foo1: null, 1: null, bar1: null, 2: null, foo2: non-falsy-string, 3: non-falsy-string, bar2?: non-falsy-string, ...} on left side of ?? always exists and is not nullable.', 11, ], ]); diff --git a/tests/PHPStan/Type/Php/RegexArrayShapeMatcherEngineTest.php b/tests/PHPStan/Type/Php/RegexArrayShapeMatcherEngineTest.php new file mode 100644 index 00000000000..d45f17f3aa4 --- /dev/null +++ b/tests/PHPStan/Type/Php/RegexArrayShapeMatcherEngineTest.php @@ -0,0 +1,209 @@ + + */ + public static function dataPatterns(): iterable + { + $parser = (new ParserFactory())->createForHostVersion(); + $finder = new NodeFinder(); + $patterns = []; + foreach (glob(__DIR__ . '/../../Analyser/nsrt/*.php') ?: [] as $file) { + $code = file_get_contents($file); + if ($code === false || preg_match('/\bpreg_(?:match|match_all|replace_callback)\s*\(/', $code) !== 1) { + continue; + } + + foreach ($finder->findInstanceOf($parser->parse($code) ?? [], FuncCall::class) as $call) { + if ( + !$call->name instanceof Name + || !in_array($call->name->toLowerString(), ['preg_match', 'preg_match_all', 'preg_replace_callback'], true) + ) { + continue; + } + + $pattern = $call->getArgs()[0]->value ?? null; + if (!$pattern instanceof String_ || @preg_match($pattern->value, '') === false) { + continue; + } + + $patterns[$pattern->value] = true; + } + } + + foreach (array_keys($patterns) as $pattern) { + yield $pattern => [$pattern]; + } + } + + #[DataProvider('dataPatterns')] + public function testTheEngineMatchesAreAccepted(string $pattern): void + { + $matcher = self::getContainer()->getByType(RegexArrayShapeMatcher::class); + $scope = self::getContainer()->getByType(ScopeFactory::class)->create(ScopeContext::create(__FILE__)); + $patternExpr = new String_($pattern); + + $failures = []; + $jit = ini_get('pcre.jit'); + ini_set('pcre.jit', '0'); + try { + foreach ($this->subjects($pattern) as $subject) { + foreach (self::MATCH_FLAGS as $flags) { + if (preg_match($pattern, $subject, $matches, $flags) === 1) { + $failures[] = $this->refusal($matcher->matchExpr($patternExpr, new ConstantIntegerType($flags), TrinaryLogic::createYes(), $scope), $matches, 'preg_match', $pattern, $subject, $flags); + } + + if (preg_match($pattern, $subject, $matches, $flags, 1) === 1) { + $failures[] = $this->refusal($matcher->matchExpr($patternExpr, new ConstantIntegerType($flags), TrinaryLogic::createYes(), $scope, true), $matches, 'preg_match from offset 1', $pattern, $subject, $flags); + } + + preg_replace_callback($pattern, function (array $matches) use ($matcher, $patternExpr, $flags, $scope, $pattern, $subject, &$failures): string { + $failures[] = $this->refusal($matcher->matchExpr($patternExpr, new ConstantIntegerType($flags), TrinaryLogic::createYes(), $scope, true), $matches, 'preg_replace_callback', $pattern, $subject, $flags); + + return ''; + }, $subject, flags: $flags); + } + + foreach (self::MATCH_ALL_FLAGS as $flags) { + // false on a match error, e.g. a subject that is not UTF-8 under /u + $matchCount = preg_match_all($pattern, $subject, $matches, $flags); + if ($matchCount === false || $matchCount === 0) { + continue; + } + + $failures[] = $this->refusal($matcher->matchAllExpr($patternExpr, new ConstantIntegerType($flags), TrinaryLogic::createYes(), $scope), $matches, 'preg_match_all', $pattern, $subject, $flags); + } + } + } finally { + ini_set('pcre.jit', is_string($jit) ? $jit : '1'); + } + + self::assertSame([], array_values(array_unique(array_filter($failures, static fn (?string $failure): bool => $failure !== null)))); + } + + /** + * @param array $matches + */ + private function refusal(?Type $type, array $matches, string $function, string $pattern, string $subject, int $flags): ?string + { + if ($type === null || $type->isSuperTypeOf(ConstantTypeHelper::getTypeFromValue($matches))->yes()) { + return null; + } + + return sprintf( + '%s(%s) with flags %d on %s wrote %s, the inferred type %s does not accept it', + $function, + $pattern, + $flags, + var_export($subject, true), + var_export($matches, true), + $type->describe(VerbosityLevel::precise()), + ); + } + + /** + * @return list + */ + private function subjects(string $pattern): array + { + $subjects = self::SUBJECTS; + + preg_match_all('/[A-Za-z0-9]+/', $pattern, $literals); + foreach (array_slice(array_values(array_unique($literals[0])), 0, 8) as $literal) { + $subjects[] = $literal; + $subjects[] = $literal . "\n"; + $subjects[] = 'x' . $literal; + $subjects[] = strtoupper($literal); + $subjects[] = strtolower($literal); + $subjects[] = $literal . $literal; + } + + $alphabet = mb_str_split(preg_replace('/[\\\\()\[\]{}?*+|^$\/#~]/', '', $pattern) ?? '') ?: []; + $alphabet = array_values(array_unique([...$alphabet, '0', '1', 'a', ' ', "\n"])); + mt_srand(crc32($pattern)); + for ($i = 0; $i < 30; $i++) { + $subject = ''; + for ($length = mt_rand(0, 8); $length > 0; $length--) { + $subject .= $alphabet[mt_rand(0, count($alphabet) - 1)]; + } + $subjects[] = $subject; + } + + return array_values(array_unique($subjects)); + } + +} diff --git a/tests/PHPStan/Type/Regex/PHPRegexBackendParityTest.php b/tests/PHPStan/Type/Regex/PHPRegexBackendParityTest.php new file mode 100644 index 00000000000..29e4de978e2 --- /dev/null +++ b/tests/PHPStan/Type/Regex/PHPRegexBackendParityTest.php @@ -0,0 +1,182 @@ + + */ + public static function providePatterns(): iterable + { + yield 'plain groups' => ['pattern' => '/(foo)(bar)(baz)+/', 'subject' => 'foobarbazbaz']; + yield 'backreference' => ['pattern' => '/(a|b)\1/', 'subject' => 'aa']; + yield 'mark verb' => ['pattern' => '/(*MARK:first)a/', 'subject' => 'a']; + yield 'quoted run' => ['pattern' => '/(\Qa.c\E)/', 'subject' => 'a.c']; + yield 'extended mode' => ['pattern' => '/(a) # comment/x', 'subject' => 'a']; + yield 'duplicate names' => ['pattern' => '/(?J)(?a)|(?b)/', 'subject' => 'b']; + yield 'version-shaped escape' => ['pattern' => '/(a\x{ 41 })/', 'subject' => 'aA']; + yield 'digit runs' => ['pattern' => '/(?\d{4})-(?\d{2})/', 'subject' => '2026-10']; + yield 'conditional' => ['pattern' => '/(a)(?(1)b|(c))/', 'subject' => 'ab']; + yield 'define' => ['pattern' => '/(?(DEFINE)(?abc))(?x)/', 'subject' => 'x']; + yield 'subroutine call' => ['pattern' => '/(a)(?1)/', 'subject' => 'aa']; + } + + #[Test] + #[DataProvider('providePatterns')] + public function testTheEngineWritesWhatTheShapeReads(string $pattern, string $subject): void + { + // PREG_UNMATCHED_AS_NULL keeps every numbered group in $matches — + // the engine omits the trailing ones it never filled, and the count + // would lie + $matched = preg_match($pattern, $subject, $matches, PREG_UNMATCHED_AS_NULL); + self::assertSame(1, $matched, $pattern . ' must match ' . $subject); + + $helper = (new ReflectionClass(RegexExpressionHelper::class))->newInstanceWithoutConstructor(); + $parser = new RegexGroupParser( + new PhpVersion(PHP_VERSION_ID), + new PHPRegexReader(new PhpVersion(PHP_VERSION_ID), PcreVersion::fromRuntime(), $helper), + ); + $this->resetParsedAstCache(); + $result = $parser->parseGroups($pattern); + self::assertNotNull($result, $pattern . ' must read whole'); + + $engineGroups = 0; + foreach (array_keys($matches) as $key) { + if (!is_int($key) || $key <= $engineGroups) { + continue; + } + + $engineGroups = $key; + } + + $shapeGroups = $result->getCapturingGroups(); + self::assertCount($engineGroups, $shapeGroups, $pattern . ': the engine numbers as many groups as the walk'); + + foreach ($shapeGroups as $group) { + $number = $group->getId(); + $written = $matches[$number] ?? null; + + // a group the engine left unset is optional in the shape; a + // duplicate name writes the value of whichever branch ran + if ($written === null) { + self::assertTrue($group->isOptional(), $pattern . ': group ' . $number . ' never filled must read optional'); + + continue; + } + + if ($group->getName() !== null && $matches[$group->getName()] !== null) { + self::assertSame($written, $matches[$group->getName()], $pattern . ': the name writes the same value'); + } + + $type = $group->getType()->describe(VerbosityLevel::precise()); + if ($type !== 'string' && str_contains($type, "'")) { + self::assertStringContainsString( + "'" . $written . "'", + $type, + $pattern . ': the engine wrote ' . $written . ', the shape reads ' . $type, + ); + } elseif ($type !== 'string') { + self::assertNotSame('', $written, $pattern . ': an inexact shape never claims constants'); + } + } + } + + #[Test] + public function testAConditionalFillsOnlyOneBranch(): void + { + // preg_match('/(a)(?(1)b|(c))/', 'ab', $m) === ['ab', 'a']: the else + // group is numbered, optional, and never set beside a set condition + self::assertSame(1, preg_match('/(a)(?(1)b|(c))/', 'ab', $m)); + self::assertSame(['ab', 'a'], $m); + + $parser = $this->createParser(); + $this->resetParsedAstCache(); + $result = $parser->parseGroups('/(a)(?(1)b|(c))/'); + self::assertNotNull($result); + + $else = $result->getCapturingGroups()[2] ?? null; + self::assertNotNull($else); + self::assertTrue($else->isOptional(), 'the else branch runs alone, or not at all'); + } + + #[Test] + public function testADefinedGroupStaysEmpty(): void + { + // preg_match('/(?(DEFINE)(?abc))(?x)/', 'x', $m): d and 1 stay + // at the empty string, whatever the block spells + self::assertSame(1, preg_match('/(?(DEFINE)(?abc))(?x)/', 'x', $m)); + self::assertSame('{"0":"x","d":"","1":"","r":"x","2":"x"}', json_encode($m), 'the engine leaves the defined group empty'); + + $parser = $this->createParser(); + $this->resetParsedAstCache(); + $result = $parser->parseGroups('/(?(DEFINE)(?abc))(?x)/'); + self::assertNotNull($result); + + $defined = $result->getCapturingGroups()[1] ?? null; + self::assertNotNull($defined); + self::assertSame("''", $defined->getType()->describe(VerbosityLevel::precise())); + } + + #[Test] + public function testDigitGroupsReadTheirLengths(): void + { + self::assertSame(1, preg_match('/(?\d{4})-(?\d{2})/', '2026-10', $m)); + self::assertSame('2026', $m['year']); + self::assertTrue(ctype_digit($m['year'])); + + $parser = $this->createParser(); + $this->resetParsedAstCache(); + $result = $parser->parseGroups('/(?\d{4})-(?\d{2})/'); + self::assertNotNull($result); + + $year = $result->getCapturingGroups()[1] ?? null; + self::assertNotNull($year); + $type = $year->getType(); + self::assertStringContainsString('numeric-string', $type->describe(VerbosityLevel::precise())); + } + + private function createParser(): RegexGroupParser + { + $helper = (new ReflectionClass(RegexExpressionHelper::class))->newInstanceWithoutConstructor(); + + return new RegexGroupParser( + new PhpVersion(PHP_VERSION_ID), + new PHPRegexReader(new PhpVersion(PHP_VERSION_ID), PcreVersion::fromRuntime(), $helper), + ); + } + + private function resetParsedAstCache(): void + { + $property = new ReflectionProperty(RegexGroupParser::class, 'parsedShapes'); + $property->setAccessible(true); + $property->setValue(null, []); + } + +} diff --git a/tests/PHPStan/Type/Regex/RegexShapeReadingTest.php b/tests/PHPStan/Type/Regex/RegexShapeReadingTest.php new file mode 100644 index 00000000000..09e7954b384 --- /dev/null +++ b/tests/PHPStan/Type/Regex/RegexShapeReadingTest.php @@ -0,0 +1,699 @@ +newInstanceWithoutConstructor(); + mb_internal_encoding('ISO-8859-1'); + + try { + $parser = new RegexGroupParser( + new PhpVersion(PHP_VERSION_ID), + new PHPRegexReader(new PhpVersion(PHP_VERSION_ID), PcreVersion::fromRuntime(), $helper), + ); + + $this->resetParsedAstCache(); + $result = $parser->parseGroups('/é/'); + self::assertNotNull($result); + self::assertSame("'é'", $result->getSubjectBaseType()->describe(VerbosityLevel::precise()), 'one character is one token, not two'); + } finally { + mb_internal_encoding($defaultEncoding); + } + } + + private function createParser(): RegexGroupParser + { + $helper = (new ReflectionClass(RegexExpressionHelper::class))->newInstanceWithoutConstructor(); + + return new RegexGroupParser( + new PhpVersion(PHP_VERSION_ID), + new PHPRegexReader(new PhpVersion(PHP_VERSION_ID), PcreVersion::fromRuntime(), $helper), + ); + } + + private function resetParsedAstCache(): void + { + $property = new ReflectionProperty(RegexGroupParser::class, 'parsedShapes'); + $property->setValue(null, []); + } + + /** + * Pattern bodies the old grammar read; the walk results they produced + * are covered by the resolver suite's fixtures. + * + * @return list + */ + private function oldGrammarReadableBodies(): array + { + return [ + 'a', + 'ab', + 'a\.b', + 'a|b', + 'a|', + '|a', + 'a||b', + '(a)', + '(a)(b)', + '((a))', + '(?a)', + '(?:a)', + '(?i:a)', + '(?i)', + '(?n)', + '(?|a|(b))', + '(?>a+)', + '()', + 'a?', + 'a??', + 'a?+', + 'a*', + 'a*?', + 'a*+', + 'a+', + 'a+?', + 'a++', + 'a{2}', + 'a{2,}', + 'a{2,4}', + 'a{2,4}?', + 'a{2,4}+', + '(ab)+', + '[ab]+', + '[abc]', + '[^abc]', + '[a-z]', + '[-a]', + '[a-]', + '[]]', + '[^]]', + '[a\-z]', + '[[:digit:]]', + '[[:^alpha:]]', + '[a-c\dx]', + '\d', + '\D', + '\w', + '\s', + '\h', + '\v', + '\R', + '\N', + '\p{L}', + '\P{L}', + '\p{Greek}', + '\x41', + '\101', + '\n', + '\t', + '\cA', + '^a$', + '\b', + '\B', + '\A', + '\z', + '\Z', + '\G', + '\K', + '(?=a)', + '(?!a)', + '(?<=a)', + '(?createParser(); + $missing = []; + + foreach ($this->oldGrammarReadableBodies() as $body) { + $this->resetParsedAstCache(); + $result = $parser->parseGroups('/' . $body . '/'); + if ($result !== null) { + continue; + } + + $missing[] = $body; + } + + self::assertSame([], $missing, 'Patterns the old grammar read that the parser now refuses: ' . implode(', ', $missing)); + } + + /** + * Pattern bodies the old grammar could not read at all, or read with a + * phantom literal: the PHPRegex backend must return a walk result anyway. + * Bodies reference groups that exist, so the engine pre-check passes. + * + * @return list + */ + private function hoaBlindBodies(): array + { + return [ + '(a)\1', + '(a)(\1)', + '(a)\g{1}', + '(a)\g1', + '(a)\g{-1}', + '(?a)\k', + "(?a)\k'n'", + '(?a)(?P=n)', + '(?a)\g{n}', + '(?a)\g', + "(?a)\g'name'", + '(?+1)(a)', + '(a)(?1)', + '\Qa.b\E', + 'a\Qb.c\E', + '(?(R)a|b)', + '(?(DEFINE)a)', + '(?(VERSION>=10.4)a|b)', + '(?(?=a)a|b)', + '(\x31)', + 'a{2}?', + '(a)(*PRUNE)b', + 'a(*SKIP)b', + '(a)(*FAIL)b', + '(?:(*ACCEPT)a)', + '(a)(?C1)b', + ]; + } + + public function testTheReaderParsesWhatTheGrammarCouldNot(): void + { + $parser = $this->createParser(); + $missing = []; + + $this->resetParsedAstCache(); + + foreach ($this->hoaBlindBodies() as $body) { + $result = $parser->parseGroups('/' . $body . '/'); + if ($result !== null) { + continue; + } + + $missing[] = $body; + } + + self::assertSame([], $missing, 'Patterns the PHPRegex backend also refused: ' . implode(', ', $missing)); + } + + public function testBackreferenceIsNotTheLiteralOne(): void + { + $parser = $this->createParser(); + + // PCRE truth: preg_match('/(a)(\1)/', 'aa') fills group 2 with 'a'. + // The old grammar read \1 as the literal "1", so group 2 becomes the + // constant string '1' today. + $this->resetParsedAstCache(); + $result = $parser->parseGroups('/(a)(\1)/'); + + self::assertNotNull($result); + $groups = array_values($result->getCapturingGroups()); + self::assertCount(2, $groups); + self::assertNotInstanceOf(ConstantStringType::class, $groups[1]->getType(), 'group 2 must not be a constant string'); + self::assertTrue($groups[1]->getType()->isSuperTypeOf(new ConstantStringType('a'))->yes()); + } + + public function testNoAutoCaptureOptionDropsTheGroups(): void + { + $parser = $this->createParser(); + + // PCRE truth: (?n) turns capturing groups off, the match array holds + // only the full match. The old grammar could not see the option, so a + // phantom group 1 survives in the shapes inferred today. + $matched = preg_match('/(?n)(\d+)/', 'a5', $matches); + self::assertSame(1, $matched); + self::assertSame(['5'], $matches, 'the engine itself must not capture group 1'); + + $this->resetParsedAstCache(); + $result = $parser->parseGroups('/(?n)(\d+)/'); + + self::assertNotNull($result); + self::assertSame([], $result->getCapturingGroups(), 'the walk must not report a capturing group'); + } + + /** + * @throws RegexException + */ + public function testTheReaderHandsBackTheParserOwnTree(): void + { + // no translation layer stands between the walkers and the parser's + // AST: what read() returns is what the parser built, walked as is + $helper = (new ReflectionClass(RegexExpressionHelper::class))->newInstanceWithoutConstructor(); + $reader = new PHPRegexReader(new PhpVersion(80400), PcreVersion::fromString('10.44'), $helper); + + $tree = $reader->read('(a)', '', '/'); + self::assertSame('/', $tree->delimiter); + $body = $tree->pattern; + self::assertInstanceOf(GroupNode::class, $body); + self::assertSame(GroupType::Capturing(), $body->type); + self::assertInstanceOf(LiteralNode::class, $body->child); + self::assertSame('a', $body->child->value); + + $tree = $reader->read('a|b', '', '/'); + self::assertInstanceOf(AlternationNode::class, $tree->pattern); + self::assertCount(2, $tree->pattern->alternatives); + + $tree = $reader->read('a{2,4}', '', '/'); + self::assertInstanceOf(QuantifierNode::class, $tree->pattern); + $bounds = QuantifierBounds::parse($tree->pattern->quantifier); + self::assertNotNull($bounds); + self::assertSame(2, $bounds->min); + self::assertSame(4, $bounds->max); + } + + public function testDuplicateGroupNamesKeepTheirShapesInlineAndTrailing(): void + { + $parser = $this->createParser(); + + // The two spellings of the duplicate-name modifier read the same + // shapes: inline (?J) and trailing /J, two groups of one name across + // alternation branches. + $this->resetParsedAstCache(); + + $result = $parser->parseGroups('/(?J)(?[a-z]+)|(?[0-9]+)/'); + self::assertNotNull($result); + $groups = array_values($result->getCapturingGroups()); + self::assertCount(2, $groups); + self::assertSame('Foo', $groups[0]->getName()); + self::assertSame('Foo', $groups[1]->getName()); + + $this->resetParsedAstCache(); + $trailing = $parser->parseGroups('/(?[a-z]+)|(?[0-9]+)/J'); + self::assertNotNull($trailing, 'the trailing spelling reads as the inline one does'); + $groups = array_values($trailing->getCapturingGroups()); + self::assertCount(2, $groups); + self::assertSame('Foo', $groups[0]->getName()); + self::assertSame('Foo', $groups[1]->getName()); + } + + /** + * @throws RegexException + */ + public function testTheAnalysedPhpVersionShapesTheEscape(): void + { + // \x{ 41 } is a padded hex escape: engines from 10.43 read it as the + // letter A, older ones PHP 7.4 ships with refuse it and read the + // letter x then plain text. The pattern compiles on the running + // engine either way, so the shapes must follow the PHP the project + // analyses for — the grammar the pipeline replaced could not tell + // the two apart at all. + $helper = (new ReflectionClass(RegexExpressionHelper::class))->newInstanceWithoutConstructor(); + // the engines each judged PHP bundles, pinned: the test says the same + // thing on every machine, whatever engine the runner links + $php74 = new RegexGroupParser(new PhpVersion(70400), new PHPRegexReader(new PhpVersion(70400), PcreVersion::fromString('10.33'), $helper)); + $php84 = new RegexGroupParser(new PhpVersion(80400), new PHPRegexReader(new PhpVersion(80400), PcreVersion::fromString('10.44'), $helper)); + + $this->resetParsedAstCache(); + $result74 = $php74->parseGroups('/(a\x{ 41 })/'); + $this->resetParsedAstCache(); + $result84 = $php84->parseGroups('/(a\x{ 41 })/'); + + self::assertNotNull($result74); + self::assertNotNull($result84); + + $group74 = array_values($result74->getCapturingGroups())[0]->getType(); + $group84 = array_values($result84->getCapturingGroups())[0]->getType(); + + self::assertInstanceOf(ConstantStringType::class, $group74, '7.4 reads the letter x then text'); + self::assertSame('ax{ 41 }', $group74->getValue()); + self::assertInstanceOf(ConstantStringType::class, $group84, '8.x reads the padded escape'); + self::assertSame('aA', $group84->getValue()); + + // the reader is a service carrying its own target: no statics, no + // environment, one instance per judged PHP, and it hands back the + // parser's own tree, not a translation of it + $reader74 = new PHPRegexReader(new PhpVersion(70400), PcreVersion::fromString('10.33'), $helper); + $reader84 = new PHPRegexReader(new PhpVersion(80400), PcreVersion::fromString('10.44'), $helper); + + $tree74 = $reader74->read('a\x{ 41 }', '', '/')->pattern; + $tree84 = $reader84->read('a\x{ 41 }', '', '/')->pattern; + + $fingerprint = static function (NodeInterface $n) use (&$fingerprint): string { + $children = ''; + foreach ($n->getChildren() as $child) { + $children .= $fingerprint($child); + } + + $name = (new ReflectionClass($n))->getShortName(); + $detail = ''; + foreach (['value', 'quantifier', 'type', 'isNegated'] as $prop) { + $propValue = property_exists($n, $prop) ? $n->$prop : null; + if ($propValue !== null && (is_string($propValue) || is_int($propValue) || is_bool($propValue))) { + $detail .= '|' . $propValue; + } elseif ($propValue !== null) { + $detail .= '|<' . (new ReflectionClass($propValue))->getShortName() . '>'; + } + } + + return sprintf('(%s%s)', $name, $detail) . $children; + }; + + self::assertSame($fingerprint($tree74), $fingerprint($reader74->read('a\x{ 41 }', '', '/')->pattern), 'the same target reads the same tree twice'); + self::assertNotSame($fingerprint($tree74), $fingerprint($tree84), 'the two targets build two trees'); + } + + public function testPcreVersionMirrorsPhpVersion(): void + { + // the same architecture PhpVersion has: sources with labels, a + // factory fed by config and composer.json, runtime as the default + $runtime = (new PcreVersionFactory(null, null))->create(); + self::assertSame(PcreVersion::SOURCE_RUNTIME, $runtime->getSource()); + self::assertSame('runtime', $runtime->getSourceLabel()); + self::assertMatchesRegularExpression('/^10\.\d\d$/', $runtime->getVersionString()); + + // the running engine in one call, its release date trimmed away + self::assertSame( + $runtime->getVersionId(), + PcreVersion::fromRuntime()->getVersionId(), + 'fromRuntime spells what the factory defaults to', + ); + self::assertSame(PcreVersion::SOURCE_RUNTIME, PcreVersion::fromRuntime()->getSource()); + + $config = (new PcreVersionFactory('10.42', null))->create(); + self::assertSame(PcreVersion::SOURCE_CONFIG, $config->getSource()); + self::assertSame('config', $config->getSourceLabel()); + self::assertSame('10.42', $config->getVersionString()); + self::assertSame(1042, $config->getVersionId()); + self::assertFalse($config->atLeast('10.43'), 'below the release padded hex escapes need'); + self::assertTrue($config->atLeast('10.42'), 'at itself'); + self::assertFalse($config->atLeast('10.44'), 'below the release long group names need'); + + $composer = (new PcreVersionFactory(null, '10.37'))->create(); + self::assertSame(PcreVersion::SOURCE_COMPOSER_PLATFORM_PCRE, $composer->getSource()); + self::assertSame('config.platform.lib-pcre in composer.json', $composer->getSourceLabel()); + self::assertSame('10.37', $composer->getVersionString()); + + // the spelling PCRE2 writes; a runtime the factory cannot read falls + // back to the engine the running PHP bundles, never takes the boot + // down + // two digits on each side, whatever the series: 11 is not out yet, + // 12 will read the same way 10 does + self::assertSame('11.00', PcreVersion::fromString('11.00')->getVersionString()); + self::assertSame('12.00', PcreVersion::fromString('12.00')->getVersionString()); + + try { + PcreVersion::fromString('banana'); + self::fail('an engine release must be spelled like one'); + } catch (InvalidArgumentException) { + // the spelling is the test: the release is refused + } + try { + PcreVersion::fromString('11.5'); + self::fail('a minor is two digits, as PCRE2 writes it'); + } catch (InvalidArgumentException) { + // the spelling is the test: the release is refused + } + try { + PcreVersion::fromString('9.42'); + self::fail('a major is two digits, as PCRE2 writes it since the 10 series'); + } catch (InvalidArgumentException) { + // the spelling is the test: the release is refused + } + } + + /** + * @throws RegexException + */ + public function testTheJudgedEngineFollowsThePinThenTheWitness(): void + { + // a named engine is a statement about the analysed project, honoured + // in both directions; the running engine is only a witness, and it + // speaks for the analysed PHP when the two share a minor — a patch + // release carries whatever engine was current, and the baseline map + // only knows the minor's first + $helper = (new ReflectionClass(RegexExpressionHelper::class))->newInstanceWithoutConstructor(); + $targetOf = static function (PHPRegexReader $reader): string { + $reader->read('a', '', '/'); + + $property = new ReflectionProperty(PHPRegexReader::class, 'parser'); + $property->setAccessible(true); + + /** @var RegexParser $parser */ + $parser = $property->getValue($reader); + + return $parser->target()->pcreVersion; + }; + + $minor = intdiv(PHP_VERSION_ID, 100) * 100; + $runtime = PcreVersion::fromRuntime(); + $witness = PcreVersion::fromString($runtime->getVersionString(), PcreVersion::SOURCE_RUNTIME); + + self::assertSame('10.49', $targetOf(new PHPRegexReader(new PhpVersion(80400), PcreVersion::fromString('10.49', PcreVersion::SOURCE_CONFIG), $helper)), 'a pin is a statement, honoured above the baseline'); + self::assertSame('10.42', $targetOf(new PHPRegexReader(new PhpVersion(80400), PcreVersion::fromString('10.42', PcreVersion::SOURCE_CONFIG), $helper)), 'a pin below the baseline narrows'); + self::assertSame($witness->getVersionString(), $targetOf(new PHPRegexReader(new PhpVersion($minor), $witness, $helper)), 'the running engine speaks for its own minor'); + self::assertSame( + PcreTarget::bundledWith(70400)->pcreVersion, + $targetOf(new PHPRegexReader(new PhpVersion(70400), $witness, $helper)), + 'a witness from another minor stays silent, and the baseline judges', + ); + self::assertSame('10.49', PcreVersion::fromString('10.49-RC1')->getVersionString(), 'a release candidate reads as its release'); + } + + public function testDefineGroupsAreCountedOnceAndNeverMatch(): void + { + $parser = $this->createParser(); + + // the engine never fills a group a (?(DEFINE)...) block: it is + // counted once — the branch it sits in is not walked as a branch — + // and carries no type of its own + $this->resetParsedAstCache(); + $result = $parser->parseGroups('/(?(DEFINE)(?x))b/'); + + self::assertNotNull($result); + $groups = array_values($result->getCapturingGroups()); + self::assertCount(1, $groups, 'the defined group is counted once'); + self::assertSame('a', $groups[0]->getName()); + self::assertTrue($groups[0]->isOptional(), 'the engine never fills it'); + } + + public function testADefinedGroupHoldsOnlyTheEmptyString(): void + { + $parser = $this->createParser(); + + // preg_match('/(?(DEFINE)(?abc))(?x)/', 'x', $m) fills + // bar and leaves foo at '' — the engine never runs what DEFINE holds + $this->resetParsedAstCache(); + $result = $parser->parseGroups('/(?(DEFINE)(?abc))(?x)/'); + + self::assertNotNull($result); + $foo = $result->getCapturingGroups()[1] ?? null; + self::assertNotNull($foo); + self::assertSame("''", $foo->getType()->describe(VerbosityLevel::precise()), 'a defined group stays empty'); + } + + public function testConditionalBranchesAreExclusive(): void + { + $parser = $this->createParser(); + + // preg_match('/(a)(?(1)b|(c))/', 'ab', $m) fills group 1 and never + // group 2 — the branches run one at a time, as an alternation + $this->resetParsedAstCache(); + $result = $parser->parseGroups('/(a)(?(1)b|(c))/'); + + self::assertNotNull($result); + $groups = $result->getCapturingGroups(); + self::assertSame("'a'", $groups[1]->getType()->describe(VerbosityLevel::precise())); + self::assertFalse($groups[1]->isOptional()); + self::assertSame("'c'", $groups[2]->getType()->describe(VerbosityLevel::precise())); + self::assertTrue($groups[2]->isOptional(), 'the else branch runs alone, or not at all'); + } + + public function testALookaroundConditionCountsItsGroups(): void + { + $parser = $this->createParser(); + + // the engine numbers the groups a lookaround condition holds before + // the branches' — preg_match numbers (a) as 1 and (c) as 2, whatever + // runs + $this->resetParsedAstCache(); + $result = $parser->parseGroups('/(?(?=(a))b|(c))/'); + + self::assertNotNull($result); + $groups = $result->getCapturingGroups(); + self::assertCount(2, $groups, 'the condition holds group 1'); + self::assertSame("'a'", $groups[1]->getType()->describe(VerbosityLevel::precise()), 'the condition group captures a when it is set'); + self::assertTrue($groups[1]->isOptional(), 'the condition group is unset when the lookahead fails'); + self::assertSame("'c'", $groups[2]->getType()->describe(VerbosityLevel::precise())); + } + + public function testTheTrailingDuplicateNameModifierReads(): void + { + $parser = $this->createParser(); + + // PCRE truth: the trailing J allows duplicate names exactly as the + // inline (?J) spelling does — preg_match('/(?a)(?b)/J', 'ab') + // fills both groups and the name with the branch that ran + self::assertSame(1, preg_match('/(?a)(?b)/J', 'ab', $m)); + self::assertSame('{"0":"ab","x":"b","1":"a","2":"b"}', json_encode($m), 'the engine fills both groups and the name with the branch that ran'); + + $this->resetParsedAstCache(); + $result = $parser->parseGroups('/(?a)(?b)/J'); + + self::assertNotNull($result, 'the trailing J reads as the inline spelling does'); + self::assertCount(2, $result->getCapturingGroups()); + self::assertSame('x', $result->getCapturingGroups()[1]->getName()); + self::assertSame('x', $result->getCapturingGroups()[2]->getName()); + } + + public function testTheMatchAndTheSubjectAreTwoChannels(): void + { + $parser = $this->createParser(); + + // $matches[0] is the match the engine wrote, constants included; + // the subject is what the pattern was run against — a different + // quantity. A pattern pinned at both ends tells what the whole subject + // holds, but $ admits one final newline: "a\n" matches /^(a|b|c)$/, + // /D closes that door + $this->resetParsedAstCache(); + $result = $parser->parseGroups('/^(a|b|c)$/'); + self::assertNotNull($result); + self::assertSame("'a'|'b'|'c'", $result->getSubjectBaseType()->describe(VerbosityLevel::precise()), 'the match channel keeps the constants'); + $narrowType = $result->getSubjectNarrowType(); + self::assertSame("'a'|\"a\\n\"|'b'|\"b\\n\"|'c'|\"c\\n\"", $narrowType->describe(VerbosityLevel::precise()), 'the subject is a value of the match, or one followed by a newline'); + + $this->resetParsedAstCache(); + $result = $parser->parseGroups('/^(a|b)$/D'); + self::assertNotNull($result); + self::assertSame("'a'|'b'", $result->getSubjectNarrowType()->describe(VerbosityLevel::precise()), 'under D the subject is the match'); + + $this->resetParsedAstCache(); + $result = $parser->parseGroups('/foo/'); + self::assertNotNull($result); + self::assertSame("'foo'", $result->getSubjectBaseType()->describe(VerbosityLevel::precise()), 'the match channel keeps the constant'); + $narrowType = $result->getSubjectNarrowType(); + self::assertSame('non-falsy-string', $narrowType->describe(VerbosityLevel::precise()), 'a subject that contains a non-falsy match is non-falsy itself'); + + $this->resetParsedAstCache(); + $result = $parser->parseGroups('/^$/'); + self::assertNotNull($result); + self::assertSame("''", $result->getSubjectBaseType()->describe(VerbosityLevel::precise()), 'the match channel keeps the empty constant'); + $narrowType = $result->getSubjectNarrowType(); + self::assertSame("''|\"\\n\"", $narrowType->describe(VerbosityLevel::precise()), 'the subject may be the empty string or the final newline'); + } + + public function testTargetInvalidPatternsGetNoShape(): void + { + // the pre-check judges for the analysed PHP, not the running one: a + // padded \x{ 41 } compiles on the runner's 10.43+ engine but the PHP + // an 8.2 project runs refuses it, so the shape is null and the + // invalid-pattern rule keeps the running engine as its second + // witness; the target's word comes first + $helper = (new ReflectionClass(RegexExpressionHelper::class))->newInstanceWithoutConstructor(); + $php82 = new RegexGroupParser(new PhpVersion(80200), new PHPRegexReader(new PhpVersion(80200), PcreVersion::fromString('10.40'), $helper)); + $php84 = new RegexGroupParser(new PhpVersion(80400), new PHPRegexReader(new PhpVersion(80400), PcreVersion::fromString('10.44'), $helper)); + + $this->resetParsedAstCache(); + self::assertNull($php82->parseGroups('/(a\x{ 41 })/'), 'the engine PHP 8.2 bundles refuses the padded escape'); + + $this->resetParsedAstCache(); + $result = $php84->parseGroups('/(a\x{ 41 })/'); + self::assertNotNull($result, 'the engine PHP 8.4 bundles reads it'); + $group84 = array_values($result->getCapturingGroups())[0]->getType(); + self::assertInstanceOf(ConstantStringType::class, $group84, 'the shape knows what it reads'); + self::assertSame('aA', $group84->getValue()); + } + + public function testEngineRefusedPatternsGetNoShapeAndBracketDelimitersWalk(): void + { + $parser = $this->createParser(); + + // the engine itself refuses container verbs in every spelling PHP + // exposes, so the pre-check nulls the read before a group inside a + // (*SR:...) or (*SCRIPT_RUN:...) could ever reach the walk — the day + // an engine takes them, its own parser will carry the body as nodes + $this->resetParsedAstCache(); + self::assertNull($parser->parseGroups('/(*SR:(?a))b/')); + $this->resetParsedAstCache(); + self::assertNull($parser->parseGroups('/(*SCRIPT_RUN:(?a))b/')); + + // the delimiter the pattern was written with, brackets included, + // travels to the reader whole + $this->resetParsedAstCache(); + $result = $parser->parseGroups('{(a)}'); + self::assertNotNull($result); + $groups = array_values($result->getCapturingGroups()); + self::assertCount(1, $groups); + self::assertInstanceOf(ConstantStringType::class, $groups[0]->getType()); + self::assertSame('a', $groups[0]->getType()->getValue()); + } + + public function testXModeCommentsAreReadByTheParserNotPreStripped(): void + { + $parser = $this->createParser(); + + // PCRE truth: under /x the # only starts a comment outside \Q..\E and + // classes. The pre-strip that runs before the grammar is blind to + // both; in front of a mode-aware parser it mangles the pattern. + $matched = preg_match('/(a\Q#b\Ec)/x', 'a#bc', $matches); + self::assertSame(1, $matched); + self::assertSame('["a#bc","a#bc"]', json_encode($matches), 'the engine reads the quoted hash as text'); + + $this->resetParsedAstCache(); + $result = $parser->parseGroups('/(a\Q#b\Ec)/x'); + + self::assertNotNull($result, 'the pattern must parse whole, comments included'); + $groups = array_values($result->getCapturingGroups()); + self::assertCount(1, $groups); + $type = $groups[0]->getType(); + self::assertInstanceOf(ConstantStringType::class, $type); + self::assertSame('a#bc', $type->getValue()); + } + +} diff --git a/turbo-ext/src/ArgumentsHandler.cpp b/turbo-ext/src/ArgumentsHandler.cpp index 8551f011a8b..ddab49c89bd 100644 --- a/turbo-ext/src/ArgumentsHandler.cpp +++ b/turbo-ext/src/ArgumentsHandler.cpp @@ -2418,6 +2418,26 @@ class ArgumentsHandler bool isArray = false; if (UNEXPECTED(!isA(value, PT_CLASS_ARRAY_EXPR, isArray))) return false; if (isArray) { + if (a.parameter != NULL) { + // an extension can type the closures of an array literal by describing the whole array + zv::Val overwritingParameterType = getParameterTypeFromParameterClosureTypeExtension(w.callLike, w.calleeReflection, a.parameter, a.scopeToPass.raw()); + if (UNEXPECTED(overwritingParameterType.isUndef())) return false; + if (!overwritingParameterType.isNull()) { + a.parameterType = std::move(overwritingParameterType); + if (UNEXPECTED(Z_TYPE_P(a.scopeToPass.raw()) != IS_OBJECT)) { + zend_throw_error(NULL, "Call to a member function doNotTreatPhpDocTypesAsCertain() on %s", zend_zval_value_name(a.scopeToPass.raw())); + return false; + } + zv::Val nativeScope = pt_mutating_scope_do_not_treat_phpdoc_types_as_certain(Z_OBJ_P(a.scopeToPass.raw())); + if (UNEXPECTED(nativeScope.isUndef())) return false; + zv::Val overwritingParameterNativeType = getParameterTypeFromParameterClosureTypeExtension(w.callLike, w.calleeReflection, a.parameter, nativeScope.raw()); + if (UNEXPECTED(overwritingParameterNativeType.isUndef())) return false; + if (!overwritingParameterNativeType.isNull()) { + a.parameterNativeType = std::move(overwritingParameterNativeType); + } + a.passedToTypeResolver.release(); + } + } zv::Val passed = pt_expression_context_enter_passed_to_type(argContext.raw(), a.parameterType.isUndef() ? NULL : a.parameterType.raw(), a.parameterNativeType.isUndef() ? NULL : a.parameterNativeType.raw()); if (UNEXPECTED(passed.isUndef())) return false; argContext = std::move(passed);