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

Correct analyze clone expression (#3382)

* Correct analyze clone, add PossibleInvalidClone issue type

* Infer mixed type when possible incorrect clone

* Remove unused variable
This commit is contained in:
Evgeniy 2020-05-18 13:22:50 -07:00 committed by GitHub
parent d60ece752c
commit 04a576708c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 198 additions and 13 deletions

View File

@ -301,6 +301,7 @@
<xs:element name="PossiblyInvalidArrayAssignment" type="IssueHandlerType" minOccurs="0" /> <xs:element name="PossiblyInvalidArrayAssignment" type="IssueHandlerType" minOccurs="0" />
<xs:element name="PossiblyInvalidArrayOffset" type="IssueHandlerType" minOccurs="0" /> <xs:element name="PossiblyInvalidArrayOffset" type="IssueHandlerType" minOccurs="0" />
<xs:element name="PossiblyInvalidCast" type="IssueHandlerType" minOccurs="0" /> <xs:element name="PossiblyInvalidCast" type="IssueHandlerType" minOccurs="0" />
<xs:element name="PossiblyInvalidClone" type="IssueHandlerType" minOccurs="0" />
<xs:element name="PossiblyInvalidFunctionCall" type="IssueHandlerType" minOccurs="0" /> <xs:element name="PossiblyInvalidFunctionCall" type="IssueHandlerType" minOccurs="0" />
<xs:element name="PossiblyInvalidIterator" type="IssueHandlerType" minOccurs="0" /> <xs:element name="PossiblyInvalidIterator" type="IssueHandlerType" minOccurs="0" />
<xs:element name="PossiblyInvalidMethodCall" type="IssueHandlerType" minOccurs="0" /> <xs:element name="PossiblyInvalidMethodCall" type="IssueHandlerType" minOccurs="0" />

View File

@ -147,6 +147,7 @@ These issues are treated as errors at level 3 and below.
- [PossiblyInvalidArrayAssignment](issues/PossiblyInvalidArrayAssignment.md) - [PossiblyInvalidArrayAssignment](issues/PossiblyInvalidArrayAssignment.md)
- [PossiblyInvalidArrayOffset](issues/PossiblyInvalidArrayOffset.md) - [PossiblyInvalidArrayOffset](issues/PossiblyInvalidArrayOffset.md)
- [PossiblyInvalidCast](issues/PossiblyInvalidCast.md) - [PossiblyInvalidCast](issues/PossiblyInvalidCast.md)
- [PossiblyInvalidClone](issues/PossiblyInvalidClone.md)
- [PossiblyInvalidFunctionCall](issues/PossiblyInvalidFunctionCall.md) - [PossiblyInvalidFunctionCall](issues/PossiblyInvalidFunctionCall.md)
- [PossiblyInvalidIterator](issues/PossiblyInvalidIterator.md) - [PossiblyInvalidIterator](issues/PossiblyInvalidIterator.md)
- [PossiblyInvalidMethodCall](issues/PossiblyInvalidMethodCall.md) - [PossiblyInvalidMethodCall](issues/PossiblyInvalidMethodCall.md)

View File

@ -0,0 +1,16 @@
# PossiblyInvalidClone
Emitted when trying to clone a value that's possibly not cloneable
```php
<?php
class A {}
/**
* @param A|string $a
*/
function foo($a) {
return clone $a;
}
```

View File

@ -2,11 +2,13 @@
namespace Psalm\Internal\Analyzer\Statements\Expression; namespace Psalm\Internal\Analyzer\Statements\Expression;
use PhpParser; use PhpParser;
use Psalm\Internal\Analyzer\MethodAnalyzer;
use Psalm\Internal\Analyzer\Statements\ExpressionAnalyzer; use Psalm\Internal\Analyzer\Statements\ExpressionAnalyzer;
use Psalm\Internal\Analyzer\StatementsAnalyzer; use Psalm\Internal\Analyzer\StatementsAnalyzer;
use Psalm\CodeLocation; use Psalm\CodeLocation;
use Psalm\Context; use Psalm\Context;
use Psalm\Issue\InvalidClone; use Psalm\Issue\InvalidClone;
use Psalm\Issue\PossiblyInvalidClone;
use Psalm\IssueBuffer; use Psalm\IssueBuffer;
use Psalm\Type; use Psalm\Type;
use Psalm\Type\Atomic\TTemplateParam; use Psalm\Type\Atomic\TTemplateParam;
@ -21,6 +23,7 @@ class CloneAnalyzer
PhpParser\Node\Expr\Clone_ $stmt, PhpParser\Node\Expr\Clone_ $stmt,
Context $context Context $context
) : bool { ) : bool {
$codebase_methods = $statements_analyzer->getCodebase()->methods;
if (ExpressionAnalyzer::analyze($statements_analyzer, $stmt->expr, $context) === false) { if (ExpressionAnalyzer::analyze($statements_analyzer, $stmt->expr, $context) === false) {
return false; return false;
} }
@ -32,12 +35,42 @@ class CloneAnalyzer
$immutable_cloned = false; $immutable_cloned = false;
foreach ($clone_type->getAtomicTypes() as $clone_type_part) { $invalid_clones = [];
if (!$clone_type_part instanceof TNamedObject $possibly_valid = false;
&& !$clone_type_part instanceof TObject $atomic_types = $clone_type->getAtomicTypes();
&& !$clone_type_part instanceof TMixed while ($atomic_types) {
&& !$clone_type_part instanceof TTemplateParam $clone_type_part = \array_pop($atomic_types);
if ($clone_type_part instanceof TMixed ||
$clone_type_part instanceof TObject
) { ) {
$invalid_clones[] = $clone_type_part->getId();
$possibly_valid = true;
} elseif ($clone_type_part instanceof TNamedObject) {
$clone_method_id = new \Psalm\Internal\MethodIdentifier(
$clone_type_part->value,
'__clone'
);
$does_method_exist = $codebase_methods->methodExists(
$clone_method_id,
$context->calling_method_id,
new CodeLocation($statements_analyzer->getSource(), $stmt)
);
$is_method_visible = MethodAnalyzer::isMethodVisible(
$clone_method_id,
$context,
$statements_analyzer->getSource()
);
if ($does_method_exist && !$is_method_visible) {
$invalid_clones[] = $clone_type_part->getId();
} else {
$possibly_valid = true;
$immutable_cloned = true;
}
} elseif ($clone_type_part instanceof TTemplateParam) {
$atomic_types = array_merge($atomic_types, $clone_type_part->as->getAtomicTypes());
} else {
if ($clone_type_part instanceof Type\Atomic\TFalse if ($clone_type_part instanceof Type\Atomic\TFalse
&& $clone_type->ignore_falsable_issues && $clone_type->ignore_falsable_issues
) { ) {
@ -50,22 +83,34 @@ class CloneAnalyzer
continue; continue;
} }
$invalid_clones[] = $clone_type_part->getId();
}
}
if ($invalid_clones) {
if ($possibly_valid) {
if (IssueBuffer::accepts( if (IssueBuffer::accepts(
new InvalidClone( new PossiblyInvalidClone(
'Cannot clone ' . $clone_type_part, 'Cannot clone ' . $invalid_clones[0],
new CodeLocation($statements_analyzer->getSource(), $stmt) new CodeLocation($statements_analyzer->getSource(), $stmt)
), ),
$statements_analyzer->getSuppressedIssues() $statements_analyzer->getSuppressedIssues()
)) { )) {
return false; // fall through
}
} else {
if (IssueBuffer::accepts(
new InvalidClone(
'Cannot clone ' . $invalid_clones[0],
new CodeLocation($statements_analyzer->getSource(), $stmt)
),
$statements_analyzer->getSuppressedIssues()
)) {
// fall through
} }
return true;
} }
if ($clone_type_part instanceof TNamedObject) { return true;
$immutable_cloned = true;
}
} }
$statements_analyzer->node_data->setType($stmt, $stmt_expr_type); $statements_analyzer->node_data->setType($stmt, $stmt_expr_type);

View File

@ -0,0 +1,8 @@
<?php
namespace Psalm\Issue;
class PossiblyInvalidClone extends CodeIssue
{
const ERROR_LEVEL = 3;
const SHORTCODE = 226;
}

114
tests/CloneTest.php Normal file
View File

@ -0,0 +1,114 @@
<?php
declare(strict_types=1);
namespace Psalm\Tests;
class CloneTest extends TestCase
{
use Traits\InvalidCodeAnalysisTestTrait;
use Traits\ValidCodeAnalysisTestTrait;
/**
* @return iterable<string,array{string,assertions?:array<string,string>,error_levels?:string[]}>
*/
public function providerValidCodeParse()
{
return [
'cloneCorrect' => [
'<?php
class A {}
function foo(A $a) : A {
return clone $a;
}
$a = foo(new A());',
],
'cloneCorrectWithPublicMethod' => [
'<?php
class A {
public function __clone() {}
}
function foo(A $a) : A {
return clone $a;
}
foo(new A());',
],
'clonePrivateInternally' => [
'<?php
class A {
private function __clone() {}
public function foo(): self {
return clone $this;
}
}',
],
];
}
/**
* @return iterable<string,array{string,error_message:string,2?:string[],3?:bool,4?:string}>
*/
public function providerInvalidCodeParse()
{
return [
'invalidIntClone' => [
'<?php
$a = 5;
clone $a;',
'error_message' => 'InvalidClone',
],
'invalidMixedClone' => [
'<?php
/** @var mixed $a */
$a = 5;
clone $a;',
'error_message' => 'PossiblyInvalidClone',
],
'notVisibleCloneMethod' => [
'<?php
class A {
private function __clone() {}
}
$a = new A();
clone $a;',
'error_message' => 'InvalidClone',
],
'invalidGenericClone' => [
'<?php
/**
* @template T as int|string
* @param T $a
*/
function foo($a): void {
clone $a;
}',
'error_message' => 'InvalidClone',
],
'possiblyInvalidGenericClone' => [
'<?php
/**
* @template T
* @param T $a
*/
function foo($a): void {
clone $a;
}',
'error_message' => 'PossiblyInvalidClone',
],
'mixedTypeInferredIfErrors' => [
'<?php
class A {}
/**
* @param A|string $a
*/
function foo($a): void {
/**
* @psalm-suppress PossiblyInvalidClone
*/
$cloned = clone $a;
}',
'error_message' => 'MixedAssignment',
]
];
}
}