1
0
mirror of https://github.com/danog/psalm.git synced 2024-11-30 04:39:00 +01:00

Merge pull request #9981 from kkmuffme/fix-replace-functions-return-type-provider-less-specific

fix mixed replace return types for arrays
This commit is contained in:
orklah 2023-07-02 12:59:01 +02:00 committed by GitHub
commit 8d1876a8a3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 110 additions and 74 deletions

View File

@ -23,6 +23,7 @@ use function glob;
use function implode;
use function is_array;
use function is_dir;
use function is_string;
use function json_decode;
use function ksort;
use function max;
@ -250,6 +251,10 @@ final class Creator
}
foreach ($paths as $path) {
if (!is_string($path)) {
continue;
}
if ($path === '') {
$nodes = [...$nodes, ...self::guessPhpFileDirs($current_dir)];

View File

@ -18,6 +18,7 @@ use function glob;
use function in_array;
use function is_dir;
use function is_iterable;
use function is_string;
use function preg_match;
use function preg_replace;
use function preg_split;
@ -57,12 +58,12 @@ class FileFilter
protected $fq_classlike_names = [];
/**
* @var array<string>
* @var array<non-empty-string>
*/
protected $fq_classlike_patterns = [];
/**
* @var array<string>
* @var array<non-empty-string>
*/
protected $method_ids = [];
@ -314,14 +315,18 @@ class FileFilter
if (isset($config['referencedMethod']) && is_iterable($config['referencedMethod'])) {
/** @var array $referenced_method */
foreach ($config['referencedMethod'] as $referenced_method) {
$method_id = (string) ($referenced_method['name'] ?? '');
if (!preg_match('/^[^:]+::[^:]+$/', $method_id) && !static::isRegularExpression($method_id)) {
$method_id = $referenced_method['name'] ?? '';
if (!is_string($method_id)
|| (!preg_match('/^[^:]+::[^:]+$/', $method_id) && !static::isRegularExpression($method_id))) {
throw new ConfigException(
'Invalid referencedMethod ' . $method_id,
'Invalid referencedMethod ' . ((string) $method_id),
);
}
if ($method_id === '') {
continue;
}
$filter->method_ids[] = strtolower($method_id);
}
}
@ -329,7 +334,20 @@ class FileFilter
if (isset($config['referencedFunction']) && is_iterable($config['referencedFunction'])) {
/** @var array $referenced_function */
foreach ($config['referencedFunction'] as $referenced_function) {
$filter->method_ids[] = strtolower((string) ($referenced_function['name'] ?? ''));
$function_id = $referenced_function['name'] ?? '';
if (!is_string($function_id)
|| (!preg_match('/^[a-zA-Z_\x80-\xff][a-zA-Z0-9_\x80-\xff]*$/', $function_id)
&& !static::isRegularExpression($function_id))) {
throw new ConfigException(
'Invalid referencedFunction ' . ((string) $function_id),
);
}
if ($function_id === '') {
continue;
}
$filter->method_ids[] = strtolower($function_id);
}
}
@ -441,8 +459,15 @@ class FileFilter
return self::loadFromArray($config, $base_dir, $inclusive);
}
/**
* @psalm-assert-if-true non-empty-string $string
*/
private static function isRegularExpression(string $string): bool
{
if ($string === '') {
return false;
}
set_error_handler(
static fn(): bool => true,
E_WARNING,

View File

@ -6,13 +6,10 @@ use Psalm\Internal\Analyzer\StatementsAnalyzer;
use Psalm\Plugin\EventHandler\Event\FunctionReturnTypeProviderEvent;
use Psalm\Plugin\EventHandler\FunctionReturnTypeProviderInterface;
use Psalm\Type;
use Psalm\Type\Atomic\TNull;
use Psalm\Type\Atomic\TString;
use Psalm\Type\Union;
use function call_user_func;
use function count;
use function in_array;
/**
* @internal
@ -27,13 +24,10 @@ class StrReplaceReturnTypeProvider implements FunctionReturnTypeProviderInterfac
return [
'str_replace',
'str_ireplace',
'substr_replace',
'preg_replace',
'preg_replace_callback',
];
}
public static function getFunctionReturnType(FunctionReturnTypeProviderEvent $event): Union
public static function getFunctionReturnType(FunctionReturnTypeProviderEvent $event): ?Union
{
$statements_source = $event->getStatementsSource();
$call_args = $event->getCallArgs();
@ -41,47 +35,34 @@ class StrReplaceReturnTypeProvider implements FunctionReturnTypeProviderInterfac
if (!$statements_source instanceof StatementsAnalyzer
|| count($call_args) < 3
) {
return Type::getMixed();
// use the defaults, it will already report an error for the invalid params
return null;
}
if ($subject_type = $statements_source->node_data->getType($call_args[2]->value)) {
if (!$subject_type->hasString() && $subject_type->hasArray()) {
return Type::getArray();
if (!$subject_type->isSingleStringLiteral()) {
return null;
}
$return_type = Type::getString();
if (in_array($function_id, ['str_replace', 'str_ireplace'], true)
&& $subject_type->isSingleStringLiteral()
$first_arg = $statements_source->node_data->getType($call_args[0]->value);
$second_arg = $statements_source->node_data->getType($call_args[1]->value);
if ($first_arg
&& $second_arg && $first_arg->isSingleStringLiteral()
&& $second_arg->isSingleStringLiteral()
) {
$first_arg = $statements_source->node_data->getType($call_args[0]->value);
$second_arg = $statements_source->node_data->getType($call_args[1]->value);
if ($first_arg
&& $second_arg && $first_arg->isSingleStringLiteral()
&& $second_arg->isSingleStringLiteral()
) {
/**
* @var string $replaced_string
*/
$replaced_string = call_user_func(
$function_id,
$first_arg->getSingleStringLiteral()->value,
$second_arg->getSingleStringLiteral()->value,
$subject_type->getSingleStringLiteral()->value,
);
$return_type = Type::getString($replaced_string);
}
} elseif (in_array($function_id, ['preg_replace', 'preg_replace_callback'], true)) {
$codebase = $statements_source->getCodebase();
$return_type = new Union([new TString, new TNull()], [
'ignore_nullable_issues' => $codebase->config->ignore_internal_nullable_issues,
]);
/**
* @var string $replaced_string
*/
$replaced_string = call_user_func(
$function_id,
$first_arg->getSingleStringLiteral()->value,
$second_arg->getSingleStringLiteral()->value,
$subject_type->getSingleStringLiteral()->value,
);
return Type::getString($replaced_string);
}
return $return_type;
}
return Type::getMixed();
return null;
}
}

View File

@ -399,10 +399,10 @@ function array_fill_keys(array $keys, $value): array
*
* @psalm-template TKey
*
* @param string $pattern
* @param array<TKey,string> $array
* @param non-empty-string $pattern
* @param array<TKey, string> $array
* @param 0|1 $flags 1=PREG_GREP_INVERT
* @return array<TKey,string>
* @return array<TKey, string>
*/
function preg_grep($pattern, array $array, $flags = 0)
{
@ -619,7 +619,7 @@ function strtoupper(string $string) : string {}
* @param int|array<int> $offset
* @param null|int|array<int> $length
*
* @return ($string is array<string> ? array<string> : string)
* @return ($string is array ? array<string> : string)
*
* @psalm-flow ($string, $replace) -> return
*/
@ -786,6 +786,8 @@ function explode(string $separator, string $string, int $limit = -1) : array {}
/**
* @psalm-pure
*
* @param non-empty-string $pattern
*
* @psalm-flow ($subject) -(array-assignment)-> return
*
* @template TFlags as int-mask<0, 1, 2, 4>
@ -998,11 +1000,13 @@ function strlen(string $string) : int {}
/**
* @psalm-pure
*
* @template TKey of array-key
*
* @param string|array<string|int|float> $search
* @param string|array<string|int|float> $replace
* @param string|array<string|int|float> $subject
* @param int $count
* @return ($subject is array ? array<string> : string)
* @param string|array<TKey, string|int|float> $subject
* @param int<0, max> $count
* @return ($subject is array ? array<TKey, string> : string)
*
* @psalm-flow ($replace, $subject) -> return
*/
@ -1011,11 +1015,13 @@ function str_replace($search, $replace, $subject, &$count = null) {}
/**
* @psalm-pure
*
* @template TKey of array-key
*
* @param string|array<string|int|float> $search
* @param string|array<string|int|float> $replace
* @param string|array<string|int|float> $subject
* @param int $count
* @return ($subject is array ? array<string> : string)
* @param string|array<TKey, string|int|float> $subject
* @param int<0, max> $count
* @return ($subject is array ? array<TKey, string> : string)
*
* @psalm-flow ($replace, $subject) -> return
*/
@ -1169,10 +1175,10 @@ function str_word_count(string $string, int $format = 0, string|null $characters
/**
* @psalm-pure
*
* @param string|string[] $pattern
* @param non-empty-string|non-empty-string[] $pattern
* @param string|array<string|int|float> $replacement
* @param string|array<string|int|float> $subject
* @param int $count
* @param int<0, max> $count
* @return ($subject is array ? array<string> : string|null)
*
* @psalm-flow ($replacement, $subject) -> return
@ -1182,22 +1188,30 @@ function preg_filter($pattern, $replacement, $subject, int $limit = -1, &$count
/**
* @psalm-pure
*
* @param string|string[] $pattern
* @template TKey of array-key
*
* @param non-empty-string|non-empty-string[] $pattern
* @param string|array<string|int|float> $replacement
* @param string|array<string|int|float> $subject
* @param int $count
* @return ($subject is array ? array<string>|null : string|null)
* @param string|array<TKey, string|int|float> $subject
* @param int<0, max> $count
* @return ($subject is array ? array<TKey, string>|null : string|null)
*
* @psalm-ignore-nullable-return
*
* @psalm-flow ($replacement, $subject) -> return
*/
function preg_replace($pattern, $replacement, $subject, int $limit = -1, &$count = null) {}
/**
* @param string|string[] $pattern
* @template TKey of array-key
*
* @param non-empty-string|non-empty-string[] $pattern
* @param callable(string[]):string $callback
* @param string|array<string|int|float> $subject
* @param int $count
* @return ($subject is array ? array<string>|null : string|null)
* @param string|array<TKey, string|int|float> $subject
* @param int<0, max> $count
* @return ($subject is array ? array<TKey, string>|null : string|null)
*
* @psalm-ignore-nullable-return
*
* @psalm-taint-specialize
* @psalm-flow ($subject) -> return
@ -1208,7 +1222,7 @@ function preg_replace_callback($pattern, $callback, $subject, int $limit = -1, &
* @psalm-pure
* @template TFlags as int
*
* @param string $pattern
* @param non-empty-string $pattern
* @param string $subject
* @param mixed $matches
* @param TFlags $flags
@ -1244,7 +1258,7 @@ function preg_match_all($pattern, $subject, &$matches = [], int $flags = 1, int
* @psalm-pure
* @template TFlags as int-mask<0, 256, 512>
*
* @param string $pattern
* @param non-empty-string $pattern
* @param string $subject
* @param mixed $matches
* @param TFlags $flags
@ -1371,11 +1385,11 @@ function str_getcsv(string $string, string $separator = ',', string $enclosure =
/**
* @template TKey as array-key
* @template TArray as array<mixed, TKey>
* @template TArray as array<TKey>
*
* @param TArray $array
*
* @return (TArray is non-empty-array ? non-empty-array<TKey, positive-int> : array<TKey, positive-int>)
* @return (TArray is non-empty-array ? non-empty-array<TKey, int<1, max>> : array<TKey, int<1, max>>)
*
* @psalm-pure
*/

View File

@ -473,7 +473,7 @@ class FunctionCallTest extends TestCase
*/
function route($callback) {
if (!is_callable($callback)) { }
$a = preg_match("", "", $b);
$a = preg_match("//", "", $b);
if ($b[0]) {}
}',
'assertions' => [],
@ -501,6 +501,16 @@ class FunctionCallTest extends TestCase
return $s;
}',
],
'pregReplaceArrayValueType' => [
'code' => '<?php
/**
* @param string[] $s
* @return string[]
*/
function foo($s): array {
return preg_replace("/hello/", "", $s);
}',
],
'extractVarCheck' => [
'code' => '<?php
function takesString(string $str): void {}
@ -1679,7 +1689,7 @@ class FunctionCallTest extends TestCase
*/
function(array $ids): array {
return \preg_replace_callback(
"",
"//",
fn (array $matches) => $matches[4],
$ids
);
@ -1814,6 +1824,7 @@ class FunctionCallTest extends TestCase
'writeArgsAllowed' => [
'code' => '<?php
/**
* @param non-empty-string $pattern
* @param 0|256|512|768 $flags
* @return false|int
*/

View File

@ -54,7 +54,7 @@ class InternalCallMapHandlerTest extends TestCase
* large ignore list for extension functions have invalid reflection
* or are not maintained.
*
* @var list<string>
* @var list<non-empty-string>
*/
private static array $skippedPatterns = [
'/\'\d$/', // skip alternate signatures