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:
parent
708f6e39de
commit
35e4b380cc
@ -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)
|
||||
),
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
),
|
||||
|
@ -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()
|
||||
|
6
src/Psalm/Issue/PossiblyNullArgument.php
Normal file
6
src/Psalm/Issue/PossiblyNullArgument.php
Normal file
@ -0,0 +1,6 @@
|
||||
<?php
|
||||
namespace Psalm\Issue;
|
||||
|
||||
class PossiblyNullArgument extends CodeError
|
||||
{
|
||||
}
|
11
src/Psalm/Issue/PossiblyNullArrayAccess.php
Normal file
11
src/Psalm/Issue/PossiblyNullArrayAccess.php
Normal 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
|
||||
{
|
||||
}
|
6
src/Psalm/Issue/PossiblyNullOperand.php
Normal file
6
src/Psalm/Issue/PossiblyNullOperand.php
Normal file
@ -0,0 +1,6 @@
|
||||
<?php
|
||||
namespace Psalm\Issue;
|
||||
|
||||
class PossiblyNullOperand extends CodeError
|
||||
{
|
||||
}
|
11
src/Psalm/Issue/PossiblyNullPropertyAssignment.php
Normal file
11
src/Psalm/Issue/PossiblyNullPropertyAssignment.php
Normal 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
|
||||
{
|
||||
}
|
11
src/Psalm/Issue/PossiblyNullPropertyFetch.php
Normal file
11
src/Psalm/Issue/PossiblyNullPropertyFetch.php
Normal 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
|
||||
{
|
||||
}
|
6
src/Psalm/Issue/PossiblyNullReference.php
Normal file
6
src/Psalm/Issue/PossiblyNullReference.php
Normal file
@ -0,0 +1,6 @@
|
||||
<?php
|
||||
namespace Psalm\Issue;
|
||||
|
||||
class PossiblyNullReference extends CodeError
|
||||
{
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
*/
|
||||
|
@ -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
|
||||
*/
|
||||
|
Loading…
Reference in New Issue
Block a user