1
0
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:
Brown 2019-10-04 11:08:08 -04:00
parent d85fbaec09
commit b0aaede9e1
6 changed files with 121 additions and 8 deletions

View File

@ -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">

View File

@ -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"
> >

View File

@ -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) {

View File

@ -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) {

View File

@ -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
*/ */

View File

@ -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[]}>
*/ */