mirror of
https://github.com/danog/psalm.git
synced 2025-01-22 05:41:20 +01:00
Add config flags to allow stricter class invocation checks
This commit is contained in:
parent
4074b3fff0
commit
357ad1aa82
@ -29,6 +29,8 @@
|
||||
<xs:attribute name="rememberPropertyAssignmentsAfterCall" type="xs:string" />
|
||||
<xs:attribute name="serializer" type="xs:string" />
|
||||
<xs:attribute name="allowPhpStormGenerics" type="xs:string" />
|
||||
<xs:attribute name="allowCoercionFromStringToClassConst" type="xs:string" />
|
||||
<xs:attribute name="allowStringToStandInForClass" type="xs:string" />
|
||||
</xs:complexType>
|
||||
|
||||
<xs:complexType name="ProjectFilesType">
|
||||
|
@ -12,6 +12,7 @@ use Psalm\Context;
|
||||
use Psalm\Issue\AbstractInstantiation;
|
||||
use Psalm\Issue\DeprecatedClass;
|
||||
use Psalm\Issue\InterfaceInstantiation;
|
||||
use Psalm\Issue\InvalidClass;
|
||||
use Psalm\Issue\TooManyArguments;
|
||||
use Psalm\IssueBuffer;
|
||||
use Psalm\Type;
|
||||
@ -111,6 +112,35 @@ class NewChecker extends \Psalm\Checker\Statements\Expression\CallChecker
|
||||
return false;
|
||||
}
|
||||
|
||||
if (isset($stmt->class->inferredType)) {
|
||||
foreach ($stmt->class->inferredType->getTypes() as $lhs_type_part) {
|
||||
// this is always OK
|
||||
if ($lhs_type_part instanceof Type\Atomic\TClassString) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($lhs_type_part instanceof Type\Atomic\TString) {
|
||||
if ($config->allow_string_standin_for_class
|
||||
&& !$lhs_type_part instanceof Type\Atomic\TNumericString
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
} elseif ($lhs_type_part instanceof Type\Atomic\TMixed) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (IssueBuffer::accepts(
|
||||
new InvalidClass(
|
||||
'Type ' . $lhs_type_part . ' cannot be called as a class',
|
||||
new CodeLocation($statements_checker->getSource(), $stmt)
|
||||
),
|
||||
$statements_checker->getSuppressedIssues()
|
||||
)) {
|
||||
// fall through
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$stmt->inferredType = Type::getObject();
|
||||
|
||||
return null;
|
||||
|
@ -11,6 +11,7 @@ use Psalm\Config;
|
||||
use Psalm\Context;
|
||||
use Psalm\FileManipulation\FileManipulationBuffer;
|
||||
use Psalm\Issue\DeprecatedClass;
|
||||
use Psalm\Issue\InvalidClass;
|
||||
use Psalm\Issue\ParentNotFound;
|
||||
use Psalm\IssueBuffer;
|
||||
use Psalm\Type;
|
||||
@ -41,6 +42,8 @@ class StaticCallChecker extends \Psalm\Checker\Statements\Expression\CallChecker
|
||||
|
||||
$stmt->inferredType = null;
|
||||
|
||||
$config = $project_checker->config;
|
||||
|
||||
if ($stmt->class instanceof PhpParser\Node\Name) {
|
||||
$fq_class_name = null;
|
||||
|
||||
@ -179,22 +182,8 @@ class StaticCallChecker extends \Psalm\Checker\Statements\Expression\CallChecker
|
||||
} else {
|
||||
ExpressionChecker::analyze($statements_checker, $stmt->class, $context);
|
||||
|
||||
/** @var Type\Union */
|
||||
/** @var Type\Union|null */
|
||||
$lhs_type = $stmt->class->inferredType;
|
||||
|
||||
if (!isset($lhs_type) || $lhs_type->hasString()) {
|
||||
if (self::checkFunctionArguments(
|
||||
$statements_checker,
|
||||
$stmt->args,
|
||||
null,
|
||||
null,
|
||||
$context
|
||||
) === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
if (!$context->check_methods || !$lhs_type) {
|
||||
@ -203,11 +192,33 @@ class StaticCallChecker extends \Psalm\Checker\Statements\Expression\CallChecker
|
||||
|
||||
$has_mock = false;
|
||||
|
||||
$config = Config::getInstance();
|
||||
|
||||
foreach ($lhs_type->getTypes() as $lhs_type_part) {
|
||||
if (!$lhs_type_part instanceof TNamedObject) {
|
||||
// @todo deal with it
|
||||
// this is always OK
|
||||
if ($lhs_type_part instanceof Type\Atomic\TClassString) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($lhs_type_part instanceof Type\Atomic\TString) {
|
||||
if ($config->allow_string_standin_for_class
|
||||
&& !$lhs_type_part instanceof Type\Atomic\TNumericString
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
} elseif ($lhs_type_part instanceof Type\Atomic\TMixed) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (IssueBuffer::accepts(
|
||||
new InvalidClass(
|
||||
'Type ' . $lhs_type_part . ' cannot be called as a class',
|
||||
new CodeLocation($statements_checker->getSource(), $stmt)
|
||||
),
|
||||
$statements_checker->getSuppressedIssues()
|
||||
)) {
|
||||
// fall through
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
|
@ -493,7 +493,10 @@ class TypeChecker
|
||||
}
|
||||
|
||||
if ($container_type_part instanceof TClassString && $input_type_part instanceof TString) {
|
||||
$type_coerced = true;
|
||||
if (\Psalm\Config::getInstance()->allow_coercion_from_string_to_class_const) {
|
||||
$type_coerced = true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
|
@ -188,6 +188,16 @@ class Config
|
||||
*/
|
||||
public $allow_phpstorm_generics = false;
|
||||
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
public $allow_coercion_from_string_to_class_const = true;
|
||||
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
public $allow_string_standin_for_class = true;
|
||||
|
||||
/**
|
||||
* @var string[]
|
||||
*/
|
||||
@ -441,6 +451,16 @@ class Config
|
||||
$config->allow_phpstorm_generics = $attribute_text === 'true' || $attribute_text === '1';
|
||||
}
|
||||
|
||||
if (isset($config_xml['allowCoercionFromStringToClassConst'])) {
|
||||
$attribute_text = (string) $config_xml['allowCoercionFromStringToClassConst'];
|
||||
$config->allow_coercion_from_string_to_class_const = $attribute_text === 'true' || $attribute_text === '1';
|
||||
}
|
||||
|
||||
if (isset($config_xml['allowStringToStandInForClass'])) {
|
||||
$attribute_text = (string) $config_xml['allowCoercionFromStringToClassConst'];
|
||||
$config->allow_string_standin_for_class = $attribute_text === 'true' || $attribute_text === '1';
|
||||
}
|
||||
|
||||
if (isset($config_xml->projectFiles)) {
|
||||
$config->project_files = ProjectFileFilter::loadFromXMLElement($config_xml->projectFiles, $base_dir, true);
|
||||
}
|
||||
|
@ -357,7 +357,7 @@ class Union
|
||||
*/
|
||||
public function hasString()
|
||||
{
|
||||
return isset($this->types['string']);
|
||||
return isset($this->types['string']) || isset($this->types['class-string']);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -105,6 +105,80 @@ class AnnotationTest extends TestCase
|
||||
$this->analyzeFile('somefile.php', new Context());
|
||||
}
|
||||
|
||||
/**
|
||||
* @expectedException \Psalm\Exception\CodeException
|
||||
* @expectedExceptionMessage InvalidArgument
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function testDontAllowStringConstCoercion()
|
||||
{
|
||||
Config::getInstance()->allow_coercion_from_string_to_class_const = false;
|
||||
|
||||
$this->addFile(
|
||||
'somefile.php',
|
||||
'<?php
|
||||
/**
|
||||
* @param class-string $s
|
||||
*/
|
||||
function takesClassConstants(string $s) : void {}
|
||||
|
||||
class A {}
|
||||
|
||||
takesClassConstants("A");'
|
||||
);
|
||||
|
||||
$this->analyzeFile('somefile.php', new Context());
|
||||
}
|
||||
|
||||
/**
|
||||
* @expectedException \Psalm\Exception\CodeException
|
||||
* @expectedExceptionMessage InvalidClass
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function testDontAllowStringStandInForNewClass()
|
||||
{
|
||||
Config::getInstance()->allow_string_standin_for_class = false;
|
||||
|
||||
$this->addFile(
|
||||
'somefile.php',
|
||||
'<?php
|
||||
class A {}
|
||||
|
||||
$a = "A";
|
||||
|
||||
new $a();'
|
||||
);
|
||||
|
||||
$this->analyzeFile('somefile.php', new Context());
|
||||
}
|
||||
|
||||
/**
|
||||
* @expectedException \Psalm\Exception\CodeException
|
||||
* @expectedExceptionMessage InvalidClass
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function testDontAllowStringStandInForStaticMethodCall()
|
||||
{
|
||||
Config::getInstance()->allow_string_standin_for_class = false;
|
||||
|
||||
$this->addFile(
|
||||
'somefile.php',
|
||||
'<?php
|
||||
class A {
|
||||
public static function foo() : void {}
|
||||
}
|
||||
|
||||
$a = "A";
|
||||
|
||||
$a::foo();'
|
||||
);
|
||||
|
||||
$this->analyzeFile('somefile.php', new Context());
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
|
@ -267,6 +267,18 @@ class MethodCallTest extends TestCase
|
||||
$arr->$b();',
|
||||
'error_message' => 'InvalidMethodCall',
|
||||
],
|
||||
'intVarStaticCall' => [
|
||||
'<?php
|
||||
$a = 5;
|
||||
$a::bar();',
|
||||
'error_message' => 'InvalidClass',
|
||||
],
|
||||
'intVarNewCall' => [
|
||||
'<?php
|
||||
$a = 5;
|
||||
new $a();',
|
||||
'error_message' => 'InvalidClass',
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user