1
0
mirror of https://github.com/danog/psalm.git synced 2024-12-13 09:47:29 +01:00
psalm/tests/ImmutableAnnotationTest.php
2022-12-18 13:20:31 -06:00

788 lines
26 KiB
PHP

<?php
namespace Psalm\Tests;
use Psalm\Tests\Traits\InvalidCodeAnalysisTestTrait;
use Psalm\Tests\Traits\ValidCodeAnalysisTestTrait;
class ImmutableAnnotationTest extends TestCase
{
use InvalidCodeAnalysisTestTrait;
use ValidCodeAnalysisTestTrait;
public function providerValidCodeParse(): iterable
{
return [
'immutableClassGenerating' => [
'code' => '<?php
/**
* @psalm-immutable
*/
class A {
/** @var int */
private $a;
/** @var string */
public $b;
public function __construct(int $a, string $b) {
$this->a = $a;
$this->b = $b;
}
public function setA(int $a) : self {
return new self($a, $this->b);
}
}',
],
'callInternalClassMethod' => [
'code' => '<?php
/**
* @psalm-immutable
*/
class A {
/** @var string */
public $a;
public function __construct(string $a) {
$this->a = $a;
}
public function getA() : string {
return $this->a;
}
public function getHelloA() : string {
return "hello" . $this->getA();
}
}',
],
'addToCart' => [
'code' => '<?php
/** @psalm-immutable */
class Cart {
/** @var CartItem[] */
public array $items;
/** @param CartItem[] $items */
public function __construct(array $items) {
$this->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' => [
'code' => '<?php
/**
* @psalm-immutable
*/
final class ClientId
{
public string $id;
private function __construct(string $id)
{
$this->id = $id;
}
public static function fromString(string $id): self
{
return new self($id . rand(0, 1));
}
}',
],
'allowPropertySetOnNewInstance' => [
'code' => '<?php
/**
* @psalm-immutable
*/
class Foo {
protected string $bar;
public function __construct(string $bar) {
$this->bar = $bar;
}
public function withBar(string $bar): self {
$new = new Foo("hello");
$new->bar = $bar;
return $new;
}
}',
],
'allowArrayMapCallable' => [
'code' => '<?php
/**
* @psalm-immutable
*/
class Address
{
private $line1;
private $line2;
private $city;
public function __construct(
string $line1,
?string $line2,
string $city
) {
$this->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' => [
'code' => '<?php
/**
* @psalm-immutable
*/
class Foo implements \Serializable {
/** @var string */
private $data;
public function __construct() {
$this->data = "Foo";
}
public function serialize() {
return $this->data;
}
public function unserialize($data) {
$this->data = $data;
}
public function getData(): string {
return $this->data;
}
}',
],
'allowPropertyAssignmentInMagicUnserialize' => [
'code' => '<?php
/**
* @psalm-immutable
*/
class Foo {
/** @var string */
private $data;
public function __construct() {
$this->data = "Foo";
}
public function __serialize(): array {
return ["data" => $this->data];
}
/** @param array{data: string} $data */
public function __unserialize(array $data): void {
$this->data = $data["data"];
}
public function getData(): string {
return $this->data;
}
}',
],
'allowMethodOverriding' => [
'code' => '<?php
class A {
private string $a;
public function __construct(string $a) {
$this->a = $a;
}
public function getA() : string {
return $this->a;
}
}
/** @method string getA() */
class B extends A {}',
],
'immutableClassWithCloneAndPropertyChange' => [
'code' => '<?php
/**
* @psalm-immutable
*/
class Foo {
protected string $bar;
public function __construct(string $bar) {
$this->bar = $bar;
}
public function withBar(string $bar): self {
$new = clone $this;
$new->bar = $bar;
return $new;
}
}',
],
'immutableClassWithCloneAndPropertyAppend' => [
'code' => '<?php
/**
* @psalm-immutable
*/
class Foo {
protected string $bar;
public function __construct(string $bar) {
$this->bar = $bar;
}
public function withBar(string $bar): self {
$new = clone $this;
$new->bar .= $bar;
return $new;
}
}',
],
'memoizeImmutableCalls' => [
'code' => '<?php
function takesString(string $s) : void {}
/**
* @psalm-immutable
*/
class DTO {
/** @var string|null */
private $error;
public function __construct(?string $error) {
$this->error = $error;
}
public function getError(): ?string {
return $this->error;
}
}
$dto = new DTO("BOOM!");
if ($dto->getError()) {
takesString($dto->getError());
}',
],
'allowConstructorPrivateUnusedMethods' => [
'code' => '<?php
/**
* @psalm-immutable
*/
class PaymentShared
{
/** @var int */
private $commission;
public function __construct()
{
$this->test();
$this->commission = 1;
}
private function test(): void {}
}',
],
'canPassImmutableIntoImmutable' => [
'code' => '<?php
/**
* @psalm-immutable
*/
class Item {
private int $i;
public function __construct(int $i) {
$this->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' => [
'code' => '<?php
/**
* @psalm-immutable
*/
trait ImmutableTrait {
public int $i = 0;
public function __construct(int $i) {
$this->i = $i;
}
}
/**
* @psalm-immutable
*/
final class NotReallyImmutableClass {
use ImmutableTrait;
}',
],
'preventImmutableClassInheritingMutableParent' => [
'code' => '<?php
/**
* @psalm-immutable
*/
class ImmutableParent {
public int $i = 0;
public function __construct(int $i) {
$this->i = $i;
}
}
/**
* @psalm-immutable
*/
final class ImmutableClass extends ImmutableParent {}',
],
'passDateTimeZone' => [
'code' => '<?php
echo (new DateTimeImmutable("now", new DateTimeZone("UTC")))->format("Y-m-d");',
],
'allowPassingCloneOfMutableIntoImmutable' => [
'code' => '<?php
class Item {
private int $i = 0;
public function mutate(): void {
$this->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' => [
'code' => '<?php
/**
* @psalm-template T
* @psalm-immutable
*/
abstract class Enum {
/** @var T */
private $value;
/** @param T $value */
public function __construct($value) {
$this->value = $value;
}
/**
* @return mixed
* @psalm-return T
*/
public function getValue() {
return $this->value;
}
}
/**
* @extends Enum<string>
* @psalm-immutable
*/
class TestEnum extends Enum {
public const TEST = "test";
}
function foo(TestEnum $e): void {
if ($e->getValue() === TestEnum::TEST
&& $e->getValue() === TestEnum::TEST
) {}
}',
],
'allowMutablePropertyFetch' => [
'code' => '<?php
class B {
public int $j = 5;
}
/**
* @psalm-immutable
*/
class A {
public int $i;
public function __construct(int $i) {
$this->i = $i;
}
public function getPlusOther(B $b) : int {
return $this->i + $b->j;
}
}',
],
'allowPassingMutableIntoImmutable' => [
'code' => '<?php
/**
* @psalm-immutable
*/
class Immutable {
private $item;
public function __construct(Item $item) {
$this->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;
}
}',
],
'allowMutationFreeCallInMutationFreeContext' => [
'code' => '<?php
/**
* @psalm-mutation-free
*/
function getData(): array {
/** @var mixed $arr */
$arr = $GLOBALS["cachedData"] ?? [];
return is_array($arr) ? $arr : [];
}
/**
* @psalm-mutation-free
* @return mixed
*/
function getDataItem(string $key) {
return getData()[$key] ?? null;
}',
],
];
}
public function providerInvalidCodeParse(): iterable
{
return [
'immutablePropertyAssignmentInternally' => [
'code' => '<?php
/**
* @psalm-immutable
*/
class A {
/** @var int */
private $a;
/** @var string */
public $b;
public function __construct(int $a, string $b) {
$this->a = $a;
$this->b = $b;
}
public function setA(int $a): void {
$this->a = $a;
}
}',
'error_message' => 'InaccessibleProperty',
],
'immutablePropertyAssignmentExternally' => [
'code' => '<?php
/**
* @psalm-immutable
*/
class A {
/** @var int */
private $a;
/** @var string */
public $b;
public function __construct(int $a, string $b) {
$this->a = $a;
$this->b = $b;
}
}
$a = new A(4, "hello");
$a->b = "goodbye";',
'error_message' => 'InaccessibleProperty',
],
'callImpureFunction' => [
'code' => '<?php
/**
* @psalm-immutable
*/
class A {
/** @var int */
private $a;
/** @var string */
public $b;
public function __construct(int $a, string $b) {
$this->a = $a;
$this->b = $b;
}
public function bar() : void {
header("Location: https://vimeo.com");
}
}',
'error_message' => 'ImpureFunctionCall',
],
'callExternalClassMethod' => [
'code' => '<?php
/**
* @psalm-immutable
*/
class A {
/** @var string */
public $a;
public function __construct(string $a) {
$this->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' => [
'code' => '<?php
/** @psalm-immutable */
interface SomethingImmutable {
public function someInteger() : int;
}
class MutableImplementation implements SomethingImmutable {
private int $counter = 0;
public function someInteger() : int {
return ++$this->counter;
}
}',
'error_message' => 'MissingImmutableAnnotation',
],
'inheritImmutabilityFromParent' => [
'code' => '<?php
/** @psalm-immutable */
abstract class SomethingImmutable {
abstract public function someInteger() : int;
}
class MutableImplementation extends SomethingImmutable {
private int $counter = 0;
public function someInteger() : int {
return ++$this->counter;
}
}',
'error_message' => 'MissingImmutableAnnotation',
],
'preventNonImmutableTraitInImmutableClass' => [
'code' => '<?php
trait MutableTrait {
public int $i = 0;
public function increment() : void {
$this->i++;
}
}
/**
* @psalm-immutable
*/
final class NotReallyImmutableClass {
use MutableTrait;
}',
'error_message' => 'MutableDependency',
],
'preventImmutableClassInheritingMutableParent' => [
'code' => '<?php
class MutableParent {
public int $i = 0;
public function increment() : void {
$this->i++;
}
}
/**
* @psalm-immutable
*/
final class NotReallyImmutableClass extends MutableParent {}',
'error_message' => 'MutableDependency',
],
'mutationInPropertyAssignment' => [
'code' => '<?php
class D {
private string $s;
public function __construct(string $s) {
$this->s = $s;
}
/**
* @psalm-mutation-free
*/
public function getShort() : string {
return substr($this->s, 0, 5);
}
/**
* @psalm-mutation-free
*/
public function getShortMutating() : string {
$this->s = "hello";
return substr($this->s, 0, 5);
}
}',
'error_message' => 'ImpurePropertyAssignment',
],
'mutationInPropertyConcat' => [
'code' => '<?php
class D {
private string $s;
public function __construct(string $s) {
$this->s = $s;
}
/**
* @psalm-mutation-free
*/
public function getShort() : string {
return substr($this->s, 0, 5);
}
/**
* @psalm-mutation-free
*/
public function getShortMutating() : string {
$this->s .= "hello";
return substr($this->s, 0, 5);
}
}',
'error_message' => 'ImpurePropertyAssignment',
],
'preventUnset' => [
'code' => '<?php
/**
* @psalm-immutable
*/
class A {
/** @var string */
public $b;
public function __construct(string $b) {
$this->b = $b;
}
}
$a = new A("hello");
unset($a->b);',
'error_message' => 'InaccessibleProperty',
],
];
}
}