mirror of
https://github.com/danog/psalm.git
synced 2025-01-21 21:31:13 +01:00
Fix #71 - prevent instantiation of abstract classes
This commit is contained in:
parent
9d1b382820
commit
894b25487f
@ -72,6 +72,7 @@
|
||||
|
||||
<xs:complexType name="IssueHandlersType">
|
||||
<xs:choice minOccurs="0" maxOccurs="unbounded">
|
||||
<xs:element name="AbstractInstantiation" type="IssueHandlerType" minOccurs="0" />
|
||||
<xs:element name="ContinueOutsideLoop" type="IssueHandlerType" minOccurs="0" />
|
||||
<xs:element name="DeprecatedMethod" type="IssueHandlerType" minOccurs="0" />
|
||||
<xs:element name="DuplicateParam" type="IssueHandlerType" minOccurs="0" />
|
||||
|
@ -314,6 +314,8 @@ abstract class ClassLikeChecker extends SourceChecker implements StatementsSourc
|
||||
$config = Config::getInstance();
|
||||
|
||||
if ($this instanceof ClassChecker && $this->class instanceof PhpParser\Node\Stmt\Class_) {
|
||||
$storage->abstract = (bool)$this->class->isAbstract();
|
||||
|
||||
foreach (ClassChecker::getInterfacesForClass(
|
||||
$this->fq_class_name
|
||||
) as $interface_id => $interface_name) {
|
||||
@ -1093,6 +1095,7 @@ abstract class ClassLikeChecker extends SourceChecker implements StatementsSourc
|
||||
$reflected_parent_class = $reflected_class->getParentClass();
|
||||
|
||||
$storage = self::$storage[$class_name_lower] = new ClassLikeStorage();
|
||||
$storage->abstract = $reflected_class->isAbstract();
|
||||
|
||||
self::$class_extends[$class_name] = [];
|
||||
|
||||
|
@ -15,6 +15,7 @@ use Psalm\CodeLocation;
|
||||
use Psalm\Config;
|
||||
use Psalm\Context;
|
||||
use Psalm\FunctionLikeParameter;
|
||||
use Psalm\Issue\AbstractInstantiation;
|
||||
use Psalm\Issue\ForbiddenCode;
|
||||
use Psalm\Issue\ImplicitToStringCast;
|
||||
use Psalm\Issue\InvalidArgument;
|
||||
@ -301,6 +302,8 @@ class CallChecker
|
||||
|
||||
$file_checker = $statements_checker->getFileChecker();
|
||||
|
||||
$late_static = false;
|
||||
|
||||
if ($stmt->class instanceof PhpParser\Node\Name) {
|
||||
if (!in_array($stmt->class->parts[0], ['self', 'static', 'parent'])) {
|
||||
$fq_class_name = ClassLikeChecker::getFQCLNFromNameObject(
|
||||
@ -336,6 +339,7 @@ class CallChecker
|
||||
case 'static':
|
||||
// @todo maybe we can do better here
|
||||
$fq_class_name = $context->self;
|
||||
$late_static = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
@ -351,83 +355,99 @@ class CallChecker
|
||||
|
||||
if (strtolower($fq_class_name) !== 'stdclass' &&
|
||||
$context->check_classes &&
|
||||
ClassChecker::classExists($fq_class_name, $file_checker) &&
|
||||
MethodChecker::methodExists($fq_class_name . '::__construct')
|
||||
ClassChecker::classExists($fq_class_name, $file_checker)
|
||||
) {
|
||||
$method_id = $fq_class_name . '::__construct';
|
||||
$storage = ClassLikeChecker::$storage[strtolower($fq_class_name)];
|
||||
|
||||
$method_params = FunctionLikeChecker::getMethodParamsById($method_id, $stmt->args, $file_checker);
|
||||
|
||||
if (self::checkFunctionArguments(
|
||||
$statements_checker,
|
||||
$stmt->args,
|
||||
$method_params,
|
||||
$context
|
||||
) === false) {
|
||||
return false;
|
||||
// if we're not calling this constructor via new static()
|
||||
if ($storage->abstract && !$late_static) {
|
||||
if (IssueBuffer::accepts(
|
||||
new AbstractInstantiation(
|
||||
'Unable to instantiate a abstract class ' . $fq_class_name,
|
||||
new CodeLocation($statements_checker->getSource(), $stmt)
|
||||
),
|
||||
$statements_checker->getSuppressedIssues()
|
||||
)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// check again after we've processed args
|
||||
$method_params = FunctionLikeChecker::getMethodParamsById($method_id, $stmt->args, $file_checker);
|
||||
if (MethodChecker::methodExists($fq_class_name . '::__construct')) {
|
||||
$method_id = $fq_class_name . '::__construct';
|
||||
|
||||
if (self::checkFunctionArgumentsMatch(
|
||||
$statements_checker,
|
||||
$stmt->args,
|
||||
$method_id,
|
||||
$method_params,
|
||||
$context,
|
||||
new CodeLocation($statements_checker->getSource(), $stmt)
|
||||
) === false) {
|
||||
// fall through
|
||||
}
|
||||
$method_params = FunctionLikeChecker::getMethodParamsById($method_id, $stmt->args, $file_checker);
|
||||
|
||||
if ($fq_class_name === 'ArrayIterator' && isset($stmt->args[0]->value->inferredType)) {
|
||||
/** @var Type\Union */
|
||||
$first_arg_type = $stmt->args[0]->value->inferredType;
|
||||
if (self::checkFunctionArguments(
|
||||
$statements_checker,
|
||||
$stmt->args,
|
||||
$method_params,
|
||||
$context
|
||||
) === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($first_arg_type->hasGeneric()) {
|
||||
/** @var Type\Union|null */
|
||||
$key_type = null;
|
||||
// check again after we've processed args
|
||||
$method_params = FunctionLikeChecker::getMethodParamsById($method_id, $stmt->args, $file_checker);
|
||||
|
||||
/** @var Type\Union|null */
|
||||
$value_type = null;
|
||||
if (self::checkFunctionArgumentsMatch(
|
||||
$statements_checker,
|
||||
$stmt->args,
|
||||
$method_id,
|
||||
$method_params,
|
||||
$context,
|
||||
new CodeLocation($statements_checker->getSource(), $stmt)
|
||||
) === false) {
|
||||
// fall through
|
||||
}
|
||||
|
||||
foreach ($first_arg_type->types as $type) {
|
||||
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];
|
||||
if ($fq_class_name === 'ArrayIterator' && isset($stmt->args[0]->value->inferredType)) {
|
||||
/** @var Type\Union */
|
||||
$first_arg_type = $stmt->args[0]->value->inferredType;
|
||||
|
||||
if ($value_type === null) {
|
||||
$value_type = clone $last_type_param;
|
||||
} else {
|
||||
$value_type = Type::combineUnionTypes($value_type, $last_type_param);
|
||||
}
|
||||
if ($first_arg_type->hasGeneric()) {
|
||||
/** @var Type\Union|null */
|
||||
$key_type = null;
|
||||
|
||||
if (!$key_type || !$first_type_param) {
|
||||
$key_type = $first_type_param ? clone $first_type_param : Type::getMixed();
|
||||
} else {
|
||||
$key_type = Type::combineUnionTypes($key_type, $first_type_param);
|
||||
/** @var Type\Union|null */
|
||||
$value_type = null;
|
||||
|
||||
foreach ($first_arg_type->types as $type) {
|
||||
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];
|
||||
|
||||
if ($value_type === null) {
|
||||
$value_type = clone $last_type_param;
|
||||
} else {
|
||||
$value_type = Type::combineUnionTypes($value_type, $last_type_param);
|
||||
}
|
||||
|
||||
if (!$key_type || !$first_type_param) {
|
||||
$key_type = $first_type_param ? clone $first_type_param : Type::getMixed();
|
||||
} else {
|
||||
$key_type = Type::combineUnionTypes($key_type, $first_type_param);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($key_type === null) {
|
||||
throw new \UnexpectedValueException('$key_type cannot be null');
|
||||
}
|
||||
if ($key_type === null) {
|
||||
throw new \UnexpectedValueException('$key_type cannot be null');
|
||||
}
|
||||
|
||||
if ($value_type === null) {
|
||||
throw new \UnexpectedValueException('$value_type cannot be null');
|
||||
}
|
||||
if ($value_type === null) {
|
||||
throw new \UnexpectedValueException('$value_type cannot be null');
|
||||
}
|
||||
|
||||
$stmt->inferredType = new Type\Union([
|
||||
new Type\Atomic\TGenericObject(
|
||||
$fq_class_name,
|
||||
[
|
||||
$key_type,
|
||||
$value_type
|
||||
]
|
||||
)
|
||||
]);
|
||||
$stmt->inferredType = new Type\Union([
|
||||
new Type\Atomic\TGenericObject(
|
||||
$fq_class_name,
|
||||
[
|
||||
$key_type,
|
||||
$value_type
|
||||
]
|
||||
)
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
6
src/Psalm/Issue/AbstractInstantiation.php
Normal file
6
src/Psalm/Issue/AbstractInstantiation.php
Normal file
@ -0,0 +1,6 @@
|
||||
<?php
|
||||
namespace Psalm\Issue;
|
||||
|
||||
class AbstractInstantiation extends CodeError
|
||||
{
|
||||
}
|
@ -89,6 +89,11 @@ class ClassLikeStorage
|
||||
*/
|
||||
public $line_number;
|
||||
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
public $abstract = false;
|
||||
|
||||
/**
|
||||
* @var array<string, bool>
|
||||
*/
|
||||
|
@ -515,4 +515,21 @@ class ClassTest extends PHPUnit_Framework_TestCase
|
||||
$context = new Context();
|
||||
$file_checker->visitAndAnalyzeMethods($context);
|
||||
}
|
||||
|
||||
/**
|
||||
* @expectedException \Psalm\Exception\CodeException
|
||||
* @expectedExceptionMessage AbstractInstantiation
|
||||
* @return void
|
||||
*/
|
||||
public function testAbstractClassInstantiation()
|
||||
{
|
||||
$stmts = self::$parser->parse('<?php
|
||||
abstract class A {}
|
||||
new A();
|
||||
');
|
||||
|
||||
$file_checker = new FileChecker('somefile.php', $this->project_checker, $stmts);
|
||||
$context = new Context();
|
||||
$file_checker->visitAndAnalyzeMethods($context);
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user