mirror of
https://github.com/danog/psalm.git
synced 2024-12-11 08:49:52 +01:00
Merge pull request #10814 from kkmuffme/int-string-keys-arrays-are-int
This commit is contained in:
commit
85ff673099
@ -44,10 +44,14 @@ use Psalm\Type\Union;
|
|||||||
use function array_merge;
|
use function array_merge;
|
||||||
use function array_values;
|
use function array_values;
|
||||||
use function count;
|
use function count;
|
||||||
|
use function filter_var;
|
||||||
use function in_array;
|
use function in_array;
|
||||||
|
use function is_int;
|
||||||
|
use function is_numeric;
|
||||||
use function is_string;
|
use function is_string;
|
||||||
use function preg_match;
|
use function trim;
|
||||||
|
|
||||||
|
use const FILTER_VALIDATE_INT;
|
||||||
use const PHP_INT_MAX;
|
use const PHP_INT_MAX;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -237,6 +241,38 @@ final class ArrayAnalyzer
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string|int $literal_array_key
|
||||||
|
* @return false|int
|
||||||
|
* @psalm-assert-if-false !numeric $literal_array_key
|
||||||
|
*/
|
||||||
|
public static function getLiteralArrayKeyInt(
|
||||||
|
$literal_array_key
|
||||||
|
) {
|
||||||
|
if (is_int($literal_array_key)) {
|
||||||
|
return $literal_array_key;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!is_numeric($literal_array_key)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// PHP 8 values with whitespace after number are counted as numeric
|
||||||
|
// and filter_var treats them as such too
|
||||||
|
// ensures that '15 ' will stay '15 '
|
||||||
|
if (trim($literal_array_key) !== $literal_array_key) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// '+5' will pass the filter_var check but won't be changed in keys
|
||||||
|
if ($literal_array_key[0] === '+') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// e.g. 015 is numeric but won't be typecast as it's not a valid int
|
||||||
|
return filter_var($literal_array_key, FILTER_VALIDATE_INT);
|
||||||
|
}
|
||||||
|
|
||||||
private static function analyzeArrayItem(
|
private static function analyzeArrayItem(
|
||||||
StatementsAnalyzer $statements_analyzer,
|
StatementsAnalyzer $statements_analyzer,
|
||||||
Context $context,
|
Context $context,
|
||||||
@ -314,20 +350,17 @@ final class ArrayAnalyzer
|
|||||||
}
|
}
|
||||||
|
|
||||||
if ($item->key instanceof PhpParser\Node\Scalar\String_
|
if ($item->key instanceof PhpParser\Node\Scalar\String_
|
||||||
&& preg_match('/^(0|[1-9][0-9]*)$/', $item->key->value)
|
&& self::getLiteralArrayKeyInt($item->key->value) !== false
|
||||||
&& (
|
|
||||||
(int) $item->key->value < PHP_INT_MAX ||
|
|
||||||
$item->key->value === (string) PHP_INT_MAX
|
|
||||||
)
|
|
||||||
) {
|
) {
|
||||||
$key_type = Type::getInt(false, (int) $item->key->value);
|
$key_type = Type::getInt(false, (int) $item->key->value);
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($key_type->isSingleStringLiteral()) {
|
if ($key_type->isSingleStringLiteral()) {
|
||||||
$item_key_literal_type = $key_type->getSingleStringLiteral();
|
$item_key_literal_type = $key_type->getSingleStringLiteral();
|
||||||
$item_key_value = $item_key_literal_type->value;
|
$string_to_int = self::getLiteralArrayKeyInt($item_key_literal_type->value);
|
||||||
|
$item_key_value = $string_to_int === false ? $item_key_literal_type->value : $string_to_int;
|
||||||
|
|
||||||
if ($item_key_literal_type instanceof TLiteralClassString) {
|
if (is_string($item_key_value) && $item_key_literal_type instanceof TLiteralClassString) {
|
||||||
$array_creation_info->class_strings[$item_key_value] = true;
|
$array_creation_info->class_strings[$item_key_value] = true;
|
||||||
}
|
}
|
||||||
} elseif ($key_type->isSingleIntLiteral()) {
|
} elseif ($key_type->isSingleIntLiteral()) {
|
||||||
@ -384,7 +417,6 @@ final class ArrayAnalyzer
|
|||||||
$array_creation_info->array_keys[$item_key_value] = true;
|
$array_creation_info->array_keys[$item_key_value] = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
if (($data_flow_graph = $statements_analyzer->data_flow_graph)
|
if (($data_flow_graph = $statements_analyzer->data_flow_graph)
|
||||||
&& ($data_flow_graph instanceof VariableUseGraph
|
&& ($data_flow_graph instanceof VariableUseGraph
|
||||||
|| !in_array('TaintedInput', $statements_analyzer->getSuppressedIssues()))
|
|| !in_array('TaintedInput', $statements_analyzer->getSuppressedIssues()))
|
||||||
|
@ -3742,7 +3742,7 @@ final class AssertionFinder
|
|||||||
if (isset($expr->getArgs()[0])
|
if (isset($expr->getArgs()[0])
|
||||||
&& isset($expr->getArgs()[1])
|
&& isset($expr->getArgs()[1])
|
||||||
&& $first_var_type
|
&& $first_var_type
|
||||||
&& $first_var_name
|
&& $first_var_name !== null
|
||||||
&& !$expr->getArgs()[0]->value instanceof PhpParser\Node\Expr\ClassConstFetch
|
&& !$expr->getArgs()[0]->value instanceof PhpParser\Node\Expr\ClassConstFetch
|
||||||
&& $source instanceof StatementsAnalyzer
|
&& $source instanceof StatementsAnalyzer
|
||||||
&& ($second_var_type = $source->node_data->getType($expr->getArgs()[1]->value))
|
&& ($second_var_type = $source->node_data->getType($expr->getArgs()[1]->value))
|
||||||
@ -3765,7 +3765,12 @@ final class AssertionFinder
|
|||||||
|
|
||||||
if ($key_type->allStringLiterals() && !$key_type->possibly_undefined) {
|
if ($key_type->allStringLiterals() && !$key_type->possibly_undefined) {
|
||||||
foreach ($key_type->getLiteralStrings() as $array_literal_type) {
|
foreach ($key_type->getLiteralStrings() as $array_literal_type) {
|
||||||
|
$string_to_int = ArrayAnalyzer::getLiteralArrayKeyInt($array_literal_type->value);
|
||||||
|
if ($string_to_int === false) {
|
||||||
$literal_assertions[] = new IsIdentical($array_literal_type);
|
$literal_assertions[] = new IsIdentical($array_literal_type);
|
||||||
|
} else {
|
||||||
|
$literal_assertions[] = new IsLooselyEqual(new TLiteralInt($string_to_int));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} elseif ($key_type->allIntLiterals() && !$key_type->possibly_undefined) {
|
} elseif ($key_type->allIntLiterals() && !$key_type->possibly_undefined) {
|
||||||
foreach ($key_type->getLiteralInts() as $array_literal_type) {
|
foreach ($key_type->getLiteralInts() as $array_literal_type) {
|
||||||
@ -3778,7 +3783,7 @@ final class AssertionFinder
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($literal_assertions && $first_var_name && $safe_to_track_literals) {
|
if ($literal_assertions && $first_var_name !== null && $safe_to_track_literals) {
|
||||||
$if_types[$first_var_name] = [$literal_assertions];
|
$if_types[$first_var_name] = [$literal_assertions];
|
||||||
} else {
|
} else {
|
||||||
$array_root = isset($expr->getArgs()[1]->value)
|
$array_root = isset($expr->getArgs()[1]->value)
|
||||||
@ -3794,7 +3799,10 @@ final class AssertionFinder
|
|||||||
$first_arg = $expr->getArgs()[0];
|
$first_arg = $expr->getArgs()[0];
|
||||||
|
|
||||||
if ($first_arg->value instanceof PhpParser\Node\Scalar\String_) {
|
if ($first_arg->value instanceof PhpParser\Node\Scalar\String_) {
|
||||||
$first_var_name = '\'' . $first_arg->value->value . '\'';
|
$string_to_int = ArrayAnalyzer::getLiteralArrayKeyInt($first_arg->value->value);
|
||||||
|
$first_var_name = $string_to_int === false
|
||||||
|
? '\'' . $first_arg->value->value . '\''
|
||||||
|
: (string) $string_to_int;
|
||||||
} elseif ($first_arg->value instanceof PhpParser\Node\Scalar\LNumber) {
|
} elseif ($first_arg->value instanceof PhpParser\Node\Scalar\LNumber) {
|
||||||
$first_var_name = (string)$first_arg->value->value;
|
$first_var_name = (string)$first_arg->value->value;
|
||||||
}
|
}
|
||||||
@ -3812,7 +3820,12 @@ final class AssertionFinder
|
|||||||
|
|
||||||
if ($const_type) {
|
if ($const_type) {
|
||||||
if ($const_type->isSingleStringLiteral()) {
|
if ($const_type->isSingleStringLiteral()) {
|
||||||
$first_var_name = '\''.$const_type->getSingleStringLiteral()->value.'\'';
|
$string_to_int = ArrayAnalyzer::getLiteralArrayKeyInt(
|
||||||
|
$const_type->getSingleStringLiteral()->value,
|
||||||
|
);
|
||||||
|
$first_var_name = $string_to_int === false
|
||||||
|
? '\'' . $const_type->getSingleStringLiteral()->value . '\''
|
||||||
|
: (string) $string_to_int;
|
||||||
} elseif ($const_type->isSingleIntLiteral()) {
|
} elseif ($const_type->isSingleIntLiteral()) {
|
||||||
$first_var_name = (string)$const_type->getSingleIntLiteral()->value;
|
$first_var_name = (string)$const_type->getSingleIntLiteral()->value;
|
||||||
} else {
|
} else {
|
||||||
@ -3829,7 +3842,11 @@ final class AssertionFinder
|
|||||||
&& ($first_var_type = $source->node_data->getType($expr->getArgs()[0]->value))
|
&& ($first_var_type = $source->node_data->getType($expr->getArgs()[0]->value))
|
||||||
) {
|
) {
|
||||||
foreach ($first_var_type->getLiteralStrings() as $array_literal_type) {
|
foreach ($first_var_type->getLiteralStrings() as $array_literal_type) {
|
||||||
$if_types[$array_root . "['" . $array_literal_type->value . "']"] = [[new ArrayKeyExists()]];
|
$string_to_int = ArrayAnalyzer::getLiteralArrayKeyInt($array_literal_type->value);
|
||||||
|
$literal_key = $string_to_int === false
|
||||||
|
? "'" . $array_literal_type->value . "'"
|
||||||
|
: $string_to_int;
|
||||||
|
$if_types[$array_root . "[" . $literal_key . "]"] = [[new ArrayKeyExists()]];
|
||||||
}
|
}
|
||||||
foreach ($first_var_type->getLiteralInts() as $array_literal_type) {
|
foreach ($first_var_type->getLiteralInts() as $array_literal_type) {
|
||||||
$if_types[$array_root . "[" . $array_literal_type->value . "]"] = [[new ArrayKeyExists()]];
|
$if_types[$array_root . "[" . $array_literal_type->value . "]"] = [[new ArrayKeyExists()]];
|
||||||
|
@ -9,6 +9,7 @@ use Psalm\CodeLocation;
|
|||||||
use Psalm\Codebase;
|
use Psalm\Codebase;
|
||||||
use Psalm\Context;
|
use Psalm\Context;
|
||||||
use Psalm\Internal\Analyzer\ClassLikeAnalyzer;
|
use Psalm\Internal\Analyzer\ClassLikeAnalyzer;
|
||||||
|
use Psalm\Internal\Analyzer\Statements\Expression\ArrayAnalyzer;
|
||||||
use Psalm\Internal\Analyzer\Statements\Expression\ExpressionIdentifier;
|
use Psalm\Internal\Analyzer\Statements\Expression\ExpressionIdentifier;
|
||||||
use Psalm\Internal\Analyzer\Statements\Expression\Fetch\ArrayFetchAnalyzer;
|
use Psalm\Internal\Analyzer\Statements\Expression\Fetch\ArrayFetchAnalyzer;
|
||||||
use Psalm\Internal\Analyzer\Statements\ExpressionAnalyzer;
|
use Psalm\Internal\Analyzer\Statements\ExpressionAnalyzer;
|
||||||
@ -48,7 +49,6 @@ use function end;
|
|||||||
use function implode;
|
use function implode;
|
||||||
use function in_array;
|
use function in_array;
|
||||||
use function is_string;
|
use function is_string;
|
||||||
use function preg_match;
|
|
||||||
use function strlen;
|
use function strlen;
|
||||||
use function strpos;
|
use function strpos;
|
||||||
|
|
||||||
@ -1084,8 +1084,9 @@ final class ArrayAssignmentAnalyzer
|
|||||||
$offset_type = $child_stmt_dim_type->getSingleStringLiteral();
|
$offset_type = $child_stmt_dim_type->getSingleStringLiteral();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (preg_match('/^(0|[1-9][0-9]*)$/', $offset_type->value)) {
|
$string_to_int = ArrayAnalyzer::getLiteralArrayKeyInt($offset_type->value);
|
||||||
$var_id_addition = '[' . $offset_type->value . ']';
|
if ($string_to_int !== false) {
|
||||||
|
$var_id_addition = '[' . $string_to_int . ']';
|
||||||
} else {
|
} else {
|
||||||
$var_id_addition = '[\'' . $offset_type->value . '\']';
|
$var_id_addition = '[\'' . $offset_type->value . '\']';
|
||||||
}
|
}
|
||||||
|
@ -1217,6 +1217,13 @@ final class AssignmentAnalyzer
|
|||||||
$offset_value = $assign_var_item->key->value;
|
$offset_value = $assign_var_item->key->value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ($offset_value !== null) {
|
||||||
|
$string_to_int = ArrayAnalyzer::getLiteralArrayKeyInt($offset_value);
|
||||||
|
if ($string_to_int !== false) {
|
||||||
|
$offset_value = $string_to_int;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
$list_var_id = ExpressionIdentifier::getExtendedVarId(
|
$list_var_id = ExpressionIdentifier::getExtendedVarId(
|
||||||
$var,
|
$var,
|
||||||
$statements_analyzer->getFQCLN(),
|
$statements_analyzer->getFQCLN(),
|
||||||
|
@ -116,9 +116,10 @@ final class ExpressionIdentifier
|
|||||||
if ($stmt->dim instanceof PhpParser\Node\Scalar\String_
|
if ($stmt->dim instanceof PhpParser\Node\Scalar\String_
|
||||||
|| $stmt->dim instanceof PhpParser\Node\Scalar\LNumber
|
|| $stmt->dim instanceof PhpParser\Node\Scalar\LNumber
|
||||||
) {
|
) {
|
||||||
$offset = $stmt->dim instanceof PhpParser\Node\Scalar\String_
|
$string_to_int = ArrayAnalyzer::getLiteralArrayKeyInt($stmt->dim->value);
|
||||||
|
$offset = $string_to_int === false
|
||||||
? '\'' . $stmt->dim->value . '\''
|
? '\'' . $stmt->dim->value . '\''
|
||||||
: $stmt->dim->value;
|
: (int) $stmt->dim->value;
|
||||||
} elseif ($stmt->dim instanceof PhpParser\Node\Expr\Variable
|
} elseif ($stmt->dim instanceof PhpParser\Node\Expr\Variable
|
||||||
&& is_string($stmt->dim->name)
|
&& is_string($stmt->dim->name)
|
||||||
) {
|
) {
|
||||||
@ -146,7 +147,13 @@ final class ExpressionIdentifier
|
|||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
if ($stmt_dim_type->isSingleStringLiteral()) {
|
if ($stmt_dim_type->isSingleStringLiteral()) {
|
||||||
$offset = '\'' . $stmt_dim_type->getSingleStringLiteral()->value . '\'';
|
$string_to_int = ArrayAnalyzer::getLiteralArrayKeyInt(
|
||||||
|
$stmt_dim_type->getSingleStringLiteral()->value,
|
||||||
|
);
|
||||||
|
|
||||||
|
$offset = $string_to_int === false
|
||||||
|
? '\'' . $stmt_dim_type->getSingleStringLiteral()->value . '\''
|
||||||
|
: (int) $stmt_dim_type->getSingleStringLiteral()->value;
|
||||||
} elseif ($stmt_dim_type->isSingleIntLiteral()) {
|
} elseif ($stmt_dim_type->isSingleIntLiteral()) {
|
||||||
$offset = $stmt_dim_type->getSingleIntLiteral()->value;
|
$offset = $stmt_dim_type->getSingleIntLiteral()->value;
|
||||||
}
|
}
|
||||||
|
@ -7,6 +7,7 @@ use Psalm\CodeLocation;
|
|||||||
use Psalm\Codebase;
|
use Psalm\Codebase;
|
||||||
use Psalm\Context;
|
use Psalm\Context;
|
||||||
use Psalm\Internal\Analyzer\FunctionLikeAnalyzer;
|
use Psalm\Internal\Analyzer\FunctionLikeAnalyzer;
|
||||||
|
use Psalm\Internal\Analyzer\Statements\Expression\ArrayAnalyzer;
|
||||||
use Psalm\Internal\Analyzer\Statements\Expression\Call\MethodCallAnalyzer;
|
use Psalm\Internal\Analyzer\Statements\Expression\Call\MethodCallAnalyzer;
|
||||||
use Psalm\Internal\Analyzer\Statements\Expression\ExpressionIdentifier;
|
use Psalm\Internal\Analyzer\Statements\Expression\ExpressionIdentifier;
|
||||||
use Psalm\Internal\Analyzer\Statements\ExpressionAnalyzer;
|
use Psalm\Internal\Analyzer\Statements\ExpressionAnalyzer;
|
||||||
@ -92,8 +93,6 @@ use function count;
|
|||||||
use function implode;
|
use function implode;
|
||||||
use function in_array;
|
use function in_array;
|
||||||
use function is_int;
|
use function is_int;
|
||||||
use function is_numeric;
|
|
||||||
use function preg_match;
|
|
||||||
use function strlen;
|
use function strlen;
|
||||||
use function strtolower;
|
use function strtolower;
|
||||||
|
|
||||||
@ -968,15 +967,24 @@ final class ArrayFetchAnalyzer
|
|||||||
$found_match = false;
|
$found_match = false;
|
||||||
|
|
||||||
foreach ($offset_type->getAtomicTypes() as $offset_type_part) {
|
foreach ($offset_type->getAtomicTypes() as $offset_type_part) {
|
||||||
if ($extended_var_id
|
if ($extended_var_id === null
|
||||||
&& $offset_type_part instanceof TLiteralString
|
|| !($offset_type_part instanceof TLiteralString)) {
|
||||||
&& isset(
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$string_to_int = ArrayAnalyzer::getLiteralArrayKeyInt(
|
||||||
|
$offset_type_part->value,
|
||||||
|
);
|
||||||
|
|
||||||
|
$literal_access = $string_to_int === false
|
||||||
|
? '\'' . $offset_type_part->value . '\''
|
||||||
|
: $string_to_int;
|
||||||
|
if (isset(
|
||||||
$context->vars_in_scope[
|
$context->vars_in_scope[
|
||||||
$extended_var_id . '[\'' . $offset_type_part->value . '\']'
|
$extended_var_id . '[' . $literal_access . ']'
|
||||||
],
|
],
|
||||||
)
|
) && !$context->vars_in_scope[
|
||||||
&& !$context->vars_in_scope[
|
$extended_var_id . '[' . $literal_access . ']'
|
||||||
$extended_var_id . '[\'' . $offset_type_part->value . '\']'
|
|
||||||
]->possibly_undefined
|
]->possibly_undefined
|
||||||
) {
|
) {
|
||||||
$found_match = true;
|
$found_match = true;
|
||||||
@ -1007,8 +1015,9 @@ final class ArrayFetchAnalyzer
|
|||||||
|
|
||||||
foreach ($offset_types as $key => $offset_type_part) {
|
foreach ($offset_types as $key => $offset_type_part) {
|
||||||
if ($offset_type_part instanceof TLiteralString) {
|
if ($offset_type_part instanceof TLiteralString) {
|
||||||
if (preg_match('/^(0|[1-9][0-9]*)$/', $offset_type_part->value)) {
|
$string_to_int = ArrayAnalyzer::getLiteralArrayKeyInt($offset_type_part->value);
|
||||||
$offset_type->addType(new TLiteralInt((int) $offset_type_part->value));
|
if ($string_to_int !== false) {
|
||||||
|
$offset_type->addType(new TLiteralInt($string_to_int));
|
||||||
$offset_type->removeType($key);
|
$offset_type->removeType($key);
|
||||||
}
|
}
|
||||||
} elseif ($offset_type_part instanceof TBool) {
|
} elseif ($offset_type_part instanceof TBool) {
|
||||||
@ -1546,7 +1555,10 @@ final class ArrayFetchAnalyzer
|
|||||||
if ($key_values) {
|
if ($key_values) {
|
||||||
$properties = $type->properties;
|
$properties = $type->properties;
|
||||||
foreach ($key_values as $key_value) {
|
foreach ($key_values as $key_value) {
|
||||||
if ($type->is_list && (!is_numeric($key_value->value) || $key_value->value < 0)) {
|
$string_to_int = ArrayAnalyzer::getLiteralArrayKeyInt($key_value->value);
|
||||||
|
$key_value = $string_to_int === false ? $key_value : new TLiteralInt($string_to_int);
|
||||||
|
|
||||||
|
if ($type->is_list && (!is_int($key_value->value) || $key_value->value < 0)) {
|
||||||
$expected_offset_types[] = $type->getGenericKeyType();
|
$expected_offset_types[] = $type->getGenericKeyType();
|
||||||
$has_valid_offset = false;
|
$has_valid_offset = false;
|
||||||
} elseif ((isset($properties[$key_value->value]) && !(
|
} elseif ((isset($properties[$key_value->value]) && !(
|
||||||
|
@ -36,7 +36,6 @@ use function array_merge;
|
|||||||
use function array_values;
|
use function array_values;
|
||||||
use function count;
|
use function count;
|
||||||
use function is_string;
|
use function is_string;
|
||||||
use function preg_match;
|
|
||||||
use function strtolower;
|
use function strtolower;
|
||||||
|
|
||||||
use const PHP_INT_MAX;
|
use const PHP_INT_MAX;
|
||||||
@ -671,11 +670,7 @@ final class SimpleTypeInferer
|
|||||||
$key_type = Type::getString('');
|
$key_type = Type::getString('');
|
||||||
}
|
}
|
||||||
if ($item->key instanceof PhpParser\Node\Scalar\String_
|
if ($item->key instanceof PhpParser\Node\Scalar\String_
|
||||||
&& preg_match('/^(0|[1-9][0-9]*)$/', $item->key->value)
|
&& ArrayAnalyzer::getLiteralArrayKeyInt($item->key->value) !== false
|
||||||
&& (
|
|
||||||
(int) $item->key->value < PHP_INT_MAX ||
|
|
||||||
$item->key->value === (string) PHP_INT_MAX
|
|
||||||
)
|
|
||||||
) {
|
) {
|
||||||
$key_type = Type::getInt(false, (int) $item->key->value);
|
$key_type = Type::getInt(false, (int) $item->key->value);
|
||||||
}
|
}
|
||||||
@ -687,9 +682,10 @@ final class SimpleTypeInferer
|
|||||||
|
|
||||||
if ($key_type->isSingleStringLiteral()) {
|
if ($key_type->isSingleStringLiteral()) {
|
||||||
$item_key_literal_type = $key_type->getSingleStringLiteral();
|
$item_key_literal_type = $key_type->getSingleStringLiteral();
|
||||||
$item_key_value = $item_key_literal_type->value;
|
$string_to_int = ArrayAnalyzer::getLiteralArrayKeyInt($item_key_literal_type->value);
|
||||||
|
$item_key_value = $string_to_int === false ? $item_key_literal_type->value : $string_to_int;
|
||||||
|
|
||||||
if ($item_key_literal_type instanceof TLiteralClassString) {
|
if (is_string($item_key_value) && $item_key_literal_type instanceof TLiteralClassString) {
|
||||||
$array_creation_info->class_strings[$item_key_value] = true;
|
$array_creation_info->class_strings[$item_key_value] = true;
|
||||||
}
|
}
|
||||||
} elseif ($key_type->isSingleIntLiteral()) {
|
} elseif ($key_type->isSingleIntLiteral()) {
|
||||||
|
@ -7,6 +7,7 @@ use LogicException;
|
|||||||
use Psalm\Codebase;
|
use Psalm\Codebase;
|
||||||
use Psalm\Exception\TypeParseTreeException;
|
use Psalm\Exception\TypeParseTreeException;
|
||||||
use Psalm\Internal\Analyzer\ProjectAnalyzer;
|
use Psalm\Internal\Analyzer\ProjectAnalyzer;
|
||||||
|
use Psalm\Internal\Analyzer\Statements\Expression\ArrayAnalyzer;
|
||||||
use Psalm\Internal\Type\ParseTree\CallableParamTree;
|
use Psalm\Internal\Type\ParseTree\CallableParamTree;
|
||||||
use Psalm\Internal\Type\ParseTree\CallableTree;
|
use Psalm\Internal\Type\ParseTree\CallableTree;
|
||||||
use Psalm\Internal\Type\ParseTree\CallableWithReturnTypeTree;
|
use Psalm\Internal\Type\ParseTree\CallableWithReturnTypeTree;
|
||||||
@ -86,7 +87,6 @@ use function count;
|
|||||||
use function defined;
|
use function defined;
|
||||||
use function end;
|
use function end;
|
||||||
use function explode;
|
use function explode;
|
||||||
use function filter_var;
|
|
||||||
use function get_class;
|
use function get_class;
|
||||||
use function in_array;
|
use function in_array;
|
||||||
use function is_int;
|
use function is_int;
|
||||||
@ -100,9 +100,6 @@ use function strpos;
|
|||||||
use function strtolower;
|
use function strtolower;
|
||||||
use function strtr;
|
use function strtr;
|
||||||
use function substr;
|
use function substr;
|
||||||
use function trim;
|
|
||||||
|
|
||||||
use const FILTER_VALIDATE_INT;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @psalm-suppress InaccessibleProperty Allowed during construction
|
* @psalm-suppress InaccessibleProperty Allowed during construction
|
||||||
@ -669,11 +666,8 @@ final class TypeParser
|
|||||||
}
|
}
|
||||||
|
|
||||||
foreach ($generic_params[0]->getAtomicTypes() as $key => $atomic_type) {
|
foreach ($generic_params[0]->getAtomicTypes() as $key => $atomic_type) {
|
||||||
// PHP 8 values with whitespace after number are counted as numeric
|
|
||||||
// and filter_var treats them as such too
|
|
||||||
if ($atomic_type instanceof TLiteralString
|
if ($atomic_type instanceof TLiteralString
|
||||||
&& ($string_to_int = filter_var($atomic_type->value, FILTER_VALIDATE_INT)) !== false
|
&& ($string_to_int = ArrayAnalyzer::getLiteralArrayKeyInt($atomic_type->value)) !== false
|
||||||
&& trim($atomic_type->value) === $atomic_type->value
|
|
||||||
) {
|
) {
|
||||||
$builder = $generic_params[0]->getBuilder();
|
$builder = $generic_params[0]->getBuilder();
|
||||||
$builder->removeType($key);
|
$builder->removeType($key);
|
||||||
@ -1481,7 +1475,7 @@ final class TypeParser
|
|||||||
$property_key = $property_branch->value;
|
$property_key = $property_branch->value;
|
||||||
}
|
}
|
||||||
if ($is_list && (
|
if ($is_list && (
|
||||||
!is_numeric($property_key)
|
ArrayAnalyzer::getLiteralArrayKeyInt($property_key) === false
|
||||||
|| ($had_optional && !$property_maybe_undefined)
|
|| ($had_optional && !$property_maybe_undefined)
|
||||||
|| $type === 'array'
|
|| $type === 'array'
|
||||||
|| $type === 'callable-array'
|
|| $type === 'callable-array'
|
||||||
|
@ -720,6 +720,19 @@ class TKeyedArray extends Atomic
|
|||||||
$quote = true;
|
$quote = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (preg_match('/^[1-9][0-9]*_([0-9]+_)*[0-9]+$/', $name)) {
|
||||||
|
$quote = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 08 should be quoted since it's numeric but it's handled as string and not cast to int
|
||||||
|
if (preg_match('/^0[0-9]+$/', $name)) {
|
||||||
|
$quote = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (preg_match('/^[0-9]+e-?[0-9]+$/', $name)) {
|
||||||
|
$quote = true;
|
||||||
|
}
|
||||||
|
|
||||||
if ($quote) {
|
if ($quote) {
|
||||||
$name = '\'' . str_replace("\n", '\n', addslashes($name)) . '\'';
|
$name = '\'' . str_replace("\n", '\n', addslashes($name)) . '\'';
|
||||||
}
|
}
|
||||||
|
@ -5,6 +5,7 @@ namespace Psalm\Type;
|
|||||||
use InvalidArgumentException;
|
use InvalidArgumentException;
|
||||||
use Psalm\CodeLocation;
|
use Psalm\CodeLocation;
|
||||||
use Psalm\Codebase;
|
use Psalm\Codebase;
|
||||||
|
use Psalm\Internal\Analyzer\Statements\Expression\ArrayAnalyzer;
|
||||||
use Psalm\Internal\Analyzer\StatementsAnalyzer;
|
use Psalm\Internal\Analyzer\StatementsAnalyzer;
|
||||||
use Psalm\Internal\Codebase\TaintFlowGraph;
|
use Psalm\Internal\Codebase\TaintFlowGraph;
|
||||||
use Psalm\Internal\Codebase\VariableUseGraph;
|
use Psalm\Internal\Codebase\VariableUseGraph;
|
||||||
@ -67,11 +68,11 @@ use function count;
|
|||||||
use function explode;
|
use function explode;
|
||||||
use function implode;
|
use function implode;
|
||||||
use function is_numeric;
|
use function is_numeric;
|
||||||
|
use function is_string;
|
||||||
use function key;
|
use function key;
|
||||||
use function ksort;
|
use function ksort;
|
||||||
use function preg_match;
|
use function preg_match;
|
||||||
use function preg_quote;
|
use function preg_quote;
|
||||||
use function str_replace;
|
|
||||||
use function str_split;
|
use function str_split;
|
||||||
use function strlen;
|
use function strlen;
|
||||||
use function strpos;
|
use function strpos;
|
||||||
@ -470,9 +471,15 @@ class Reconciler
|
|||||||
$array_key = array_shift($key_parts);
|
$array_key = array_shift($key_parts);
|
||||||
array_shift($key_parts);
|
array_shift($key_parts);
|
||||||
|
|
||||||
|
if ($array_key[0] === '\'' || $array_key[0] === '"') {
|
||||||
|
$possibly_property_key = substr($array_key, 1, -1);
|
||||||
|
$string_to_int = ArrayAnalyzer::getLiteralArrayKeyInt($possibly_property_key);
|
||||||
|
$array_key = $string_to_int === false ? $array_key : $string_to_int;
|
||||||
|
}
|
||||||
|
|
||||||
$new_base_key = $base_key . '[' . $array_key . ']';
|
$new_base_key = $base_key . '[' . $array_key . ']';
|
||||||
|
|
||||||
if (strpos($array_key, '\'') !== false) {
|
if (is_string($array_key) && strpos($array_key, '\'') !== false) {
|
||||||
$new_types[$base_key][] = [new HasStringArrayAccess()];
|
$new_types[$base_key][] = [new HasStringArrayAccess()];
|
||||||
} else {
|
} else {
|
||||||
$new_types[$base_key][] = [new HasIntOrStringArrayAccess()];
|
$new_types[$base_key][] = [new HasIntOrStringArrayAccess()];
|
||||||
@ -781,7 +788,8 @@ class Reconciler
|
|||||||
return null;
|
return null;
|
||||||
} elseif (!$existing_key_type_part instanceof TKeyedArray) {
|
} elseif (!$existing_key_type_part instanceof TKeyedArray) {
|
||||||
return Type::getMixed();
|
return Type::getMixed();
|
||||||
} elseif ($array_key[0] === '$' || ($array_key[0] !== '\'' && !is_numeric($array_key[0]))) {
|
} elseif ($array_key[0] === '$'
|
||||||
|
|| ($array_key[0] !== '\'' && ArrayAnalyzer::getLiteralArrayKeyInt($array_key) === false)) {
|
||||||
if ($has_empty) {
|
if ($has_empty) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@ -790,7 +798,10 @@ class Reconciler
|
|||||||
} else {
|
} else {
|
||||||
$array_properties = $existing_key_type_part->properties;
|
$array_properties = $existing_key_type_part->properties;
|
||||||
|
|
||||||
$key_parts_key = str_replace('\'', '', $array_key);
|
$key_parts_key = $array_key;
|
||||||
|
if ($array_key[0] === '\'' || $array_key[0] === '"') {
|
||||||
|
$key_parts_key = substr($array_key, 1, -1);
|
||||||
|
}
|
||||||
|
|
||||||
if (!isset($array_properties[$key_parts_key])) {
|
if (!isset($array_properties[$key_parts_key])) {
|
||||||
if ($existing_key_type_part->fallback_params !== null) {
|
if ($existing_key_type_part->fallback_params !== null) {
|
||||||
@ -1182,13 +1193,14 @@ class Reconciler
|
|||||||
$properties = $base_atomic_type->properties;
|
$properties = $base_atomic_type->properties;
|
||||||
$properties[$array_key_offset] = $result_type;
|
$properties[$array_key_offset] = $result_type;
|
||||||
if ($base_atomic_type->is_list
|
if ($base_atomic_type->is_list
|
||||||
&& (!is_numeric($array_key_offset)
|
&& (ArrayAnalyzer::getLiteralArrayKeyInt($array_key_offset) === false
|
||||||
|| ($array_key_offset
|
|| ($array_key_offset
|
||||||
&& !isset($properties[$array_key_offset-1])
|
&& !isset($properties[$array_key_offset-1])
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
if ($base_atomic_type->fallback_params && is_numeric($array_key_offset)) {
|
if ($base_atomic_type->fallback_params
|
||||||
|
&& ArrayAnalyzer::getLiteralArrayKeyInt($array_key_offset) !== false) {
|
||||||
$fallback = $base_atomic_type->fallback_params[1]->setPossiblyUndefined(
|
$fallback = $base_atomic_type->fallback_params[1]->setPossiblyUndefined(
|
||||||
$result_type->isNever(),
|
$result_type->isNever(),
|
||||||
);
|
);
|
||||||
|
@ -183,6 +183,30 @@ class ArrayKeysTest extends TestCase
|
|||||||
//$b->e["b"] = 123;
|
//$b->e["b"] = 123;
|
||||||
',
|
',
|
||||||
],
|
],
|
||||||
|
'intStringKeyAsInt' => [
|
||||||
|
'code' => '<?php
|
||||||
|
$a = ["15" => "a"];
|
||||||
|
$b = ["15.7" => "a"];
|
||||||
|
// since PHP 8 this is_numeric but will not be int key
|
||||||
|
$c = ["15 " => "a"];
|
||||||
|
$d = ["-15" => "a"];
|
||||||
|
// see https://github.com/php/php-src/issues/9029#issuecomment-1186226676
|
||||||
|
$e = ["+15" => "a"];
|
||||||
|
$f = ["015" => "a"];
|
||||||
|
$g = ["1e2" => "a"];
|
||||||
|
$h = ["1_0" => "a"];
|
||||||
|
',
|
||||||
|
'assertions' => [
|
||||||
|
'$a===' => "array{15: 'a'}",
|
||||||
|
'$b===' => "array{'15.7': 'a'}",
|
||||||
|
'$c===' => "array{'15 ': 'a'}",
|
||||||
|
'$d===' => "array{-15: 'a'}",
|
||||||
|
'$e===' => "array{'+15': 'a'}",
|
||||||
|
'$f===' => "array{'015': 'a'}",
|
||||||
|
'$g===' => "array{'1e2': 'a'}",
|
||||||
|
'$h===' => "array{'1_0': 'a'}",
|
||||||
|
],
|
||||||
|
],
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1576,6 +1576,15 @@ class RedundantConditionTest extends TestCase
|
|||||||
}',
|
}',
|
||||||
'error_message' => 'DocblockTypeContradiction',
|
'error_message' => 'DocblockTypeContradiction',
|
||||||
],
|
],
|
||||||
|
'array_key_exists_int_string_juggle' => [
|
||||||
|
'code' => '<?php
|
||||||
|
/**
|
||||||
|
* @var string[] $a
|
||||||
|
*/
|
||||||
|
|
||||||
|
if (array_key_exists("10", $a) && array_key_exists(10, $a)) {}',
|
||||||
|
'error_message' => 'RedundantCondition',
|
||||||
|
],
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user