mirror of
https://github.com/danog/psalm.git
synced 2024-11-30 04:39:00 +01:00
Add support for checking integer array offsets
This commit is contained in:
parent
d85fbaec09
commit
b0aaede9e1
@ -56,6 +56,7 @@
|
|||||||
<xs:attribute name="includePhpVersionsInErrorBaseline" type="xs:string" />
|
<xs:attribute name="includePhpVersionsInErrorBaseline" type="xs:string" />
|
||||||
<xs:attribute name="loadXdebugStub" type="xs:string" />
|
<xs:attribute name="loadXdebugStub" type="xs:string" />
|
||||||
<xs:attribute name="ensureArrayStringOffsetsExist" type="xs:string" />
|
<xs:attribute name="ensureArrayStringOffsetsExist" type="xs:string" />
|
||||||
|
<xs:attribute name="ensureArrayIntOffsetsExist" type="xs:string" />
|
||||||
</xs:complexType>
|
</xs:complexType>
|
||||||
|
|
||||||
<xs:complexType name="ProjectFilesType">
|
<xs:complexType name="ProjectFilesType">
|
||||||
|
@ -11,6 +11,7 @@
|
|||||||
throwExceptionOnError="0"
|
throwExceptionOnError="0"
|
||||||
findUnusedCode="true"
|
findUnusedCode="true"
|
||||||
ensureArrayStringOffsetsExist="true"
|
ensureArrayStringOffsetsExist="true"
|
||||||
|
ensureArrayIntOffsetsExist="false"
|
||||||
resolveFromConfigFile="true"
|
resolveFromConfigFile="true"
|
||||||
xsi:schemaLocation="https://getpsalm.org/schema/config config.xsd"
|
xsi:schemaLocation="https://getpsalm.org/schema/config config.xsd"
|
||||||
>
|
>
|
||||||
|
@ -326,6 +326,11 @@ class Config
|
|||||||
*/
|
*/
|
||||||
public $ensure_array_string_offsets_exist = false;
|
public $ensure_array_string_offsets_exist = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var bool
|
||||||
|
*/
|
||||||
|
public $ensure_array_int_offsets_exist = false;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @var array<string, bool>
|
* @var array<string, bool>
|
||||||
*/
|
*/
|
||||||
@ -697,6 +702,7 @@ class Config
|
|||||||
'includePhpVersionsInErrorBaseline' => 'include_php_versions_in_error_baseline',
|
'includePhpVersionsInErrorBaseline' => 'include_php_versions_in_error_baseline',
|
||||||
'loadXdebugStub' => 'load_xdebug_stub',
|
'loadXdebugStub' => 'load_xdebug_stub',
|
||||||
'ensureArrayStringOffsetsExist' => 'ensure_array_string_offsets_exist',
|
'ensureArrayStringOffsetsExist' => 'ensure_array_string_offsets_exist',
|
||||||
|
'ensureArrayIntOffsetsExist' => 'ensure_array_int_offsets_exist',
|
||||||
];
|
];
|
||||||
|
|
||||||
foreach ($booleanAttributes as $xmlName => $internalName) {
|
foreach ($booleanAttributes as $xmlName => $internalName) {
|
||||||
|
@ -537,7 +537,20 @@ class ArrayFetchAnalyzer
|
|||||||
if ($codebase->config->ensure_array_string_offsets_exist
|
if ($codebase->config->ensure_array_string_offsets_exist
|
||||||
&& $offset_type_contained_by_expected
|
&& $offset_type_contained_by_expected
|
||||||
) {
|
) {
|
||||||
self::checkLiteralArrayOffset(
|
self::checkLiteralStringArrayOffset(
|
||||||
|
$offset_type,
|
||||||
|
$expected_offset_type,
|
||||||
|
$array_var_id,
|
||||||
|
$stmt,
|
||||||
|
$context,
|
||||||
|
$statements_analyzer
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($codebase->config->ensure_array_int_offsets_exist
|
||||||
|
&& $offset_type_contained_by_expected
|
||||||
|
) {
|
||||||
|
self::checkLiteralIntArrayOffset(
|
||||||
$offset_type,
|
$offset_type,
|
||||||
$expected_offset_type,
|
$expected_offset_type,
|
||||||
$array_var_id,
|
$array_var_id,
|
||||||
@ -662,7 +675,18 @@ class ArrayFetchAnalyzer
|
|||||||
$has_valid_offset = true;
|
$has_valid_offset = true;
|
||||||
|
|
||||||
if ($codebase->config->ensure_array_string_offsets_exist) {
|
if ($codebase->config->ensure_array_string_offsets_exist) {
|
||||||
self::checkLiteralArrayOffset(
|
self::checkLiteralStringArrayOffset(
|
||||||
|
$offset_type,
|
||||||
|
$type->getGenericKeyType(),
|
||||||
|
$array_var_id,
|
||||||
|
$stmt,
|
||||||
|
$context,
|
||||||
|
$statements_analyzer
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($codebase->config->ensure_array_int_offsets_exist) {
|
||||||
|
self::checkLiteralIntArrayOffset(
|
||||||
$offset_type,
|
$offset_type,
|
||||||
$type->getGenericKeyType(),
|
$type->getGenericKeyType(),
|
||||||
$array_var_id,
|
$array_var_id,
|
||||||
@ -1122,7 +1146,7 @@ class ArrayFetchAnalyzer
|
|||||||
return $array_access_type;
|
return $array_access_type;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static function checkLiteralArrayOffset(
|
private static function checkLiteralIntArrayOffset(
|
||||||
Type\Union $offset_type,
|
Type\Union $offset_type,
|
||||||
Type\Union $expected_offset_type,
|
Type\Union $expected_offset_type,
|
||||||
?string $array_var_id,
|
?string $array_var_id,
|
||||||
@ -1130,11 +1154,60 @@ class ArrayFetchAnalyzer
|
|||||||
Context $context,
|
Context $context,
|
||||||
StatementsAnalyzer $statements_analyzer
|
StatementsAnalyzer $statements_analyzer
|
||||||
) : void {
|
) : void {
|
||||||
if ($offset_type->hasLiteralString()
|
if ($context->inside_isset || $context->inside_unset) {
|
||||||
&& !$expected_offset_type->hasLiteralClassString()
|
return;
|
||||||
&& !$context->inside_isset
|
}
|
||||||
&& !$context->inside_unset
|
|
||||||
) {
|
if ($offset_type->hasLiteralInt()) {
|
||||||
|
$found_match = false;
|
||||||
|
|
||||||
|
foreach ($offset_type->getTypes() as $offset_type_part) {
|
||||||
|
if ($array_var_id
|
||||||
|
&& $offset_type_part instanceof TLiteralInt
|
||||||
|
&& isset(
|
||||||
|
$context->vars_in_scope[
|
||||||
|
$array_var_id . '[' . $offset_type_part->value . ']'
|
||||||
|
]
|
||||||
|
)
|
||||||
|
&& !$context->vars_in_scope[
|
||||||
|
$array_var_id . '[' . $offset_type_part->value . ']'
|
||||||
|
]->possibly_undefined
|
||||||
|
) {
|
||||||
|
$found_match = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$found_match) {
|
||||||
|
if (IssueBuffer::accepts(
|
||||||
|
new PossiblyUndefinedArrayOffset(
|
||||||
|
'Possibly undefined array offset \''
|
||||||
|
. $offset_type->getId() . '\' '
|
||||||
|
. 'is risky given expected type \''
|
||||||
|
. $expected_offset_type->getId() . '\'.'
|
||||||
|
. ' Consider using isset beforehand.',
|
||||||
|
new CodeLocation($statements_analyzer->getSource(), $stmt)
|
||||||
|
),
|
||||||
|
$statements_analyzer->getSuppressedIssues()
|
||||||
|
)) {
|
||||||
|
// fall through
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static function checkLiteralStringArrayOffset(
|
||||||
|
Type\Union $offset_type,
|
||||||
|
Type\Union $expected_offset_type,
|
||||||
|
?string $array_var_id,
|
||||||
|
PhpParser\Node\Expr\ArrayDimFetch $stmt,
|
||||||
|
Context $context,
|
||||||
|
StatementsAnalyzer $statements_analyzer
|
||||||
|
) : void {
|
||||||
|
if ($context->inside_isset || $context->inside_unset) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($offset_type->hasLiteralString() && !$expected_offset_type->hasLiteralClassString()) {
|
||||||
$found_match = false;
|
$found_match = false;
|
||||||
|
|
||||||
foreach ($offset_type->getTypes() as $offset_type_part) {
|
foreach ($offset_type->getTypes() as $offset_type_part) {
|
||||||
|
@ -1726,6 +1726,14 @@ class Union
|
|||||||
return count($this->literal_string_types) > 0;
|
return count($this->literal_string_types) > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public function hasLiteralInt()
|
||||||
|
{
|
||||||
|
return count($this->literal_int_types) > 0;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return bool true if this is a int literal with only one possible value
|
* @return bool true if this is a int literal with only one possible value
|
||||||
*/
|
*/
|
||||||
|
@ -116,6 +116,30 @@ class ArrayAccessTest extends TestCase
|
|||||||
$this->analyzeFile('somefile.php', new \Psalm\Context());
|
$this->analyzeFile('somefile.php', new \Psalm\Context());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function testEnsureArrayIntOffsetsExist()
|
||||||
|
{
|
||||||
|
$this->expectException(\Psalm\Exception\CodeException::class);
|
||||||
|
$this->expectExceptionMessage('PossiblyUndefinedArrayOffset');
|
||||||
|
|
||||||
|
\Psalm\Config::getInstance()->ensure_array_int_offsets_exist = true;
|
||||||
|
|
||||||
|
$this->addFile(
|
||||||
|
'somefile.php',
|
||||||
|
'<?php
|
||||||
|
function takesString(string $s): void {}
|
||||||
|
|
||||||
|
/** @param array<int, string> $arr */
|
||||||
|
function takesArrayIteratorOfString(array $arr): void {
|
||||||
|
echo $arr[4];
|
||||||
|
}'
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->analyzeFile('somefile.php', new \Psalm\Context());
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return iterable<string,array{string,assertions?:array<string,string>,error_levels?:string[]}>
|
* @return iterable<string,array{string,assertions?:array<string,string>,error_levels?:string[]}>
|
||||||
*/
|
*/
|
||||||
|
Loading…
Reference in New Issue
Block a user