From 4017da4315e285b8479e2744d0b6f89c97cdf9cf Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Fri, 21 Jun 2024 13:10:25 +0200 Subject: [PATCH 01/15] Implement array shapes for `Preg::match` $matches by-ref parameter --- composer.json | 2 +- extension.neon | 14 +++ phpstan.neon.dist | 8 +- .../PregMatchParameterOutTypeExtension.php | 63 +++++++++++++ .../PregMatchTypeSpecifyingExtension.php | 93 +++++++++++++++++++ tests/PHPStanTests/TypeInferenceTest.php | 43 +++++++++ tests/PHPStanTests/nsrt/preg-match.php | 21 +++++ 7 files changed, 241 insertions(+), 3 deletions(-) create mode 100644 extension.neon create mode 100644 src/PHPStan/PregMatchParameterOutTypeExtension.php create mode 100644 src/PHPStan/PregMatchTypeSpecifyingExtension.php create mode 100644 tests/PHPStanTests/TypeInferenceTest.php create mode 100644 tests/PHPStanTests/nsrt/preg-match.php diff --git a/composer.json b/composer.json index 8e78c8c..52622f3 100644 --- a/composer.json +++ b/composer.json @@ -21,7 +21,7 @@ }, "require-dev": { "symfony/phpunit-bridge": "^7", - "phpstan/phpstan": "^1.3", + "phpstan/phpstan": "^1.11", "phpstan/phpstan-strict-rules": "^1.1" }, "autoload": { diff --git a/extension.neon b/extension.neon new file mode 100644 index 0000000..d779288 --- /dev/null +++ b/extension.neon @@ -0,0 +1,14 @@ +# composer/pcre PHPStan extensions +# +# These can be reused by third party packages by including 'vendor/composer/pcre/extension.neon' +# in your phpstan config + +services: + - + class: Composer\Pcre\PHPStan\PregMatchParameterOutTypeExtension + tags: + - phpstan.staticMethodParameterOutTypeExtension + - + class: Composer\Pcre\PHPStan\PregMatchTypeSpecifyingExtension + tags: + - phpstan.typeSpecifier.staticMethodTypeSpecifyingExtension \ No newline at end of file diff --git a/phpstan.neon.dist b/phpstan.neon.dist index add4274..6d1203d 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -7,10 +7,14 @@ parameters: reportUnmatchedIgnoredErrors: false treatPhpDocTypesAsCertain: false - bootstrapFiles: - - tests/phpstan-locate-phpunit-autoloader.php + ignoreErrors: + - '#Test::data[a-zA-Z0-9_]+\(\) return type has no value type specified in iterable type#' + + excludePaths: + - tests/PHPStanTests/nsrt/* includes: + - extension.neon - vendor/phpstan/phpstan/conf/bleedingEdge.neon - vendor/phpstan/phpstan-strict-rules/rules.neon - phpstan-baseline.neon diff --git a/src/PHPStan/PregMatchParameterOutTypeExtension.php b/src/PHPStan/PregMatchParameterOutTypeExtension.php new file mode 100644 index 0000000..5ab9015 --- /dev/null +++ b/src/PHPStan/PregMatchParameterOutTypeExtension.php @@ -0,0 +1,63 @@ +regexShapeMatcher = $regexShapeMatcher; + } + + public function isStaticMethodSupported(MethodReflection $methodReflection, ParameterReflection $parameter): bool + { + return + $methodReflection->getDeclaringClass()->getName() === Preg::class + && $methodReflection->getName() === 'match' + && $parameter->getName() === 'matches' + ; + } + + public function getParameterOutTypeFromStaticMethodCall(MethodReflection $methodReflection, StaticCall $methodCall, ParameterReflection $parameter, Scope $scope): ?Type + { + $args = $methodCall->getArgs(); + $patternArg = $args[0] ?? null; + $matchesArg = $args[2] ?? null; + $flagsArg = $args[3] ?? null; + + if ( + $patternArg === null || $matchesArg === null + ) { + return null; + } + + $patternType = $scope->getType($patternArg->value); + $flagsType = null; + if ($flagsArg !== null) { + $flagsType = $scope->getType($flagsArg->value); + } + + return $this->regexShapeMatcher->matchType($patternType, $flagsType, TrinaryLogic::createMaybe()); + } + +} diff --git a/src/PHPStan/PregMatchTypeSpecifyingExtension.php b/src/PHPStan/PregMatchTypeSpecifyingExtension.php new file mode 100644 index 0000000..4f9f1e7 --- /dev/null +++ b/src/PHPStan/PregMatchTypeSpecifyingExtension.php @@ -0,0 +1,93 @@ +regexShapeMatcher = $regexShapeMatcher; + } + + + public function setTypeSpecifier(TypeSpecifier $typeSpecifier): void + { + $this->typeSpecifier = $typeSpecifier; + } + + public function getClass(): string { + return Preg::class; + } + + public function isStaticMethodSupported(MethodReflection $methodReflection, StaticCall $node, TypeSpecifierContext $context) : bool + { + return $methodReflection->getName() === 'match' && !$context->null(); + } + + public function specifyTypes(MethodReflection $methodReflection, StaticCall $node, Scope $scope, TypeSpecifierContext $context) : SpecifiedTypes + { + $args = $node->getArgs(); + $patternArg = $args[0] ?? null; + $matchesArg = $args[2] ?? null; + $flagsArg = $args[3] ?? null; + + if ( + $patternArg === null || $matchesArg === null + ) { + return new SpecifiedTypes(); + } + + $patternType = $scope->getType($patternArg->value); + $flagsType = null; + if ($flagsArg !== null) { + $flagsType = $scope->getType($flagsArg->value); + } + + $matchedType = $this->regexShapeMatcher->matchType($patternType, $flagsType, TrinaryLogic::createFromBoolean($context->true())); + if ($matchedType === null) { + return new SpecifiedTypes(); + } + + $overwrite = false; + if ($context->false()) { + $overwrite = true; + $context = $context->negate(); + } + + return $this->typeSpecifier->create( + $matchesArg->value, + $matchedType, + $context, + $overwrite, + $scope, + $node, + ); + } + +} diff --git a/tests/PHPStanTests/TypeInferenceTest.php b/tests/PHPStanTests/TypeInferenceTest.php new file mode 100644 index 0000000..ab0509e --- /dev/null +++ b/tests/PHPStanTests/TypeInferenceTest.php @@ -0,0 +1,43 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace Composer\Pcre\PHPStanTests; + +use PHPStan\Testing\TypeInferenceTestCase; + +class TypeInferenceTest extends TypeInferenceTestCase +{ + public function dataFileAsserts(): iterable + { + yield from $this->gatherAssertTypesFromDirectory(__DIR__ . '/nsrt'); + + } + + /** + * @dataProvider dataFileAsserts + * @param mixed ...$args + */ + public function testFileAsserts( + string $assertType, + string $file, + ...$args + ): void + { + $this->assertFileAsserts($assertType, $file, ...$args); + } + + public static function getAdditionalConfigFiles(): array + { + return [ + __DIR__ . '/../../extension.neon', + ]; + } +} \ No newline at end of file diff --git a/tests/PHPStanTests/nsrt/preg-match.php b/tests/PHPStanTests/nsrt/preg-match.php new file mode 100644 index 0000000..1ae633b --- /dev/null +++ b/tests/PHPStanTests/nsrt/preg-match.php @@ -0,0 +1,21 @@ + Date: Fri, 21 Jun 2024 13:14:36 +0200 Subject: [PATCH 02/15] added more tests --- tests/PHPStanTests/nsrt/preg-match.php | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/tests/PHPStanTests/nsrt/preg-match.php b/tests/PHPStanTests/nsrt/preg-match.php index 1ae633b..eacc5a1 100644 --- a/tests/PHPStanTests/nsrt/preg-match.php +++ b/tests/PHPStanTests/nsrt/preg-match.php @@ -9,6 +9,8 @@ function doMatch(string $s): void { if (Preg::match('/Price: /i', $s, $matches)) { assertType('array{string}', $matches); + } else { + assertType('array{}', $matches); } assertType('array{}|array{string}', $matches); @@ -19,3 +21,23 @@ function doMatch(string $s): void } assertType('array{}|array{string, string}', $matches); } + +function identicalMatch(string $s): void +{ + if (Preg::match('/Price: /i', $s, $matches) === 1) { + assertType('array{string}', $matches); + } else { + assertType('array{}', $matches); + } + assertType('array{}|array{string}', $matches); +} + +function equalMatch(string $s): void +{ + if (Preg::match('/Price: /i', $s, $matches) == 1) { + assertType('array{string}', $matches); + } else { + assertType('array{}', $matches); + } + assertType('array{}|array{string}', $matches); +} \ No newline at end of file From 6e6c529bc28a639a1975d7039d6f50d7dcc7a878 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Sat, 22 Jun 2024 16:39:47 +0200 Subject: [PATCH 03/15] reflect flags behaviour --- src/PHPStan/PregMatchFlags.php | 35 ++++++ .../PregMatchParameterOutTypeExtension.php | 60 +++++------ .../PregMatchTypeSpecifyingExtension.php | 100 ++++++++---------- tests/PHPStanTests/nsrt/preg-match.php | 11 +- 4 files changed, 118 insertions(+), 88 deletions(-) create mode 100644 src/PHPStan/PregMatchFlags.php diff --git a/src/PHPStan/PregMatchFlags.php b/src/PHPStan/PregMatchFlags.php new file mode 100644 index 0000000..cd0f841 --- /dev/null +++ b/src/PHPStan/PregMatchFlags.php @@ -0,0 +1,35 @@ +getType($flagsArg->value); + + $constantScalars = $flagsType->getConstantScalarValues(); + if ($constantScalars === []) { + return null; + } + + $internalFlagsTypes = []; + foreach ($flagsType->getConstantScalarValues() as $constantScalarValue) { + if (!is_int($constantScalarValue)) { + return null; + } + + $internalFlagsTypes[] = $constantScalarValue | PREG_UNMATCHED_AS_NULL; + } + return TypeCombinator::union(...$internalFlagsTypes); + } +} \ No newline at end of file diff --git a/src/PHPStan/PregMatchParameterOutTypeExtension.php b/src/PHPStan/PregMatchParameterOutTypeExtension.php index 5ab9015..2947d64 100644 --- a/src/PHPStan/PregMatchParameterOutTypeExtension.php +++ b/src/PHPStan/PregMatchParameterOutTypeExtension.php @@ -1,63 +1,57 @@ -regexShapeMatcher = $regexShapeMatcher; - } + } public function isStaticMethodSupported(MethodReflection $methodReflection, ParameterReflection $parameter): bool { return $methodReflection->getDeclaringClass()->getName() === Preg::class && $methodReflection->getName() === 'match' - && $parameter->getName() === 'matches' - ; + && $parameter->getName() === 'matches'; } public function getParameterOutTypeFromStaticMethodCall(MethodReflection $methodReflection, StaticCall $methodCall, ParameterReflection $parameter, Scope $scope): ?Type { - $args = $methodCall->getArgs(); - $patternArg = $args[0] ?? null; - $matchesArg = $args[2] ?? null; - $flagsArg = $args[3] ?? null; - - if ( - $patternArg === null || $matchesArg === null - ) { - return null; - } - - $patternType = $scope->getType($patternArg->value); - $flagsType = null; - if ($flagsArg !== null) { - $flagsType = $scope->getType($flagsArg->value); - } - - return $this->regexShapeMatcher->matchType($patternType, $flagsType, TrinaryLogic::createMaybe()); - } + $args = $methodCall->getArgs(); + $patternArg = $args[0] ?? null; + $matchesArg = $args[2] ?? null; + $flagsArg = $args[3] ?? null; + + if ( + $patternArg === null || $matchesArg === null + ) { + return null; + } + + $flagsType = PregMatchFlags::getType($flagsArg, $scope); + if ($flagsType === null) { + return null; + } + $patternType = $scope->getType($patternArg->value); + + return $this->regexShapeMatcher->matchType($patternType, $flagsType, TrinaryLogic::createMaybe()); + } } diff --git a/src/PHPStan/PregMatchTypeSpecifyingExtension.php b/src/PHPStan/PregMatchTypeSpecifyingExtension.php index 4f9f1e7..eed8aca 100644 --- a/src/PHPStan/PregMatchTypeSpecifyingExtension.php +++ b/src/PHPStan/PregMatchTypeSpecifyingExtension.php @@ -1,30 +1,23 @@ -typeSpecifier = $typeSpecifier; - } + public function setTypeSpecifier(TypeSpecifier $typeSpecifier): void + { + $this->typeSpecifier = $typeSpecifier; + } - public function getClass(): string { + public function getClass(): string + { return Preg::class; } - public function isStaticMethodSupported(MethodReflection $methodReflection, StaticCall $node, TypeSpecifierContext $context) : bool + public function isStaticMethodSupported(MethodReflection $methodReflection, StaticCall $node, TypeSpecifierContext $context): bool { return $methodReflection->getName() === 'match' && !$context->null(); } - public function specifyTypes(MethodReflection $methodReflection, StaticCall $node, Scope $scope, TypeSpecifierContext $context) : SpecifiedTypes + public function specifyTypes(MethodReflection $methodReflection, StaticCall $node, Scope $scope, TypeSpecifierContext $context): SpecifiedTypes { - $args = $node->getArgs(); - $patternArg = $args[0] ?? null; - $matchesArg = $args[2] ?? null; - $flagsArg = $args[3] ?? null; - - if ( - $patternArg === null || $matchesArg === null - ) { - return new SpecifiedTypes(); - } - - $patternType = $scope->getType($patternArg->value); - $flagsType = null; - if ($flagsArg !== null) { - $flagsType = $scope->getType($flagsArg->value); - } - - $matchedType = $this->regexShapeMatcher->matchType($patternType, $flagsType, TrinaryLogic::createFromBoolean($context->true())); - if ($matchedType === null) { - return new SpecifiedTypes(); - } - - $overwrite = false; - if ($context->false()) { - $overwrite = true; - $context = $context->negate(); - } - - return $this->typeSpecifier->create( - $matchesArg->value, - $matchedType, - $context, - $overwrite, - $scope, - $node, - ); - } + $args = $node->getArgs(); + $patternArg = $args[0] ?? null; + $matchesArg = $args[2] ?? null; + $flagsArg = $args[3] ?? null; + + if ( + $patternArg === null || $matchesArg === null + ) { + return new SpecifiedTypes(); + } + + $flagsType = PregMatchFlags::getType($flagsArg, $scope); + if ($flagsType === null) { + return new SpecifiedTypes(); + } + $patternType = $scope->getType($patternArg->value); + + $matchedType = $this->regexShapeMatcher->matchType($patternType, $flagsType, TrinaryLogic::createFromBoolean($context->true())); + if ($matchedType === null) { + return new SpecifiedTypes(); + } + + $overwrite = false; + if ($context->false()) { + $overwrite = true; + $context = $context->negate(); + } + + return $this->typeSpecifier->create( + $matchesArg->value, + $matchedType, + $context, + $overwrite, + $scope, + $node, + ); + } } diff --git a/tests/PHPStanTests/nsrt/preg-match.php b/tests/PHPStanTests/nsrt/preg-match.php index eacc5a1..7d8993d 100644 --- a/tests/PHPStanTests/nsrt/preg-match.php +++ b/tests/PHPStanTests/nsrt/preg-match.php @@ -15,11 +15,18 @@ function doMatch(string $s): void assertType('array{}|array{string}', $matches); if (Preg::match('/Price: (£|€)\d+/', $s, $matches)) { - assertType('array{string, string}', $matches); + assertType('array{string, string|null}', $matches); } else { assertType('array{}', $matches); } - assertType('array{}|array{string, string}', $matches); + assertType('array{}|array{string, string|null}', $matches); + + if (Preg::match('/Price: (£|€)?\d+/', $s, $matches)) { + assertType('array{0: string, 1?: string|null}', $matches); + } else { + assertType('array{}', $matches); + } + assertType('array{}|array{0: string, 1?: string|null}', $matches); } function identicalMatch(string $s): void From e6fb2f257da8c7dfb67018621e46be46a124d7df Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Sat, 22 Jun 2024 16:58:38 +0200 Subject: [PATCH 04/15] utilize phpstan feature flag --- extension.neon | 12 +++++++----- tests/PHPStanTests/TypeInferenceTest.php | 1 + 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/extension.neon b/extension.neon index d779288..8fbd453 100644 --- a/extension.neon +++ b/extension.neon @@ -3,12 +3,14 @@ # These can be reused by third party packages by including 'vendor/composer/pcre/extension.neon' # in your phpstan config +conditionalTags: + Composer\Pcre\PHPStan\PregMatchParameterOutTypeExtension: + phpstan.staticMethodParameterOutTypeExtension: %featureToggles.narrowPregMatches% + Composer\Pcre\PHPStan\PregMatchTypeSpecifyingExtension: + phpstan.typeSpecifier.staticMethodTypeSpecifyingExtension: %featureToggles.narrowPregMatches% + services: - class: Composer\Pcre\PHPStan\PregMatchParameterOutTypeExtension - tags: - - phpstan.staticMethodParameterOutTypeExtension - - class: Composer\Pcre\PHPStan\PregMatchTypeSpecifyingExtension - tags: - - phpstan.typeSpecifier.staticMethodTypeSpecifyingExtension \ No newline at end of file + class: Composer\Pcre\PHPStan\PregMatchTypeSpecifyingExtension \ No newline at end of file diff --git a/tests/PHPStanTests/TypeInferenceTest.php b/tests/PHPStanTests/TypeInferenceTest.php index ab0509e..aa59b8f 100644 --- a/tests/PHPStanTests/TypeInferenceTest.php +++ b/tests/PHPStanTests/TypeInferenceTest.php @@ -37,6 +37,7 @@ public function testFileAsserts( public static function getAdditionalConfigFiles(): array { return [ + 'phar://' . __DIR__ . '/../../vendor/phpstan/phpstan/phpstan.phar/conf/bleedingEdge.neon', __DIR__ . '/../../extension.neon', ]; } From 23e29a4914025f5c3e49b399cf36fa69947383ab Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Sat, 22 Jun 2024 16:58:51 +0200 Subject: [PATCH 05/15] declare conflict with phpstan < 1.11.6 --- composer.json | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 52622f3..9531696 100644 --- a/composer.json +++ b/composer.json @@ -21,9 +21,12 @@ }, "require-dev": { "symfony/phpunit-bridge": "^7", - "phpstan/phpstan": "^1.11", + "phpstan/phpstan": "^1.11.6", "phpstan/phpstan-strict-rules": "^1.1" }, + "conflict": { + "phpstan/phpstan": "<1.11.6" + }, "autoload": { "psr-4": { "Composer\\Pcre\\": "src" From bbc50e25f82ec91b58c9375142adda14b353139d Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Thu, 11 Jul 2024 10:53:05 +0200 Subject: [PATCH 06/15] Tweaks --- extension.neon | 2 +- phpstan.neon.dist | 4 ++-- src/PHPStan/PregMatchFlags.php | 2 +- .../PregMatchParameterOutTypeExtension.php | 6 ++++-- .../PregMatchTypeSpecifyingExtension.php | 17 +++++++++-------- tests/PHPStanTests/TypeInferenceTest.php | 12 +++++++----- tests/PHPStanTests/nsrt/preg-match.php | 2 +- 7 files changed, 25 insertions(+), 20 deletions(-) diff --git a/extension.neon b/extension.neon index 8fbd453..282b8d4 100644 --- a/extension.neon +++ b/extension.neon @@ -13,4 +13,4 @@ services: - class: Composer\Pcre\PHPStan\PregMatchParameterOutTypeExtension - - class: Composer\Pcre\PHPStan\PregMatchTypeSpecifyingExtension \ No newline at end of file + class: Composer\Pcre\PHPStan\PregMatchTypeSpecifyingExtension diff --git a/phpstan.neon.dist b/phpstan.neon.dist index 6d1203d..900cf2d 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -7,8 +7,8 @@ parameters: reportUnmatchedIgnoredErrors: false treatPhpDocTypesAsCertain: false - ignoreErrors: - - '#Test::data[a-zA-Z0-9_]+\(\) return type has no value type specified in iterable type#' + bootstrapFiles: + - tests/phpstan-locate-phpunit-autoloader.php excludePaths: - tests/PHPStanTests/nsrt/* diff --git a/src/PHPStan/PregMatchFlags.php b/src/PHPStan/PregMatchFlags.php index cd0f841..08d7e6b 100644 --- a/src/PHPStan/PregMatchFlags.php +++ b/src/PHPStan/PregMatchFlags.php @@ -32,4 +32,4 @@ static public function getType(?Arg $flagsArg, Scope $scope): ?Type } return TypeCombinator::union(...$internalFlagsTypes); } -} \ No newline at end of file +} diff --git a/src/PHPStan/PregMatchParameterOutTypeExtension.php b/src/PHPStan/PregMatchParameterOutTypeExtension.php index 2947d64..7793844 100644 --- a/src/PHPStan/PregMatchParameterOutTypeExtension.php +++ b/src/PHPStan/PregMatchParameterOutTypeExtension.php @@ -14,8 +14,10 @@ final class PregMatchParameterOutTypeExtension implements StaticMethodParameterOutTypeExtension { - - private RegexArrayShapeMatcher $regexShapeMatcher; + /** + * @var RegexArrayShapeMatcher + */ + private $regexShapeMatcher; public function __construct( RegexArrayShapeMatcher $regexShapeMatcher diff --git a/src/PHPStan/PregMatchTypeSpecifyingExtension.php b/src/PHPStan/PregMatchTypeSpecifyingExtension.php index eed8aca..6bd5201 100644 --- a/src/PHPStan/PregMatchTypeSpecifyingExtension.php +++ b/src/PHPStan/PregMatchTypeSpecifyingExtension.php @@ -16,19 +16,21 @@ final class PregMatchTypeSpecifyingExtension implements StaticMethodTypeSpecifyingExtension, TypeSpecifierAwareExtension { + /** + * @var TypeSpecifier + */ + private $typeSpecifier; - private TypeSpecifier $typeSpecifier; + /** + * @var RegexArrayShapeMatcher + */ + private $regexShapeMatcher; - private RegexArrayShapeMatcher $regexShapeMatcher; - - public function __construct( - RegexArrayShapeMatcher $regexShapeMatcher - ) + public function __construct(RegexArrayShapeMatcher $regexShapeMatcher) { $this->regexShapeMatcher = $regexShapeMatcher; } - public function setTypeSpecifier(TypeSpecifier $typeSpecifier): void { $this->typeSpecifier = $typeSpecifier; @@ -83,5 +85,4 @@ public function specifyTypes(MethodReflection $methodReflection, StaticCall $nod $node, ); } - } diff --git a/tests/PHPStanTests/TypeInferenceTest.php b/tests/PHPStanTests/TypeInferenceTest.php index aa59b8f..ed5be84 100644 --- a/tests/PHPStanTests/TypeInferenceTest.php +++ b/tests/PHPStanTests/TypeInferenceTest.php @@ -15,10 +15,12 @@ class TypeInferenceTest extends TypeInferenceTestCase { + /** + * @return mixed + */ public function dataFileAsserts(): iterable { yield from $this->gatherAssertTypesFromDirectory(__DIR__ . '/nsrt'); - } /** @@ -28,9 +30,9 @@ public function dataFileAsserts(): iterable public function testFileAsserts( string $assertType, string $file, - ...$args - ): void - { + ...$args + ): void + { $this->assertFileAsserts($assertType, $file, ...$args); } @@ -41,4 +43,4 @@ public static function getAdditionalConfigFiles(): array __DIR__ . '/../../extension.neon', ]; } -} \ No newline at end of file +} diff --git a/tests/PHPStanTests/nsrt/preg-match.php b/tests/PHPStanTests/nsrt/preg-match.php index 7d8993d..0f3701b 100644 --- a/tests/PHPStanTests/nsrt/preg-match.php +++ b/tests/PHPStanTests/nsrt/preg-match.php @@ -47,4 +47,4 @@ function equalMatch(string $s): void assertType('array{}', $matches); } assertType('array{}|array{string}', $matches); -} \ No newline at end of file +} From 41eadc302501917eb11103e08aa6744583e3b0c3 Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Thu, 11 Jul 2024 11:17:13 +0200 Subject: [PATCH 07/15] Fork off phpstan CI in another job --- .gitattributes | 1 + .github/workflows/continuous-integration.yml | 45 +++++++++++++++++-- phpunit-phpstan.xml.dist | 20 +++++++++ phpunit.xml.dist | 3 +- .../PregMatchTypeSpecifyingExtension.php | 2 +- 5 files changed, 66 insertions(+), 5 deletions(-) create mode 100644 phpunit-phpstan.xml.dist diff --git a/.gitattributes b/.gitattributes index 49067b5..b525bdb 100644 --- a/.gitattributes +++ b/.gitattributes @@ -14,5 +14,6 @@ /phpstan.neon.dist export-ignore /phpstan-baseline.neon export-ignore /phpunit.xml.dist export-ignore +/phpunit-phpstan.xml.dist export-ignore /tests export-ignore /CONTRIBUTING.md export-ignore diff --git a/.github/workflows/continuous-integration.yml b/.github/workflows/continuous-integration.yml index dc841a4..6ba1fca 100644 --- a/.github/workflows/continuous-integration.yml +++ b/.github/workflows/continuous-integration.yml @@ -5,7 +5,6 @@ on: - pull_request env: - COMPOSER_FLAGS: "--ansi --no-interaction --no-progress --prefer-dist" SYMFONY_PHPUNIT_REMOVE_RETURN_TYPEHINT: "1" jobs: @@ -49,9 +48,49 @@ jobs: - name: "Install latest dependencies" run: | - # Remove PHPStan as it requires a newer PHP - composer remove phpstan/phpstan phpstan/phpstan-strict-rules --dev --no-update composer update ${{ env.COMPOSER_FLAGS }} - name: "Run tests" run: "vendor/bin/simple-phpunit --verbose" + + phpstan_tests: + name: "CI PHPStan Ext" + + runs-on: ubuntu-latest + + strategy: + matrix: + php-version: + - "7.3" + - "8.3" + + steps: + - name: "Checkout" + uses: "actions/checkout@v2" + + - name: "Install PHP" + uses: "shivammathur/setup-php@v2" + with: + coverage: "none" + php-version: "${{ matrix.php-version }}" + + - name: Get composer cache directory + id: composercache + run: echo "::set-output name=dir::$(composer config cache-files-dir)" + + - name: Cache dependencies + uses: actions/cache@v2 + with: + path: ${{ steps.composercache.outputs.dir }} + key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.json') }} + restore-keys: ${{ runner.os }}-composer- + + - name: "Install latest dependencies" + run: | + # Require PHPUnit directly bypassing the symfony bridge + composer remove symfony/phpunit-bridge --dev --no-update + composer require phpunit/phpunit:^9 --dev --no-update + composer update ${{ env.COMPOSER_FLAGS }} + + - name: "Run tests" + run: vendor/bin/phpunit -c phpunit-phpstan.xml.dist diff --git a/phpunit-phpstan.xml.dist b/phpunit-phpstan.xml.dist new file mode 100644 index 0000000..1ab1ead --- /dev/null +++ b/phpunit-phpstan.xml.dist @@ -0,0 +1,20 @@ + + + + + tests/PHPStanTests + + + + + + src + + + diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 4a7697f..4b57ef9 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -8,7 +8,8 @@ > - tests + tests/PregTests + tests/RegexTests diff --git a/src/PHPStan/PregMatchTypeSpecifyingExtension.php b/src/PHPStan/PregMatchTypeSpecifyingExtension.php index 6bd5201..63559cc 100644 --- a/src/PHPStan/PregMatchTypeSpecifyingExtension.php +++ b/src/PHPStan/PregMatchTypeSpecifyingExtension.php @@ -82,7 +82,7 @@ public function specifyTypes(MethodReflection $methodReflection, StaticCall $nod $context, $overwrite, $scope, - $node, + $node ); } } From df2c96978da1dd1a5ba22dec5d1c9f11002643fb Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Thu, 11 Jul 2024 11:37:06 +0200 Subject: [PATCH 08/15] Fixes --- src/PHPStan/PregMatchFlags.php | 1 + tests/PHPStanTests/nsrt/preg-match.php | 19 ++++++++++--------- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/src/PHPStan/PregMatchFlags.php b/src/PHPStan/PregMatchFlags.php index 08d7e6b..3421add 100644 --- a/src/PHPStan/PregMatchFlags.php +++ b/src/PHPStan/PregMatchFlags.php @@ -6,6 +6,7 @@ use PHPStan\Type\Constant\ConstantIntegerType; use PHPStan\Type\TypeCombinator; use PHPStan\Type\Type; +use PhpParser\Node\Arg; final class PregMatchFlags { diff --git a/tests/PHPStanTests/nsrt/preg-match.php b/tests/PHPStanTests/nsrt/preg-match.php index 0f3701b..78fc4e9 100644 --- a/tests/PHPStanTests/nsrt/preg-match.php +++ b/tests/PHPStanTests/nsrt/preg-match.php @@ -39,12 +39,13 @@ function identicalMatch(string $s): void assertType('array{}|array{string}', $matches); } -function equalMatch(string $s): void -{ - if (Preg::match('/Price: /i', $s, $matches) == 1) { - assertType('array{string}', $matches); - } else { - assertType('array{}', $matches); - } - assertType('array{}|array{string}', $matches); -} +// disabled until https://github.com/phpstan/phpstan-src/pull/3185 can be resolved +//function equalMatch(string $s): void +//{ +// if (Preg::match('/Price: /i', $s, $matches) == 1) { +// assertType('array{string}', $matches); +// } else { +// assertType('array{}', $matches); +// } +// assertType('array{}|array{string}', $matches); +//} From 95d3cc674dc03aeb158ddb80375bb429c98e6330 Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Thu, 11 Jul 2024 11:41:45 +0200 Subject: [PATCH 09/15] Use ConstantIntegerType instead of ints for TypeCombinator::union --- src/PHPStan/PregMatchFlags.php | 2 +- tests/PHPStanTests/nsrt/preg-match.php | 21 +++++++++++---------- 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/src/PHPStan/PregMatchFlags.php b/src/PHPStan/PregMatchFlags.php index 3421add..9cd3598 100644 --- a/src/PHPStan/PregMatchFlags.php +++ b/src/PHPStan/PregMatchFlags.php @@ -29,7 +29,7 @@ static public function getType(?Arg $flagsArg, Scope $scope): ?Type return null; } - $internalFlagsTypes[] = $constantScalarValue | PREG_UNMATCHED_AS_NULL; + $internalFlagsTypes[] = new ConstantIntegerType($constantScalarValue | PREG_UNMATCHED_AS_NULL); } return TypeCombinator::union(...$internalFlagsTypes); } diff --git a/tests/PHPStanTests/nsrt/preg-match.php b/tests/PHPStanTests/nsrt/preg-match.php index 78fc4e9..a217b57 100644 --- a/tests/PHPStanTests/nsrt/preg-match.php +++ b/tests/PHPStanTests/nsrt/preg-match.php @@ -29,17 +29,18 @@ function doMatch(string $s): void assertType('array{}|array{0: string, 1?: string|null}', $matches); } -function identicalMatch(string $s): void -{ - if (Preg::match('/Price: /i', $s, $matches) === 1) { - assertType('array{string}', $matches); - } else { - assertType('array{}', $matches); - } - assertType('array{}|array{string}', $matches); -} - // disabled until https://github.com/phpstan/phpstan-src/pull/3185 can be resolved +// +//function identicalMatch(string $s): void +//{ +// if (Preg::match('/Price: /i', $s, $matches) === 1) { +// assertType('array{string}', $matches); +// } else { +// assertType('array{}', $matches); +// } +// assertType('array{}|array{string}', $matches); +//} +// //function equalMatch(string $s): void //{ // if (Preg::match('/Price: /i', $s, $matches) == 1) { From 590984a3863a10eec6c21e15ae7db64b44d7ccc2 Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Thu, 11 Jul 2024 11:43:16 +0200 Subject: [PATCH 10/15] Last fix --- tests/PHPStanTests/TypeInferenceTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/PHPStanTests/TypeInferenceTest.php b/tests/PHPStanTests/TypeInferenceTest.php index ed5be84..504c9b3 100644 --- a/tests/PHPStanTests/TypeInferenceTest.php +++ b/tests/PHPStanTests/TypeInferenceTest.php @@ -16,7 +16,7 @@ class TypeInferenceTest extends TypeInferenceTestCase { /** - * @return mixed + * @return iterable */ public function dataFileAsserts(): iterable { From 0850175999e7d1b6a11502f8a88225b04ed3743f Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Thu, 11 Jul 2024 11:46:33 +0200 Subject: [PATCH 11/15] Get rid of phpunit bridge --- .gitattributes | 1 - .github/workflows/continuous-integration.yml | 44 +------------------- .github/workflows/phpstan.yml | 3 -- composer.json | 4 +- phpstan.neon.dist | 2 +- phpunit-phpstan.xml.dist | 20 --------- phpunit.xml.dist | 3 +- tests/phpstan-locate-phpunit-autoloader.php | 22 ---------- 8 files changed, 5 insertions(+), 94 deletions(-) delete mode 100644 phpunit-phpstan.xml.dist delete mode 100644 tests/phpstan-locate-phpunit-autoloader.php diff --git a/.gitattributes b/.gitattributes index b525bdb..49067b5 100644 --- a/.gitattributes +++ b/.gitattributes @@ -14,6 +14,5 @@ /phpstan.neon.dist export-ignore /phpstan-baseline.neon export-ignore /phpunit.xml.dist export-ignore -/phpunit-phpstan.xml.dist export-ignore /tests export-ignore /CONTRIBUTING.md export-ignore diff --git a/.github/workflows/continuous-integration.yml b/.github/workflows/continuous-integration.yml index 6ba1fca..b2c8900 100644 --- a/.github/workflows/continuous-integration.yml +++ b/.github/workflows/continuous-integration.yml @@ -51,46 +51,4 @@ jobs: composer update ${{ env.COMPOSER_FLAGS }} - name: "Run tests" - run: "vendor/bin/simple-phpunit --verbose" - - phpstan_tests: - name: "CI PHPStan Ext" - - runs-on: ubuntu-latest - - strategy: - matrix: - php-version: - - "7.3" - - "8.3" - - steps: - - name: "Checkout" - uses: "actions/checkout@v2" - - - name: "Install PHP" - uses: "shivammathur/setup-php@v2" - with: - coverage: "none" - php-version: "${{ matrix.php-version }}" - - - name: Get composer cache directory - id: composercache - run: echo "::set-output name=dir::$(composer config cache-files-dir)" - - - name: Cache dependencies - uses: actions/cache@v2 - with: - path: ${{ steps.composercache.outputs.dir }} - key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.json') }} - restore-keys: ${{ runner.os }}-composer- - - - name: "Install latest dependencies" - run: | - # Require PHPUnit directly bypassing the symfony bridge - composer remove symfony/phpunit-bridge --dev --no-update - composer require phpunit/phpunit:^9 --dev --no-update - composer update ${{ env.COMPOSER_FLAGS }} - - - name: "Run tests" - run: vendor/bin/phpunit -c phpunit-phpstan.xml.dist + run: "vendor/bin/phpunit" diff --git a/.github/workflows/phpstan.yml b/.github/workflows/phpstan.yml index 93bea17..f135b11 100644 --- a/.github/workflows/phpstan.yml +++ b/.github/workflows/phpstan.yml @@ -44,8 +44,5 @@ jobs: - name: "Install latest dependencies" run: "composer update ${{ env.COMPOSER_FLAGS }}" - - name: "Initialize PHPUnit sources" - run: "vendor/bin/simple-phpunit --filter NO_TEST_JUST_AUTOLOAD_THANKS" - - name: "Run PHPStan" run: "composer phpstan" diff --git a/composer.json b/composer.json index 9531696..ec707cc 100644 --- a/composer.json +++ b/composer.json @@ -20,7 +20,7 @@ "php": "^7.2 || ^8.0" }, "require-dev": { - "symfony/phpunit-bridge": "^7", + "phpunit/phpunit": "^8 || ^9", "phpstan/phpstan": "^1.11.6", "phpstan/phpstan-strict-rules": "^1.1" }, @@ -43,7 +43,7 @@ } }, "scripts": { - "test": "SYMFONY_PHPUNIT_REMOVE_RETURN_TYPEHINT=1 vendor/bin/simple-phpunit", + "test": "vendor/bin/phpunit", "phpstan": "phpstan analyse" } } diff --git a/phpstan.neon.dist b/phpstan.neon.dist index 900cf2d..890f3bc 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -8,7 +8,7 @@ parameters: treatPhpDocTypesAsCertain: false bootstrapFiles: - - tests/phpstan-locate-phpunit-autoloader.php + - vendor/autoload.php excludePaths: - tests/PHPStanTests/nsrt/* diff --git a/phpunit-phpstan.xml.dist b/phpunit-phpstan.xml.dist deleted file mode 100644 index 1ab1ead..0000000 --- a/phpunit-phpstan.xml.dist +++ /dev/null @@ -1,20 +0,0 @@ - - - - - tests/PHPStanTests - - - - - - src - - - diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 4b57ef9..4a7697f 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -8,8 +8,7 @@ > - tests/PregTests - tests/RegexTests + tests diff --git a/tests/phpstan-locate-phpunit-autoloader.php b/tests/phpstan-locate-phpunit-autoloader.php deleted file mode 100644 index a9e670c..0000000 --- a/tests/phpstan-locate-phpunit-autoloader.php +++ /dev/null @@ -1,22 +0,0 @@ -= 80000 && false !== strpos((string) $dir, 'phpunit-9')) { - break; - } - if (PHP_VERSION_ID < 80000 && false !== strpos((string) $dir, 'phpunit-8')) { - break; - } -} - -if (null === $bestDirFound) { - echo 'Run "composer test" to initialize PHPUnit sources before running PHPStan'.PHP_EOL; - exit(1); -} - -include $bestDirFound.'/vendor/autoload.php'; From 0613a55494c9b3466de23384ada59d69eb851d5d Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Thu, 11 Jul 2024 12:54:52 +0200 Subject: [PATCH 12/15] Fix expectations as php8 polyfills are loaded by phpstan.phar --- composer.json | 4 ++-- tests/BaseTestCase.php | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/composer.json b/composer.json index ec707cc..f81a2e4 100644 --- a/composer.json +++ b/composer.json @@ -43,7 +43,7 @@ } }, "scripts": { - "test": "vendor/bin/phpunit", - "phpstan": "phpstan analyse" + "test": "@php vendor/bin/phpunit", + "phpstan": "@php phpstan analyse" } } diff --git a/tests/BaseTestCase.php b/tests/BaseTestCase.php index 4db4409..1ccb601 100644 --- a/tests/BaseTestCase.php +++ b/tests/BaseTestCase.php @@ -37,7 +37,7 @@ protected function doExpectWarning(string $message): void protected function expectPcreEngineException(string $pattern): void { - $error = PHP_VERSION_ID >= 80000 ? 'Backtrack limit exhausted' : 'PREG_BACKTRACK_LIMIT_ERROR'; + $error = function_exists('preg_last_error_msg') ? 'Backtrack limit exhausted' : 'PREG_BACKTRACK_LIMIT_ERROR'; $this->expectPcreException($pattern, $error); } @@ -49,9 +49,9 @@ protected function expectPcreException(string $pattern, ?string $error = null): if (null === $error) { // Only use a message if the error can be reliably determined - if (PHP_VERSION_ID >= 80000) { + if (function_exists('preg_last_error_msg')) { $error = 'Internal error'; - } elseif (PHP_VERSION_ID >= 70201) { + } else { $error = 'PREG_INTERNAL_ERROR'; } } From f457867a49f953faa075aaf4751cca2968142205 Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Thu, 11 Jul 2024 13:23:14 +0200 Subject: [PATCH 13/15] Split off the phpstan tests again --- .github/workflows/continuous-integration.yml | 4 +++- phpunit.xml.dist | 6 ++++++ tests/BaseTestCase.php | 10 +++------- tests/PHPStanTests/TypeInferenceTest.php | 7 +++++++ 4 files changed, 19 insertions(+), 8 deletions(-) diff --git a/.github/workflows/continuous-integration.yml b/.github/workflows/continuous-integration.yml index b2c8900..8f93fb2 100644 --- a/.github/workflows/continuous-integration.yml +++ b/.github/workflows/continuous-integration.yml @@ -51,4 +51,6 @@ jobs: composer update ${{ env.COMPOSER_FLAGS }} - name: "Run tests" - run: "vendor/bin/phpunit" + run: | + vendor/bin/phpunit + vendor/bin/phpunit --group phpstan diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 4a7697f..7f73f73 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -12,6 +12,12 @@ + + + phpstan + + + src diff --git a/tests/BaseTestCase.php b/tests/BaseTestCase.php index 1ccb601..20557ab 100644 --- a/tests/BaseTestCase.php +++ b/tests/BaseTestCase.php @@ -37,7 +37,7 @@ protected function doExpectWarning(string $message): void protected function expectPcreEngineException(string $pattern): void { - $error = function_exists('preg_last_error_msg') ? 'Backtrack limit exhausted' : 'PREG_BACKTRACK_LIMIT_ERROR'; + $error = PHP_VERSION_ID >= 80000 ? 'Backtrack limit exhausted' : 'PREG_BACKTRACK_LIMIT_ERROR'; $this->expectPcreException($pattern, $error); } @@ -49,18 +49,14 @@ protected function expectPcreException(string $pattern, ?string $error = null): if (null === $error) { // Only use a message if the error can be reliably determined - if (function_exists('preg_last_error_msg')) { + if (PHP_VERSION_ID >= 80000) { $error = 'Internal error'; } else { $error = 'PREG_INTERNAL_ERROR'; } } - if (null !== $error) { - $message = sprintf('%s: failed executing "%s": %s', $this->pregFunction, $pattern, $error); - } else { - $message = null; - } + $message = sprintf('%s: failed executing "%s": %s', $this->pregFunction, $pattern, $error); $this->doExpectException('Composer\Pcre\PcreException', $message); } diff --git a/tests/PHPStanTests/TypeInferenceTest.php b/tests/PHPStanTests/TypeInferenceTest.php index 504c9b3..9c0d2c7 100644 --- a/tests/PHPStanTests/TypeInferenceTest.php +++ b/tests/PHPStanTests/TypeInferenceTest.php @@ -13,6 +13,13 @@ use PHPStan\Testing\TypeInferenceTestCase; +/** + * Run with "vendor/bin/phpunit --group phpstan" + * + * This is excluded by default to avoid side effects with the library tests + * + * @group phpstan + */ class TypeInferenceTest extends TypeInferenceTestCase { /** From 02e63f23fd106cb49adb6a4e5aefb6ef9a132687 Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Thu, 11 Jul 2024 13:24:19 +0200 Subject: [PATCH 14/15] Let php 8.4 build fail --- .github/workflows/continuous-integration.yml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/workflows/continuous-integration.yml b/.github/workflows/continuous-integration.yml index 8f93fb2..eb822d8 100644 --- a/.github/workflows/continuous-integration.yml +++ b/.github/workflows/continuous-integration.yml @@ -12,6 +12,7 @@ jobs: name: "CI" runs-on: ubuntu-latest + continue-on-error: ${{ matrix.experimental }} strategy: matrix: @@ -23,7 +24,10 @@ jobs: - "8.1" - "8.2" - "8.3" - - "8.4" + experimental: [false] + include: + - php-version: "8.4" + experimental: true steps: - name: "Checkout" From bedef2dd1b1b50b13fe469a19a2d188207b57951 Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Thu, 11 Jul 2024 13:32:12 +0200 Subject: [PATCH 15/15] Fork test suite as groups are not enough to avoid loading --- .github/workflows/continuous-integration.yml | 2 +- phpunit.xml.dist | 15 +++++++-------- tests/PHPStanTests/TypeInferenceTest.php | 2 +- 3 files changed, 9 insertions(+), 10 deletions(-) diff --git a/.github/workflows/continuous-integration.yml b/.github/workflows/continuous-integration.yml index eb822d8..2f19aba 100644 --- a/.github/workflows/continuous-integration.yml +++ b/.github/workflows/continuous-integration.yml @@ -57,4 +57,4 @@ jobs: - name: "Run tests" run: | vendor/bin/phpunit - vendor/bin/phpunit --group phpstan + vendor/bin/phpunit --testsuite phpstan diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 7f73f73..ea52b72 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -5,19 +5,18 @@ backupGlobals="false" colors="true" bootstrap="vendor/autoload.php" + defaultTestSuite="pcre" > - - tests + + tests/PregTests + tests/RegexTests + + + tests/PHPStanTests - - - phpstan - - - src diff --git a/tests/PHPStanTests/TypeInferenceTest.php b/tests/PHPStanTests/TypeInferenceTest.php index 9c0d2c7..b669f0a 100644 --- a/tests/PHPStanTests/TypeInferenceTest.php +++ b/tests/PHPStanTests/TypeInferenceTest.php @@ -14,7 +14,7 @@ use PHPStan\Testing\TypeInferenceTestCase; /** - * Run with "vendor/bin/phpunit --group phpstan" + * Run with "vendor/bin/phpunit --testsuite phpstan" * * This is excluded by default to avoid side effects with the library tests *