Skip to content

Commit

Permalink
phpunit warnings and exceptions
Browse files Browse the repository at this point in the history
  • Loading branch information
huntervk committed Feb 5, 2024
1 parent be732f0 commit 976c5ce
Show file tree
Hide file tree
Showing 21 changed files with 110 additions and 27 deletions.
49 changes: 49 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,55 @@ Hamlet Type

## Motivation

The support for types in the PHP type system can be split into two big domains: static analysis time and runtime.
The PHP interpreter and the static analyzers like Psalm and PHPStan have different levels of support for the types.

On the language level there are three general mechanisms for type support: hinting, assertions and cascading.

The hinting can be done at compile time or run time. The compile time hinting is done through a hierarchy of different
mechansisms:
- the lowest level (lowest in terms of the level of support for different types) is the type hints,
- followed by phpdoc,
- followed by Psalm specific types.

```php
/**
* @return array<string>
* @psalm-return array<non-empty-string>
*/
public function number(): array ...
```

Additionally, there's a support for type hints by use of type expression in this library

```php
/**
* @template T
* @param mixed $value
* @param Type<T> $type
* @return T
*/
function convert(mixed $value, Type $type): mixed ...
```

By wrapping the `Type<T>`, you can use the type hinting to enforce the return type.

The compile time type hinting is only useful if you run static analysis tools, like Psalm or PHPStan.















The PHP type system is a complicated beast. PHP supports types in three ways: hinting, assertions, and casting.

To unwrap the complexity, it's easiest to start with the basic type `int`. The type hinting support is thorough;
Expand Down
12 changes: 11 additions & 1 deletion phpunit.xml
Original file line number Diff line number Diff line change
@@ -1,5 +1,15 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/10.5/phpunit.xsd" bootstrap="vendor/autoload.php" executionOrder="depends,defects" cacheDirectory=".phpunit.cache">
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/10.5/phpunit.xsd"
bootstrap="vendor/autoload.php"
executionOrder="depends,defects"
displayDetailsOnIncompleteTests="true"
displayDetailsOnSkippedTests="true"
displayDetailsOnTestsThatTriggerDeprecations="true"
displayDetailsOnTestsThatTriggerErrors="true"
displayDetailsOnTestsThatTriggerNotices="true"
displayDetailsOnTestsThatTriggerWarnings="true"
cacheDirectory=".phpunit.cache">
<testsuites>
<testsuite name="default">
<directory suffix="Test.php">tests</directory>
Expand Down
2 changes: 1 addition & 1 deletion src/Parser/DocBlockAstTraverser.php
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ private function traverseTupleNode(ArrayShapeNode $node, ?NameContext $nameConte
{
$fields = [];
foreach ($node->items as $item) {
if ($item->keyName->name) {
if ($item->keyName?->name) {

Check failure on line 128 in src/Parser/DocBlockAstTraverser.php

View workflow job for this annotation

GitHub Actions / build

UndefinedPropertyFetch

src/Parser/DocBlockAstTraverser.php:128:17: UndefinedPropertyFetch: Instance property PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprIntegerNode::$name is not defined (see https://psalm.dev/039)

Check failure on line 128 in src/Parser/DocBlockAstTraverser.php

View workflow job for this annotation

GitHub Actions / build

UndefinedPropertyFetch

src/Parser/DocBlockAstTraverser.php:128:17: UndefinedPropertyFetch: Instance property PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprStringNode::$name is not defined (see https://psalm.dev/039)

Check failure on line 128 in src/Parser/DocBlockAstTraverser.php

View workflow job for this annotation

GitHub Actions / build

RiskyTruthyFalsyComparison

src/Parser/DocBlockAstTraverser.php:128:17: RiskyTruthyFalsyComparison: Operand of type mixed|null|string contains types mixed|string, which can be falsy and truthy. This can cause possibly unexpected behavior. Use strict comparison instead. (see https://psalm.dev/356)

Check failure on line 128 in src/Parser/DocBlockAstTraverser.php

View workflow job for this annotation

GitHub Actions / Check PHP syntax

UndefinedPropertyFetch

src/Parser/DocBlockAstTraverser.php:128:17: UndefinedPropertyFetch: Instance property PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprIntegerNode::$name is not defined (see https://psalm.dev/039)

Check failure on line 128 in src/Parser/DocBlockAstTraverser.php

View workflow job for this annotation

GitHub Actions / Check PHP syntax

UndefinedPropertyFetch

src/Parser/DocBlockAstTraverser.php:128:17: UndefinedPropertyFetch: Instance property PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprStringNode::$name is not defined (see https://psalm.dev/039)

Check failure on line 128 in src/Parser/DocBlockAstTraverser.php

View workflow job for this annotation

GitHub Actions / Check PHP syntax

RiskyTruthyFalsyComparison

src/Parser/DocBlockAstTraverser.php:128:17: RiskyTruthyFalsyComparison: Operand of type mixed|null|string contains types mixed|string, which can be falsy and truthy. This can cause possibly unexpected behavior. Use strict comparison instead. (see https://psalm.dev/356)
throw new RuntimeException('Unsupported object like arrays: ' . $node);
}
$fields[] = $this->traverse($item->valueType, $nameContext);
Expand Down
6 changes: 5 additions & 1 deletion src/Types/NonEmptyStringType.php
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,11 @@
if (is_object($value) && !method_exists($value, '__toString')) {
throw new CastException($value, $this);
}
$stringValue = (string) $value;
if (is_array($value)) {
$stringValue = 'Array';
} else {
$stringValue = (string) $value;
}
if ($stringValue === '') {
throw new CastException($value, $this);
}
Expand Down
1 change: 1 addition & 0 deletions src/Types/TupleType.php
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ public function __construct(array $fields)
}

$result = [];
$value = array_values($value);
foreach ($this->fields as $i => $field) {
$result[] = $field->resolveAndCast($value[$i], $resolver);
}
Expand Down
11 changes: 11 additions & 0 deletions src/Types/UnionType.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,10 @@
*/
public function __construct(array $options)
{
$options = array_unique($options);
usort($options, fn (Type $a, Type $b) =>
$a instanceof NullType <=> $b instanceof NullType
);
$this->options = $options;
}

Expand All @@ -42,6 +46,13 @@ public function __construct(array $options)
if ($this->matches($value)) {
return $value;
}
if (count($this->options) == 2 && $this->options[1] instanceof NullType) {
if ($value === null) {
return null;
} else {
return $this->options[0]->resolveAndCast($value, $resolver);
}
}
throw new CastException($value, $this);
}

Expand Down
8 changes: 4 additions & 4 deletions tests/Parser/ParserTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ public static function typeDeclarations(): array
#[DataProvider('typeDeclarations')] public function testTypeParser(string $specification): void
{
$type = type_of($specification);
Assert::assertNotNull($type);
$this->assertNotNull($type);
}

public static function phpDocDeclarations(): array
Expand Down Expand Up @@ -118,7 +118,7 @@ public static function phpDocDeclarations(): array
#[DataProvider('phpDocDeclarations')] public function testPhpDocParser(string $specification)
{
$data = DocBlockParser::parseDoc($specification);
Assert::assertNotNull($data);
$this->assertNotNull($data);
}

/**
Expand All @@ -134,8 +134,8 @@ public function testNameResolver(): void
$typeA = DocBlockParser::fromProperty($type, $type->getProperty('a'));
$typeB = DocBlockParser::fromProperty($type, $type->getProperty('b'));

Assert::assertEquals('array<int,array<list{DateTime}>>', (string) $typeA);
Assert::assertEquals("'x'|'y'|'z'|Hamlet\Type\CastException|DateTime|null", (string) $typeB);
$this->assertEquals('array<int,array<list{DateTime}>>', (string)$typeA);
$this->assertEquals("'x'|'y'|'z'|Hamlet\Type\CastException|DateTime|null", (string)$typeB);
}

#[DataProvider('typeDeclarations')] public function testSerialization(string $specification): void
Expand Down
2 changes: 1 addition & 1 deletion tests/TypeDeclarationTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ public function testTypeString(): void
_class(DateTime::class)
)
);
$this->assertEquals('array<int,null|DateTime>', (string)$type);
$this->assertEquals('array<int,DateTime|null>', (string)$type);
}

public function testLiteralType(): void
Expand Down
2 changes: 1 addition & 1 deletion tests/Types/ArrayKeyTypeTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ protected function strictCastResultComparison(): bool
protected function baselineCast(mixed $value): int|string|null
{
try {
$a = [
$a = @[
$value => 1
];
return array_key_first($a);
Expand Down
4 changes: 2 additions & 2 deletions tests/Types/ArrayTypeTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,10 @@ protected function type(): Type
protected function baselineCast(mixed $value): array
{
try {
$arrayValue = is_array($value) ? $value : (array) $value;
$arrayValue = is_array($value) ? $value : (array)$value;
$expectedResult = [];
foreach ($arrayValue as $key => $property) {
$expectedResult[$key] = (int) $property;
$expectedResult[$key] = @(int)$property;
}
return $expectedResult;
} catch (Error) {
Expand Down
2 changes: 1 addition & 1 deletion tests/Types/BoolTypeTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ protected function type(): Type
protected function baselineCast(mixed $value): bool
{
try {
return (bool) $value;
return (bool)$value;
} catch (TypeError) {
throw new RuntimeException;
}
Expand Down
3 changes: 3 additions & 0 deletions tests/Types/CastCasesTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,8 @@ public function __toString()
[[null, 1 => null]],
[[false, 'barbeque' => 'sausage']],
[['sausage', 2 => 3.1415, true]],
[['sausage', 2 => [0, 0 => null]]],
[[1 => [1 => [1 => [1 => [1 => [1 => [1 => [1 => [1 => []]]]]]]]]]],
[new stdClass],
[$object],
[$stringableObject],
Expand All @@ -131,6 +133,7 @@ public function __toString()
[null],
[$intGenerator()],
[$stringGenerator()],
[[1 => new stdClass, 2 => $object, 3 => $stringableObject, 4 => $callable, 5 => $resource, 6 => null]],
];
}
}
4 changes: 2 additions & 2 deletions tests/Types/FloatTypeTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,10 @@ protected function type(): Type
return _float();
}

protected function baselineCast(mixed $value): mixed
protected function baselineCast(mixed $value): float
{
try {
return (float) $value;
return @(float)$value;
} catch (TypeError) {
throw new RuntimeException;
}
Expand Down
2 changes: 1 addition & 1 deletion tests/Types/IntTypeTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ protected function type(): Type
protected function baselineCast(mixed $value): int
{
try {
return (int) $value;
return @(int)$value;
} catch (TypeError) {
throw new RuntimeException;
}
Expand Down
4 changes: 2 additions & 2 deletions tests/Types/NonEmptyArrayTypeTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,13 @@ protected function type(): Type
protected function baselineCast(mixed $value): array
{
try {
$arrayValue = is_array($value) ? $value : (array) $value;
$arrayValue = is_array($value) ? $value : (array)$value;
if (count($arrayValue) == 0) {
throw new RuntimeException;
} else {
$expectedResult = [];
foreach ($arrayValue as $key => $property) {
$expectedResult[$key] = (int) $property;
$expectedResult[$key] = @(int)$property;
}
return $expectedResult;
}
Expand Down
2 changes: 1 addition & 1 deletion tests/Types/NonEmptyListTypeTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ protected function baselineCast(mixed $value): array
} else {
$expectedResult = [];
foreach ($arrayValue as $property) {
$expectedResult[] = (int) $property;
$expectedResult[] = @(int)$property;
}
return $expectedResult;
}
Expand Down
2 changes: 1 addition & 1 deletion tests/Types/NonEmptyStringTypeTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ protected function type(): Type
protected function baselineCast(mixed $value): string
{
try {
$stringValue = (string) $value;
$stringValue = @(string)$value;
if ($stringValue !== '') {
return $stringValue;
} else {
Expand Down
2 changes: 1 addition & 1 deletion tests/Types/NumericStringTypeTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ protected function type(): Type
protected function baselineCast(mixed $value): string
{
try {
$stringValue = (string) $value;
$stringValue = @(string)$value;
if (is_numeric($stringValue)) {
return $stringValue;
} else {
Expand Down
2 changes: 1 addition & 1 deletion tests/Types/StringTypeTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ protected function type(): Type
protected function baselineCast(mixed $value): string
{
try {
return (string) $value;
return @(string)$value;
} catch (Error) {
throw new RuntimeException;
}
Expand Down
9 changes: 4 additions & 5 deletions tests/Types/TupleTypeTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,17 +19,16 @@ protected function type(): Type
return _tuple(_int(), _string());
}

protected function baselineCast(mixed $value): mixed
protected function baselineCast(mixed $value): array
{
try {
$arrayValue = is_array($value) ? $value : (array) $value;
$arrayValue = array_values(is_array($value) ? $value : (array) $value);
if (count($arrayValue) == 2) {
return [(int) $arrayValue[0], (string) $arrayValue[1]];
} else {
throw new RuntimeException;
return [@(int)$arrayValue[0], @(string)$arrayValue[1]];
}
} catch (Error) {
throw new RuntimeException;
}
throw new RuntimeException;
}
}
8 changes: 7 additions & 1 deletion tests/Types/UnionTypeTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,14 @@

use DateTime;
use Exception;
use Hamlet\Type\CastException;
use Hamlet\Type\Type;
use InvalidArgumentException;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\TestCase;
use RuntimeException;
use stdClass;
use function Hamlet\Type\_bool;
use function Hamlet\Type\_class;
use function Hamlet\Type\_int;
use function Hamlet\Type\_literal;
use function Hamlet\Type\_null;
Expand Down Expand Up @@ -162,4 +162,10 @@ public function testNullableTailsFail8(): void
$this->expectException(InvalidArgumentException::class);
_union(_literal(1), _literal(2), _literal(3), _literal(4), _literal(5), _literal(6), _literal(7), null);
}

public function testCollapsingAndSorting(): void
{
$type = _union(_null(), _int(), _class(DateTime::class), _null(), _int(), _string());
$this->assertEquals('int|DateTime|string|null', (string) $type);
}
}

0 comments on commit 976c5ce

Please sign in to comment.