Skip to content

Commit

Permalink
Support nette 4 for Strings::replace + support regexp array shapes fo…
Browse files Browse the repository at this point in the history
…r Strings::replace callback
  • Loading branch information
kamil-zacek committed Aug 12, 2024
1 parent f41257b commit 83f2356
Show file tree
Hide file tree
Showing 5 changed files with 159 additions and 0 deletions.
5 changes: 5 additions & 0 deletions extension.neon
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,8 @@ conditionalTags:
phpstan.broker.dynamicStaticMethodReturnTypeExtension: %featureToggles.narrowPregMatches%
PHPStan\Type\Nette\StringsMatchAllDynamicReturnTypeExtension:
phpstan.broker.dynamicStaticMethodReturnTypeExtension: %featureToggles.narrowPregMatches%
PHPStan\Type\Nette\StringsReplaceCallbackClosureTypeExtension:
phpstan.staticMethodParameterClosureTypeExtension: %featureToggles.narrowPregMatches%

services:
-
Expand Down Expand Up @@ -126,3 +128,6 @@ services:

-
class: PHPStan\Type\Nette\StringsMatchDynamicReturnTypeExtension

-
class: PHPStan\Type\Nette\StringsReplaceCallbackClosureTypeExtension
6 changes: 6 additions & 0 deletions phpstan-baseline.neon
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
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/Type/Nette/StringsReplaceCallbackClosureTypeExtension.php
1 change: 1 addition & 0 deletions phpstan.neon
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ includes:
- vendor/phpstan/phpstan-phpunit/rules.neon
- vendor/phpstan/phpstan-strict-rules/rules.neon
- vendor/phpstan/phpstan/conf/bleedingEdge.neon
- phpstan-baseline.neon

parameters:
excludePaths:
Expand Down
97 changes: 97 additions & 0 deletions src/Type/Nette/StringsReplaceCallbackClosureTypeExtension.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
<?php declare(strict_types = 1);

namespace PHPStan\Type\Nette;

use Nette\Utils\Strings;
use PhpParser\Node\Arg;
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\ConstantBooleanType;
use PHPStan\Type\Constant\ConstantIntegerType;
use PHPStan\Type\Php\RegexArrayShapeMatcher;
use PHPStan\Type\StaticMethodParameterClosureTypeExtension;
use PHPStan\Type\StringType;
use PHPStan\Type\Type;
use function array_key_exists;
use const PREG_OFFSET_CAPTURE;
use const PREG_UNMATCHED_AS_NULL;

final class StringsReplaceCallbackClosureTypeExtension implements StaticMethodParameterClosureTypeExtension
{

/** @var RegexArrayShapeMatcher */
private $regexArrayShapeMatcher;

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

public function isStaticMethodSupported(MethodReflection $methodReflection, ParameterReflection $parameter): bool
{
return $methodReflection->getDeclaringClass()->getName() === Strings::class
&& $methodReflection->getName() === 'replace'
&& $parameter->getName() === 'replacement';
}

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

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

$replacementType = $scope->getType($args[2]->value);

if (!$replacementType->isCallable()->yes()) {
return null;
}

$matchesType = $this->regexArrayShapeMatcher->matchExpr(
$patternArg->value,
$this->resolveFlagsType($args, $scope),
TrinaryLogic::createYes(),
$scope
);

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

return new ClosureType(
[
new NativeParameterReflection(
$parameter->getName(),
$parameter->isOptional(),
$matchesType,
$parameter->passedByReference(),
$parameter->isVariadic(),
$parameter->getDefaultValue()
),
],
new StringType()
);
}

/**
* @param array<Arg> $args
*/
private function resolveFlagsType(array $args, Scope $scope): ConstantIntegerType
{
$captureOffsetType = array_key_exists(4, $args) ? $scope->getType($args[4]->value) : new ConstantBooleanType(false);
$unmatchedAsNullType = array_key_exists(5, $args) ? $scope->getType($args[5]->value) : new ConstantBooleanType(false);

$captureOffset = $captureOffsetType->isTrue()->yes();
$unmatchedAsNull = $unmatchedAsNullType->isTrue()->yes();

return new ConstantIntegerType(($captureOffset ? PREG_OFFSET_CAPTURE : 0) | ($unmatchedAsNull ? PREG_UNMATCHED_AS_NULL : 0));
}

}
50 changes: 50 additions & 0 deletions tests/Type/Nette/data/strings-match.php
Original file line number Diff line number Diff line change
Expand Up @@ -43,3 +43,53 @@ function (string $s): void {
$result = Strings::matchAll($lineContent, '~\[gallery ids=(„|")(?<allIds>([0-9]+,? ?)+)(“|")~');
assertType('list<array{0: string, 1: non-empty-string, allIds: non-empty-string, 2: non-empty-string, 3: non-empty-string, 4: non-empty-string}>', $result);
};

function (string $s): void {
Strings::replace(
$s,
'/(foo)?(bar)?(baz)?/',
function ($matches) {
assertType('array{string, \'foo\'|null, \'bar\'|null, \'baz\'|null}', $matches);
return '';
},
-1,
false,
true
);
};

function (string $s): void {
Strings::replace(
$s,
'/(foo)?(bar)?(baz)?/',
function ($matches) {
assertType('array{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);
return '';
},
-1,
true
);
};

function (string $s): void {
Strings::replace(
$s,
'/(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 '';
},
-1,
true,
true
);
};

function (string $s): void {
$result = Strings::replace(
$s,
'/(foo)?(bar)?(baz)?/',
'bee'
);
assertType('string', $result);
};

0 comments on commit 83f2356

Please sign in to comment.