Skip to content

Implement PregGrepDynamicReturnTypeExtension #2560

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions conf/config.neon
Original file line number Diff line number Diff line change
Expand Up @@ -1716,6 +1716,14 @@ services:
tags:
- phpstan.broker.dynamicStaticMethodReturnTypeExtension

-
class: PHPStan\Type\Php\PregMatchingDynamicReturnTypeExtension
tags:
- phpstan.broker.dynamicFunctionReturnTypeExtension

-
class: PHPStan\Rules\Regexp\RegularExpressionHelper

-
class: PHPStan\Type\ClosureTypeFactory
arguments:
Expand Down
22 changes: 22 additions & 0 deletions src/Rules/Regexp/RegularExpressionHelper.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<?php declare(strict_types = 1);

namespace PHPStan\Rules\Regexp;

use Nette\Utils\RegexpException;
use Nette\Utils\Strings;

final class RegularExpressionHelper
{

public function validatePattern(string $pattern): ?string
{
try {
Strings::match('', $pattern);
} catch (RegexpException $e) {
return $e->getMessage();
}

return null;
}

}
21 changes: 7 additions & 14 deletions src/Rules/Regexp/RegularExpressionPatternRule.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@

namespace PHPStan\Rules\Regexp;

use Nette\Utils\RegexpException;
use Nette\Utils\Strings;
use PhpParser\Node;
use PhpParser\Node\Expr\FuncCall;
use PHPStan\Analyser\Scope;
Expand All @@ -20,6 +18,12 @@
class RegularExpressionPatternRule implements Rule
{

public function __construct(
private RegularExpressionHelper $regularExpressionHelper,
)
{
}

public function getNodeType(): string
{
return FuncCall::class;
Expand All @@ -31,7 +35,7 @@ public function processNode(Node $node, Scope $scope): array

$errors = [];
foreach ($patterns as $pattern) {
$errorMessage = $this->validatePattern($pattern);
$errorMessage = $this->regularExpressionHelper->validatePattern($pattern);
if ($errorMessage === null) {
continue;
}
Expand Down Expand Up @@ -110,15 +114,4 @@ private function extractPatterns(FuncCall $functionCall, Scope $scope): array
return $patternStrings;
}

private function validatePattern(string $pattern): ?string
{
try {
Strings::match('', $pattern);
} catch (RegexpException $e) {
return $e->getMessage();
}

return null;
}

}
59 changes: 59 additions & 0 deletions src/Type/Php/PregMatchingDynamicReturnTypeExtension.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
<?php declare(strict_types = 1);

namespace PHPStan\Type\Php;

use PhpParser\Node\Expr\FuncCall;
use PHPStan\Analyser\Scope;
use PHPStan\Reflection\FunctionReflection;
use PHPStan\Reflection\ParametersAcceptorSelector;
use PHPStan\Rules\Regexp\RegularExpressionHelper;
use PHPStan\Type\Constant\ConstantBooleanType;
use PHPStan\Type\DynamicFunctionReturnTypeExtension;
use PHPStan\Type\Type;
use PHPStan\Type\TypeCombinator;
use function count;
use function in_array;
use function strtolower;

class PregMatchingDynamicReturnTypeExtension implements DynamicFunctionReturnTypeExtension
{

public function __construct(
private RegularExpressionHelper $regularExpressionHelper,
)
{
}

public function isFunctionSupported(FunctionReflection $functionReflection): bool
{
return in_array(strtolower($functionReflection->getName()), ['preg_grep', 'preg_match', 'preg_match_all'], true);
}

public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): ?Type
{
$args = $functionCall->getArgs();
if (count($args) < 1) {
return null;
}

$patternType = $scope->getType($args[0]->value);
$constantStrings = $patternType->getConstantStrings();
if (count($constantStrings) === 0) {
return null;
}

foreach ($constantStrings as $constantString) {
if ($this->regularExpressionHelper->validatePattern($constantString->getValue()) !== null) {
return null;
}
}

$defaultReturn = ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType();

return TypeCombinator::remove(
$defaultReturn,
new ConstantBooleanType(false),
);
}

}
29 changes: 27 additions & 2 deletions src/Type/Php/PregSplitDynamicReturnTypeExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
use PHPStan\Analyser\Scope;
use PHPStan\Reflection\FunctionReflection;
use PHPStan\Reflection\ParametersAcceptorSelector;
use PHPStan\Rules\Regexp\RegularExpressionHelper;
use PHPStan\TrinaryLogic;
use PHPStan\Type\Accessory\AccessoryArrayListType;
use PHPStan\Type\ArrayType;
Expand All @@ -19,13 +20,15 @@
use PHPStan\Type\StringType;
use PHPStan\Type\Type;
use PHPStan\Type\TypeCombinator;
use function count;
use function strtolower;

class PregSplitDynamicReturnTypeExtension implements DynamicFunctionReturnTypeExtension
{

public function __construct(
private BitwiseFlagHelper $bitwiseFlagAnalyser,
private RegularExpressionHelper $regularExpressionHelper,
)
{
}
Expand All @@ -44,10 +47,32 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection,
new IntegerType(),
new ConstantArrayType([new ConstantIntegerType(0), new ConstantIntegerType(1)], [new StringType(), IntegerRangeType::fromInterval(0, null)], [2], [], TrinaryLogic::createYes()),
);
return TypeCombinator::union(AccessoryArrayListType::intersectWith($type), new ConstantBooleanType(false));
$returnType = TypeCombinator::union(AccessoryArrayListType::intersectWith($type), new ConstantBooleanType(false));
} else {
$returnType = ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType();
}

return ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType();
$patternArg = $functionCall->getArgs()[0] ?? null;
if ($patternArg === null) {
return $returnType;
}

$patternType = $scope->getType($patternArg->value);
$constantStrings = $patternType->getConstantStrings();
if (count($constantStrings) === 0) {
return $returnType;
}

foreach ($constantStrings as $constantString) {
if ($this->regularExpressionHelper->validatePattern($constantString->getValue()) !== null) {
return $returnType;
}
}

return TypeCombinator::remove(
$returnType,
new ConstantBooleanType(false),
);
}

}
19 changes: 5 additions & 14 deletions tests/PHPStan/Analyser/AnalyserIntegrationTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@
use PHPStan\Type\Constant\ConstantStringType;
use function extension_loaded;
use function restore_error_handler;
use function sprintf;
use const PHP_VERSION_ID;

class AnalyserIntegrationTest extends PHPStanTestCase
Expand Down Expand Up @@ -835,13 +834,11 @@ public function testOffsetAccess(): void
public function testUnresolvableParameter(): void
{
$errors = $this->runAnalyse(__DIR__ . '/data/unresolvable-parameter.php');
$this->assertCount(3, $errors);
$this->assertSame('Parameter #2 $array of function array_map expects array, array<int, string>|false given.', $errors[0]->getMessage());
$this->assertSame(18, $errors[0]->getLine());
$this->assertSame('Method UnresolvableParameter\Collection::pipeInto() has parameter $class with no type specified.', $errors[1]->getMessage());
$this->assertCount(2, $errors);
$this->assertSame('Method UnresolvableParameter\Collection::pipeInto() has parameter $class with no type specified.', $errors[0]->getMessage());
$this->assertSame(30, $errors[0]->getLine());
$this->assertSame('PHPDoc tag @param for parameter $class contains unresolvable type.', $errors[1]->getMessage());
$this->assertSame(30, $errors[1]->getLine());
$this->assertSame('PHPDoc tag @param for parameter $class contains unresolvable type.', $errors[2]->getMessage());
$this->assertSame(30, $errors[2]->getLine());
}

public function testBug7248(): void
Expand Down Expand Up @@ -883,13 +880,7 @@ public function testBug7500(): void
public function testBug7554(): void
{
$errors = $this->runAnalyse(__DIR__ . '/data/bug-7554.php');
$this->assertCount(2, $errors);

$this->assertSame(sprintf('Parameter #1 $%s of function count expects array|Countable, array<int, array<int, int|string>>|false given.', PHP_VERSION_ID < 80000 ? 'var' : 'value'), $errors[0]->getMessage());
$this->assertSame(26, $errors[0]->getLine());

$this->assertSame('Cannot access offset int<1, max> on list<array{string, int<0, max>}>|false.', $errors[1]->getMessage());
$this->assertSame(27, $errors[1]->getLine());
$this->assertNoErrors($errors);
}

public function testBug7637(): void
Expand Down
5 changes: 5 additions & 0 deletions tests/PHPStan/Analyser/NodeScopeResolverTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -1325,6 +1325,11 @@ public function dataFileAsserts(): iterable
yield from $this->gatherAssertTypes(__DIR__ . '/data/trigger-error-php7.php');
}

if (PHP_VERSION_ID >= 80000) {
yield from $this->gatherAssertTypes(__DIR__ . '/data/preg_matching_php8.php');
} else {
yield from $this->gatherAssertTypes(__DIR__ . '/data/preg_matching_php7.php');
}
yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-7915.php');
yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-9714.php');
}
Expand Down
4 changes: 2 additions & 2 deletions tests/PHPStan/Analyser/data/preg_match_php7.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

class Foo {
public function doFoo() {
assertType('0|1|false', preg_match('{}', ''));
assertType('int<0, max>|false|null', preg_match_all('{}', ''));
assertType('0|1', preg_match('{}', ''));
assertType('int<0, max>|null', preg_match_all('{}', ''));
}
}
4 changes: 2 additions & 2 deletions tests/PHPStan/Analyser/data/preg_match_php8.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@

class Foo {
public function doFoo() {
assertType('0|1|false', preg_match('{}', ''));
assertType('int<0, max>|false', preg_match_all('{}', ''));
assertType('0|1', preg_match('{}', ''));
assertType('int<0, max>', preg_match_all('{}', ''));

}
}
37 changes: 37 additions & 0 deletions tests/PHPStan/Analyser/data/preg_matching_php7.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
<?php

namespace PregMatching;

use function PHPStan\Testing\assertType;

function compilablePattern(array $a) {
assertType('array', preg_grep('/^[0-9]+$/', $a));
assertType('0|1', preg_match('/^[0-9]+$/', $a));
assertType('int<0, max>|null', preg_match_all('/^[0-9]+$/', $a));
assertType('list<string>', preg_split('/^[0-9]+$/', $a));
}

function bogusPattern(array $a) {
assertType('array|false', preg_grep('/bogus-pattern-[0^-9]$/', $a));
assertType('0|1|false', preg_match('/bogus-pattern-[0^-9]$/', $a));
assertType('int<0, max>|false|null', preg_match_all('/bogus-pattern-[0^-9]$/', $a));
assertType('list<string>|false', preg_split('/bogus-pattern-[0^-9]$/', $a));
}

function unknownPattern(string $p, array $a) {
assertType('array|false', preg_grep($p, $a));
assertType('0|1|false', preg_match($p, $a));
assertType('int<0, max>|false|null', preg_match_all($p, $a));
assertType('list<string>|false', preg_split($p, $a));
}

function sometimesCompilablePattern(array $a) {
$p = '/^[0-9]+$/';
if (rand(0,1)) {
$p = '/bogus-pattern-[0^-9]$/';
}
assertType('array|false', preg_grep($p, $a));
assertType('0|1|false', preg_match($p, $a));
assertType('int<0, max>|false|null', preg_match_all($p, $a));
assertType('list<string>|false', preg_split($p, $a));
}
37 changes: 37 additions & 0 deletions tests/PHPStan/Analyser/data/preg_matching_php8.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
<?php

namespace PregMatching;

use function PHPStan\Testing\assertType;

function compilablePattern(array $a) {
assertType('array', preg_grep('/^[0-9]+$/', $a));
assertType('0|1', preg_match('/^[0-9]+$/', $a));
assertType('int<0, max>', preg_match_all('/^[0-9]+$/', $a));
assertType('list<string>', preg_split('/^[0-9]+$/', $a));
}

function bogusPattern(array $a) {
assertType('array|false', preg_grep('/bogus-pattern-[0^-9]$/', $a));
assertType('0|1|false', preg_match('/bogus-pattern-[0^-9]$/', $a));
assertType('int<0, max>|false', preg_match_all('/bogus-pattern-[0^-9]$/', $a));
assertType('list<string>|false', preg_split('/bogus-pattern-[0^-9]$/', $a));
}

function unknownPattern(string $p, array $a) {
assertType('array|false', preg_grep($p, $a));
assertType('0|1|false', preg_match($p, $a));
assertType('int<0, max>|false', preg_match_all($p, $a));
assertType('list<string>|false', preg_split($p, $a));
}

function sometimesCompilablePattern(array $a) {
$p = '/^[0-9]+$/';
if (rand(0,1)) {
$p = '/bogus-pattern-[0^-9]$/';
}
assertType('array|false', preg_grep($p, $a));
assertType('0|1|false', preg_match($p, $a));
assertType('int<0, max>|false', preg_match_all($p, $a));
assertType('list<string>|false', preg_split($p, $a));
}
8 changes: 4 additions & 4 deletions tests/PHPStan/Analyser/data/preg_split.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,10 @@ class HelloWorld
{
public function doFoo()
{
assertType('list<string>|false', preg_split('/-/', '1-2-3'));
assertType('list<string>|false', preg_split('/-/', '1-2-3', -1, PREG_SPLIT_NO_EMPTY));
assertType('list<array{string, int<0, max>}>|false', preg_split('/-/', '1-2-3', -1, PREG_SPLIT_OFFSET_CAPTURE));
assertType('list<array{string, int<0, max>}>|false', preg_split('/-/', '1-2-3', -1, PREG_SPLIT_NO_EMPTY | PREG_SPLIT_OFFSET_CAPTURE));
assertType('list<string>', preg_split('/-/', '1-2-3'));
assertType('list<string>', preg_split('/-/', '1-2-3', -1, PREG_SPLIT_NO_EMPTY));
assertType('list<array{string, int<0, max>}>', preg_split('/-/', '1-2-3', -1, PREG_SPLIT_OFFSET_CAPTURE));
assertType('list<array{string, int<0, max>}>', preg_split('/-/', '1-2-3', -1, PREG_SPLIT_NO_EMPTY | PREG_SPLIT_OFFSET_CAPTURE));
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ class RegularExpressionPatternRuleTest extends RuleTestCase

protected function getRule(): Rule
{
return new RegularExpressionPatternRule();
return new RegularExpressionPatternRule(new RegularExpressionHelper());
}

public function testValidRegexPatternBefore73(): void
Expand Down