mirror of
https://github.com/danog/psalm.git
synced 2024-11-26 20:34:47 +01:00
Add more type-system protections for bad array args
This commit is contained in:
parent
2829530ea6
commit
5eb2ebc508
@ -198,6 +198,10 @@ class TryAnalyzer
|
||||
throw new \UnexpectedValueException('Catch var name must be a string');
|
||||
}
|
||||
|
||||
if (!$catch->types) {
|
||||
throw new \UnexpectedValueException('Very bad');
|
||||
}
|
||||
|
||||
foreach ($catch->types as $catch_type) {
|
||||
$fq_catch_class = ClassLikeAnalyzer::getFQCLNFromNameObject(
|
||||
$catch_type,
|
||||
|
@ -298,6 +298,7 @@ class ArrayAnalyzer
|
||||
&& $item_key_type
|
||||
&& ($item_key_type->hasString() || $item_key_type->hasInt())
|
||||
&& $can_create_objectlike
|
||||
&& $property_types
|
||||
) {
|
||||
$object_like = new Type\Atomic\ObjectLike($property_types, $class_strings);
|
||||
$object_like->sealed = true;
|
||||
|
@ -1744,7 +1744,7 @@ class MethodCallAnalyzer extends \Psalm\Internal\Analyzer\Statements\Expression\
|
||||
}
|
||||
|
||||
$class_template_params[$type_name][$candidate_class_storage->name] = [
|
||||
$output_type_extends ?: Type::getMixed()
|
||||
$output_type_extends
|
||||
];
|
||||
}
|
||||
}
|
||||
|
@ -1996,7 +1996,7 @@ class CallAnalyzer
|
||||
}
|
||||
}
|
||||
|
||||
$template_types[$template_name][$class_storage->name] = [$output_type ?: Type::getMixed()];
|
||||
$template_types[$template_name][$class_storage->name] = [$output_type];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1051,6 +1051,10 @@ class ArrayFetchAnalyzer
|
||||
$valid_offsets[] = new TLiteralInt($i);
|
||||
}
|
||||
|
||||
if (!$valid_offsets) {
|
||||
throw new \UnexpectedValueException('This is weird');
|
||||
}
|
||||
|
||||
$valid_offset_type = new Type\Union($valid_offsets);
|
||||
} else {
|
||||
$valid_offset_type = Type::getInt();
|
||||
|
@ -299,7 +299,7 @@ class ExpressionAnalyzer
|
||||
}
|
||||
}
|
||||
|
||||
if ($unacceptable_type) {
|
||||
if ($unacceptable_type || !$acceptable_types) {
|
||||
$message = 'Cannot negate a non-numeric non-string type ' . $unacceptable_type;
|
||||
if ($has_valid_operand) {
|
||||
if (IssueBuffer::accepts(
|
||||
@ -648,7 +648,7 @@ class ExpressionAnalyzer
|
||||
}
|
||||
}
|
||||
|
||||
if ($all_permissible) {
|
||||
if ($permissible_atomic_types && $all_permissible) {
|
||||
$statements_analyzer->node_data->setType(
|
||||
$stmt,
|
||||
TypeCombination::combineTypes($permissible_atomic_types)
|
||||
@ -1218,7 +1218,7 @@ class ExpressionAnalyzer
|
||||
* @param string|null $self_class
|
||||
* @param string|Type\Atomic\TNamedObject|Type\Atomic\TTemplateParam|null $static_class_type
|
||||
*
|
||||
* @return Type\Atomic|array<int, Type\Atomic>
|
||||
* @return Type\Atomic|non-empty-array<int, Type\Atomic>
|
||||
*/
|
||||
private static function fleshOutAtomicType(
|
||||
Codebase $codebase,
|
||||
@ -1974,8 +1974,14 @@ class ExpressionAnalyzer
|
||||
// todo: emit error here
|
||||
}
|
||||
|
||||
$valid_types = array_merge($valid_strings, $castable_types);
|
||||
|
||||
if (!$valid_types) {
|
||||
return Type::getString();
|
||||
}
|
||||
|
||||
return \Psalm\Internal\Type\TypeCombination::combineTypes(
|
||||
array_merge($valid_strings, $castable_types),
|
||||
$valid_types,
|
||||
$codebase
|
||||
);
|
||||
}
|
||||
|
@ -1948,6 +1948,7 @@ class StatementsAnalyzer extends SourceAnalyzer implements StatementsSource
|
||||
&& $item_key_type
|
||||
&& ($item_key_type->hasString() || $item_key_type->hasInt())
|
||||
&& $can_create_objectlike
|
||||
&& $property_types
|
||||
) {
|
||||
$objectlike = new Type\Atomic\ObjectLike($property_types, $class_strings);
|
||||
$objectlike->sealed = true;
|
||||
|
@ -762,7 +762,7 @@ class TypeAnalyzer
|
||||
}
|
||||
}
|
||||
|
||||
if ($all_string_int_literals) {
|
||||
if ($all_string_int_literals && $properties) {
|
||||
$input_type_part = new ObjectLike($properties);
|
||||
}
|
||||
}
|
||||
@ -1970,14 +1970,12 @@ class TypeAnalyzer
|
||||
}
|
||||
}
|
||||
|
||||
if ($new_input_param) {
|
||||
$new_input_param = clone $new_input_param;
|
||||
$new_input_param->replaceTemplateTypesWithArgTypes(
|
||||
$replacement_templates
|
||||
);
|
||||
}
|
||||
$new_input_param = clone $new_input_param;
|
||||
$new_input_param->replaceTemplateTypesWithArgTypes(
|
||||
$replacement_templates
|
||||
);
|
||||
|
||||
$new_input_params[] = $new_input_param ?: Type::getMixed();
|
||||
$new_input_params[] = $new_input_param;
|
||||
}
|
||||
}
|
||||
|
||||
@ -2520,7 +2518,7 @@ class TypeAnalyzer
|
||||
}
|
||||
}
|
||||
|
||||
if (count($unique_types) === 0) {
|
||||
if (!$unique_types) {
|
||||
throw new \UnexpectedValueException('There must be more than one unique type');
|
||||
}
|
||||
|
||||
|
@ -61,7 +61,11 @@ class ArrayMapReturnTypeProvider implements \Psalm\Plugin\Hook\FunctionReturnTyp
|
||||
}
|
||||
}
|
||||
|
||||
return new Type\Union([new Type\Atomic\ObjectLike($array_arg_types)]);
|
||||
if ($array_arg_types) {
|
||||
return new Type\Union([new Type\Atomic\ObjectLike($array_arg_types)]);
|
||||
}
|
||||
|
||||
return Type::getArray();
|
||||
}
|
||||
|
||||
$array_arg = isset($call_args[1]->value) ? $call_args[1]->value : null;
|
||||
|
@ -39,6 +39,7 @@ class ArrayMergeReturnTypeProvider implements \Psalm\Plugin\Hook\FunctionReturnT
|
||||
$generic_properties = [];
|
||||
$all_int_offsets = true;
|
||||
$all_nonempty_lists = true;
|
||||
$any_nonempty = false;
|
||||
|
||||
foreach ($call_args as $call_arg) {
|
||||
if (!($call_arg_type = $statements_source->node_data->getType($call_arg->value))) {
|
||||
@ -90,12 +91,18 @@ class ArrayMergeReturnTypeProvider implements \Psalm\Plugin\Hook\FunctionReturnT
|
||||
$all_nonempty_lists = false;
|
||||
}
|
||||
|
||||
if ($unpacked_type_part->sealed) {
|
||||
$any_nonempty = true;
|
||||
}
|
||||
|
||||
$unpacked_type_part = $unpacked_type_part->getGenericArrayType();
|
||||
} elseif ($unpacked_type_part instanceof Type\Atomic\TList) {
|
||||
$generic_properties = null;
|
||||
|
||||
if (!$unpacked_type_part instanceof Type\Atomic\TNonEmptyList) {
|
||||
$all_nonempty_lists = false;
|
||||
} else {
|
||||
$any_nonempty = true;
|
||||
}
|
||||
} else {
|
||||
if ($unpacked_type_part instanceof Type\Atomic\TMixed
|
||||
@ -122,6 +129,10 @@ class ArrayMergeReturnTypeProvider implements \Psalm\Plugin\Hook\FunctionReturnT
|
||||
if (!$unpacked_type_part->type_params[0]->isInt()) {
|
||||
$all_int_offsets = false;
|
||||
}
|
||||
|
||||
if ($unpacked_type_part instanceof Type\Atomic\TNonEmptyArray) {
|
||||
$any_nonempty = true;
|
||||
}
|
||||
}
|
||||
|
||||
$inner_key_types = array_merge(
|
||||
@ -154,7 +165,7 @@ class ArrayMergeReturnTypeProvider implements \Psalm\Plugin\Hook\FunctionReturnT
|
||||
$inner_value_type = TypeCombination::combineTypes($inner_value_types, $codebase, true);
|
||||
|
||||
if ($all_int_offsets) {
|
||||
if ($all_nonempty_lists) {
|
||||
if ($any_nonempty) {
|
||||
return new Type\Union([
|
||||
new Type\Atomic\TNonEmptyList($inner_value_type),
|
||||
]);
|
||||
@ -165,7 +176,18 @@ class ArrayMergeReturnTypeProvider implements \Psalm\Plugin\Hook\FunctionReturnT
|
||||
]);
|
||||
}
|
||||
|
||||
$inner_key_type = TypeCombination::combineTypes($inner_key_types, $codebase, true);
|
||||
$inner_key_type = $inner_key_types
|
||||
? TypeCombination::combineTypes($inner_key_types, $codebase, true)
|
||||
: Type::getArrayKey();
|
||||
|
||||
if ($any_nonempty) {
|
||||
return new Type\Union([
|
||||
new Type\Atomic\TNonEmptyArray([
|
||||
$inner_key_type,
|
||||
$inner_value_type,
|
||||
]),
|
||||
]);
|
||||
}
|
||||
|
||||
return new Type\Union([
|
||||
new Type\Atomic\TArray([
|
||||
|
@ -148,7 +148,7 @@ class TypeCombination
|
||||
* - and `array<string> + array<empty> = array<string>`
|
||||
* - and `array + array<string> = array<mixed>`
|
||||
*
|
||||
* @param list<Atomic> $types
|
||||
* @param non-empty-list<Atomic> $types
|
||||
* @param int $literal_limit any greater number of literal types than this
|
||||
* will be merged to a scalar
|
||||
*
|
||||
@ -176,10 +176,6 @@ class TypeCombination
|
||||
return $union_type;
|
||||
}
|
||||
|
||||
if (!$types) {
|
||||
throw new \InvalidArgumentException('You must pass at least one type to combineTypes');
|
||||
}
|
||||
|
||||
$combination = new TypeCombination();
|
||||
|
||||
$from_docblock = false;
|
||||
|
@ -470,6 +470,12 @@ abstract class Type
|
||||
$atomic_types[] = new TNull;
|
||||
}
|
||||
|
||||
if (!$atomic_types) {
|
||||
throw new TypeParseTreeException(
|
||||
'No atomic types found'
|
||||
);
|
||||
}
|
||||
|
||||
return TypeCombination::combineTypes($atomic_types);
|
||||
}
|
||||
|
||||
|
@ -24,7 +24,7 @@ use Psalm\Type\Union;
|
||||
class ObjectLike extends \Psalm\Type\Atomic
|
||||
{
|
||||
/**
|
||||
* @var array<string|int, Union>
|
||||
* @var non-empty-array<string|int, Union>
|
||||
*/
|
||||
public $properties;
|
||||
|
||||
@ -62,7 +62,7 @@ class ObjectLike extends \Psalm\Type\Atomic
|
||||
/**
|
||||
* Constructs a new instance of a generic type
|
||||
*
|
||||
* @param array<string|int, Union> $properties
|
||||
* @param non-empty-array<string|int, Union> $properties
|
||||
* @param array<string, bool> $class_strings
|
||||
*/
|
||||
public function __construct(array $properties, array $class_strings = null)
|
||||
@ -231,10 +231,6 @@ class ObjectLike extends \Psalm\Type\Atomic
|
||||
}
|
||||
}
|
||||
|
||||
if (!$value_type) {
|
||||
throw new \UnexpectedValueException('$value_type should not be null here');
|
||||
}
|
||||
|
||||
if ($this->previous_value_type) {
|
||||
$value_type = Type::combineUnionTypes($this->previous_value_type, $value_type);
|
||||
}
|
||||
@ -268,10 +264,6 @@ class ObjectLike extends \Psalm\Type\Atomic
|
||||
}
|
||||
}
|
||||
|
||||
if (!$value_type) {
|
||||
throw new \UnexpectedValueException('$value_type should not be null here');
|
||||
}
|
||||
|
||||
$key_type = TypeCombination::combineTypes($key_types);
|
||||
|
||||
if ($this->previous_value_type) {
|
||||
|
@ -52,9 +52,9 @@ class Union
|
||||
| self::TAINTED_SYSTEM_SECRET;
|
||||
|
||||
/**
|
||||
* @var array<string, Atomic>
|
||||
* @var non-empty-array<string, Atomic>
|
||||
*/
|
||||
private $types = [];
|
||||
private $types;
|
||||
|
||||
/**
|
||||
* Whether the type originated in a docblock
|
||||
@ -196,19 +196,17 @@ class Union
|
||||
/**
|
||||
* Constructs an Union instance
|
||||
*
|
||||
* @param array<int, Atomic> $types
|
||||
* @param non-empty-array<int, Atomic> $types
|
||||
*/
|
||||
public function __construct(array $types)
|
||||
{
|
||||
$from_docblock = false;
|
||||
|
||||
if (!$types) {
|
||||
throw new \UnexpectedValueException('Cannot construct a union with empty types');
|
||||
}
|
||||
$keyed_types = [];
|
||||
|
||||
foreach ($types as $type) {
|
||||
$key = $type->getKey();
|
||||
$this->types[$key] = $type;
|
||||
$keyed_types[$key] = $type;
|
||||
|
||||
if ($type instanceof TLiteralInt) {
|
||||
$this->literal_int_types[$key] = $type;
|
||||
@ -223,6 +221,8 @@ class Union
|
||||
$from_docblock = $from_docblock || $type->from_docblock;
|
||||
}
|
||||
|
||||
$this->types = $keyed_types;
|
||||
|
||||
$this->from_docblock = $from_docblock;
|
||||
}
|
||||
|
||||
@ -237,7 +237,7 @@ class Union
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, Atomic>
|
||||
* @return non-empty-array<string, Atomic>
|
||||
*/
|
||||
public function getAtomicTypes()
|
||||
{
|
||||
@ -486,15 +486,15 @@ class Union
|
||||
$types = $this->types;
|
||||
|
||||
if (isset($types['null'])) {
|
||||
if (count($types) === 1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
unset($types['null']);
|
||||
|
||||
$nullable = true;
|
||||
}
|
||||
|
||||
if (!$types) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$atomic_type = array_values($types)[0];
|
||||
|
||||
$atomic_type_string = $atomic_type->toPhpString(
|
||||
@ -524,11 +524,11 @@ class Union
|
||||
$types = $this->types;
|
||||
|
||||
if (isset($types['null'])) {
|
||||
unset($types['null']);
|
||||
}
|
||||
|
||||
if (!$types) {
|
||||
return false;
|
||||
if (count($types) > 1) {
|
||||
unset($types['null']);
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
$atomic_type = array_values($types)[0];
|
||||
@ -1252,6 +1252,10 @@ class Union
|
||||
$this->id = null;
|
||||
|
||||
if ($is_mixed) {
|
||||
if (!$new_types) {
|
||||
throw new \UnexpectedValueException('This array should be full');
|
||||
}
|
||||
|
||||
$this->types = $new_types;
|
||||
|
||||
return;
|
||||
@ -1271,10 +1275,14 @@ class Union
|
||||
}
|
||||
}
|
||||
|
||||
$this->types = TypeCombination::combineTypes(
|
||||
array_values(array_merge($this->types, $new_types)),
|
||||
$codebase
|
||||
)->getAtomicTypes();
|
||||
$atomic_types = array_values(array_merge($this->types, $new_types));
|
||||
|
||||
if ($atomic_types) {
|
||||
$this->types = TypeCombination::combineTypes(
|
||||
$atomic_types,
|
||||
$codebase
|
||||
)->getAtomicTypes();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -27,6 +27,8 @@ class TypeCombinationTest extends TestCase
|
||||
$converted_types[] = $converted_type;
|
||||
}
|
||||
|
||||
$this->assertNotEmpty($converted_types);
|
||||
|
||||
$this->assertSame(
|
||||
$expected,
|
||||
(string) TypeCombination::combineTypes($converted_types)
|
||||
|
Loading…
Reference in New Issue
Block a user