Skip to content
Permalink
Browse files

Add concept of purity to functions and methods

  • Loading branch information...
muglug committed Jul 18, 2019
1 parent e58660f commit 3df248eea2ab537ddd26bd0bc5ba004dee4be131
@@ -169,6 +169,9 @@
<xs:element name="ImplementedParamTypeMismatch" type="IssueHandlerType" minOccurs="0" />
<xs:element name="ImplementedReturnTypeMismatch" type="IssueHandlerType" minOccurs="0" />
<xs:element name="ImplicitToStringCast" type="IssueHandlerType" minOccurs="0" />
<xs:element name="ImpureFunctionCall" type="IssueHandlerType" minOccurs="0" />
<xs:element name="ImpureMethodCall" type="IssueHandlerType" minOccurs="0" />
<xs:element name="ImpurePropertyAssignment" type="IssueHandlerType" minOccurs="0" />
<xs:element name="InaccessibleClassConstant" type="IssueHandlerType" minOccurs="0" />
<xs:element name="InaccessibleMethod" type="MethodIssueHandlerType" minOccurs="0" />
<xs:element name="InaccessibleProperty" type="IssueHandlerType" minOccurs="0" />
@@ -332,6 +332,63 @@ function takesString(string $s) : void {}
takesString(new A);
```

### ImpureFunctionCall

Emitted when calling an impure function from a function or method marked as pure.

```php
/** @psalm-pure */
function filterOdd(array $a) : void {
extract($a);
}
```

### ImpureMethodCall

Emitted when calling an impure method from a function or method marked as pure.

```php
class A {
public int $a = 5;
public function foo() : void {
$this->a++;
}
}
/** @psalm-pure */
function filterOdd(int $i, A $a) : ?int {
$a->foo();
if ($i % 2 === 0 || $a->a === 2) {
return $i;
}
return null;
}
```

### ImpurePropertyAssignment

Emitted when updating a property value from a function or method marked as pure.

```php
class A {
public int $a = 5;
}
/** @psalm-pure */
function filterOdd(int $i, A $a) : ?int {
$a->a++;
if ($i % 2 === 0 || $a->a === 2) {
return $i;
}
return null;
}
```

### InaccessibleClassConstant

Emitted when a public/private class constant is not accessible from the calling context
@@ -296,6 +296,11 @@ class Context
*/
public $ignore_variable_method = false;
/**
* @var bool
*/
public $pure = false;
/**
* @param string|null $self
*/
@@ -148,7 +148,7 @@ public static function parse($docblock, $line_number = null, $preserve_format =
'assert', 'assert-if-true', 'assert-if-false', 'suppress',
'ignore-nullable-return', 'override-property-visibility',
'override-method-visibility', 'seal-properties', 'seal-methods',
'generator-return', 'ignore-falsable-return', 'variadic',
'generator-return', 'ignore-falsable-return', 'variadic', 'pure',
'ignore-variable-method', 'ignore-variable-property', 'internal',
],
true
@@ -269,7 +269,7 @@ public static function parsePreservingLength(\PhpParser\Comment\Doc $docblock)
'assert', 'assert-if-true', 'assert-if-false', 'suppress',
'ignore-nullable-return', 'override-property-visibility',
'override-method-visibility', 'seal-properties', 'seal-methods',
'generator-return', 'ignore-falsable-return', 'variadic',
'generator-return', 'ignore-falsable-return', 'variadic', 'pure',
'ignore-variable-method', 'ignore-variable-property', 'internal',
],
true
@@ -635,6 +635,8 @@ public static function extractFunctionDocblockInfo(PhpParser\Comment\Doc $commen
}
$info->variadic = isset($parsed_docblock['specials']['psalm-variadic']);
$info->pure = isset($parsed_docblock['specials']['psalm-pure'])
|| isset($parsed_docblock['specials']['pure']);
$info->ignore_nullable_return = isset($parsed_docblock['specials']['psalm-ignore-nullable-return']);
$info->ignore_falsable_return = isset($parsed_docblock['specials']['psalm-ignore-falsable-return']);
@@ -760,6 +760,10 @@ function (FunctionLikeParameter $p) {
);
}
if ($storage->pure) {
$context->pure = true;
}
if ($storage->unused_docblock_params) {
foreach ($storage->unused_docblock_params as $param_name => $param_location) {
if (IssueBuffer::accepts(
@@ -13,6 +13,7 @@
use Psalm\Exception\DocblockParseException;
use Psalm\Exception\IncorrectDocblockException;
use Psalm\Issue\AssignmentToVoid;
use Psalm\Issue\ImpurePropertyAssignment;
use Psalm\Issue\InvalidDocblock;
use Psalm\Issue\InvalidScope;
use Psalm\Issue\LoopInvalidation;
@@ -554,6 +555,18 @@ public static function analyze(
$assign_value_type
);
} elseif ($assign_var instanceof PhpParser\Node\Expr\PropertyFetch) {
if ($context->pure) {
if (IssueBuffer::accepts(
new ImpurePropertyAssignment(
'Cannot assign to a property from a pure context',
new CodeLocation($statements_analyzer, $assign_var)
),
$statements_analyzer->getSuppressedIssues()
)) {
// fall through
}
}
if (!$assign_var->name instanceof PhpParser\Node\Identifier) {
// this can happen when the user actually means to type $this-><autocompleted>, but there's
// a variable on the next line
@@ -694,6 +707,18 @@ public static function analyzeAssignmentOperation(
$statements_analyzer
);
if ($array_var_id && $context->pure && strpos($array_var_id, '->')) {
if (IssueBuffer::accepts(
new ImpurePropertyAssignment(
'Cannot assign to a property from a pure context',
new CodeLocation($statements_analyzer, $stmt->var)
),
$statements_analyzer->getSuppressedIssues()
)) {
// fall through
}
}
if ($array_var_id && $context->collect_references && $stmt->var instanceof PhpParser\Node\Expr\Variable) {
$location = new CodeLocation($statements_analyzer, $stmt->var);
$context->assigned_var_ids[$array_var_id] = true;
@@ -15,6 +15,7 @@
use Psalm\Issue\ForbiddenCode;
use Psalm\Issue\MixedFunctionCall;
use Psalm\Issue\InvalidFunctionCall;
use Psalm\Issue\ImpureFunctionCall;
use Psalm\Issue\NullFunctionCall;
use Psalm\Issue\PossiblyInvalidFunctionCall;
use Psalm\Issue\PossiblyNullFunctionCall;
@@ -597,6 +598,20 @@ public static function analyze(
);
}
if ($context->pure) {
if (!$function_storage || !$function_storage->pure) {
if (IssueBuffer::accepts(
new ImpureFunctionCall(
'Cannot call an impure function from a pure context',
new CodeLocation($statements_analyzer, $stmt->name)
),
$statements_analyzer->getSuppressedIssues()
)) {
// fall through
}
}
}
if ($function_storage) {
if ($function_storage->assertions && $stmt->name instanceof PhpParser\Node\Name) {
self::applyAssertionsToContext(
@@ -13,6 +13,7 @@
use Psalm\CodeLocation;
use Psalm\Context;
use Psalm\Internal\FileManipulation\FileManipulationBuffer;
use Psalm\Issue\ImpureMethodCall;
use Psalm\Issue\InvalidMethodCall;
use Psalm\Issue\InvalidPropertyAssignmentValue;
use Psalm\Issue\InvalidScope;
@@ -1174,6 +1175,18 @@ function (PhpParser\Node\Arg $arg) {
$method_storage = $codebase->methods->getUserMethodStorage($method_id);
if ($method_storage) {
if ($context->pure && !$method_storage->pure) {
if (IssueBuffer::accepts(
new ImpureMethodCall(
'Cannot call an impure method from a pure context',
new CodeLocation($source, $stmt->name)
),
$statements_analyzer->getSuppressedIssues()
)) {
// fall through
}
}
if ($method_storage->assertions) {
self::applyAssertionsToContext(
$stmt->name,
@@ -12,6 +12,7 @@
use Psalm\Context;
use Psalm\Issue\AbstractInstantiation;
use Psalm\Issue\DeprecatedClass;
use Psalm\Issue\ImpureMethodCall;
use Psalm\Issue\InterfaceInstantiation;
use Psalm\Issue\InternalClass;
use Psalm\Issue\InvalidStringClass;
@@ -403,6 +404,26 @@ public static function analyze(
return false;
}
if ($context->pure) {
$declaring_method_id = $codebase->methods->getDeclaringMethodId($method_id);
if ($declaring_method_id) {
$method_storage = $codebase->methods->getStorage($declaring_method_id);
if (!$method_storage->pure) {
if (IssueBuffer::accepts(
new ImpureMethodCall(
'Cannot call an impure constructor from a pure context',
new CodeLocation($statements_analyzer, $stmt)
),
$statements_analyzer->getSuppressedIssues()
)) {
// fall through
}
}
}
}
$generic_param_types = null;
if ($storage->template_types) {
@@ -12,6 +12,7 @@
use Psalm\Context;
use Psalm\Internal\FileManipulation\FileManipulationBuffer;
use Psalm\Issue\DeprecatedClass;
use Psalm\Issue\ImpureMethodCall;
use Psalm\Issue\InvalidStringClass;
use Psalm\Issue\InternalClass;
use Psalm\Issue\MixedMethodCall;
@@ -883,6 +884,18 @@ function (PhpParser\Node\Arg $arg) {
$method_storage = $codebase->methods->getUserMethodStorage($method_id);
if ($method_storage) {
if ($context->pure && !$method_storage->pure) {
if (IssueBuffer::accepts(
new ImpureMethodCall(
'Cannot call an impure method from a pure context',
new CodeLocation($source, $stmt->name)
),
$statements_analyzer->getSuppressedIssues()
)) {
// fall through
}
}
if ($method_storage->assertions) {
self::applyAssertionsToContext(
$stmt->name,
@@ -32,6 +32,7 @@
use Psalm\FileSource;
use Psalm\Issue\DuplicateParam;
use Psalm\Issue\ForbiddenCode;
use Psalm\Issue\ImpurePropertyAssignment;
use Psalm\Issue\InvalidCast;
use Psalm\Issue\InvalidClone;
use Psalm\Issue\InvalidDocblock;
@@ -356,6 +357,18 @@ public static function analyze(
$var_id = self::getArrayVarId($stmt->var, null);
if ($var_id && $context->pure && strpos($var_id, '->')) {
if (IssueBuffer::accepts(
new ImpurePropertyAssignment(
'Cannot assign to a property from a pure context',
new CodeLocation($statements_analyzer, $stmt->var)
),
$statements_analyzer->getSuppressedIssues()
)) {
// fall through
}
}
if ($var_id && isset($context->vars_in_scope[$var_id])) {
$context->vars_in_scope[$var_id] = $stmt->inferredType;
@@ -74,6 +74,13 @@ class FunctionDocblockComment
*/
public $variadic = false;
/**
* Whether or not the function is pure
*
* @var bool
*/
public $pure = false;
/**
* Whether or not to ignore the nullability of this function's return type
*
@@ -1977,6 +1977,10 @@ private function registerFunctionLike(PhpParser\Node\FunctionLike $stmt, $fake_m
$storage->variadic = true;
}
if ($docblock_info->pure) {
$storage->pure = true;
}
if ($docblock_info->ignore_nullable_return && $storage->return_type) {
$storage->return_type->ignore_nullable_issues = true;
}
@@ -0,0 +1,6 @@
<?php
namespace Psalm\Issue;
class ImpureFunctionCall extends CodeIssue
{
}
@@ -0,0 +1,6 @@
<?php
namespace Psalm\Issue;
class ImpureMethodCall extends CodeIssue
{
}
@@ -0,0 +1,6 @@
<?php
namespace Psalm\Issue;
class ImpurePropertyAssignment extends CodeIssue
{
}
@@ -166,6 +166,11 @@ class FunctionLikeStorage
*/
public $unused_docblock_params;
/**
* @var bool
*/
public $pure = false;
public function __toString()
{
return $this->getSignature(false);

0 comments on commit 3df248e

Please sign in to comment.
You can’t perform that action at this time.