Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
Expand Up @@ -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
45 changes: 42 additions & 3 deletions .github/workflows/continuous-integration.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ on:
- pull_request

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

jobs:
Expand Down Expand Up @@ -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
5 changes: 4 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,12 @@
},
"require-dev": {
"symfony/phpunit-bridge": "^7",
"phpstan/phpstan": "^1.3",
"phpstan/phpstan": "^1.11.6",
"phpstan/phpstan-strict-rules": "^1.1"
},
"conflict": {
"phpstan/phpstan": "<1.11.6"
},
"autoload": {
"psr-4": {
"Composer\\Pcre\\": "src"
Expand Down
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
4 changes: 4 additions & 0 deletions phpstan.neon.dist
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,11 @@ parameters:
bootstrapFiles:
- tests/phpstan-locate-phpunit-autoloader.php

excludePaths:
- tests/PHPStanTests/nsrt/*

includes:
- extension.neon
- vendor/phpstan/phpstan/conf/bleedingEdge.neon
- vendor/phpstan/phpstan-strict-rules/rules.neon
- phpstan-baseline.neon
20 changes: 20 additions & 0 deletions phpunit-phpstan.xml.dist
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="http://schema.phpunit.de/4.5/phpunit.xsd"
backupGlobals="false"
colors="true"
bootstrap="vendor/autoload.php"
>
<testsuites>
<testsuite name="PCRE Test Suite">
<directory>tests/PHPStanTests</directory>
</testsuite>
</testsuites>

<filter>
<whitelist>
<directory>src</directory>
</whitelist>
</filter>
</phpunit>
3 changes: 2 additions & 1 deletion phpunit.xml.dist
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@
>
<testsuites>
<testsuite name="PCRE Test Suite">
<directory>tests</directory>
<directory>tests/PregTests</directory>
<directory>tests/RegexTests</directory>
</testsuite>
</testsuites>

Expand Down
35 changes: 35 additions & 0 deletions src/PHPStan/PregMatchFlags.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
<?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;

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[] = $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
);
}
}
46 changes: 46 additions & 0 deletions tests/PHPStanTests/TypeInferenceTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
<?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;

class TypeInferenceTest extends TypeInferenceTestCase
{
/**
* @return 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