1
0
mirror of https://github.com/danog/psalm.git synced 2024-11-30 04:39:00 +01:00

Prohibit property fetches from pure contexts except when they’re on immutable objects

This commit is contained in:
Brown 2020-08-23 10:57:24 -04:00
parent 1cf5153700
commit c8ea4b4e8b
4 changed files with 144 additions and 10 deletions

View File

@ -13,6 +13,7 @@ use Psalm\Internal\Type\TemplateResult;
use Psalm\CodeLocation;
use Psalm\Context;
use Psalm\Issue\DeprecatedProperty;
use Psalm\Issue\ImpurePropertyFetch;
use Psalm\Issue\InvalidPropertyFetch;
use Psalm\Issue\InternalProperty;
use Psalm\Issue\MissingPropertyType;
@ -179,12 +180,14 @@ class InstancePropertyFetchAnalyzer
$property_id = $lhs_type_part->value . '::$' . $stmt->name->name;
$class_storage = $codebase->classlike_storage_provider->get($lhs_type_part->value);
self::processTaints(
$statements_analyzer,
$stmt,
$stmt_type,
$property_id,
$codebase->classlike_storage_provider->get($lhs_type_part->value),
$class_storage,
$in_assignment
);
@ -208,6 +211,32 @@ class InstancePropertyFetchAnalyzer
$property_id
);
}
if (!$context->collect_mutations
&& !$context->collect_initializations
&& !($class_storage->external_mutation_free
&& $stmt_type->allow_mutations)
) {
$project_analyzer = $statements_analyzer->getProjectAnalyzer();
if ($context->pure) {
if (IssueBuffer::accepts(
new ImpurePropertyFetch(
'Cannot access a property on a mutable object from a pure context',
new CodeLocation($statements_analyzer, $stmt)
),
$statements_analyzer->getSuppressedIssues()
)) {
// fall through
}
} elseif ($codebase->alter_code
&& isset($project_analyzer->getIssuesToFix()['MissingPureAnnotation'])
&& $statements_analyzer->getSource()
instanceof \Psalm\Internal\Analyzer\FunctionLikeAnalyzer
) {
$statements_analyzer->getSource()->inferred_impure = true;
}
}
}
}
}
@ -956,6 +985,32 @@ class InstancePropertyFetchAnalyzer
}
}
if (!$context->collect_mutations
&& !$context->collect_initializations
&& !($class_storage->external_mutation_free
&& $class_property_type->allow_mutations)
) {
$project_analyzer = $statements_analyzer->getProjectAnalyzer();
if ($context->pure) {
if (IssueBuffer::accepts(
new ImpurePropertyFetch(
'Cannot access a property on a mutable object from a pure context',
new CodeLocation($statements_analyzer, $stmt)
),
$statements_analyzer->getSuppressedIssues()
)) {
// fall through
}
} elseif ($codebase->alter_code
&& isset($project_analyzer->getIssuesToFix()['MissingPureAnnotation'])
&& $statements_analyzer->getSource()
instanceof \Psalm\Internal\Analyzer\FunctionLikeAnalyzer
) {
$statements_analyzer->getSource()->inferred_impure = true;
}
}
self::processTaints(
$statements_analyzer,
$stmt,

View File

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

View File

@ -40,6 +40,27 @@ class PureAnnotationAdditionTest extends FileManipulationTest
['MissingPureAnnotation'],
true,
],
'dontAddPureAnnotationToMutationFreeMethod' => [
'<?php
class A {
public string $foo = "hello";
public function getFoo() : string {
return $this->foo;
}
}',
'<?php
class A {
public string $foo = "hello";
public function getFoo() : string {
return $this->foo;
}
}',
'7.4',
['MissingPureAnnotation'],
true,
],
];
}
}

View File

@ -18,18 +18,12 @@ class PureAnnotationTest extends TestCase
'<?php
namespace Bar;
class A {
public int $a = 5;
}
/** @psalm-pure */
function filterOdd(int $i, A $a) : ?int {
if ($i % 2 === 0 || $a->a === 2) {
function filterOdd(int $i) : ?int {
if ($i % 2 === 0) {
return $i;
}
$a = new A();
return null;
}',
],
@ -345,6 +339,28 @@ class PureAnnotationTest extends TestCase
}
}'
],
'allowPropertyAccessOnImmutableClass' => [
'<?php
namespace Bar;
/** @psalm-immutable */
class A {
public int $a;
public function __construct(int $a) {
$this->a = $a;
}
}
/** @psalm-pure */
function filterOdd(A $a) : bool {
if ($a->a % 2 === 0) {
return true;
}
return false;
}',
],
];
}
@ -364,7 +380,7 @@ class PureAnnotationTest extends TestCase
/** @psalm-pure */
function filterOdd(int $i, A $a) : ?int {
$a->a++;
$a->a = $i;
if ($i % 2 === 0 || $a->a === 2) {
return $i;
@ -598,6 +614,40 @@ class PureAnnotationTest extends TestCase
}',
'error_message' => 'ImpureFunctionCall',
],
'propertyFetchIsNotPure' => [
'<?php
class A {
public string $foo = "hello";
/** @psalm-pure */
public function getFoo() : string {
return $this->foo;
}
}',
'error_message' => 'ImpurePropertyFetch',
],
'preventPropertyAccessOnMutableClass' => [
'<?php
namespace Bar;
class A {
public int $a;
public function __construct(int $a) {
$this->a = $a;
}
}
/** @psalm-pure */
function filterOdd(A $a) : bool {
if ($a->a % 2 === 0) {
return true;
}
return false;
}',
'error_message' => 'ImpurePropertyFetch',
],
];
}
}