Skip to content

Commit c870edc

Browse files
committed
Merge branch '2.x'
2 parents 9f21bdb + b730621 commit c870edc

23 files changed

+923
-52
lines changed

.github/workflows/continuous-integration.yml

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,14 @@ on:
55
- pull_request
66

77
env:
8-
COMPOSER_FLAGS: "--ansi --no-interaction --no-progress --prefer-dist"
98
SYMFONY_PHPUNIT_REMOVE_RETURN_TYPEHINT: "1"
109

1110
jobs:
1211
tests:
1312
name: "CI"
1413

1514
runs-on: ubuntu-latest
15+
continue-on-error: ${{ matrix.experimental }}
1616

1717
strategy:
1818
matrix:
@@ -22,7 +22,10 @@ jobs:
2222
- "8.1"
2323
- "8.2"
2424
- "8.3"
25-
- "8.4"
25+
experimental: [false]
26+
include:
27+
- php-version: "8.4"
28+
experimental: true
2629

2730
steps:
2831
- name: "Checkout"
@@ -47,9 +50,9 @@ jobs:
4750

4851
- name: "Install latest dependencies"
4952
run: |
50-
# Remove PHPStan as it requires a newer PHP
51-
composer remove phpstan/phpstan phpstan/phpstan-strict-rules --dev --no-update
5253
composer update ${{ env.COMPOSER_FLAGS }}
5354
5455
- name: "Run tests"
55-
run: "vendor/bin/simple-phpunit --verbose"
56+
run: |
57+
vendor/bin/phpunit
58+
vendor/bin/phpunit --testsuite phpstan

.github/workflows/phpstan.yml

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -44,8 +44,5 @@ jobs:
4444
- name: "Install latest dependencies"
4545
run: "composer update ${{ env.COMPOSER_FLAGS }}"
4646

47-
- name: "Initialize PHPUnit sources"
48-
run: "vendor/bin/simple-phpunit --filter NO_TEST_JUST_AUTOLOAD_THANKS"
49-
5047
- name: "Run PHPStan"
5148
run: "composer phpstan"

composer.json

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,13 @@
2020
"php": "^7.4 || ^8.0"
2121
},
2222
"require-dev": {
23-
"symfony/phpunit-bridge": "^7",
24-
"phpstan/phpstan": "^1.3",
23+
"phpunit/phpunit": "^8 || ^9",
24+
"phpstan/phpstan": "^1.11.8",
2525
"phpstan/phpstan-strict-rules": "^1.1"
2626
},
27+
"conflict": {
28+
"phpstan/phpstan": "<1.11.8"
29+
},
2730
"autoload": {
2831
"psr-4": {
2932
"Composer\\Pcre\\": "src"
@@ -40,7 +43,7 @@
4043
}
4144
},
4245
"scripts": {
43-
"test": "vendor/bin/simple-phpunit",
44-
"phpstan": "phpstan analyse"
46+
"test": "@php vendor/bin/phpunit",
47+
"phpstan": "@php phpstan analyse"
4548
}
4649
}

extension.neon

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
# composer/pcre PHPStan extensions
2+
#
3+
# These can be reused by third party packages by including 'vendor/composer/pcre/extension.neon'
4+
# in your phpstan config
5+
6+
conditionalTags:
7+
Composer\Pcre\PHPStan\PregMatchParameterOutTypeExtension:
8+
phpstan.staticMethodParameterOutTypeExtension: %featureToggles.narrowPregMatches%
9+
Composer\Pcre\PHPStan\PregMatchTypeSpecifyingExtension:
10+
phpstan.typeSpecifier.staticMethodTypeSpecifyingExtension: %featureToggles.narrowPregMatches%
11+
Composer\Pcre\PHPStan\UnsafeStrictGroupsCallRule:
12+
phpstan.rules.rule: %featureToggles.narrowPregMatches%
13+
14+
services:
15+
-
16+
class: Composer\Pcre\PHPStan\PregMatchParameterOutTypeExtension
17+
-
18+
class: Composer\Pcre\PHPStan\PregMatchTypeSpecifyingExtension
19+
20+
rules:
21+
- Composer\Pcre\PHPStan\UnsafeStrictGroupsCallRule
22+
- Composer\Pcre\PHPStan\InvalidRegexPatternRule

phpstan-baseline.neon

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,93 @@ parameters:
99
message: "#^Parameter &\\$matches @param\\-out type of method Composer\\\\Pcre\\\\Preg\\:\\:matchAllWithOffsets\\(\\) expects array\\<int\\|string, list\\<array\\{string\\|null, int\\<\\-1, max\\>\\}\\>\\>, array given\\.$#"
1010
count: 1
1111
path: src/Preg.php
12+
13+
-
14+
message: "#^Regex pattern is invalid\\: No ending matching delimiter '\\}' found$#"
15+
count: 2
16+
path: tests/PregTests/GrepTest.php
17+
18+
-
19+
message: "#^Regex pattern is invalid\\: No ending matching delimiter '\\}' found$#"
20+
count: 2
21+
path: tests/PregTests/IsMatchAllTest.php
22+
23+
-
24+
message: "#^Regex pattern is invalid\\: No ending matching delimiter '\\}' found$#"
25+
count: 2
26+
path: tests/PregTests/IsMatchAllWithOffsetsTest.php
27+
28+
-
29+
message: "#^Regex pattern is invalid\\: No ending matching delimiter '\\}' found$#"
30+
count: 2
31+
path: tests/PregTests/IsMatchTest.php
32+
33+
-
34+
message: "#^Regex pattern is invalid\\: No ending matching delimiter '\\}' found$#"
35+
count: 2
36+
path: tests/PregTests/IsMatchWithOffsetsTest.php
37+
38+
-
39+
message: "#^Regex pattern is invalid\\: No ending matching delimiter '\\}' found$#"
40+
count: 2
41+
path: tests/PregTests/MatchAllTest.php
42+
43+
-
44+
message: "#^Regex pattern is invalid\\: No ending matching delimiter '\\}' found$#"
45+
count: 2
46+
path: tests/PregTests/MatchTest.php
47+
48+
-
49+
message: "#^Regex pattern is invalid\\: No ending matching delimiter '\\}' found$#"
50+
count: 2
51+
path: tests/PregTests/ReplaceCallbackArrayTest.php
52+
53+
-
54+
message: "#^Regex pattern is invalid\\: No ending matching delimiter '\\}' found$#"
55+
count: 2
56+
path: tests/PregTests/ReplaceCallbackTest.php
57+
58+
-
59+
message: "#^Regex pattern is invalid\\: No ending matching delimiter '\\}' found$#"
60+
count: 2
61+
path: tests/PregTests/ReplaceTest.php
62+
63+
-
64+
message: "#^Regex pattern is invalid\\: No ending matching delimiter '\\}' found$#"
65+
count: 2
66+
path: tests/PregTests/SplitTest.php
67+
68+
-
69+
message: "#^Regex pattern is invalid\\: No ending matching delimiter '\\}' found$#"
70+
count: 2
71+
path: tests/PregTests/SplitWithOffsetsTest.php
72+
73+
-
74+
message: "#^Regex pattern is invalid\\: No ending matching delimiter '\\}' found$#"
75+
count: 2
76+
path: tests/RegexTests/IsMatchTest.php
77+
78+
-
79+
message: "#^Regex pattern is invalid\\: No ending matching delimiter '\\}' found$#"
80+
count: 2
81+
path: tests/RegexTests/MatchAllTest.php
82+
83+
-
84+
message: "#^Regex pattern is invalid\\: No ending matching delimiter '\\}' found$#"
85+
count: 2
86+
path: tests/RegexTests/MatchTest.php
87+
88+
-
89+
message: "#^Regex pattern is invalid\\: No ending matching delimiter '\\}' found$#"
90+
count: 2
91+
path: tests/RegexTests/ReplaceCallbackArrayTest.php
92+
93+
-
94+
message: "#^Regex pattern is invalid\\: No ending matching delimiter '\\}' found$#"
95+
count: 2
96+
path: tests/RegexTests/ReplaceCallbackTest.php
97+
98+
-
99+
message: "#^Regex pattern is invalid\\: No ending matching delimiter '\\}' found$#"
100+
count: 2
101+
path: tests/RegexTests/ReplaceTest.php

phpstan.neon.dist

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,14 @@ parameters:
77
treatPhpDocTypesAsCertain: false
88

99
bootstrapFiles:
10-
- tests/phpstan-locate-phpunit-autoloader.php
10+
- vendor/autoload.php
11+
12+
excludePaths:
13+
- tests/PHPStanTests/nsrt/*
14+
- tests/PHPStanTests/fixtures/*
1115

1216
includes:
17+
- extension.neon
1318
- vendor/phpstan/phpstan/conf/bleedingEdge.neon
1419
- vendor/phpstan/phpstan-strict-rules/rules.neon
1520
- phpstan-baseline.neon

phpunit.xml.dist

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,15 @@
55
backupGlobals="false"
66
colors="true"
77
bootstrap="vendor/autoload.php"
8+
defaultTestSuite="pcre"
89
>
910
<testsuites>
10-
<testsuite name="PCRE Test Suite">
11-
<directory>tests</directory>
11+
<testsuite name="pcre">
12+
<directory>tests/PregTests</directory>
13+
<directory>tests/RegexTests</directory>
14+
</testsuite>
15+
<testsuite name="phpstan">
16+
<directory>tests/PHPStanTests</directory>
1217
</testsuite>
1318
</testsuites>
1419

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace Composer\Pcre\PHPStan;
4+
5+
use Composer\Pcre\Preg;
6+
use Composer\Pcre\Regex;
7+
use Composer\Pcre\PcreException;
8+
use Nette\Utils\RegexpException;
9+
use Nette\Utils\Strings;
10+
use PhpParser\Node;
11+
use PhpParser\Node\Expr\StaticCall;
12+
use PhpParser\Node\Name\FullyQualified;
13+
use PHPStan\Analyser\Scope;
14+
use PHPStan\Rules\Rule;
15+
use PHPStan\Rules\RuleErrorBuilder;
16+
use function in_array;
17+
use function sprintf;
18+
19+
/**
20+
* Copy of PHPStan's RegularExpressionPatternRule
21+
*
22+
* @implements Rule<StaticCall>
23+
*/
24+
class InvalidRegexPatternRule implements Rule
25+
{
26+
public function getNodeType(): string
27+
{
28+
return StaticCall::class;
29+
}
30+
31+
public function processNode(Node $node, Scope $scope): array
32+
{
33+
$patterns = $this->extractPatterns($node, $scope);
34+
35+
$errors = [];
36+
foreach ($patterns as $pattern) {
37+
$errorMessage = $this->validatePattern($pattern);
38+
if ($errorMessage === null) {
39+
continue;
40+
}
41+
42+
$errors[] = RuleErrorBuilder::message(sprintf('Regex pattern is invalid: %s', $errorMessage))->identifier('regexp.pattern')->build();
43+
}
44+
45+
return $errors;
46+
}
47+
48+
/**
49+
* @return string[]
50+
*/
51+
private function extractPatterns(StaticCall $node, Scope $scope): array
52+
{
53+
if (!$node->class instanceof FullyQualified) {
54+
return [];
55+
}
56+
$isRegex = $node->class->toString() === Regex::class;
57+
$isPreg = $node->class->toString() === Preg::class;
58+
if (!$isRegex && !$isPreg) {
59+
return [];
60+
}
61+
if (!$node->name instanceof Node\Identifier || !Preg::isMatch('{^(match|isMatch|grep|replace|split)}', $node->name->name)) {
62+
return [];
63+
}
64+
65+
$functionName = $node->name->name;
66+
if (!isset($node->getArgs()[0])) {
67+
return [];
68+
}
69+
70+
$patternNode = $node->getArgs()[0]->value;
71+
$patternType = $scope->getType($patternNode);
72+
73+
$patternStrings = [];
74+
75+
foreach ($patternType->getConstantStrings() as $constantStringType) {
76+
if ($functionName === 'replaceCallbackArray') {
77+
continue;
78+
}
79+
80+
$patternStrings[] = $constantStringType->getValue();
81+
}
82+
83+
foreach ($patternType->getConstantArrays() as $constantArrayType) {
84+
if (
85+
in_array($functionName, [
86+
'replace',
87+
'replaceCallback',
88+
], true)
89+
) {
90+
foreach ($constantArrayType->getValueTypes() as $arrayKeyType) {
91+
foreach ($arrayKeyType->getConstantStrings() as $constantString) {
92+
$patternStrings[] = $constantString->getValue();
93+
}
94+
}
95+
}
96+
97+
if ($functionName !== 'replaceCallbackArray') {
98+
continue;
99+
}
100+
101+
foreach ($constantArrayType->getKeyTypes() as $arrayKeyType) {
102+
foreach ($arrayKeyType->getConstantStrings() as $constantString) {
103+
$patternStrings[] = $constantString->getValue();
104+
}
105+
}
106+
}
107+
108+
return $patternStrings;
109+
}
110+
111+
private function validatePattern(string $pattern): ?string
112+
{
113+
try {
114+
$msg = null;
115+
$prev = set_error_handler(function (int $severity, string $message, string $file) use (&$msg): bool {
116+
$msg = preg_replace("#^preg_match(_all)?\\(.*?\\): #", '', $message);
117+
118+
return true;
119+
});
120+
121+
if ($pattern === '') {
122+
return 'Empty string is not a valid regular expression';
123+
}
124+
125+
Preg::match($pattern, '');
126+
if ($msg !== null) {
127+
return $msg;
128+
}
129+
} catch (PcreException $e) {
130+
if ($e->getCode() === PREG_INTERNAL_ERROR && $msg !== null) {
131+
return $msg;
132+
}
133+
134+
return preg_replace('{.*? failed executing ".*": }', '', $e->getMessage());
135+
} finally {
136+
restore_error_handler();
137+
}
138+
139+
return null;
140+
}
141+
142+
}

0 commit comments

Comments
 (0)