Skip to content

Commit 062d8a0

Browse files
authored
RegexArrayShapeMatcher - Support preg_quote()'d patterns
1 parent 0f3622a commit 062d8a0

File tree

4 files changed

+114
-4
lines changed

4 files changed

+114
-4
lines changed

src/Type/Php/PregMatchParameterOutTypeExtension.php

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,13 +41,12 @@ public function getParameterOutTypeFromFunctionCall(FunctionReflection $function
4141
return null;
4242
}
4343

44-
$patternType = $scope->getType($patternArg->value);
4544
$flagsType = null;
4645
if ($flagsArg !== null) {
4746
$flagsType = $scope->getType($flagsArg->value);
4847
}
4948

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

5352
}

src/Type/Php/PregMatchTypeSpecifyingExtension.php

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,13 +48,12 @@ public function specifyTypes(FunctionReflection $functionReflection, FuncCall $n
4848
return new SpecifiedTypes();
4949
}
5050

51-
$patternType = $scope->getType($patternArg->value);
5251
$flagsType = null;
5352
if ($flagsArg !== null) {
5453
$flagsType = $scope->getType($flagsArg->value);
5554
}
5655

57-
$matchedType = $this->regexShapeMatcher->matchType($patternType, $flagsType, TrinaryLogic::createFromBoolean($context->true()));
56+
$matchedType = $this->regexShapeMatcher->matchExpr($patternArg->value, $flagsType, TrinaryLogic::createFromBoolean($context->true()), $scope);
5857
if ($matchedType === null) {
5958
return new SpecifiedTypes();
6059
}

src/Type/Php/RegexArrayShapeMatcher.php

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@
77
use Hoa\Compiler\Llk\TreeNode;
88
use Hoa\Exception\Exception;
99
use Hoa\File\Read;
10+
use PhpParser\Node\Expr;
11+
use PhpParser\Node\Name;
12+
use PHPStan\Analyser\Scope;
1013
use PHPStan\Php\PhpVersion;
1114
use PHPStan\TrinaryLogic;
1215
use PHPStan\Type\Constant\ConstantArrayType;
@@ -45,7 +48,20 @@ public function __construct(
4548
{
4649
}
4750

51+
public function matchExpr(Expr $patternExpr, ?Type $flagsType, TrinaryLogic $wasMatched, Scope $scope): ?Type
52+
{
53+
return $this->matchPatternType($this->getPatternType($patternExpr, $scope), $flagsType, $wasMatched);
54+
}
55+
56+
/**
57+
* @deprecated use matchExpr() instead for a more precise result
58+
*/
4859
public function matchType(Type $patternType, ?Type $flagsType, TrinaryLogic $wasMatched): ?Type
60+
{
61+
return $this->matchPatternType($patternType, $flagsType, $wasMatched);
62+
}
63+
64+
private function matchPatternType(Type $patternType, ?Type $flagsType, TrinaryLogic $wasMatched): ?Type
4965
{
5066
if ($wasMatched->no()) {
5167
return new ConstantArrayType([], []);
@@ -484,4 +500,56 @@ private function walkRegexAst(
484500
}
485501
}
486502

503+
private function getPatternType(Expr $patternExpr, Scope $scope): Type
504+
{
505+
if ($patternExpr instanceof Expr\BinaryOp\Concat) {
506+
return $this->resolvePatternConcat($patternExpr, $scope);
507+
}
508+
509+
return $scope->getType($patternExpr);
510+
}
511+
512+
/**
513+
* Ignores preg_quote() calls in the concatenation as these are not relevant for array-shape matching.
514+
*
515+
* This assumption only works for the ArrayShapeMatcher therefore it is not implemented for the common case in Scope.
516+
*
517+
* see https://github.com/phpstan/phpstan-src/pull/3233#discussion_r1676938085
518+
*/
519+
private function resolvePatternConcat(Expr\BinaryOp\Concat $concat, Scope $scope): Type
520+
{
521+
if (
522+
$concat->left instanceof Expr\FuncCall
523+
&& $concat->left->name instanceof Name
524+
&& $concat->left->name->toLowerString() === 'preg_quote'
525+
) {
526+
$left = new ConstantStringType('');
527+
} elseif ($concat->left instanceof Expr\BinaryOp\Concat) {
528+
$left = $this->resolvePatternConcat($concat->left, $scope);
529+
} else {
530+
$left = $scope->getType($concat->left);
531+
}
532+
533+
if (
534+
$concat->right instanceof Expr\FuncCall
535+
&& $concat->right->name instanceof Name
536+
&& $concat->right->name->toLowerString() === 'preg_quote'
537+
) {
538+
$right = new ConstantStringType('');
539+
} elseif ($concat->right instanceof Expr\BinaryOp\Concat) {
540+
$right = $this->resolvePatternConcat($concat->right, $scope);
541+
} else {
542+
$right = $scope->getType($concat->right);
543+
}
544+
545+
$strings = [];
546+
foreach ($left->getConstantStrings() as $leftString) {
547+
foreach ($right->getConstantStrings() as $rightString) {
548+
$strings[] = new ConstantStringType($leftString->getValue() . $rightString->getValue());
549+
}
550+
}
551+
552+
return TypeCombinator::union(...$strings);
553+
}
554+
487555
}

tests/PHPStan/Analyser/nsrt/preg_match_shapes.php

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -393,3 +393,47 @@ function unmatchedAsNullWithMandatoryGroup(string $s): void {
393393
assertType('array{}|array{0: string, currency: string, 1: string}', $matches);
394394
}
395395

396+
function (string $s): void {
397+
if (preg_match('{' . preg_quote('xxx') . '(z)}', $s, $matches)) {
398+
assertType('array{string, string}', $matches);
399+
} else {
400+
assertType('array{}', $matches);
401+
}
402+
assertType('array{}|array{string, string}', $matches);
403+
};
404+
405+
function (string $s): void {
406+
if (preg_match('{' . preg_quote($s) . '(z)}', $s, $matches)) {
407+
assertType('array{string, string}', $matches);
408+
} else {
409+
assertType('array{}', $matches);
410+
}
411+
assertType('array{}|array{string, string}', $matches);
412+
};
413+
414+
function (string $s): void {
415+
if (preg_match('/' . preg_quote($s, '/') . '(\d)/', $s, $matches)) {
416+
assertType('array{string, string}', $matches);
417+
} else {
418+
assertType('array{}', $matches);
419+
}
420+
assertType('array{}|array{string, string}', $matches);
421+
};
422+
423+
function (string $s): void {
424+
if (preg_match('{' . preg_quote($s) . '(z)' . preg_quote($s) . '(?:abc)(def)?}', $s, $matches)) {
425+
assertType('array{0: string, 1: string, 2?: string}', $matches);
426+
} else {
427+
assertType('array{}', $matches);
428+
}
429+
assertType('array{}|array{0: string, 1: string, 2?: string}', $matches);
430+
};
431+
432+
function (string $s, $mixed): void {
433+
if (preg_match('{' . preg_quote($s) . '(z)' . preg_quote($s) . '(?:abc)'. $mixed .'(def)?}', $s, $matches)) {
434+
assertType('array<string>', $matches);
435+
} else {
436+
assertType('array{}', $matches);
437+
}
438+
assertType('array<string>', $matches);
439+
};

0 commit comments

Comments
 (0)