Skip to content

Commit

Permalink
Merge branch '2.x'
Browse files Browse the repository at this point in the history
  • Loading branch information
Seldaek committed Aug 19, 2024
2 parents ea4ab6f + 06d0e49 commit 5cc3c12
Show file tree
Hide file tree
Showing 9 changed files with 252 additions and 15 deletions.
4 changes: 2 additions & 2 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,11 @@
},
"require-dev": {
"phpunit/phpunit": "^8 || ^9",
"phpstan/phpstan": "^1.11.8",
"phpstan/phpstan": "^1.11.10",
"phpstan/phpstan-strict-rules": "^1.1"
},
"conflict": {
"phpstan/phpstan": "<1.11.8"
"phpstan/phpstan": "<1.11.10"
},
"autoload": {
"psr-4": {
Expand Down
16 changes: 8 additions & 8 deletions extension.neon
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,19 @@
# 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
tags:
- phpstan.staticMethodParameterOutTypeExtension
-
class: Composer\Pcre\PHPStan\PregMatchTypeSpecifyingExtension
tags:
- phpstan.typeSpecifier.staticMethodTypeSpecifyingExtension
-
class: Composer\Pcre\PHPStan\PregReplaceCallbackClosureTypeExtension
tags:
- phpstan.staticMethodParameterClosureTypeExtension

rules:
- Composer\Pcre\PHPStan\UnsafeStrictGroupsCallRule
Expand Down
10 changes: 10 additions & 0 deletions phpstan-baseline.neon
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
parameters:
ignoreErrors:
-
message: "#^Creating new PHPStan\\\\Reflection\\\\Native\\\\NativeParameterReflection is not covered by backward compatibility promise\\. The class might change in a minor PHPStan version\\.$#"
count: 1
path: src/PHPStan/PregReplaceCallbackClosureTypeExtension.php

-
message: "#^Parameter \\#2 \\$callback of function preg_replace_callback expects callable\\(array\\<int\\|string, string\\>\\)\\: string, \\(callable\\(array\\<int\\|string, array\\{string\\|null, int\\<\\-1, max\\>\\}\\>\\)\\: string\\)\\|\\(callable\\(array\\<int\\|string, string\\|null\\>\\)\\: string\\) given\\.$#"
count: 1
Expand Down Expand Up @@ -50,6 +55,11 @@ parameters:
count: 2
path: tests/PregTests/ReplaceCallbackArrayTest.php

-
message: "#^Parameter \\#1 \\$string of function strtoupper expects string, string\\|null given\\.$#"
count: 2
path: tests/PregTests/ReplaceCallbackTest.php

-
message: "#^Regex pattern is invalid\\: No ending matching delimiter '\\}' found$#"
count: 2
Expand Down
4 changes: 2 additions & 2 deletions src/PHPStan/PregMatchFlags.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ 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);
return new ConstantIntegerType(PREG_UNMATCHED_AS_NULL);
}

$flagsType = $scope->getType($flagsArg->value);
Expand All @@ -30,7 +30,7 @@ static public function getType(?Arg $flagsArg, Scope $scope): ?Type
return null;
}

$internalFlagsTypes[] = new ConstantIntegerType($constantScalarValue | PREG_UNMATCHED_AS_NULL | RegexArrayShapeMatcher::PREG_UNMATCHED_AS_NULL_ON_72_73);
$internalFlagsTypes[] = new ConstantIntegerType($constantScalarValue | PREG_UNMATCHED_AS_NULL);
}
return TypeCombinator::union(...$internalFlagsTypes);
}
Expand Down
9 changes: 8 additions & 1 deletion src/PHPStan/PregMatchParameterOutTypeExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,10 @@ public function isStaticMethodSupported(MethodReflection $methodReflection, Para
{
return
$methodReflection->getDeclaringClass()->getName() === Preg::class
&& in_array($methodReflection->getName(), ['match', 'isMatch', 'matchStrictGroups', 'isMatchStrictGroups'], true)
&& in_array($methodReflection->getName(), [
'match', 'isMatch', 'matchStrictGroups', 'isMatchStrictGroups',
'matchAll', 'isMatchAll', 'matchAllStrictGroups', 'isMatchAllStrictGroups'
], true)
&& $parameter->getName() === 'matches';
}

Expand All @@ -52,6 +55,10 @@ public function getParameterOutTypeFromStaticMethodCall(MethodReflection $method
return null;
}

if (stripos($methodReflection->getName(), 'matchAll') !== false) {
return $this->regexShapeMatcher->matchAllExpr($patternArg->value, $flagsType, TrinaryLogic::createMaybe(), $scope);
}

return $this->regexShapeMatcher->matchExpr($patternArg->value, $flagsType, TrinaryLogic::createMaybe(), $scope);
}

Expand Down
13 changes: 11 additions & 2 deletions src/PHPStan/PregMatchTypeSpecifyingExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,11 @@ public function getClass(): string

public function isStaticMethodSupported(MethodReflection $methodReflection, StaticCall $node, TypeSpecifierContext $context): bool
{
return in_array($methodReflection->getName(), ['match', 'isMatch', 'matchStrictGroups', 'isMatchStrictGroups'], true) && !$context->null();
return in_array($methodReflection->getName(), [
'match', 'isMatch', 'matchStrictGroups', 'isMatchStrictGroups',
'matchAll', 'isMatchAll', 'matchAllStrictGroups', 'isMatchAllStrictGroups'
], true)
&& !$context->null();
}

public function specifyTypes(MethodReflection $methodReflection, StaticCall $node, Scope $scope, TypeSpecifierContext $context): SpecifiedTypes
Expand All @@ -67,7 +71,12 @@ public function specifyTypes(MethodReflection $methodReflection, StaticCall $nod
return new SpecifiedTypes();
}

$matchedType = $this->regexShapeMatcher->matchExpr($patternArg->value, $flagsType, TrinaryLogic::createFromBoolean($context->true()), $scope);
if (stripos($methodReflection->getName(), 'matchAll') !== false) {
$matchedType = $this->regexShapeMatcher->matchAllExpr($patternArg->value, $flagsType, TrinaryLogic::createFromBoolean($context->true()), $scope);
} else {
$matchedType = $this->regexShapeMatcher->matchExpr($patternArg->value, $flagsType, TrinaryLogic::createFromBoolean($context->true()), $scope);
}

if ($matchedType === null) {
return new SpecifiedTypes();
}
Expand Down
79 changes: 79 additions & 0 deletions src/PHPStan/PregReplaceCallbackClosureTypeExtension.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
<?php declare(strict_types=1);

namespace Composer\Pcre\PHPStan;

use Composer\Pcre\Preg;
use Composer\Pcre\Regex;
use PhpParser\Node\Expr\StaticCall;
use PHPStan\Analyser\Scope;
use PHPStan\Reflection\MethodReflection;
use PHPStan\Reflection\Native\NativeParameterReflection;
use PHPStan\Reflection\ParameterReflection;
use PHPStan\TrinaryLogic;
use PHPStan\Type\ClosureType;
use PHPStan\Type\Constant\ConstantArrayType;
use PHPStan\Type\Php\RegexArrayShapeMatcher;
use PHPStan\Type\StaticMethodParameterClosureTypeExtension;
use PHPStan\Type\StringType;
use PHPStan\Type\TypeCombinator;
use PHPStan\Type\Type;

final class PregReplaceCallbackClosureTypeExtension implements StaticMethodParameterClosureTypeExtension
{
/**
* @var RegexArrayShapeMatcher
*/
private $regexShapeMatcher;

public function __construct(RegexArrayShapeMatcher $regexShapeMatcher)
{
$this->regexShapeMatcher = $regexShapeMatcher;
}

public function isStaticMethodSupported(MethodReflection $methodReflection, ParameterReflection $parameter): bool
{
return in_array($methodReflection->getDeclaringClass()->getName(), [Preg::class, Regex::class], true)
&& in_array($methodReflection->getName(), ['replaceCallback', 'replaceCallbackStrictGroups'], true)
&& $parameter->getName() === 'replacement';
}

public function getTypeFromStaticMethodCall(MethodReflection $methodReflection, StaticCall $methodCall, ParameterReflection $parameter, Scope $scope): ?Type
{
$args = $methodCall->getArgs();
$patternArg = $args[0] ?? null;
$flagsArg = $args[5] ?? null;

if (
$patternArg === null
) {
return null;
}

$flagsType = PregMatchFlags::getType($flagsArg, $scope);

$matchesType = $this->regexShapeMatcher->matchExpr($patternArg->value, $flagsType, TrinaryLogic::createYes(), $scope);
if ($matchesType === null) {
return null;
}

if ($methodReflection->getName() === 'replaceCallbackStrictGroups' && count($matchesType->getConstantArrays()) === 1) {
$matchesType = $matchesType->getConstantArrays()[0];
$matchesType = new ConstantArrayType(
$matchesType->getKeyTypes(),
array_map(static function (Type $valueType): Type {
return TypeCombinator::removeNull($valueType);
}, $matchesType->getValueTypes()),
$matchesType->getNextAutoIndexes(),
[],
$matchesType->isList()
);
}

return new ClosureType(
[
new NativeParameterReflection($parameter->getName(), $parameter->isOptional(), $matchesType, $parameter->passedByReference(), $parameter->isVariadic(), $parameter->getDefaultValue()),
],
new StringType()
);
}
}
24 changes: 24 additions & 0 deletions tests/PHPStanTests/nsrt/preg-match.php
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,30 @@ function doMatchStrictGroupsUnsafe(string $s): void
}
}

function doMatchAllStrictGroups(string $s): void
{
if (Preg::matchAllStrictGroups('/Price: /i', $s, $matches)) {
assertType('array{list<string>}', $matches);
} else {
assertType('array{}', $matches);
}
assertType('array{}|array{list<string>}', $matches);

if (Preg::matchAllStrictGroups('/Price: (£|€)\d+/', $s, $matches)) {
assertType('array{list<string>, list<non-empty-string>}', $matches);
} else {
assertType('array{}', $matches);
}
assertType('array{}|array{list<string>, list<non-empty-string>}', $matches);

if (Preg::isMatchAllStrictGroups('/Price: (?<test>£|€)\d+/', $s, $matches)) {
assertType('array{0: list<string>, test: list<non-empty-string>, 1: list<non-empty-string>}', $matches);
} else {
assertType('array{}', $matches);
}
assertType('array{}|array{0: list<string>, test: list<non-empty-string>, 1: list<non-empty-string>}', $matches);
}

// disabled until https://github.com/phpstan/phpstan-src/pull/3185 can be resolved
//
//function identicalMatch(string $s): void
Expand Down
108 changes: 108 additions & 0 deletions tests/PHPStanTests/nsrt/preg-replace-callback.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
<?php

namespace PregMatchShapes;

use Composer\Pcre\Preg;
use Composer\Pcre\Regex;
use function PHPStan\Testing\assertType;

function (string $s): void {
Preg::replaceCallback(
$s,
function ($matches) {
assertType('array<int|string, string|null>', $matches);
return '';
},
$s
);

Regex::replaceCallback(
$s,
function ($matches) {
assertType('array<int|string, string|null>', $matches);
return '';
},
$s
);
};

function (string $s): void {
Preg::replaceCallback(
'|<p>(\s*)\w|',
function ($matches) {
assertType('array{string, string}', $matches);
return '';
},
$s
);
};

function (string $s): void {
Preg::replaceCallback(
'/(foo)?(bar)?(baz)?/',
function ($matches) {
assertType("array{string, 'foo'|null, 'bar'|null, 'baz'|null}", $matches);
return '';
},
$s,
-1,
$count,
PREG_UNMATCHED_AS_NULL
);
};

function (string $s): void {
Preg::replaceCallback(
'/(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);
return '';
},
$s,
-1,
$count,
PREG_OFFSET_CAPTURE
);
};

function (string $s): void {
Preg::replaceCallback(
'/(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);
return '';
},
$s,
-1,
$count,
PREG_OFFSET_CAPTURE|PREG_UNMATCHED_AS_NULL
);
};

function (string $s): void {
Preg::replaceCallbackStrictGroups(
'/(foo)?(bar)?(baz)?/',
function ($matches) {
assertType("array{string, 'foo', 'bar', 'baz'}", $matches);
return '';
},
$s,
-1,
$count
);
};

function (string $s): void {
Preg::replaceCallbackStrictGroups(
'/(foo)?(bar)?(baz)?/',
function ($matches) {
// should be array{array{string, int<-1, max>}, array{'foo', int<-1, max>}, array{'bar', int<-1, max>}, array{'baz', int<-1, max>}}
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);
return '';
},
$s,
-1,
$count,
PREG_OFFSET_CAPTURE
);
};

0 comments on commit 5cc3c12

Please sign in to comment.