1
0
mirror of https://github.com/danog/psalm.git synced 2024-11-26 20:34:47 +01:00
psalm/tests/PureAnnotationTest.php
Grégoire Paris bc91de015e
Mark throwable methods as pure (#3171)
* Write stub as valid PHP

This works probably because it is not parsed by PHP but by something
more tolerant, but let's make it more valid (the final access type in
the signature is not valid) PHP anyway, that will raise fewer eyebrows.

* Document actual return type

See ca006e54e3/Zend/zend_exceptions.stub.php (L8)

* Mark all methods from Throwable as mutation free

Exception is the only possible class implementation of Throwable, and
all of its methods except __toString() are final.
See ca006e54e3/Zend/zend_exceptions.stub.php (L3-L25)

Closes #3170
2020-04-17 22:08:26 -04:00

518 lines
17 KiB
PHP

<?php
namespace Psalm\Tests;
use const DIRECTORY_SEPARATOR;
class PureAnnotationTest extends TestCase
{
use Traits\InvalidCodeAnalysisTestTrait;
use Traits\ValidCodeAnalysisTestTrait;
/**
* @return iterable<string,array{string,assertions?:array<string,string>,error_levels?:string[]}>
*/
public function providerValidCodeParse()
{
return [
'simplePureFunction' => [
'<?php
namespace Bar;
class A {
public int $a = 5;
}
/** @psalm-pure */
function filterOdd(int $i, A $a) : ?int {
if ($i % 2 === 0 || $a->a === 2) {
return $i;
}
$a = new A();
return null;
}',
],
'pureFunctionCallingBuiltinFunctions' => [
'<?php
namespace Bar;
/** @psalm-pure */
function lower(string $s) : string {
return substr(strtolower($s), 0, 10);
}',
],
'pureWithStrReplace' => [
'<?php
/** @psalm-pure */
function highlight(string $needle, string $output) : string {
$needle = preg_quote($needle, \'#\');
$needles = str_replace([\'"\', \' \'], [\'\', \'|\'], $needle);
$output = preg_replace("#({$needles})#im", "<mark>$1</mark>", $output);
return $output;
}'
],
'implicitAnnotations' => [
'<?php
abstract class Foo {
private array $options;
private array $defaultOptions;
function __construct(array $options) {
$this->setOptions($options);
$this->setDefaultOptions($this->getOptions());
}
function getOptions(): array {
return $this->options;
}
function setOptions(array $options): void {
$this->options = $options;
}
function setDefaultOptions(array $defaultOptions): void {
$this->defaultOptions = $defaultOptions;
}
}',
],
'canCreateObjectWithNoExternalMutations' => [
'<?php
/** @psalm-external-mutation-free */
class Counter {
private int $count = 0;
public function __construct(int $count) {
$this->count = $count;
}
public function increment() : void {
$this->count++;
}
public function incrementByTwo() : void {
$this->count = $this->count + 2;
}
public function incrementByFive() : void {
$this->count += 5;
}
}
/** @psalm-pure */
function makesACounter(int $i) : Counter {
$c = new Counter($i);
$c->increment();
$c->incrementByTwo();
$c->incrementByFive();
return $c;
}',
],
'canCreateImmutableObject' => [
'<?php
/** @psalm-immutable */
class A {
private string $s;
public function __construct(string $s) {
$this->s = $s;
}
public function getShort() : string {
return substr($this->s, 0, 5);
}
}
/** @psalm-pure */
function makeA(string $s) : A {
$a = new A($s);
if ($a->getShort() === "bar") {
return new A("foo");
}
return $a;
}'
],
'assertIsPureInProductionn' => [
'<?php
/**
* @psalm-pure
*/
function toDateTime(?DateTime $dateTime) : DateTime {
assert($dateTime instanceof DateTime);
return $dateTime;
}'
],
'allowArrayMapClosure' => [
'<?php
/**
* @psalm-pure
* @param string[] $arr
*/
function foo(array $arr) : array {
return \array_map(function(string $s) { return $s;}, $arr);
}'
],
'pureBuiltinCall' => [
'<?php
final class Date
{
/** @psalm-pure */
public static function timeZone(string $tzString) : DateTimeZone
{
return new \DateTimeZone($tzString);
}
}',
],
'sortFunction' => [
'<?php
/**
* @psalm-pure
*
* @param int[] $ar
*/
function foo(array $ar): int
{
usort($ar, static function (int $a, int $b): int {
return $a <=> $b;
});
return $ar[0] ?? 0;
}',
],
'allowPureToString' => [
'<?php
class A {
/** @psalm-pure */
public function __toString() {
return "bar";
}
}
/**
* @psalm-pure
*/
function foo(string $s, A $a) : string {
if ($a == $s) {}
return $s;
}',
],
'exceptionGetMessage' => [
'<?php
/**
* @psalm-pure
*/
function getMessage(Throwable $e): string {
return $e->getMessage();
}
echo getMessage(new Exception("test"));'
],
'exceptionGetCode' => [
'<?php
/**
* @psalm-pure
*/
function getCode(Throwable $e): int {
return $e->getCode();
}
echo getCode(new Exception("test"));'
],
'exceptionGetFile' => [
'<?php
/**
* @psalm-pure
*/
function getFile(Throwable $e): string {
return $e->getFile();
}
echo getFile(new Exception("test"));'
],
'exceptionGetLine' => [
'<?php
/**
* @psalm-pure
*/
function getLine(Throwable $e): int {
return $e->getLine();
}
echo getLine(new Exception("test"));'
],
'exceptionGetTrace' => [
'<?php
/**
* @psalm-pure
*/
function getTrace(Throwable $e): array {
return $e->getTrace();
}
echo count(getTrace(new Exception("test")));'
],
'exceptionGetPrevious' => [
'<?php
/**
* @psalm-pure
*/
function getPrevious(Throwable $e): ?Throwable {
return $e->getPrevious();
}
echo gettype(getPrevious(new Exception("test")));'
],
'exceptionGetTraceAsString' => [
'<?php
/**
* @psalm-pure
*/
function getTraceAsString(Throwable $e): string {
return $e->getTraceAsString();
}
echo getTraceAsString(new Exception("test"));'
],
];
}
/**
* @return iterable<string,array{string,error_message:string,2?:string[],3?:bool,4?:string}>
*/
public function providerInvalidCodeParse()
{
return [
'impurePropertyAssignment' => [
'<?php
namespace Bar;
class A {
public int $a = 5;
}
/** @psalm-pure */
function filterOdd(int $i, A $a) : ?int {
$a->a++;
if ($i % 2 === 0 || $a->a === 2) {
return $i;
}
return null;
}',
'error_message' => 'ImpurePropertyAssignment',
],
'impureMethodCall' => [
'<?php
namespace Bar;
class A {
public int $a = 5;
public function foo() : void {
$this->a++;
}
}
/** @psalm-pure */
function filterOdd(int $i, A $a) : ?int {
$a->foo();
if ($i % 2 === 0 || $a->a === 2) {
return $i;
}
return null;
}',
'error_message' => 'ImpureMethodCall',
],
'impureFunctionCall' => [
'<?php
namespace Bar;
function impure() : ?string {
/** @var int */
static $i = 0;
++$i;
return $i % 2 ? "hello" : null;
}
/** @psalm-pure */
function filterOdd(array $a) : void {
impure();
}',
'error_message' => 'ImpureFunctionCall',
],
'impureConstructorCall' => [
'<?php
namespace Bar;
class A {
public int $a = 5;
}
class B {
public function __construct(A $a) {
$a->a++;
}
}
/** @psalm-pure */
function filterOdd(int $i, A $a) : ?int {
$b = new B($a);
if ($i % 2 === 0 || $a->a === 2) {
return $i;
}
return null;
}',
'error_message' => 'ImpureMethodCall',
],
'canCreateObjectWithNoExternalMutations' => [
'<?php
class Counter {
private int $count = 0;
public function __construct(int $count) {
$this->count = $count;
}
public function increment() : void {
$this->count += rand(0, 5);
}
}
/** @psalm-pure */
function makesACounter(int $i) : Counter {
$c = new Counter($i);
$c->increment();
return $c;
}',
'error_message' => 'ImpureMethodCall',
],
'useOfStaticMakesFunctionImpure' => [
'<?php
/** @psalm-pure */
function addCumulative(int $left) : int {
/** @var int */
static $i = 0;
$i += $left;
return $left;
}',
'error_message' => 'ImpureStaticVariable',
],
'preventImpureArrayMapClosure' => [
'<?php
/**
* @psalm-pure
* @param string[] $arr
*/
function foo(array $arr) : array {
return \array_map(function(string $s) { return $s . rand(0, 1);}, $arr);
}',
'error_message' => 'ImpureFunctionCall',
],
'sortFunctionImpure' => [
'<?php
/**
* @psalm-pure
*
* @param int[] $ar
*/
function foo(array $ar): int
{
usort($ar, static function (int $a, int $b): int {
session_start();
return $a <=> $b;
});
return $ar[0] ?? 0;
}',
'error_message' => 'ImpureFunctionCall',
],
'impureByRef' => [
'<?php
/**
* @psalm-pure
*/
function foo(string &$a): string {
$a = "B";
return $a;
}',
'error_message' => 'ImpureByReferenceAssignment'
],
'staticPropertyFetch' => [
'<?php
final class Number1 {
public static ?string $zero = null;
/**
* @psalm-pure
*/
public static function zero(): ?string {
return self::$zero;
}
}',
'error_message' => 'ImpureStaticProperty',
],
'staticPropertyAssignment' => [
'<?php
final class Number1 {
/** @var string|null */
private static $zero;
/**
* @psalm-pure
*/
public static function zero(): string {
self::$zero = "Zero";
return "hello";
}
}',
'error_message' => 'ImpureStaticProperty',
],
'preventImpureToStringViaComparison' => [
'<?php
class A {
public function __toString() {
echo "hi";
return "bar";
}
}
/**
* @psalm-pure
*/
function foo(string $s, A $a) : string {
if ($a == $s) {}
return $s;
}',
'error_message' => 'ImpureMethodCall'
],
'preventImpureToStringViaConcatenation' => [
'<?php
class A {
public function __toString() {
echo "hi";
return "bar";
}
}
/**
* @psalm-pure
*/
function foo(string $s, A $a) : string {
return $a . $s;
}',
'error_message' => 'ImpureMethodCall'
],
];
}
}