1
0
mirror of https://github.com/danog/psalm.git synced 2024-11-30 04:39:00 +01:00

Add class-string type for enforcing use of ::class constants

This commit is contained in:
Matt Brown 2018-03-05 16:06:06 -05:00
parent 88e0a65f18
commit 850998ed1a
11 changed files with 186 additions and 6 deletions

View File

@ -174,6 +174,7 @@ class MethodCallChecker extends \Psalm\Checker\Statements\Expression\CallChecker
case 'Psalm\\Type\\Atomic\\TArray':
case 'Psalm\\Type\\Atomic\\TString':
case 'Psalm\\Type\\Atomic\\TNumericString':
case 'Psalm\\Type\\Atomic\\TClassString':
$invalid_method_call_types[] = (string)$class_type_part;
break;

View File

@ -1380,7 +1380,36 @@ class CallChecker
|| $input_expr instanceof PhpParser\Node\Expr\Array_
) {
foreach ($param_type->getTypes() as $param_type_part) {
if ($param_type_part instanceof TCallable) {
if ($param_type_part instanceof TClassString
&& $input_expr instanceof PhpParser\Node\Scalar\String_
) {
if (ClassLikeChecker::checkFullyQualifiedClassLikeName(
$statements_checker,
$input_expr->value,
$code_location,
$statements_checker->getSuppressedIssues()
) === false
) {
return false;
}
} elseif ($param_type_part instanceof TArray
&& isset($param_type_part->type_params[1]->getTypes()['class-string'])
&& $input_expr instanceof PhpParser\Node\Expr\Array_
) {
foreach ($input_expr->items as $item) {
if ($item && $item->value instanceof PhpParser\Node\Scalar\String_) {
if (ClassLikeChecker::checkFullyQualifiedClassLikeName(
$statements_checker,
$item->value->value,
$code_location,
$statements_checker->getSuppressedIssues()
) === false
) {
return false;
}
}
}
} elseif ($param_type_part instanceof TCallable) {
$function_ids = self::getFunctionIdsFromCallableArg(
$statements_checker,
$input_expr

View File

@ -129,7 +129,7 @@ class ConstFetchChecker
}
if ($stmt->name === 'class') {
$stmt->inferredType = Type::getString();
$stmt->inferredType = Type::getClassString();
return null;
}

View File

@ -729,6 +729,10 @@ class StatementsChecker extends SourceChecker implements StatementsSource
return $existing_class_constants[$stmt->name];
}
if (strtolower($stmt->name) === 'class') {
return Type::getClassString();
}
return null;
}

View File

@ -8,6 +8,7 @@ use Psalm\Type\Atomic\ObjectLike;
use Psalm\Type\Atomic\Scalar;
use Psalm\Type\Atomic\TArray;
use Psalm\Type\Atomic\TBool;
use Psalm\Type\Atomic\TClassString;
use Psalm\Type\Atomic\TCallable;
use Psalm\Type\Atomic\TFalse;
use Psalm\Type\Atomic\TFloat;
@ -487,6 +488,15 @@ class TypeChecker
return true;
}
if ($container_type_part instanceof TString && $input_type_part instanceof TClassString) {
return true;
}
if ($container_type_part instanceof TClassString && $input_type_part instanceof TString) {
$type_coerced = true;
return false;
}
if ($container_type_part instanceof TString &&
$input_type_part instanceof TNamedObject
) {

View File

@ -6,6 +6,7 @@ use Psalm\Type\Atomic;
use Psalm\Type\Atomic\ObjectLike;
use Psalm\Type\Atomic\TArray;
use Psalm\Type\Atomic\TBool;
use Psalm\Type\Atomic\TClassString;
use Psalm\Type\Atomic\TEmpty;
use Psalm\Type\Atomic\TFalse;
use Psalm\Type\Atomic\TFloat;
@ -42,7 +43,7 @@ abstract class Type
public static function parseString($type_string, $php_compatible = false)
{
// remove all unacceptable characters
$type_string = preg_replace('/[^A-Za-z0-9_\\\\|\? \<\>\{\}:,\]\[\(\)\$]/', '', trim($type_string));
$type_string = preg_replace('/[^A-Za-z0-9\-_\\\\|\? \<\>\{\}:,\]\[\(\)\$]/', '', trim($type_string));
if (strpos($type_string, '[') !== false) {
$type_string = self::convertSquareBrackets($type_string);
@ -432,6 +433,16 @@ abstract class Type
return new Union([$type]);
}
/**
* @return Type\Union
*/
public static function getClassString()
{
$type = new TClassString;
return new Union([$type]);
}
/**
* @return Type\Union
*/

View File

@ -13,6 +13,7 @@ use Psalm\Type\Atomic\ObjectLike;
use Psalm\Type\Atomic\TArray;
use Psalm\Type\Atomic\TBool;
use Psalm\Type\Atomic\TCallable;
use Psalm\Type\Atomic\TClassString;
use Psalm\Type\Atomic\TEmpty;
use Psalm\Type\Atomic\TFalse;
use Psalm\Type\Atomic\TFloat;
@ -104,10 +105,14 @@ abstract class Atomic
case 'mixed':
return $php_compatible ? new TNamedObject($value) : new TMixed();
case 'numeric-string':
return new TNumericString();
case 'class-string':
return new TClassString();
default:
if (strpos($value, '-')) {
throw new \Psalm\Exception\TypeParseTreeException('no hyphens allowed');
}
return new TNamedObject($value);
}
}

View File

@ -0,0 +1,31 @@
<?php
namespace Psalm\Type\Atomic;
class TClassString extends TString
{
public function __toString()
{
return 'class-string';
}
/**
* @return string
*/
public function getKey()
{
return 'class-string';
}
public function getId()
{
return $this->getKey();
}
/**
* @return bool
*/
public function canBeFullyExpressedInPhp()
{
return false;
}
}

View File

@ -529,6 +529,56 @@ class AnnotationTest extends TestCase
'assertions' => [],
'error_level' => ['MixedAssignment', 'MixedTypeCoercion'],
],
'arrayOfClassConstants' => [
'<?php
/**
* @param array<class-string> $arr
*/
function takesClassConstants(array $arr) : void {}
class A {}
class B {}
takesClassConstants([A::class, B::class]);',
],
'arrayOfStringClasses' => [
'<?php
/**
* @param array<class-string> $arr
*/
function takesClassConstants(array $arr) : void {}
class A {}
class B {}
takesClassConstants(["A", "B"]);',
'annotations' => [],
'error_levels' => ['TypeCoercion'],
],
'singleClassConstant' => [
'<?php
/**
* @param class-string $s
*/
function takesClassConstants(string $s) : void {}
class A {}
takesClassConstants(A::class);',
],
'singleClassConstant' => [
'<?php
/**
* @param class-string $s
*/
function takesClassConstants(string $s) : void {}
class A {}
takesClassConstants("A");',
'annotations' => [],
'error_levels' => ['TypeCoercion'],
],
];
}
@ -1134,6 +1184,37 @@ class AnnotationTest extends TestCase
'error_message' => 'MixedTypeCoercion',
'error_levels' => ['MixedAssignment'],
],
'arrayOfStringClasses' => [
'<?php
/**
* @param array<class-string> $arr
*/
function takesClassConstants(array $arr) : void {}
class A {}
class B {}
takesClassConstants(["A", "B"]);',
'error_message' => 'TypeCoercion',
],
'arrayOfNonExistentStringClasses' => [
'<?php
/**
* @param array<class-string> $arr
*/
function takesClassConstants(array $arr) : void {}
takesClassConstants(["A", "B"]);',
'error_message' => 'UndefinedClass',
'error_levels' => ['TypeCoercion'],
],
'singleClassConstantWithInvalidDocblock' => [
'<?php
/**
* @param clas-string $s
*/
function takesClassConstants(string $s) : void {}',
'error_message' => 'InvalidDocblock',
],
];
}
}

View File

@ -73,7 +73,7 @@ class Php55Test extends TestCase
$a = ClassName::class;',
'assertions' => [
'$a' => 'string',
'$a' => 'class-string',
],
],
];

View File

@ -61,6 +61,14 @@ class TypeParseTest extends TestCase
$this->assertSame('array<mixed, A|B>|C', (string) Type::parseString('A[]|B[]|C'));
}
/**
* @return void
*/
public function testPsalmOnlyAtomic()
{
$this->assertSame('class-string', (string) Type::parseString('class-string'));
}
/**
* @expectedException \Psalm\Exception\TypeParseTreeException
*