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
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,15 @@ and leans on several of its generics features:
via `@phpstan-assert-if-true`; `unwrap()`/`unwrapErr()` use conditional return
types (`never` on the impossible side), and `unwrapOr()`/`unwrapOrElse()`
resolve to `T` on `Ok` and to the default's type on `Err`.
- **Exhaustive error matching** — when the error type `E` is a native `enum` or a
`@phpstan-sealed` union, the error value can be matched exhaustively (over the
enum cases, or `instanceof` arms for a sealed union) and PHPStan enforces it — a
missing case becomes an analysis error. Reach the error through `isErr()` +
`unwrapErr()`, or the `match()` method's `err` arm; narrowing with
`instanceof Err` drops `E` to `mixed` and loses the enum/sealed type (the same
`instanceof` limitation as above). As long as the error classes are themselves
non-generic, their own `instanceof` checks have no type arguments to lose
(unlike the generic `Ok`/`Err`).
- **Precise concrete receivers** — when the receiver is statically `Ok<T>` or
`Err<E>`, no-op methods keep their exact type (`$ok->orElse(...)` stays
`Ok<T>`, `$err->andThen(...)` stays `Err<E>`) instead of widening to a union.
Expand Down
95 changes: 95 additions & 0 deletions tests/Types/result.php
Original file line number Diff line number Diff line change
Expand Up @@ -348,3 +348,98 @@ function testMap(Result $result): void
$result->mapErr(static fn (RuntimeException $e): LogicException => new LogicException($e->getMessage())),
);
}

/**
* エラー側の網羅性テスト用フィクスチャ: native enum.
*/
enum HttpError
{
case NotFound;
case Forbidden;
}

/**
* エラー側の網羅性テスト用フィクスチャ: @phpstan-sealed なエラー union.
*
* @phpstan-sealed ValidationFailure|NetworkFailure
*/
interface AppError
{
}

final class ValidationFailure implements AppError
{
}

final class NetworkFailure implements AppError
{
}

/**
* エラー型が native enum の場合: isErr() 経由なら unwrapErr() が enum 型を保ち、
* 全ケースを網羅する match は default なしで網羅と認識される.
* (いずれかのケースを落とすと phpstan analyse がエラーになるため、これ自体が網羅性のピン留めになる)
*
* @param Result<int, HttpError> $result
*/
function testEnumErrorExhaustiveness(Result $result): string
{
if ($result->isErr()) {
$error = $result->unwrapErr();
assertType('Valbeat\Result\Tests\Types\HttpError', $error);

return match ($error) {
HttpError::NotFound => 'not found',
HttpError::Forbidden => 'forbidden',
};
Comment thread
valbeat marked this conversation as resolved.
}

return 'ok';
}

/**
* エラー型が @phpstan-sealed union の場合: isErr() 経由で取り出した値に対する
* match(true)+instanceof が網羅と認識される(sealed 指定が前提。エラークラスは非ジェネリックなので型引数喪失は起きない).
*
* @param Result<int, AppError> $result
*/
function testSealedErrorExhaustiveness(Result $result): string
{
if ($result->isErr()) {
$error = $result->unwrapErr();
assertType('Valbeat\Result\Tests\Types\AppError', $error);

return match (true) {
$error instanceof ValidationFailure => 'validation',
$error instanceof NetworkFailure => 'network',
};
}

return 'ok';
}

/**
* 既知の落とし穴のピン留め(enum エラー): instanceof Err では E が失われ、
* enum であっても unwrapErr() は mixed になる。値を扱う分岐は isErr() を使う(上の2ケース参照).
*
* @param Result<int, HttpError> $result
*/
function testInstanceofErrLosesEnumErrorType(Result $result): void
{
if ($result instanceof Err) {
assertType('mixed', $result->unwrapErr());
}
}

/**
* 既知の落とし穴のピン留め(sealed エラー): sealed union でも instanceof Err では
* E が失われ unwrapErr() は mixed になる.
*
* @param Result<int, AppError> $result
*/
function testInstanceofErrLosesSealedErrorType(Result $result): void
{
if ($result instanceof Err) {
assertType('mixed', $result->unwrapErr());
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}
Loading