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:
parent
88e0a65f18
commit
850998ed1a
@ -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;
|
||||
|
||||
|
@ -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
|
||||
|
@ -129,7 +129,7 @@ class ConstFetchChecker
|
||||
}
|
||||
|
||||
if ($stmt->name === 'class') {
|
||||
$stmt->inferredType = Type::getString();
|
||||
$stmt->inferredType = Type::getClassString();
|
||||
|
||||
return null;
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
) {
|
||||
|
@ -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
|
||||
*/
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
31
src/Psalm/Type/Atomic/TClassString.php
Normal file
31
src/Psalm/Type/Atomic/TClassString.php
Normal 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;
|
||||
}
|
||||
}
|
@ -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',
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
@ -73,7 +73,7 @@ class Php55Test extends TestCase
|
||||
|
||||
$a = ClassName::class;',
|
||||
'assertions' => [
|
||||
'$a' => 'string',
|
||||
'$a' => 'class-string',
|
||||
],
|
||||
],
|
||||
];
|
||||
|
@ -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
|
||||
*
|
||||
|
Loading…
Reference in New Issue
Block a user