Skip to content

Commit

Permalink
RegexArrayShapeMatcher - Support preg_quote()'d patterns
Browse files Browse the repository at this point in the history
  • Loading branch information
staabm authored Jul 14, 2024
1 parent 0f3622a commit 062d8a0
Show file tree
Hide file tree
Showing 4 changed files with 114 additions and 4 deletions.
3 changes: 1 addition & 2 deletions src/Type/Php/PregMatchParameterOutTypeExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -41,13 +41,12 @@ public function getParameterOutTypeFromFunctionCall(FunctionReflection $function
return null;
}

$patternType = $scope->getType($patternArg->value);
$flagsType = null;
if ($flagsArg !== null) {
$flagsType = $scope->getType($flagsArg->value);
}

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

}
3 changes: 1 addition & 2 deletions src/Type/Php/PregMatchTypeSpecifyingExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -48,13 +48,12 @@ public function specifyTypes(FunctionReflection $functionReflection, FuncCall $n
return new SpecifiedTypes();
}

$patternType = $scope->getType($patternArg->value);
$flagsType = null;
if ($flagsArg !== null) {
$flagsType = $scope->getType($flagsArg->value);
}

$matchedType = $this->regexShapeMatcher->matchType($patternType, $flagsType, TrinaryLogic::createFromBoolean($context->true()));
$matchedType = $this->regexShapeMatcher->matchExpr($patternArg->value, $flagsType, TrinaryLogic::createFromBoolean($context->true()), $scope);
if ($matchedType === null) {
return new SpecifiedTypes();
}
Expand Down
68 changes: 68 additions & 0 deletions src/Type/Php/RegexArrayShapeMatcher.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@
use Hoa\Compiler\Llk\TreeNode;
use Hoa\Exception\Exception;
use Hoa\File\Read;
use PhpParser\Node\Expr;
use PhpParser\Node\Name;
use PHPStan\Analyser\Scope;
use PHPStan\Php\PhpVersion;
use PHPStan\TrinaryLogic;
use PHPStan\Type\Constant\ConstantArrayType;
Expand Down Expand Up @@ -45,7 +48,20 @@ public function __construct(
{
}

public function matchExpr(Expr $patternExpr, ?Type $flagsType, TrinaryLogic $wasMatched, Scope $scope): ?Type
{
return $this->matchPatternType($this->getPatternType($patternExpr, $scope), $flagsType, $wasMatched);
}

/**
* @deprecated use matchExpr() instead for a more precise result
*/
public function matchType(Type $patternType, ?Type $flagsType, TrinaryLogic $wasMatched): ?Type
{
return $this->matchPatternType($patternType, $flagsType, $wasMatched);
}

private function matchPatternType(Type $patternType, ?Type $flagsType, TrinaryLogic $wasMatched): ?Type
{
if ($wasMatched->no()) {
return new ConstantArrayType([], []);
Expand Down Expand Up @@ -484,4 +500,56 @@ private function walkRegexAst(
}
}

private function getPatternType(Expr $patternExpr, Scope $scope): Type
{
if ($patternExpr instanceof Expr\BinaryOp\Concat) {
return $this->resolvePatternConcat($patternExpr, $scope);
}

return $scope->getType($patternExpr);
}

/**
* Ignores preg_quote() calls in the concatenation as these are not relevant for array-shape matching.
*
* This assumption only works for the ArrayShapeMatcher therefore it is not implemented for the common case in Scope.
*
* see https://github.com/phpstan/phpstan-src/pull/3233#discussion_r1676938085
*/
private function resolvePatternConcat(Expr\BinaryOp\Concat $concat, Scope $scope): Type
{
if (
$concat->left instanceof Expr\FuncCall
&& $concat->left->name instanceof Name
&& $concat->left->name->toLowerString() === 'preg_quote'
) {
$left = new ConstantStringType('');
} elseif ($concat->left instanceof Expr\BinaryOp\Concat) {
$left = $this->resolvePatternConcat($concat->left, $scope);
} else {
$left = $scope->getType($concat->left);
}

if (
$concat->right instanceof Expr\FuncCall
&& $concat->right->name instanceof Name
&& $concat->right->name->toLowerString() === 'preg_quote'
) {
$right = new ConstantStringType('');
} elseif ($concat->right instanceof Expr\BinaryOp\Concat) {
$right = $this->resolvePatternConcat($concat->right, $scope);
} else {
$right = $scope->getType($concat->right);
}

$strings = [];
foreach ($left->getConstantStrings() as $leftString) {
foreach ($right->getConstantStrings() as $rightString) {
$strings[] = new ConstantStringType($leftString->getValue() . $rightString->getValue());
}
}

return TypeCombinator::union(...$strings);
}

}
44 changes: 44 additions & 0 deletions tests/PHPStan/Analyser/nsrt/preg_match_shapes.php
Original file line number Diff line number Diff line change
Expand Up @@ -393,3 +393,47 @@ function unmatchedAsNullWithMandatoryGroup(string $s): void {
assertType('array{}|array{0: string, currency: string, 1: string}', $matches);
}

function (string $s): void {
if (preg_match('{' . preg_quote('xxx') . '(z)}', $s, $matches)) {
assertType('array{string, string}', $matches);
} else {
assertType('array{}', $matches);
}
assertType('array{}|array{string, string}', $matches);
};

function (string $s): void {
if (preg_match('{' . preg_quote($s) . '(z)}', $s, $matches)) {
assertType('array{string, string}', $matches);
} else {
assertType('array{}', $matches);
}
assertType('array{}|array{string, string}', $matches);
};

function (string $s): void {
if (preg_match('/' . preg_quote($s, '/') . '(\d)/', $s, $matches)) {
assertType('array{string, string}', $matches);
} else {
assertType('array{}', $matches);
}
assertType('array{}|array{string, string}', $matches);
};

function (string $s): void {
if (preg_match('{' . preg_quote($s) . '(z)' . preg_quote($s) . '(?:abc)(def)?}', $s, $matches)) {
assertType('array{0: string, 1: string, 2?: string}', $matches);
} else {
assertType('array{}', $matches);
}
assertType('array{}|array{0: string, 1: string, 2?: string}', $matches);
};

function (string $s, $mixed): void {
if (preg_match('{' . preg_quote($s) . '(z)' . preg_quote($s) . '(?:abc)'. $mixed .'(def)?}', $s, $matches)) {
assertType('array<string>', $matches);
} else {
assertType('array{}', $matches);
}
assertType('array<string>', $matches);
};

0 comments on commit 062d8a0

Please sign in to comment.