-
-
Notifications
You must be signed in to change notification settings - Fork 72
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[Regex] Introduce matching() function
- Loading branch information
Showing
7 changed files
with
281 additions
and
0 deletions.
There are no files selected for viewing
76 changes: 76 additions & 0 deletions
76
integration/Psalm/EventHandler/RegexCaptureGroupsFunctionReturnTypeProvider.php
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,76 @@ | ||
<?php | ||
|
||
declare(strict_types=1); | ||
|
||
namespace Psl\Integration\Psalm\EventHandler; | ||
|
||
use Psalm\Plugin\EventHandler\Event\FunctionReturnTypeProviderEvent; | ||
use Psalm\Plugin\EventHandler\FunctionReturnTypeProviderInterface; | ||
use Psalm\Type; | ||
|
||
final class RegexCaptureGroupsFunctionReturnTypeProvider implements FunctionReturnTypeProviderInterface | ||
{ | ||
/** | ||
* @return array<lowercase-string> | ||
*/ | ||
public static function getFunctionIds(): array | ||
{ | ||
return [ | ||
'psl\regex\type\capture_groups' | ||
]; | ||
} | ||
|
||
public static function getFunctionReturnType(FunctionReturnTypeProviderEvent $event): ?Type\Union | ||
{ | ||
$statements_source = $event->getStatementsSource(); | ||
$call_args = $event->getCallArgs(); | ||
|
||
$argument = $call_args[0] ?? null; | ||
if (null === $argument) { | ||
return self::fallbackType(); | ||
} | ||
|
||
$type = null; | ||
$argument_value = $argument->value; | ||
$type = $statements_source->getNodeTypeProvider()->getType($argument_value); | ||
if (null === $type) { | ||
return self::fallbackType(); | ||
} | ||
|
||
$atomic = $type->getAtomicTypes(); | ||
$capture_groups = $atomic['array'] ?? null; | ||
if (!$capture_groups instanceof Type\Atomic\TKeyedArray) { | ||
return self::fallbackType(); | ||
} | ||
|
||
$properties = []; | ||
foreach ($capture_groups->properties as $index => $value) { | ||
$type = array_values($value->getAtomicTypes())[0] ?? null; | ||
if (!$type instanceof Type\Atomic\TLiteralInt && !$type instanceof Type\Atomic\TLiteralString) { | ||
return self::fallbackType(); | ||
} | ||
|
||
$name = $type->value; | ||
|
||
$properties[$name] = new Type\Union([new Type\Atomic\TString()]); | ||
} | ||
|
||
return new Type\Union([new Type\Atomic\TGenericObject('Psl\Type\TypeInterface', [ | ||
new Type\Union([ | ||
new Type\Atomic\TKeyedArray($properties) | ||
]) | ||
])]); | ||
} | ||
|
||
private static function fallbackType(): Type\Union | ||
{ | ||
return new Type\Union([new Type\Atomic\TGenericObject('Psl\Type\TypeInterface', [ | ||
new Type\Union([ | ||
new Type\Atomic\TArray([ | ||
new Type\Union([new Type\Atomic\TArrayKey()]), | ||
new Type\Union([new Type\Atomic\TString()]) | ||
]) | ||
]) | ||
])]); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
<?php | ||
|
||
declare(strict_types=1); | ||
|
||
namespace Psl\Regex\Type; | ||
|
||
use Psl\Type\TypeInterface; | ||
|
||
use function Psl\Dict\from_keys; | ||
use function Psl\Dict\unique; | ||
use function Psl\Type\shape; | ||
use function Psl\Type\string; | ||
|
||
/** | ||
* @param list<array-key> $groups | ||
* | ||
* @return TypeInterface<array<array-key, string>> | ||
* | ||
* @psalm-suppress MixedReturnTypeCoercion - Psalm uses track of the types: Trust the return type above. | ||
*/ | ||
function capture_groups(array $groups): TypeInterface | ||
{ | ||
return shape( | ||
from_keys( | ||
unique([0, ...$groups]), | ||
/** | ||
* @return TypeInterface<string> | ||
*/ | ||
static fn() => string() | ||
) | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,46 @@ | ||
<?php | ||
|
||
declare(strict_types=1); | ||
|
||
namespace Psl\Regex; | ||
|
||
use Psl\Regex\Exception\RuntimeException; | ||
use Psl\Type\Exception\CoercionException; | ||
use Psl\Type\TypeInterface; | ||
|
||
use function preg_match; | ||
|
||
/** | ||
* Determine if $subject matches the given $pattern and return the matches. | ||
* | ||
* @template T of array | ||
* | ||
* @param non-empty-string $pattern The pattern to match against. | ||
* @param TypeInterface<T> $capture_groups What shape does the matching items have? | ||
* | ||
* @return T | ||
* | ||
* @throws Exception\RuntimeException If an internal error accord. | ||
* @throws CoercionException If the capture groups don't match the pattern matches | ||
* @throws Exception\InvalidPatternException If $pattern is invalid. | ||
*/ | ||
function matching(string $subject, string $pattern, TypeInterface $capture_groups, int $offset = 0): array | ||
{ | ||
$matching = Internal\call_preg( | ||
'preg_match', | ||
static function () use ($subject, $pattern, $offset): array { | ||
$matching = []; | ||
$matches = preg_match($pattern, $subject, $matching, 0, $offset); | ||
|
||
if ($matches === 0) { | ||
throw new RuntimeException( | ||
'Could not match the pattern ' . $pattern . ' to the given input: ' . $subject . '.' | ||
); | ||
} | ||
|
||
return $matching; | ||
} | ||
); | ||
|
||
return $capture_groups->coerce($matching); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,103 @@ | ||
<?php | ||
|
||
declare(strict_types=1); | ||
|
||
namespace Psl\Tests\Regex; | ||
|
||
use PHPUnit\Framework\TestCase; | ||
use Psl\Regex; | ||
use Psl\Type\TypeInterface; | ||
|
||
use function Psl\Regex\Type\capture_groups; | ||
|
||
final class MatchingTest extends TestCase | ||
{ | ||
/** | ||
* @dataProvider provideMatchingData | ||
*/ | ||
public function testMatching( | ||
array $expected, | ||
string $subject, | ||
string $pattern, | ||
TypeInterface $shape, | ||
int $offset = 0 | ||
): void { | ||
static::assertSame($expected, Regex\matching($subject, $pattern, $shape, $offset)); | ||
} | ||
|
||
/** | ||
* @dataProvider provideNonMatchingData | ||
*/ | ||
public function testNotMatching(string $subject, string $pattern, int $offset = 0) | ||
{ | ||
$this->expectException(Regex\Exception\RuntimeException::class); | ||
$this->expectErrorMessage('Could not match the pattern ' . $pattern . ' to the given input: ' . $subject . '.'); | ||
|
||
Regex\matching($subject, $pattern, capture_groups([]), $offset); | ||
} | ||
|
||
public function testMatchingWithInvalidPattern(): void | ||
{ | ||
$this->expectException(Regex\Exception\InvalidPatternException::class); | ||
$this->expectExceptionMessage("No ending delimiter '/' found"); | ||
|
||
Regex\matching('hello', '/hello', capture_groups([])); | ||
} | ||
|
||
public function provideMatchingData(): iterable | ||
{ | ||
yield [ | ||
[ | ||
0 => 'PHP', | ||
1 => 'PHP', | ||
], | ||
'PHP is the web scripting language of choice.', | ||
'/(php)/i', | ||
capture_groups([0, 1]) | ||
]; | ||
yield [ | ||
[ | ||
0 => 'Hello world', | ||
1 => 'Hello', | ||
], | ||
'Hello world is the web scripting language of choice.', | ||
'/(hello) world/i', | ||
capture_groups([0, 1]) | ||
]; | ||
yield [ | ||
[ | ||
0 => 'web', | ||
1 => 'web', | ||
], | ||
'PHP is the web scripting language of choice.', | ||
'/(\bweb\b)/i', | ||
capture_groups([0, 1]) | ||
]; | ||
yield [ | ||
[ | ||
0 => 'PHP', | ||
'language' => 'PHP', | ||
], | ||
'PHP is the web scripting language of choice.', | ||
'/(?P<language>PHP)/', | ||
capture_groups([0, 'language']) | ||
]; | ||
yield [ | ||
[ | ||
0 => 'http://www.php.net', | ||
1 => 'www.php.net' | ||
], | ||
'http://www.php.net/index.html', | ||
'@^(?:http://)?([^/]+)@i', | ||
capture_groups([1]) | ||
]; | ||
} | ||
|
||
public function provideNonMatchingData(): iterable | ||
{ | ||
yield ['PHP is the web scripting language of choice.', '/php/']; | ||
yield ['PHP is the website scripting language of choice.', '/\bweb\b/i']; | ||
yield ['php is the web scripting language of choice.', '/PHP/']; | ||
yield ['hello', '/[^.]+\.[^.]+$/']; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
<?php | ||
|
||
declare(strict_types=1); | ||
|
||
namespace Psl\Regex\Type; | ||
|
||
use PHPUnit\Framework\TestCase; | ||
|
||
final class CaptureGroupsTest extends TestCase | ||
{ | ||
|
||
public function testItAlwaysAddsZeroCaptureResult(): void | ||
{ | ||
$data = [0 => 'Hello', 1 => 'World']; | ||
$shape = capture_groups([1]); | ||
$actual = $shape->coerce($data); | ||
|
||
static::assertSame($actual, $data); | ||
} | ||
} |