Skip to content

Commit

Permalink
[Regex] Introduce matching() function
Browse files Browse the repository at this point in the history
  • Loading branch information
veewee committed Apr 9, 2021
1 parent 963a0bb commit fbf4788
Show file tree
Hide file tree
Showing 12 changed files with 560 additions and 15 deletions.
2 changes: 1 addition & 1 deletion docs/component/json.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,6 @@

- [decode](./../../src/Psl/Json/decode.php#L24)
- [encode](./../../src/Psl/Json/encode.php#L27)
- [typed](./../../src/Psl/Json/typed.php#L22)
- [typed](./../../src/Psl/Json/typed.php#L20)


3 changes: 3 additions & 0 deletions docs/component/regex.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@

#### `Functions`

- [capture_groups](./../../src/Psl/Regex/capture_groups.php#L17)
- [every_match](./../../src/Psl/Regex/every_match.php#L25)
- [first_match](./../../src/Psl/Regex/first_match.php#L24)
- [matches](./../../src/Psl/Regex/matches.php#L19)
- [replace](./../../src/Psl/Regex/replace.php#L26)
- [replace_every](./../../src/Psl/Regex/replace_every.php#L27)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
<?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\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();
}

$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();
}

$string = static fn (): Type\Union => new Type\Union([new Type\Atomic\TString()]);
$properties = [
0 => $string()
];
foreach ($capture_groups->properties as $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] = $string();
}

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()])
])
])
])]);
}
}
2 changes: 2 additions & 0 deletions integration/Psalm/Plugin.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,11 @@ final class Plugin implements PluginEntryPointInterface
{
public function __invoke(RegistrationInterface $registration, ?SimpleXMLElement $config = null): void
{
require_once __DIR__ . '/EventHandler/RegexCaptureGroupsFunctionReturnTypeProvider.php';
require_once __DIR__ . '/EventHandler/OptionalFunctionReturnTypeProvider.php';
require_once __DIR__ . '/EventHandler/ShapeFunctionReturnTypeProvider.php';

$registration->registerHooksFromClass(EventHandler\RegexCaptureGroupsFunctionReturnTypeProvider::class);
$registration->registerHooksFromClass(EventHandler\OptionalFunctionReturnTypeProvider::class);
$registration->registerHooksFromClass(EventHandler\ShapeFunctionReturnTypeProvider::class);
}
Expand Down
3 changes: 3 additions & 0 deletions src/Psl/Internal/Loader.php
Original file line number Diff line number Diff line change
Expand Up @@ -258,6 +258,9 @@ final class Loader
'Psl\Math\tan',
'Psl\Math\to_base',
'Psl\Result\wrap',
'Psl\Regex\capture_groups',
'Psl\Regex\every_match',
'Psl\Regex\first_match',
'Psl\Regex\split',
'Psl\Regex\matches',
'Psl\Regex\replace',
Expand Down
19 changes: 5 additions & 14 deletions src/Psl/Json/typed.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,33 +4,24 @@

namespace Psl\Json;

use Psl\Type\Exception\AssertException;
use Psl\Type\Exception\CoercionException;
use Psl\Type\TypeInterface;
use Psl\Type;

/**
* Decode a json encoded string into a dynamic variable.
*
* @template T
*
* @param TypeInterface<T> $type
* @param Type\TypeInterface<T> $type
*
* @throws Exception\DecodeException If an error occurred.
*
* @return T
*/
function typed(string $json, TypeInterface $type)
function typed(string $json, Type\TypeInterface $type)
{
$value = decode($json);

try {
return $type->assert($value);
} catch (AssertException $e) {
}

try {
return $type->coerce($value);
} catch (CoercionException $e) {
return $type->coerce(decode($json));
} catch (Type\Exception\CoercionException $e) {
throw new Exception\DecodeException($e->getMessage(), (int)$e->getCode(), $e);
}
}
28 changes: 28 additions & 0 deletions src/Psl/Regex/capture_groups.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<?php

declare(strict_types=1);

namespace Psl\Regex;

use Psl\Dict;
use Psl\Type;

/**
* @param list<array-key> $groups
*
* @return Type\TypeInterface<array<array-key, string>>
*
* @psalm-suppress MixedReturnTypeCoercion - Psalm loses track of the keys. No worries, another psalm plugin fixes this!
*/
function capture_groups(array $groups): Type\TypeInterface
{
return Type\shape(
Dict\from_keys(
Dict\unique([0, ...$groups]),
/**
* @return Type\TypeInterface<string>
*/
static fn(): Type\TypeInterface => Type\string()
)
);
}
52 changes: 52 additions & 0 deletions src/Psl/Regex/every_match.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
<?php

declare(strict_types=1);

namespace Psl\Regex;

use Psl\Exception\InvariantViolationException;
use Psl\Type;

use function preg_match_all;

/**
* Determine if $subject matches the given $pattern and return every matches.
*
* @template T of array|null
*
* @param non-empty-string $pattern The pattern to match against.
* @param ?Type\TypeInterface<T> $capture_groups What shape does a single set of matching items have?
*
* @throws Exception\RuntimeException If an internal error accord.
* @throws Exception\InvalidPatternException If $pattern is invalid.
*
* @return (T is null ? list<array<array-key, string>> : list<T>)|null
*/
function every_match(
string $subject,
string $pattern,
?Type\TypeInterface $capture_groups = null,
int $offset = 0
): ?array {
$matching = Internal\call_preg(
'preg_match_all',
static function () use ($subject, $pattern, $offset): ?array {
$matching = [];
$matches = preg_match_all($pattern, $subject, $matching, PREG_SET_ORDER, $offset);

return $matches === 0 ? null : $matching;
}
);

if ($matching === null) {
return null;
}

$capture_groups ??= Type\dict(Type\array_key(), Type\string());

try {
return Type\vec($capture_groups)->coerce($matching);
} catch (InvariantViolationException | Type\Exception\CoercionException $e) {
throw new Exception\RuntimeException('Invalid capture groups', 0, $e);
}
}
51 changes: 51 additions & 0 deletions src/Psl/Regex/first_match.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
<?php

declare(strict_types=1);

namespace Psl\Regex;

use Psl\Type;

use function preg_match;

/**
* Determine if $subject matches the given $pattern and return the first matches.
*
* @template T of array|null
*
* @param non-empty-string $pattern The pattern to match against.
* @param ?Type\TypeInterface<T> $capture_groups What shape does the matching items have?
*
* @throws Exception\RuntimeException If an internal error accord.
* @throws Exception\InvalidPatternException If $pattern is invalid.
*
* @return (T is null ? array<array-key, string> : T)|null
*/
function first_match(
string $subject,
string $pattern,
?Type\TypeInterface $capture_groups = null,
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);

return $matches === 0 ? null : $matching;
}
);

if ($matching === null) {
return null;
}

$capture_groups ??= Type\dict(Type\array_key(), Type\string());

try {
return $capture_groups->coerce($matching);
} catch (Type\Exception\CoercionException $e) {
throw new Exception\RuntimeException('Invalid capture groups', 0, $e);
}
}
21 changes: 21 additions & 0 deletions tests/Psl/Regex/CaptureGroupsTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<?php

declare(strict_types=1);

namespace Psl\Tests\Regex;

use PHPUnit\Framework\TestCase;

use function Psl\Regex\capture_groups;

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);
}
}
Loading

0 comments on commit fbf4788

Please sign in to comment.