,error_levels?:string[]}> */ public function providerValidCodeParse() { return [ 'immutableClassGenerating' => [ 'a = $a; $this->b = $b; } public function setA(int $a) : self { return new self($a, $this->b); } }', ], 'callInternalClassMethod' => [ 'a = $a; } public function getA() : string { return $this->a; } public function getHelloA() : string { return "hello" . $this->getA(); } }', ], 'addToCart' => [ 'items = $items; } public function addItem(CartItem $item) : self { $items = $this->items; $items[] = $item; return new Cart($items); } } /** @psalm-immutable */ class CartItem { public string $name; public float $price; public function __construct(string $name, float $price) { $this->name = $name; $this->price = $price; } } /** @psalm-pure */ function addItemToCart(Cart $c, string $name, float $price) : Cart { return $c->addItem(new CartItem($name, $price)); }', ], 'allowImpureStaticMethod' => [ 'id = $id; } public static function fromString(string $id): self { return new self($id . rand(0, 1)); } }' ], 'allowPropertySetOnNewInstance' => [ 'bar = $bar; } public function withBar(string $bar): self { $new = new Foo("hello"); $new->bar = $bar; return $new; } }' ], 'allowArrayMapCallable' => [ 'line1 = $line1; $this->line2 = $line2; $this->city = $city; } public function __toString() { $parts = [ $this->line1, $this->line2 ?? "", $this->city, ]; // Remove empty parts $parts = \array_map("trim", $parts); $parts = \array_filter($parts, "strlen"); $parts = \array_map(function(string $s) { return $s;}, $parts); return \implode(", ", $parts); } }' ], 'allowPropertyAssignmentInUnserialize' => [ 'data = "Foo"; } public function serialize() { return $this->data; } public function unserialize($data) { $this->data = $data; } public function getData(): string { return $this->data; } }' ], 'allowMethodOverriding' => [ 'a = $a; } public function getA() : string { return $this->a; } } /** @method string getA() */ class B extends A {}', ], 'immutableClassWithCloneAndPropertyChange' => [ 'bar = $bar; } public function withBar(string $bar): self { $new = clone $this; $new->bar = $bar; return $new; } }', ], 'immutableClassWithCloneAndPropertyAppend' => [ 'bar = $bar; } public function withBar(string $bar): self { $new = clone $this; $new->bar .= $bar; return $new; } }', ], 'memoizeImmutableCalls' => [ 'error = $error; } public function getError(): ?string { return $this->error; } } $dto = new DTO("BOOM!"); if ($dto->getError()) { takesString($dto->getError()); }' ], 'allowConstructorPrivateUnusedMethods' => [ 'test(); $this->commission = 1; } private function test(): void {} }' ], 'canPassImmutableIntoImmutable' => [ 'i = $i; } /** @psalm-mutation-free */ public function get(): int { return $this->i; } } /** * @psalm-immutable */ class Immutable { private $item; public function __construct(Item $item) { $this->item = $item; } public function get(): int { return $this->item->get(); } } $item = new Item(5); new Immutable($item);', ], 'preventNonImmutableTraitInImmutableClass' => [ 'i = $i; } } /** * @psalm-immutable */ final class NotReallyImmutableClass { use ImmutableTrait; }', ], 'preventImmutableClassInheritingMutableParent' => [ 'i = $i; } } /** * @psalm-immutable */ final class ImmutableClass extends ImmutableParent {}', ], 'passDateTimeZone' => [ 'format("Y-m-d");' ], 'allowPassingCloneOfMutableIntoImmutable' => [ 'i++; } /** @psalm-mutation-free */ public function get(): int { return $this->i; } } /** * @psalm-immutable */ class Immutable { private Item $item; public function __construct(Item $item) { $this->item = clone $item; } public function get(): int { return $this->item->get(); } } $item = new Item(); new Immutable($item);', ], 'noCrashWhenCheckingValueTwice' => [ 'value = $value; } /** * @psalm-pure * @return mixed * @psalm-return T */ public function getValue() { return $this->value; } } /** * @extends Enum * @psalm-immutable */ class TestEnum extends Enum { public const TEST = "test"; } function foo(TestEnum $e): void { if ($e->getValue() === TestEnum::TEST && $e->getValue() === TestEnum::TEST ) {} }' ], ]; } /** * @return iterable */ public function providerInvalidCodeParse() { return [ 'immutablePropertyAssignmentInternally' => [ 'a = $a; $this->b = $b; } public function setA(int $a): void { $this->a = $a; } }', 'error_message' => 'InaccessibleProperty', ], 'immutablePropertyAssignmentExternally' => [ 'a = $a; $this->b = $b; } } $a = new A(4, "hello"); $a->b = "goodbye";', 'error_message' => 'InaccessibleProperty', ], 'callImpureFunction' => [ 'a = $a; $this->b = $b; } public function bar() : void { header("Location: https://vimeo.com"); } }', 'error_message' => 'ImpureFunctionCall', ], 'callExternalClassMethod' => [ 'a = $a; } public function getA() : string { return $this->a; } public function redirectToA() : void { B::setRedirectHeader($this->getA()); } } class B { public static function setRedirectHeader(string $s) : void { header("Location: $s"); } }', 'error_message' => 'ImpureMethodCall', ], 'mustBeImmutableLikeInterfaces' => [ 'counter; } }', 'error_message' => 'MissingImmutableAnnotation', ], 'inheritImmutabilityFromParent' => [ 'counter; } }', 'error_message' => 'MissingImmutableAnnotation', ], 'preventPassingMutableIntoImmutable' => [ 'item = $item; } public function get(): int { return $this->item->get(); } } class Item { private int $i = 0; public function mutate(): void { $this->i++; } /** @psalm-mutation-free */ public function get(): int { return $this->i; } }', 'error_message' => 'ImpurePropertyAssignment', ], 'preventNonImmutableTraitInImmutableClass' => [ 'i++; } } /** * @psalm-immutable */ final class NotReallyImmutableClass { use MutableTrait; }', 'error_message' => 'MutableDependency' ], 'preventImmutableClassInheritingMutableParent' => [ 'i++; } } /** * @psalm-immutable */ final class NotReallyImmutableClass extends MutableParent {}', 'error_message' => 'MutableDependency' ], 'preventAssigningArrayToImmutableProperty' => [ 'items = $items; } }', 'error_message' => 'ImpurePropertyAssignment', ], ]; } }