Skip to content

Commit ac573c1

Browse files
authored
Merge branch refs/heads/1.11.x into 1.12.x
2 parents 4b1ec02 + 465af9e commit ac573c1

File tree

6 files changed

+151
-53
lines changed

6 files changed

+151
-53
lines changed

conf/config.neon

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1492,6 +1492,9 @@ services:
14921492
-
14931493
class: PHPStan\Type\Php\RegexArrayShapeMatcher
14941494

1495+
-
1496+
class: PHPStan\Type\Php\RegexExpressionHelper
1497+
14951498
-
14961499
class: PHPStan\Type\Php\ReflectionClassConstructorThrowTypeExtension
14971500
tags:

src/Rules/Regexp/RegularExpressionPatternRule.php

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
use PHPStan\Analyser\Scope;
1010
use PHPStan\Rules\Rule;
1111
use PHPStan\Rules\RuleErrorBuilder;
12+
use PHPStan\Type\Php\RegexExpressionHelper;
1213
use function in_array;
1314
use function sprintf;
1415
use function str_starts_with;
@@ -20,6 +21,12 @@
2021
final class RegularExpressionPatternRule implements Rule
2122
{
2223

24+
public function __construct(
25+
private RegexExpressionHelper $regexExpressionHelper,
26+
)
27+
{
28+
}
29+
2330
public function getNodeType(): string
2431
{
2532
return FuncCall::class;
@@ -74,6 +81,9 @@ private function extractPatterns(FuncCall $functionCall, Scope $scope): array
7481
'preg_filter',
7582
], true)
7683
) {
84+
if ($patternNode instanceof Node\Expr\BinaryOp\Concat) {
85+
$patternType = $this->regexExpressionHelper->resolvePatternConcat($patternNode, $scope);
86+
}
7787
foreach ($patternType->getConstantStrings() as $constantStringType) {
7888
$patternStrings[] = $constantStringType->getValue();
7989
}

src/Type/Php/RegexArrayShapeMatcher.php

Lines changed: 2 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,8 @@
1010
use Nette\Utils\RegexpException;
1111
use Nette\Utils\Strings;
1212
use PhpParser\Node\Expr;
13-
use PhpParser\Node\Name;
1413
use PHPStan\Analyser\Scope;
1514
use PHPStan\Php\PhpVersion;
16-
use PHPStan\Reflection\InitializerExprTypeResolver;
1715
use PHPStan\ShouldNotHappenException;
1816
use PHPStan\TrinaryLogic;
1917
use PHPStan\Type\Accessory\AccessoryNonEmptyStringType;
@@ -56,7 +54,7 @@ final class RegexArrayShapeMatcher
5654

5755
public function __construct(
5856
private PhpVersion $phpVersion,
59-
private InitializerExprTypeResolver $initializerExprTypeResolver,
57+
private RegexExpressionHelper $regexExpressionHelper,
6058
)
6159
{
6260
}
@@ -712,57 +710,10 @@ private function getLiteralValue(TreeNode $node): ?string
712710
private function getPatternType(Expr $patternExpr, Scope $scope): Type
713711
{
714712
if ($patternExpr instanceof Expr\BinaryOp\Concat) {
715-
return $this->resolvePatternConcat($patternExpr, $scope);
713+
return $this->regexExpressionHelper->resolvePatternConcat($patternExpr, $scope);
716714
}
717715

718716
return $scope->getType($patternExpr);
719717
}
720718

721-
/**
722-
* Ignores preg_quote() calls in the concatenation as these are not relevant for array-shape matching.
723-
*
724-
* This assumption only works for the ArrayShapeMatcher therefore it is not implemented for the common case in Scope.
725-
*
726-
* see https://github.com/phpstan/phpstan-src/pull/3233#discussion_r1676938085
727-
*/
728-
private function resolvePatternConcat(Expr\BinaryOp\Concat $concat, Scope $scope): Type
729-
{
730-
$resolver = new class($scope) {
731-
732-
public function __construct(private Scope $scope)
733-
{
734-
}
735-
736-
public function resolve(Expr $expr): Type
737-
{
738-
if (
739-
$expr instanceof Expr\FuncCall
740-
&& $expr->name instanceof Name
741-
&& $expr->name->toLowerString() === 'preg_quote'
742-
) {
743-
return new ConstantStringType('');
744-
}
745-
746-
if ($expr instanceof Expr\BinaryOp\Concat) {
747-
$left = $this->resolve($expr->left);
748-
$right = $this->resolve($expr->right);
749-
750-
$strings = [];
751-
foreach ($left->toString()->getConstantStrings() as $leftString) {
752-
foreach ($right->toString()->getConstantStrings() as $rightString) {
753-
$strings[] = new ConstantStringType($leftString->getValue() . $rightString->getValue());
754-
}
755-
}
756-
757-
return TypeCombinator::union(...$strings);
758-
}
759-
760-
return $this->scope->getType($expr);
761-
}
762-
763-
};
764-
765-
return $this->initializerExprTypeResolver->getConcatType($concat->left, $concat->right, static fn (Expr $expr): Type => $resolver->resolve($expr));
766-
}
767-
768719
}
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Type\Php;
4+
5+
use PhpParser\Node\Expr;
6+
use PhpParser\Node\Name;
7+
use PHPStan\Analyser\Scope;
8+
use PHPStan\Reflection\InitializerExprTypeResolver;
9+
use PHPStan\Type\Constant\ConstantStringType;
10+
use PHPStan\Type\Type;
11+
use PHPStan\Type\TypeCombinator;
12+
13+
final class RegexExpressionHelper
14+
{
15+
16+
public function __construct(
17+
private InitializerExprTypeResolver $initializerExprTypeResolver,
18+
)
19+
{
20+
}
21+
22+
/**
23+
* Ignores preg_quote() calls in the concatenation as these are not relevant for array-shape matching.
24+
*
25+
* This assumption only works for the ArrayShapeMatcher therefore it is not implemented for the common case in Scope.
26+
*
27+
* see https://github.com/phpstan/phpstan-src/pull/3233#discussion_r1676938085
28+
*/
29+
public function resolvePatternConcat(Expr\BinaryOp\Concat $concat, Scope $scope): Type
30+
{
31+
$resolver = new class($scope) {
32+
33+
public function __construct(private Scope $scope)
34+
{
35+
}
36+
37+
public function resolve(Expr $expr): Type
38+
{
39+
if (
40+
$expr instanceof Expr\FuncCall
41+
&& $expr->name instanceof Name
42+
&& $expr->name->toLowerString() === 'preg_quote'
43+
) {
44+
return new ConstantStringType('');
45+
}
46+
47+
if ($expr instanceof Expr\BinaryOp\Concat) {
48+
$left = $this->resolve($expr->left);
49+
$right = $this->resolve($expr->right);
50+
51+
$strings = [];
52+
foreach ($left->toString()->getConstantStrings() as $leftString) {
53+
foreach ($right->toString()->getConstantStrings() as $rightString) {
54+
$strings[] = new ConstantStringType($leftString->getValue() . $rightString->getValue());
55+
}
56+
}
57+
58+
return TypeCombinator::union(...$strings);
59+
}
60+
61+
return $this->scope->getType($expr);
62+
}
63+
64+
};
65+
66+
return $this->initializerExprTypeResolver->getConcatType($concat->left, $concat->right, static fn (Expr $expr): Type => $resolver->resolve($expr));
67+
}
68+
69+
}

tests/PHPStan/Rules/Regexp/RegularExpressionPatternRuleTest.php

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
use PHPStan\Rules\Rule;
66
use PHPStan\Testing\RuleTestCase;
7+
use PHPStan\Type\Php\RegexExpressionHelper;
78
use function sprintf;
89
use const PHP_VERSION_ID;
910

@@ -15,7 +16,9 @@ class RegularExpressionPatternRuleTest extends RuleTestCase
1516

1617
protected function getRule(): Rule
1718
{
18-
return new RegularExpressionPatternRule();
19+
return new RegularExpressionPatternRule(
20+
self::getContainer()->getByType(RegexExpressionHelper::class),
21+
);
1922
}
2023

2124
public function testValidRegexPatternBefore73(): void
@@ -115,6 +118,30 @@ public function testValidRegexPatternBefore73(): void
115118
'Regex pattern is invalid: Compilation failed: missing ) at offset 1 in pattern: ~(~',
116119
43,
117120
],
121+
[
122+
'Regex pattern is invalid: Delimiter must not be alphanumeric, backslash, or NUL in pattern: nok',
123+
57,
124+
],
125+
[
126+
'Regex pattern is invalid: Delimiter must not be alphanumeric, backslash, or NUL in pattern: nok',
127+
58,
128+
],
129+
[
130+
'Regex pattern is invalid: Compilation failed: missing closing parenthesis at offset 1 in pattern: ~(~',
131+
59,
132+
],
133+
[
134+
'Regex pattern is invalid: Delimiter must not be alphanumeric, backslash, or NUL in pattern: noknono',
135+
61,
136+
],
137+
[
138+
'Regex pattern is invalid: Delimiter must not be alphanumeric, backslash, or NUL in pattern: noknope',
139+
62,
140+
],
141+
[
142+
'Regex pattern is invalid: Compilation failed: missing closing parenthesis at offset 1 in pattern: ~(~',
143+
63,
144+
],
118145
],
119146
);
120147
}
@@ -221,6 +248,30 @@ public function testValidRegexPatternAfter73(): void
221248
'Regex pattern is invalid: Compilation failed: missing closing parenthesis at offset 1 in pattern: ~(~',
222249
43,
223250
],
251+
[
252+
sprintf('Regex pattern is invalid: Delimiter must not be %s in pattern: nok', $messagePart),
253+
57,
254+
],
255+
[
256+
sprintf('Regex pattern is invalid: Delimiter must not be %s in pattern: nok', $messagePart),
257+
58,
258+
],
259+
[
260+
'Regex pattern is invalid: Compilation failed: missing closing parenthesis at offset 1 in pattern: ~(~',
261+
59,
262+
],
263+
[
264+
sprintf('Regex pattern is invalid: Delimiter must not be %s in pattern: noknono', $messagePart),
265+
61,
266+
],
267+
[
268+
sprintf('Regex pattern is invalid: Delimiter must not be %s in pattern: noknope', $messagePart),
269+
62,
270+
],
271+
[
272+
'Regex pattern is invalid: Compilation failed: missing closing parenthesis at offset 1 in pattern: ~(~',
273+
63,
274+
],
224275
],
225276
);
226277
}

tests/PHPStan/Rules/Regexp/data/valid-regex-pattern.php

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
<?php
1+
<?php namespace RegexExpressionPatterns;
22

33
$string = (function (): string {})();
44

@@ -48,3 +48,17 @@
4848
],
4949
''
5050
);
51+
52+
function doFoo(string $s) {
53+
preg_match('~ok'. preg_quote($s, '~') .'~', '');
54+
preg_match('~ok'. preg_quote($s) .'~', '');
55+
56+
// invalid preg_quote delimiters will be reported by RegularExpressionQuotingRule
57+
preg_match('nok'. preg_quote($s), '');
58+
preg_match('nok'. preg_quote($s), '');
59+
preg_match('~('. preg_quote($s, '~') .'~', '');
60+
61+
preg_replace('nok'. preg_quote($s).'nono', '');
62+
preg_replace('nok'. preg_quote($s).'nope', '');
63+
preg_replace('~('. preg_quote($s, '~') .'~', '');
64+
}

0 commit comments

Comments
 (0)