Skip to content

Commit

Permalink
Understand the exact array shape coming from `Nette\Utils\Strings::ma…
Browse files Browse the repository at this point in the history
…tch()` based on pattern
  • Loading branch information
ondrejmirtes committed Jun 21, 2024
1 parent 9eebad8 commit 3e68a5d
Show file tree
Hide file tree
Showing 5 changed files with 128 additions and 1 deletion.
2 changes: 1 addition & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
],
"require": {
"php": "^7.2 || ^8.0",
"phpstan/phpstan": "^1.10"
"phpstan/phpstan": "^1.11.6"
},
"conflict": {
"nette/application": "<2.3.0",
Expand Down
7 changes: 7 additions & 0 deletions extension.neon
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,10 @@ parameters:
- terminate
- forward

conditionalTags:
PHPStan\Type\Nette\StringsMatchDynamicReturnTypeExtension:
phpstan.broker.dynamicStaticMethodReturnTypeExtension: %featureToggles.narrowPregMatches%

services:
-
class: PHPStan\Reflection\Nette\HtmlClassReflectionExtension
Expand Down Expand Up @@ -114,3 +118,6 @@ services:
class: PHPStan\Rule\Nette\PresenterInjectedPropertiesExtension
tags:
- phpstan.properties.readWriteExtension

-
class: PHPStan\Type\Nette\StringsMatchDynamicReturnTypeExtension
60 changes: 60 additions & 0 deletions src/Type/Nette/StringsMatchDynamicReturnTypeExtension.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
<?php declare(strict_types = 1);

namespace PHPStan\Type\Nette;

use Nette\Utils\Strings;
use PhpParser\Node\Expr\StaticCall;
use PHPStan\Analyser\Scope;
use PHPStan\Reflection\MethodReflection;
use PHPStan\TrinaryLogic;
use PHPStan\Type\DynamicStaticMethodReturnTypeExtension;
use PHPStan\Type\NullType;
use PHPStan\Type\Php\RegexArrayShapeMatcher;
use PHPStan\Type\Type;
use PHPStan\Type\TypeCombinator;

class StringsMatchDynamicReturnTypeExtension implements DynamicStaticMethodReturnTypeExtension
{

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

public function __construct(RegexArrayShapeMatcher $regexArrayShapeMatcher)
{
$this->regexArrayShapeMatcher = $regexArrayShapeMatcher;
}

public function getClass(): string
{
return Strings::class;
}

public function isStaticMethodSupported(MethodReflection $methodReflection): bool
{
return $methodReflection->getName() === 'match';
}

public function getTypeFromStaticMethodCall(MethodReflection $methodReflection, StaticCall $methodCall, Scope $scope): ?Type
{
$args = $methodCall->getArgs();
$patternArg = $args[1] ?? null;
if ($patternArg === null) {
return null;
}

$patternType = $scope->getType($patternArg->value);
$flagsArg = $args[2] ?? null;
$flagsType = null;
if ($flagsArg !== null) {
$flagsType = $scope->getType($flagsArg->value);
}

$arrayShape = $this->regexArrayShapeMatcher->matchType($patternType, $flagsType, TrinaryLogic::createYes());
if ($arrayShape === null) {
return null;
}

return TypeCombinator::union($arrayShape, new NullType());
}

}
39 changes: 39 additions & 0 deletions tests/Type/Nette/StringsMatchDynamicReturnTypeExtensionTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
<?php declare(strict_types = 1);

namespace PHPStan\Type\Nette;

use PHPStan\Testing\TypeInferenceTestCase;

class StringsMatchDynamicReturnTypeExtensionTest extends TypeInferenceTestCase
{

/**
* @return iterable<string, mixed[]>
*/
public function dataFileAsserts(): iterable
{
yield from $this->gatherAssertTypes(__DIR__ . '/data/strings-match.php');

Check failure on line 15 in tests/Type/Nette/StringsMatchDynamicReturnTypeExtensionTest.php

View workflow job for this annotation

GitHub Actions / PHPStan (7.2, lowest)

Dynamic call to static method PHPStan\Testing\TypeInferenceTestCase::gatherAssertTypes().

Check failure on line 15 in tests/Type/Nette/StringsMatchDynamicReturnTypeExtensionTest.php

View workflow job for this annotation

GitHub Actions / PHPStan (7.3, lowest)

Dynamic call to static method PHPStan\Testing\TypeInferenceTestCase::gatherAssertTypes().

Check failure on line 15 in tests/Type/Nette/StringsMatchDynamicReturnTypeExtensionTest.php

View workflow job for this annotation

GitHub Actions / PHPStan (7.4, lowest)

Dynamic call to static method PHPStan\Testing\TypeInferenceTestCase::gatherAssertTypes().

Check failure on line 15 in tests/Type/Nette/StringsMatchDynamicReturnTypeExtensionTest.php

View workflow job for this annotation

GitHub Actions / PHPStan (8.0, lowest)

Dynamic call to static method PHPStan\Testing\TypeInferenceTestCase::gatherAssertTypes().

Check failure on line 15 in tests/Type/Nette/StringsMatchDynamicReturnTypeExtensionTest.php

View workflow job for this annotation

GitHub Actions / PHPStan (8.1, lowest)

Dynamic call to static method PHPStan\Testing\TypeInferenceTestCase::gatherAssertTypes().

Check failure on line 15 in tests/Type/Nette/StringsMatchDynamicReturnTypeExtensionTest.php

View workflow job for this annotation

GitHub Actions / PHPStan (8.2, lowest)

Dynamic call to static method PHPStan\Testing\TypeInferenceTestCase::gatherAssertTypes().
}

/**
* @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__ . '/phpstan.neon',
];
}

}
21 changes: 21 additions & 0 deletions tests/Type/Nette/data/strings-match.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<?php

namespace StringsMatch;

use Nette\Utils\Strings;
use function PHPStan\Testing\assertType;
use const PREG_OFFSET_CAPTURE;

function (string $s): void {
$result = Strings::match($s, '/%env\((.*)\:.*\)%/U');
assertType('array{string, string}|null', $result);

$result = Strings::match($s, '/%env\((.*)\:.*\)%/U');
assertType('array{string, string}|null', $result);

$result = Strings::match($s, '/(foo)(bar)(baz)/', PREG_OFFSET_CAPTURE);
assertType('array{array{string, int<0, max>}, array{string, int<0, max>}, array{string, int<0, max>}, array{string, int<0, max>}}|null', $result);

$result = Strings::match($s, '/(foo)(bar)(baz)/');
assertType('array{string, string, string, string}|null', $result);
};

0 comments on commit 3e68a5d

Please sign in to comment.