1
0
mirror of https://github.com/danog/psalm.git synced 2024-11-26 20:34:47 +01:00

Add collection of PossiblyNull* issues

This commit is contained in:
Matthew Brown 2017-02-11 17:55:08 -05:00
parent 708f6e39de
commit 35e4b380cc
13 changed files with 286 additions and 49 deletions

View File

@ -26,6 +26,7 @@ use Psalm\Issue\MixedPropertyAssignment;
use Psalm\Issue\MixedStringOffsetAssignment;
use Psalm\Issue\NoInterfaceProperties;
use Psalm\Issue\NullPropertyAssignment;
use Psalm\Issue\PossiblyNullPropertyAssignment;
use Psalm\Issue\UndefinedClass;
use Psalm\Issue\UndefinedPropertyAssignment;
use Psalm\Issue\UndefinedThisPropertyAssignment;
@ -488,7 +489,7 @@ class AssignmentChecker
if ($lhs_type->isNullable()) {
if (IssueBuffer::accepts(
new NullPropertyAssignment(
new PossiblyNullPropertyAssignment(
$lhs_var_id . ' with possibly null type \'' . $lhs_type . '\' cannot be assigned to',
new CodeLocation($statements_checker->getSource(), $stmt->var)
),

View File

@ -27,6 +27,8 @@ use Psalm\Issue\MixedMethodCall;
use Psalm\Issue\NullArgument;
use Psalm\Issue\NullReference;
use Psalm\Issue\ParentNotFound;
use Psalm\Issue\PossiblyNullArgument;
use Psalm\Issue\PossiblyNullReference;
use Psalm\Issue\TooFewArguments;
use Psalm\Issue\TooManyArguments;
use Psalm\Issue\TypeCoercion;
@ -597,6 +599,32 @@ class CallChecker
$has_mock = false;
if ($class_type && is_string($stmt->name) && $class_type->isNull()) {
if (IssueBuffer::accepts(
new NullReference(
'Cannot call method ' . $stmt->name . ' on null variable ' . $var_id,
new CodeLocation($statements_checker->getSource(), $stmt->var)
),
$statements_checker->getSuppressedIssues()
)) {
return false;
}
return null;
}
if ($class_type && is_string($stmt->name) && $class_type->isNullable()) {
if (IssueBuffer::accepts(
new PossiblyNullReference(
'Cannot call method ' . $stmt->name . ' on possibly null variable ' . $var_id,
new CodeLocation($statements_checker->getSource(), $stmt->var)
),
$statements_checker->getSuppressedIssues()
)) {
return false;
}
}
if ($class_type && is_string($stmt->name)) {
/** @var Type\Union|null */
$return_type = null;
@ -605,15 +633,7 @@ class CallChecker
if (!$class_type_part instanceof TNamedObject) {
switch (get_class($class_type_part)) {
case 'Psalm\\Type\\Atomic\\TNull':
if (IssueBuffer::accepts(
new NullReference(
'Cannot call method ' . $stmt->name . ' on possibly null variable ' . $var_id,
new CodeLocation($statements_checker->getSource(), $stmt)
),
$statements_checker->getSuppressedIssues()
)) {
return false;
}
// handled above
break;
case 'Psalm\\Type\\Atomic\\TInt':
@ -1670,16 +1690,33 @@ class CallChecker
return null;
}
if ($input_type->isNullable() && !$param_type->isNullable() && $cased_method_id !== 'echo') {
if (IssueBuffer::accepts(
new NullArgument(
'Argument ' . ($argument_offset + 1) . $method_identifier . ' cannot be null, possibly ' .
'null value provided',
$code_location
),
$statements_checker->getSuppressedIssues()
)) {
return false;
if (!$param_type->isNullable() && $cased_method_id !== 'echo') {
if ($input_type->isNull()) {
if (IssueBuffer::accepts(
new NullArgument(
'Argument ' . ($argument_offset + 1) . $method_identifier . ' cannot be null, possibly ' .
'null value provided',
$code_location
),
$statements_checker->getSuppressedIssues()
)) {
return false;
}
return null;
}
if ($input_type->isNullable()) {
if (IssueBuffer::accepts(
new PossiblyNullArgument(
'Argument ' . ($argument_offset + 1) . $method_identifier . ' cannot be null, possibly ' .
'null value provided',
$code_location
),
$statements_checker->getSuppressedIssues()
)) {
return false;
}
}
}

View File

@ -25,6 +25,9 @@ use Psalm\Issue\NullArrayAccess;
use Psalm\Issue\NullPropertyFetch;
use Psalm\Issue\NullReference;
use Psalm\Issue\ParentNotFound;
use Psalm\Issue\PossiblyNullArrayAccess;
use Psalm\Issue\PossiblyNullPropertyFetch;
use Psalm\Issue\PossiblyNullReference;
use Psalm\Issue\UndefinedClass;
use Psalm\Issue\UndefinedConstant;
use Psalm\Issue\UndefinedPropertyFetch;
@ -154,7 +157,7 @@ class FetchChecker
if ($stmt_var_type->isNullable()) {
if (IssueBuffer::accepts(
new NullPropertyFetch(
new PossiblyNullPropertyFetch(
'Cannot get property on possibly null variable ' . $stmt_var_id . ' of type ' . $stmt_var_type,
new CodeLocation($statements_checker->getSource(), $stmt)
),
@ -759,6 +762,20 @@ class FetchChecker
$keyed_assignment_type_array->type_params[1] = $assignment_value_type;
}
} else {
if ($keyed_assignment_type->isNull()) {
if (IssueBuffer::accepts(
new NullReference(
'Cannot assign value on null array' . ($var_id ? ' ' . $var_id : ''),
new CodeLocation($statements_checker->getSource(), $stmt)
),
$statements_checker->getSuppressedIssues()
)) {
return false;
}
return;
}
foreach ($keyed_assignment_type->types as &$type) {
if ($type instanceof TInt || $type instanceof TFloat || $type instanceof TBool) {
if (IssueBuffer::accepts(
@ -826,7 +843,45 @@ class FetchChecker
/** @var Type\Union */
$var_type = $stmt->var->inferredType;
if ($var_type->isNull()) {
if (IssueBuffer::accepts(
new NullArrayAccess(
'Cannot access array value on null variable ' . $array_var_id,
new CodeLocation($statements_checker->getSource(), $stmt)
),
$statements_checker->getSuppressedIssues()
)) {
// fall through
}
if (isset($stmt->inferredType)) {
$stmt->inferredType = Type::combineUnionTypes($stmt->inferredType, Type::getNull());
} else {
$stmt->inferredType = Type::getNull();
}
return;
}
foreach ($var_type->types as &$type) {
if ($type instanceof TNull) {
if (IssueBuffer::accepts(
new PossiblyNullArrayAccess(
'Cannot access array value on possibly null variable ' . $array_var_id .
' of type ' . $var_type,
new CodeLocation($statements_checker->getSource(), $stmt)
),
$statements_checker->getSuppressedIssues()
)) {
if (isset($stmt->inferredType)) {
$stmt->inferredType = Type::combineUnionTypes($stmt->inferredType, Type::getNull());
} else {
$stmt->inferredType = Type::getNull();
}
continue;
}
}
if ($type instanceof Type\Atomic\TArray || $type instanceof Type\Atomic\ObjectLike) {
$value_index = null;
@ -996,7 +1051,10 @@ class FetchChecker
}
}
}
} elseif ($type instanceof TString) {
continue;
}
if ($type instanceof TString) {
if ($key_type) {
$key_type = Type::combineUnionTypes($key_type, Type::getInt());
} else {
@ -1010,22 +1068,10 @@ class FetchChecker
}
$stmt->inferredType = Type::getString();
} elseif ($type instanceof TNull) {
if (IssueBuffer::accepts(
new NullArrayAccess(
'Cannot access array value on possibly null variable ' . $array_var_id . ' of type ' . $var_type,
new CodeLocation($statements_checker->getSource(), $stmt)
),
$statements_checker->getSuppressedIssues()
)) {
if (isset($stmt->inferredType)) {
$stmt->inferredType = Type::combineUnionTypes($stmt->inferredType, Type::getNull());
} else {
$stmt->inferredType = Type::getNull();
}
continue;
}
} elseif ($type instanceof TMixed || $type instanceof TEmpty) {
continue;
}
if ($type instanceof TMixed || $type instanceof TEmpty) {
if (IssueBuffer::accepts(
new MixedArrayAccess(
'Cannot access array value on mixed variable ' . $array_var_id,
@ -1036,7 +1082,10 @@ class FetchChecker
$stmt->inferredType = Type::getMixed();
break;
}
} elseif (!$type instanceof TNamedObject ||
continue;
}
if (!$type instanceof TNamedObject ||
(strtolower($type->value) !== 'simplexmlelement' &&
ClassChecker::classExists($type->value, $statements_checker->getFileChecker()) &&
!ClassChecker::classImplements($type->value, 'ArrayAccess')
@ -1128,7 +1177,7 @@ class FetchChecker
) {
if ($type instanceof TNull) {
if (IssueBuffer::accepts(
new NullReference(
new PossiblyNullReference(
'Cannot assign value on possibly null array' . ($var_id ? ' ' . $var_id : ''),
$code_location
),

View File

@ -23,6 +23,7 @@ use Psalm\Issue\InvalidScope;
use Psalm\Issue\InvalidStaticVariable;
use Psalm\Issue\MixedOperand;
use Psalm\Issue\NullOperand;
use Psalm\Issue\PossiblyNullOperand;
use Psalm\Issue\PossiblyUndefinedVariable;
use Psalm\Issue\UndefinedVariable;
use Psalm\Issue\UnrecognizedExpression;
@ -1140,7 +1141,7 @@ class ExpressionChecker
return;
}
if ($left_type->isNullable()) {
if ($left_type->isNull()) {
if (IssueBuffer::accepts(
new NullOperand(
'Cannot concatenate with a ' . $left_type,
@ -1150,12 +1151,40 @@ class ExpressionChecker
)) {
// fall through
}
return;
}
if ($right_type->isNull()) {
if (IssueBuffer::accepts(
new NullOperand(
'Cannot concatenate with a ' . $right_type,
new CodeLocation($statements_checker->getSource(), $right)
),
$statements_checker->getSuppressedIssues()
)) {
// fall through
}
return;
}
if ($left_type->isNullable()) {
if (IssueBuffer::accepts(
new PossiblyNullOperand(
'Cannot concatenate with a possibly null ' . $left_type,
new CodeLocation($statements_checker->getSource(), $left)
),
$statements_checker->getSuppressedIssues()
)) {
// fall through
}
}
if ($right_type->isNullable()) {
if (IssueBuffer::accepts(
new NullOperand(
'Cannot concatenate with a ' . $right_type,
new PossiblyNullOperand(
'Cannot concatenate with a possibly null ' . $right_type,
new CodeLocation($statements_checker->getSource(), $right)
),
$statements_checker->getSuppressedIssues()

View File

@ -0,0 +1,6 @@
<?php
namespace Psalm\Issue;
class PossiblyNullArgument extends CodeError
{
}

View File

@ -0,0 +1,11 @@
<?php
namespace Psalm\Issue;
/**
* This is different from PossiblyNullReference, as PHP throws a notice (vs the possibility of a fatal error with a null
* reference)
*
*/
class PossiblyNullArrayAccess extends CodeError
{
}

View File

@ -0,0 +1,6 @@
<?php
namespace Psalm\Issue;
class PossiblyNullOperand extends CodeError
{
}

View File

@ -0,0 +1,11 @@
<?php
namespace Psalm\Issue;
/**
* This is different from PossiblyNullReference, as PHP throws a notice (vs the possibility of a fatal error with a null
* reference)
*
*/
class PossiblyNullPropertyAssignment extends CodeError
{
}

View File

@ -0,0 +1,11 @@
<?php
namespace Psalm\Issue;
/**
* This is different from PossiblyNullReference, as PHP throws a notice (vs the possibility of a fatal error with a null
* reference)
*
*/
class PossiblyNullPropertyFetch extends CodeError
{
}

View File

@ -0,0 +1,6 @@
<?php
namespace Psalm\Issue;
class PossiblyNullReference extends CodeError
{
}

View File

@ -193,4 +193,21 @@ class ArrayAccessTest extends PHPUnit_Framework_TestCase
$file_checker = new FileChecker('somefile.php', $this->project_checker, $stmts);
$file_checker->visitAndAnalyzeMethods($context);
}
/**
* @expectedException \Psalm\Exception\CodeException
* @expectedExceptionMessage PossiblyNullArrayAccess
* @return void
*/
public function testPossiblyNullArrayAccess()
{
$context = new Context();
$stmts = self::$parser->parse('<?php
$a = rand(0, 1) ? [1, 2] : null;
echo $a[0];
');
$file_checker = new FileChecker('somefile.php', $this->project_checker, $stmts);
$file_checker->visitAndAnalyzeMethods($context);
}
}

View File

@ -367,10 +367,10 @@ class PropertyTypeTest extends PHPUnit_Framework_TestCase
/**
* @expectedException \Psalm\Exception\CodeException
* @expectedExceptionMessage NullPropertyAssignment
* @expectedExceptionMessage PossiblyNullPropertyAssignment
* @return void
*/
public function testNullablePropertyAssignment()
public function testPossiblyNullablePropertyAssignment()
{
$stmts = self::$parser->parse('<?php
class Foo {
@ -390,10 +390,28 @@ class PropertyTypeTest extends PHPUnit_Framework_TestCase
/**
* @expectedException \Psalm\Exception\CodeException
* @expectedExceptionMessage NullPropertyFetch
* @expectedExceptionMessage NullPropertyAssignment
* @return void
*/
public function testNullablePropertyFetch()
public function testNullablePropertyAssignment()
{
$stmts = self::$parser->parse('<?php
$a = null;
$a->foo = "hello";
');
$file_checker = new FileChecker('somefile.php', $this->project_checker, $stmts);
$context = new Context();
$file_checker->visitAndAnalyzeMethods($context);
}
/**
* @expectedException \Psalm\Exception\CodeException
* @expectedExceptionMessage PossiblyNullPropertyFetch
* @return void
*/
public function testPossiblyNullablePropertyFetch()
{
$stmts = self::$parser->parse('<?php
class Foo {
@ -411,6 +429,24 @@ class PropertyTypeTest extends PHPUnit_Framework_TestCase
$file_checker->visitAndAnalyzeMethods($context);
}
/**
* @expectedException \Psalm\Exception\CodeException
* @expectedExceptionMessage NullPropertyFetch
* @return void
*/
public function testNullablePropertyFetch()
{
$stmts = self::$parser->parse('<?php
$a = null;
echo $a->foo;
');
$file_checker = new FileChecker('somefile.php', $this->project_checker, $stmts);
$context = new Context();
$file_checker->visitAndAnalyzeMethods($context);
}
/**
* @return void
*/

View File

@ -1779,10 +1779,10 @@ class TypeTest extends PHPUnit_Framework_TestCase
/**
* @expectedException \Psalm\Exception\CodeException
* @expectedExceptionMessage NullReference
* @expectedExceptionMessage PossiblyNullReference
* @return void
*/
public function testNullCheckInsideForeachWithNoLeaveStatement()
public function testPossiblyNullCheckInsideForeachWithNoLeaveStatement()
{
$stmts = self::$parser->parse('<?php
class A {
@ -1811,6 +1811,23 @@ class TypeTest extends PHPUnit_Framework_TestCase
$file_checker->visitAndAnalyzeMethods();
}
/**
* @expectedException \Psalm\Exception\CodeException
* @expectedExceptionMessage NullReference
* @return void
*/
public function testNullCheckInsideForeachWithNoLeaveStatement()
{
$stmts = self::$parser->parse('<?php
$a = null;
$a->fooBar();
');
$file_checker = new FileChecker('somefile.php', $this->project_checker, $stmts);
$file_checker->visitAndAnalyzeMethods();
}
/**
* @return void
*/