Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@ jobs:
- "7.4"
- "8.0"
- "8.1"
- "8.2"
- "8.3"
- "8.4"
- "8.5"

steps:
- name: "Checkout"
Expand Down Expand Up @@ -77,6 +81,10 @@ jobs:
- "7.4"
- "8.0"
- "8.1"
- "8.2"
- "8.3"
- "8.4"
- "8.5"
dependencies:
- "lowest"
- "highest"
Expand Down Expand Up @@ -113,6 +121,10 @@ jobs:
- "7.4"
- "8.0"
- "8.1"
- "8.2"
- "8.3"
- "8.4"
- "8.5"
dependencies:
- "lowest"
- "highest"
Expand Down
2 changes: 2 additions & 0 deletions extension.neon
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
parameters:
stubFiles:
- stubs/Option.stub
- stubs/nullish.stub
- stubs/NullishType.stub
- stubs/optional.stub
- stubs/OptionalType.stub
- stubs/Type.stub
Expand Down
12 changes: 12 additions & 0 deletions phpstan-baseline-psl-1.neon
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,15 @@ parameters:
identifier: argument.type
count: 1
path: src/Option/OptionFilterReturnTypeExtension.php

-
message: '#^Class Psl\\Type\\Internal\\NullishType not found\.$#'
identifier: class.notFound
count: 1
path: src/Type/TypeShapeReturnTypeExtension.php

-
message: '#^Parameter \#1 \$ancestorClassName of method PHPStan\\Type\\Type\:\:getTemplateType\(\) expects class\-string, string given\.$#'
identifier: argument.type
count: 1
path: src/Type/TypeShapeReturnTypeExtension.php
18 changes: 17 additions & 1 deletion src/Type/TypeShapeReturnTypeExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
use PHPStan\Type\Generic\GenericObjectType;
use PHPStan\Type\Type;
use PHPStan\Type\TypeCombinator;
use Psl\Type\Internal\NullishType;
use Psl\Type\Internal\OptionalType;
use Psl\Type\TypeInterface;
use function count;
Expand Down Expand Up @@ -55,14 +56,29 @@ private function createResult(ConstantArrayType $arrayType): Type
$builder = ConstantArrayTypeBuilder::createEmpty();
foreach ($arrayType->getKeyTypes() as $key) {
$valueType = $arrayType->getOffsetValueType($key);
[$type, $optional] = $this->extractOptional($valueType->getTemplateType(TypeInterface::class, 'T'));
$templateType = $valueType->getTemplateType(TypeInterface::class, 'T');
[$type, $optional] = $this->extractOptional($templateType);
[$type] = $this->extractNullish($type);

$builder->setOffsetValueType($key, $type, $optional);
}

return $builder->getArray();
}

/**
* @return array{Type, bool}
*/
private function extractNullish(Type $type): array
{
$nullishType = $type->getTemplateType(NullishType::class, 'T');
if ($nullishType instanceof ErrorType) {
return [$type, false];
}

return [TypeCombinator::addNull($nullishType), false];
}

/**
* @return array{Type, bool}
*/
Expand Down
17 changes: 17 additions & 0 deletions stubs/NullishType.stub
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<?php

namespace Psl\Type\Internal;

use Psl\Type;

/**
* @template T
*
* @extends Type\Type<T>
*
* @internal
*/
final class NullishType extends Type\Type
{

}
17 changes: 17 additions & 0 deletions stubs/nullish.stub
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<?php

namespace Psl\Type;

use Psl\Type\Internal\NullishType;

/**
* @template T
*
* @param TypeInterface<T> $inner_type
*
* @return TypeInterface<NullishType<T>>
*/
function nullish(TypeInterface $inner_type): TypeInterface
{

}
7 changes: 7 additions & 0 deletions tests/Type/PslTypeSpecifyingExtensionTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
use Composer\InstalledVersions;
use Composer\Semver\VersionParser;
use PHPStan\Testing\TypeInferenceTestCase;
use Psl\Type\Internal\NullishType;
use function class_exists;

class PslTypeSpecifyingExtensionTest extends TypeInferenceTestCase
{
Expand All @@ -17,6 +19,11 @@ public function dataFileAsserts(): iterable
yield from $this->gatherAssertTypes(__DIR__ . '/data/coerce.php');
yield from $this->gatherAssertTypes(__DIR__ . '/data/assert.php');
yield from $this->gatherAssertTypes(__DIR__ . '/data/matches.php');
if (class_exists(NullishType::class)) {
yield from $this->gatherAssertTypes(__DIR__ . '/data/nullishCoerce.php');
yield from $this->gatherAssertTypes(__DIR__ . '/data/nullishAssert.php');
yield from $this->gatherAssertTypes(__DIR__ . '/data/nullishMatches.php');
}
if (InstalledVersions::satisfies(new VersionParser(), 'azjezz/psl', '<2.0.0')) {
yield from $this->gatherAssertTypes(__DIR__ . '/data/complexTypev1.php');
} else {
Expand Down
41 changes: 41 additions & 0 deletions tests/Type/data/nullishAssert.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
<?php declare(strict_types=1);

namespace PslShapeTest;

use Psl\Type;

use function PHPStan\Testing\assertType;

class NullishAssertTest
{
/**
* @param array<mixed> $a
*/
public function assertNullishShape(array $a): void
{
$specification = Type\shape([
'name' => Type\string(),
'bio' => Type\nullish(Type\string()),
]);

$b = $specification->assert($a);

assertType('array{name: string, bio: string|null}', $a);
assertType('array{name: string, bio: string|null}', $b);
}

/**
* @param array<mixed> $a
*/
public function assertOptionalNullishShape(array $a): void
{
$specification = Type\shape([
'bio' => Type\optional(Type\nullish(Type\string())),
]);

$b = $specification->assert($a);

assertType('array{bio?: string|null}', $a);
assertType('array{bio?: string|null}', $b);
}
}
39 changes: 39 additions & 0 deletions tests/Type/data/nullishCoerce.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
<?php declare(strict_types=1);

namespace PslShapeTest;

use Psl\Type;

use function PHPStan\Testing\assertType;

class NullishCoerceTest
{
/**
* @param array<mixed> $input
*/
public function coerceNullishShape(array $input): void
{
$specification = Type\shape([
'name' => Type\string(),
'bio' => Type\nullish(Type\string()),
]);

$output = $specification->coerce($input);

assertType('array{name: string, bio: string|null}', $output);
}

/**
* @param array<mixed> $input
*/
public function coerceOptionalNullishShape(array $input): void
{
$specification = Type\shape([
'bio' => Type\optional(Type\nullish(Type\string())),
]);

$output = $specification->coerce($input);

assertType('array{bio?: string|null}', $output);
}
}
43 changes: 43 additions & 0 deletions tests/Type/data/nullishMatches.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
<?php declare(strict_types=1);

namespace PslShapeTest;

use Psl\Type;

use function PHPStan\Testing\assertType;

class NullishMatchesTest
{
/**
* @param array<mixed> $a
*/
public function matchesNullishShape(array $a): void
{
$specification = Type\shape([
'name' => Type\string(),
'bio' => Type\nullish(Type\string()),
]);

if ($specification->matches($a)) {
assertType('array{name: string, bio: string|null}', $a);
} else {
assertType('array<mixed>', $a);
}
}

/**
* @param array<mixed> $a
*/
public function matchesOptionalNullishShape(array $a): void
{
$specification = Type\shape([
'bio' => Type\optional(Type\nullish(Type\string())),
]);

if ($specification->matches($a)) {
assertType('array{bio?: string|null}', $a);
} else {
assertType('non-empty-array<mixed>', $a);
}
}
}
Loading