diff --git a/.github/workflows/continuous-integration.yml b/.github/workflows/continuous-integration.yml
index dc841a4..2f19aba 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:
@@ -13,6 +12,7 @@ jobs:
name: "CI"
runs-on: ubuntu-latest
+ continue-on-error: ${{ matrix.experimental }}
strategy:
matrix:
@@ -24,7 +24,10 @@ jobs:
- "8.1"
- "8.2"
- "8.3"
- - "8.4"
+ experimental: [false]
+ include:
+ - php-version: "8.4"
+ experimental: true
steps:
- name: "Checkout"
@@ -49,9 +52,9 @@ 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"
+ run: |
+ vendor/bin/phpunit
+ vendor/bin/phpunit --testsuite phpstan
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 8e78c8c..f81a2e4 100644
--- a/composer.json
+++ b/composer.json
@@ -20,10 +20,13 @@
"php": "^7.2 || ^8.0"
},
"require-dev": {
- "symfony/phpunit-bridge": "^7",
- "phpstan/phpstan": "^1.3",
+ "phpunit/phpunit": "^8 || ^9",
+ "phpstan/phpstan": "^1.11.6",
"phpstan/phpstan-strict-rules": "^1.1"
},
+ "conflict": {
+ "phpstan/phpstan": "<1.11.6"
+ },
"autoload": {
"psr-4": {
"Composer\\Pcre\\": "src"
@@ -40,7 +43,7 @@
}
},
"scripts": {
- "test": "SYMFONY_PHPUNIT_REMOVE_RETURN_TYPEHINT=1 vendor/bin/simple-phpunit",
- "phpstan": "phpstan analyse"
+ "test": "@php vendor/bin/phpunit",
+ "phpstan": "@php phpstan analyse"
}
}
diff --git a/extension.neon b/extension.neon
new file mode 100644
index 0000000..282b8d4
--- /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
diff --git a/phpstan.neon.dist b/phpstan.neon.dist
index add4274..890f3bc 100644
--- a/phpstan.neon.dist
+++ b/phpstan.neon.dist
@@ -8,9 +8,13 @@ parameters:
treatPhpDocTypesAsCertain: false
bootstrapFiles:
- - tests/phpstan-locate-phpunit-autoloader.php
+ - vendor/autoload.php
+
+ 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/phpunit.xml.dist b/phpunit.xml.dist
index 4a7697f..ea52b72 100644
--- a/phpunit.xml.dist
+++ b/phpunit.xml.dist
@@ -5,10 +5,15 @@
backupGlobals="false"
colors="true"
bootstrap="vendor/autoload.php"
+ defaultTestSuite="pcre"
>
-
- tests
+
+ tests/PregTests
+ tests/RegexTests
+
+
+ tests/PHPStanTests
diff --git a/src/PHPStan/PregMatchFlags.php b/src/PHPStan/PregMatchFlags.php
new file mode 100644
index 0000000..9cd3598
--- /dev/null
+++ b/src/PHPStan/PregMatchFlags.php
@@ -0,0 +1,36 @@
+getType($flagsArg->value);
+
+ $constantScalars = $flagsType->getConstantScalarValues();
+ if ($constantScalars === []) {
+ return null;
+ }
+
+ $internalFlagsTypes = [];
+ foreach ($flagsType->getConstantScalarValues() as $constantScalarValue) {
+ if (!is_int($constantScalarValue)) {
+ return null;
+ }
+
+ $internalFlagsTypes[] = new ConstantIntegerType($constantScalarValue | PREG_UNMATCHED_AS_NULL);
+ }
+ return TypeCombinator::union(...$internalFlagsTypes);
+ }
+}
diff --git a/src/PHPStan/PregMatchParameterOutTypeExtension.php b/src/PHPStan/PregMatchParameterOutTypeExtension.php
new file mode 100644
index 0000000..7793844
--- /dev/null
+++ b/src/PHPStan/PregMatchParameterOutTypeExtension.php
@@ -0,0 +1,59 @@
+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..63559cc
--- /dev/null
+++ b/src/PHPStan/PregMatchTypeSpecifyingExtension.php
@@ -0,0 +1,88 @@
+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/BaseTestCase.php b/tests/BaseTestCase.php
index 4db4409..20557ab 100644
--- a/tests/BaseTestCase.php
+++ b/tests/BaseTestCase.php
@@ -51,16 +51,12 @@ protected function expectPcreException(string $pattern, ?string $error = null):
// Only use a message if the error can be reliably determined
if (PHP_VERSION_ID >= 80000) {
$error = 'Internal error';
- } elseif (PHP_VERSION_ID >= 70201) {
+ } 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
new file mode 100644
index 0000000..b669f0a
--- /dev/null
+++ b/tests/PHPStanTests/TypeInferenceTest.php
@@ -0,0 +1,53 @@
+
+ *
+ * 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;
+
+/**
+ * Run with "vendor/bin/phpunit --testsuite phpstan"
+ *
+ * This is excluded by default to avoid side effects with the library tests
+ *
+ * @group phpstan
+ */
+class TypeInferenceTest extends TypeInferenceTestCase
+{
+ /**
+ * @return iterable
+ */
+ 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',
+ ];
+ }
+}
diff --git a/tests/PHPStanTests/nsrt/preg-match.php b/tests/PHPStanTests/nsrt/preg-match.php
new file mode 100644
index 0000000..a217b57
--- /dev/null
+++ b/tests/PHPStanTests/nsrt/preg-match.php
@@ -0,0 +1,52 @@
+= 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';