Skip to content

Commit

Permalink
Implement array shapes for Preg::match $matches by-ref parameter (#25)
Browse files Browse the repository at this point in the history
* Implement array shapes for `Preg::match` $matches by-ref parameter
* declare conflict with phpstan < 1.11.6
* Fork off phpstan CI in another job
* Get rid of phpunit bridge

Co-authored-by: Markus Staab <[email protected]>
  • Loading branch information
Seldaek and staabm authored Jul 11, 2024
1 parent b13ea67 commit 37ef71e
Show file tree
Hide file tree
Showing 13 changed files with 333 additions and 43 deletions.
13 changes: 8 additions & 5 deletions .github/workflows/continuous-integration.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,14 @@ on:
- pull_request

env:
COMPOSER_FLAGS: "--ansi --no-interaction --no-progress --prefer-dist"
SYMFONY_PHPUNIT_REMOVE_RETURN_TYPEHINT: "1"

jobs:
tests:
name: "CI"

runs-on: ubuntu-latest
continue-on-error: ${{ matrix.experimental }}

strategy:
matrix:
Expand All @@ -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"
Expand All @@ -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
3 changes: 0 additions & 3 deletions .github/workflows/phpstan.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
11 changes: 7 additions & 4 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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"
}
}
16 changes: 16 additions & 0 deletions extension.neon
Original file line number Diff line number Diff line change
@@ -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
6 changes: 5 additions & 1 deletion phpstan.neon.dist
Original file line number Diff line number Diff line change
Expand Up @@ -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
9 changes: 7 additions & 2 deletions phpunit.xml.dist
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,15 @@
backupGlobals="false"
colors="true"
bootstrap="vendor/autoload.php"
defaultTestSuite="pcre"
>
<testsuites>
<testsuite name="PCRE Test Suite">
<directory>tests</directory>
<testsuite name="pcre">
<directory>tests/PregTests</directory>
<directory>tests/RegexTests</directory>
</testsuite>
<testsuite name="phpstan">
<directory>tests/PHPStanTests</directory>
</testsuite>
</testsuites>

Expand Down
36 changes: 36 additions & 0 deletions src/PHPStan/PregMatchFlags.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<?php declare(strict_types=1);

namespace Composer\Pcre\PHPStan;

use PHPStan\Analyser\Scope;
use PHPStan\Type\Constant\ConstantIntegerType;
use PHPStan\Type\TypeCombinator;
use PHPStan\Type\Type;
use PhpParser\Node\Arg;

final class PregMatchFlags
{
static public function getType(?Arg $flagsArg, Scope $scope): ?Type
{
if ($flagsArg === null) {
return new ConstantIntegerType(PREG_UNMATCHED_AS_NULL);
}

$flagsType = $scope->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);
}
}
59 changes: 59 additions & 0 deletions src/PHPStan/PregMatchParameterOutTypeExtension.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
<?php declare(strict_types=1);

namespace Composer\Pcre\PHPStan;

use Composer\Pcre\Preg;
use PhpParser\Node\Expr\StaticCall;
use PHPStan\Analyser\Scope;
use PHPStan\Reflection\MethodReflection;
use PHPStan\Reflection\ParameterReflection;
use PHPStan\TrinaryLogic;
use PHPStan\Type\Php\RegexArrayShapeMatcher;
use PHPStan\Type\StaticMethodParameterOutTypeExtension;
use PHPStan\Type\Type;

final class PregMatchParameterOutTypeExtension implements StaticMethodParameterOutTypeExtension
{
/**
* @var RegexArrayShapeMatcher
*/
private $regexShapeMatcher;

public function __construct(
RegexArrayShapeMatcher $regexShapeMatcher
)
{
$this->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());
}

}
88 changes: 88 additions & 0 deletions src/PHPStan/PregMatchTypeSpecifyingExtension.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
<?php declare(strict_types=1);

namespace Composer\Pcre\PHPStan;

use Composer\Pcre\Preg;
use PhpParser\Node\Expr\StaticCall;
use PHPStan\Analyser\Scope;
use PHPStan\Analyser\SpecifiedTypes;
use PHPStan\Analyser\TypeSpecifier;
use PHPStan\Analyser\TypeSpecifierAwareExtension;
use PHPStan\Analyser\TypeSpecifierContext;
use PHPStan\Reflection\MethodReflection;
use PHPStan\TrinaryLogic;
use PHPStan\Type\Php\RegexArrayShapeMatcher;
use PHPStan\Type\StaticMethodTypeSpecifyingExtension;

final class PregMatchTypeSpecifyingExtension implements StaticMethodTypeSpecifyingExtension, TypeSpecifierAwareExtension
{
/**
* @var TypeSpecifier
*/
private $typeSpecifier;

/**
* @var RegexArrayShapeMatcher
*/
private $regexShapeMatcher;

public function __construct(RegexArrayShapeMatcher $regexShapeMatcher)
{
$this->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
);
}
}
8 changes: 2 additions & 6 deletions tests/BaseTestCase.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down
53 changes: 53 additions & 0 deletions tests/PHPStanTests/TypeInferenceTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
<?php

/*
* This file is part of composer/pcre.
*
* (c) Composer <https://github.com/composer>
*
* 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<mixed>
*/
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',
];
}
}
Loading

0 comments on commit 37ef71e

Please sign in to comment.