1
0
mirror of https://github.com/danog/psalm.git synced 2025-01-21 21:31:13 +01:00

Move string-stored types into named classes

This commit is contained in:
Matthew Brown 2017-01-14 19:06:58 -05:00
parent dc592f7a6e
commit 03141e41c2
44 changed files with 1173 additions and 794 deletions

View File

@ -9,4 +9,4 @@
<!-- This is a vendor file that we don't want to bother linting. -->
<exclude-pattern>src/Psalm/CallMap.php</exclude-pattern>
</ruleset>
</ruleset>

View File

@ -21,6 +21,7 @@ use Psalm\Storage\ClassLikeStorage;
use Psalm\Storage\MethodStorage;
use Psalm\Storage\PropertyStorage;
use Psalm\Type;
use Psalm\Type\Atomic\TNamedObject;
use ReflectionClass;
use ReflectionMethod;
use ReflectionProperty;
@ -176,7 +177,7 @@ abstract class ClassLikeChecker extends SourceChecker implements StatementsSourc
$class_context->vars_possibly_in_scope = $global_context->vars_possibly_in_scope;
}
$class_context->vars_in_scope['$this'] = new Type\Union([new Type\Atomic($this->fq_class_name)]);
$class_context->vars_in_scope['$this'] = new Type\Union([new TNamedObject($this->fq_class_name)]);
}
// set all constants first

View File

@ -214,6 +214,7 @@ class CommentChecker
* @param string $docblock
* @param int $line_number
* @return array Array of the main comment and specials
* @psalm-return array{description:string, specials:array<string, array<mixed, string>>}
*/
public static function parseDocComment($docblock, $line_number = null)
{

View File

@ -341,11 +341,11 @@ class FunctionChecker extends FunctionLikeChecker
if (isset($first_arg->inferredType)) {
if ($first_arg->inferredType->hasArray()) {
$array_type = $first_arg->inferredType->types['array'];
if ($array_type instanceof Type\ObjectLike) {
if ($array_type instanceof Type\Atomic\ObjectLike) {
return $array_type->getGenericTypeParam();
}
if ($array_type instanceof Type\Generic) {
if ($array_type instanceof Type\Atomic\TArray) {
return clone $array_type->type_params[1];
}
} elseif ($first_arg->inferredType->hasScalarType() &&
@ -389,14 +389,14 @@ class FunctionChecker extends FunctionLikeChecker
$first_arg_array_generic = $first_arg
&& isset($first_arg->inferredType)
&& isset($first_arg->inferredType->types['array'])
&& $first_arg->inferredType->types['array'] instanceof Type\Generic
&& $first_arg->inferredType->types['array'] instanceof Type\Atomic\TArray
? $first_arg->inferredType->types['array']
: null;
$first_arg_array_objectlike = $first_arg
&& isset($first_arg->inferredType)
&& isset($first_arg->inferredType->types['array'])
&& $first_arg->inferredType->types['array'] instanceof Type\ObjectLike
&& $first_arg->inferredType->types['array'] instanceof Type\Atomic\ObjectLike
? $first_arg->inferredType->types['array']
: null;
@ -405,11 +405,16 @@ class FunctionChecker extends FunctionLikeChecker
if ($first_arg_array_generic) {
$inner_type = clone $first_arg_array_generic->type_params[1];
} else {
/** @var Type\ObjectLike $first_arg_array_objectlike */
/** @var Type\Atomic\ObjectLike $first_arg_array_objectlike */
$inner_type = $first_arg_array_objectlike->getGenericTypeParam();
}
return new Type\Union([new Type\Generic('array', [Type::getInt(), $inner_type])]);
return new Type\Union([
new Type\Atomic\TArray([
Type::getInt(),
$inner_type
])
]);
}
}
@ -421,7 +426,12 @@ class FunctionChecker extends FunctionLikeChecker
$inner_type = Type::getString();
}
return new Type\Union([new Type\Generic('array', [Type::getInt(), $inner_type])]);
return new Type\Union([
new Type\Atomic\TArray([
Type::getInt(),
$inner_type
])
]);
}
}
@ -435,7 +445,7 @@ class FunctionChecker extends FunctionLikeChecker
}
foreach ($call_arg->value->inferredType->types as $type_part) {
if (!$type_part instanceof Type\Generic) {
if (!$type_part instanceof Type\Atomic\TArray) {
return Type::getArray();
}
@ -452,13 +462,10 @@ class FunctionChecker extends FunctionLikeChecker
if ($inner_value_types) {
return new Type\Union([
new Type\Generic(
'array',
[
Type::combineTypes($inner_key_types),
Type::combineTypes($inner_value_types)
]
)
new Type\Atomic\TArray([
Type::combineTypes($inner_key_types),
Type::combineTypes($inner_value_types)
])
]);
}
}
@ -472,13 +479,10 @@ class FunctionChecker extends FunctionLikeChecker
}
return new Type\Union([
new Type\Generic(
'array',
[
Type::getInt(),
clone $first_arg_array_generic->type_params[1]
]
)
new Type\Atomic\TArray([
Type::getInt(),
clone $first_arg_array_generic->type_params[1]
])
]);
}
@ -497,13 +501,10 @@ class FunctionChecker extends FunctionLikeChecker
}
return new Type\Union([
new Type\Generic(
'array',
[
clone $first_arg_array_generic->type_params[0],
$inner_type
]
)
new Type\Atomic\TArray([
clone $first_arg_array_generic->type_params[0],
$inner_type
])
]);
}
@ -547,7 +548,7 @@ class FunctionChecker extends FunctionLikeChecker
$array_arg_type = $array_arg
&& isset($array_arg->inferredType)
&& isset($array_arg->inferredType->types['array'])
&& $array_arg->inferredType->types['array'] instanceof Type\Generic
&& $array_arg->inferredType->types['array'] instanceof Type\Atomic\TArray
? $array_arg->inferredType->types['array']
: null;
@ -556,7 +557,7 @@ class FunctionChecker extends FunctionLikeChecker
if ($function_call_arg->value instanceof PhpParser\Node\Expr\Closure &&
isset($function_call_arg->value->inferredType) &&
$function_call_arg->value->inferredType->types['Closure'] instanceof Type\Fn
$function_call_arg->value->inferredType->types['Closure'] instanceof Type\Atomic\Fn
) {
$closure_return_type = $function_call_arg->value->inferredType->types['Closure']->return_type;
@ -576,12 +577,22 @@ class FunctionChecker extends FunctionLikeChecker
if ($call_map_key === 'array_map') {
$inner_type = clone $closure_return_type;
return new Type\Union([new Type\Generic('array', [$key_type, $inner_type])]);
return new Type\Union([
new Type\Atomic\TArray([
$key_type,
$inner_type
])
]);
}
if ($array_arg_type) {
$inner_type = clone $array_arg_type->type_params[1];
return new Type\Union([new Type\Generic('array', [$key_type, $inner_type])]);
return new Type\Union([
new Type\Atomic\TArray([
$key_type,
$inner_type
])
]);
}
} elseif ($function_call_arg->value instanceof PhpParser\Node\Scalar\String_) {
$mapped_function_id = strtolower($function_call_arg->value->value);
@ -591,7 +602,12 @@ class FunctionChecker extends FunctionLikeChecker
if (isset($call_map[$mapped_function_id][0])) {
if ($call_map[$mapped_function_id][0]) {
$mapped_function_return = Type::parseString($call_map[$mapped_function_id][0]);
return new Type\Union([new Type\Generic('array', [Type::getInt(), $mapped_function_return])]);
return new Type\Union([
new Type\Atomic\TArray([
Type::getInt(),
$mapped_function_return
])
]);
}
} else {
// @todo handle array_map('some_custom_function', $arr)

View File

@ -28,6 +28,7 @@ use Psalm\StatementsSource;
use Psalm\Storage\FunctionLikeStorage;
use Psalm\Storage\MethodStorage;
use Psalm\Type;
use Psalm\Type\Atomic\TNamedObject;
abstract class FunctionLikeChecker extends SourceChecker implements StatementsSource
{
@ -121,7 +122,7 @@ abstract class FunctionLikeChecker extends SourceChecker implements StatementsSo
return null;
}
} elseif ($context->self) {
$context->vars_in_scope['$this'] = new Type\Union([new Type\Atomic($context->self)]);
$context->vars_in_scope['$this'] = new Type\Union([new TNamedObject($context->self)]);
}
$declaring_method_id = (string)MethodChecker::getDeclaringMethodId($method_id);
@ -239,7 +240,7 @@ abstract class FunctionLikeChecker extends SourceChecker implements StatementsSo
/** @var PhpParser\Node\Expr\Closure $this->function */
$this->function->inferredType = new Type\Union([
new Type\Fn(
new Type\Atomic\Fn(
'Closure',
$storage->params,
$storage->return_type ?: Type::getMixed()
@ -324,7 +325,7 @@ abstract class FunctionLikeChecker extends SourceChecker implements StatementsSo
);
if ($closure_return_types && $this->function->inferredType) {
/** @var Type\Fn */
/** @var Type\Atomic\Fn */
$closure_atomic = $this->function->inferredType->types['Closure'];
$closure_atomic->return_type = new Type\Union($closure_return_types);
}
@ -1006,7 +1007,7 @@ abstract class FunctionLikeChecker extends SourceChecker implements StatementsSo
$existing_param_type_nullable = $function_signature_param->is_nullable;
if ($existing_param_type_nullable && !$new_param_type->isNullable()) {
$new_param_type->types['null'] = new Type\Atomic('null');
$new_param_type->types['null'] = new Type\Atomic\TNull();
}
$function_signature_param->signature_type = $function_signature_param->type;
@ -1083,13 +1084,10 @@ abstract class FunctionLikeChecker extends SourceChecker implements StatementsSo
if ($param->variadic) {
$param_type = new Type\Union([
new Type\GenericArray(
'array',
[
Type::getInt(),
$param_type
]
)
new Type\Atomic\TArray([
Type::getInt(),
$param_type
])
]);
}
}

View File

@ -64,7 +64,7 @@ class ForeachChecker
continue;
}
if ($return_type instanceof Type\Generic) {
if ($return_type instanceof Type\Atomic\TArray || $return_type instanceof Type\Atomic\TGenericObject) {
$value_index = count($return_type->type_params) - 1;
$value_type_part = $return_type->type_params[$value_index];
@ -86,88 +86,66 @@ class ForeachChecker
continue;
}
switch ($return_type->value) {
case 'mixed':
case 'empty':
case 'Generator':
$value_type = Type::getMixed();
break;
if ($return_type instanceof Type\Atomic\Scalar ||
$return_type instanceof Type\Atomic\TNull ||
$return_type instanceof Type\Atomic\TVoid
) {
if (IssueBuffer::accepts(
new InvalidIterator(
'Cannot iterate over ' . $return_type->getKey(),
new CodeLocation($statements_checker->getSource(), $stmt->expr)
),
$statements_checker->getSuppressedIssues()
)) {
return false;
}
case 'array':
case 'object':
$value_type = Type::getMixed();
break;
case 'null':
if (IssueBuffer::accepts(
new NullReference(
'Cannot iterate over ' . $return_type->value,
new CodeLocation($statements_checker->getSource(), $stmt->expr)
),
$statements_checker->getSuppressedIssues()
)) {
return false;
}
$value_type = Type::getMixed();
break;
case 'string':
case 'void':
case 'int':
case 'bool':
case 'false':
if (IssueBuffer::accepts(
new InvalidIterator(
'Cannot iterate over ' . $return_type->value,
new CodeLocation($statements_checker->getSource(), $stmt->expr)
),
$statements_checker->getSuppressedIssues()
)) {
return false;
}
$value_type = Type::getMixed();
break;
default:
if ($return_type->value !== 'Traversable' &&
$return_type->value !== $statements_checker->getClassName()
) {
if (ClassLikeChecker::checkFullyQualifiedClassLikeName(
$return_type->value,
$statements_checker->getFileChecker(),
new CodeLocation($statements_checker->getSource(), $stmt->expr),
$statements_checker->getSuppressedIssues()
) === false) {
return false;
}
}
if (ClassChecker::classImplements(
$value_type = Type::getMixed();
} elseif ($return_type instanceof Type\Atomic\TArray ||
$return_type instanceof Type\Atomic\TObject ||
$return_type instanceof Type\Atomic\TMixed ||
$return_type instanceof Type\Atomic\TEmpty ||
($return_type instanceof Type\Atomic\TNamedObject && $return_type->value === 'Generator')
) {
$value_type = Type::getMixed();
} elseif ($return_type instanceof Type\Atomic\TNamedObject) {
if ($return_type->value !== 'Traversable' &&
$return_type->value !== $statements_checker->getClassName()
) {
if (ClassLikeChecker::checkFullyQualifiedClassLikeName(
$return_type->value,
'Iterator'
)) {
$iterator_method = $return_type->value . '::current';
$iterator_class_type = MethodChecker::getMethodReturnType($iterator_method);
if ($iterator_class_type) {
$value_type_part = ExpressionChecker::fleshOutTypes(
$iterator_class_type,
[],
$return_type->value,
$iterator_method
);
if (!$value_type) {
$value_type = $value_type_part;
} else {
$value_type = Type::combineUnionTypes($value_type, $value_type_part);
}
} else {
$value_type = Type::getMixed();
}
$statements_checker->getFileChecker(),
new CodeLocation($statements_checker->getSource(), $stmt->expr),
$statements_checker->getSuppressedIssues()
) === false) {
return false;
}
}
if (ClassChecker::classImplements(
$return_type->value,
'Iterator'
)) {
$iterator_method = $return_type->value . '::current';
$iterator_class_type = MethodChecker::getMethodReturnType($iterator_method);
if ($iterator_class_type) {
$value_type_part = ExpressionChecker::fleshOutTypes(
$iterator_class_type,
[],
$return_type->value,
$iterator_method
);
if (!$value_type) {
$value_type = $value_type_part;
} else {
$value_type = Type::combineUnionTypes($value_type, $value_type_part);
}
} else {
$value_type = Type::getMixed();
}
}
}
}
}

View File

@ -32,8 +32,8 @@ class SwitchChecker
return false;
}
if (isset($stmt->cond->inferredType) && array_values($stmt->cond->inferredType->types)[0] instanceof Type\T) {
/** @var Type\T */
if (isset($stmt->cond->inferredType) && array_values($stmt->cond->inferredType->types)[0] instanceof Type\Atomic\T) {
/** @var Type\Atomic\T */
$type_type = array_values($stmt->cond->inferredType->types)[0];
$type_candidate_var = $type_type->typeof;
}

View File

@ -8,6 +8,7 @@ use Psalm\Checker\ScopeChecker;
use Psalm\Checker\StatementsChecker;
use Psalm\Context;
use Psalm\Type;
use Psalm\Type\Atomic\TNamedObject;
class TryChecker
{
@ -62,7 +63,7 @@ class TryChecker
* @return Type\Atomic
*/
function ($fq_catch_class) {
return new Type\Atomic($fq_catch_class);
return new TNamedObject($fq_catch_class);
},
$fq_catch_classes
)

View File

@ -31,6 +31,26 @@ use Psalm\Issue\UndefinedPropertyAssignment;
use Psalm\Issue\UndefinedThisPropertyAssignment;
use Psalm\IssueBuffer;
use Psalm\Type;
use Psalm\Type\Atomic\Generic;
use Psalm\Type\Atomic\ObjectLike;
use Psalm\Type\Atomic\Scalar;
use Psalm\Type\Atomic\TNumeric;
use Psalm\Type\Atomic\TInt;
use Psalm\Type\Atomic\TVoid;
use Psalm\Type\Atomic\TFloat;
use Psalm\Type\Atomic\TString;
use Psalm\Type\Atomic\TBool;
use Psalm\Type\Atomic\TFalse;
use Psalm\Type\Atomic\TNull;
use Psalm\Type\Atomic\TEmpty;
use Psalm\Type\Atomic\TArray;
use Psalm\Type\Atomic\TMixed;
use Psalm\Type\Atomic\TObject;
use Psalm\Type\Atomic\TResource;
use Psalm\Type\Atomic\TCallable;
use Psalm\Type\Atomic\TNamedObject;
use Psalm\Type\Atomic\TGenericObject;
use Psalm\Type\Atomic\TNumericString;
class AssignmentChecker
{
@ -156,11 +176,11 @@ class AssignmentChecker
$statements_checker->registerVariable($list_var_id, $var->getLine());
if (isset($assign_value_type->types['array'])) {
if ($assign_value_type->types['array'] instanceof Type\Generic) {
if ($assign_value_type->types['array'] instanceof Type\Atomic\TArray) {
$context->vars_in_scope[$list_var_id] = clone $assign_value_type->types['array']->type_params[1];
continue;
} elseif ($assign_value_type->types['array'] instanceof Type\ObjectLike) {
} elseif ($assign_value_type->types['array'] instanceof Type\Atomic\ObjectLike) {
if ($assign_var_item->key
&& $assign_var_item->key instanceof PhpParser\Node\Scalar\String_
&& isset($assign_value_type->types['array']->properties[$assign_var_item->key->value])
@ -412,14 +432,15 @@ class AssignmentChecker
$has_regular_setter = false;
foreach ($lhs_type->types as $lhs_type_part) {
if ($lhs_type_part->isNull()) {
if ($lhs_type_part instanceof TNull) {
continue;
}
if (!$lhs_type_part->isObjectType()) {
if (!$lhs_type_part instanceof TObject && !$lhs_type_part instanceof TNamedObject) {
if (IssueBuffer::accepts(
new InvalidPropertyAssignment(
$lhs_var_id . ' with possible non-object type \'' . $lhs_type_part . '\' cannot treated as an object',
$lhs_var_id . ' with possible non-object type \'' . $lhs_type_part .
'\' cannot treated as an object',
new CodeLocation($statements_checker->getSource(), $stmt->var)
),
$statements_checker->getSuppressedIssues()
@ -433,13 +454,15 @@ class AssignmentChecker
// stdClass and SimpleXMLElement are special cases where we cannot infer the return types
// but we don't want to throw an error
// Hack has a similar issue: https://github.com/facebook/hhvm/issues/5164
if ($lhs_type_part->isObject() ||
in_array(
strtolower($lhs_type_part->value),
['stdclass', 'simplexmlelement', 'dateinterval', 'domdocument', 'domnode']
if ($lhs_type_part instanceof TObject ||
($lhs_type_part instanceof TNamedObject &&
in_array(
strtolower($lhs_type_part->value),
['stdclass', 'simplexmlelement', 'dateinterval', 'domdocument', 'domnode']
)
)
) {
if (strtolower($lhs_type_part->value) === 'stdclass') {
if ($lhs_type_part instanceof TNamedObject && strtolower($lhs_type_part->value) === 'stdclass') {
$context->vars_in_scope[$var_id] = $assignment_value_type;
} else {
$context->vars_in_scope[$var_id] = Type::getMixed();
@ -448,17 +471,13 @@ class AssignmentChecker
return null;
}
if (!$lhs_type_part->isObject() && MethodChecker::methodExists($lhs_type_part . '::__set')) {
if (MethodChecker::methodExists($lhs_type_part . '::__set')) {
$context->vars_in_scope[$var_id] = Type::getMixed();
continue;
}
$has_regular_setter = true;
if ($lhs_type_part->isObject()) {
continue;
}
if (ExpressionChecker::isMock($lhs_type_part->value)) {
$context->vars_in_scope[$var_id] = Type::getMixed();
@ -862,8 +881,8 @@ class AssignmentChecker
// do nothing
} elseif ($is_string) {
foreach ($assignment_value_type->types as $value_type) {
if (!$value_type->isString()) {
if ($value_type->isMixed()) {
if (!$value_type instanceof TString) {
if ($value_type instanceof TMixed) {
if (IssueBuffer::accepts(
new MixedStringOffsetAssignment(
'Cannot assign a mixed variable to a string offset for ' . $var_id,
@ -929,9 +948,9 @@ class AssignmentChecker
}
if (!$nesting) {
/** @var Type\Generic|null */
/** @var Type\Atomic\TArray|null */
$array_type = isset($context->vars_in_scope[$var_id]->types['array'])
&& $context->vars_in_scope[$var_id]->types['array'] instanceof Type\Generic
&& $context->vars_in_scope[$var_id]->types['array'] instanceof Type\Atomic\TArray
? $context->vars_in_scope[$var_id]->types['array']
: null;
@ -942,22 +961,16 @@ class AssignmentChecker
|| ($array_type && $array_type->type_params[0]->isEmpty()))
) {
$assignment_value_type = new Type\Union([
new Type\ObjectLike(
'array',
[
$assignment_key_value => $assignment_value_type
]
)
new Type\Atomic\ObjectLike([
$assignment_key_value => $assignment_value_type
])
]);
} else {
$assignment_value_type = new Type\Union([
new Type\Generic(
'array',
[
$assignment_key_type,
$assignment_value_type
]
)
new Type\Atomic\TArray([
$assignment_key_type,
$assignment_value_type
])
]);
}

View File

@ -31,6 +31,26 @@ use Psalm\Issue\TypeCoercion;
use Psalm\Issue\UndefinedFunction;
use Psalm\IssueBuffer;
use Psalm\Type;
use Psalm\Type\Atomic\Generic;
use Psalm\Type\Atomic\ObjectLike;
use Psalm\Type\Atomic\Scalar;
use Psalm\Type\Atomic\TNumeric;
use Psalm\Type\Atomic\TInt;
use Psalm\Type\Atomic\TVoid;
use Psalm\Type\Atomic\TFloat;
use Psalm\Type\Atomic\TString;
use Psalm\Type\Atomic\TBool;
use Psalm\Type\Atomic\TFalse;
use Psalm\Type\Atomic\TNull;
use Psalm\Type\Atomic\TEmpty;
use Psalm\Type\Atomic\TArray;
use Psalm\Type\Atomic\TMixed;
use Psalm\Type\Atomic\TObject;
use Psalm\Type\Atomic\TResource;
use Psalm\Type\Atomic\TCallable;
use Psalm\Type\Atomic\TNamedObject;
use Psalm\Type\Atomic\TGenericObject;
use Psalm\Type\Atomic\TNumericString;
class CallChecker
{
@ -110,7 +130,7 @@ class CallChecker
if (isset($stmt->name->inferredType)) {
foreach ($stmt->name->inferredType->types as $var_type_part) {
if ($var_type_part instanceof Type\Fn) {
if ($var_type_part instanceof Type\Atomic\Fn) {
$function_params = $var_type_part->params;
if ($var_type_part->return_type) {
@ -123,9 +143,9 @@ class CallChecker
$stmt->inferredType = $var_type_part->return_type;
}
}
} elseif (!$var_type_part->isMixed() &&
$var_type_part->value !== 'Closure' &&
!$var_type_part->isCallable()
} elseif (!$var_type_part instanceof TMixed &&
(!$var_type_part instanceof TNamedObject || $var_type_part->value !== 'Closure') &&
!$var_type_part instanceof TCallable
) {
$var_id = ExpressionChecker::getVarId(
$stmt->name,
@ -236,7 +256,7 @@ class CallChecker
$var = $stmt->args[0]->value;
if ($var instanceof PhpParser\Node\Expr\Variable && is_string($var->name)) {
$stmt->inferredType = new Type\Union([new Type\T('$' . $var->name)]);
$stmt->inferredType = new Type\Union([new Type\Atomic\T('$' . $var->name)]);
}
}
@ -308,7 +328,7 @@ class CallChecker
}
if ($fq_class_name) {
$stmt->inferredType = new Type\Union([new Type\Atomic($fq_class_name)]);
$stmt->inferredType = new Type\Union([new TNamedObject($fq_class_name)]);
if (strtolower($fq_class_name) !== 'stdclass' &&
($class_checked || ClassChecker::classExists($fq_class_name, $file_checker)) &&
@ -353,7 +373,7 @@ class CallChecker
$value_type = null;
foreach ($first_arg_type->types as $type) {
if ($type instanceof Type\Generic) {
if ($type instanceof Type\Atomic\TArray) {
$first_type_param = count($type->type_params) ? $type->type_params[0] : null;
$last_type_param = $type->type_params[count($type->type_params) - 1];
@ -380,7 +400,7 @@ class CallChecker
}
$stmt->inferredType = new Type\Union([
new Type\Generic(
new Type\Atomic\TGenericObject(
$fq_class_name,
[
$key_type,
@ -489,136 +509,140 @@ class CallChecker
$return_type = null;
foreach ($class_type->types as $type) {
if (!$type instanceof TNamedObject) {
switch (get_class($type)) {
case 'Psalm\\Type\\Atomic\\TNull':
if (IssueBuffer::accepts(
new NullReference(
'Cannot call method ' . $stmt->name . ' on possibly null variable ' . $var_id,
new CodeLocation($statements_checker->getSource(), $stmt)
),
$statements_checker->getSuppressedIssues()
)) {
return false;
}
break;
case 'Psalm\\Type\\Atomic\\TInt':
case 'Psalm\\Type\\Atomic\\TBool':
case 'Psalm\\Type\\Atomic\\TFalse':
case 'Psalm\\Type\\Atomic\\TArray':
case 'Psalm\\Type\\Atomic\\TString':
case 'Psalm\\Type\\Atomic\\TNumericString':
if (IssueBuffer::accepts(
new InvalidArgument(
'Cannot call method ' . $stmt->name . ' on ' . $class_type . ' variable ' . $var_id,
new CodeLocation($statements_checker->getSource(), $stmt)
),
$statements_checker->getSuppressedIssues()
)) {
return false;
}
break;
case 'Psalm\\Type\\Atomic\\TMixed':
case 'Psalm\\Type\\Atomic\\TObject':
if (IssueBuffer::accepts(
new MixedMethodCall(
'Cannot call method ' . $stmt->name . ' on a mixed variable ' . $var_id,
new CodeLocation($statements_checker->getSource(), $stmt)
),
$statements_checker->getSuppressedIssues()
)) {
return false;
}
break;
}
continue;
}
$fq_class_name = $type->value;
$is_mock = ExpressionChecker::isMock($fq_class_name);
$has_mock = $has_mock || $is_mock;
switch ($fq_class_name) {
case 'null':
if (IssueBuffer::accepts(
new NullReference(
'Cannot call method ' . $stmt->name . ' on possibly null variable ' . $var_id,
new CodeLocation($statements_checker->getSource(), $stmt)
),
$statements_checker->getSuppressedIssues()
)) {
return false;
}
break;
if ($fq_class_name === 'static') {
$fq_class_name = (string) $context->self;
}
case 'int':
case 'bool':
case 'false':
case 'array':
case 'string':
if (IssueBuffer::accepts(
new InvalidArgument(
'Cannot call method ' . $stmt->name . ' on ' . $class_type . ' variable ' . $var_id,
new CodeLocation($statements_checker->getSource(), $stmt)
),
$statements_checker->getSuppressedIssues()
)) {
return false;
}
break;
if ($is_mock ||
$context->isPhantomClass($fq_class_name)
) {
$return_type = Type::getMixed();
continue;
}
case 'mixed':
case 'object':
if (IssueBuffer::accepts(
new MixedMethodCall(
'Cannot call method ' . $stmt->name . ' on a mixed variable ' . $var_id,
new CodeLocation($statements_checker->getSource(), $stmt)
),
$statements_checker->getSuppressedIssues()
)) {
return false;
}
break;
$does_class_exist = ClassLikeChecker::checkFullyQualifiedClassLikeName(
$fq_class_name,
$statements_checker->getFileChecker(),
new CodeLocation($statements_checker->getSource(), $stmt->var),
$statements_checker->getSuppressedIssues(),
true
);
case 'static':
$fq_class_name = (string) $context->self;
// fall through to default
if (!$does_class_exist) {
return $does_class_exist;
}
default:
if ($is_mock ||
$context->isPhantomClass($fq_class_name)
) {
$return_type = Type::getMixed();
continue;
}
if (MethodChecker::methodExists($fq_class_name . '::__call')) {
$return_type = Type::getMixed();
continue;
}
$does_class_exist = ClassLikeChecker::checkFullyQualifiedClassLikeName(
$fq_class_name,
$statements_checker->getFileChecker(),
new CodeLocation($statements_checker->getSource(), $stmt->var),
$statements_checker->getSuppressedIssues(),
true
);
$method_id = $fq_class_name . '::' . strtolower($stmt->name);
$cased_method_id = $fq_class_name . '::' . $stmt->name;
if (!$does_class_exist) {
return $does_class_exist;
}
$does_method_exist = MethodChecker::checkMethodExists(
$cased_method_id,
new CodeLocation($statements_checker->getSource(), $stmt),
$statements_checker->getSuppressedIssues()
);
if (MethodChecker::methodExists($fq_class_name . '::__call')) {
$return_type = Type::getMixed();
continue;
}
if (!$does_method_exist) {
return $does_method_exist;
}
$method_id = $fq_class_name . '::' . strtolower($stmt->name);
$cased_method_id = $fq_class_name . '::' . $stmt->name;
if (FunctionChecker::inCallMap($cased_method_id)) {
$return_type_candidate = FunctionChecker::getReturnTypeFromCallMap($method_id);
} else {
if (MethodChecker::checkMethodVisibility(
$method_id,
$context->self,
$statements_checker->getSource(),
new CodeLocation($statements_checker->getSource(), $stmt),
$statements_checker->getSuppressedIssues()
) === false) {
return false;
}
$does_method_exist = MethodChecker::checkMethodExists(
$cased_method_id,
new CodeLocation($statements_checker->getSource(), $stmt),
$statements_checker->getSuppressedIssues()
);
if (MethodChecker::checkMethodNotDeprecated(
$method_id,
new CodeLocation($statements_checker->getSource(), $stmt),
$statements_checker->getSuppressedIssues()
) === false) {
return false;
}
if (!$does_method_exist) {
return $does_method_exist;
}
$return_type_candidate = MethodChecker::getMethodReturnType($method_id);
}
if (FunctionChecker::inCallMap($cased_method_id)) {
$return_type_candidate = FunctionChecker::getReturnTypeFromCallMap($method_id);
} else {
if (MethodChecker::checkMethodVisibility(
$method_id,
$context->self,
$statements_checker->getSource(),
new CodeLocation($statements_checker->getSource(), $stmt),
$statements_checker->getSuppressedIssues()
) === false) {
return false;
}
if ($return_type_candidate) {
$return_type_candidate = ExpressionChecker::fleshOutTypes(
$return_type_candidate,
$stmt->args,
$fq_class_name,
$method_id
);
if (MethodChecker::checkMethodNotDeprecated(
$method_id,
new CodeLocation($statements_checker->getSource(), $stmt),
$statements_checker->getSuppressedIssues()
) === false) {
return false;
}
$return_type_candidate = MethodChecker::getMethodReturnType($method_id);
}
if ($return_type_candidate) {
$return_type_candidate = ExpressionChecker::fleshOutTypes(
$return_type_candidate,
$stmt->args,
$fq_class_name,
$method_id
);
if (!$return_type) {
$return_type = $return_type_candidate;
} else {
$return_type = Type::combineUnionTypes($return_type_candidate, $return_type);
}
} else {
$return_type = Type::getMixed();
}
if (!$return_type) {
$return_type = $return_type_candidate;
} else {
$return_type = Type::combineUnionTypes($return_type_candidate, $return_type);
}
} else {
$return_type = Type::getMixed();
}
}
@ -749,7 +773,7 @@ class CallChecker
}
if ($fq_class_name) {
$lhs_type = new Type\Union([new Type\Atomic($fq_class_name)]);
$lhs_type = new Type\Union([new TNamedObject($fq_class_name)]);
}
} else {
ExpressionChecker::analyze($statements_checker, $stmt->class, $context);
@ -765,6 +789,11 @@ class CallChecker
$has_mock = false;
foreach ($lhs_type->types as $lhs_type_part) {
if (!$lhs_type_part instanceof TNamedObject) {
// @todo deal with it
continue;
}
$fq_class_name = $lhs_type_part->value;
$is_mock = ExpressionChecker::isMock($fq_class_name);
@ -1134,7 +1163,7 @@ class CallChecker
$array_arg_types[] = $array_arg
&& isset($array_arg->inferredType)
&& isset($array_arg->inferredType->types['array'])
&& $array_arg->inferredType->types['array'] instanceof Type\Generic
&& $array_arg->inferredType->types['array'] instanceof Type\Atomic\TArray
? $array_arg->inferredType->types['array']
: null;
}
@ -1151,7 +1180,7 @@ class CallChecker
$expected_closure_param_count = $method_id === 'array_filter' ? 1 : count($array_arg_types);
foreach ($closure_arg_type->types as $closure_type) {
if (!$closure_type instanceof Type\Fn) {
if (!$closure_type instanceof Type\Atomic\Fn) {
continue;
}
@ -1189,7 +1218,7 @@ class CallChecker
continue;
}
/** @var Type\Generic */
/** @var Type\Atomic\TArray */
$array_arg_type = $array_arg_types[$i];
$input_type = $array_arg_type->type_params[1];

View File

@ -31,6 +31,26 @@ use Psalm\Issue\UndefinedPropertyFetch;
use Psalm\Issue\UndefinedThisPropertyFetch;
use Psalm\IssueBuffer;
use Psalm\Type;
use Psalm\Type\Atomic\Generic;
use Psalm\Type\Atomic\ObjectLike;
use Psalm\Type\Atomic\Scalar;
use Psalm\Type\Atomic\TNumeric;
use Psalm\Type\Atomic\TInt;
use Psalm\Type\Atomic\TVoid;
use Psalm\Type\Atomic\TFloat;
use Psalm\Type\Atomic\TString;
use Psalm\Type\Atomic\TBool;
use Psalm\Type\Atomic\TFalse;
use Psalm\Type\Atomic\TNull;
use Psalm\Type\Atomic\TEmpty;
use Psalm\Type\Atomic\TArray;
use Psalm\Type\Atomic\TMixed;
use Psalm\Type\Atomic\TObject;
use Psalm\Type\Atomic\TResource;
use Psalm\Type\Atomic\TCallable;
use Psalm\Type\Atomic\TNamedObject;
use Psalm\Type\Atomic\TGenericObject;
use Psalm\Type\Atomic\TNumericString;
class FetchChecker
{
@ -153,7 +173,7 @@ class FetchChecker
}
foreach ($stmt_var_type->types as $lhs_type_part) {
if ($lhs_type_part->isNull()) {
if ($lhs_type_part instanceof TNull) {
continue;
}
@ -180,8 +200,10 @@ class FetchChecker
// stdClass and SimpleXMLElement are special cases where we cannot infer the return types
// but we don't want to throw an error
// Hack has a similar issue: https://github.com/facebook/hhvm/issues/5164
if ($lhs_type_part->isObject() ||
in_array(strtolower($lhs_type_part->value), ['stdclass', 'simplexmlelement'])
if ($lhs_type_part instanceof TObject ||
($lhs_type_part instanceof TNamedObject &&
in_array(strtolower($lhs_type_part->value), ['stdclass', 'simplexmlelement'])
)
) {
$stmt->inferredType = Type::getMixed();
continue;
@ -189,6 +211,11 @@ class FetchChecker
$file_checker = $statements_checker->getFileChecker();
if (!$lhs_type_part instanceof TNamedObject) {
// @todo deal with this
continue;
}
if (!ClassChecker::classExists($lhs_type_part->value, $file_checker)) {
if (InterfaceChecker::interfaceExists($lhs_type_part->value, $file_checker)) {
if (IssueBuffer::accepts(
@ -544,7 +571,7 @@ class FetchChecker
}
}
$stmt->class->inferredType = $fq_class_name ? new Type\Union([new Type\Atomic($fq_class_name)]) : null;
$stmt->class->inferredType = $fq_class_name ? new Type\Union([new TNamedObject($fq_class_name)]) : null;
}
if ($fq_class_name &&
@ -685,26 +712,23 @@ class FetchChecker
if (!$keyed_assignment_type || $keyed_assignment_type->isEmpty()) {
if (!$assignment_key_type->isMixed() && !$assignment_key_type->hasInt() && $assignment_key_value) {
$keyed_assignment_type = new Type\Union([
new Type\ObjectLike(
'array',
[
$assignment_key_value => $assignment_value_type
]
)
new Type\Atomic\ObjectLike([
$assignment_key_value => $assignment_value_type
])
]);
} else {
$keyed_assignment_type = Type::getEmptyArray();
/** @var Type\Generic */
/** @var Type\Atomic\TArray */
$keyed_assignment_type_array = $keyed_assignment_type->types['array'];
$keyed_assignment_type_array->type_params[0] = $assignment_key_type;
$keyed_assignment_type_array->type_params[1] = $assignment_value_type;
}
} else {
foreach ($keyed_assignment_type->types as &$type) {
if ($type->isScalarType() && !$type->isString()) {
if ($type instanceof TInt || $type instanceof TFloat || $type instanceof TBool) {
if (IssueBuffer::accepts(
new InvalidArrayAssignment(
'Cannot assign value on variable ' . $var_id . ' of scalar type ' . $type->value,
'Cannot assign value on variable ' . $var_id . ' of scalar type ' . $type->getKey(),
new CodeLocation($statements_checker->getSource(), $stmt)
),
$statements_checker->getSuppressedIssues()
@ -715,7 +739,7 @@ class FetchChecker
continue;
}
if ($type instanceof Type\Generic) {
if ($type instanceof TString || $type instanceof Type\Atomic\TArray) {
$refined_type = self::refineArrayType(
$statements_checker,
$type,
@ -734,7 +758,7 @@ class FetchChecker
}
$type = $refined_type;
} elseif ($type instanceof Type\ObjectLike && $assignment_key_value) {
} elseif ($type instanceof Type\Atomic\ObjectLike && $assignment_key_value) {
if (isset($type->properties[$assignment_key_value])) {
$type->properties[$assignment_key_value] = Type::combineUnionTypes(
$type->properties[$assignment_key_value],
@ -768,10 +792,10 @@ class FetchChecker
$var_type = $stmt->var->inferredType;
foreach ($var_type->types as &$type) {
if ($type instanceof Type\Generic || $type instanceof Type\ObjectLike) {
if ($type instanceof Type\Atomic\TArray || $type instanceof Type\Atomic\ObjectLike) {
$value_index = null;
if ($type instanceof Type\Generic) {
if ($type instanceof Type\Atomic\TArray) {
// create a union type to pass back to the statement
$value_index = count($type->type_params) - 1;
@ -821,16 +845,17 @@ class FetchChecker
}
if ($array_var_id === $var_id) {
if ($type instanceof Type\ObjectLike ||
($type->isGenericArray() && !$key_type->hasInt() && $type->type_params[1]->isEmpty())
if ($type instanceof Type\Atomic\ObjectLike ||
(
$type instanceof TArray &&
!$key_type->hasInt() &&
$type->type_params[1]->isEmpty()
)
) {
$properties = $key_value ? [$key_value => $keyed_assignment_type] : [];
$assignment_type = new Type\Union([
new Type\ObjectLike(
'array',
$properties
)
new Type\Atomic\ObjectLike($properties)
]);
} else {
if (!$keyed_assignment_type) {
@ -838,13 +863,10 @@ class FetchChecker
}
$assignment_type = new Type\Union([
new Type\Generic(
'array',
[
$key_type,
$keyed_assignment_type
]
)
new Type\Atomic\TArray([
$key_type,
$keyed_assignment_type
])
]);
}
@ -858,7 +880,9 @@ class FetchChecker
}
}
if ($type instanceof Type\Generic && $type->type_params[$value_index]->isEmpty()) {
if ($type instanceof Type\Atomic\TArray &&
$type->type_params[$value_index]->isEmpty()
) {
$empty_type = Type::getEmptyArray();
if (!isset($stmt->inferredType)) {
@ -873,14 +897,14 @@ class FetchChecker
for ($i = 0; $i < $nesting + 1; $i++) {
if (isset($array_type->types['array']) &&
$array_type->types['array'] instanceof Type\Generic
$array_type->types['array'] instanceof Type\Atomic\TArray
) {
$atomic_array = $array_type->types['array'];
if ($i < $nesting) {
if ($atomic_array->type_params[1]->isEmpty()) {
$new_empty = clone $empty_type;
/** @var Type\Generic */
/** @var Type\Atomic\TArray */
$new_atomic_empty = $new_empty->types['array'];
$new_atomic_empty->type_params[0] = $key_type;
@ -901,9 +925,9 @@ class FetchChecker
$context->vars_in_scope[$var_id] = $context_type;
}
} elseif ($type instanceof Type\Generic && $value_index !== null) {
} elseif ($type instanceof Type\Atomic\TArray && $value_index !== null) {
$stmt->inferredType = $type->type_params[$value_index];
} elseif ($type instanceof Type\ObjectLike) {
} elseif ($type instanceof Type\Atomic\ObjectLike) {
if ($key_value && isset($type->properties[$key_value])) {
$stmt->inferredType = clone $type->properties[$key_value];
} elseif ($key_type->hasInt()) {
@ -932,7 +956,7 @@ class FetchChecker
}
}
}
} elseif ($type->isString()) {
} elseif ($type instanceof TString) {
if ($key_type) {
$key_type = Type::combineUnionTypes($key_type, Type::getInt());
} else {
@ -946,7 +970,7 @@ class FetchChecker
}
$stmt->inferredType = Type::getString();
} elseif ($type->isNull()) {
} elseif ($type instanceof TNull) {
if (IssueBuffer::accepts(
new NullArrayAccess(
'Cannot access array value on possibly null variable ' . $array_var_id . ' of type ' . $var_type,
@ -961,7 +985,7 @@ class FetchChecker
}
continue;
}
} elseif ($type->isMixed() || $type->isEmpty()) {
} elseif ($type instanceof TMixed || $type instanceof TEmpty) {
if (IssueBuffer::accepts(
new MixedArrayAccess(
'Cannot access array value on mixed variable ' . $array_var_id,
@ -972,12 +996,15 @@ class FetchChecker
$stmt->inferredType = Type::getMixed();
break;
}
} elseif (strtolower($type->value) !== 'simplexmlelement' &&
!ClassChecker::classImplements($type->value, 'ArrayAccess')
} elseif (!$type instanceof TNamedObject ||
(strtolower($type->value) !== 'simplexmlelement' &&
!ClassChecker::classImplements($type->value, 'ArrayAccess')
)
) {
if (IssueBuffer::accepts(
new InvalidArrayAccess(
'Cannot access array value on non-array variable ' . $array_var_id . ' of type ' . $var_type,
'Cannot access array value on non-array variable ' .
$array_var_id . ' of type ' . $var_type,
new CodeLocation($statements_checker->getSource(), $stmt)
),
$statements_checker->getSuppressedIssues()
@ -999,15 +1026,15 @@ class FetchChecker
if (!$key_type) {
$key_type = new Type\Union([
new Type\Atomic('int'),
new Type\Atomic('string')
new TInt,
new TString
]);
}
if ($stmt->dim) {
if (isset($stmt->dim->inferredType) && $key_type && !$key_type->isEmpty()) {
foreach ($stmt->dim->inferredType->types as $at) {
if (($at->isMixed() || $at->isEmpty()) &&
if (($at instanceof TMixed || $at instanceof TEmpty) &&
$inferred_key_type &&
!$inferred_key_type->isMixed() &&
!$inferred_key_type->isEmpty()
@ -1058,7 +1085,7 @@ class FetchChecker
$var_id,
CodeLocation $code_location
) {
if ($type->value === 'null') {
if ($type instanceof TNull) {
if (IssueBuffer::accepts(
new NullReference(
'Cannot assign value on possibly null array' . ($var_id ? ' ' . $var_id : ''),
@ -1072,16 +1099,16 @@ class FetchChecker
return $type;
}
if ($type->value === 'string' && $assignment_value_type->hasString() && !$assignment_key_type->hasString()) {
if ($type instanceof TString && $assignment_value_type->hasString() && !$assignment_key_type->hasString()) {
return null;
}
if (!$type->isArray() &&
!ClassChecker::classImplements($type->value, 'ArrayAccess')
if (!$type instanceof TArray &&
(!$type instanceof TNamedObject || !ClassChecker::classImplements($type->value, 'ArrayAccess'))
) {
if (IssueBuffer::accepts(
new InvalidArrayAssignment(
'Cannot assign value on variable' . ($var_id ? ' ' . $var_id : '') . ' of type ' . $type->value . ' that does not ' .
'Cannot assign value on variable' . ($var_id ? ' ' . $var_id : '') . ' of type ' . $type . ' that does not ' .
'implement ArrayAccess',
$code_location
),
@ -1093,7 +1120,7 @@ class FetchChecker
return $type;
}
if ($type instanceof Type\Generic && $type->value === 'array') {
if ($type instanceof Type\Atomic\Generic && $type instanceof TArray) {
if ($type->type_params[1]->isEmpty()) {
$type->type_params[0] = $assignment_key_type;
$type->type_params[1] = $assignment_value_type;

View File

@ -28,6 +28,26 @@ use Psalm\Issue\UnrecognizedExpression;
use Psalm\IssueBuffer;
use Psalm\StatementsSource;
use Psalm\Type;
use Psalm\Type\Atomic\Generic;
use Psalm\Type\Atomic\ObjectLike;
use Psalm\Type\Atomic\Scalar;
use Psalm\Type\Atomic\TNumeric;
use Psalm\Type\Atomic\TInt;
use Psalm\Type\Atomic\TVoid;
use Psalm\Type\Atomic\TFloat;
use Psalm\Type\Atomic\TString;
use Psalm\Type\Atomic\TBool;
use Psalm\Type\Atomic\TFalse;
use Psalm\Type\Atomic\TNull;
use Psalm\Type\Atomic\TEmpty;
use Psalm\Type\Atomic\TArray;
use Psalm\Type\Atomic\TMixed;
use Psalm\Type\Atomic\TObject;
use Psalm\Type\Atomic\TResource;
use Psalm\Type\Atomic\TCallable;
use Psalm\Type\Atomic\TNamedObject;
use Psalm\Type\Atomic\TGenericObject;
use Psalm\Type\Atomic\TNumericString;
class ExpressionChecker
{
@ -205,7 +225,7 @@ class ExpressionChecker
) {
$use_context->vars_in_scope['$this'] = clone $context->vars_in_scope['$this'];
} elseif ($context->self) {
$use_context->vars_in_scope['$this'] = new Type\Union([new Type\Atomic($context->self)]);
$use_context->vars_in_scope['$this'] = new Type\Union([new TNamedObject($context->self)]);
}
}
@ -637,18 +657,15 @@ class ExpressionChecker
// if this array looks like an object-like array, let's return that instead
if ($item_value_type && $item_key_type && $item_key_type->hasString() && !$item_key_type->hasInt()) {
$stmt->inferredType = new Type\Union([new Type\ObjectLike('array', $property_types)]);
$stmt->inferredType = new Type\Union([new Type\Atomic\ObjectLike($property_types)]);
return null;
}
$stmt->inferredType = new Type\Union([
new Type\Generic(
'array',
[
$item_key_type ?: new Type\Union([new Type\Atomic('int'), new Type\Atomic('string')]),
$item_value_type ?: Type::getMixed()
]
)
new Type\Atomic\TArray([
$item_key_type ?: new Type\Union([new TInt, new TString]),
$item_value_type ?: Type::getMixed()
])
]);
return null;
@ -887,8 +904,8 @@ class ExpressionChecker
if ($left_type && $right_type) {
foreach ($left_type->types as $left_type_part) {
foreach ($right_type->types as $right_type_part) {
if ($left_type_part->isMixed() || $right_type_part->isMixed()) {
if ($left_type_part->isMixed()) {
if ($left_type_part instanceof TMixed || $right_type_part instanceof TMixed) {
if ($left_type_part instanceof TMixed) {
if (IssueBuffer::accepts(
new MixedOperand(
'Left operand cannot be mixed',
@ -914,9 +931,9 @@ class ExpressionChecker
return;
}
if ($left_type_part->isArray() || $right_type_part->isArray()) {
if (!$right_type_part->isArray() || !$left_type_part->isArray()) {
if (!$left_type_part->isArray()) {
if ($left_type_part instanceof TArray || $right_type_part instanceof TArray) {
if (!$right_type_part instanceof TArray || !$left_type_part instanceof TArray) {
if (!$left_type_part instanceof TArray) {
if (IssueBuffer::accepts(
new InvalidOperand(
'Cannot add an array to a non-array',
@ -926,7 +943,7 @@ class ExpressionChecker
)) {
// fall through
}
} elseif (!$right_type_part->isArray()) {
} elseif (!$right_type_part instanceof TArray) {
if (IssueBuffer::accepts(
new InvalidOperand(
'Cannot add an array to a non-array',
@ -954,9 +971,7 @@ class ExpressionChecker
}
if ($left_type_part->isNumericType() || $right_type_part->isNumericType()) {
if ($left_type_part->isInt() &&
$right_type_part->isInt()
) {
if ($left_type_part instanceof TInt && $right_type_part instanceof TInt) {
if (!$result_type) {
$result_type = Type::getInt();
} else {
@ -966,9 +981,7 @@ class ExpressionChecker
continue;
}
if ($left_type_part->isFloat() &&
$right_type_part->isFloat()
) {
if ($left_type_part instanceof TFloat && $right_type_part instanceof TFloat) {
if (!$result_type) {
$result_type = Type::getFloat();
} else {
@ -978,8 +991,8 @@ class ExpressionChecker
continue;
}
if (($left_type_part->isFloat() && $right_type_part->isInt()) ||
($left_type_part->isInt() && $right_type_part->isFloat())
if (($left_type_part instanceof TFloat && $right_type_part instanceof TInt) ||
($left_type_part instanceof TInt && $right_type_part instanceof TFloat)
) {
if ($config->strict_binary_operands) {
if (IssueBuffer::accepts(
@ -1269,48 +1282,53 @@ class ExpressionChecker
*/
protected static function fleshOutAtomicType(Type\Atomic $return_type, array $args, $calling_class, $method_id)
{
if ($return_type->value === '$this' || $return_type->value === 'static' || $return_type->value === 'self') {
if (!$calling_class) {
throw new \InvalidArgumentException(
'Cannot handle ' . $return_type->value . ' when $calling_class is empty'
);
}
if ($return_type instanceof TNamedObject) {
if ($return_type->value === '$this' ||
$return_type->value === 'static' ||
$return_type->value === 'self'
) {
if (!$calling_class) {
throw new \InvalidArgumentException(
'Cannot handle ' . $return_type->value . ' when $calling_class is empty'
);
}
if ($return_type->value === 'static' || !$method_id) {
$return_type->value = $calling_class;
} else {
list(, $method_name) = explode('::', $method_id);
if ($return_type->value === 'static' || !$method_id) {
$return_type->value = $calling_class;
} else {
list(, $method_name) = explode('::', $method_id);
$appearing_method_id = MethodChecker::getAppearingMethodId($calling_class . '::' . $method_name);
$appearing_method_id = MethodChecker::getAppearingMethodId($calling_class . '::' . $method_name);
$return_type->value = explode('::', (string)$appearing_method_id)[0];
}
} elseif ($return_type->value[0] === '$' && $method_id) {
$method_params = MethodChecker::getMethodParams($method_id);
$return_type->value = explode('::', (string)$appearing_method_id)[0];
}
} elseif ($return_type->value[0] === '$' && $method_id) {
$method_params = MethodChecker::getMethodParams($method_id);
if (!$method_params) {
throw new \InvalidArgumentException(
'Cannot get method params of ' . $method_id
);
}
if (!$method_params) {
throw new \InvalidArgumentException(
'Cannot get method params of ' . $method_id
);
}
foreach ($args as $i => $arg) {
$method_param = $method_params[$i];
foreach ($args as $i => $arg) {
$method_param = $method_params[$i];
if ($return_type->value === '$' . $method_param->name) {
$arg_value = $arg->value;
if ($arg_value instanceof PhpParser\Node\Scalar\String_) {
$return_type->value = preg_replace('/^\\\/', '', $arg_value->value);
if ($return_type->value === '$' . $method_param->name) {
$arg_value = $arg->value;
if ($arg_value instanceof PhpParser\Node\Scalar\String_) {
$return_type->value = preg_replace('/^\\\/', '', $arg_value->value);
}
}
}
}
if ($return_type->value[0] === '$') {
$return_type = new Type\Atomic('mixed');
if ($return_type->value[0] === '$') {
$return_type = new TMixed;
}
}
}
if ($return_type instanceof Type\Generic) {
if ($return_type instanceof Type\Atomic\TArray || $return_type instanceof Type\Atomic\TGenericObject) {
foreach ($return_type->type_params as &$type_param) {
$type_param = self::fleshOutTypes($type_param, $args, $calling_class, $method_id);
}

View File

@ -552,7 +552,7 @@ class StatementsChecker extends SourceChecker implements StatementsSource
if (isset($dim->inferredType)) {
return $dim->inferredType;
} else {
return new Type\Union([Type::getInt()->types['int'], Type::getString()->types['string']]);
return new Type\Union([new Type\Atomic\TInt(), new Type\Atomic\TString()]);
}
} else {
return Type::getInt();

View File

@ -11,6 +11,27 @@ use Psalm\Issue\TypeDoesNotContainType;
use Psalm\IssueBuffer;
use Psalm\StatementsSource;
use Psalm\Type;
use Psalm\Type\Atomic\Generic;
use Psalm\Type\Atomic\ObjectLike;
use Psalm\Type\Atomic\Scalar;
use Psalm\Type\Atomic\TScalar;
use Psalm\Type\Atomic\TNumeric;
use Psalm\Type\Atomic\TInt;
use Psalm\Type\Atomic\TVoid;
use Psalm\Type\Atomic\TFloat;
use Psalm\Type\Atomic\TString;
use Psalm\Type\Atomic\TBool;
use Psalm\Type\Atomic\TFalse;
use Psalm\Type\Atomic\TNull;
use Psalm\Type\Atomic\TEmpty;
use Psalm\Type\Atomic\TArray;
use Psalm\Type\Atomic\TMixed;
use Psalm\Type\Atomic\TObject;
use Psalm\Type\Atomic\TResource;
use Psalm\Type\Atomic\TCallable;
use Psalm\Type\Atomic\TNamedObject;
use Psalm\Type\Atomic\TGenericObject;
use Psalm\Type\Atomic\TNumericString;
class TypeChecker
{
@ -597,7 +618,7 @@ class TypeChecker
if ($new_var_type === 'empty') {
if ($existing_var_type->hasType('bool')) {
$existing_var_type->removeType('bool');
$existing_var_type->types['false'] = new Type\Atomic('false');
$existing_var_type->types['false'] = new TFalse;
}
$existing_var_type->removeObjects();
@ -625,7 +646,7 @@ class TypeChecker
if ($new_var_type === 'numeric' && $existing_var_type->hasString()) {
$existing_var_type->removeType('string');
$existing_var_type->types['numeric-string'] = new Type\Atomic('numeric-string');
$existing_var_type->types['numeric-string'] = new TNumericString;
return $existing_var_type;
}
@ -701,7 +722,7 @@ class TypeChecker
$has_type_mismatch = false;
foreach ($input_type->types as $input_type_part) {
if ($input_type_part->isNull() && $ignore_null) {
if ($input_type_part instanceof TNull && $ignore_null) {
continue;
}
@ -709,18 +730,19 @@ class TypeChecker
$scalar_type_match_found = false;
foreach ($container_type->types as $container_type_part) {
if ($container_type_part->isNull() && $ignore_null) {
if ($container_type_part instanceof TNull && $ignore_null) {
continue;
}
$input_is_object = $input_type_part->isObjectType();
$container_is_object = $container_type_part->isObjectType();
if (strtolower($input_type_part->value) === strtolower($container_type_part->value) ||
if ($input_type_part->shallowEquals($container_type_part) ||
(
$input_is_object &&
$container_is_object &&
!$input_type_part->isObject() &&
$input_type_part instanceof TNamedObject &&
$container_type_part instanceof TNamedObject &&
ClassChecker::classExists($input_type_part->value, $file_checker) &&
(
ClassChecker::classExtendsOrImplements(
@ -733,7 +755,7 @@ class TypeChecker
) {
$all_types_contain = true;
if ($input_type_part instanceof Type\Generic && $container_type_part instanceof Type\Generic) {
if ($input_type_part instanceof TArray && $container_type_part instanceof TArray) {
foreach ($input_type_part->type_params as $i => $input_param) {
$container_param = $container_type_part->type_params[$i];
@ -763,45 +785,50 @@ class TypeChecker
break;
}
if ($input_type_part->value === 'false' && $container_type_part->value === 'bool') {
if ($input_type_part instanceof TFalse && $container_type_part instanceof TBool) {
$type_match_found = true;
}
if ($input_type_part->value === 'int' && $container_type_part->value === 'float') {
if ($input_type_part instanceof TInt && $container_type_part instanceof TFloat) {
$type_match_found = true;
}
if ($input_type_part->value === 'Closure' && $container_type_part->value === 'callable') {
if ($input_type_part instanceof TNamedObject &&
$input_type_part->value === 'Closure' &&
$container_type_part instanceof TCallable
) {
$type_match_found = true;
}
if ($container_type_part->isNumeric() && $input_type_part->isNumericType()) {
if ($container_type_part instanceof TNumeric && $input_type_part->isNumericType()) {
$type_match_found = true;
}
if ($container_type_part->isGenericArray() && $input_type_part->isObjectLike()) {
if ($container_type_part instanceof TArray && $input_type_part instanceof ObjectLike) {
$type_match_found = true;
}
if ($container_type_part->isIterable() &&
if ($container_type_part instanceof TNamedObject &&
strtolower($container_type_part->value) === 'iterable' &&
(
$input_type_part->isArray() ||
ClassChecker::classExtendsOrImplements(
$input_type_part->value,
'Traversable'
$input_type_part instanceof TArray ||
($input_type_part instanceof TNamedObject &&
ClassChecker::classExtendsOrImplements(
$input_type_part->value,
'Traversable'
)
)
)
) {
$type_match_found = true;
}
if ($container_type_part->isScalar() && $input_type_part->isScalarType()) {
if ($container_type_part instanceof TScalar && $input_type_part instanceof Scalar) {
$type_match_found = true;
}
if ($container_type_part->isString() &&
$input_type_part->isObjectType() &&
!$input_type_part->isObject()
if ($container_type_part instanceof TString &&
$input_type_part instanceof TNamedObject
) {
// check whether the object has a __toString method
if (ClassChecker::classExists($input_type_part->value, $file_checker) &&
@ -812,30 +839,30 @@ class TypeChecker
}
}
if ($container_type_part->isCallable() &&
($input_type_part->value === 'string' || $input_type_part->value === 'array')
if ($container_type_part instanceof TCallable &&
($input_type_part instanceof TString || $input_type_part instanceof TArray)
) {
// @todo add value checks if possible here
$type_match_found = true;
}
if ($input_type_part->isNumeric()) {
if ($input_type_part instanceof TNumeric) {
if ($container_type_part->isNumericType()) {
$scalar_type_match_found = true;
}
}
if ($input_type_part->isScalarType() || $input_type_part->isScalar()) {
if ($container_type_part->isScalarType()) {
if ($input_type_part instanceof Scalar || $input_type_part instanceof TScalar) {
if ($container_type_part instanceof Scalar) {
$scalar_type_match_found = true;
}
} elseif ($container_type_part->isObject() &&
!$input_type_part->isArray() &&
!$input_type_part->isResource()
} elseif ($container_type_part instanceof TObject &&
!$input_type_part instanceof TArray &&
!$input_type_part instanceof TResource
) {
$type_match_found = true;
} elseif ($input_is_object &&
$container_is_object &&
} elseif ($container_type_part instanceof TNamedObject &&
$input_type_part instanceof TNamedObject &&
ClassChecker::classExists($container_type_part->value, $file_checker) &&
ClassChecker::classOrInterfaceExists($input_type_part->value, $file_checker) &&
ClassChecker::classExtendsOrImplements(
@ -888,14 +915,15 @@ class TypeChecker
$new_base_type = null;
foreach ($existing_keys[$base_key]->types as $existing_key_type_part) {
if ($existing_key_type_part->isNull()) {
if ($existing_key_type_part instanceof TNull) {
$class_property_type = Type::getNull();
} elseif ($existing_key_type_part->isMixed() ||
$existing_key_type_part->isObject() ||
strtolower($existing_key_type_part->value) === 'stdclass'
} elseif ($existing_key_type_part instanceof TMixed ||
$existing_key_type_part instanceof TObject ||
($existing_key_type_part instanceof TNamedObject &&
strtolower($existing_key_type_part->value) === 'stdclass')
) {
$class_property_type = Type::getMixed();
} else {
} elseif ($existing_key_type_part instanceof TNamedObject) {
$property_id = $existing_key_type_part->value . '::$' . $key_parts[$i];
if (!ClassLikeChecker::propertyExists($property_id)) {
@ -909,6 +937,9 @@ class TypeChecker
$class_property_type = $class_storage->properties[$key_parts[$i]]->type;
$class_property_type = $class_property_type ? clone $class_property_type : Type::getMixed();
} else {
// @todo handle this
continue;
}
if ($new_base_type instanceof Type\Union) {
@ -959,9 +990,9 @@ class TypeChecker
$new_base_type = null;
foreach ($existing_keys[$base_key]->types as $existing_key_type_part) {
if ($existing_key_type_part instanceof Type\Generic) {
if ($existing_key_type_part instanceof Type\Atomic\TArray) {
$new_base_type_candidate = clone $existing_key_type_part->type_params[1];
} elseif (!$existing_key_type_part instanceof Type\ObjectLike) {
} elseif (!$existing_key_type_part instanceof Type\Atomic\ObjectLike) {
return null;
} else {
$array_properties = $existing_key_type_part->properties;
@ -1252,11 +1283,15 @@ class TypeChecker
$inferred_atomic_type = $inferred_type->types[$key];
if (!($declared_atomic_type instanceof Type\Generic)) {
if (!$declared_atomic_type instanceof Type\Atomic\TArray &&
!$declared_atomic_type instanceof Type\Atomic\TGenericObject
) {
continue;
}
if (!($inferred_atomic_type instanceof Type\Generic)) {
if (!$inferred_atomic_type instanceof Type\Atomic\TArray &&
!$inferred_atomic_type instanceof Type\Atomic\TGenericObject
) {
// @todo handle this better
continue;
}
@ -1279,11 +1314,11 @@ class TypeChecker
$inferred_atomic_type = $inferred_type->types[$key];
if (!($declared_atomic_type instanceof Type\ObjectLike)) {
if (!($declared_atomic_type instanceof Type\Atomic\ObjectLike)) {
continue;
}
if (!($inferred_atomic_type instanceof Type\ObjectLike)) {
if (!($inferred_atomic_type instanceof Type\Atomic\ObjectLike)) {
// @todo handle this better
continue;
}

View File

@ -2,6 +2,7 @@
namespace Psalm;
use PhpParser;
use Psalm\Type\Atomic;
/**
* A class for analysing a given method call's effects in relation to $this/self and also looking at return types
@ -37,7 +38,7 @@ class EffectsAnalyser
if (isset($stmt->inferredType)) {
$return_types = array_merge(array_values($stmt->inferredType->types), $return_types);
} else {
$return_types[] = new Type\Atomic('mixed');
$return_types[] = new Atomic\TMixed();
}
}
} elseif ($stmt instanceof PhpParser\Node\Expr\Yield_ || $stmt instanceof PhpParser\Node\Expr\YieldFrom) {
@ -88,7 +89,7 @@ class EffectsAnalyser
$value_type = null;
foreach ($yield_types as $type) {
if ($type instanceof Type\Generic) {
if ($type instanceof Type\Atomic\TArray || $type instanceof Type\Atomic\TGenericObject) {
$first_type_param = count($type->type_params) ? $type->type_params[0] : null;
$last_type_param = $type->type_params[count($type->type_params) - 1];
@ -107,7 +108,7 @@ class EffectsAnalyser
}
$yield_types = [
new Type\Generic(
new Atomic\TGenericObject(
'Generator',
[
$key_type ?: Type::getMixed(),
@ -124,8 +125,8 @@ class EffectsAnalyser
) {
// only add null if we have a return statement elsewhere and it wasn't void
foreach ($return_types as $return_type) {
if (!$return_type->isVoid()) {
$return_types[] = new Type\Atomic('null');
if (!$return_type instanceof Atomic\TVoid) {
$return_types[] = new Atomic\TNull();
break;
}
}
@ -149,7 +150,7 @@ class EffectsAnalyser
}
if (isset($stmt->inferredType)) {
$generator_type = new Type\Generic(
$generator_type = new Atomic\TGenericObject(
'Generator',
[
$key_type ?: Type::getInt(),
@ -159,7 +160,7 @@ class EffectsAnalyser
return [$generator_type];
} else {
return [new Type\Atomic('mixed')];
return [new Atomic\TMixed()];
}
} elseif ($stmt instanceof PhpParser\Node\Expr\YieldFrom) {
$key_type = null;
@ -167,7 +168,7 @@ class EffectsAnalyser
if (isset($stmt->inferredType)) {
return [$stmt->inferredType->types];
} else {
return [new Type\Atomic('mixed')];
return [new Atomic\TMixed()];
}
}

View File

@ -3,8 +3,26 @@ namespace Psalm;
use Psalm\Exception\TypeParseTreeException;
use Psalm\Type\Atomic;
use Psalm\Type\Generic;
use Psalm\Type\ObjectLike;
use Psalm\Type\Atomic\Generic;
use Psalm\Type\Atomic\ObjectLike;
use Psalm\Type\Atomic\Scalar;
use Psalm\Type\Atomic\TNumeric;
use Psalm\Type\Atomic\TInt;
use Psalm\Type\Atomic\TVoid;
use Psalm\Type\Atomic\TFloat;
use Psalm\Type\Atomic\TString;
use Psalm\Type\Atomic\TBool;
use Psalm\Type\Atomic\TFalse;
use Psalm\Type\Atomic\TNull;
use Psalm\Type\Atomic\TEmpty;
use Psalm\Type\Atomic\TArray;
use Psalm\Type\Atomic\TMixed;
use Psalm\Type\Atomic\TObject;
use Psalm\Type\Atomic\TResource;
use Psalm\Type\Atomic\TCallable;
use Psalm\Type\Atomic\TNamedObject;
use Psalm\Type\Atomic\TGenericObject;
use Psalm\Type\Atomic\TNumericString;
use Psalm\Type\ParseTree;
use Psalm\Type\Union;
@ -32,17 +50,12 @@ abstract class Type
if (count($type_tokens) === 1) {
$type_tokens[0] = self::fixScalarTerms($type_tokens[0]);
if ($type_tokens[0] === 'array') {
return Type::getArray();
}
return new Union([new Atomic($type_tokens[0])]);
return new Union([Atomic::create($type_tokens[0])]);
}
try {
$parse_tree = ParseTree::createFromTokens($type_tokens);
}
catch (TypeParseTreeException $e) {
} catch (TypeParseTreeException $e) {
throw $e;
}
@ -66,6 +79,7 @@ abstract class Type
[
'numeric',
'int',
'void',
'float',
'string',
'bool',
@ -94,7 +108,7 @@ abstract class Type
/**
* @param ParseTree $parse_tree
* @return Atomic|Generic|ObjectLike|Union
* @return Atomic|TArray|TGenericObject|ObjectLike|Union
*/
private static function getTypeFromTree(ParseTree $parse_tree)
{
@ -125,7 +139,7 @@ abstract class Type
if (($generic_type_value === 'array' || $generic_type_value === 'Generator') &&
count($generic_params) === 1
) {
array_unshift($generic_params, Type::getMixed());
array_unshift($generic_params, new Union([new TMixed]));
}
if (!$generic_params) {
@ -133,10 +147,10 @@ abstract class Type
}
if ($generic_type_value === 'array') {
return new Type\GenericArray($generic_type_value, $generic_params);
return new TArray($generic_params);
}
return new Generic($generic_type_value, $generic_params);
return new TGenericObject($generic_type_value, $generic_params);
}
if ($parse_tree->value === ParseTree::UNION) {
@ -174,20 +188,16 @@ abstract class Type
$properties[(string)($property_branch->children[0]->value)] = $property_type;
}
if (!$type->value) {
throw new \InvalidArgumentException('Object-like type must have a value');
if ($type->value !== 'array') {
throw new \InvalidArgumentException('Object-like type must be array');
}
return new ObjectLike($type->value, $properties);
return new ObjectLike($properties);
}
$atomic_type = self::fixScalarTerms($parse_tree->value);
if ($atomic_type === 'array') {
return self::getArray()->types['array'];
}
return new Atomic($atomic_type);
return Atomic::create($atomic_type);
}
/**
@ -262,7 +272,7 @@ abstract class Type
*/
public static function getInt()
{
$type = new Atomic('int');
$type = new TInt;
return new Union([$type]);
}
@ -272,7 +282,7 @@ abstract class Type
*/
public static function getString()
{
$type = new Atomic('string');
$type = new TString;
return new Union([$type]);
}
@ -282,7 +292,7 @@ abstract class Type
*/
public static function getNull()
{
$type = new Atomic('null');
$type = new TNull;
return new Union([$type]);
}
@ -292,7 +302,7 @@ abstract class Type
*/
public static function getMixed()
{
$type = new Atomic('mixed');
$type = new TMixed;
return new Union([$type]);
}
@ -302,7 +312,7 @@ abstract class Type
*/
public static function getBool()
{
$type = new Atomic('bool');
$type = new TBool;
return new Union([$type]);
}
@ -312,7 +322,7 @@ abstract class Type
*/
public static function getFloat()
{
$type = new Atomic('float');
$type = new TFloat;
return new Union([$type]);
}
@ -322,7 +332,7 @@ abstract class Type
*/
public static function getObject()
{
$type = new Atomic('object');
$type = new TObject;
return new Union([$type]);
}
@ -332,7 +342,7 @@ abstract class Type
*/
public static function getClosure()
{
$type = new Atomic('Closure');
$type = new TNamedObject('Closure');
return new Union([$type]);
}
@ -342,11 +352,10 @@ abstract class Type
*/
public static function getArray()
{
$type = new Type\GenericArray(
'array',
$type = new TArray(
[
Type::getMixed(),
Type::getMixed()
new Type\Union([new TMixed]),
new Type\Union([new TMixed])
]
);
@ -359,11 +368,10 @@ abstract class Type
public static function getEmptyArray()
{
return new Type\Union([
new Type\GenericArray(
'array',
new TArray(
[
new Type\Union([new Type\Atomic('empty')]),
new Type\Union([new Type\Atomic('empty')])
new Type\Union([new TEmpty]),
new Type\Union([new TEmpty])
]
)
]);
@ -374,7 +382,7 @@ abstract class Type
*/
public static function getVoid()
{
$type = new Atomic('void');
$type = new TVoid;
return new Union([$type]);
}
@ -384,7 +392,7 @@ abstract class Type
*/
public static function getFalse()
{
$type = new Atomic('false');
$type = new TFalse;
return new Union([$type]);
}
@ -399,8 +407,8 @@ abstract class Type
foreach ($redefined_vars as $var_name => $redefined_union_type) {
foreach ($redefined_union_type->types as $redefined_atomic_type) {
foreach ($context->vars_in_scope[$var_name]->types as $context_type) {
if ($context_type instanceof Type\Generic &&
$redefined_atomic_type instanceof Type\Generic &&
if ($context_type instanceof Type\Atomic\TArray &&
$redefined_atomic_type instanceof Type\Atomic\TArray &&
$context_type->value === $redefined_atomic_type->value
) {
// index of last param
@ -463,8 +471,8 @@ abstract class Type
}
if (count($types) === 1) {
if ($types[0]->value === 'false') {
$types[0]->value = 'bool';
if ($types[0] instanceof TFalse) {
$types[0] = new TBool;
}
return new Union([$types[0]]);
@ -492,7 +500,7 @@ abstract class Type
if (count($value_types) === 1) {
if (isset($value_types['false'])) {
return self::getBool();
return Type::getBool();
}
} elseif (isset($value_types['void'])) {
unset($value_types['void']);
@ -508,7 +516,7 @@ abstract class Type
// special case for ObjectLike where $value_type is actually an array of properties
if ($generic_type === 'object-like') {
if (!isset($value_types['array']) || isset($value_types['array']['empty'])) {
$new_types[] = new ObjectLike('array', $value_type);
$new_types[] = new ObjectLike($value_type);
}
continue;
@ -542,9 +550,16 @@ abstract class Type
array_unshift($generic_type_params, self::combineTypes($expanded_key_types));
}
$new_types[] = $value_type_param
? new Generic($generic_type, $generic_type_params)
: new Atomic($generic_type);
if ($value_type_param) {
if ($generic_type === 'array') {
$new_types[] = new TArray($generic_type_params);
} else {
$new_types[] = new TGenericObject($generic_type, $generic_type_params);
}
} else {
$new_types[] = Atomic::create($generic_type);
}
continue;
}
@ -560,7 +575,7 @@ abstract class Type
array_values($expandable_value_type->types)
);
} else {
$expanded_value_types = [Type::getMixed()->types['mixed']];
$expanded_value_types = [new TMixed];
}
}
@ -570,8 +585,11 @@ abstract class Type
array_unshift($generic_type_params, self::combineTypes($expanded_key_types));
}
// we have a generic type with
$new_types[] = new Generic($generic_type, $generic_type_params);
if ($generic_type === 'array') {
$new_types[] = new TArray($generic_type_params);
} else {
$new_types[] = new TGenericObject($generic_type, $generic_type_params);
}
}
$new_types = array_values($new_types);
@ -588,32 +606,35 @@ abstract class Type
public static function scrapeTypeProperties(Atomic $type, array &$key_types, array &$value_types)
{
// if we see the magic empty value and there's more than one type, ignore it
if ($type->value === 'empty') {
if ($type instanceof TEmpty) {
return null;
}
if ($type->value === 'mixed') {
if ($type instanceof TMixed) {
return Type::getMixed();
}
// deal with false|bool => bool
if ($type->value === 'false' && isset($value_types['bool'])) {
if ($type instanceof TFalse && isset($value_types['bool'])) {
return null;
} elseif ($type->value === 'bool' && isset($value_types['false'])) {
} elseif ($type instanceof TBool && isset($value_types['false'])) {
unset($value_types['false']);
}
if ($type instanceof Generic) {
if (!isset($value_types[$type->value])) {
$value_types[$type->value] = [];
$type_key = $type->getKey();
if ($type instanceof TArray || $type instanceof TGenericObject) {
if (!isset($value_types[$type_key])) {
$value_types[$type_key] = [];
}
$value_type_param_index = count($type->type_params) - 1;
$value_types[$type->value][(string) $type->type_params[$value_type_param_index]] =
$value_types[$type_key][(string) $type->type_params[$value_type_param_index]] =
$type->type_params[$value_type_param_index];
if ($value_type_param_index) {
$key_types[$type->value][(string) $type->type_params[0]] = $type->type_params[0];
$key_types[$type_key][(string) $type->type_params[0]] = $type->type_params[0];
}
} elseif ($type instanceof ObjectLike) {
foreach ($type->properties as $candidate_property_name => $candidate_property_type) {
@ -631,15 +652,15 @@ abstract class Type
}
}
} else {
if (!isset($value_types[$type->value])) {
$value_types[$type->value] = [];
if (!isset($value_types[$type_key])) {
$value_types[$type_key] = [];
}
if ($type->value === 'array') {
if ($type instanceof TArray) {
throw new \InvalidArgumentException('Cannot have a non-generic array');
}
$value_types[$type->value][(string) $type] = null;
$value_types[$type_key][(string) $type] = null;
}
}
}

View File

@ -2,67 +2,88 @@
namespace Psalm\Type;
use Psalm\Type;
use Psalm\Type\Atomic\ObjectLike;
use Psalm\Type\Atomic\Scalar;
use Psalm\Type\Atomic\TNumeric;
use Psalm\Type\Atomic\TInt;
use Psalm\Type\Atomic\TVoid;
use Psalm\Type\Atomic\TFloat;
use Psalm\Type\Atomic\TString;
use Psalm\Type\Atomic\TBool;
use Psalm\Type\Atomic\TFalse;
use Psalm\Type\Atomic\TNull;
use Psalm\Type\Atomic\TEmpty;
use Psalm\Type\Atomic\TArray;
use Psalm\Type\Atomic\TMixed;
use Psalm\Type\Atomic\TObject;
use Psalm\Type\Atomic\TResource;
use Psalm\Type\Atomic\TCallable;
use Psalm\Type\Atomic\TNamedObject;
use Psalm\Type\Atomic\TNumericString;
use Psalm\Type\Union;
use Psalm\Checker\ClassChecker;
use Psalm\Checker\ClassLikeChecker;
use Psalm\Checker\FileChecker;
use Psalm\CodeLocation;
use Psalm\StatementsSource;
class Atomic extends Type
abstract class Atomic extends Type
{
/**
* @var string
*/
public $value;
const KEY = 'atomic';
/**
* Constructs an Atomic instance
*
* @param string $value
* @param string $value
* @return Atomic
*/
public function __construct($value)
public static function create($value)
{
$this->value = $value;
}
switch ($value) {
case 'numeric':
return new TNumeric();
/**
* @return string
*/
public function __toString()
{
if ($this->isNumericString()) {
return 'string';
case 'int':
return new TInt();
case 'void':
return new TVoid();
case 'float':
return new TFloat();
case 'string':
return new TString();
case 'bool':
case 'true':
return new TBool();
case 'false':
return new TFalse();
case 'empty':
return new TEmpty();
case 'null':
return new TNull();
case 'array':
return new TArray([new Union([new TMixed]), new Union([new TMixed])]);
case 'object':
return new TObject();
case 'mixed':
return new TMixed();
case 'resource':
return new TResource();
case 'callable':
return new TCallable();
default:
return new TNamedObject($value);
}
return $this->value;
}
/**
* @param array<string> $aliased_classes
* @param string|null $this_class
* @param bool $use_phpdoc_format
* @return string
*/
public function toNamespacedString(array $aliased_classes, $this_class, $use_phpdoc_format)
{
if (!$this->isObjectType()) {
if ($this->isNumericString()) {
return 'string';
}
return $this->value;
}
if ($this->value === $this_class) {
$class_parts = explode('\\', $this_class);
return array_pop($class_parts);
}
if (isset($aliased_classes[strtolower($this->value)])) {
return $aliased_classes[strtolower($this->value)];
}
return '\\' . $this->value;
}
/**
@ -76,7 +97,10 @@ class Atomic extends Type
return true;
}
if ($parent->hasType('object') && ClassLikeChecker::classOrInterfaceExists($this->value, $file_checker)) {
if ($parent->hasType('object') &&
$this instanceof TNamedObject &&
ClassLikeChecker::classOrInterfaceExists($this->value, $file_checker)
) {
return true;
}
@ -84,25 +108,27 @@ class Atomic extends Type
return true;
}
if ($parent->hasType('array') && $this->isObjectLike()) {
if ($parent->hasType('array') && $this instanceof ObjectLike) {
return true;
}
if ($this->value === 'false' && $parent->hasType('bool')) {
if ($this instanceof TFalse && $parent->hasType('bool')) {
// this is fine
return true;
}
if ($parent->hasType($this->value)) {
if ($parent->hasType($this->getKey())) {
return true;
}
// last check to see if class is subclass
if (ClassChecker::classExists($this->value, $file_checker)) {
if ($this instanceof TNamedObject && ClassChecker::classExists($this->value, $file_checker)) {
$this_is_subclass = false;
foreach ($parent->types as $parent_type) {
if (ClassChecker::classExtendsOrImplements($this->value, $parent_type->value)) {
if ($parent_type instanceof TNamedObject &&
ClassChecker::classExtendsOrImplements($this->value, $parent_type->value)
) {
$this_is_subclass = true;
break;
}
@ -117,57 +143,16 @@ class Atomic extends Type
}
/**
* @return bool
* @return string
*/
public function isArray()
{
return $this->value === 'array';
}
/**
* @return bool
*/
public function isGenericArray()
{
return $this->value === 'array' && $this instanceof Generic;
}
/**
* @return bool
*/
public function isObjectLike()
{
return $this instanceof ObjectLike;
}
/**
* @return bool
*/
public function isObject()
{
return $this->value === 'object';
}
abstract public function getKey();
/**
* @return bool
*/
public function isNumericType()
{
return $this->value === 'int' || $this->value === 'float' || $this->value === 'numeric-string';
}
/**
* @return bool
*/
public function isScalarType()
{
return $this->value === 'int' ||
$this->value === 'string' ||
$this->value === 'float' ||
$this->value === 'bool' ||
$this->value === 'false' ||
$this->value === 'numeric' ||
$this->value === 'numeric-string';
return $this instanceof TInt || $this instanceof TFloat || $this instanceof TNumericString;
}
/**
@ -175,131 +160,7 @@ class Atomic extends Type
*/
public function isObjectType()
{
return $this->isObject()
|| (
!$this->isScalarType()
&& !$this->isCallable()
&& !$this->isArray()
&& !$this->isMixed()
&& !$this->isNull()
&& !$this->isVoid()
&& !$this->isEmpty()
&& !$this->isResource()
&& !$this->isIterable()
&& !$this->isScalar()
);
}
/**
* @return bool
*/
public function isString()
{
return $this->value === 'string' || $this->value === 'numeric-string';
}
/**
* @return bool
*/
public function isInt()
{
return $this->value === 'int';
}
/**
* @return bool
*/
public function isFloat()
{
return $this->value === 'float';
}
/**
* @return bool
*/
public function isNumeric()
{
return $this->value === 'numeric';
}
/**
* @return bool
*/
public function isNumericString()
{
return $this->value === 'numeric-string';
}
/**
* @return bool
*/
public function isScalar()
{
return $this->value === 'scalar';
}
/**
* @return bool
*/
public function isResource()
{
return $this->value === 'resource';
}
/**
* @return bool
*/
public function isCallable()
{
return $this->value === 'callable';
}
/**
* @return bool
*/
public function isGenerator()
{
return $this->value === 'Generator';
}
/**
* @return bool
*/
public function isIterable()
{
return $this->value === 'iterable';
}
/**
* @return bool
*/
public function isMixed()
{
return $this->value === 'mixed';
}
/**
* @return bool
*/
public function isNull()
{
return $this->value === 'null';
}
/**
* @return bool
*/
public function isVoid()
{
return $this->value === 'void';
}
/**
* @return bool
*/
public function isEmpty()
{
return $this->value === 'empty';
return $this instanceof TObject || $this instanceof TNamedObject;
}
/**
@ -310,8 +171,7 @@ class Atomic extends Type
*/
public function check(StatementsSource $source, CodeLocation $code_location, array $suppressed_issues)
{
if ($this->isObjectType()
&& !$this->isObject()
if ($this instanceof TNamedObject
&& ClassLikeChecker::checkFullyQualifiedClassLikeName(
$this->value,
$source->getFileChecker(),
@ -322,10 +182,38 @@ class Atomic extends Type
return false;
}
if ($this instanceof Type\Generic) {
if ($this instanceof Type\Atomic\TArray || $this instanceof Type\Atomic\TGenericObject) {
foreach ($this->type_params as $type_param) {
$type_param->check($source, $code_location, $suppressed_issues);
}
}
}
/**
* @param Atomic $other
* @return bool
*/
public function shallowEquals(Atomic $other)
{
return $this->getKey() === $other->getKey();
}
/**
* @return string
*/
public function __toString()
{
return '';
}
/**
* @param array<string> $aliased_classes
* @param string|null $this_class
* @param bool $use_phpdoc_format
* @return string
*/
public function toNamespacedString(array $aliased_classes, $this_class, $use_phpdoc_format)
{
return $this->getKey();
}
}

View File

@ -1,9 +1,10 @@
<?php
namespace Psalm\Type;
namespace Psalm\Type\Atomic;
use Psalm\FunctionLikeParameter;
use Psalm\Type\Union;
class Fn extends Atomic
class Fn extends TNamedObject
{
/**
* @var string
@ -32,4 +33,12 @@ class Fn extends Atomic
$this->params = $params;
$this->return_type = $return_type;
}
/**
* @return string
*/
public function getKey()
{
return 'Closure';
}
}

View File

@ -0,0 +1,6 @@
<?php
namespace Psalm\Type\Atomic;
interface Generic
{
}

View File

@ -1,25 +1,15 @@
<?php
namespace Psalm\Type;
namespace Psalm\Type\Atomic;
class Generic extends Atomic
use Psalm\Type\Union;
trait GenericTrait
{
/**
* @var array<Union>
*/
public $type_params;
/**
* Constructs a new instance of a generic type
*
* @param string $value
* @param array<int,Union> $type_params
*/
public function __construct($value, array $type_params)
{
$this->value = $value;
$this->type_params = $type_params;
}
/**
* @return string
*/

View File

@ -1,15 +1,11 @@
<?php
namespace Psalm\Type;
namespace Psalm\Type\Atomic;
use Psalm\Type;
use Psalm\Type\Union;
class ObjectLike extends Atomic
class ObjectLike extends \Psalm\Type\Atomic
{
/**
* @var string
*/
public $value = 'array';
/**
* @var array<string,Union>
*/
@ -18,10 +14,9 @@ class ObjectLike extends Atomic
/**
* Constructs a new instance of a generic type
*
* @param string $value
* @param array<string,Union> $properties
*/
public function __construct($value, array $properties)
public function __construct(array $properties)
{
$this->properties = $properties;
}
@ -31,8 +26,7 @@ class ObjectLike extends Atomic
*/
public function __toString()
{
return $this->value .
'{' .
return 'array{' .
implode(
', ',
array_map(
@ -60,11 +54,10 @@ class ObjectLike extends Atomic
public function toNamespacedString(array $aliased_classes, $this_class, $use_phpdoc_format)
{
if ($use_phpdoc_format) {
return $this->value;
return 'array';
}
return $this->value .
'{' .
return 'array{' .
implode(
', ',
array_map(
@ -103,4 +96,12 @@ class ObjectLike extends Atomic
$property = clone $property;
}
}
/**
* @return string
*/
public function getKey()
{
return 'array';
}
}

View File

@ -0,0 +1,6 @@
<?php
namespace Psalm\Type\Atomic;
abstract class Scalar extends \Psalm\Type\Atomic
{
}

View File

@ -1,7 +1,7 @@
<?php
namespace Psalm\Type;
namespace Psalm\Type\Atomic;
class T extends Atomic
class T extends TString
{
/**
* Used to hold information as to what this refers to
@ -14,8 +14,6 @@ class T extends Atomic
*/
public function __construct($typeof)
{
$this->value = 'string';
$this->typeof = $typeof;
}
}

View File

@ -0,0 +1,30 @@
<?php
namespace Psalm\Type\Atomic;
class TArray extends \Psalm\Type\Atomic implements Generic
{
use GenericTrait;
/**
* @var string
*/
public $value = 'array';
/**
* Constructs a new instance of a generic type
*
* @param array<int, \Psalm\Type\Union> $type_params
*/
public function __construct(array $type_params)
{
$this->type_params = $type_params;
}
/**
* @return string
*/
public function getKey()
{
return 'array';
}
}

View File

@ -0,0 +1,18 @@
<?php
namespace Psalm\Type\Atomic;
class TBool extends Scalar
{
public function __toString()
{
return 'bool';
}
/**
* @return string
*/
public function getKey()
{
return 'bool';
}
}

View File

@ -0,0 +1,18 @@
<?php
namespace Psalm\Type\Atomic;
class TCallable extends \Psalm\Type\Atomic
{
public function __toString()
{
return 'callable';
}
/**
* @return string
*/
public function getKey()
{
return 'callable';
}
}

View File

@ -0,0 +1,18 @@
<?php
namespace Psalm\Type\Atomic;
class TEmpty extends Scalar
{
public function __toString()
{
return 'empty';
}
/**
* @return string
*/
public function getKey()
{
return 'empty';
}
}

View File

@ -0,0 +1,18 @@
<?php
namespace Psalm\Type\Atomic;
class TFalse extends TBool
{
public function __toString()
{
return 'false';
}
/**
* @return string
*/
public function getKey()
{
return 'false';
}
}

View File

@ -0,0 +1,18 @@
<?php
namespace Psalm\Type\Atomic;
class TFloat extends Scalar
{
public function __toString()
{
return 'float';
}
/**
* @return string
*/
public function getKey()
{
return 'float';
}
}

View File

@ -0,0 +1,17 @@
<?php
namespace Psalm\Type\Atomic;
class TGenericObject extends TNamedObject implements Generic
{
use GenericTrait;
/**
* @param string $value the name of the object
* @param array<int, \Psalm\Type\Union> $type_params
*/
public function __construct($value, array $type_params)
{
$this->value = $value;
$this->type_params = $type_params;
}
}

View File

@ -0,0 +1,18 @@
<?php
namespace Psalm\Type\Atomic;
class TInt extends Scalar
{
public function __toString()
{
return 'int';
}
/**
* @return string
*/
public function getKey()
{
return 'int';
}
}

View File

@ -0,0 +1,18 @@
<?php
namespace Psalm\Type\Atomic;
class TMixed extends \Psalm\Type\Atomic
{
public function __toString()
{
return 'mixed';
}
/**
* @return string
*/
public function getKey()
{
return 'mixed';
}
}

View File

@ -0,0 +1,51 @@
<?php
namespace Psalm\Type\Atomic;
class TNamedObject extends \Psalm\Type\Atomic
{
/**
* @var string
*/
public $value;
/**
* @param string $value the name of the object
*/
public function __construct($value)
{
$this->value = $value;
}
public function __toString()
{
return $this->value;
}
/**
* @return string
*/
public function getKey()
{
return $this->value;
}
/**
* @param array<string> $aliased_classes
* @param string|null $this_class
* @param bool $use_phpdoc_format
* @return string
*/
public function toNamespacedString(array $aliased_classes, $this_class, $use_phpdoc_format)
{
if ($this->value === $this_class) {
$class_parts = explode('\\', $this_class);
return array_pop($class_parts);
}
if (isset($aliased_classes[strtolower($this->value)])) {
return $aliased_classes[strtolower($this->value)];
}
return '\\' . $this->value;
}
}

View File

@ -0,0 +1,18 @@
<?php
namespace Psalm\Type\Atomic;
class TNull extends \Psalm\Type\Atomic
{
public function __toString()
{
return 'null';
}
/**
* @return string
*/
public function getKey()
{
return 'null';
}
}

View File

@ -0,0 +1,18 @@
<?php
namespace Psalm\Type\Atomic;
class TNumeric extends Scalar
{
public function __toString()
{
return 'numeric';
}
/**
* @return string
*/
public function getKey()
{
return 'numeric';
}
}

View File

@ -0,0 +1,13 @@
<?php
namespace Psalm\Type\Atomic;
class TNumericString extends TString
{
/**
* @return string
*/
public function getKey()
{
return 'numeric-string';
}
}

View File

@ -0,0 +1,18 @@
<?php
namespace Psalm\Type\Atomic;
class TObject extends \Psalm\Type\Atomic
{
public function __toString()
{
return 'object';
}
/**
* @return string
*/
public function getKey()
{
return 'object';
}
}

View File

@ -0,0 +1,18 @@
<?php
namespace Psalm\Type\Atomic;
class TResource extends \Psalm\Type\Atomic
{
public function __toString()
{
return 'resource';
}
/**
* @return string
*/
public function getKey()
{
return 'resource';
}
}

View File

@ -0,0 +1,18 @@
<?php
namespace Psalm\Type\Atomic;
class TScalar extends Scalar
{
public function __toString()
{
return 'scalar';
}
/**
* @return string
*/
public function getKey()
{
return 'scalar';
}
}

View File

@ -0,0 +1,18 @@
<?php
namespace Psalm\Type\Atomic;
class TString extends Scalar
{
public function __toString()
{
return 'string';
}
/**
* @return string
*/
public function getKey()
{
return 'string';
}
}

View File

@ -0,0 +1,18 @@
<?php
namespace Psalm\Type\Atomic;
class TVoid extends \Psalm\Type\Atomic
{
public function __toString()
{
return 'void';
}
/**
* @return string
*/
public function getKey()
{
return 'void';
}
}

View File

@ -1,21 +0,0 @@
<?php
namespace Psalm\Type;
class GenericArray extends Generic
{
/**
* @var string
*/
public $value = 'array';
/**
* Constructs a new instance of a generic type
*
* @param string $value
* @param array<int,Union> $type_params
*/
public function __construct($value, array $type_params)
{
$this->type_params = $type_params;
}
}

View File

@ -8,18 +8,18 @@ use Psalm\Type;
class Union extends Type
{
/**
* @var array<string,Atomic>
* @var array<string, Atomic>
*/
public $types = [];
/**
* Constructs an Union instance
* @param array<int,Atomic> $types
* @param array<int, Atomic> $types
*/
public function __construct(array $types)
{
foreach ($types as $type) {
$this->types[$type->value] = $type;
$this->types[$type->getKey()] = $type;
}
}
@ -43,7 +43,7 @@ class Union extends Type
* @return string
*/
function ($type) {
return $type;
return (string)$type;
},
$this->types
)
@ -93,7 +93,7 @@ class Union extends Type
public function hasGeneric()
{
foreach ($this->types as $type) {
if ($type instanceof Generic) {
if ($type instanceof Atomic\Generic) {
return true;
}
}
@ -122,7 +122,7 @@ class Union extends Type
*/
public function hasObjectLike()
{
return isset($this->types['array']) && $this->types['array'] instanceof ObjectLike;
return isset($this->types['array']) && $this->types['array'] instanceof Atomic\ObjectLike;
}
/**
@ -297,7 +297,7 @@ class Union extends Type
}
foreach ($old_type->types as $old_type_part) {
$this->removeType($old_type_part->value);
$this->removeType($old_type_part->getKey());
}
if ($new_type) {
@ -318,7 +318,7 @@ class Union extends Type
$type = array_values($this->types)[0];
if (!$type instanceof Generic) {
if (!$type instanceof Atomic\TArray && !$type instanceof Atomic\TGenericObject) {
return true;
}

View File

@ -357,7 +357,7 @@ class Php71Test extends PHPUnit_Framework_TestCase
{
$stmts = self::$parser->parse('<?php
/**
* @param iterable<string> $iter
* @param iterable<int> $iter
*/
function iterator(iterable $iter) : void
{