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: 3 additions & 0 deletions src/Internal/Ast/ContractVisitor.php
Original file line number Diff line number Diff line change
Expand Up @@ -191,11 +191,13 @@ private function enterClassLike(Node\Stmt\Class_|Node\Stmt\Interface_|Node\Stmt\
$typeName = $this->resolveQualifiedName($node->name);
$hasInheritance = true;
$hasPropertyWithDoc = false;
$isReadonly = false;

if ($node instanceof Node\Stmt\Class_) {
$hasExtends = $node->extends !== null;
$hasImplements = $node->implements !== [];
$hasTraits = false;
$isReadonly = ($node->flags & Node\Stmt\Class_::MODIFIER_READONLY) !== 0;

foreach ($node->stmts as $stmt) {
if ($stmt instanceof Node\Stmt\TraitUse) {
Expand All @@ -222,6 +224,7 @@ private function enterClassLike(Node\Stmt\Class_|Node\Stmt\Interface_|Node\Stmt\
'isAnonymous' => ($node instanceof Node\Stmt\Class_ && $node->name === null),
'hasInheritance' => $hasInheritance,
'hasPropertyWithDoc' => $hasPropertyWithDoc,
'isReadonly' => $isReadonly,
];
}

Expand Down
18 changes: 12 additions & 6 deletions src/Internal/Ast/FunctionContractInjector.php
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ public static function inject(Node\Stmt\Function_|Node\Stmt\ClassMethod $node, ?

$hasInheritance = $classContext['hasInheritance'] ?? true;
$hasPropertyWithDoc = $classContext['hasPropertyWithDoc'] ?? true;
$isReadonlyClass = $classContext['isReadonly'] ?? false;

$methodName = $isClassMethod ? strtolower($node->name->toString()) : '';
$isConstructor = $isClassMethod && $methodName === '__construct';
Expand Down Expand Up @@ -100,7 +101,7 @@ public static function inject(Node\Stmt\Function_|Node\Stmt\ClassMethod $node, ?

$injectedStmts = [];
if ($hasParam) {
$injectedStmts = self::buildParamInjections($node->params, $docText, $thisArg);
$injectedStmts = self::buildParamInjections($node->params, $docText, $thisArg, $isReadonlyClass);
}

if ($hasReturn || $hasParamOut || $hasSelfOut) {
Expand Down Expand Up @@ -322,11 +323,12 @@ public function enterNode(Node $n): ?int
private static function buildParamInjections(
array $params,
string $docText,
Node\Expr $thisArg
Node\Expr $thisArg,
bool $isReadonlyClass = false
): array {
$injectedStmts = [self::buildSetupScopeStmt($params, $thisArg)];
$callableWrappers = self::buildParamWrappers($params, $docText, $thisArg, [self::class, 'isCallableCandidate'], 'wrapCallable');
$iterableWrappers = self::buildParamWrappers($params, $docText, $thisArg, [self::class, 'isIterableCandidate'], 'wrapIterable');
$callableWrappers = self::buildParamWrappers($params, $docText, $thisArg, [self::class, 'isCallableCandidate'], 'wrapCallable', $isReadonlyClass);
$iterableWrappers = self::buildParamWrappers($params, $docText, $thisArg, [self::class, 'isIterableCandidate'], 'wrapIterable', $isReadonlyClass);

return [...$injectedStmts, ...$callableWrappers, ...$iterableWrappers];
}
Expand Down Expand Up @@ -457,7 +459,8 @@ private static function buildParamWrappers(
string $docText,
Node\Expr $thisArg,
callable $predicate,
string $wrapperMethod
string $wrapperMethod,
bool $isReadonlyClass = false
): array {
$wrappers = [];

Expand All @@ -481,7 +484,10 @@ private static function buildParamWrappers(
$expr->setAttribute('typephp_injected', true);
$wrappers[] = $expr;

if ($param->isPromoted()) {
$isReadonlyParam = ($param->flags & Node\Stmt\Class_::MODIFIER_READONLY) !== 0;
$isReadonly = $isReadonlyParam || $isReadonlyClass;

if ($param->isPromoted() && ! $isReadonly) {
$propAssign = new Node\Stmt\Expression(
new Node\Expr\Assign(
new Node\Expr\PropertyFetch(new Node\Expr\Variable('this'), $paramName),
Expand Down
2 changes: 1 addition & 1 deletion tests/Fixtures/Forwarding/AbstractBox.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,4 @@
*/
abstract class AbstractBox implements BoxInterface
{
}
}
2 changes: 1 addition & 1 deletion tests/Fixtures/Forwarding/BoxConsumer.php
Original file line number Diff line number Diff line change
Expand Up @@ -69,4 +69,4 @@ public function passThrough(BoxInterface $box): BoxInterface
{
return $box;
}
}
}
2 changes: 1 addition & 1 deletion tests/Fixtures/Forwarding/BoxInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,4 @@
*/
interface BoxInterface
{
}
}
2 changes: 1 addition & 1 deletion tests/Fixtures/Forwarding/ConcreteBox.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,4 @@
*/
final class ConcreteBox extends AbstractBox
{
}
}
2 changes: 1 addition & 1 deletion tests/Fixtures/Forwarding/ItemA.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,4 @@ final class ItemA implements ItemBase
public function __construct(public string $name = 'A')
{
}
}
}
2 changes: 1 addition & 1 deletion tests/Fixtures/Forwarding/ItemB.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,4 @@ final class ItemB implements ItemBase
public function __construct(public string $name = 'B')
{
}
}
}
2 changes: 1 addition & 1 deletion tests/Fixtures/Forwarding/ItemBase.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,4 @@

interface ItemBase
{
}
}
77 changes: 77 additions & 0 deletions tests/TypeChecking/Boundaries/PromotedReadonlyPropertyTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
<?php

declare(strict_types=1);

namespace TypePHP\Tests\TypeChecking\Boundaries;

use Closure;
use TypePHP\Exception\TypeError;

interface ReadonlyAdapterInterface
{
}

final class ReadonlyAdapterA implements ReadonlyAdapterInterface
{
}

final class ReadonlyAdapterInvalid
{
}

final class PromotedReadonlyIterable
{
/**
* @param iterable<ReadonlyAdapterInterface> $adapters
*/
public function __construct(public readonly iterable $adapters)
{
}
}

final class PromotedReadonlyClosure
{
/**
* @param Closure(int): int $factory
*/
public function __construct(public readonly Closure $factory)
{
}
}

readonly class ReadonlyClassWithPromotedProperty
{
/**
* @param iterable<ReadonlyAdapterInterface> $adapters
*/
public function __construct(public iterable $adapters)
{
}
}

describe('Promoted Readonly Property Contracts', function () {
test('instantiates class with promoted readonly iterable without throwing Cannot modify readonly property error', function () {
$instance = new PromotedReadonlyIterable([new ReadonlyAdapterA()]);

expect($instance->adapters)->toHaveCount(1);
});

test('instantiates class with promoted readonly closure without throwing Cannot modify readonly property error', function () {
$fn = static fn (int $i): int => $i + 1;
$instance = new PromotedReadonlyClosure($fn);

expect($instance->factory)->toBe($fn);
});

test('instantiates PHP 8.2 readonly class with promoted property without throwing Cannot modify readonly property error', function () {
$instance = new ReadonlyClassWithPromotedProperty([new ReadonlyAdapterA()]);

expect($instance->adapters)->toHaveCount(1);
});

test('still validates incoming constructor arguments on promoted readonly properties upon entry', function () {
expect(fn () => new PromotedReadonlyIterable([new ReadonlyAdapterInvalid()]))
->toThrow(TypeError::class)
;
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -64,4 +64,4 @@
expect($result)->toBe($concreteBox);
});
});
});
});
Loading