Skip to content

Commit 901d0e4

Browse files
authored
use texthtml/object-reaper (#59)
1 parent 2daee07 commit 901d0e4

File tree

12 files changed

+105
-102
lines changed

12 files changed

+105
-102
lines changed

composer.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,8 @@
2121
}
2222
],
2323
"require": {
24-
"php": "^8.1"
24+
"php": "^8.1",
25+
"texthtml/object-reaper": "^1.0"
2526
},
2627
"require-dev": {
2728
"phpunit/phpunit": "^10.0|^11.0|^12.0",

phpstan.neon.dist

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ parameters:
77
check:
88
missingCheckedExceptionInThrows: true
99
tooWideThrowType: true
10+
uncheckedExceptionClasses:
11+
- LogicException
1012
ignoreErrors:
1113
-
1214
message: '#Unreachable statement - code above always terminates.#'

psalm.xml

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,15 @@
2626
</projectFiles>
2727

2828
<issueHandlers>
29+
<MissingOverrideAttribute>
30+
<errorLevel type="suppress">
31+
<directory name="src" />
32+
<directory name="tests" />
33+
</errorLevel>
34+
</MissingOverrideAttribute>
2935
<MissingThrowsDocblock>
3036
<errorLevel type="suppress">
31-
<directory name="tests/Unit" />
37+
<directory name="tests" />
3238
</errorLevel>
3339
</MissingThrowsDocblock>
3440
<MissingClosureReturnType>
@@ -73,4 +79,7 @@
7379
</errorLevel>
7480
</InvalidArgument>
7581
</issueHandlers>
82+
<ignoreExceptions>
83+
<classAndDescendants name="LogicException" />
84+
</ignoreExceptions>
7685
</psalm>

src/Option/Some.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -198,7 +198,7 @@ public function zip(Option $option): Option
198198
* @param Option<U> $option
199199
* @param callable(T, U):V $callback
200200
* @return (U is never ? Option\None : Option<V>)
201-
* @return Option<V>
201+
* @phpstan-return Option<V>
202202
*/
203203
public function zipWith(Option $option, callable $callback): Option
204204
{

src/Result/MustBeUsed.php

Lines changed: 18 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -2,64 +2,46 @@
22

33
namespace TH\Maybe\Result;
44

5-
use TH\Maybe\Result;
5+
use TH\ObjectReaper\Reaper;
66

77
/** @internal */
88
trait MustBeUsed
99
{
10-
/** @var \ArrayAccess<Result<mixed,mixed>,ResultCreationTrace>|null */
11-
private static ?\ArrayAccess $toBeUsed = null;
12-
13-
/**
14-
* @return \ArrayAccess<Result<mixed,mixed>,ResultCreationTrace>
15-
*/
16-
private static function toBeUsedMap(): \ArrayAccess
10+
/** @return \WeakMap<static,Reaper> */
11+
private static function reapers(): \WeakMap
1712
{
18-
return self::$toBeUsed ??= self::emptyToBeUsedMap();
19-
}
13+
static $reapers;
2014

21-
/**
22-
* @return \WeakMap<Result<mixed,mixed>,ResultCreationTrace>
23-
*/
24-
private static function emptyToBeUsedMap(): \WeakMap
25-
{
26-
/** @var \WeakMap<Result<mixed,mixed>,ResultCreationTrace> */
27-
return new \WeakMap();
15+
return $reapers ??= new \WeakMap();
2816
}
2917

3018
/**
31-
* Mark a result as needed to be used
19+
* Mark a result as needed to be used. Must be called at most once on an object.
3220
*/
3321
private function mustBeUsed(): void
3422
{
35-
self::toBeUsedMap()[$this] = new ResultCreationTrace();
23+
$reapers = self::reapers();
24+
25+
if (isset($reapers[$this])) {
26+
throw new \LogicException('Object already register to be used');
27+
}
28+
29+
$creationTrace = new ResultCreationTrace();
30+
31+
$reapers[$this] = Reaper::watch($this, static fn () => throw new UnusedResultException($creationTrace));
3632
}
3733

3834
/**
39-
* Mark a result as used
35+
* Mark a result as used.
4036
*/
4137
private function used(): void
4238
{
43-
unset(self::toBeUsedMap()[$this]);
39+
$reaper = self::reapers()[$this] ?? throw new \LogicException('Object not registered to be used');
40+
$reaper->forget();
4441
}
4542

4643
public function __clone()
4744
{
4845
$this->mustBeUsed();
4946
}
50-
51-
/**
52-
* @throws UnusedResultException
53-
*/
54-
public function __destruct()
55-
{
56-
$map = self::toBeUsedMap();
57-
58-
if (isset($map[$this])) {
59-
$creationTrace = $map[$this];
60-
unset($map[$this]);
61-
62-
throw new UnusedResultException($creationTrace);
63-
}
64-
}
6547
}

src/Result/ResultCreationTrace.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
namespace TH\Maybe\Result;
44

55
/** @internal */
6-
final class ResultCreationTrace extends \RuntimeException
6+
final class ResultCreationTrace extends \LogicException
77
{
88
public function __construct()
99
{

src/functions/internal.php

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
<?php declare(strict_types=1);
2+
3+
namespace TH\Maybe\Internal;
4+
5+
/**
6+
* Call $callback with $exception if it matches one of $exceptionClasses
7+
* and return its value, or rethrow it otherwise.
8+
*
9+
* @template E of \Throwable
10+
* @template T
11+
* @param E $error
12+
* @param callable(E): T $callback
13+
* @param class-string<E> $exceptionClasses
14+
* @throws \Throwable
15+
* @return T
16+
* @internal
17+
* @nodoc
18+
*/
19+
function trap(
20+
\Throwable $error,
21+
callable $callback,
22+
string ...$exceptionClasses,
23+
): mixed {
24+
foreach ($exceptionClasses as $exceptionClass) {
25+
if (\is_a($error, $exceptionClass)) {
26+
return $callback($error);
27+
}
28+
}
29+
30+
throw $error;
31+
}

src/functions/option.php

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -209,8 +209,7 @@ function unzip(Option $option): array
209209
function transpose(Option $option): Result
210210
{
211211
/** @var Result<Option<U>, E> */
212-
return $option->mapOrElse( // @phpstan-ignore varTag.type
213-
// @phpstan-ignore-next-line
212+
return $option->mapOrElse(
214213
static fn (Result $result) => $result->map(Option\some(...)),
215214
static fn () => Result\ok(Option\none()),
216215
);

src/functions/result.php

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -156,8 +156,7 @@ function flatten(Result $result): Result
156156
function transpose(Result $result): Option
157157
{
158158
/** @var Option<Result<U, F>> */
159-
return $result->mapOrElse( // @phpstan-ignore varTag.type
160-
// @phpstan-ignore-next-line
159+
return $result->mapOrElse(
161160
static fn (Option $option) => $option->map(Result\ok(...)),
162161
static fn () => Option\some(clone $result),
163162
);

tests/Constraint/HasBeen.php

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
use PHPUnit\Framework\Constraint\Constraint;
66
use TH\Maybe\Result;
7+
use TH\ObjectReaper\Reaper;
78

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

3031
/**
31-
* @param Result<mixed,mixed> $result
32+
* @template T of Result<mixed,mixed>
33+
* @param T $result
3234
*/
3335
protected function toBeUsed(Result $result): bool
3436
{
3537
$ro = new \ReflectionObject($result);
3638

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

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

46-
return isset($toBeUsedMap[$result]);
46+
/** @throws void */
47+
$rp = $ro->getProperty('active');
48+
/** @psalm-suppress UnusedMethodCall */
49+
$rp->setAccessible(true);
50+
51+
$result = $rp->getValue($reaper);
52+
53+
\assert(\is_bool($result));
54+
55+
return $result;
4756
}
4857
}

0 commit comments

Comments
 (0)