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

Merge branch '4.x' into feature/upgrade-lsp

This commit is contained in:
Andrew Nagy 2022-02-14 21:06:01 +00:00
commit c679692e06
24 changed files with 784 additions and 30 deletions

View File

@ -2,7 +2,7 @@
<psalm
name="Psalm for Psalm"
useDocblockTypes="true"
totallyTyped="true"
errorLevel=1
strictBinaryOperands="false"
rememberPropertyAssignmentsAfterCall="true"
throwExceptionOnError="0"

View File

@ -97,7 +97,18 @@
<xs:attribute name="resolveFromConfigFile" type="xs:boolean" default="true" />
<xs:attribute name="strictBinaryOperands" type="xs:boolean" default="false" />
<xs:attribute name="throwExceptionOnError" type="xs:boolean" default="false" />
<xs:attribute name="totallyTyped" type="xs:boolean" default="false" />
<xs:attribute name="totallyTyped" type="xs:boolean" default="false">
<xs:annotation>
<xs:documentation xml:lang="en">
Setting `totallyTyped` to `"true"` is equivalent to setting `errorLevel` to `"1"`. Setting `totallyTyped` to `"false"` is equivalent to setting `errorLevel` to `"2"` and `reportMixedIssues` to `"false"`
</xs:documentation>
<!-- note: for PHPStorm to mark the attribute as deprecated the doc entry has to be *single line* and start with the word `deprecated` -->
<xs:documentation xml:lang="en">
Deprecated. Replaced by `errorLevel` and `reportMixedIssues`.
</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="errorLevel" type="xs:integer" default="2" />
<xs:attribute name="reportMixedIssues" type="xs:boolean" default="true" />
<xs:attribute name="useDocblockTypes" type="xs:boolean" default="true" />

View File

@ -2535,7 +2535,7 @@ return [
'error_log' => ['bool', 'message'=>'string', 'message_type='=>'int', 'destination='=>'string', 'additional_headers='=>'string'],
'error_reporting' => ['int', 'error_level='=>'int'],
'ErrorException::__clone' => ['void'],
'ErrorException::__construct' => ['void', 'message='=>'string', 'code='=>'int', 'severity='=>'int', 'filename='=>'string', 'lineno='=>'int', 'previous='=>'?Throwable|?ErrorException'],
'ErrorException::__construct' => ['void', 'message='=>'string', 'code='=>'int', 'severity='=>'int', 'filename='=>'string', 'line='=>'int', 'previous='=>'?Throwable|?ErrorException'],
'ErrorException::__toString' => ['string'],
'ErrorException::getCode' => ['int'],
'ErrorException::getFile' => ['string'],

View File

@ -1382,7 +1382,7 @@ return [
'Error::getTrace' => ['list<array<string,mixed>>'],
'Error::getTraceAsString' => ['string'],
'ErrorException::__clone' => ['void'],
'ErrorException::__construct' => ['void', 'message='=>'string', 'code='=>'int', 'severity='=>'int', 'filename='=>'string', 'lineno='=>'int', 'previous='=>'?Throwable|?ErrorException'],
'ErrorException::__construct' => ['void', 'message='=>'string', 'code='=>'int', 'severity='=>'int', 'filename='=>'string', 'line='=>'int', 'previous='=>'?Throwable|?ErrorException'],
'ErrorException::__toString' => ['string'],
'ErrorException::getCode' => ['int'],
'ErrorException::getFile' => ['string'],

View File

@ -256,7 +256,7 @@ return [
'client_info' => 'string',
'client_version' => 'int',
'connect_errno' => 'int',
'connect_error' => 'string',
'connect_error' => '?string',
'errno' => 'int',
'error' => 'string',
'error_list' => 'array',

View File

@ -416,6 +416,13 @@ Whether or not to allow `require`/`include` calls in your PHP. Defaults to `true
```
Allows you to hard-code a serializer for Psalm to use when caching data. By default, Psalm uses `ext-igbinary` *if* the version is greater than or equal to 2.0.5, otherwise it defaults to PHP's built-in serializer.
#### threads
```xml
<psalm
threads="[int]"
>
```
Allows you to hard-code the number of threads Psalm will use (similar to `--threads` on the command line). This value will be used in place of detecting threads from the host machine, but will be overridden by using `--threads` or `--debug` (which sets threads to 1) on the command line
## Project settings

View File

@ -584,6 +584,9 @@ class Config
*/
public $internal_stubs = [];
/** @var ?int */
public $threads;
protected function __construct()
{
self::$instance = $this;
@ -811,7 +814,9 @@ class Config
$deprecated_attributes = [
'allowCoercionFromStringToClassConst',
'allowPhpStormGenerics',
'forbidEcho'
'forbidEcho',
'loadXdebugStub',
'totallyTyped'
];
$deprecated_elements = [
@ -1241,6 +1246,10 @@ class Config
}
}
if (isset($config_xml->threads)) {
$config->threads = (int)$config_xml->threads;
}
return $config;
}
@ -1781,11 +1790,22 @@ class Config
public function getReportingLevelForFunction(string $issue_type, string $function_id): ?string
{
$level = null;
if (isset($this->issue_handlers[$issue_type])) {
return $this->issue_handlers[$issue_type]->getReportingLevelForFunction($function_id);
$level = $this->issue_handlers[$issue_type]->getReportingLevelForFunction($function_id);
if ($level === null && $issue_type === 'UndefinedFunction') {
// undefined functions trigger global namespace fallback
// so we should also check reporting levels for the symbol in global scope
$root_function_id = preg_replace('/.*\\\/', '', $function_id);
if ($root_function_id !== $function_id) {
/** @psalm-suppress PossiblyUndefinedStringArrayOffset https://github.com/vimeo/psalm/issues/7656 */
$level = $this->issue_handlers[$issue_type]->getReportingLevelForFunction($root_function_id);
}
}
}
return null;
return $level;
}
public function getReportingLevelForArgument(string $issue_type, string $function_id): ?string
@ -2009,6 +2029,12 @@ class Config
$this->internal_stubs[] = $ext_decimal_path;
}
// phpredis
if (extension_loaded('redis')) {
$ext_phpredis_path = $dir_lvl_2 . DIRECTORY_SEPARATOR . 'stubs' . DIRECTORY_SEPARATOR . 'phpredis.phpstub';
$this->internal_stubs[] = $ext_phpredis_path;
}
foreach ($this->internal_stubs as $stub_path) {
if (!file_exists($stub_path)) {
throw new UnexpectedValueException('Cannot locate ' . $stub_path);

View File

@ -108,6 +108,13 @@ class Context
*/
public $inside_assignment = false;
/**
* Whether or not we're inside a try block.
*
* @var bool
*/
public $inside_try = false;
/**
* @var null|CodeLocation
*/

View File

@ -86,9 +86,12 @@ class TryAnalyzer
$old_referenced_var_ids = $try_context->referenced_var_ids;
$was_inside_try = $context->inside_try;
$context->inside_try = true;
if ($statements_analyzer->analyze($stmt->stmts, $context) === false) {
return false;
}
$context->inside_try = $was_inside_try;
if ($try_context->finally_scope) {
foreach ($context->vars_in_scope as $var_id => $type) {

View File

@ -315,6 +315,14 @@ class AssignmentAnalyzer
$assign_value_type->parent_nodes = [
$assignment_node->id => $assignment_node
];
if ($context->inside_try) {
// Copy previous assignment's parent nodes inside a try. Since an exception could be thrown at any
// point this is a workaround to ensure that use of a variable also uses all previous assignments.
if (isset($context->vars_in_scope[$array_var_id])) {
$assign_value_type->parent_nodes += $context->vars_in_scope[$array_var_id]->parent_nodes;
}
}
}
if ($array_var_id && isset($context->vars_in_scope[$array_var_id])) {

View File

@ -16,6 +16,8 @@ use Psalm\Type\Atomic\TCallableString;
use Psalm\Type\Atomic\TNonEmptyString;
use Psalm\Type\Union;
use function dirname;
class MagicConstAnalyzer
{
public static function analyze(
@ -84,10 +86,16 @@ class MagicConstAnalyzer
} else {
$statements_analyzer->node_data->setType($stmt, new Union([new TCallableString]));
}
} elseif ($stmt instanceof PhpParser\Node\Scalar\MagicConst\File
|| $stmt instanceof PhpParser\Node\Scalar\MagicConst\Dir
) {
$statements_analyzer->node_data->setType($stmt, new Union([new TNonEmptyString()]));
} elseif ($stmt instanceof PhpParser\Node\Scalar\MagicConst\Dir) {
$statements_analyzer->node_data->setType(
$stmt,
Type::getString(dirname($statements_analyzer->getSource()->getFilePath()))
);
} elseif ($stmt instanceof PhpParser\Node\Scalar\MagicConst\File) {
$statements_analyzer->node_data->setType(
$stmt,
Type::getString($statements_analyzer->getSource()->getFilePath())
);
} elseif ($stmt instanceof PhpParser\Node\Scalar\MagicConst\Trait_) {
if ($statements_analyzer->getSource() instanceof TraitAnalyzer) {
$statements_analyzer->node_data->setType($stmt, new Union([new TNonEmptyString()]));

View File

@ -246,7 +246,7 @@ final class Psalm
$options['long-progress'] = true;
}
$threads = self::detectThreads($options, $in_ci);
$threads = self::detectThreads($options, $config, $in_ci);
self::emitMacPcreWarning($options, $threads);
@ -909,12 +909,14 @@ final class Psalm
}
}
private static function detectThreads(array $options, bool $in_ci): int
private static function detectThreads(array $options, Config $config, bool $in_ci): int
{
if (isset($options['threads'])) {
$threads = (int)$options['threads'];
} elseif (isset($options['debug']) || $in_ci) {
$threads = 1;
} elseif ($config->threads) {
$threads = $config->threads;
} else {
$threads = max(1, ProjectAnalyzer::getCpuCount() - 1);
}

View File

@ -691,7 +691,7 @@ class IssueBuffer
: $error_count . ' errors'
) . ' found' . "\n";
} else {
self::printSuccessMessage();
self::printSuccessMessage($project_analyzer);
}
$show_info = $project_analyzer->stdout_report_options->show_info;
@ -782,8 +782,12 @@ class IssueBuffer
}
}
public static function printSuccessMessage(): void
public static function printSuccessMessage(ProjectAnalyzer $project_analyzer): void
{
if (!$project_analyzer->stdout_report_options) {
throw new UnexpectedValueException('Cannot print success message without stdout report options');
}
// this message will be printed
$message = "No errors found!";
@ -808,9 +812,15 @@ class IssueBuffer
// text style, 1 = bold
$style = "1";
echo "\e[{$background};{$style}m{$paddingTop}\e[0m" . "\n";
echo "\e[{$background};{$foreground};{$style}m{$messageWithPadding}\e[0m" . "\n";
echo "\e[{$background};{$style}m{$paddingBottom}\e[0m" . "\n";
if ($project_analyzer->stdout_report_options->use_color) {
echo "\e[{$background};{$style}m{$paddingTop}\e[0m" . "\n";
echo "\e[{$background};{$foreground};{$style}m{$messageWithPadding}\e[0m" . "\n";
echo "\e[{$background};{$style}m{$paddingBottom}\e[0m" . "\n";
} else {
echo "\n";
echo "$messageWithPadding\n";
echo "\n";
}
}
/**

View File

@ -56,6 +56,13 @@ namespace {
*/
public function getBackingValue(): int|string;
}
class ReflectionIntersectionType extends ReflectionType {
/**
* @return non-empty-list<ReflectionType>
*/
public function getTypes() {}
}
}
namespace FTP {

542
stubs/phpredis.phpstub Normal file
View File

@ -0,0 +1,542 @@
<?php
/**
* @generate-function-entries
* @generate-legacy-arginfo
*/
class Redis {
public function __construct(array $options = null);
public function _compress(string $value): string;
public function __destruct();
public function _pack(mixed $value): string;
public function _prefix(string $key): string;
public function _serialize(mixed $value): string;
public function _uncompress(string $value): string;
public function _unpack(string $value): mixed;
public function _unserialize(string $value): mixed;
/**
* @param string $args
* @return mixed|Redis
*/
public function acl(string $subcmd, ...$args);
/** @return int|Redis */
public function append(string $key, mixed $value);
public function auth(mixed $credentials): bool;
public function bgSave(): bool;
public function bgrewriteaof(): bool;
/** @return int|Redis */
public function bitcount(string $key, int $start = 0, int $end = -1);
/**
* @return int|Redis
*/
public function bitop(string $operation, string $deskey, string $srckey, string ...$other_keys): int;
/** @return int|Redis */
public function bitpos(string $key, int $bit, int $start = 0, int $end = -1);
public function blPop(string|array $key, string|int $timeout_or_key, mixed ...$extra_args): array|null|false;
public function brPop(string|array $key, string|int $timeout_or_key, mixed ...$extra_args): array|null|false;
public function brpoplpush(string $src, string $dst, int $timeout): Redis|string|false;
public function bzPopMax(string|array $key, string|int $timeout_or_key, mixed ...$extra_args): array;
public function bzPopMin(string|array $key, string|int $timeout_or_key, mixed ...$extra_args): array;
public function clearLastError(): bool;
public function client(string $opt, string $arg = null): mixed;
public function close(): bool;
public function command(string $opt = null, string|array $arg): mixed;
public function config(string $operation, string $key, mixed $value = null): mixed;
public function connect(string $host, int $port = 26379, float $timeout = 0, string $persistent_id = null, int $retry_interval = 0, float $read_timeout = 0, array $context = null): bool;
public function copy(string $src, string $dst, array $options = null): bool;
public function dbSize(): int;
public function debug(string $key): string;
/** @return int|Redis */
public function decr(string $key, int $by = 1);
/** @return int|Redis */
public function decrBy(string $key, int $value);
/**
* @return int|Redis
*/
public function del(array|string $key, string ...$other_keys);
/**
* @deprecated
* @alias Redis::del
* @return int|Redis
*/
public function delete(array|string $key, string ...$other_keys);
public function discard(): bool;
public function dump(string $key): string;
/** @return string|Redis */
public function echo(string $str);
public function eval(string $script, array $keys = null, int $num_keys = 0): mixed;
public function evalsha(string $sha1, array $keys = null, int $num_keys = 0): mixed;
public function exec(): Redis|array|false;
/** @return int|Redis|bool */
public function exists(mixed $key, mixed ...$other_keys);
public function expire(string $key, int $timeout): Redis|bool;
public function expireAt(string $key, int $timestamp): Redis|bool;
public function flushAll(bool $async = false): bool;
public function flushDB(bool $async = false): bool;
public function geoadd(string $key, float $lng, float $lat, string $member, mixed ...$other_triples): int;
public function geodist(string $key, string $src, string $dst, ?string $unit = null): Redis|float|false;
public function geohash(string $key, string $member, string ...$other_members): array;
public function geopos(string $key, string $member, string ...$other_members): Redis|array|false;
public function georadius(string $key, float $lng, float $lat, float $radius, string $unit, array $options = []): Redis|mixed|false;
public function georadius_ro(string $key, float $lng, float $lat, float $radius, string $unit, array $options = []): Redis|mixed|false;
public function georadiusbymember(string $key, string $member, float $radius, string $unit, array $options = []): Redis|mixed|false;
public function georadiusbymember_ro(string $key, string $member, float $radius, string $unit, array $options = []): Redis|mixed|false;
/** @return string|Redis */
public function get(string $key);
public function getAuth(): mixed;
/** @return int|Redis */
public function getBit(string $key, int $idx);
public function getDBNum(): int;
public function getHost(): string;
public function getLastError(): ?string;
public function getMode(): int;
public function getOption(int $option): mixed;
public function getPersistentID(): ?string;
public function getPort(): int;
/** @return string|Redis */
public function getRange(string $key, int $start, int $end);
public function getReadTimeout(): int;
/** @return string|Redis */
public function getset(string $key, mixed $value);
public function getTimeout(): int;
public function hDel(string $key, string $member, string ...$other_members): Redis|int|false;
public function hExists(string $key, string $member): Redis|bool;
public function hGet(string $key, string $member): Redis|mixed|false;
public function hGetAll(string $key): Redis|array|false;
public function hIncrBy(string $key, string $member, int $value): Redis|int|false;
public function hIncrByFloat(string $key, string $member, float $value): Redis|float|false;
public function hKeys(string $key): Redis|array|false;
public function hLen(string $key): Redis|int|false;
public function hMget(string $key, array $keys): Redis|array|false;
public function hMset(string $key, array $keyvals): Redis|bool|false;
public function hSet(string $key, string $member, mixed $value): Redis|int|false;
public function hSetNx(string $key, string $member, string $value): Redis|bool;
public function hStrLen(string $key, string $member): int;
public function hVals(string $key): Redis|array|false;
public function hscan(string $key, int &$iterator, ?string $pattern = null, int $count = 0): bool|array;
/** @return int|Redis */
public function incr(string $key, int $by = 1);
/** @return int|Redis */
public function incrBy(string $key, int $value);
/** @return int|Redis */
public function incrByFloat(string $key, float $value);
public function info(string $opt = null): Redis|array|false;
public function isConnected(): bool;
/** @return array|Redis */
public function keys(string $pattern);
/**
* @param mixed $elements
* @return int|Redis
*/
public function lInsert(string $key, string $pos, mixed $pivot, mixed $value);
public function lLen(string $key): Redis|int|false;
public function lMove(string $src, string $dst, string $wherefrom, string $whereto): string;
/** @return string|Redis */
public function lPop(string $key);
/**
* @param mixed $elements
* @return int|Redis
*/
public function lPush(string $key, ...$elements);
/**
* @param mixed $elements
* @return int|Redis
*/
public function rPush(string $key, ...$elements);
/** @return int|Redis */
public function lPushx(string $key, mixed $value);
/** @return int|Redis */
public function rPushx(string $key, mixed $value);
public function lSet(string $key, int $index, mixed $value): Redis|bool;
public function lastSave(): int;
public function lindex(string $key, int $index): Redis|mixed|false;
public function lrange(string $key, int $start , int $end): Redis|array|false;
/**
* @return int|Redis|false
*/
public function lrem(string $key, mixed $value, int $count = 0);
public function ltrim(string $key, int $start , int $end): Redis|bool;
/** @return array|Redis */
public function mget(array $keys);
public function migrate(string $host, int $port, string $key, string $dst, int $timeout, bool $copy = false, bool $replace = false): bool;
public function move(string $key, int $index): bool;
public function mset(array $key_values): Redis|bool;
public function msetnx(array $key_values): Redis|bool;
public function multi(int $value = Redis::MULTI): bool|Redis;
public function object(string $subcommand, string $key): Redis|int|string|false;
/**
* @deprecated
* @alias Redis::connect
*/
public function open(string $host, int $port = 26379, float $timeout = 0, string $persistent_id = NULL, int $retry_interval = 0, float $read_timeout = 0, array $context = NULL): bool;
public function pconnect(string $host, int $port = 26379, float $timeout = 0, string $persistent_id = NULL, int $retry_interval = 0, float $read_timeout = 0, array $context = NULL): bool;
public function persist(string $key): bool;
public function pexpire(string $key, int $timeout): bool;
public function pexpireAt(string $key, int $timestamp): bool;
public function pfadd(string $key, array $elements): int;
public function pfcount(string $key): int;
public function pfmerge(string $dst, array $keys): bool;
/** @return string|Redis */
public function ping(string $key = NULL);
public function pipeline(): bool|Redis;
/**
* @deprecated
* @alias Redis::pconnect
*/
public function popen(string $host, int $port = 26379, float $timeout = 0, string $persistent_id = NULL, int $retry_interval = 0, float $read_timeout = 0, array $context = NULL): bool;
/** @return bool|Redis */
public function psetex(string $key, int $expire, mixed $value);
public function psubscribe(array $patterns): void;
public function pttl(string $key): Redis|int|false;
public function publish(string $channel, string $message): mixed;
public function pubsub(string $command, mixed $arg = null): mixed;
public function punsubscribe(array $patterns): array;
/** @return string|Redis */
public function rPop(string $key);
/** @return string|Redis */
public function randomKey();
public function rawcommand(string $command, mixed ...$args): mixed;
/** @return bool|Redis */
public function rename(string $key_src, string $key_dst);
/** @return bool|Redis */
public function renameNx(string $key_src, string $key_dst);
public function restore(string $key, int $timeout, string $value): bool;
public function role(): mixed;
public function rpoplpush(string $src, string $dst): Redis|string|false;
public function sAdd(string $key, mixed $value, mixed ...$other_values): Redis|int|false;
public function sAddArray(string $key, array $values): int;
public function sDiff(string $key, string ...$other_keys): Redis|array|false;
public function sDiffStore(string $dst, string $key, string ...$other_keys): Redis|int|false;
public function sInter(array|string $key, string ...$other_keys): Redis|array|false;
public function sInterStore(array|string $key, string ...$other_keys): Redis|int|false;
public function sMembers(string $key): Redis|array|false;
public function sMisMember(string $key, string $member, string ...$other_members): array;
public function sMove(string $src, string $dst, mixed $value): Redis|bool;
public function sPop(string $key, int $count = 0): Redis|string|array|false;
public function sRandMember(string $key, int $count = 0): Redis|string|array|false;
public function sUnion(string $key, string ...$other_keys): Redis|array|false;
public function sUnionStore(string $dst, string $key, string ...$other_keys): Redis|int|false;
public function save(): bool;
public function scan(?int &$iterator, ?string $pattern = null, int $count = 0, string $type = NULL): array|false;
public function scard(string $key): Redis|int|false;
public function script(string $command, mixed ...$args): mixed;
public function select(int $db): bool;
/** @return bool|Redis */
public function set(string $key, mixed $value, mixed $opt = NULL);
/** @return int|Redis */
public function setBit(string $key, int $idx, bool $value);
/** @return int|Redis */
public function setRange(string $key, int $start, string $value);
public function setOption(string $option, mixed $value): bool;
/** @return bool|Redis */
public function setex(string $key, int $expire, mixed $value);
/** @return bool|array|Redis */
public function setnx(string $key, mixed $value);
public function sismember(string $key, mixed $value): Redis|bool;
public function slaveof(string $host = null, int $port = 6379): bool;
public function slowlog(string $mode, int $option = 0): mixed;
public function sort(string $key, array $options = null): mixed;
/**
* @deprecated
*/
public function sortAsc(string $key, ?string $pattern = null, mixed $get = null, int $offset = -1, int $count = -1, ?string $store = null): array;
/**
* @deprecated
*/
public function sortAscAlpha(string $key, ?string $pattern = null, mixed $get = null, int $offset = -1, int $count = -1, ?string $store = null): array;
/**
* @deprecated
*/
public function sortDesc(string $key, ?string $pattern = null, mixed $get = null, int $offset = -1, int $count = -1, ?string $store = null): array;
/**
* @deprecated
*/
public function sortDescAlpha(string $key, ?string $pattern = null, mixed $get = null, int $offset = -1, int $count = -1, ?string $store = null): array;
public function srem(string $key, mixed $value, mixed ...$other_values): Redis|int|false;
public function sscan(string $key, int &$iterator, ?string $pattern = null, int $count = 0): array|false;
/** @return int|Redis */
public function strlen(string $key);
public function subscribe(string $channel, string ...$other_channels): array;
public function swapdb(string $src, string $dst): bool;
public function time(): array;
public function ttl(string $key): Redis|int|false;
/** @return int|Redis */
public function type(string $key);
/**
* @return int|Redis
*/
public function unlink(array|string $key, string ...$other_keys);
public function unsubscribe(string $channel, string ...$other_channels): array;
/** @return bool|Redis */
public function unwatch();
/**
* @return bool|Redis
*/
public function watch(array|string $key, string ...$other_keys);
public function wait(int $count, int $timeout): int|false;
public function xack(string $key, string $group, array $ids): int|false;
public function xadd(string $key, string $id, array $values, int $maxlen = 0, bool $approx = false): string|false;
public function xclaim(string $key, string $group, string $consumer, int $min_iddle, array $ids, array $options): string|array;
public function xdel(string $key, array $ids): Redis|int|false;
public function xgroup(string $operation, string $key = null, string $arg1 = null, string $arg2 = null, bool $arg3 = false): mixed;
public function xinfo(string $operation, ?string $arg1 = null, ?string $arg2 = null, int $count = -1): mixed;
public function xlen(string $key): int;
public function xpending(string $key, string $group, string $start = null, string $end = null, int $count = -1, string $consumer = null): Redis|array|false;
public function xrange(string $key, string $start, string $end, int $count = -1): bool|array;
public function xread(array $streams, int $count = -1, int $block = -1): bool|array;
public function xreadgroup(string $group, string $consumer, array $streams, int $count = 1, int $block = 1): bool|array;
public function xrevrange(string $key, string $start, string $end, int $count = -1): bool|array;
public function xtrim(string $key, int $maxlen, bool $approx = false): int;
public function zAdd(string $key, array|float $score_or_options, mixed ...$more_scores_and_mems): Redis|int|false;
public function zCard(string $key): Redis|int|false;
public function zCount(string $key, string $start , string $end): Redis|int|false;
public function zIncrBy(string $key, float $value, mixed $member): Redis|float|false;
public function zLexCount(string $key, string $min, string $max): Redis|int|false;
public function zMscore(string $key, string $member, string ...$other_members): array;
public function zPopMax(string $key, int $value = null): array;
public function zPopMin(string $key, int $value = null): array;
public function zRange(string $key, int $start, int $end, mixed $scores = null): Redis|array|false;
public function zRangeByLex(string $key, string $min, string $max, int $offset = -1, int $count = -1): array;
public function zRangeByScore(string $key, string $start, string $end, array $options = []): Redis|array|false;
public function zRank(string $key, mixed $member): Redis|int|false;
public function zRem(mixed $key, mixed $member, mixed ...$other_members): Redis|int|false;
public function zRemRangeByLex(string $key, string $min, string $max): int;
public function zRemRangeByRank(string $key, int $start, int $end): Redis|int|false;
public function zRemRangeByScore(string $key, string $start, string $end): Redis|int|false;
public function zRevRange(string $key, int $start, int $end, mixed $scores = null): Redis|array|false;
public function zRevRangeByLex(string $key, string $min, string $max, int $offset = -1, int $count = -1): array;
public function zRevRangeByScore(string $key, string $start, string $end, array $options = []): array;
public function zRevRank(string $key, mixed $member): Redis|int|false;
public function zScore(string $key, mixed $member): Redis|float|false;
public function zdiff(array $keys, array $options = null): array;
public function zdiffstore(string $dst, array $keys, array $options = null): int;
public function zinter(array $keys, ?array $weights = null, ?array $options = null): Redis|array|false;
public function zinterstore(string $dst, array $keys, ?array $weights = null, ?string $aggregate = null): Redis|int|false;
public function zscan(string $key, int &$iterator, ?string $pattern = null, int $count = 0): bool|array;
public function zunion(array $keys, ?array $weights = null, ?array $options = null): Redis|array|false;
public function zunionstore(string $dst, array $keys, ?array $weights = NULL, ?string $aggregate = NULL): Redis|int|false;
}

View File

@ -174,9 +174,7 @@ class ConfigFileTest extends TestCase
{
$noPlugins = trim('
<?xml version="1.0"?>
<psalm
totallyTyped="false"
>
<psalm>
<plugins>
<pluginClass class="d\e\f"/>
</plugins>
@ -185,9 +183,7 @@ class ConfigFileTest extends TestCase
$abcEnabled = trim('
<?xml version="1.0"?>
<psalm
totallyTyped="false"
>
<psalm>
<plugins>
<pluginClass class="a\b\c"/>
<pluginClass class="d\e\f"/>

View File

@ -355,6 +355,36 @@ class ConfigTest extends TestCase
$this->assertFalse($config->reportIssueInFile('MissingReturnType', realpath('src/Psalm/Type.php')));
}
public function testGlobalUndefinedFunctionSuppression(): void
{
$this->project_analyzer = $this->getProjectAnalyzerWithConfig(
Config::loadFromXML(
dirname(__DIR__, 2),
'<?xml version="1.0"?>
<psalm>
<projectFiles>
<directory name="src" />
<directory name="tests" />
</projectFiles>
<issueHandlers>
<UndefinedFunction>
<errorLevel type="suppress">
<referencedFunction name="zzz"/>
</errorLevel>
</UndefinedFunction>
</issueHandlers>
</psalm>'
)
);
$config = $this->project_analyzer->getConfig();
$this->assertSame(
Config::REPORT_SUPPRESS,
$config->getReportingLevelForFunction('UndefinedFunction', 'Some\Namespace\zzz')
);
}
public function testMultipleIssueHandlers(): void
{
$this->project_analyzer = $this->getProjectAnalyzerWithConfig(

View File

@ -113,8 +113,10 @@ class IssueBufferTest extends TestCase
public function testPrintSuccessMessageWorks(): void
{
$project_analyzer = $this->createMock(ProjectAnalyzer::class);
$project_analyzer->stdout_report_options = new ReportOptions;
ob_start();
IssueBuffer::printSuccessMessage();
IssueBuffer::printSuccessMessage($project_analyzer);
$output = ob_get_clean();
$this->assertStringContainsString('No errors found!', $output);

View File

@ -2285,6 +2285,25 @@ class TaintTest extends TestCase
echo sinkNotWorking($_GET["taint"]);',
'error_message' => 'TaintedHtml',
],
'taintEscapedInTryMightNotWork' => [
'<?php
/** @psalm-taint-escape html */
function escapeHtml(string $arg): string
{
return htmlspecialchars($arg);
}
$tainted = $_GET["foo"];
try {
$tainted = escapeHtml($tainted);
} catch (Throwable $_) {
}
echo $tainted;
',
'error_message' => 'TaintedHtml',
],
];
}

View File

@ -2436,6 +2436,43 @@ class UnusedVariableTest extends TestCase
$a = false;
}'
],
'usedInCatchIsAlwaysUsedInTry' => [
'<?php
$step = 0;
try {
$step = 1;
$step = 2;
} catch (Throwable $_) {
echo $step;
}
',
],
'usedInFinallyIsAlwaysUsedInTry' => [
'<?php
$step = 0;
try {
$step = 1;
$step = 2;
} finally {
echo $step;
}
',
],
'usedInFinallyIsAlwaysUsedInTryWithNestedTry' => [
'<?php
$step = 0;
try {
try {
$step = 1;
} finally {
}
$step = 2;
$step = 3;
} finally {
echo $step;
}
',
],
];
}
@ -3416,6 +3453,49 @@ class UnusedVariableTest extends TestCase
}',
'error_message' => 'MixedArgumentTypeCoercion - src' . DIRECTORY_SEPARATOR . 'somefile.php:12:44 - Argument 1 of takesArrayOfString expects array<array-key, string>, parent type array{mixed} provided. Consider improving the type at src' . DIRECTORY_SEPARATOR . 'somefile.php:10:41'
],
'warnAboutUnusedVariableInTryReassignedInCatch' => [
'<?php
$step = 0;
try {
$step = 1;
$step = 2;
} catch (Throwable $_) {
$step = 3;
echo $step;
}
',
'error_message' => 'UnusedVariable',
],
'warnAboutUnusedVariableInTryReassignedInFinally' => [
'<?php
$step = 0;
try {
$step = 1;
$step = 2;
} finally {
$step = 3;
echo $step;
}
',
'error_message' => 'UnusedVariable',
],
'SKIPPED-warnAboutVariableUsedInNestedTryNotUsedInOuterTry' => [
'<?php
$step = 0;
try {
$step = 1; // Unused
$step = 2;
try {
$step = 3;
$step = 4;
} finally {
echo $step;
}
} finally {
}
',
'error_message' => 'UnusedVariable',
],
];
}
}

View File

@ -1,6 +1,5 @@
<?xml version="1.0"?>
<psalm
totallyTyped="true"
autoloader="autoloader.php"
resolveFromConfigFile="true"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"

View File

@ -1,6 +1,5 @@
<?xml version="1.0"?>
<psalm
totallyTyped="true"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="https://getpsalm.org/schema/config"
xsi:schemaLocation="https://getpsalm.org/schema/config vendor/vimeo/psalm/config.xsd"

View File

@ -1,6 +1,5 @@
<?xml version="1.0"?>
<psalm
totallyTyped="true"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="https://getpsalm.org/schema/config"
xsi:schemaLocation="https://getpsalm.org/schema/config vendor/vimeo/psalm/config.xsd"

View File

@ -1,6 +1,5 @@
<?xml version="1.0"?>
<psalm
totallyTyped="true"
autoloader="autoloader.php"
resolveFromConfigFile="true"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"