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
22 changes: 21 additions & 1 deletion documentation/components/libs/types/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ The DSL definition can thus be considered the library's API, located in the
file [functions.php](/src/lib/types/src/Flow/Types/DSL/functions.php).

One of the library's key principles is tight integration with static code analysis tools. This is achieved through
proper use of template mechanisms and type narrowing, `@template` and `@phpstan-assert`.
proper use of template mechanisms and type narrowing, `@template` and `@assert`.

More details can be found at:

Expand All @@ -81,6 +81,26 @@ More details can be found at:

Therefore, it is critical that all classes and functions in this library are properly documented in PHPDoc.

Two rules follow from that, and both are load-bearing rather than stylistic.

**Every implementation restates `@return` when its own type is narrower than its native signature.** Mago resolves a
return type from the implementation's docblock, never from the interface's — an implementation that declares
`assert(mixed $value): string` and omits `@return non-empty-string` silently hands back `string`, discarding exactly
the narrowing the library exists to provide. This produces no diagnostic on its own: the resulting program is valid,
and nothing observes an inferred type.
[`TypeNarrowing`](/src/lib/types/tests/Flow/Types/Tests/Unit/Type/Fixtures/TypeNarrowing.php) funnels every `type_*()`
through `assert()`, `cast()` and `isValid()` into sinks that accept only the narrow type, so `just analyze` fails on
the line where a narrowing is lost.

**Every implementation declares `@template T of X` and `@implements Type<T>`, including those where nothing can bind
`T` to anything but its own bound.** These templates look like ceremony and are not. Mago's `flow-php` analyzer plugin
builds a `type_structure()` shape by reading type parameter 0 off each element's concrete class; an element class with
no type parameters makes the plugin discard the *entire* shape, degrading it to `array<array-key, mixed>` — again with
no diagnostic.
[`StructureShapeInference`](/src/lib/types/tests/Flow/Types/Tests/Unit/Type/Fixtures/StructureShapeInference.php)
pins those shapes so `just analyze` fails if this is broken. Removing the templates requires an upstream mago change
that resolves the element through the `Flow\Types\Type` template instead.

## Testing

This library provides a set of unit tests that verify the correct operation of all types and DSL functions.
Expand Down
6 changes: 3 additions & 3 deletions src/lib/types/src/Flow/Types/Type.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ interface Type
*
* @return T
*
* @phpstan-assert T $value
* @assert T $value
*/
public function assert(mixed $value): mixed;

Expand All @@ -39,10 +39,10 @@ public function cast(mixed $value): mixed;
/**
* Checks if the value is of the type of this object, returning a boolean instead of throwing.
* When this method returns true, static analysis tools narrow the value's type to T at the call site
* (via @phpstan-assert-if-true). Use this when you want to branch on the result; use assert() when you
* (via @assert-if-true). Use this when you want to branch on the result; use assert() when you
* want the call to fail loudly on a mismatch.
*
* @phpstan-assert-if-true T $value
* @assert-if-true T $value
*/
public function isValid(mixed $value): bool;

Expand Down
4 changes: 2 additions & 2 deletions src/lib/types/src/Flow/Types/Type/ArrayContentDetector.php
Original file line number Diff line number Diff line change
Expand Up @@ -75,15 +75,15 @@ public function firstValueType(): ?Type
}

/**
* @phpstan-assert-if-true Type<int> $this->firstKeyType()
* @assert-if-true Type<int> $this->firstKeyType()
*/
public function isList(): bool
{
return 1 === $this->uniqueValuesTypeCount && $this->firstKeyType() instanceof IntegerType && $this->isList;
}

/**
* @phpstan-assert-if-true Type<int|string> $this->firstKeyType()
* @assert-if-true Type<int|string> $this->firstKeyType()
*/
public function isMap(): bool
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@
*/
final class NonEmptyStringType implements Type
{
/**
* @return non-empty-string
*/
public function assert(mixed $value): string
{
if ($this->isValid($value)) {
Expand All @@ -38,6 +41,9 @@ public function assert(mixed $value): string
throw InvalidTypeException::value($value, $this);
}

/**
* @return non-empty-string
*/
public function cast(mixed $value): string
{
if ($this->isValid($value)) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@
*/
final readonly class PositiveIntegerType implements Type
{
/**
* @return int<0, max>
*/
public function assert(mixed $value): int
{
if ($this->isValid($value)) {
Expand All @@ -32,6 +35,9 @@ public function assert(mixed $value): int
throw InvalidTypeException::value($value, $this);
}

/**
* @return int<0, max>
*/
public function cast(mixed $value): int
{
if ($this->isValid($value)) {
Expand Down
3 changes: 3 additions & 0 deletions src/lib/types/src/Flow/Types/Type/Logical/UuidType.php
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,9 @@ public function assert(mixed $value): Uuid
throw InvalidTypeException::value($value, $this);
}

/**
* @return Uuid
*/
public function cast(mixed $value): mixed
{
if ($this->isValid($value)) {
Expand Down
6 changes: 5 additions & 1 deletion src/lib/types/src/Flow/Types/Type/Native/EnumType.php
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,9 @@ public function assert(mixed $value): UnitEnum
throw InvalidTypeException::value($value, $this);
}

/**
* @return T
*/
public function cast(mixed $value): UnitEnum
{
if ($this->isValid($value)) {
Expand All @@ -95,8 +98,9 @@ public function cast(mixed $value): UnitEnum
throw new CastingException($value, $this);
}

// is_a() above narrows $enumClass to class-string<BackedEnum>, so ::from() loses T; assert() rebinds it.
// @mago-ignore analysis:possibly-static-access-on-interface
return $enumClass::from($value);
return $this->assert($enumClass::from($value));
}

throw new CastingException($value, $this);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,9 @@ public function assert(mixed $value): mixed
return $value;
}

/**
* @return TLeft&TRight
*/
public function cast(mixed $value): mixed
{
if ($this->isValid($value)) {
Expand Down
3 changes: 3 additions & 0 deletions src/lib/types/src/Flow/Types/Type/Native/UnionType.php
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,9 @@ public function assert(mixed $value): mixed
throw InvalidTypeException::value($value, $this);
}

/**
* @return TLeft|TRight
*/
public function cast(mixed $value): mixed
{
if ($this->isValid($value)) {
Expand Down
Loading
Loading