Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve string-int juggle consistency in array keys and display for int-like strings in type #10814

Merged
merged 5 commits into from
Mar 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -44,10 +44,14 @@
use function array_merge;
use function array_values;
use function count;
use function filter_var;
use function in_array;
use function is_int;
use function is_numeric;
use function is_string;
use function preg_match;
use function trim;

use const FILTER_VALIDATE_INT;
use const PHP_INT_MAX;

/**
Expand Down Expand Up @@ -237,6 +241,38 @@ public static function analyze(
return true;
}

/**
* @param string|int $literal_array_key
* @return false|int
* @psalm-assert-if-false !numeric $literal_array_key
*/
public static function getLiteralArrayKeyInt(
weirdan marked this conversation as resolved.
Show resolved Hide resolved
$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(
StatementsAnalyzer $statements_analyzer,
Context $context,
Expand Down Expand Up @@ -314,20 +350,17 @@ private static function analyzeArrayItem(
}

if ($item->key instanceof PhpParser\Node\Scalar\String_
&& preg_match('/^(0|[1-9][0-9]*)$/', $item->key->value)
&& (
(int) $item->key->value < PHP_INT_MAX ||
$item->key->value === (string) PHP_INT_MAX
)
&& self::getLiteralArrayKeyInt($item->key->value) !== false
) {
$key_type = Type::getInt(false, (int) $item->key->value);
}

if ($key_type->isSingleStringLiteral()) {
$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;
}
} elseif ($key_type->isSingleIntLiteral()) {
Expand Down Expand Up @@ -384,7 +417,6 @@ private static function analyzeArrayItem(
$array_creation_info->array_keys[$item_key_value] = true;
}


if (($data_flow_graph = $statements_analyzer->data_flow_graph)
&& ($data_flow_graph instanceof VariableUseGraph
|| !in_array('TaintedInput', $statements_analyzer->getSuppressedIssues()))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3742,7 +3742,7 @@ private static function getArrayKeyExistsAssertions(
if (isset($expr->getArgs()[0])
&& isset($expr->getArgs()[1])
&& $first_var_type
&& $first_var_name
&& $first_var_name !== null
&& !$expr->getArgs()[0]->value instanceof PhpParser\Node\Expr\ClassConstFetch
&& $source instanceof StatementsAnalyzer
&& ($second_var_type = $source->node_data->getType($expr->getArgs()[1]->value))
Expand All @@ -3765,7 +3765,12 @@ private static function getArrayKeyExistsAssertions(

if ($key_type->allStringLiterals() && !$key_type->possibly_undefined) {
foreach ($key_type->getLiteralStrings() as $array_literal_type) {
$literal_assertions[] = new IsIdentical($array_literal_type);
$string_to_int = ArrayAnalyzer::getLiteralArrayKeyInt($array_literal_type->value);
if ($string_to_int === false) {
$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) {
foreach ($key_type->getLiteralInts() as $array_literal_type) {
Expand All @@ -3778,7 +3783,7 @@ private static function getArrayKeyExistsAssertions(
}
}

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];
} else {
$array_root = isset($expr->getArgs()[1]->value)
Expand All @@ -3794,7 +3799,10 @@ private static function getArrayKeyExistsAssertions(
$first_arg = $expr->getArgs()[0];

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) {
$first_var_name = (string)$first_arg->value->value;
}
Expand All @@ -3812,7 +3820,12 @@ private static function getArrayKeyExistsAssertions(

if ($const_type) {
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()) {
$first_var_name = (string)$const_type->getSingleIntLiteral()->value;
} else {
Expand All @@ -3829,7 +3842,11 @@ private static function getArrayKeyExistsAssertions(
&& ($first_var_type = $source->node_data->getType($expr->getArgs()[0]->value))
) {
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) {
$if_types[$array_root . "[" . $array_literal_type->value . "]"] = [[new ArrayKeyExists()]];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
use Psalm\Codebase;
use Psalm\Context;
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\Fetch\ArrayFetchAnalyzer;
use Psalm\Internal\Analyzer\Statements\ExpressionAnalyzer;
Expand Down Expand Up @@ -48,7 +49,6 @@
use function implode;
use function in_array;
use function is_string;
use function preg_match;
use function strlen;
use function strpos;

Expand Down Expand Up @@ -1084,8 +1084,9 @@ private static function getArrayAssignmentOffsetType(
$offset_type = $child_stmt_dim_type->getSingleStringLiteral();
}

if (preg_match('/^(0|[1-9][0-9]*)$/', $offset_type->value)) {
$var_id_addition = '[' . $offset_type->value . ']';
$string_to_int = ArrayAnalyzer::getLiteralArrayKeyInt($offset_type->value);
if ($string_to_int !== false) {
$var_id_addition = '[' . $string_to_int . ']';
} else {
$var_id_addition = '[\'' . $offset_type->value . '\']';
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1217,6 +1217,13 @@ private static function analyzeDestructuringAssignment(
$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(
$var,
$statements_analyzer->getFQCLN(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -116,9 +116,10 @@ public static function getExtendedVarId(
if ($stmt->dim instanceof PhpParser\Node\Scalar\String_
|| $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;
: (int) $stmt->dim->value;
} elseif ($stmt->dim instanceof PhpParser\Node\Expr\Variable
&& is_string($stmt->dim->name)
) {
Expand Down Expand Up @@ -146,7 +147,13 @@ public static function getExtendedVarId(
)
) {
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()) {
$offset = $stmt_dim_type->getSingleIntLiteral()->value;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
use Psalm\Codebase;
use Psalm\Context;
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\ExpressionIdentifier;
use Psalm\Internal\Analyzer\Statements\ExpressionAnalyzer;
Expand Down Expand Up @@ -92,8 +93,6 @@
use function implode;
use function in_array;
use function is_int;
use function is_numeric;
use function preg_match;
use function strlen;
use function strtolower;

Expand Down Expand Up @@ -968,16 +967,25 @@ private static function checkLiteralStringArrayOffset(
$found_match = false;

foreach ($offset_type->getAtomicTypes() as $offset_type_part) {
if ($extended_var_id
&& $offset_type_part instanceof TLiteralString
&& isset(
$context->vars_in_scope[
$extended_var_id . '[\'' . $offset_type_part->value . '\']'
],
)
&& !$context->vars_in_scope[
$extended_var_id . '[\'' . $offset_type_part->value . '\']'
]->possibly_undefined
if ($extended_var_id === null
|| !($offset_type_part instanceof TLiteralString)) {
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[
$extended_var_id . '[' . $literal_access . ']'
],
) && !$context->vars_in_scope[
$extended_var_id . '[' . $literal_access . ']'
]->possibly_undefined
) {
$found_match = true;
break;
Expand Down Expand Up @@ -1007,8 +1015,9 @@ public static function replaceOffsetTypeWithInts(Union $offset_type): Union

foreach ($offset_types as $key => $offset_type_part) {
if ($offset_type_part instanceof TLiteralString) {
if (preg_match('/^(0|[1-9][0-9]*)$/', $offset_type_part->value)) {
$offset_type->addType(new TLiteralInt((int) $offset_type_part->value));
$string_to_int = ArrayAnalyzer::getLiteralArrayKeyInt($offset_type_part->value);
if ($string_to_int !== false) {
$offset_type->addType(new TLiteralInt($string_to_int));
$offset_type->removeType($key);
}
} elseif ($offset_type_part instanceof TBool) {
Expand Down Expand Up @@ -1546,7 +1555,10 @@ private static function handleArrayAccessOnKeyedArray(
if ($key_values) {
$properties = $type->properties;
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();
$has_valid_offset = false;
} elseif ((isset($properties[$key_value->value]) && !(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,6 @@
use function array_values;
use function count;
use function is_string;
use function preg_match;
use function strtolower;

use const PHP_INT_MAX;
Expand Down Expand Up @@ -671,11 +670,7 @@ private static function handleArrayItem(
$key_type = Type::getString('');
}
if ($item->key instanceof PhpParser\Node\Scalar\String_
&& preg_match('/^(0|[1-9][0-9]*)$/', $item->key->value)
&& (
(int) $item->key->value < PHP_INT_MAX ||
$item->key->value === (string) PHP_INT_MAX
)
&& ArrayAnalyzer::getLiteralArrayKeyInt($item->key->value) !== false
) {
$key_type = Type::getInt(false, (int) $item->key->value);
}
Expand All @@ -687,9 +682,10 @@ private static function handleArrayItem(

if ($key_type->isSingleStringLiteral()) {
$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;
}
} elseif ($key_type->isSingleIntLiteral()) {
Expand Down
12 changes: 3 additions & 9 deletions src/Psalm/Internal/Type/TypeParser.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
use Psalm\Codebase;
use Psalm\Exception\TypeParseTreeException;
use Psalm\Internal\Analyzer\ProjectAnalyzer;
use Psalm\Internal\Analyzer\Statements\Expression\ArrayAnalyzer;
use Psalm\Internal\Type\ParseTree\CallableParamTree;
use Psalm\Internal\Type\ParseTree\CallableTree;
use Psalm\Internal\Type\ParseTree\CallableWithReturnTypeTree;
Expand Down Expand Up @@ -86,7 +87,6 @@
use function defined;
use function end;
use function explode;
use function filter_var;
use function get_class;
use function in_array;
use function is_int;
Expand All @@ -100,9 +100,6 @@
use function strtolower;
use function strtr;
use function substr;
use function trim;

use const FILTER_VALIDATE_INT;

/**
* @psalm-suppress InaccessibleProperty Allowed during construction
Expand Down Expand Up @@ -669,11 +666,8 @@ private static function getTypeFromGenericTree(
}

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
&& ($string_to_int = filter_var($atomic_type->value, FILTER_VALIDATE_INT)) !== false
&& trim($atomic_type->value) === $atomic_type->value
&& ($string_to_int = ArrayAnalyzer::getLiteralArrayKeyInt($atomic_type->value)) !== false
) {
$builder = $generic_params[0]->getBuilder();
$builder->removeType($key);
Expand Down Expand Up @@ -1481,7 +1475,7 @@ private static function getTypeFromKeyedArrayTree(
$property_key = $property_branch->value;
}
if ($is_list && (
!is_numeric($property_key)
ArrayAnalyzer::getLiteralArrayKeyInt($property_key) === false
|| ($had_optional && !$property_maybe_undefined)
|| $type === 'array'
|| $type === 'callable-array'
Expand Down