Skip to content
Permalink
Browse files

Fix #2515 - allow chained assertions on @psalm-mutation-free methods

  • Loading branch information
muglug committed Dec 27, 2019
1 parent 5e90453 commit 982fe627e0bd1c7ce2b697d97745fa0863b0d38d
@@ -1432,6 +1432,11 @@ function (Assertion $assertion) use ($class_template_params) : Assertion {

if (isset($context->vars_in_scope[$method_var_id])) {
$return_type_candidate = clone $context->vars_in_scope[$method_var_id];

if ($can_memoize) {
/** @psalm-suppress UndefinedPropertyAssignment */
$stmt->pure = true;
}
} elseif ($return_type_candidate) {
$context->vars_in_scope[$method_var_id] = $return_type_candidate;
}
@@ -1847,92 +1847,6 @@ private function registerFunctionLike(PhpParser\Node\FunctionLike $stmt, $fake_m

$storage->required_param_count = $required_param_count;

if (($stmt instanceof PhpParser\Node\Stmt\Function_
|| $stmt instanceof PhpParser\Node\Stmt\ClassMethod)
&& $stmt->stmts
) {
if ($stmt instanceof PhpParser\Node\Stmt\ClassMethod
&& $storage instanceof MethodStorage
&& $class_storage
&& !$class_storage->mutation_free
&& count($stmt->stmts) === 1
&& !count($stmt->params)
&& $stmt->stmts[0] instanceof PhpParser\Node\Stmt\Return_
&& $stmt->stmts[0]->expr instanceof PhpParser\Node\Expr\PropertyFetch
&& $stmt->stmts[0]->expr->var instanceof PhpParser\Node\Expr\Variable
&& $stmt->stmts[0]->expr->var->name === 'this'
) {
$storage->mutation_free = true;
$storage->external_mutation_free = true;
$storage->mutation_free_inferred = true;
} elseif (strpos($stmt->name->name, 'assert') === 0) {
$var_assertions = [];

foreach ($stmt->stmts as $function_stmt) {
if ($function_stmt instanceof PhpParser\Node\Stmt\If_) {
$final_actions = \Psalm\Internal\Analyzer\ScopeAnalyzer::getFinalControlActions(
$function_stmt->stmts,
null,
$this->config->exit_functions,
false,
false
);

if ($final_actions !== [\Psalm\Internal\Analyzer\ScopeAnalyzer::ACTION_END]) {
$var_assertions = [];
break;
}

$if_clauses = \Psalm\Type\Algebra::getFormula(
\spl_object_id($function_stmt->cond),
$function_stmt->cond,
$this->fq_classlike_names
? $this->fq_classlike_names[count($this->fq_classlike_names) - 1]
: null,
$this->file_scanner,
null
);

$negated_formula = \Psalm\Type\Algebra::negateFormula($if_clauses);

$rules = \Psalm\Type\Algebra::getTruthsFromFormula($negated_formula);

if (!$rules) {
$var_assertions = [];
break;
}

foreach ($rules as $var_id => $rule) {
foreach ($rule as $rule_part) {
if (count($rule_part) > 1) {
continue 2;
}
}

if (isset($existing_params[$var_id])) {
$param_offset = $existing_params[$var_id];

$var_assertions[] = new \Psalm\Storage\Assertion(
$param_offset,
$rule
);
} elseif (strpos($var_id, '$this->') === 0) {
$var_assertions[] = new \Psalm\Storage\Assertion(
$var_id,
$rule
);
}
}
} else {
$var_assertions = [];
break;
}
}

$storage->assertions = $var_assertions;
}
}

if (!$this->scan_deep
&& ($stmt instanceof PhpParser\Node\Stmt\Function_
|| $stmt instanceof PhpParser\Node\Stmt\ClassMethod
@@ -2066,6 +1980,93 @@ private function registerFunctionLike(PhpParser\Node\FunctionLike $stmt, $fake_m
$storage->external_mutation_free = true;
}

if (($stmt instanceof PhpParser\Node\Stmt\Function_
|| $stmt instanceof PhpParser\Node\Stmt\ClassMethod)
&& $stmt->stmts
) {
if ($stmt instanceof PhpParser\Node\Stmt\ClassMethod
&& $storage instanceof MethodStorage
&& $class_storage
&& !$class_storage->mutation_free
&& !$storage->mutation_free
&& count($stmt->stmts) === 1
&& !count($stmt->params)
&& $stmt->stmts[0] instanceof PhpParser\Node\Stmt\Return_
&& $stmt->stmts[0]->expr instanceof PhpParser\Node\Expr\PropertyFetch
&& $stmt->stmts[0]->expr->var instanceof PhpParser\Node\Expr\Variable
&& $stmt->stmts[0]->expr->var->name === 'this'
) {
$storage->mutation_free = true;
$storage->external_mutation_free = true;
$storage->mutation_free_inferred = true;
} elseif (strpos($stmt->name->name, 'assert') === 0) {
$var_assertions = [];

foreach ($stmt->stmts as $function_stmt) {
if ($function_stmt instanceof PhpParser\Node\Stmt\If_) {
$final_actions = \Psalm\Internal\Analyzer\ScopeAnalyzer::getFinalControlActions(
$function_stmt->stmts,
null,
$this->config->exit_functions,
false,
false
);

if ($final_actions !== [\Psalm\Internal\Analyzer\ScopeAnalyzer::ACTION_END]) {
$var_assertions = [];
break;
}

$if_clauses = \Psalm\Type\Algebra::getFormula(
\spl_object_id($function_stmt->cond),
$function_stmt->cond,
$this->fq_classlike_names
? $this->fq_classlike_names[count($this->fq_classlike_names) - 1]
: null,
$this->file_scanner,
null
);

$negated_formula = \Psalm\Type\Algebra::negateFormula($if_clauses);

$rules = \Psalm\Type\Algebra::getTruthsFromFormula($negated_formula);

if (!$rules) {
$var_assertions = [];
break;
}

foreach ($rules as $var_id => $rule) {
foreach ($rule as $rule_part) {
if (count($rule_part) > 1) {
continue 2;
}
}

if (isset($existing_params[$var_id])) {
$param_offset = $existing_params[$var_id];

$var_assertions[] = new \Psalm\Storage\Assertion(
$param_offset,
$rule
);
} elseif (strpos($var_id, '$this->') === 0) {
$var_assertions[] = new \Psalm\Storage\Assertion(
$var_id,
$rule
);
}
}
} else {
$var_assertions = [];
break;
}
}

$storage->assertions = $var_assertions;
}
}

if ($docblock_info->deprecated) {
$storage->deprecated = true;
}
@@ -600,34 +600,67 @@ private static function getValueForKey(
if (!$codebase->classOrInterfaceExists($existing_key_type_part->value)) {
$class_property_type = Type::getMixed();
} else {
$property_id = $existing_key_type_part->value . '::$' . $property_name;
if (substr($property_name, -2) === '()') {
$method_id = $existing_key_type_part->value . '::' . substr($property_name, 0, -2);

if (!$codebase->properties->propertyExists($property_id, true)) {
return null;
}
if (!$codebase->methods->methodExists($method_id)) {
return null;
}

$declaring_method_id = $codebase->methods->getDeclaringMethodId(
$method_id
);

$declaring_property_class = $codebase->properties->getDeclaringClassForProperty(
$property_id,
true
);

$class_property_type = $codebase->properties->getPropertyType(
$property_id,
false,
null,
null
);

if ($class_property_type) {
$class_property_type = ExpressionAnalyzer::fleshOutType(
$codebase,
clone $class_property_type,
$declaring_property_class,
$declaring_property_class,
$declaring_class = explode('::', (string) $declaring_method_id)[0];

$method_return_type = $codebase->methods->getMethodReturnType(
$method_id,
$declaring_class,
null,
null
);

if ($method_return_type) {
$class_property_type = ExpressionAnalyzer::fleshOutType(
$codebase,
clone $method_return_type,
$declaring_class,
$declaring_class,
null
);
} else {
$class_property_type = Type::getMixed();
}
} else {
$class_property_type = Type::getMixed();
$property_id = $existing_key_type_part->value . '::$' . $property_name;

if (!$codebase->properties->propertyExists($property_id, true)) {
return null;
}

$declaring_property_class = $codebase->properties->getDeclaringClassForProperty(
$property_id,
true
);

$class_property_type = $codebase->properties->getPropertyType(
$property_id,
false,
null,
null
);

if ($class_property_type) {
$class_property_type = ExpressionAnalyzer::fleshOutType(
$codebase,
clone $class_property_type,
$declaring_property_class,
$declaring_property_class,
null
);
} else {
$class_property_type = Type::getMixed();
}
}
}
} else {
@@ -2390,6 +2390,86 @@ function order(iterable $collection): array {
return $collection;
}'
],
'memoizeChainedImmutableCallsInside' => [
'<?php
class Assessment {
private ?string $root = null;
/** @psalm-mutation-free */
public function getRoot(): ?string {
return $this->root;
}
}
class Project {
private ?Assessment $assessment = null;
/** @psalm-mutation-free */
public function getAssessment(): ?Assessment {
return $this->assessment;
}
}
function f(Project $project): int {
if (($project->getAssessment() !== null)
&& ($project->getAssessment()->getRoot() !== null)
) {
return strlen($project->getAssessment()->getRoot());
}
throw new RuntimeException();
}',
],
'memoizeChainedImmutableCallsOutside' => [
'<?php
class Assessment {
private ?string $root = null;
/** @psalm-mutation-free */
public function getRoot(): ?string {
return $this->root;
}
}
class Project {
private ?Assessment $assessment = null;
/** @psalm-mutation-free */
public function getAssessment(): ?Assessment {
return $this->assessment;
}
}
function f(Project $project): int {
if (($project->getAssessment() === null)
|| ($project->getAssessment()->getRoot() === null)
) {
throw new RuntimeException();
}
return strlen($project->getAssessment()->getRoot());
}',
],
'propertyChainedOutside' => [
'<?php
class Assessment {
public ?string $root = null;
}
class Project {
public ?Assessment $assessment = null;
}
function f(Project $project): int {
if (($project->assessment === null)
|| ($project->assessment->root === null)
) {
throw new RuntimeException();
}
return strlen($project->assessment->root);
}'
],
];
}

0 comments on commit 982fe62

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