mirror of
https://github.com/danog/psalm.git
synced 2025-01-22 05:41:20 +01:00
Allow class constant as offset key (#5943)
* allow class-constant as an array offset when the type match * remove object with __toString as a possible offset as it's not valid
This commit is contained in:
parent
872f1c232c
commit
79478a60b1
@ -14,6 +14,7 @@ use Psalm\Internal\Type\Comparator\UnionTypeComparator;
|
|||||||
use Psalm\Internal\Type\TemplateInferredTypeReplacer;
|
use Psalm\Internal\Type\TemplateInferredTypeReplacer;
|
||||||
use Psalm\Internal\Type\TemplateResult;
|
use Psalm\Internal\Type\TemplateResult;
|
||||||
use Psalm\Internal\Type\TypeCombiner;
|
use Psalm\Internal\Type\TypeCombiner;
|
||||||
|
use Psalm\Internal\Type\TypeExpander;
|
||||||
use Psalm\Issue\EmptyArrayAccess;
|
use Psalm\Issue\EmptyArrayAccess;
|
||||||
use Psalm\Issue\InvalidArrayAccess;
|
use Psalm\Issue\InvalidArrayAccess;
|
||||||
use Psalm\Issue\InvalidArrayAssignment;
|
use Psalm\Issue\InvalidArrayAssignment;
|
||||||
@ -42,6 +43,7 @@ use Psalm\Node\VirtualIdentifier;
|
|||||||
use Psalm\Node\VirtualName;
|
use Psalm\Node\VirtualName;
|
||||||
use Psalm\Plugin\EventHandler\Event\AddRemoveTaintsEvent;
|
use Psalm\Plugin\EventHandler\Event\AddRemoveTaintsEvent;
|
||||||
use Psalm\Type;
|
use Psalm\Type;
|
||||||
|
use Psalm\Type\Atomic;
|
||||||
use Psalm\Type\Atomic\TArray;
|
use Psalm\Type\Atomic\TArray;
|
||||||
use Psalm\Type\Atomic\TArrayKey;
|
use Psalm\Type\Atomic\TArrayKey;
|
||||||
use Psalm\Type\Atomic\TClassStringMap;
|
use Psalm\Type\Atomic\TClassStringMap;
|
||||||
@ -450,7 +452,6 @@ class ArrayFetchAnalyzer
|
|||||||
$non_array_types = [];
|
$non_array_types = [];
|
||||||
|
|
||||||
$has_valid_expected_offset = false;
|
$has_valid_expected_offset = false;
|
||||||
$has_valid_absolute_offset = false;
|
|
||||||
$expected_offset_types = [];
|
$expected_offset_types = [];
|
||||||
|
|
||||||
$key_values = [];
|
$key_values = [];
|
||||||
@ -519,28 +520,11 @@ class ArrayFetchAnalyzer
|
|||||||
}
|
}
|
||||||
|
|
||||||
if ($array_type->isArray()) {
|
if ($array_type->isArray()) {
|
||||||
foreach ($offset_type->getAtomicTypes() as $atomic_offset_type) {
|
$has_valid_absolute_offset = self::checkArrayOffsetType(
|
||||||
if ($atomic_offset_type instanceof Type\Atomic\TFalse &&
|
$offset_type,
|
||||||
$offset_type->ignore_falsable_issues === true
|
$offset_type->getAtomicTypes(),
|
||||||
) {
|
$codebase
|
||||||
//do nothing
|
);
|
||||||
} elseif ($atomic_offset_type instanceof Type\Atomic\TNull &&
|
|
||||||
$offset_type->ignore_nullable_issues === true
|
|
||||||
) {
|
|
||||||
//do nothing
|
|
||||||
} elseif ($atomic_offset_type instanceof Type\Atomic\TString ||
|
|
||||||
$atomic_offset_type instanceof Type\Atomic\TInt ||
|
|
||||||
$atomic_offset_type instanceof Type\Atomic\TArrayKey ||
|
|
||||||
$atomic_offset_type instanceof Type\Atomic\TMixed ||
|
|
||||||
$atomic_offset_type instanceof Type\Atomic\TTemplateParam ||
|
|
||||||
(
|
|
||||||
$atomic_offset_type instanceof Type\Atomic\TObjectWithProperties
|
|
||||||
&& isset($atomic_offset_type->methods['__toString'])
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
$has_valid_absolute_offset = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($has_valid_absolute_offset === false) {
|
if ($has_valid_absolute_offset === false) {
|
||||||
//we didn't find a single type that could be valid
|
//we didn't find a single type that could be valid
|
||||||
@ -2069,4 +2053,74 @@ class ArrayFetchAnalyzer
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param Atomic[] $offset_types
|
||||||
|
*/
|
||||||
|
private static function checkArrayOffsetType(
|
||||||
|
Type\Union $offset_type,
|
||||||
|
array $offset_types,
|
||||||
|
\Psalm\Codebase $codebase
|
||||||
|
): bool {
|
||||||
|
$has_valid_absolute_offset = false;
|
||||||
|
foreach ($offset_types as $atomic_offset_type) {
|
||||||
|
if ($atomic_offset_type instanceof Type\Atomic\TClassConstant) {
|
||||||
|
$expanded = TypeExpander::expandAtomic(
|
||||||
|
$codebase,
|
||||||
|
$atomic_offset_type,
|
||||||
|
$atomic_offset_type->fq_classlike_name,
|
||||||
|
$atomic_offset_type->fq_classlike_name,
|
||||||
|
null,
|
||||||
|
true,
|
||||||
|
true
|
||||||
|
);
|
||||||
|
|
||||||
|
if ($expanded instanceof Atomic) {
|
||||||
|
$has_valid_absolute_offset = self::checkArrayOffsetType(
|
||||||
|
$offset_type,
|
||||||
|
[$expanded],
|
||||||
|
$codebase
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
$has_valid_absolute_offset = self::checkArrayOffsetType(
|
||||||
|
$offset_type,
|
||||||
|
$expanded,
|
||||||
|
$codebase
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($has_valid_absolute_offset) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($atomic_offset_type instanceof Type\Atomic\TFalse &&
|
||||||
|
$offset_type->ignore_falsable_issues === true
|
||||||
|
) {
|
||||||
|
//do nothing
|
||||||
|
} elseif ($atomic_offset_type instanceof Type\Atomic\TNull &&
|
||||||
|
$offset_type->ignore_nullable_issues === true
|
||||||
|
) {
|
||||||
|
//do nothing
|
||||||
|
} elseif ($atomic_offset_type instanceof Type\Atomic\TString ||
|
||||||
|
$atomic_offset_type instanceof Type\Atomic\TInt ||
|
||||||
|
$atomic_offset_type instanceof Type\Atomic\TArrayKey ||
|
||||||
|
$atomic_offset_type instanceof Type\Atomic\TMixed
|
||||||
|
) {
|
||||||
|
$has_valid_absolute_offset = true;
|
||||||
|
break;
|
||||||
|
} elseif ($atomic_offset_type instanceof Type\Atomic\TTemplateParam) {
|
||||||
|
$has_valid_absolute_offset = self::checkArrayOffsetType(
|
||||||
|
$offset_type,
|
||||||
|
$atomic_offset_type->as->getAtomicTypes(),
|
||||||
|
$codebase
|
||||||
|
);
|
||||||
|
|
||||||
|
if ($has_valid_absolute_offset) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $has_valid_absolute_offset;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,7 +3,9 @@
|
|||||||
namespace Psalm\Internal\Type\Comparator;
|
namespace Psalm\Internal\Type\Comparator;
|
||||||
|
|
||||||
use Psalm\Codebase;
|
use Psalm\Codebase;
|
||||||
|
use Psalm\Internal\Type\TypeExpander;
|
||||||
use Psalm\Type;
|
use Psalm\Type;
|
||||||
|
use Psalm\Type\Atomic;
|
||||||
use Psalm\Type\Atomic\TArrayKey;
|
use Psalm\Type\Atomic\TArrayKey;
|
||||||
use Psalm\Type\Atomic\TFalse;
|
use Psalm\Type\Atomic\TFalse;
|
||||||
use Psalm\Type\Atomic\TMixed;
|
use Psalm\Type\Atomic\TMixed;
|
||||||
@ -67,6 +69,26 @@ class UnionTypeComparator
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ($input_type_part instanceof Type\Atomic\TClassConstant) {
|
||||||
|
$expanded = TypeExpander::expandAtomic(
|
||||||
|
$codebase,
|
||||||
|
$input_type_part,
|
||||||
|
$input_type_part->fq_classlike_name,
|
||||||
|
$input_type_part->fq_classlike_name,
|
||||||
|
null,
|
||||||
|
true,
|
||||||
|
true
|
||||||
|
);
|
||||||
|
|
||||||
|
if ($expanded instanceof Atomic) {
|
||||||
|
$input_atomic_types[] = $expanded;
|
||||||
|
} else {
|
||||||
|
$input_atomic_types = array_merge($expanded, $input_atomic_types);
|
||||||
|
}
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
$type_match_found = false;
|
$type_match_found = false;
|
||||||
$scalar_type_match_found = false;
|
$scalar_type_match_found = false;
|
||||||
$all_to_string_cast = true;
|
$all_to_string_cast = true;
|
||||||
|
@ -1579,6 +1579,37 @@ class ArrayAssignmentTest extends TestCase
|
|||||||
return (array)$a;
|
return (array)$a;
|
||||||
}',
|
}',
|
||||||
],
|
],
|
||||||
|
'ClassConstantAsKey' => [
|
||||||
|
'<?php
|
||||||
|
/**
|
||||||
|
* @property Foo::C_* $aprop
|
||||||
|
*/
|
||||||
|
class Foo {
|
||||||
|
public const C_ONE = 1;
|
||||||
|
public const C_TWO = 2;
|
||||||
|
|
||||||
|
public function __get(string $prop) {
|
||||||
|
if ($prop === "aprop")
|
||||||
|
return self::C_ONE;
|
||||||
|
throw new \RuntimeException("Unsupported property: $prop");
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @return array<Foo::C_*, string> */
|
||||||
|
public static function getNames(): array {
|
||||||
|
return [
|
||||||
|
self::C_ONE => "One",
|
||||||
|
self::C_TWO => "Two",
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getThisName(): string {
|
||||||
|
$names = self::getNames();
|
||||||
|
$aprop = $this->aprop;
|
||||||
|
|
||||||
|
return $names[$aprop];
|
||||||
|
}
|
||||||
|
}',
|
||||||
|
],
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1991,6 +2022,22 @@ class ArrayAssignmentTest extends TestCase
|
|||||||
}',
|
}',
|
||||||
'error_message' => 'InvalidArrayOffset'
|
'error_message' => 'InvalidArrayOffset'
|
||||||
],
|
],
|
||||||
|
'TemplateAsKey' => [
|
||||||
|
'<?php
|
||||||
|
|
||||||
|
class Foo {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @psalm-template T of array
|
||||||
|
* @param T $offset
|
||||||
|
* @param array<array, string> $weird_array
|
||||||
|
*/
|
||||||
|
public function getThisName($offset, $weird_array): string {
|
||||||
|
return $weird_array[$offset];
|
||||||
|
}
|
||||||
|
}',
|
||||||
|
'error_message' => 'InvalidArrayOffset'
|
||||||
|
],
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user