Skip to content

Commit

Permalink
Merge pull request #6662 from orklah/remove-empty
Browse files Browse the repository at this point in the history
  • Loading branch information
weirdan committed Jan 3, 2022
2 parents 2f50070 + 1bb2661 commit fe02697
Show file tree
Hide file tree
Showing 74 changed files with 238 additions and 244 deletions.
13 changes: 7 additions & 6 deletions docs/annotating_code/type_syntax/atomic_types.md
Expand Up @@ -52,19 +52,20 @@ Atomic types are the basic building block of all type information used in Psalm.
- `value-of<Foo\Bar::ARRAY_CONST>`
- `T[K]`

## Top types, bottom types and empty
## Top types, bottom types

### `mixed`

This is the _top type_ in PHP's type system, and represents a lack of type information. Psalm warns about `mixed` types when the `totallyTyped` flag is turned on, or when you're on level 1.

### `never`
It can be aliased to `no-return` or `never-return` in docblocks. Note: it replaced the old `empty` type that used to exist in Psalm

This is the _bottom type_ in PHP's type system, and usually represents a return type for a function that can never actually return, such as `die()`, `exit()`, or a function that always throws an exception. It may also be written in docblocks as `no-return` or `never-return`.

### `empty`

A type that's equivalent to a "coming soon" sign. Psalm uses this type when it’s awaiting more information — a good example is the type of the empty array `[]`, which Psalm types as `array<empty, empty>`. Psalm treats `empty` in a somewhat similar fashion to `never` when combining types together — `empty|int` becomes `int`, just as `never|string` becomes `string`.
This is the _bottom type_ in PHP's type system. It's used to describe a type that has no possible value. It can happen in multiple cases:
- the actual `never` type from PHP 8.1 (can be used in docblocks for older versions). This type can be used as a return type for functions that will never return, either because they always throw exceptions or always exit()
- an union type that have been stripped for all its possible types. (For example, if a variable is `string|int` and we perform a is_bool() check in a condition, the type of the variable in the condition will be `never` as the condition will never be entered)
- it can represent a placeholder for types yet to come — a good example is the type of the empty array `[]`, which Psalm types as `array<never, never>`, the content of the array is void so it can accept any content
- it can also happen in the same context as the line above for templates that have yet to be defined

## Other

Expand Down
4 changes: 1 addition & 3 deletions docs/running_psalm/plugins/plugins_type_system.md
Expand Up @@ -27,16 +27,14 @@ The classes are as follows:

`TNull` - denotes the `null` type

`TNever` - denotes the `no-return`/`never-return` type for functions that never return, either throwing an exception or terminating (like the builtin `exit()`).
`TNever` - denotes the `no-return`/`never-return` type for functions that never return, either throwing an exception or terminating (like the builtin `exit()`). Also used for union types that can have no possible types (impossible intersections for example). Empty arrays `[]` have the type `array<never, never>`.

`TMixed` - denotes the `mixed` type, used when you don’t know the type of an expression.

`TNonEmptyMixed `- as above, but not empty. Generated for `$x` inside the `if` statement `if ($x) {...}` when `$x` is `mixed` outside.

`TEmptyMixed` - as above, but empty. Generated for `$x` inside the `if` statement `if (!$x) {...}` when `$x` is `mixed` outside.

`TEmpty` - denotes the `empty` type, used to describe a type corresponding to no value whatsoever. Empty arrays `[]` have the type `array<empty, empty>`.

`TIterable` - denotes the [`iterable` type](https://www.php.net/manual/en/language.types.iterable.php) (which can also result from an `is_iterable` check).

`TResource` - denotes the `resource` type (e.g. a file handle).
Expand Down
2 changes: 1 addition & 1 deletion src/Psalm/Internal/Analyzer/ClassAnalyzer.php
Expand Up @@ -1553,7 +1553,7 @@ private function checkForMissingPropertyType(

if ($suggested_type && !$suggested_type->isNull()) {
$message .= ' - consider ' . str_replace(
['<array-key, mixed>', '<empty, empty>'],
['<array-key, mixed>', '<never, never>'],
'',
(string)$suggested_type
);
Expand Down
2 changes: 1 addition & 1 deletion src/Psalm/Internal/Analyzer/ClassLikeAnalyzer.php
Expand Up @@ -50,7 +50,7 @@ abstract class ClassLikeAnalyzer extends SourceAnalyzer
'bool' => 'bool',
'false' => 'false',
'object' => 'object',
'empty' => 'empty',
'never' => 'never',
'callable' => 'callable',
'array' => 'array',
'iterable' => 'iterable',
Expand Down
Expand Up @@ -237,12 +237,12 @@ public static function verifyReturnType(
}

$number_of_types = count($inferred_return_type_parts);
// we filter TNever and TEmpty that have no bearing on the return type
// we filter TNever that have no bearing on the return type
if ($number_of_types > 1) {
$inferred_return_type_parts = array_filter(
$inferred_return_type_parts,
static function (Union $union_type): bool {
return !($union_type->isNever() || $union_type->isEmpty());
return !$union_type->isNever();
}
);
}
Expand Down Expand Up @@ -490,7 +490,7 @@ static function (Union $union_type): bool {
return null;
}

if ($inferred_return_type->hasMixed() || $inferred_return_type->isEmpty()) {
if ($inferred_return_type->hasMixed()) {
if (IssueBuffer::accepts(
new MixedInferredReturnType(
'Could not verify return type \'' . $declared_return_type . '\' for ' .
Expand Down
2 changes: 1 addition & 1 deletion src/Psalm/Internal/Analyzer/FunctionLikeAnalyzer.php
Expand Up @@ -503,7 +503,7 @@ public function analyze(
&& !$inferred_return_type->isSingleIntLiteral()
&& !$inferred_return_type->isSingleStringLiteral()
&& !$inferred_return_type->isTrue()
&& $inferred_return_type->getId() !== 'array<empty, empty>'
&& $inferred_return_type->getId() !== 'array<never, never>'
) {
$manipulator->makePure();
}
Expand Down
10 changes: 6 additions & 4 deletions src/Psalm/Internal/Analyzer/Statements/Block/ForeachAnalyzer.php
Expand Up @@ -49,6 +49,7 @@
use Psalm\Type\Atomic\TList;
use Psalm\Type\Atomic\TMixed;
use Psalm\Type\Atomic\TNamedObject;
use Psalm\Type\Atomic\TNever;
use Psalm\Type\Atomic\TNonEmptyArray;
use Psalm\Type\Atomic\TNonEmptyList;
use Psalm\Type\Atomic\TNull;
Expand Down Expand Up @@ -434,9 +435,7 @@ public static function checkIteratorType(
}

// if it's an empty array, we cannot iterate over it
if ($iterator_atomic_type instanceof TArray
&& $iterator_atomic_type->type_params[1]->isEmpty()
) {
if ($iterator_atomic_type instanceof TArray && $iterator_atomic_type->isEmptyArray()) {
$always_non_empty_array = false;
$has_valid_iterator = true;
continue;
Expand Down Expand Up @@ -505,7 +504,10 @@ public static function checkIteratorType(
$invalid_iterator_types[] = $iterator_atomic_type->getKey();

$value_type = Type::getMixed();
} elseif ($iterator_atomic_type instanceof TObject || $iterator_atomic_type instanceof TMixed) {
} elseif ($iterator_atomic_type instanceof TObject ||
$iterator_atomic_type instanceof TMixed ||
$iterator_atomic_type instanceof TNever
) {
$has_valid_iterator = true;
$value_type = Type::getMixed();
$key_type = Type::getMixed();
Expand Down
Expand Up @@ -171,7 +171,7 @@ public static function analyze(
);

if (isset($case_vars_in_scope_reconciled[$switch_var_id])
&& $case_vars_in_scope_reconciled[$switch_var_id]->isEmpty()
&& $case_vars_in_scope_reconciled[$switch_var_id]->isNever()
) {
$all_options_matched = true;
}
Expand Down
Expand Up @@ -591,7 +591,7 @@ private static function handleNonReturningCase(
if (!$case->cond
&& $switch_var_id
&& isset($case_context->vars_in_scope[$switch_var_id])
&& $case_context->vars_in_scope[$switch_var_id]->isEmpty()
&& $case_context->vars_in_scope[$switch_var_id]->isNever()
) {
if (IssueBuffer::accepts(
new ParadoxicalCondition(
Expand Down
Expand Up @@ -547,7 +547,7 @@ private static function handleUnpackedArray(
&& count($unpacked_atomic_type->type_params) === 2
)) {
/** @psalm-suppress PossiblyUndefinedArrayOffset provably true, but Psalm can’t see it */
if ($unpacked_atomic_type->type_params[1]->isEmpty()) {
if ($unpacked_atomic_type->type_params[1]->isNever()) {
continue;
}
$array_creation_info->can_create_objectlike = false;
Expand Down Expand Up @@ -579,7 +579,7 @@ private static function handleUnpackedArray(
)
);
} elseif ($unpacked_atomic_type instanceof TList) {
if ($unpacked_atomic_type->type_param->isEmpty()) {
if ($unpacked_atomic_type->type_param->isNever()) {
continue;
}
$array_creation_info->can_create_objectlike = false;
Expand Down
Expand Up @@ -739,7 +739,7 @@ private static function analyzeNestedArrayAssignment(
return;
}

if ($child_stmt_var_type->isEmpty()) {
if ($child_stmt_var_type->isNever()) {
$child_stmt_var_type = Type::getEmptyArray();
$statements_analyzer->node_data->setType($child_stmt->var, $child_stmt_var_type);
}
Expand Down
Expand Up @@ -569,7 +569,7 @@ public static function analyze(
return false;
}

$context->vars_in_scope[$var_id] = Type::getEmpty();
$context->vars_in_scope[$var_id] = Type::getNever();

$context->inside_assignment = $was_in_assignment;

Expand Down Expand Up @@ -1024,11 +1024,11 @@ public static function assignByRefParam(
$by_ref_out_type->parent_nodes += $existing_type->parent_nodes;
}

if ($existing_type->getId() !== 'array<empty, empty>') {
if ($existing_type->getId() !== 'array<never, never>') {
$context->vars_in_scope[$var_id] = $by_ref_out_type;

if (!($stmt_type = $statements_analyzer->node_data->getType($stmt))
|| $stmt_type->isEmpty()
|| $stmt_type->isNever()
) {
$statements_analyzer->node_data->setType($stmt, clone $by_ref_type);
}
Expand All @@ -1043,7 +1043,7 @@ public static function assignByRefParam(

$stmt_type = $statements_analyzer->node_data->getType($stmt);

if (!$stmt_type || $stmt_type->isEmpty()) {
if (!$stmt_type || $stmt_type->isNever()) {
$statements_analyzer->node_data->setType($stmt, clone $by_ref_type);
}

Expand Down
Expand Up @@ -75,9 +75,9 @@ public static function analyze(
$right_type = $nodes->getType($right);
$config = Config::getInstance();

if ($left_type && $left_type->isEmpty()) {
if ($left_type && $left_type->isNever()) {
$left_type = $right_type;
} elseif ($right_type && $right_type->isEmpty()) {
} elseif ($right_type && $right_type->isNever()) {
$right_type = $left_type;
}

Expand Down Expand Up @@ -896,7 +896,7 @@ public static function arithmeticOperation(
$result = $operand1 - $operand2;
} elseif ($operation instanceof PhpParser\Node\Expr\BinaryOp\Mod) {
if ($operand2 === 0) {
return Type::getEmpty();
return Type::getNever();
}

$result = $operand1 % $operand2;
Expand All @@ -916,7 +916,7 @@ public static function arithmeticOperation(
$result = $operand1 >> $operand2;
} elseif ($operation instanceof PhpParser\Node\Expr\BinaryOp\Div) {
if ($operand2 === 0) {
return Type::getEmpty();
return Type::getNever();
}

$result = $operand1 / $operand2;
Expand Down Expand Up @@ -1269,9 +1269,11 @@ private static function analyzePowBetweenIntRange(
$new_result_type = Type::getInt(true, 0);
} elseif ($right_type_part->min_bound === 0 && $right_type_part->max_bound === 0) {
$new_result_type = Type::getInt(true, 1);
} elseif ($right_type_part->isNegative()) {
$new_result_type = Type::getFloat();
} else {
//technically could be a float(INF)...
$new_result_type = Type::getEmpty();
$new_result_type = new Union([new TFloat(), new TLiteralInt(0), new TLiteralInt(1)]);
$new_result_type->from_calculation = true;
}
} else {
//$left_type_part may be a mix of positive, negative and 0
Expand Down Expand Up @@ -1305,7 +1307,7 @@ private static function analyzeModBetweenIntRange(
if ($right_type_part->min_bound !== null && $right_type_part->min_bound === $right_type_part->max_bound) {
//if the second operand is a literal, we can be pretty detailed
if ($right_type_part->max_bound === 0) {
$new_result_type = Type::getEmpty();
$new_result_type = Type::getNever();
} else {
if ($left_type_part->isPositiveOrZero()) {
if ($right_type_part->isPositive()) {
Expand Down
Expand Up @@ -1360,7 +1360,7 @@ private static function coerceValueAfterGatekeeperArgument(
&& $input_atomic_type->value === $param_atomic_type->value
) {
foreach ($input_atomic_type->type_params as $i => $type_param) {
if ($type_param->isEmpty() && isset($param_atomic_type->type_params[$i])) {
if ($type_param->isNever() && isset($param_atomic_type->type_params[$i])) {
$input_type_changed = true;

$input_atomic_type->type_params[$i] = clone $param_atomic_type->type_params[$i];
Expand Down
Expand Up @@ -1207,7 +1207,7 @@ private static function evaluateArbitraryParam(
);

foreach ($context->vars_in_scope[$var_id]->getAtomicTypes() as $type) {
if ($type instanceof TArray && $type->type_params[1]->isEmpty()) {
if ($type instanceof TArray && $type->isEmptyArray()) {
$context->vars_in_scope[$var_id]->removeType('array');
$context->vars_in_scope[$var_id]->addType(
new TArray(
Expand Down
Expand Up @@ -33,9 +33,9 @@
use Psalm\Type\Atomic\TArray;
use Psalm\Type\Atomic\TCallable;
use Psalm\Type\Atomic\TClosure;
use Psalm\Type\Atomic\TEmpty;
use Psalm\Type\Atomic\TKeyedArray;
use Psalm\Type\Atomic\TList;
use Psalm\Type\Atomic\TNever;
use Psalm\Type\Atomic\TNonEmptyArray;
use Psalm\Type\Atomic\TNonEmptyList;
use Psalm\Type\Union;
Expand Down Expand Up @@ -544,8 +544,8 @@ public static function handleByRefArrayAdjustment(
if ($array_atomic_type->count === 0) {
$array_atomic_type = new TArray(
[
new Union([new TEmpty]),
new Union([new TEmpty]),
new Union([new TNever]),
new Union([new TNever]),
]
);
} else {
Expand All @@ -561,8 +561,8 @@ public static function handleByRefArrayAdjustment(
if ($array_atomic_type->count === 0) {
$array_atomic_type = new TArray(
[
new Union([new TEmpty]),
new Union([new TEmpty]),
new Union([new TNever]),
new Union([new TNever]),
]
);
} else {
Expand Down
Expand Up @@ -144,7 +144,7 @@ public static function fetch(
$template_result->lower_bounds[$template_name] = [
'fn-' . $function_id => [
new TemplateBound(
Type::getEmpty()
Type::getNever()
)
]
];
Expand Down Expand Up @@ -390,14 +390,13 @@ private static function getReturnTypeFromCallMapWithArgs(
foreach ($atomic_types['array']->properties as $property) {
// empty, never and possibly undefined can't count for min value
if (!$property->possibly_undefined
&& !$property->isEmpty()
&& !$property->isNever()
) {
$min++;
}

//empty and never can't count for max value because we know keys are undefined
if (!$property->isEmpty() && !$property->isNever()) {
//never can't count for max value because we know keys are undefined
if (!$property->isNever()) {
$max++;
}
}
Expand All @@ -416,8 +415,7 @@ private static function getReturnTypeFromCallMapWithArgs(
}

if ($atomic_types['array'] instanceof TArray
&& $atomic_types['array']->type_params[0]->isEmpty()
&& $atomic_types['array']->type_params[1]->isEmpty()
&& $atomic_types['array']->isEmptyArray()
) {
return Type::getInt(false, 0);
}
Expand Down
Expand Up @@ -27,12 +27,12 @@
use Psalm\Storage\ClassLikeStorage;
use Psalm\Type;
use Psalm\Type\Atomic;
use Psalm\Type\Atomic\TEmpty;
use Psalm\Type\Atomic\TEmptyMixed;
use Psalm\Type\Atomic\TFalse;
use Psalm\Type\Atomic\TGenericObject;
use Psalm\Type\Atomic\TMixed;
use Psalm\Type\Atomic\TNamedObject;
use Psalm\Type\Atomic\TNever;
use Psalm\Type\Atomic\TNonEmptyMixed;
use Psalm\Type\Atomic\TNull;
use Psalm\Type\Atomic\TObject;
Expand Down Expand Up @@ -571,7 +571,7 @@ private static function handleInvalidClass(

case TTemplateParam::class:
case TEmptyMixed::class:
case TEmpty::class:
case TNever::class:
case TMixed::class:
case TNonEmptyMixed::class:
case TObject::class:
Expand Down
Expand Up @@ -588,7 +588,7 @@ public static function replaceTemplateTypes(
} else {
$template_result->lower_bounds[$template_type->param_name] = [
($template_type->defining_class) => [
new TemplateBound(Type::getEmpty())
new TemplateBound(Type::getNever())
]
];
}
Expand Down
Expand Up @@ -516,7 +516,7 @@ function ($bounds) use ($codebase) {
);
} else {
if ($fq_class_name === 'SplObjectStorage') {
$generic_param_type = Type::getEmpty();
$generic_param_type = Type::getNever();
} else {
$generic_param_type = clone array_values($base_type)[0];
}
Expand Down

0 comments on commit fe02697

Please sign in to comment.