diff --git a/composer.json b/composer.json index 40477ff..f2b1815 100644 --- a/composer.json +++ b/composer.json @@ -20,10 +20,13 @@ "php": "^7.4 || ^8.0" }, "require-dev": { - "symfony/phpunit-bridge": "^5", - "phpstan/phpstan": "^1.3", + "phpunit/phpunit": "^9.5", + "phpstan/phpstan": "^1.11.6", "phpstan/phpstan-strict-rules": "^1.1" }, + "conflict": { + "phpstan/phpstan": "<1.11.6" + }, "autoload": { "psr-4": { "Composer\\Pcre\\": "src" diff --git a/extension.neon b/extension.neon new file mode 100644 index 0000000..8fbd453 --- /dev/null +++ b/extension.neon @@ -0,0 +1,16 @@ +# composer/pcre PHPStan extensions +# +# 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 + - + class: Composer\Pcre\PHPStan\PregMatchTypeSpecifyingExtension \ No newline at end of file diff --git a/phpstan.neon.dist b/phpstan.neon.dist index 4670cbd..ec5adff 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -6,10 +6,14 @@ parameters: 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/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 new file mode 100644 index 0000000..2947d64 --- /dev/null +++ b/src/PHPStan/PregMatchParameterOutTypeExtension.php @@ -0,0 +1,57 @@ +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; + } + + $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 new file mode 100644 index 0000000..eed8aca --- /dev/null +++ b/src/PHPStan/PregMatchTypeSpecifyingExtension.php @@ -0,0 +1,87 @@ +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(); + } + + $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/TypeInferenceTest.php b/tests/PHPStanTests/TypeInferenceTest.php new file mode 100644 index 0000000..aa59b8f --- /dev/null +++ b/tests/PHPStanTests/TypeInferenceTest.php @@ -0,0 +1,44 @@ + + * + * 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 [ + 'phar://' . __DIR__ . '/../../vendor/phpstan/phpstan/phpstan.phar/conf/bleedingEdge.neon', + __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..7d8993d --- /dev/null +++ b/tests/PHPStanTests/nsrt/preg-match.php @@ -0,0 +1,50 @@ +