Skip to content
Snippets Groups Projects
Commit 2dcee8be authored by David Prévot's avatar David Prévot
Browse files

New upstream version 3.2.0

parents d84da7b2 ea4ab6f9
No related branches found
No related tags found
No related merge requests found
Showing
with 925 additions and 22 deletions
......@@ -5,7 +5,6 @@ on:
- pull_request
env:
COMPOSER_FLAGS: "--ansi --no-interaction --no-progress --prefer-dist"
SYMFONY_PHPUNIT_REMOVE_RETURN_TYPEHINT: "1"
jobs:
......@@ -13,6 +12,7 @@ jobs:
name: "CI"
runs-on: ubuntu-latest
continue-on-error: ${{ matrix.experimental }}
strategy:
matrix:
......@@ -22,7 +22,10 @@ jobs:
- "8.1"
- "8.2"
- "8.3"
- "8.4"
experimental: [false]
include:
- php-version: "8.4"
experimental: true
steps:
- name: "Checkout"
......@@ -47,9 +50,9 @@ jobs:
- name: "Install latest dependencies"
run: |
# Remove PHPStan as it requires a newer PHP
composer remove phpstan/phpstan phpstan/phpstan-strict-rules --dev --no-update
composer update ${{ env.COMPOSER_FLAGS }}
- name: "Run tests"
run: "vendor/bin/simple-phpunit --verbose"
run: |
vendor/bin/phpunit
vendor/bin/phpunit --testsuite phpstan
......@@ -44,8 +44,5 @@ jobs:
- name: "Install latest dependencies"
run: "composer update ${{ env.COMPOSER_FLAGS }}"
- name: "Initialize PHPUnit sources"
run: "vendor/bin/simple-phpunit --filter NO_TEST_JUST_AUTOLOAD_THANKS"
- name: "Run PHPStan"
run: "composer phpstan"
......@@ -12,7 +12,8 @@ to understand the implications.
It thus makes it easier to work with static analysis tools like PHPStan or Psalm as it
simplifies and reduces the possible return values from all the `preg_*` functions which
are quite packed with edge cases.
are quite packed with edge cases. As of v2.2.0 / v3.2.0 the library also comes with a
[PHPStan extension](#phpstan-extension) for parsing regular expressions and giving you even better output types.
This library is a thin wrapper around `preg_*` functions with [some limitations](#restrictions--limitations).
If you are looking for a richer API to handle regular expressions have a look at
......@@ -175,6 +176,13 @@ preg_match('/(a)(b)*(c)(d)*/', 'ac', $matches, $flags);
| group 2 (any unmatched group preceding one that matched) is set to `''`. You cannot tell if it matched an empty string or did not match at all | group 2 is `null` when unmatched and a string if it matched, easy to check for |
| group 4 (any optional group without a matching one following) is missing altogether. So you have to check with `isset()`, but really you want `isset($m[4]) && $m[4] !== ''` for safety unless you are very careful to check that a non-optional group follows it | group 4 is always set, and null in this case as there was no match, easy to check for with `$m[4] !== null` |
PHPStan Extension
-----------------
To use the PHPStan extension if you do not use `phpstan/extension-installer` you can include `vendor/composer/pcre/extension.neon` in your PHPStan config.
The extension provides much better type information for $matches as well as regex validation where possible.
License
-------
......
......@@ -20,10 +20,13 @@
"php": "^7.4 || ^8.0"
},
"require-dev": {
"symfony/phpunit-bridge": "^5",
"phpstan/phpstan": "^1.3",
"phpunit/phpunit": "^8 || ^9",
"phpstan/phpstan": "^1.11.8",
"phpstan/phpstan-strict-rules": "^1.1"
},
"conflict": {
"phpstan/phpstan": "<1.11.8"
},
"autoload": {
"psr-4": {
"Composer\\Pcre\\": "src"
......@@ -37,10 +40,15 @@
"extra": {
"branch-alias": {
"dev-main": "3.x-dev"
},
"phpstan": {
"includes": [
"extension.neon"
]
}
},
"scripts": {
"test": "vendor/bin/simple-phpunit",
"phpstan": "phpstan analyse"
"test": "@php vendor/bin/phpunit",
"phpstan": "@php phpstan analyse"
}
}
# composer/pcre PHPStan extensions
#
# These can be reused by third party packages by including 'vendor/composer/pcre/extension.neon'
# in your phpstan config
conditionalTags:
Composer\Pcre\PHPStan\PregMatchParameterOutTypeExtension:
phpstan.staticMethodParameterOutTypeExtension: %featureToggles.narrowPregMatches%
Composer\Pcre\PHPStan\PregMatchTypeSpecifyingExtension:
phpstan.typeSpecifier.staticMethodTypeSpecifyingExtension: %featureToggles.narrowPregMatches%
Composer\Pcre\PHPStan\UnsafeStrictGroupsCallRule:
phpstan.rules.rule: %featureToggles.narrowPregMatches%
services:
-
class: Composer\Pcre\PHPStan\PregMatchParameterOutTypeExtension
-
class: Composer\Pcre\PHPStan\PregMatchTypeSpecifyingExtension
rules:
- Composer\Pcre\PHPStan\UnsafeStrictGroupsCallRule
- Composer\Pcre\PHPStan\InvalidRegexPatternRule
......@@ -9,3 +9,93 @@ parameters:
message: "#^Parameter &\\$matches @param\\-out type of method Composer\\\\Pcre\\\\Preg\\:\\:matchAllWithOffsets\\(\\) expects array\\<int\\|string, list\\<array\\{string\\|null, int\\<\\-1, max\\>\\}\\>\\>, array given\\.$#"
count: 1
path: src/Preg.php
-
message: "#^Regex pattern is invalid\\: No ending matching delimiter '\\}' found$#"
count: 2
path: tests/PregTests/GrepTest.php
-
message: "#^Regex pattern is invalid\\: No ending matching delimiter '\\}' found$#"
count: 2
path: tests/PregTests/IsMatchAllTest.php
-
message: "#^Regex pattern is invalid\\: No ending matching delimiter '\\}' found$#"
count: 2
path: tests/PregTests/IsMatchAllWithOffsetsTest.php
-
message: "#^Regex pattern is invalid\\: No ending matching delimiter '\\}' found$#"
count: 2
path: tests/PregTests/IsMatchTest.php
-
message: "#^Regex pattern is invalid\\: No ending matching delimiter '\\}' found$#"
count: 2
path: tests/PregTests/IsMatchWithOffsetsTest.php
-
message: "#^Regex pattern is invalid\\: No ending matching delimiter '\\}' found$#"
count: 2
path: tests/PregTests/MatchAllTest.php
-
message: "#^Regex pattern is invalid\\: No ending matching delimiter '\\}' found$#"
count: 2
path: tests/PregTests/MatchTest.php
-
message: "#^Regex pattern is invalid\\: No ending matching delimiter '\\}' found$#"
count: 2
path: tests/PregTests/ReplaceCallbackArrayTest.php
-
message: "#^Regex pattern is invalid\\: No ending matching delimiter '\\}' found$#"
count: 2
path: tests/PregTests/ReplaceCallbackTest.php
-
message: "#^Regex pattern is invalid\\: No ending matching delimiter '\\}' found$#"
count: 2
path: tests/PregTests/ReplaceTest.php
-
message: "#^Regex pattern is invalid\\: No ending matching delimiter '\\}' found$#"
count: 2
path: tests/PregTests/SplitTest.php
-
message: "#^Regex pattern is invalid\\: No ending matching delimiter '\\}' found$#"
count: 2
path: tests/PregTests/SplitWithOffsetsTest.php
-
message: "#^Regex pattern is invalid\\: No ending matching delimiter '\\}' found$#"
count: 2
path: tests/RegexTests/IsMatchTest.php
-
message: "#^Regex pattern is invalid\\: No ending matching delimiter '\\}' found$#"
count: 2
path: tests/RegexTests/MatchAllTest.php
-
message: "#^Regex pattern is invalid\\: No ending matching delimiter '\\}' found$#"
count: 2
path: tests/RegexTests/MatchTest.php
-
message: "#^Regex pattern is invalid\\: No ending matching delimiter '\\}' found$#"
count: 2
path: tests/RegexTests/ReplaceCallbackArrayTest.php
-
message: "#^Regex pattern is invalid\\: No ending matching delimiter '\\}' found$#"
count: 2
path: tests/RegexTests/ReplaceCallbackTest.php
-
message: "#^Regex pattern is invalid\\: No ending matching delimiter '\\}' found$#"
count: 2
path: tests/RegexTests/ReplaceTest.php
......@@ -7,9 +7,14 @@ parameters:
treatPhpDocTypesAsCertain: false
bootstrapFiles:
- tests/phpstan-locate-phpunit-autoloader.php
- vendor/autoload.php
excludePaths:
- tests/PHPStanTests/nsrt/*
- tests/PHPStanTests/fixtures/*
includes:
- extension.neon
- vendor/phpstan/phpstan/conf/bleedingEdge.neon
- vendor/phpstan/phpstan-strict-rules/rules.neon
- phpstan-baseline.neon
......@@ -5,10 +5,15 @@
backupGlobals="false"
colors="true"
bootstrap="vendor/autoload.php"
defaultTestSuite="pcre"
>
<testsuites>
<testsuite name="PCRE Test Suite">
<directory>tests</directory>
<testsuite name="pcre">
<directory>tests/PregTests</directory>
<directory>tests/RegexTests</directory>
</testsuite>
<testsuite name="phpstan">
<directory>tests/PHPStanTests</directory>
</testsuite>
</testsuites>
......
<?php declare(strict_types = 1);
namespace Composer\Pcre\PHPStan;
use Composer\Pcre\Preg;
use Composer\Pcre\Regex;
use Composer\Pcre\PcreException;
use Nette\Utils\RegexpException;
use Nette\Utils\Strings;
use PhpParser\Node;
use PhpParser\Node\Expr\StaticCall;
use PhpParser\Node\Name\FullyQualified;
use PHPStan\Analyser\Scope;
use PHPStan\Rules\Rule;
use PHPStan\Rules\RuleErrorBuilder;
use function in_array;
use function sprintf;
/**
* Copy of PHPStan's RegularExpressionPatternRule
*
* @implements Rule<StaticCall>
*/
class InvalidRegexPatternRule implements Rule
{
public function getNodeType(): string
{
return StaticCall::class;
}
public function processNode(Node $node, Scope $scope): array
{
$patterns = $this->extractPatterns($node, $scope);
$errors = [];
foreach ($patterns as $pattern) {
$errorMessage = $this->validatePattern($pattern);
if ($errorMessage === null) {
continue;
}
$errors[] = RuleErrorBuilder::message(sprintf('Regex pattern is invalid: %s', $errorMessage))->identifier('regexp.pattern')->build();
}
return $errors;
}
/**
* @return string[]
*/
private function extractPatterns(StaticCall $node, Scope $scope): array
{
if (!$node->class instanceof FullyQualified) {
return [];
}
$isRegex = $node->class->toString() === Regex::class;
$isPreg = $node->class->toString() === Preg::class;
if (!$isRegex && !$isPreg) {
return [];
}
if (!$node->name instanceof Node\Identifier || !Preg::isMatch('{^(match|isMatch|grep|replace|split)}', $node->name->name)) {
return [];
}
$functionName = $node->name->name;
if (!isset($node->getArgs()[0])) {
return [];
}
$patternNode = $node->getArgs()[0]->value;
$patternType = $scope->getType($patternNode);
$patternStrings = [];
foreach ($patternType->getConstantStrings() as $constantStringType) {
if ($functionName === 'replaceCallbackArray') {
continue;
}
$patternStrings[] = $constantStringType->getValue();
}
foreach ($patternType->getConstantArrays() as $constantArrayType) {
if (
in_array($functionName, [
'replace',
'replaceCallback',
], true)
) {
foreach ($constantArrayType->getValueTypes() as $arrayKeyType) {
foreach ($arrayKeyType->getConstantStrings() as $constantString) {
$patternStrings[] = $constantString->getValue();
}
}
}
if ($functionName !== 'replaceCallbackArray') {
continue;
}
foreach ($constantArrayType->getKeyTypes() as $arrayKeyType) {
foreach ($arrayKeyType->getConstantStrings() as $constantString) {
$patternStrings[] = $constantString->getValue();
}
}
}
return $patternStrings;
}
private function validatePattern(string $pattern): ?string
{
try {
$msg = null;
$prev = set_error_handler(function (int $severity, string $message, string $file) use (&$msg): bool {
$msg = preg_replace("#^preg_match(_all)?\\(.*?\\): #", '', $message);
return true;
});
if ($pattern === '') {
return 'Empty string is not a valid regular expression';
}
Preg::match($pattern, '');
if ($msg !== null) {
return $msg;
}
} catch (PcreException $e) {
if ($e->getCode() === PREG_INTERNAL_ERROR && $msg !== null) {
return $msg;
}
return preg_replace('{.*? failed executing ".*": }', '', $e->getMessage());
} finally {
restore_error_handler();
}
return null;
}
}
<?php declare(strict_types=1);
namespace Composer\Pcre\PHPStan;
use PHPStan\Analyser\Scope;
use PHPStan\Type\Constant\ConstantIntegerType;
use PHPStan\Type\TypeCombinator;
use PHPStan\Type\Type;
use PhpParser\Node\Arg;
use PHPStan\Type\Php\RegexArrayShapeMatcher;
final class PregMatchFlags
{
static public function getType(?Arg $flagsArg, Scope $scope): ?Type
{
if ($flagsArg === null) {
return new ConstantIntegerType(PREG_UNMATCHED_AS_NULL | RegexArrayShapeMatcher::PREG_UNMATCHED_AS_NULL_ON_72_73);
}
$flagsType = $scope->getType($flagsArg->value);
$constantScalars = $flagsType->getConstantScalarValues();
if ($constantScalars === []) {
return null;
}
$internalFlagsTypes = [];
foreach ($flagsType->getConstantScalarValues() as $constantScalarValue) {
if (!is_int($constantScalarValue)) {
return null;
}
$internalFlagsTypes[] = new ConstantIntegerType($constantScalarValue | PREG_UNMATCHED_AS_NULL | RegexArrayShapeMatcher::PREG_UNMATCHED_AS_NULL_ON_72_73);
}
return TypeCombinator::union(...$internalFlagsTypes);
}
}
<?php declare(strict_types=1);
namespace Composer\Pcre\PHPStan;
use Composer\Pcre\Preg;
use PhpParser\Node\Expr\StaticCall;
use PHPStan\Analyser\Scope;
use PHPStan\Reflection\MethodReflection;
use PHPStan\Reflection\ParameterReflection;
use PHPStan\TrinaryLogic;
use PHPStan\Type\Php\RegexArrayShapeMatcher;
use PHPStan\Type\StaticMethodParameterOutTypeExtension;
use PHPStan\Type\Type;
final class PregMatchParameterOutTypeExtension implements StaticMethodParameterOutTypeExtension
{
/**
* @var RegexArrayShapeMatcher
*/
private $regexShapeMatcher;
public function __construct(
RegexArrayShapeMatcher $regexShapeMatcher
)
{
$this->regexShapeMatcher = $regexShapeMatcher;
}
public function isStaticMethodSupported(MethodReflection $methodReflection, ParameterReflection $parameter): bool
{
return
$methodReflection->getDeclaringClass()->getName() === Preg::class
&& in_array($methodReflection->getName(), ['match', 'isMatch', 'matchStrictGroups', 'isMatchStrictGroups'], true)
&& $parameter->getName() === 'matches';
}
public function getParameterOutTypeFromStaticMethodCall(MethodReflection $methodReflection, StaticCall $methodCall, ParameterReflection $parameter, Scope $scope): ?Type
{
$args = $methodCall->getArgs();
$patternArg = $args[0] ?? null;
$matchesArg = $args[2] ?? null;
$flagsArg = $args[3] ?? null;
if (
$patternArg === null || $matchesArg === null
) {
return null;
}
$flagsType = PregMatchFlags::getType($flagsArg, $scope);
if ($flagsType === null) {
return null;
}
return $this->regexShapeMatcher->matchExpr($patternArg->value, $flagsType, TrinaryLogic::createMaybe(), $scope);
}
}
<?php declare(strict_types=1);
namespace Composer\Pcre\PHPStan;
use Composer\Pcre\Preg;
use PhpParser\Node\Expr\StaticCall;
use PHPStan\Analyser\Scope;
use PHPStan\Analyser\SpecifiedTypes;
use PHPStan\Analyser\TypeSpecifier;
use PHPStan\Analyser\TypeSpecifierAwareExtension;
use PHPStan\Analyser\TypeSpecifierContext;
use PHPStan\Reflection\MethodReflection;
use PHPStan\TrinaryLogic;
use PHPStan\Type\Constant\ConstantArrayType;
use PHPStan\Type\Php\RegexArrayShapeMatcher;
use PHPStan\Type\StaticMethodTypeSpecifyingExtension;
use PHPStan\Type\TypeCombinator;
use PHPStan\Type\Type;
final class PregMatchTypeSpecifyingExtension implements StaticMethodTypeSpecifyingExtension, TypeSpecifierAwareExtension
{
/**
* @var TypeSpecifier
*/
private $typeSpecifier;
/**
* @var RegexArrayShapeMatcher
*/
private $regexShapeMatcher;
public function __construct(RegexArrayShapeMatcher $regexShapeMatcher)
{
$this->regexShapeMatcher = $regexShapeMatcher;
}
public function setTypeSpecifier(TypeSpecifier $typeSpecifier): void
{
$this->typeSpecifier = $typeSpecifier;
}
public function getClass(): string
{
return Preg::class;
}
public function isStaticMethodSupported(MethodReflection $methodReflection, StaticCall $node, TypeSpecifierContext $context): bool
{
return in_array($methodReflection->getName(), ['match', 'isMatch', 'matchStrictGroups', 'isMatchStrictGroups'], true) && !$context->null();
}
public function specifyTypes(MethodReflection $methodReflection, StaticCall $node, Scope $scope, TypeSpecifierContext $context): SpecifiedTypes
{
$args = $node->getArgs();
$patternArg = $args[0] ?? null;
$matchesArg = $args[2] ?? null;
$flagsArg = $args[3] ?? null;
if (
$patternArg === null || $matchesArg === null
) {
return new SpecifiedTypes();
}
$flagsType = PregMatchFlags::getType($flagsArg, $scope);
if ($flagsType === null) {
return new SpecifiedTypes();
}
$matchedType = $this->regexShapeMatcher->matchExpr($patternArg->value, $flagsType, TrinaryLogic::createFromBoolean($context->true()), $scope);
if ($matchedType === null) {
return new SpecifiedTypes();
}
if (
in_array($methodReflection->getName(), ['matchStrictGroups', 'isMatchStrictGroups'], true)
&& count($matchedType->getConstantArrays()) === 1
) {
$matchedType = $matchedType->getConstantArrays()[0];
$matchedType = new ConstantArrayType(
$matchedType->getKeyTypes(),
array_map(static function (Type $valueType): Type {
return TypeCombinator::removeNull($valueType);
}, $matchedType->getValueTypes()),
$matchedType->getNextAutoIndexes(),
[],
$matchedType->isList()
);
}
$overwrite = false;
if ($context->false()) {
$overwrite = true;
$context = $context->negate();
}
return $this->typeSpecifier->create(
$matchesArg->value,
$matchedType,
$context,
$overwrite,
$scope,
$node
);
}
}
<?php declare(strict_types=1);
namespace Composer\Pcre\PHPStan;
use Composer\Pcre\Preg;
use Composer\Pcre\Regex;
use PhpParser\Node;
use PhpParser\Node\Expr\StaticCall;
use PhpParser\Node\Name\FullyQualified;
use PHPStan\Analyser\Scope;
use PHPStan\Analyser\SpecifiedTypes;
use PHPStan\Rules\Rule;
use PHPStan\Rules\RuleErrorBuilder;
use PHPStan\TrinaryLogic;
use PHPStan\Type\ObjectType;
use PHPStan\Type\Type;
use PHPStan\Type\TypeCombinator;
use PHPStan\Type\Php\RegexArrayShapeMatcher;
use function sprintf;
/**
* @implements Rule<StaticCall>
*/
final class UnsafeStrictGroupsCallRule implements Rule
{
/**
* @var RegexArrayShapeMatcher
*/
private $regexShapeMatcher;
public function __construct(RegexArrayShapeMatcher $regexShapeMatcher)
{
$this->regexShapeMatcher = $regexShapeMatcher;
}
public function getNodeType(): string
{
return StaticCall::class;
}
public function processNode(Node $node, Scope $scope): array
{
if (!$node->class instanceof FullyQualified) {
return [];
}
$isRegex = $node->class->toString() === Regex::class;
$isPreg = $node->class->toString() === Preg::class;
if (!$isRegex && !$isPreg) {
return [];
}
if (!$node->name instanceof Node\Identifier || !in_array($node->name->name, ['matchStrictGroups', 'isMatchStrictGroups', 'matchAllStrictGroups', 'isMatchAllStrictGroups'], true)) {
return [];
}
$args = $node->getArgs();
if (!isset($args[0])) {
return [];
}
$patternArg = $args[0] ?? null;
if ($isPreg) {
if (!isset($args[2])) { // no matches set, skip as the matches won't be used anyway
return [];
}
$flagsArg = $args[3] ?? null;
} else {
$flagsArg = $args[2] ?? null;
}
if ($patternArg === null) {
return [];
}
$flagsType = PregMatchFlags::getType($flagsArg, $scope);
if ($flagsType === null) {
return [];
}
$matchedType = $this->regexShapeMatcher->matchExpr($patternArg->value, $flagsType, TrinaryLogic::createYes(), $scope);
if ($matchedType === null) {
return [
RuleErrorBuilder::message(sprintf('The %s call is potentially unsafe as $matches\' type could not be inferred.', $node->name->name))
->identifier('composerPcre.maybeUnsafeStrictGroups')
->build(),
];
}
if (count($matchedType->getConstantArrays()) === 1) {
$matchedType = $matchedType->getConstantArrays()[0];
$nullableGroups = [];
foreach ($matchedType->getValueTypes() as $index => $type) {
if (TypeCombinator::containsNull($type)) {
$nullableGroups[] = $matchedType->getKeyTypes()[$index]->getValue();
}
}
if (\count($nullableGroups) > 0) {
return [
RuleErrorBuilder::message(sprintf(
'The %s call is unsafe as match group%s "%s" %s optional and may be null.',
$node->name->name,
\count($nullableGroups) > 1 ? 's' : '',
implode('", "', $nullableGroups),
\count($nullableGroups) > 1 ? 'are' : 'is'
))->identifier('composerPcre.unsafeStrictGroups')->build(),
];
}
}
return [];
}
}
......@@ -43,6 +43,7 @@ class Regex
*/
public static function matchStrictGroups(string $pattern, string $subject, int $flags = 0, int $offset = 0): MatchStrictGroupsResult
{
// @phpstan-ignore composerPcre.maybeUnsafeStrictGroups
$count = Preg::matchStrictGroups($pattern, $subject, $matches, $flags, $offset);
return new MatchStrictGroupsResult($count, $matches);
......@@ -87,6 +88,7 @@ class Regex
self::checkOffsetCapture($flags, 'matchAllWithOffsets');
self::checkSetOrder($flags);
// @phpstan-ignore composerPcre.maybeUnsafeStrictGroups
$count = Preg::matchAllStrictGroups($pattern, $subject, $matches, $flags, $offset);
return new MatchAllStrictGroupsResult($count, $matches);
......
......@@ -51,16 +51,12 @@ class BaseTestCase extends TestCase
// Only use a message if the error can be reliably determined
if (PHP_VERSION_ID >= 80000) {
$error = 'Internal error';
} elseif (PHP_VERSION_ID >= 70201) {
} else {
$error = 'PREG_INTERNAL_ERROR';
}
}
if (null !== $error) {
$message = sprintf('%s: failed executing "%s": %s', $this->pregFunction, $pattern, $error);
} else {
$message = null;
}
$message = sprintf('%s: failed executing "%s": %s', $this->pregFunction, $pattern, $error);
$this->doExpectException('Composer\Pcre\PcreException', $message);
}
......
<?php
/*
* This file is part of composer/pcre.
*
* (c) Composer <https://github.com/composer>
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Composer\Pcre\PHPStanTests;
use PHPStan\Testing\RuleTestCase;
use Composer\Pcre\PHPStan\InvalidRegexPatternRule;
use PHPStan\Type\Php\RegexArrayShapeMatcher;
/**
* Run with "vendor/bin/phpunit --testsuite phpstan"
*
* This is excluded by default to avoid side effects with the library tests
*
* @extends RuleTestCase<InvalidRegexPatternRule>
*/
class InvalidRegexPatternRuleTest extends RuleTestCase
{
protected function getRule(): \PHPStan\Rules\Rule
{
return new InvalidRegexPatternRule();
}
public function testRule(): void
{
$missing = PHP_VERSION_ID < 70300 ? ')' : 'closing parenthesis';
$this->analyse([__DIR__ . '/fixtures/invalid-patterns.php'], [
[
'Regex pattern is invalid: Compilation failed: missing '.$missing.' at offset 1',
11,
],
[
'Regex pattern is invalid: Compilation failed: missing '.$missing.' at offset 1',
13,
],
[
'Regex pattern is invalid: Compilation failed: missing '.$missing.' at offset 1',
15,
],
[
'Regex pattern is invalid: Compilation failed: missing '.$missing.' at offset 1',
17,
],
[
'Regex pattern is invalid: Compilation failed: missing '.$missing.' at offset 1',
19,
],
[
'Regex pattern is invalid: Compilation failed: missing '.$missing.' at offset 1',
21,
],
]);
}
public static function getAdditionalConfigFiles(): array
{
return [
'phar://' . __DIR__ . '/../../vendor/phpstan/phpstan/phpstan.phar/conf/bleedingEdge.neon',
__DIR__ . '/../../extension.neon',
];
}
}
<?php
/*
* This file is part of composer/pcre.
*
* (c) Composer <https://github.com/composer>
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Composer\Pcre\PHPStanTests;
use PHPStan\Testing\TypeInferenceTestCase;
/**
* Run with "vendor/bin/phpunit --testsuite phpstan"
*
* This is excluded by default to avoid side effects with the library tests
*/
class TypeInferenceTest extends TypeInferenceTestCase
{
/**
* @return iterable<mixed>
*/
public function dataFileAsserts(): iterable
{
yield from $this->gatherAssertTypesFromDirectory(__DIR__ . '/nsrt');
}
/**
* @dataProvider dataFileAsserts
* @param mixed ...$args
*/
public function testFileAsserts(
string $assertType,
string $file,
...$args
): void
{
$this->assertFileAsserts($assertType, $file, ...$args);
}
public static function getAdditionalConfigFiles(): array
{
return [
'phar://' . __DIR__ . '/../../vendor/phpstan/phpstan/phpstan.phar/conf/bleedingEdge.neon',
__DIR__ . '/../../extension.neon',
];
}
}
<?php
/*
* This file is part of composer/pcre.
*
* (c) Composer <https://github.com/composer>
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Composer\Pcre\PHPStanTests;
use PHPStan\Testing\RuleTestCase;
use Composer\Pcre\PHPStan\UnsafeStrictGroupsCallRule;
use PHPStan\Type\Php\RegexArrayShapeMatcher;
/**
* Run with "vendor/bin/phpunit --testsuite phpstan"
*
* This is excluded by default to avoid side effects with the library tests
*
* @extends RuleTestCase<UnsafeStrictGroupsCallRule>
*/
class UnsafeStrictGroupsCallRuleTest extends RuleTestCase
{
protected function getRule(): \PHPStan\Rules\Rule
{
return new UnsafeStrictGroupsCallRule(self::getContainer()->getByType(RegexArrayShapeMatcher::class));
}
public function testRule(): void
{
$this->analyse([__DIR__ . '/nsrt/preg-match.php'], [
[
'The matchStrictGroups call is unsafe as match group "1" is optional and may be null.',
80,
],
[
'The matchAllStrictGroups call is unsafe as match groups "foo", "2" are optional and may be null.',
82,
],
[
'The isMatchStrictGroups call is potentially unsafe as $matches\' type could not be inferred.',
86,
],
]);
}
public static function getAdditionalConfigFiles(): array
{
return [
'phar://' . __DIR__ . '/../../vendor/phpstan/phpstan/phpstan.phar/conf/bleedingEdge.neon',
__DIR__ . '/../../extension.neon',
];
}
}
<?php
namespace PregMatchShapes;
use Composer\Pcre\Preg;
use Composer\Pcre\Regex;
use function PHPStan\Testing\assertType;
function doMatch(string $s): void
{
Preg::match('/(/i', $s, $matches);
Regex::isMatch('/(/i', $s);
Preg::grep('/(/i', [$s]);
Preg::replaceCallback('/(/i', function ($match) { return ''; }, $s);
Preg::replaceCallback(['/(/i', '{}'], function ($match) { return ''; }, $s);
Preg::replaceCallbackArray(['/(/i' => function ($match) { return ''; }], $s);
}
<?php
namespace PregMatchShapes;
use Composer\Pcre\Preg;
use Composer\Pcre\Regex;
use function PHPStan\Testing\assertType;
function doMatch(string $s): void
{
if (Preg::match('/Price: /i', $s, $matches)) {
assertType('array{string}', $matches);
} else {
assertType('array{}', $matches);
}
assertType('array{}|array{string}', $matches);
if (Preg::match('/Price: (£|€)\d+/', $s, $matches)) {
assertType('array{string, non-empty-string}', $matches);
} else {
assertType('array{}', $matches);
}
assertType('array{}|array{string, non-empty-string}', $matches);
if (Preg::match('/Price: (£|€)?\d+/', $s, $matches)) {
assertType('array{string, non-empty-string|null}', $matches);
} else {
assertType('array{}', $matches);
}
assertType('array{}|array{string, non-empty-string|null}', $matches);
// passing the PREG_UNMATCHED_AS_NULL should change nothing compared to above as it is always set
if (Preg::match('/Price: (£|€)?\d+/', $s, $matches, PREG_UNMATCHED_AS_NULL)) {
assertType('array{string, non-empty-string|null}', $matches);
} else {
assertType('array{}', $matches);
}
assertType('array{}|array{string, non-empty-string|null}', $matches);
if (Preg::isMatch('/Price: (?<currency>£|€)\d+/', $s, $matches)) {
assertType('array{0: string, currency: non-empty-string, 1: non-empty-string}', $matches);
} else {
assertType('array{}', $matches);
}
assertType('array{}|array{0: string, currency: non-empty-string, 1: non-empty-string}', $matches);
}
function doMatchStrictGroups(string $s): void
{
if (Preg::matchStrictGroups('/Price: /i', $s, $matches)) {
assertType('array{string}', $matches);
} else {
assertType('array{}', $matches);
}
assertType('array{}|array{string}', $matches);
if (Preg::matchStrictGroups('/Price: (£|€)\d+/', $s, $matches)) {
assertType('array{string, non-empty-string}', $matches);
} else {
assertType('array{}', $matches);
}
assertType('array{}|array{string, non-empty-string}', $matches);
if (Preg::isMatchStrictGroups('/Price: (?<test>£|€)\d+/', $s, $matches)) {
assertType('array{0: string, test: non-empty-string, 1: non-empty-string}', $matches);
} else {
assertType('array{}', $matches);
}
assertType('array{}|array{0: string, test: non-empty-string, 1: non-empty-string}', $matches);
}
function doMatchStrictGroupsUnsafe(string $s): void
{
if (Preg::isMatchStrictGroups('{Configure Command(?: *</td><td class="v">| *=> *)(.*)(?:</td>|$)}m', $s, $matches)) {
// does not error because the match group might be empty but is not optional
assertType('array{string, string}', $matches);
}
// should error as it is unsafe due to the optional group 1
Regex::matchStrictGroups('{Configure Command(?: *</td><td class="v">| *=> *)(.*)?(?:</td>|$)}m', $s);
if (Preg::matchAllStrictGroups('{((?<foo>.)?z)}m', $s, $matches)) {
// should error as it is unsafe due to the optional group foo/2
}
if (Preg::isMatchStrictGroups('{'.$s.'}', $s, $matches)) {
// should error as it is unsafe due not being introspectable with the dynamic string
}
}
// disabled until https://github.com/phpstan/phpstan-src/pull/3185 can be resolved
//
//function identicalMatch(string $s): void
//{
// if (Preg::match('/Price: /i', $s, $matches) === 1) {
// assertType('array{string}', $matches);
// } else {
// assertType('array{}', $matches);
// }
// assertType('array{}|array{string}', $matches);
//}
//
//function equalMatch(string $s): void
//{
// if (Preg::match('/Price: /i', $s, $matches) == 1) {
// assertType('array{string}', $matches);
// } else {
// assertType('array{}', $matches);
// }
// assertType('array{}|array{string}', $matches);
//}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment