Skip to content

Commit

Permalink
Implement runtime type assertion for iterables
Browse files Browse the repository at this point in the history
  • Loading branch information
drjayvee committed Oct 20, 2024
1 parent 3ea3d01 commit 6419e34
Show file tree
Hide file tree
Showing 3 changed files with 73 additions and 1 deletion.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ they are rendered. This is the aim of a similar project: [TwigStan](https://gith
* ✅ Invalid type declared (e.g., `{% types {i: 'nit'} %}`)
* ✅ Context contains variable (unless defined as optional)
* ✅ Runtime variable is null when type is not nullable
* Runtime type doesn't match declaration
* Runtime type doesn't match declaration
* ⌛ Invalid object property or method (e.g., `{{ user.nmae }}`)
* ⌛ Undocumented context variable (i.e., missing in `{% types %}`)
* ⌛ Use of short-hand form (e.g., `{{ user.admin }}` instead of `isAdmin`) [Notice]
Expand Down
31 changes: 31 additions & 0 deletions src/Assertion/AssertType.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,18 @@ final class AssertType
{
public static function matches($value, string $type): bool
{
// convert iterable short form to canonical form (e.g., `string[]` => `iterable<string>`)
if (str_ends_with($type, '[]')) {
$type = 'iterable<' . substr($type, 0, -2) . '>';
}

if (str_starts_with($type, 'iterable<')) {
$matches = [];
preg_match('/<((string|number),\s*)?(.+)>/', substr($type, 8), $matches);
[, , $keyType, $valueType] = $matches;
return self::iterableMatches($value, $valueType, $keyType ?: null);
}

return match ($type) {
'string' => is_string($value),
'number' => is_int($value) || is_float($value),
Expand All @@ -15,4 +27,23 @@ public static function matches($value, string $type): bool
default => true,
};
}

private static function iterableMatches($iterable, string $valueType, ?string $keyType = null): bool
{
if (!is_iterable($iterable)) {
return false;
}

foreach ($iterable as $key => $value) {
if (!self::matches($value, $valueType)) {
return false;
}

if ($keyType !== null && !self::matches($key, $keyType)) {
return false;
}
}

return true;
}
}
41 changes: 41 additions & 0 deletions tests/TypeAssertionsTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

namespace AlisQI\TwigQI\Tests;

use ArrayIterator;
use Exception;

class TypeAssertionsTest extends AbstractTestCase
Expand Down Expand Up @@ -100,6 +101,46 @@ public static function getTypes(): array
['object', 'object', false],
['object', true, false],
['object', [], false],

['iterable', [], true],
['iterable', [13, 37], true],
['iterable', new ArrayIterator([13, 37]), true],
['iterable', ['foo' => 'bar'], true],
['iterable', 'hello', false],

['iterable<string>', [], true],
['iterable<string>', ['hello'], true],
['iterable<string>', new ArrayIterator(['hello']), true],
['iterable<string>', [1337], false],
['iterable<string>', 'hello', false],

['string[]', [], true],
['string[]', ['foo'], true],
['string[]', 'foo', false],
['string[]', [1337], false],

['iterable<string, string>', [], true],
['iterable<string, string>', ['foo' => 'bar'], true],
['iterable<string, string>', new ArrayIterator(['foo' => 'bar']), true],
['iterable<string, string>', ['foo' => 1337], false],
['iterable<string, string>', ['foo' => ['bar']], false],
['iterable<string, string>', [13, 37], false],

['iterable<number, number>', [], true],
['iterable<number, number>', [13, 37], true],
['iterable<number, number>', [13 => 37], true],
['iterable<number, number>', new ArrayIterator([13 => 37]), true],
['iterable<number, number>', ['13' => 37], true],
['iterable<number, number>', ['leet' => 1337], false],

['iterable<string, iterable<string>>', ['foo' => ['bar']], true],
['iterable<string, iterable<number>>', ['foo' => [13, 37]], true],
['iterable<string, iterable<string>>', ['foo' => [13, 37]], false],

['iterable<iterable<iterable<string, number>>>', [[[]]], true],
['iterable<iterable<iterable<string, number>>>', [[['foo' => 1337]]], true],
['iterable<iterable<iterable<string, number>>>', [[[13, 37]]], false],
['iterable<iterable<iterable<string, number>>>', [[['foo' => 'bar']]], false],
];
}

Expand Down

0 comments on commit 6419e34

Please sign in to comment.