1
0
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:
Matt Brown 2018-03-06 11:20:54 -05:00
parent 4074b3fff0
commit 357ad1aa82
8 changed files with 172 additions and 20 deletions

View File

@ -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">

View File

@ -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;

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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);
}

View File

@ -357,7 +357,7 @@ class Union
*/
public function hasString()
{
return isset($this->types['string']);
return isset($this->types['string']) || isset($this->types['class-string']);
}
/**

View File

@ -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
*/

View File

@ -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',
],
];
}
}