1
0
mirror of https://github.com/danog/psalm.git synced 2024-12-11 16:59:45 +01:00
psalm/tests/FunctionCallTest.php

2369 lines
81 KiB
PHP
Raw Normal View History

2016-12-12 05:41:11 +01:00
<?php
namespace Psalm\Tests;
2021-12-03 20:11:20 +01:00
use Psalm\Config;
use Psalm\Context;
2021-12-04 21:55:53 +01:00
use Psalm\Tests\Traits\InvalidCodeAnalysisTestTrait;
use Psalm\Tests\Traits\ValidCodeAnalysisTestTrait;
2021-12-03 20:11:20 +01:00
use const DIRECTORY_SEPARATOR;
class FunctionCallTest extends TestCase
2016-12-12 05:41:11 +01:00
{
2021-12-04 21:55:53 +01:00
use InvalidCodeAnalysisTestTrait;
use ValidCodeAnalysisTestTrait;
/**
2019-03-01 21:55:20 +01:00
* @return iterable<string,array{string,assertions?:array<string,string>,error_levels?:string[]}>
Refactor scanning and analysis, introducing multithreading (#191) * Add failing test * Add visitor to soup up classlike references * Move a whole bunch of code into the visitor * Move some methods back, move onto analysis stage * Use the getAliases method everywhere * Fix refs * Fix more refs * Fix some tests * Fix more tests * Fix include tests * Shift config class finding to project checker and fix bugs * Fix a few more tests * transition test to new syntax * Remove var_dump * Delete a bunch of code and fix mutation test * Remove unnecessary visitation * Transition to better mocked out file provider, breaking some cached statement loading * Use different scheme for naming anonymous classes * Fix anonymous class issues * Refactor file/statement loading * Add specific property types * Fix mapped property assignment * Improve how we deal with traits * Fix trait checking * Pass Psalm checks * Add multi-process support * Delay console output until the end * Remove PHP 7 syntax * Update file storage with classes * Fix scanning individual files and add reflection return types * Always turn XDebug off * Add quicker method of getting method mutations * Queue return types for crawling * Interpret all strings as possible classes once we see a `get_class` call * Check invalid return types again * Fix template namespacing issues * Default to class-insensitive file names for includes * Don’t overwrite existing issues data * Add var docblocks for scanning * Add null check * Fix loading of external classes in templates * Only try to populate class when we haven’t yet seen it’s not a class * Fix trait property accessibility * Only ever improve docblock param type * Make param replacement more robust * Fix static const missing inferred type * Fix a few more tests * Register constant definitions * Fix trait aliasing * Skip constant type tests for now * Fix linting issues * Make sure caching is off for tests * Remove unnecessary return * Use emulative parser if on PHP 5.6 * Cache parser for faster first-time parse * Fix constant resolution when scanning classes * Remove test that’s beyond a practical scope * Add back --diff support * Add --help for --threads * Remove unused vars
2017-07-25 22:11:02 +02:00
*/
public function providerValidCodeParse(): iterable
Refactor scanning and analysis, introducing multithreading (#191) * Add failing test * Add visitor to soup up classlike references * Move a whole bunch of code into the visitor * Move some methods back, move onto analysis stage * Use the getAliases method everywhere * Fix refs * Fix more refs * Fix some tests * Fix more tests * Fix include tests * Shift config class finding to project checker and fix bugs * Fix a few more tests * transition test to new syntax * Remove var_dump * Delete a bunch of code and fix mutation test * Remove unnecessary visitation * Transition to better mocked out file provider, breaking some cached statement loading * Use different scheme for naming anonymous classes * Fix anonymous class issues * Refactor file/statement loading * Add specific property types * Fix mapped property assignment * Improve how we deal with traits * Fix trait checking * Pass Psalm checks * Add multi-process support * Delay console output until the end * Remove PHP 7 syntax * Update file storage with classes * Fix scanning individual files and add reflection return types * Always turn XDebug off * Add quicker method of getting method mutations * Queue return types for crawling * Interpret all strings as possible classes once we see a `get_class` call * Check invalid return types again * Fix template namespacing issues * Default to class-insensitive file names for includes * Don’t overwrite existing issues data * Add var docblocks for scanning * Add null check * Fix loading of external classes in templates * Only try to populate class when we haven’t yet seen it’s not a class * Fix trait property accessibility * Only ever improve docblock param type * Make param replacement more robust * Fix static const missing inferred type * Fix a few more tests * Register constant definitions * Fix trait aliasing * Skip constant type tests for now * Fix linting issues * Make sure caching is off for tests * Remove unnecessary return * Use emulative parser if on PHP 5.6 * Cache parser for faster first-time parse * Fix constant resolution when scanning classes * Remove test that’s beyond a practical scope * Add back --diff support * Add --help for --threads * Remove unused vars
2017-07-25 22:11:02 +02:00
{
return [
2019-12-29 17:05:08 +01:00
'preg_grep' => [
'<?php
/**
* @param array<int,string> $strings
* @return array<int,string>
*/
function filter(array $strings): array {
return preg_grep("/search/", $strings, PREG_GREP_INVERT);
}
'
],
'typedArrayWithDefault' => [
'<?php
class A {}
/** @param array<A> $a */
2018-01-11 21:50:45 +01:00
function fooFoo(array $a = []): void {
2017-05-27 02:05:57 +02:00
}',
],
'abs' => [
'<?php
$a = abs(-5);
$b = abs(-7.5);
$c = $_GET["c"];
$c = is_numeric($c) ? abs($c) : null;',
'assertions' => [
'$a' => 'int',
'$b' => 'float',
'$c' => 'float|int|null',
],
'error_levels' => ['MixedAssignment', 'MixedArgument'],
],
'validDocblockParamDefault' => [
'<?php
/**
* @param int|false $p
* @return void
*/
2017-05-27 02:05:57 +02:00
function f($p = false) {}',
],
'byRefNewString' => [
'<?php
function fooFoo(?string &$v): void {}
2017-05-27 02:05:57 +02:00
fooFoo($a);',
],
'byRefVariableFunctionExistingArray' => [
'<?php
$arr = [];
function fooFoo(array &$v): void {}
$function = "fooFoo";
$function($arr);
if ($arr) {}',
],
'byRefProperty' => [
'<?php
class A {
/** @var string */
public $foo = "hello";
}
$a = new A();
function fooFoo(string &$v): void {}
fooFoo($a->foo);',
],
'namespaced' => [
'<?php
namespace A;
/** @return void */
function f(int $p) {}
2017-05-27 02:05:57 +02:00
f(5);',
],
'namespacedRootFunctionCall' => [
'<?php
namespace {
/** @return void */
function foo() { }
}
namespace A\B\C {
foo();
2017-05-27 02:05:57 +02:00
}',
],
'namespacedAliasedFunctionCall' => [
'<?php
namespace Aye {
/** @return void */
function foo() { }
}
namespace Bee {
use Aye as A;
A\foo();
2017-05-27 02:05:57 +02:00
}',
],
'noRedundantConditionAfterArrayObjectCountCheck' => [
'<?php
/** @var ArrayObject<int, int> */
$a = [];
$b = 5;
if (count($a)) {}',
],
'noRedundantConditionAfterMixedOrEmptyArrayCountCheck' => [
'<?php
function foo(string $s) : void {
2020-04-04 17:51:24 +02:00
$a = $_GET["s"] ?: [];
if (count($a)) {}
if (!count($a)) {}
}',
'assertions' => [],
2019-03-23 19:27:54 +01:00
'error_levels' => ['MixedAssignment', 'MixedArgument'],
],
'objectLikeArrayAssignmentInConditional' => [
'<?php
$a = [];
if (rand(0, 1)) {
$a["a"] = 5;
}
if (count($a)) {}
if (!count($a)) {}',
],
'noRedundantConditionAfterCheckingExplodeLength' => [
'<?php
/** @var string */
$s = "hello";
$segments = explode(".", $s);
if (count($segments) === 1) {}',
],
'arrayPopNonEmptyAfterThreeAssertions' => [
'<?php
class A {}
class B extends A {
/** @var array<int, string> */
public $arr = [];
}
/** @var array<A> */
$replacement_stmts = [];
if (!$replacement_stmts
|| !$replacement_stmts[0] instanceof B
|| count($replacement_stmts[0]->arr) > 1
) {
return null;
}
$b = $replacement_stmts[0]->arr;',
'assertions' => [
'$b' => 'array<int, string>',
],
],
2019-02-21 23:17:10 +01:00
'countMoreThan0CanBeInverted' => [
'<?php
$a = [];
if (rand(0, 1)) {
$a[] = "hello";
}
if (count($a) > 0) {
exit;
}',
'assertions' => [
'$a' => 'array<empty, empty>',
],
],
'byRefAfterCallable' => [
'<?php
/**
* @param callable $callback
* @return void
*/
function route($callback) {
if (!is_callable($callback)) { }
$a = preg_match("", "", $b);
if ($b[0]) {}
}',
'assertions' => [],
'error_levels' => [
'MixedAssignment',
2017-05-27 02:05:57 +02:00
'MixedArrayAccess',
'RedundantConditionGivenDocblockType',
2017-05-27 02:05:57 +02:00
],
],
'ignoreNullablePregReplace' => [
'<?php
function foo(string $s): string {
$s = preg_replace("/hello/", "", $s);
if ($s === null) {
return "hello";
}
return $s;
}
function bar(string $s): string {
$s = preg_replace("/hello/", "", $s);
return $s;
}
function bat(string $s): ?string {
$s = preg_replace("/hello/", "", $s);
return $s;
}',
],
'extractVarCheck' => [
'<?php
2018-01-11 21:50:45 +01:00
function takesString(string $str): void {}
$foo = null;
$a = ["$foo" => "bar"];
extract($a);
takesString($foo);',
'assertions' => [],
'error_levels' => [
'MixedAssignment',
2017-05-27 02:05:57 +02:00
'MixedArrayAccess',
'MixedArgument',
2017-05-27 02:05:57 +02:00
],
],
'compact' => [
'<?php
/**
* @return array<string, mixed>
*/
2018-01-11 21:50:45 +01:00
function test(): array {
return compact(["val"]);
}',
],
'objectLikeKeyChecksAgainstGeneric' => [
'<?php
/**
* @param array<string, string> $b
*/
2018-01-11 21:50:45 +01:00
function a($b): string
{
return $b["a"];
}
a(["a" => "hello"]);',
],
'objectLikeKeyChecksAgainstTKeyedArray' => [
'<?php
/**
* @param array{a: string} $b
*/
2018-01-11 21:50:45 +01:00
function a($b): string
{
return $b["a"];
}
a(["a" => "hello"]);',
],
2018-01-08 06:09:22 +01:00
'getenv' => [
'<?php
$a = getenv();
$b = getenv("some_key");',
'assertions' => [
2020-05-11 17:36:50 +02:00
'$a' => 'array<string, string>',
'$b' => 'false|string',
2018-01-08 06:09:22 +01:00
],
],
'ignoreFalsableFileGetContents' => [
'<?php
function foo(string $s): string {
return file_get_contents($s);
}
function bar(string $s): string {
$a = file_get_contents($s);
if ($a === false) {
return "hello";
}
return $a;
}
/**
* @return false|string
*/
function bat(string $s) {
return file_get_contents($s);
}',
],
2018-03-02 06:49:53 +01:00
'validCallables' => [
'<?php
class A {
public static function b() : void {}
}
function c() : void {}
["a", "b"]();
"A::b"();
2019-03-23 19:27:54 +01:00
"c"();',
2018-03-02 06:49:53 +01:00
],
'noInvalidOperandForCoreFunctions' => [
'<?php
function foo(string $a, string $b) : int {
$aTime = strtotime($a);
$bTime = strtotime($b);
return $aTime - $bTime;
}',
],
'strposIntSecondParam' => [
'<?php
function hasZeroByteOffset(string $s) : bool {
return strpos($s, 0) !== false;
2019-03-23 19:27:54 +01:00
}',
],
'functionCallInGlobalScope' => [
'<?php
$a = function() use ($argv) : void {};',
],
2018-04-03 04:19:58 +02:00
'varExport' => [
'<?php
$a = var_export(["a"], true);',
'assertions' => [
'$a' => 'string',
],
],
'varExportConstFetch' => [
'<?php
class Foo {
const BOOL_VAR_EXPORT_RETURN = true;
/**
* @param mixed $mixed
*/
public static function Baz($mixed) : string {
return var_export($mixed, self::BOOL_VAR_EXPORT_RETURN);
}
}',
],
'explode' => [
2018-04-16 22:03:04 +02:00
'<?php
/** @var string $string */
$elements = explode(" ", $string);',
'assertions' => [
'$elements' => 'non-empty-list<string>',
],
],
'explodeWithPositiveLimit' => [
'<?php
/** @var string $string */
$elements = explode(" ", $string, 5);',
'assertions' => [
'$elements' => 'non-empty-list<string>',
],
],
'explodeWithNegativeLimit' => [
'<?php
/** @var string $string */
$elements = explode(" ", $string, -5);',
'assertions' => [
'$elements' => 'list<string>',
],
],
'explodeWithDynamicLimit' => [
'<?php
/**
* @var string $string
* @var int $limit
*/
$elements = explode(" ", $string, $limit);',
'assertions' => [
'$elements' => 'list<string>',
],
],
'explodeWithDynamicDelimiter' => [
'<?php
/**
* @var string $delim
* @var string $string
*/
$elements = explode($delim, $string);',
'assertions' => [
'$elements' => 'false|non-empty-list<string>',
],
],
'explodeWithDynamicDelimiterAndPositiveLimit' => [
'<?php
/**
* @var string $delim
* @var string $string
*/
$elements = explode($delim, $string, 5);',
'assertions' => [
'$elements' => 'false|non-empty-list<string>',
],
],
'explodeWithDynamicDelimiterAndNegativeLimit' => [
'<?php
/**
* @var string $delim
* @var string $string
*/
$elements = explode($delim, $string, -5);',
'assertions' => [
'$elements' => 'false|list<string>',
],
2018-04-16 22:03:04 +02:00
],
'explodeWithDynamicDelimiterAndLimit' => [
'<?php
/**
* @var string $delim
* @var string $string
* @var int $limit
*/
$elements = explode($delim, $string, $limit);',
'assertions' => [
'$elements' => 'false|list<string>',
],
],
'explodeWithDynamicNonEmptyDelimiter' => [
'<?php
/**
* @var non-empty-string $delim
* @var string $string
*/
$elements = explode($delim, $string);',
'assertions' => [
'$elements' => 'non-empty-list<string>',
],
],
'explodeWithLiteralNonEmptyDelimiter' => [
'<?php
/**
* @var string $string
*/
$elements = explode(" ", $string);',
'assertions' => [
'$elements' => 'non-empty-list<string>',
],
],
'explodeWithLiteralEmptyDelimiter' => [
'<?php
/**
* @var string $string
*/
$elements = explode("", $string);',
'assertions' => [
'$elements' => 'false',
],
],
'explodeWithPossiblyFalse' => [
'<?php
/** @return non-empty-list<string> */
function exploder(string $d, string $s) : array {
return explode($d, $s);
}',
],
'allowPossiblyUndefinedClassInClassExists' => [
'<?php
2019-03-23 19:27:54 +01:00
if (class_exists(Foo::class)) {}',
],
'allowConstructorAfterClassExists' => [
'<?php
function foo(string $s) : void {
if (class_exists($s)) {
new $s();
}
}',
'assertions' => [],
'error_levels' => ['MixedMethodCall'],
],
'next' => [
'<?php
$arr = ["one", "two", "three"];
$n = next($arr);',
'assertions' => [
'$n' => 'false|string',
],
],
'iteratorToArray' => [
'<?php
/**
* @return Generator<stdClass>
*/
function generator(): Generator {
yield new stdClass;
}
$a = iterator_to_array(generator());',
'assertions' => [
2020-02-21 07:26:51 +01:00
'$a' => 'array<array-key, stdClass>',
],
],
'iteratorToArrayWithGetIterator' => [
'<?php
class C implements IteratorAggregate {
/**
* @return Traversable<int,string>
*/
public function getIterator() {
yield 1 => "1";
}
}
$a = iterator_to_array(new C);',
'assertions' => [
'$a' => 'array<int, string>',
],
],
'iteratorToArrayWithGetIteratorReturningList' => [
'<?php
class C implements IteratorAggregate {
/**
* @return Traversable<int,string>
*/
public function getIterator() {
yield 1 => "1";
}
}
2020-01-31 19:58:02 +01:00
$a = iterator_to_array(new C, false);',
'assertions' => [
2020-01-31 19:58:02 +01:00
'$a' => 'list<string>',
],
],
2020-01-31 19:58:02 +01:00
'strtrWithPossiblyFalseFirstArg' => [
'<?php
2020-01-31 19:58:02 +01:00
/**
* @param false|string $str
* @param array<string, string> $replace_pairs
* @return string
*/
function strtr_wrapper($str, array $replace_pairs) {
/** @psalm-suppress PossiblyFalseArgument */
return strtr($str, $replace_pairs);
}',
],
'versionCompare' => [
'<?php
2020-01-28 05:52:06 +01:00
/** @return "="|"==" */
function getString() : string {
2020-01-28 05:52:06 +01:00
return rand(0, 1) ? "==" : "=";
}
$a = version_compare("5.0.0", "7.0.0");
$b = version_compare("5.0.0", "7.0.0", "==");
$c = version_compare("5.0.0", "7.0.0", getString());
',
'assertions' => [
'$a' => 'int',
'$b' => 'bool',
2020-01-28 05:52:06 +01:00
'$c' => 'bool',
],
],
2018-07-08 02:35:24 +02:00
'getTimeOfDay' => [
'<?php
$a = gettimeofday(true) - gettimeofday(true);
$b = gettimeofday();
$c = gettimeofday(false);',
'assertions' => [
'$a' => 'float',
'$b' => 'array<string, int>',
'$c' => 'array<string, int>',
],
],
'parseUrlArray' => [
'<?php
function foo(string $s) : string {
2020-11-26 02:04:57 +01:00
$parts = parse_url($s);
return $parts["host"] ?? "";
}
function hereisanotherone(string $s) : string {
$parsed = parse_url($s);
if (isset($parsed["host"])) {
return $parsed["host"];
}
return "";
}
function hereisthelastone(string $s) : string {
$parsed = parse_url($s);
if (isset($parsed["host"])) {
return $parsed["host"];
}
return "";
}
function portisint(string $s) : int {
$parsed = parse_url($s);
if (isset($parsed["port"])) {
return $parsed["port"];
}
return 80;
}
function portismaybeint(string $s) : ? int {
$parsed = parse_url($s);
return $parsed["port"] ?? null;
}
$porta = parse_url("", PHP_URL_PORT);
$porte = parse_url("localhost:443", PHP_URL_PORT);',
'assertions' => [
'$porta' => 'false|int|null',
'$porte' => 'false|int|null',
],
'error_levels' => ['MixedReturnStatement', 'MixedInferredReturnType'],
],
'parseUrlComponent' => [
'<?php
function foo(string $s) : string {
return parse_url($s, PHP_URL_HOST) ?? "";
}
function bar(string $s) : string {
return parse_url($s, PHP_URL_HOST);
}
function bag(string $s) : string {
$host = parse_url($s, PHP_URL_HOST);
if (is_string($host)) {
return $host;
}
return "";
}',
],
'parseUrlTypes' => [
'<?php
$url = "foo";
$components = parse_url($url);
$scheme = parse_url($url, PHP_URL_SCHEME);
$host = parse_url($url, PHP_URL_HOST);
$port = parse_url($url, PHP_URL_PORT);
$user = parse_url($url, PHP_URL_USER);
$pass = parse_url($url, PHP_URL_PASS);
$path = parse_url($url, PHP_URL_PATH);
$query = parse_url($url, PHP_URL_QUERY);
$fragment = parse_url($url, PHP_URL_FRAGMENT);',
'assertions' => [
'$components' => 'array{fragment?: string, host?: string, pass?: string, path?: string, port?: int, query?: string, scheme?: string, user?: string}|false',
'$scheme' => 'false|null|string',
'$host' => 'false|null|string',
'$port' => 'false|int|null',
'$user' => 'false|null|string',
'$pass' => 'false|null|string',
'$path' => 'false|null|string',
'$query' => 'false|null|string',
'$fragment' => 'false|null|string',
],
],
'triggerUserError' => [
'<?php
function mightLeave() : string {
if (rand(0, 1)) {
trigger_error("bad", E_USER_ERROR);
} else {
return "here";
}
}',
],
'getParentClass' => [
'<?php
class A {}
class B extends A {}
$b = get_parent_class(new A());
if ($b === false) {}
$c = new $b();',
'assertions' => [],
'error_levels' => ['MixedMethodCall'],
],
'suppressError' => [
'<?php
$a = @file_get_contents("foo");',
'assertions' => [
'$a' => 'false|string',
],
],
'echo' => [
'<?php
echo false;',
],
'printrOutput' => [
'<?php
function foo(string $s) : void {
echo $s;
}
foo(print_r(1, true));',
],
2018-10-10 22:03:00 +02:00
'microtime' => [
'<?php
$a = microtime(true);
$b = microtime();
2019-06-30 17:14:05 +02:00
/** @psalm-suppress InvalidScalarArgument */
2018-10-10 22:03:00 +02:00
$c = microtime(1);
$d = microtime(false);',
'assertions' => [
'$a' => 'float',
'$b' => 'string',
'$c' => 'float|string',
'$d' => 'string',
],
],
2018-10-23 20:38:36 +02:00
'filterVar' => [
'<?php
function filterInt(string $s) : int {
$filtered = filter_var($s, FILTER_VALIDATE_INT);
if ($filtered === false) {
return 0;
}
return $filtered;
}
function filterNullableInt(string $s) : ?int {
return filter_var($s, FILTER_VALIDATE_INT, ["options" => ["default" => null]]);
2018-10-23 20:38:36 +02:00
}
function filterIntWithDefault(string $s) : int {
return filter_var($s, FILTER_VALIDATE_INT, ["options" => ["default" => 5]]);
2018-10-23 20:38:36 +02:00
}
function filterBool(string $s) : bool {
return filter_var($s, FILTER_VALIDATE_BOOLEAN);
}
function filterNullableBool(string $s) : ?bool {
return filter_var($s, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE);
}
function filterNullableBoolWithFlagsArray(string $s) : ?bool {
return filter_var($s, FILTER_VALIDATE_BOOLEAN, ["flags" => FILTER_NULL_ON_FAILURE]);
}
function filterFloat(string $s) : float {
$filtered = filter_var($s, FILTER_VALIDATE_FLOAT);
if ($filtered === false) {
return 0.0;
}
return $filtered;
}
function filterFloatWithDefault(string $s) : float {
return filter_var($s, FILTER_VALIDATE_FLOAT, ["options" => ["default" => 5.0]]);
2018-10-23 20:38:36 +02:00
}',
],
'callVariableVar' => [
'<?php
class Foo
{
public static function someInt(): int
{
return 1;
}
}
/**
* @return int
*/
function makeInt()
{
$fooClass = Foo::class;
return $fooClass::someInt();
}',
],
'expectsIterable' => [
'<?php
function foo(iterable $i) : void {}
function bar(array $a) : void {
foo($a);
}',
],
'getTypeHasValues' => [
2019-01-05 20:50:11 +01:00
'<?php
/**
* @param mixed $maybe
*/
function matchesTypes($maybe) : void {
$t = gettype($maybe);
if ($t === "object") {}
2019-03-23 19:27:54 +01:00
}',
2019-01-05 20:50:11 +01:00
],
'getTypeSwitchClosedResource' => [
'<?php
$data = "foo";
switch (gettype($data)) {
case "resource (closed)":
case "unknown type":
return "foo";
}',
],
2019-01-05 20:50:11 +01:00
'functionResolutionInNamespace' => [
'<?php
namespace Foo;
function sort(int $_) : void {}
2019-03-23 19:27:54 +01:00
sort(5);',
2019-01-05 20:50:11 +01:00
],
'rangeWithIntStep' => [
'<?php
function foo(int $bar) : string {
return (string) $bar;
}
foreach (range(1, 10, 1) as $x) {
foo($x);
}',
],
'rangeWithNoStep' => [
'<?php
function foo(int $bar) : string {
return (string) $bar;
}
foreach (range(1, 10) as $x) {
foo($x);
}',
],
2019-02-07 16:50:42 +01:00
'rangeWithNoStepAndString' => [
'<?php
function foo(string $bar) : void {}
foreach (range("a", "z") as $x) {
foo($x);
}',
],
'rangeWithFloatStep' => [
'<?php
function foo(float $bar) : string {
return (string) $bar;
}
foreach (range(1, 10, .3) as $x) {
foo($x);
}',
],
'rangeWithFloatStart' => [
'<?php
function foo(float $bar) : string {
return (string) $bar;
}
foreach (range(1.5, 10) as $x) {
foo($x);
}',
],
'rangeWithIntOrFloatStep' => [
'<?php
/** @var int|float */
$step = 1;
$a = range(1, 10, $step);
/** @var int */
$step = 1;
$b = range(1, 10, $step);
/** @var float */
$step = 1.;
$c = range(1, 10, $step);
',
'assertions' => [
'$a' => 'non-empty-list<float|int>',
'$b' => 'non-empty-list<int>',
'$c' => 'non-empty-list<float>',
],
],
2019-01-06 22:40:44 +01:00
'duplicateNamespacedFunction' => [
'<?php
namespace Bar;
function sort() : void {}',
],
'arrayMapAfterFunctionMissingFile' => [
'<?php
require_once(FOO);
$urls = array_map("strval", [1, 2, 3]);',
[],
'error_levels' => ['UndefinedConstant', 'UnresolvableInclude'],
],
'noNamespaceClash' => [
'<?php
namespace FunctionNamespace {
function foo() : void {}
}
namespace ClassNamespace {
class Foo {}
}
namespace {
use ClassNamespace\Foo;
use function FunctionNamespace\foo;
new Foo();
foo();
}',
],
'hashInit70' => [
'<?php
$h = hash_init("sha256");',
[
'$h' => 'resource',
],
[],
2019-03-23 19:27:54 +01:00
'7.1',
],
'hashInit71' => [
'<?php
$h = hash_init("sha256");',
[
'$h' => 'resource',
],
[],
2019-03-23 19:27:54 +01:00
'7.1',
],
'hashInit72' => [
'<?php
$h = hash_init("sha256");',
[
'$h' => 'HashContext|false',
],
[],
2019-03-23 19:27:54 +01:00
'7.2',
],
'hashInit73' => [
'<?php
$h = hash_init("sha256");',
[
'$h' => 'HashContext|false',
],
[],
2019-03-23 19:27:54 +01:00
'7.3',
],
'hashInit80' => [
'<?php
$h = hash_init("sha256");',
[
'$h' => 'HashContext',
],
[],
'8.0',
],
'nullableByRef' => [
'<?php
function foo(?string &$s) : void {}
function bar() : void {
foo($bar);
2019-03-23 19:27:54 +01:00
}',
],
'getClassNewInstance' => [
'<?php
interface I {}
class C implements I {}
class Props {
/** @var class-string<I>[] */
public $arr = [];
}
2019-03-23 19:27:54 +01:00
(new Props)->arr[] = get_class(new C);',
],
'getClassVariable' => [
'<?php
interface I {}
class C implements I {}
$c_instance = new C;
class Props {
/** @var class-string<I>[] */
public $arr = [];
}
2019-03-23 19:27:54 +01:00
(new Props)->arr[] = get_class($c_instance);',
],
'getClassAnonymousNewInstance' => [
'<?php
interface I {}
class Props {
/** @var class-string<I>[] */
public $arr = [];
}
2019-03-23 19:27:54 +01:00
(new Props)->arr[] = get_class(new class implements I{});',
],
'getClassAnonymousVariable' => [
'<?php
interface I {}
$anon_instance = new class implements I {};
class Props {
/** @var class-string<I>[] */
public $arr = [];
}
2019-03-23 19:27:54 +01:00
(new Props)->arr[] = get_class($anon_instance);',
],
'mktime' => [
'<?php
/** @psalm-suppress InvalidScalarArgument */
$a = mktime("foo");
/** @psalm-suppress MixedArgument */
$b = mktime($_GET["foo"]);
$c = mktime(1, 2, 3);',
'assertions' => [
'$a' => 'false|int',
'$b' => 'false|int',
'$c' => 'int',
2019-07-05 22:24:00 +02:00
],
],
2019-04-03 23:08:37 +02:00
'PHP73-hrtime' => [
'<?php
$a = hrtime(true);
$b = hrtime();
/** @psalm-suppress InvalidScalarArgument */
2019-04-03 23:08:37 +02:00
$c = hrtime(1);
$d = hrtime(false);',
'assertions' => [
'$a' => 'int',
2019-06-16 15:42:34 +02:00
'$b' => 'array{0: int, 1: int}',
'$c' => 'array{0: int, 1: int}|int',
'$d' => 'array{0: int, 1: int}',
2019-04-03 23:08:37 +02:00
],
],
'PHP73-hrtimeCanBeFloat' => [
'<?php
$a = hrtime(true);
if (is_int($a)) {}
if (is_float($a)) {}',
],
'min' => [
'<?php
$a = min(0, 1);
$b = min([0, 1]);
$c = min("a", "b");
$d = min(1, 2, 3, 4);
$e = min(1, 2, 3, 4, 5);
$f = min(...[1, 2, 3]);',
'assertions' => [
'$a' => 'int',
'$b' => 'int',
'$c' => 'string',
'$d' => 'int',
'$e' => 'int',
'$f' => 'int',
],
],
'minUnpackedArg' => [
'<?php
$f = min(...[1, 2, 3]);',
'assertions' => [
'$f' => 'int',
],
],
'sscanf' => [
'<?php
sscanf("10:05:03", "%d:%d:%d", $hours, $minutes, $seconds);',
'assertions' => [
'$hours' => 'float|int|null|string',
'$minutes' => 'float|int|null|string',
'$seconds' => 'float|int|null|string',
],
],
'noImplicitAssignmentToStringFromMixedWithDocblockTypes' => [
'<?php
/** @param string $s */
function takesString($s) : void {}
function takesInt(int $i) : void {}
/**
* @param mixed $s
* @psalm-suppress MixedArgument
*/
function bar($s) : void {
takesString($s);
takesInt($s);
}',
],
'ignoreNullableIssuesAfterMixedCoercion' => [
'<?php
function takesNullableString(?string $s) : void {}
function takesString(string $s) : void {}
/**
* @param mixed $s
* @psalm-suppress MixedArgument
*/
function bar($s) : void {
takesNullableString($s);
takesString($s);
}',
],
'countableSimpleXmlElement' => [
'<?php
$xml = new SimpleXMLElement("<?xml version=\"1.0\"?><a><b></b><b></b></a>");
2019-07-05 22:24:00 +02:00
echo count($xml);',
],
'countableCallableArray' => [
'<?php
/** @param callable|false $x */
function example($x) : void {
if (is_array($x)) {
echo "Count is: " . count($x);
}
}'
],
'countNonEmptyArrayShouldBePositiveInt' => [
'<?php
/**
* @psalm-pure
* @param non-empty-list $x
* @return positive-int
*/
function example($x) : int {
return count($x);
}',
],
'countListShouldBeZeroOrPositive' => [
'<?php
/**
* @psalm-pure
* @param list $x
* @return positive-int|0
*/
function example($x) : int {
return count($x);
}',
],
'countArrayShouldBeZeroOrPositive' => [
'<?php
/**
* @psalm-pure
* @param array $x
* @return positive-int|0
*/
function example($x) : int {
return count($x);
}',
],
'countEmptyArrayShouldBeZero' => [
'<?php
/**
* @psalm-pure
* @param array<empty, empty> $x
* @return 0
*/
function example($x) : int {
return count($x);
}',
],
'countConstantSizeArrayShouldBeConstantInteger' => [
'<?php
/**
* @psalm-pure
* @param array{int, int, string} $x
* @return 3
*/
function example($x) : int {
return count($x);
}',
],
'countCallableArrayShouldBe2' => [
'<?php
/**
* @psalm-pure
* @return 2
*/
function example(callable $x) : int {
assert(is_array($x));
return count($x);
}',
],
'countOnPureObjectIsPure' => [
'<?php
class PureCountable implements \Countable {
/** @psalm-pure */
public function count(): int { return 1; }
}
/** @psalm-pure */
function example(PureCountable $x) : int {
return count($x);
}',
],
'refineWithTraitExists' => [
'<?php
function foo(string $s) : void {
if (trait_exists($s)) {
new ReflectionClass($s);
}
2019-07-05 22:24:00 +02:00
}',
],
'refineWithClassExistsOrTraitExists' => [
'<?php
function foo(string $s) : void {
if (trait_exists($s) || class_exists($s)) {
new ReflectionClass($s);
}
}
function bar(string $s) : void {
if (class_exists($s) || trait_exists($s)) {
new ReflectionClass($s);
}
}
function baz(string $s) : void {
if (class_exists($s) || interface_exists($s) || trait_exists($s)) {
new ReflectionClass($s);
}
2019-07-05 22:24:00 +02:00
}',
],
'minSingleArg' => [
'<?php
/** @psalm-suppress TooFewArguments */
min(0);',
],
'PHP73-allowIsCountableToInformType' => [
'<?php
function getObject() : iterable{
return [];
}
$iterableObject = getObject();
if (is_countable($iterableObject)) {
if (count($iterableObject) === 0) {}
}',
],
'versionCompareAsCallable' => [
'<?php
$a = ["1.0", "2.0"];
usort($a, "version_compare");',
],
'coerceToObjectAfterBeingCalled' => [
'<?php
class Foo {
public function bar() : void {}
}
function takesFoo(Foo $foo) : void {}
/** @param mixed $f */
function takesMixed($f) : void {
if (rand(0, 1)) {
$f = new Foo();
}
/** @psalm-suppress MixedArgument */
takesFoo($f);
$f->bar();
2019-07-05 22:24:00 +02:00
}',
],
'functionExists' => [
'<?php
if (!function_exists("in_array")) {
function in_array($a, $b) {
return true;
}
2019-07-05 22:24:00 +02:00
}',
],
'pregMatch' => [
'<?php
function takesInt(int $i) : void {}
takesInt(preg_match("{foo}", "foo"));',
],
2021-11-30 03:19:27 +01:00
'pregMatch2' => [
'<?php
$r = preg_match("{foo}", "foo");',
'assertions' => [
'$r===' => '0|1|false',
],
],
'pregMatchWithMatches' => [
'<?php
/** @param string[] $matches */
function takesMatches(array $matches) : void {}
preg_match("{foo}", "foo", $matches);
takesMatches($matches);',
],
2021-11-30 03:19:27 +01:00
'pregMatchWithMatches2' => [
'<?php
$r = preg_match("{foo}", "foo", $matches);',
'assertions' => [
'$r===' => '0|1|false',
'$matches===' => 'array<array-key, string>',
],
],
'pregMatchWithOffset' => [
'<?php
/** @param string[] $matches */
function takesMatches(array $matches) : void {}
preg_match("{foo}", "foo", $matches, 0, 10);
takesMatches($matches);',
],
2021-11-30 03:19:27 +01:00
'pregMatchWithOffset2' => [
'<?php
$r = preg_match("{foo}", "foo", $matches, 0, 10);',
'assertions' => [
'$r===' => '0|1|false',
'$matches===' => 'array<array-key, string>',
],
],
'pregMatchWithFlags' => [
'<?php
function takesInt(int $i) : void {}
if (preg_match("{foo}", "this is foo", $matches, PREG_OFFSET_CAPTURE)) {
takesInt($matches[0][1]);
}',
],
2021-11-30 03:19:27 +01:00
'pregMatchWithFlagOffsetCapture' => [
'<?php
$r = preg_match("{foo}", "foo", $matches, PREG_OFFSET_CAPTURE);',
'assertions' => [
'$r===' => '0|1|false',
'$matches===' => 'array<array-key, array{string, int}>',
],
],
'PHP72-pregMatchWithFlagUnmatchedAsNull' => [
2021-11-30 03:19:27 +01:00
'<?php
$r = preg_match("{foo}", "foo", $matches, PREG_UNMATCHED_AS_NULL);',
'assertions' => [
'$r===' => '0|1|false',
'$matches===' => 'array<array-key, null|string>',
],
],
'PHP72-pregMatchWithFlagOffsetCaptureAndUnmatchedAsNull' => [
2021-11-30 03:19:27 +01:00
'<?php
$r = preg_match("{foo}", "foo", $matches, PREG_OFFSET_CAPTURE | PREG_UNMATCHED_AS_NULL);',
2021-11-30 03:19:27 +01:00
'assertions' => [
'$r===' => '0|1|false',
'$matches===' => 'array<array-key, array{null|string, int}>',
],
],
'pregReplaceCallback' => [
'<?php
function foo(string $s) : string {
return preg_replace_callback(
\'/<files (psalm-version="[^"]+") (?:php-version="(.+)">\n)/\',
/** @param string[] $matches */
function (array $matches) : string {
return $matches[1];
},
$s
);
}',
],
'pregReplaceCallbackWithArray' => [
'<?php
/**
* @param string[] $ids
* @psalm-suppress MissingClosureReturnType
* @psalm-suppress MixedArgumentTypeCoercion
*/
function(array $ids): array {
return \preg_replace_callback(
"",
fn (array $matches) => $matches[4],
$ids
);
Test parallelization (#4045) * Run tests in random order Being able to run tests in any order is a pre-requisite for being able to run them in parallel. * Reset type coverage between tests, fix affected tests * Reset parser and lexer between test runs and on php version change Previously lexer was reset, but parser kept the reference to the old one, and reference to the parser was kept by StatementsProvider. This resulted in order-dependent tests - if the parser was first initialized with phpVersion set to 7.4 then arrow functions worked fine, but were failing when the parser was initially constructed with settings for 7.3 This can be demonstrated on current master by upgrading to nikic/php-parser:4.9 and running: ``` vendor/bin/phpunit --no-coverage --filter="inferredArgArrowFunction" tests/ClosureTest.php ``` Now all tests using PHP 7.4 features must set the PHP version accordingly. * Marked more tests using 7.4 syntax * Reset newline-between-annotation flag between tests * Resolve real paths before passing them to checkPaths When checkPaths is called from psalm.php the paths are resolved, so we just mimicking SUT behaviour here. * Restore newline-between-annotations in DocCommentTest * Tweak Appveyor caches * Tweak TravisCI caches * Tweak CircleCI caches * Run tests in parallel Use `vendor/bin/paratest` instead of `vendor/bin/phpunit` * Use default paratest runner on Windows WrapperRunner is not supported on Windows. * TRAVIS_TAG could be empty * Restore appveyor conditional caching
2020-08-23 16:32:07 +02:00
};',
'assertions' => [],
'error_levels' => [],
'7.4'
],
2019-07-24 23:24:23 +02:00
'compactDefinedVariable' => [
'<?php
/**
* @return array<string, mixed>
*/
2019-07-24 23:24:23 +02:00
function foo(int $a, string $b, bool $c) : array {
return compact("a", "b", "c");
}',
],
'PHP73-setCookiePhp73' => [
'<?php
setcookie(
"name",
"value",
[
"path" => "/",
"expires" => 0,
"httponly" => true,
"secure" => true,
"samesite" => "Lax"
]
);',
],
'printrBadArg' => [
'<?php
/** @psalm-suppress InvalidScalarArgument */
$a = print_r([], 1);
echo $a;',
],
'dontCoerceCallMapArgs' => [
'<?php
function getStr() : ?string {
return rand(0,1) ? "test" : null;
}
function test() : void {
$g = getStr();
/** @psalm-suppress PossiblyNullArgument */
$x = strtoupper($g);
$c = "prefix " . (strtoupper($g ?? "") === "x" ? "xa" : "ya");
echo "$x, $c\n";
}'
],
'mysqliRealConnectFunctionAllowsNullParameters' => [
'<?php
$mysqli = mysqli_init();
mysqli_real_connect($mysqli, null, \'test\', null);',
],
'callUserFunc' => [
'<?php
$func = function(int $arg1, int $arg2) : int {
return $arg1 * $arg2;
};
$a = call_user_func($func, 2, 4);',
[
'$a' => 'int',
]
],
'callUserFuncArray' => [
'<?php
$func = function(int $arg1, int $arg2) : int {
return $arg1 * $arg2;
};
$a = call_user_func_array($func, [2, 4]);',
[
'$a' => 'int',
]
],
2020-04-15 03:07:44 +02:00
'dateTest' => [
'<?php
$y = date("Y");
$m = date("m");
$F = date("F");
$y2 = date("Y", 10000);
$F2 = date("F", 10000);
/** @psalm-suppress MixedArgument */
$F3 = date("F", $_GET["F3"]);',
[
'$y' => 'numeric-string',
'$m' => 'numeric-string',
'$F' => 'string',
'$y2' => 'numeric-string',
'$F2' => 'string',
'$F3' => 'false|string',
]
],
'sscanfReturnTypeWithTwoParameters' => [
'<?php
$data = sscanf("42 psalm road", "%s %s");',
[
'$data' => 'list<float|int|null|string>|null',
]
],
'sscanfReturnTypeWithMoreThanTwoParameters' => [
'<?php
$n = sscanf("42 psalm road", "%s %s", $p1, $p2);',
[
'$n' => 'int',
'$p1' => 'float|int|null|string',
'$p2' => 'float|int|null|string',
],
],
2020-06-24 17:51:24 +02:00
'writeArgsAllowed' => [
'<?php
2021-11-29 14:24:15 +01:00
/**
* @param 0|256|512|768 $flags
* @return false|int
*/
2020-06-24 17:51:24 +02:00
function safeMatch(string $pattern, string $subject, ?array $matches = null, int $flags = 0) {
return \preg_match($pattern, $subject, $matches, $flags);
}
safeMatch("/a/", "b");'
],
'fgetcsv' => [
'<?php
$headers = fgetcsv(fopen("test.txt", "r"));
if (empty($headers)) {
throw new Exception("invalid headers");
}
print_r(array_map("strval", $headers));'
],
'allowListEqualToRange' => [
'<?php
/** @param array<int, int> $two */
function collectCommit(array $one, array $two) : void {
if ($one && array_values($one) === array_values($two)) {}
}'
],
'pregMatchAll' => [
'<?php
/**
2020-09-05 00:31:50 +02:00
* @return array<list<string>>
*/
function extractUsernames(string $input): array {
preg_match_all(\'/([a-zA-Z])*/\', $input, $matches);
return $matches;
}'
],
'pregMatchAllOffsetCapture' => [
'<?php
function foo(string $input): array {
preg_match_all(\'/([a-zA-Z])*/\', $input, $matches, PREG_OFFSET_CAPTURE);
return $matches[0];
}'
],
'pregMatchAllReturnsFalse' => [
'<?php
/**
* @return int|false
*/
function badpattern() {
return @preg_match_all("foo", "foo", $matches);
}'
],
'strposAllowDictionary' => [
'<?php
function sayHello(string $format): void {
if (strpos("abcdefghijklmno", $format)) {}
}',
],
2020-10-12 15:57:11 +02:00
'curlInitIsResourceAllowedIn7x' => [
'<?php
$ch = curl_init();
if (!is_resource($ch)) {}',
[],
[],
'7.4'
],
2020-10-12 19:04:28 +02:00
'pregSplit' => [
'<?php
/** @return non-empty-list */
function foo(string $s) {
return preg_split("/ /", $s);
}'
],
2020-10-13 02:25:46 +02:00
'pregSplitWithFlags' => [
'<?php
/** @return list<string> */
function foo(string $s) {
return preg_split("/ /", $s, -1, PREG_SPLIT_NO_EMPTY | PREG_SPLIT_DELIM_CAPTURE);
}'
],
'mbConvertEncodingWithArray' => [
'<?php
/**
* @param array<int, string> $str
* @return array<int, string>
*/
function test2(array $str): array {
return mb_convert_encoding($str, "UTF-8", "UTF-8");
}'
],
2020-10-14 23:30:08 +02:00
'getDebugType' => [
'<?php
function foo(mixed $var) : void {
switch (get_debug_type($var)) {
case "string":
echo "a string";
break;
case Exception::class;
echo "an Exception with message " . $var->getMessage();
break;
}
}',
[],
[],
'8.0'
],
'getTypeDoubleThenInt' => [
'<?php
function safe_float(mixed $val): bool {
switch (gettype($val)) {
case "double":
case "integer":
return true;
// ... more cases omitted
default:
return false;
}
}',
[],
[],
'8.0'
],
2021-03-11 06:13:17 +01:00
'maxWithFloats' => [
'<?php
function foo(float $_float): void
{}
foo(max(1.1, 1.2));',
],
'maxWithObjects' => [
'<?php
function foo(DateTimeImmutable $fooDate): string
{
return $fooDate->format("Y");
}
foo(max(new DateTimeImmutable(), new DateTimeImmutable()));',
],
'maxWithMisc' => [
'<?php
$a = max(new DateTimeImmutable(), 1.2);',
[
'$a' => 'DateTimeImmutable|float',
],
],
'strtolowerEmptiness' => [
'<?php
/** @param non-empty-string $s */
function foo(string $s) : void {
$s = strtolower($s);
foo($s);
}',
],
'preventObjectLeakingFromCallmapReference' => [
'<?php
function one(): void
{
try {
exec("", $output);
} catch (Exception $e){
}
}
2021-09-29 22:55:01 +02:00
function two(): array
{
exec("", $lines);
return $lines;
}',
],
2021-09-14 22:42:08 +02:00
'array_is_list' => [
'<?php
function getArray() : array {
return [];
}
$s = getArray();
assert(array_is_list($s));
',
'assertions' => [
'$s' => 'list<mixed>',
],
[],
'8.1',
],
'array_is_list_on_empty_array' => [
'<?php
$a = [];
if(array_is_list($a)) {
//$a is still empty array
}
',
[],
[],
'8.1',
],
'possiblyUndefinedArrayDestructurationOnOptionalArg' => [
'<?php
class A
{
}
function foo(A $a1, A $a2 = null): void
{
}
$arguments = [new A()];
if (mt_rand(1, 10) > 5) {
// when this is done outside if - no errors
$arguments[] = new A();
}
foo(...$arguments);
',
],
'is_aWithStringableClass' => [
'<?php
/**
* @psalm-var class-string<Throwable> $exceptionType
*/
if (\is_a(new Exception(), $exceptionType)) {}
',
],
'strposFirstParamAllowClassString' => [
'<?php
function sayHello(string $needle): void {
if (strpos(DateTime::class, $needle)) {}
}',
],
'mb_strtolowerProducesStringWithSecondArgument' => [
'<?php
$r = mb_strtolower("École", "BASE64");
',
'assertions' => [
'$r===' => 'string',
],
],
'mb_strtolowerProducesLowercaseStringWithNullOrAbsentEncoding' => [
'<?php
$a = mb_strtolower("AAA");
$b = mb_strtolower("AAA", null);
',
'assertions' => [
'$a===' => 'lowercase-string',
'$b===' => 'lowercase-string',
],
[],
'8.1',
],
];
}
/**
* @return iterable<string,array{string,error_message:string,1?:string[],2?:bool,3?:string}>
*/
public function providerInvalidCodeParse(): iterable
{
return [
'invalidScalarArgument' => [
'<?php
2018-01-11 21:50:45 +01:00
function fooFoo(int $a): void {}
fooFoo("string");',
2017-05-27 02:05:57 +02:00
'error_message' => 'InvalidScalarArgument',
],
'invalidArgumentWithDeclareStrictTypes' => [
'<?php declare(strict_types=1);
function fooFoo(int $a): void {}
fooFoo("string");',
'error_message' => 'InvalidArgument',
],
'builtinFunctioninvalidArgumentWithWeakTypes' => [
'<?php
$s = substr(5, 4);',
'error_message' => 'InvalidScalarArgument',
],
'builtinFunctioninvalidArgumentWithDeclareStrictTypes' => [
'<?php declare(strict_types=1);
$s = substr(5, 4);',
'error_message' => 'InvalidArgument',
],
'builtinFunctioninvalidArgumentWithDeclareStrictTypesInClass' => [
'<?php declare(strict_types=1);
class A {
public function foo() : void {
$s = substr(5, 4);
}
}',
'error_message' => 'InvalidArgument',
],
'mixedArgument' => [
'<?php
2018-01-11 21:50:45 +01:00
function fooFoo(int $a): void {}
/** @var mixed */
$a = "hello";
fooFoo($a);',
'error_message' => 'MixedArgument',
2017-05-27 02:05:57 +02:00
'error_levels' => ['MixedAssignment'],
],
'nullArgument' => [
'<?php
2018-01-11 21:50:45 +01:00
function fooFoo(int $a): void {}
fooFoo(null);',
2017-05-27 02:05:57 +02:00
'error_message' => 'NullArgument',
],
'tooFewArguments' => [
'<?php
2018-01-11 21:50:45 +01:00
function fooFoo(int $a): void {}
fooFoo();',
2017-05-27 02:05:57 +02:00
'error_message' => 'TooFewArguments',
],
'tooManyArguments' => [
'<?php
2018-01-11 21:50:45 +01:00
function fooFoo(int $a): void {}
fooFoo(5, "dfd");',
'error_message' => 'TooManyArguments - src' . DIRECTORY_SEPARATOR . 'somefile.php:3:21 - Too many arguments for fooFoo '
. '- expecting 1 but saw 2',
],
'tooManyArgumentsForConstructor' => [
'<?php
class A { }
new A("hello");',
2017-05-27 02:05:57 +02:00
'error_message' => 'TooManyArguments',
],
'typeCoercion' => [
'<?php
class A {}
class B extends A{}
2018-01-11 21:50:45 +01:00
function fooFoo(B $b): void {}
fooFoo(new A());',
'error_message' => 'ArgumentTypeCoercion',
],
'arrayTypeCoercion' => [
'<?php
class A {}
class B extends A{}
/**
* @param B[] $b
* @return void
*/
function fooFoo(array $b) {}
fooFoo([new A()]);',
'error_message' => 'ArgumentTypeCoercion',
],
'duplicateParam' => [
'<?php
/**
* @return void
*/
function f($p, $p) {}',
2017-05-27 02:05:57 +02:00
'error_message' => 'DuplicateParam',
'error_levels' => ['MissingParamType'],
],
'invalidParamDefault' => [
'<?php
function f(int $p = false) {}',
2017-05-27 02:05:57 +02:00
'error_message' => 'InvalidParamDefault',
],
'invalidDocblockParamDefault' => [
'<?php
/**
* @param int $p
* @return void
*/
function f($p = false) {}',
2017-05-27 02:05:57 +02:00
'error_message' => 'InvalidParamDefault',
],
2017-09-16 19:16:21 +02:00
'badByRef' => [
'<?php
2018-01-11 21:50:45 +01:00
function fooFoo(string &$v): void {}
fooFoo("a");',
2017-05-27 02:05:57 +02:00
'error_message' => 'InvalidPassByReference',
],
2017-12-22 15:21:23 +01:00
'badArrayByRef' => [
'<?php
2018-01-11 21:50:45 +01:00
function fooFoo(array &$a): void {}
2017-12-22 15:21:23 +01:00
fooFoo([1, 2, 3]);',
'error_message' => 'InvalidPassByReference',
],
'invalidArgAfterCallable' => [
'<?php
/**
* @param callable $callback
* @return void
*/
function route($callback) {
if (!is_callable($callback)) { }
takes_int("string");
}
function takes_int(int $i) {}',
'error_message' => 'InvalidScalarArgument',
'error_levels' => [
'MixedAssignment',
2017-05-27 02:05:57 +02:00
'MixedArrayAccess',
'RedundantConditionGivenDocblockType',
2017-05-27 02:05:57 +02:00
],
],
'undefinedFunctionInArrayMap' => [
'<?php
array_map(
"undefined_function",
[1, 2, 3]
);',
'error_message' => 'UndefinedFunction',
],
'objectLikeKeyChecksAgainstDifferentGeneric' => [
'<?php
/**
* @param array<string, int> $b
*/
2018-01-11 21:50:45 +01:00
function a($b): int
{
return $b["a"];
}
a(["a" => "hello"]);',
'error_message' => 'InvalidScalarArgument',
],
'objectLikeKeyChecksAgainstDifferentTKeyedArray' => [
'<?php
/**
* @param array{a: int} $b
*/
2018-01-11 21:50:45 +01:00
function a($b): int
{
return $b["a"];
}
a(["a" => "hello"]);',
'error_message' => 'InvalidArgument',
],
2018-01-22 06:17:16 +01:00
'possiblyNullFunctionCall' => [
'<?php
$a = rand(0, 1) ? function(): void {} : null;
$a();',
'error_message' => 'PossiblyNullFunctionCall',
],
'possiblyInvalidFunctionCall' => [
'<?php
$a = rand(0, 1) ? function(): void {} : 23515;
2018-01-22 06:17:16 +01:00
$a();',
'error_message' => 'PossiblyInvalidFunctionCall',
],
2018-04-03 04:19:58 +02:00
'varExportAssignmentToVoid' => [
'<?php
$a = var_export(["a"]);',
'error_message' => 'AssignmentToVoid',
],
2018-04-16 22:03:04 +02:00
'explodeWithEmptyString' => [
'<?php
function exploder(string $s) : array {
return explode("", $s);
}',
'error_message' => 'FalsableReturnStatement',
],
'complainAboutArrayToIterable' => [
'<?php
class A {}
class B {}
/**
* @param iterable<mixed,A> $p
*/
function takesIterableOfA(iterable $p): void {}
takesIterableOfA([new B]); // should complain',
'error_message' => 'InvalidArgument',
],
'complainAboutArrayToIterableSingleParam' => [
'<?php
class A {}
class B {}
/**
* @param iterable<A> $p
*/
function takesIterableOfA(iterable $p): void {}
takesIterableOfA([new B]); // should complain',
'error_message' => 'InvalidArgument',
],
'putInvalidTypeMessagesFirst' => [
'<?php
$q = rand(0,1) ? new stdClass : false;
strlen($q);',
'error_message' => 'InvalidArgument',
],
'getTypeInvalidValue' => [
'<?php
/**
* @param mixed $maybe
*/
function matchesTypes($maybe) : void {
$t = gettype($maybe);
if ($t === "bool") {}
}',
'error_message' => 'TypeDoesNotContainType',
],
'rangeWithFloatStep' => [
'<?php
function foo(int $bar) : string {
return (string) $bar;
}
foreach (range(1, 10, .3) as $x) {
foo($x);
}',
'error_message' => 'InvalidScalarArgument',
],
'rangeWithFloatStart' => [
'<?php
function foo(int $bar) : string {
return (string) $bar;
}
foreach (range(1.4, 10) as $x) {
foo($x);
}',
'error_message' => 'InvalidScalarArgument',
],
2019-01-06 22:40:44 +01:00
'duplicateFunction' => [
'<?php
function f() : void {}
function f() : void {}',
'error_message' => 'DuplicateFunction',
],
'duplicateCoreFunction' => [
'<?php
function sort() : void {}',
'error_message' => 'DuplicateFunction',
],
'functionCallOnMixed' => [
'<?php
/**
* @var mixed $s
* @psalm-suppress MixedAssignment
*/
$s = 1;
$s();',
'error_message' => 'MixedFunctionCall',
],
2019-02-18 22:35:23 +01:00
'iterableOfObjectCannotAcceptIterableOfInt' => [
'<?php
/** @param iterable<string,object> $_p */
function accepts(iterable $_p): void {}
/** @return iterable<int,int> */
function iterable() { yield 1; }
accepts(iterable());',
'error_message' => 'InvalidArgument',
],
'iterableOfObjectCannotAcceptTraversableOfInt' => [
'<?php
/** @param iterable<string,object> $_p */
function accepts(iterable $_p): void {}
/** @return Traversable<int,int> */
function traversable() { yield 1; }
accepts(traversable());',
'error_message' => 'InvalidArgument',
],
'iterableOfObjectCannotAcceptGeneratorOfInt' => [
'<?php
/** @param iterable<string,object> $_p */
function accepts(iterable $_p): void {}
/** @return Generator<int,int,mixed,void> */
function generator() { yield 1; }
accepts(generator());',
'error_message' => 'InvalidArgument',
],
'iterableOfObjectCannotAcceptArrayOfInt' => [
'<?php
/** @param iterable<string,object> $_p */
function accepts(iterable $_p): void {}
/** @return array<int,int> */
function arr() { return [1]; }
accepts(arr());',
'error_message' => 'InvalidArgument',
],
'nonNullableByRef' => [
'<?php
function foo(string &$s) : void {}
function bar() : void {
foo($bar);
}',
'error_message' => 'NullReference',
],
2019-02-21 19:26:37 +01:00
'intCastByRef' => [
'<?php
function foo(int &$i) : void {}
$a = rand(0, 1) ? null : 5;
/** @psalm-suppress MixedArgument */
foo((int) $a);',
'error_message' => 'InvalidPassByReference',
],
'implicitAssignmentToStringFromMixed' => [
'<?php
/** @param "a"|"b" $s */
function takesString(string $s) : void {}
function takesInt(int $i) : void {}
/**
* @param mixed $s
* @psalm-suppress MixedArgument
*/
function bar($s) : void {
takesString($s);
takesInt($s);
}',
2019-07-05 22:24:00 +02:00
'error_message' => 'InvalidScalarArgument',
2019-02-21 19:26:37 +01:00
],
'tooFewArgsAccurateCount' => [
'<?php
preg_match(\'/adsf/\');',
'error_message' => 'TooFewArguments - src' . DIRECTORY_SEPARATOR . 'somefile.php:2:21 - Too few arguments for preg_match - expecting 2 but saw 1',
2019-07-05 22:24:00 +02:00
],
2019-07-24 23:24:23 +02:00
'compactUndefinedVariable' => [
'<?php
/**
* @return array<string, mixed>
*/
2019-07-24 23:24:23 +02:00
function foo() : array {
return compact("a", "b", "c");
}',
'error_message' => 'UndefinedVariable',
],
'countCallableArrayShouldBeTwo' => [
'<?php
/** @param callable|false $x */
function example($x) : void {
if (is_array($x)) {
$c = count($x);
if ($c !== 2) {}
}
}',
'error_message' => 'TypeDoesNotContainType',
],
'countOnObjectCannotBePositive' => [
'<?php
/** @return positive-int|0 */
function example(\Countable $x) : int {
return count($x);
}',
'error_message' => 'LessSpecificReturnStatement',
],
'countOnUnknownObjectCannotBePure' => [
'<?php
/** @psalm-pure */
function example(\Countable $x) : int {
return count($x);
}',
'error_message' => 'ImpureFunctionCall',
],
'coerceCallMapArgsInStrictMode' => [
'<?php
declare(strict_types=1);
function getStr() : ?string {
return rand(0,1) ? "test" : null;
}
function test() : void {
$g = getStr();
/** @psalm-suppress PossiblyNullArgument */
$x = strtoupper($g);
$c = "prefix " . (strtoupper($g ?? "") === "x" ? "xa" : "ya");
echo "$x, $c\n";
}',
2020-11-26 02:04:57 +01:00
'error_message' => 'RedundantCondition',
],
2019-10-01 14:45:36 +02:00
'noCrashOnEmptyArrayPush' => [
'<?php
array_push();',
'error_message' => 'TooFewArguments',
],
'printOnlyString' => [
'<?php
print [];',
'error_message' => 'InvalidArgument',
],
'printReturns1' => [
'<?php
(print "test") === 2;',
'error_message' => 'TypeDoesNotContainType',
],
'sodiumMemzeroNullifyString' => [
'<?php
function returnsStr(): string {
$str = "x";
sodium_memzero($str);
return $str;
}',
'error_message' => 'NullableReturnStatement'
],
'noCrashWithPattern' => [
'<?php
echo !\is_callable($loop_callback)
|| (\is_array($loop_callback)
&& !\method_exists(...$loop_callback));',
'error_message' => 'UndefinedGlobalVariable'
],
'parseUrlPossiblyUndefined' => [
'<?php
function bar(string $s) : string {
$parsed = parse_url($s);
return $parsed["host"];
}',
'error_message' => 'PossiblyUndefinedArrayOffset',
],
'parseUrlPossiblyUndefined2' => [
'<?php
function bag(string $s) : string {
$parsed = parse_url($s);
if (is_string($parsed["host"] ?? false)) {
return $parsed["host"];
}
return "";
}',
'error_message' => 'PossiblyUndefinedArrayOffset',
],
'strposNoSetFirstParam' => [
'<?php
function sayHello(string $format): void {
if (strpos("u", $format)) {}
}',
2020-09-11 04:44:35 +02:00
'error_message' => 'InvalidLiteralArgument',
],
2020-10-12 15:57:11 +02:00
'curlInitIsResourceFailsIn8x' => [
'<?php
$ch = curl_init();
if (!is_resource($ch)) {}',
'error_message' => 'RedundantCondition',
[],
false,
'8.0'
],
'maxCallWithArray' => [
'<?php
function foo(array $a) {
max($a);
}',
'error_message' => 'ArgumentTypeCoercion',
],
2020-10-12 19:04:28 +02:00
'pregSplitNoEmpty' => [
'<?php
/** @return non-empty-list */
function foo(string $s) {
return preg_split("/ /", $s, -1, PREG_SPLIT_NO_EMPTY);
}',
'error_message' => 'InvalidReturnStatement'
],
2021-03-11 06:13:17 +01:00
'maxWithMixed' => [
'<?php
/** @var mixed $b */;
/** @var mixed $c */;
$a = max($b, $c);',
'error_message' => 'MixedAssignment'
],
'literalFalseArgument' => [
'<?php
function takesAString(string $s): void{
echo $s;
}
takesAString(false);',
'error_message' => 'InvalidArgument'
],
'getClassWithoutArgsOutsideClass' => [
'<?php
echo get_class();',
'error_message' => 'TooFewArguments',
],
];
}
2021-07-19 19:40:17 +02:00
public function testTriggerErrorDefault(): void
{
2021-12-03 20:11:20 +01:00
$config = Config::getInstance();
2021-07-19 19:40:17 +02:00
$config->trigger_error_exits = 'default';
$this->addFile(
'somefile.php',
'<?php
/** @return true */
function returnsTrue(): bool {
return trigger_error("", E_USER_NOTICE);
}
/** @return never */
function returnsNever(): void {
trigger_error("", E_USER_ERROR);
}
/**
* @psalm-suppress ArgumentTypeCoercion
* @return mixed
*/
function returnsNeverOrBool(int $i) {
return trigger_error("", $i);
}'
);
//will only pass if no exception is thrown
$this->assertTrue(true);
2021-12-03 20:11:20 +01:00
$this->analyzeFile('somefile.php', new Context());
2021-07-19 19:40:17 +02:00
}
public function testTriggerErrorAlways(): void
{
2021-12-03 20:11:20 +01:00
$config = Config::getInstance();
2021-07-19 19:40:17 +02:00
$config->trigger_error_exits = 'always';
$this->addFile(
'somefile.php',
'<?php
/** @return never */
function returnsNever1(): void {
trigger_error("", E_USER_NOTICE);
}
/** @return never */
function returnsNever2(): void {
trigger_error("", E_USER_ERROR);
}'
);
//will only pass if no exception is thrown
$this->assertTrue(true);
2021-12-03 20:11:20 +01:00
$this->analyzeFile('somefile.php', new Context());
2021-07-19 19:40:17 +02:00
}
2021-07-20 23:55:49 +02:00
public function testTriggerErrorNever(): void
2021-07-19 19:40:17 +02:00
{
2021-12-03 20:11:20 +01:00
$config = Config::getInstance();
2021-07-20 23:53:04 +02:00
$config->trigger_error_exits = 'never';
2021-07-19 19:40:17 +02:00
$this->addFile(
'somefile.php',
'<?php
/** @return true */
function returnsTrue1(): bool {
return trigger_error("", E_USER_NOTICE);
}
/** @return true */
function returnsTrue2(): bool {
return trigger_error("", E_USER_ERROR);
}'
);
//will only pass if no exception is thrown
$this->assertTrue(true);
2021-12-03 20:11:20 +01:00
$this->analyzeFile('somefile.php', new Context());
2021-07-19 19:40:17 +02:00
}
2016-12-12 05:41:11 +01:00
}