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
3 changes: 2 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@
}
],
"require": {
"php": "^8.1"
"php": "^8.1",
"texthtml/object-reaper": "^1.0"
},
"require-dev": {
"phpunit/phpunit": "^10.0|^11.0|^12.0",
Expand Down
2 changes: 2 additions & 0 deletions phpstan.neon.dist
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ parameters:
check:
missingCheckedExceptionInThrows: true
tooWideThrowType: true
uncheckedExceptionClasses:
- LogicException
ignoreErrors:
-
message: '#Unreachable statement - code above always terminates.#'
Expand Down
11 changes: 10 additions & 1 deletion psalm.xml
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,15 @@
</projectFiles>

<issueHandlers>
<MissingOverrideAttribute>
<errorLevel type="suppress">
<directory name="src" />
<directory name="tests" />
</errorLevel>
</MissingOverrideAttribute>
<MissingThrowsDocblock>
<errorLevel type="suppress">
<directory name="tests/Unit" />
<directory name="tests" />
</errorLevel>
</MissingThrowsDocblock>
<MissingClosureReturnType>
Expand Down Expand Up @@ -73,4 +79,7 @@
</errorLevel>
</InvalidArgument>
</issueHandlers>
<ignoreExceptions>
<classAndDescendants name="LogicException" />
</ignoreExceptions>
</psalm>
2 changes: 1 addition & 1 deletion src/Option/Some.php
Original file line number Diff line number Diff line change
Expand Up @@ -198,7 +198,7 @@ public function zip(Option $option): Option
* @param Option<U> $option
* @param callable(T, U):V $callback
* @return (U is never ? Option\None : Option<V>)
* @return Option<V>
* @phpstan-return Option<V>
*/
public function zipWith(Option $option, callable $callback): Option
{
Expand Down
54 changes: 18 additions & 36 deletions src/Result/MustBeUsed.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,64 +2,46 @@

namespace TH\Maybe\Result;

use TH\Maybe\Result;
use TH\ObjectReaper\Reaper;

/** @internal */
trait MustBeUsed
{
/** @var \ArrayAccess<Result<mixed,mixed>,ResultCreationTrace>|null */
private static ?\ArrayAccess $toBeUsed = null;

/**
* @return \ArrayAccess<Result<mixed,mixed>,ResultCreationTrace>
*/
private static function toBeUsedMap(): \ArrayAccess
/** @return \WeakMap<static,Reaper> */
private static function reapers(): \WeakMap
{
return self::$toBeUsed ??= self::emptyToBeUsedMap();
}
static $reapers;

/**
* @return \WeakMap<Result<mixed,mixed>,ResultCreationTrace>
*/
private static function emptyToBeUsedMap(): \WeakMap
{
/** @var \WeakMap<Result<mixed,mixed>,ResultCreationTrace> */
return new \WeakMap();
return $reapers ??= new \WeakMap();
}

/**
* Mark a result as needed to be used
* Mark a result as needed to be used. Must be called at most once on an object.
*/
private function mustBeUsed(): void
{
self::toBeUsedMap()[$this] = new ResultCreationTrace();
$reapers = self::reapers();

if (isset($reapers[$this])) {
throw new \LogicException('Object already register to be used');
}

$creationTrace = new ResultCreationTrace();

$reapers[$this] = Reaper::watch($this, static fn () => throw new UnusedResultException($creationTrace));
}

/**
* Mark a result as used
* Mark a result as used.
*/
private function used(): void
{
unset(self::toBeUsedMap()[$this]);
$reaper = self::reapers()[$this] ?? throw new \LogicException('Object not registered to be used');
$reaper->forget();
}

public function __clone()
{
$this->mustBeUsed();
}

/**
* @throws UnusedResultException
*/
public function __destruct()
{
$map = self::toBeUsedMap();

if (isset($map[$this])) {
$creationTrace = $map[$this];
unset($map[$this]);

throw new UnusedResultException($creationTrace);
}
}
}
2 changes: 1 addition & 1 deletion src/Result/ResultCreationTrace.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
namespace TH\Maybe\Result;

/** @internal */
final class ResultCreationTrace extends \RuntimeException
final class ResultCreationTrace extends \LogicException
{
public function __construct()
{
Expand Down
31 changes: 31 additions & 0 deletions src/functions/internal.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<?php declare(strict_types=1);

namespace TH\Maybe\Internal;

/**
* Call $callback with $exception if it matches one of $exceptionClasses
* and return its value, or rethrow it otherwise.
*
* @template E of \Throwable
* @template T
* @param E $error
* @param callable(E): T $callback
* @param class-string<E> $exceptionClasses
* @throws \Throwable
* @return T
* @internal
* @nodoc
*/
function trap(
\Throwable $error,
callable $callback,
string ...$exceptionClasses,
): mixed {
foreach ($exceptionClasses as $exceptionClass) {
if (\is_a($error, $exceptionClass)) {
return $callback($error);
}
}

throw $error;
}
3 changes: 1 addition & 2 deletions src/functions/option.php
Original file line number Diff line number Diff line change
Expand Up @@ -209,8 +209,7 @@ function unzip(Option $option): array
function transpose(Option $option): Result
{
/** @var Result<Option<U>, E> */
return $option->mapOrElse( // @phpstan-ignore varTag.type
// @phpstan-ignore-next-line
return $option->mapOrElse(
static fn (Result $result) => $result->map(Option\some(...)),
static fn () => Result\ok(Option\none()),
);
Expand Down
3 changes: 1 addition & 2 deletions src/functions/result.php
Original file line number Diff line number Diff line change
Expand Up @@ -156,8 +156,7 @@ function flatten(Result $result): Result
function transpose(Result $result): Option
{
/** @var Option<Result<U, F>> */
return $result->mapOrElse( // @phpstan-ignore varTag.type
// @phpstan-ignore-next-line
return $result->mapOrElse(
static fn (Option $option) => $option->map(Result\ok(...)),
static fn () => Option\some(clone $result),
);
Expand Down
27 changes: 18 additions & 9 deletions tests/Constraint/HasBeen.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

use PHPUnit\Framework\Constraint\Constraint;
use TH\Maybe\Result;
use TH\ObjectReaper\Reaper;

final class HasBeen extends Constraint
{
Expand All @@ -28,21 +29,29 @@ protected function matches(mixed $other): bool
}

/**
* @param Result<mixed,mixed> $result
* @template T of Result<mixed,mixed>
* @param T $result
*/
protected function toBeUsed(Result $result): bool
{
$ro = new \ReflectionObject($result);

/**
* @phpstan-throws void
* @psalm-suppress MissingThrowsDocblock
*/
$rp = $ro->getProperty("toBeUsed");
/** @throws void */
$reapers = $ro->getMethod('reapers')->invoke(null);
/** @var \WeakMap<T,Reaper> $reapers */
$reaper = $reapers[$result];

/** @var \ArrayAccess<Result<mixed, mixed>, mixed> $toBeUsedMap */
$toBeUsedMap = $rp->getValue(null);
$ro = new \ReflectionObject($reaper);

return isset($toBeUsedMap[$result]);
/** @throws void */
$rp = $ro->getProperty('active');
/** @psalm-suppress UnusedMethodCall */
$rp->setAccessible(true);

$result = $rp->getValue($reaper);

\assert(\is_bool($result));

return $result;
}
}
67 changes: 20 additions & 47 deletions tests/Helpers/IgnoreUnusedResults.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,75 +2,48 @@

namespace TH\Maybe\Tests\Helpers;

use TH\Maybe\Result;
use TH\Maybe\Result\Err;
use TH\Maybe\Result\Ok;
use TH\Maybe\Result\ResultCreationTrace;
use TH\ObjectReaper\Reaper;

/**
* @internal
*/
final class IgnoreUnusedResults
{
/** @var \SplObjectStorage<\ReflectionProperty, \ArrayAccess<mixed,mixed>|null> */
private \SplObjectStorage $unusedResults;
public function __construct()
{
$this->clearReapers();
}

/**
* Don't check whether new Results are used or not.
* @return iterable<Reaper>
*/
public function __construct()
private function reapers(): iterable
{
$this->unusedResults = new \SplObjectStorage();

foreach ($this->properties() as $rp) {
$this->unusedResults[$rp] = $rp->isInitialized()
? $rp->getValue()
: null;

$rp->setValue(
null,
new
/**
* @implements \ArrayAccess<Result<mixed,mixed>,ResultCreationTrace>
*/
class implements \ArrayAccess {
public function offsetExists(mixed $offset): bool
{
return false;
}

public function offsetGet(mixed $offset): mixed
{
return null;
}
foreach ([Ok::class, Err::class] as $className) {
/** @phpstan-throws void */
$rm = (new \ReflectionClass($className))->getMethod("reapers");

public function offsetSet(mixed $offset, mixed $value): void
{
}
/**
* @var iterable<Reaper> $reapers
* @throws void
*/
$reapers = $rm->invoke(null);

public function offsetUnset(mixed $offset): void
{
}
},
);
yield from $reapers;
}
}

/**
* @return iterable<\ReflectionProperty>
*/
private function properties(): iterable
private function clearReapers(): void
{
foreach ([Ok::class, Err::class] as $className) {
/** @phpstan-throws void */
yield (new \ReflectionClass($className))->getProperty("toBeUsed");
foreach ($this->reapers() as $reaper) {
$reaper->forget();
}
}

public function __destruct()
{
foreach ($this->unusedResults as $rp) {
$rp->setValue(null, $this->unusedResults[$rp] ?? new \WeakMap());
}
$this->clearReapers();
}
}
2 changes: 0 additions & 2 deletions tests/Unit/Result/ConvertToOptionTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -76,9 +76,7 @@ public function testTranspose(Option $option, Result $expected): void
Assert::assertEquals($option, $option2 = Result\transpose($expected));

Assert::assertResultUsed($expected);
// @phpstan-ignore-next-line
$option->map(Assert::assertResultNotUsed(...));
// @phpstan-ignore-next-line
$option2->map(Assert::assertResultNotUsed(...));
}
}