Skip to content

Commit 4b581de

Browse files
committed
RegexArrayShapeMatcher - Support preg_quote()'d patterns
1 parent a0dc9ed commit 4b581de

File tree

4 files changed

+89
-4
lines changed

4 files changed

+89
-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: 61 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,49 @@ 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+
private function resolvePatternConcat(Expr\BinaryOp\Concat $concat, Scope $scope): Type
513+
{
514+
if (
515+
$concat->left instanceof Expr\FuncCall
516+
&& $concat->left->name instanceof Name
517+
&& $concat->left->name->toLowerString() === 'preg_quote'
518+
) {
519+
$left = new ConstantStringType('');
520+
} elseif ($concat->left instanceof Expr\BinaryOp\Concat) {
521+
$left = $this->resolvePatternConcat($concat->left, $scope);
522+
} else {
523+
$left = $scope->getType($concat->left);
524+
}
525+
526+
if (
527+
$concat->right instanceof Expr\FuncCall
528+
&& $concat->right->name instanceof Name
529+
&& $concat->right->name->toLowerString() === 'preg_quote'
530+
) {
531+
$right = new ConstantStringType('');
532+
} elseif ($concat->right instanceof Expr\BinaryOp\Concat) {
533+
$right = $this->resolvePatternConcat($concat->right, $scope);
534+
} else {
535+
$right = $scope->getType($concat->right);
536+
}
537+
538+
$strings = [];
539+
foreach ($left->getConstantStrings() as $leftString) {
540+
foreach ($right->getConstantStrings() as $rightString) {
541+
$strings[] = new ConstantStringType($leftString->getValue() . $rightString->getValue());
542+
}
543+
}
544+
545+
return TypeCombinator::union(...$strings);
546+
}
547+
487548
}

tests/PHPStan/Analyser/nsrt/preg_match_shapes.php

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -393,3 +393,29 @@ 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) . '(z)' . preg_quote($s) . '(?:abc)(def)?}', $s, $matches)) {
416+
assertType('array{0: string, 1: string, 2?: string}', $matches);
417+
} else {
418+
assertType('array{}', $matches);
419+
}
420+
assertType('array{}|array{0: string, 1: string, 2?: string}', $matches);
421+
};

0 commit comments

Comments
 (0)