2019-08-30 18:36:35 +02:00
|
|
|
<?php
|
|
|
|
namespace Psalm\Tests;
|
|
|
|
|
|
|
|
use const DIRECTORY_SEPARATOR;
|
|
|
|
|
|
|
|
class ImmutableAnnotationTest extends TestCase
|
|
|
|
{
|
|
|
|
use Traits\InvalidCodeAnalysisTestTrait;
|
|
|
|
use Traits\ValidCodeAnalysisTestTrait;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @return iterable<string,array{string,assertions?:array<string,string>,error_levels?:string[]}>
|
|
|
|
*/
|
|
|
|
public function providerValidCodeParse()
|
|
|
|
{
|
|
|
|
return [
|
|
|
|
'immutableClassGenerating' => [
|
|
|
|
'<?php
|
|
|
|
/**
|
2019-09-01 00:43:39 +02:00
|
|
|
* @psalm-immutable
|
2019-08-30 18:36:35 +02:00
|
|
|
*/
|
|
|
|
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 {
|
2019-09-02 04:13:16 +02:00
|
|
|
return new self($a, $this->b);
|
2019-08-30 18:36:35 +02:00
|
|
|
}
|
|
|
|
}',
|
|
|
|
],
|
|
|
|
'callInternalClassMethod' => [
|
|
|
|
'<?php
|
|
|
|
/**
|
2019-09-01 00:43:39 +02:00
|
|
|
* @psalm-immutable
|
2019-08-30 18:36:35 +02:00
|
|
|
*/
|
|
|
|
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();
|
|
|
|
}
|
|
|
|
}',
|
|
|
|
],
|
2019-09-01 04:03:37 +02:00
|
|
|
'addToCart' => [
|
|
|
|
'<?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));
|
|
|
|
}',
|
|
|
|
],
|
2019-09-09 16:38:55 +02:00
|
|
|
'allowImpureStaticMethod' => [
|
|
|
|
'<?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));
|
|
|
|
}
|
|
|
|
}'
|
2019-09-09 17:14:40 +02:00
|
|
|
],
|
|
|
|
'allowPropertySetOnNewInstance' => [
|
|
|
|
'<?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");
|
|
|
|
/** @psalm-suppress InaccessibleProperty */
|
|
|
|
$new->bar = $bar;
|
|
|
|
|
|
|
|
return $new;
|
|
|
|
}
|
|
|
|
}'
|
|
|
|
],
|
|
|
|
'allowClone' => [
|
|
|
|
'<?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;
|
|
|
|
/** @psalm-suppress InaccessibleProperty */
|
|
|
|
$new->bar = $bar;
|
|
|
|
|
|
|
|
return $new;
|
|
|
|
}
|
|
|
|
}'
|
|
|
|
],
|
2019-08-30 18:36:35 +02:00
|
|
|
];
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @return iterable<string,array{string,error_message:string,2?:string[],3?:bool,4?:string}>
|
|
|
|
*/
|
|
|
|
public function providerInvalidCodeParse()
|
|
|
|
{
|
|
|
|
return [
|
|
|
|
'immutablePropertyAssignmentInternally' => [
|
|
|
|
'<?php
|
|
|
|
/**
|
2019-09-01 00:43:39 +02:00
|
|
|
* @psalm-immutable
|
2019-08-30 18:36:35 +02:00
|
|
|
*/
|
|
|
|
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;
|
|
|
|
}
|
|
|
|
}',
|
2019-09-09 17:14:40 +02:00
|
|
|
'error_message' => 'InaccessibleProperty',
|
2019-08-30 18:36:35 +02:00
|
|
|
],
|
|
|
|
'immutablePropertyAssignmentExternally' => [
|
|
|
|
'<?php
|
|
|
|
/**
|
2019-09-01 00:43:39 +02:00
|
|
|
* @psalm-immutable
|
2019-08-30 18:36:35 +02:00
|
|
|
*/
|
|
|
|
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' => [
|
|
|
|
'<?php
|
|
|
|
/**
|
2019-09-01 00:43:39 +02:00
|
|
|
* @psalm-immutable
|
2019-08-30 18:36:35 +02:00
|
|
|
*/
|
|
|
|
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' => [
|
|
|
|
'<?php
|
|
|
|
/**
|
2019-09-01 00:43:39 +02:00
|
|
|
* @psalm-immutable
|
2019-08-30 18:36:35 +02:00
|
|
|
*/
|
|
|
|
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',
|
|
|
|
],
|
2019-09-09 17:14:40 +02:00
|
|
|
'cloneMutatingClass' => [
|
|
|
|
'<?php
|
|
|
|
/**
|
|
|
|
* @psalm-immutable
|
|
|
|
*/
|
|
|
|
class Foo {
|
|
|
|
protected string $bar;
|
|
|
|
|
|
|
|
public function __construct(string $bar) {
|
|
|
|
$this->bar = $bar;
|
|
|
|
}
|
|
|
|
|
|
|
|
public function withBar(Bar $b): Bar {
|
|
|
|
$new = clone $b;
|
|
|
|
$b->a = $this->bar;
|
|
|
|
|
|
|
|
return $new;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
class Bar {
|
|
|
|
public string $a = "hello";
|
|
|
|
}',
|
|
|
|
'error_message' => 'ImpurePropertyAssignment',
|
|
|
|
],
|
2019-08-30 18:36:35 +02:00
|
|
|
];
|
|
|
|
}
|
|
|
|
}
|