Skip to content

Commit 1e32a0f

Browse files
authored
Implement RegularExpressionPatternRule
1 parent 50543e8 commit 1e32a0f

File tree

5 files changed

+281
-1
lines changed

5 files changed

+281
-1
lines changed

Diff for: composer.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
],
88
"require": {
99
"php": "^7.2 || ^8.0",
10-
"phpstan/phpstan": "^1.7"
10+
"phpstan/phpstan": "^1.9.4"
1111
},
1212
"conflict": {
1313
"nette/application": "<2.3.0",

Diff for: rules.neon

+6
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,16 @@ parametersSchema:
1717
rules:
1818
- PHPStan\Rule\Nette\DoNotExtendNetteObjectRule
1919

20+
conditionalTags:
21+
PHPStan\Rule\Nette\RegularExpressionPatternRule:
22+
phpstan.rules.rule: %featureToggles.bleedingEdge%
23+
2024
services:
2125
-
2226
class: PHPStan\Rule\Nette\RethrowExceptionRule
2327
arguments:
2428
methods: %methodsThrowingExceptions%
2529
tags:
2630
- phpstan.rules.rule
31+
-
32+
class: PHPStan\Rule\Nette\RegularExpressionPatternRule

Diff for: src/Rule/Nette/RegularExpressionPatternRule.php

+109
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Rule\Nette;
4+
5+
use Nette\Utils\RegexpException;
6+
use Nette\Utils\Strings;
7+
use PhpParser\Node;
8+
use PhpParser\Node\Expr\StaticCall;
9+
use PHPStan\Analyser\Scope;
10+
use PHPStan\Rules\Rule;
11+
use PHPStan\Rules\RuleErrorBuilder;
12+
use PHPStan\Type\Constant\ConstantStringType;
13+
use function in_array;
14+
use function sprintf;
15+
use function strtolower;
16+
17+
/**
18+
* @implements Rule<Node\Expr\StaticCall>
19+
*/
20+
class RegularExpressionPatternRule implements Rule
21+
{
22+
23+
public function getNodeType(): string
24+
{
25+
return StaticCall::class;
26+
}
27+
28+
public function processNode(Node $node, Scope $scope): array
29+
{
30+
$patterns = $this->extractPatterns($node, $scope);
31+
32+
$errors = [];
33+
foreach ($patterns as $pattern) {
34+
$errorMessage = $this->validatePattern($pattern);
35+
if ($errorMessage === null) {
36+
continue;
37+
}
38+
39+
$errors[] = RuleErrorBuilder::message(sprintf('Regex pattern is invalid: %s', $errorMessage))->build();
40+
}
41+
42+
return $errors;
43+
}
44+
45+
/**
46+
* @return string[]
47+
*/
48+
private function extractPatterns(StaticCall $staticCall, Scope $scope): array
49+
{
50+
if (!$staticCall->class instanceof Node\Name || !$staticCall->name instanceof Node\Identifier) {
51+
return [];
52+
}
53+
$className = $scope->resolveName($staticCall->class);
54+
if ($className !== Strings::class) {
55+
return [];
56+
}
57+
$methodName = strtolower((string) $staticCall->name);
58+
if (
59+
!in_array($methodName, [
60+
'split',
61+
'match',
62+
'matchall',
63+
'replace',
64+
], true)
65+
) {
66+
return [];
67+
}
68+
69+
if (!isset($staticCall->getArgs()[1])) {
70+
return [];
71+
}
72+
$patternNode = $staticCall->getArgs()[1]->value;
73+
$patternType = $scope->getType($patternNode);
74+
75+
$patternStrings = [];
76+
77+
foreach ($patternType->getConstantStrings() as $constantStringType) {
78+
$patternStrings[] = $constantStringType->getValue();
79+
}
80+
81+
foreach ($patternType->getConstantArrays() as $constantArrayType) {
82+
if ($methodName !== 'replace') {
83+
continue;
84+
}
85+
86+
foreach ($constantArrayType->getKeyTypes() as $arrayKeyType) {
87+
if (!$arrayKeyType instanceof ConstantStringType) {
88+
continue;
89+
}
90+
91+
$patternStrings[] = $arrayKeyType->getValue();
92+
}
93+
}
94+
95+
return $patternStrings;
96+
}
97+
98+
private function validatePattern(string $pattern): ?string
99+
{
100+
try {
101+
Strings::match('', $pattern);
102+
} catch (RegexpException $e) {
103+
return $e->getMessage();
104+
}
105+
106+
return null;
107+
}
108+
109+
}
+132
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Rule\Nette;
4+
5+
use PHPStan\Rules\Rule;
6+
use PHPStan\Testing\RuleTestCase;
7+
use function sprintf;
8+
use const PHP_VERSION_ID;
9+
10+
/**
11+
* @extends RuleTestCase<RegularExpressionPatternRule>
12+
*/
13+
class RegularExpressionPatternRuleTest extends RuleTestCase
14+
{
15+
16+
protected function getRule(): Rule
17+
{
18+
return new RegularExpressionPatternRule();
19+
}
20+
21+
public function testValidRegexPatternBefore73(): void
22+
{
23+
if (PHP_VERSION_ID >= 70300) {
24+
self::markTestSkipped('This test requires PHP < 7.3.0');
25+
}
26+
27+
$this->analyse(
28+
[__DIR__ . '/data/valid-regex-pattern.php'],
29+
[
30+
[
31+
'Regex pattern is invalid: Delimiter must not be alphanumeric or backslash in pattern: nok',
32+
6,
33+
],
34+
[
35+
'Regex pattern is invalid: Compilation failed: missing ) at offset 1 in pattern: ~(~',
36+
7,
37+
],
38+
[
39+
'Regex pattern is invalid: Delimiter must not be alphanumeric or backslash in pattern: nok',
40+
11,
41+
],
42+
[
43+
'Regex pattern is invalid: Compilation failed: missing ) at offset 1 in pattern: ~(~',
44+
12,
45+
],
46+
[
47+
'Regex pattern is invalid: Delimiter must not be alphanumeric or backslash in pattern: nok',
48+
16,
49+
],
50+
[
51+
'Regex pattern is invalid: Compilation failed: missing ) at offset 1 in pattern: ~(~',
52+
17,
53+
],
54+
[
55+
'Regex pattern is invalid: Delimiter must not be alphanumeric or backslash in pattern: nok',
56+
21,
57+
],
58+
[
59+
'Regex pattern is invalid: Compilation failed: missing ) at offset 1 in pattern: ~(~',
60+
22,
61+
],
62+
[
63+
'Regex pattern is invalid: Delimiter must not be alphanumeric or backslash in pattern: nok',
64+
26,
65+
],
66+
[
67+
'Regex pattern is invalid: Compilation failed: missing ) at offset 1 in pattern: ~(~',
68+
26,
69+
],
70+
]
71+
);
72+
}
73+
74+
public function testValidRegexPatternAfter73(): void
75+
{
76+
if (PHP_VERSION_ID < 70300) {
77+
self::markTestSkipped('This test requires PHP >= 7.3.0');
78+
}
79+
80+
$messagePart = 'alphanumeric or backslash';
81+
if (PHP_VERSION_ID >= 80200) {
82+
$messagePart = 'alphanumeric, backslash, or NUL';
83+
}
84+
85+
$this->analyse(
86+
[__DIR__ . '/data/valid-regex-pattern.php'],
87+
[
88+
[
89+
sprintf('Regex pattern is invalid: Delimiter must not be %s in pattern: nok', $messagePart),
90+
6,
91+
],
92+
[
93+
'Regex pattern is invalid: Compilation failed: missing closing parenthesis at offset 1 in pattern: ~(~',
94+
7,
95+
],
96+
[
97+
sprintf('Regex pattern is invalid: Delimiter must not be %s in pattern: nok', $messagePart),
98+
11,
99+
],
100+
[
101+
'Regex pattern is invalid: Compilation failed: missing closing parenthesis at offset 1 in pattern: ~(~',
102+
12,
103+
],
104+
[
105+
sprintf('Regex pattern is invalid: Delimiter must not be %s in pattern: nok', $messagePart),
106+
16,
107+
],
108+
[
109+
'Regex pattern is invalid: Compilation failed: missing closing parenthesis at offset 1 in pattern: ~(~',
110+
17,
111+
],
112+
[
113+
sprintf('Regex pattern is invalid: Delimiter must not be %s in pattern: nok', $messagePart),
114+
21,
115+
],
116+
[
117+
'Regex pattern is invalid: Compilation failed: missing closing parenthesis at offset 1 in pattern: ~(~',
118+
22,
119+
],
120+
[
121+
sprintf('Regex pattern is invalid: Delimiter must not be %s in pattern: nok', $messagePart),
122+
26,
123+
],
124+
[
125+
'Regex pattern is invalid: Compilation failed: missing closing parenthesis at offset 1 in pattern: ~(~',
126+
26,
127+
],
128+
]
129+
);
130+
}
131+
132+
}

Diff for: tests/Rule/Nette/data/valid-regex-pattern.php

+33
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
<?php
2+
3+
$string = (function (): string {})();
4+
5+
\Nette\Utils\Strings::match('', '~ok~');
6+
\Nette\Utils\Strings::match('', 'nok');
7+
\Nette\Utils\Strings::match('', '~(~');
8+
\Nette\Utils\Strings::match('', $string);
9+
10+
\Nette\Utils\Strings::matchAll('', '~ok~');
11+
\Nette\Utils\Strings::matchAll('', 'nok');
12+
\Nette\Utils\Strings::matchAll('', '~(~');
13+
\Nette\Utils\Strings::matchAll('', $string);
14+
15+
\Nette\Utils\Strings::split('', '~ok~');
16+
\Nette\Utils\Strings::split('', 'nok');
17+
\Nette\Utils\Strings::split('', '~(~');
18+
\Nette\Utils\Strings::split('', $string);
19+
20+
\Nette\Utils\Strings::replace('', '~ok~', '');
21+
\Nette\Utils\Strings::replace('', 'nok', '');
22+
\Nette\Utils\Strings::replace('', '~(~', '');
23+
\Nette\Utils\Strings::replace('', $string, '');
24+
\Nette\Utils\Strings::replace('', ['~ok~', 'nok', '~(~', $string], '');
25+
26+
\Nette\Utils\Strings::replace(
27+
'',
28+
[
29+
'~ok~' => function () {},
30+
'nok' => function () {},
31+
'~(~' => function () {},
32+
]
33+
);

0 commit comments

Comments
 (0)