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

Fix #1379 - allow @param-out to change type

This commit is contained in:
Matthew Brown 2019-03-03 15:11:09 -05:00
parent 82e6876011
commit 472cdf6bea
8 changed files with 91 additions and 32 deletions

View File

@ -19,6 +19,7 @@ use Psalm\Issue\MismatchingDocblockParamType;
use Psalm\Issue\MissingClosureParamType;
use Psalm\Issue\MissingParamType;
use Psalm\Issue\MissingThrowsDocblock;
use Psalm\Issue\ReferenceConstraintViolation;
use Psalm\Issue\ReservedWord;
use Psalm\Issue\UnusedParam;
use Psalm\IssueBuffer;
@ -540,11 +541,6 @@ abstract class FunctionLikeAnalyzer extends SourceAnalyzer implements Statements
}
}
if ($function_param->by_ref) {
$context->byref_constraints['$' . $function_param->name]
= new \Psalm\Internal\ReferenceConstraint(!$param_type->hasMixed() ? $param_type : null);
}
if ($function_param->by_ref) {
// register by ref params as having been used, to avoid false positives
// @todo change the assignment analysis *just* for byref params
@ -598,7 +594,7 @@ abstract class FunctionLikeAnalyzer extends SourceAnalyzer implements Statements
$statements_analyzer->analyze($function_stmts, $context, $global_context, true);
$this->addPossibleParamTypes($context, $codebase);
$this->examineParamTypes($statements_analyzer, $context, $codebase);
foreach ($storage->params as $offset => $function_param) {
// only complain if there's no type defined by a parent type
@ -981,7 +977,7 @@ abstract class FunctionLikeAnalyzer extends SourceAnalyzer implements Statements
/**
* @return void
*/
public function addPossibleParamTypes(Context $context, Codebase $codebase)
public function examineParamTypes(StatementsAnalyzer $statements_analyzer, Context $context, Codebase $codebase)
{
if ($context->infer_types) {
foreach ($context->possible_param_types as $var_id => $type) {
@ -995,6 +991,44 @@ abstract class FunctionLikeAnalyzer extends SourceAnalyzer implements Statements
$this->possible_param_types[$var_id] = clone $type;
}
}
} else {
$storage = $this->getFunctionLikeStorage($statements_analyzer);
foreach ($storage->params as $i => $param) {
if ($param->by_ref && isset($context->vars_in_scope['$' . $param->name])) {
$actual_type = $context->vars_in_scope['$' . $param->name];
$param_out_type = $param->type;
if (isset($storage->param_out_types[$i])) {
$param_out_type = $storage->param_out_types[$i];
}
if ($param_out_type && !$actual_type->isMixed() && $param->location) {
if (!TypeAnalyzer::isContainedBy(
$codebase,
$actual_type,
$param_out_type,
$actual_type->ignore_nullable_issues,
$actual_type->ignore_falsable_issues
)
) {
if (IssueBuffer::accepts(
new ReferenceConstraintViolation(
'Variable ' . '$' . $param->name . ' is limited to values of type '
. $param_out_type->getId()
. ' because it is passed by reference, '
. $actual_type->getId() . ' type found. Use @param-out to specify '
. 'a different output type',
$param->location
),
$statements_analyzer->getSuppressedIssues()
)) {
// fall through
}
}
}
}
}
}
}

View File

@ -588,6 +588,7 @@ class CallAnalyzer
$statements_analyzer,
$arg->value,
$by_ref_type,
$by_ref_type,
$context,
false
);
@ -701,6 +702,7 @@ class CallAnalyzer
$statements_analyzer,
$array_arg,
$by_ref_type,
$by_ref_type,
$context,
false
);
@ -804,6 +806,7 @@ class CallAnalyzer
$statements_analyzer,
$array_arg,
$by_ref_type,
$by_ref_type,
$context,
false
);
@ -811,10 +814,13 @@ class CallAnalyzer
return;
}
$array_type = Type::getArray();
ExpressionAnalyzer::assignByRefParam(
$statements_analyzer,
$array_arg,
Type::getArray(),
$array_type,
$array_type,
$context,
false
);
@ -1157,7 +1163,7 @@ class CallAnalyzer
$codebase = $statements_analyzer->getCodebase();
if (!isset($arg->value->inferredType)) {
if ($function_param) {
if ($function_param && !$function_param->by_ref) {
$codebase->analyzer->incrementMixedCount($statements_analyzer->getFilePath());
$param_type = $function_param->type;
@ -1270,6 +1276,7 @@ class CallAnalyzer
true
)) {
$by_ref_type = null;
$by_ref_out_type = null;
$check_null_ref = true;
@ -1282,7 +1289,7 @@ class CallAnalyzer
}
if (isset($function_storage->param_out_types[$argument_offset])) {
$by_ref_type = $function_storage->param_out_types[$argument_offset];
$by_ref_out_type = $function_storage->param_out_types[$argument_offset];
}
} else {
$by_ref_type = $last_param->type;
@ -1322,6 +1329,7 @@ class CallAnalyzer
$statements_analyzer,
$arg->value,
$by_ref_type,
$by_ref_out_type ?: $by_ref_type,
$context,
$method_id && (strpos($method_id, '::') !== false || !CallMap::inCallMap($method_id)),
$check_null_ref

View File

@ -166,7 +166,7 @@ class VariableFetchAnalyzer
}
if ($passed_by_reference && $by_ref_type) {
ExpressionAnalyzer::assignByRefParam($statements_analyzer, $stmt, $by_ref_type, $context);
ExpressionAnalyzer::assignByRefParam($statements_analyzer, $stmt, $by_ref_type, $by_ref_type, $context);
return null;
}

View File

@ -644,6 +644,7 @@ class ExpressionAnalyzer
StatementsAnalyzer $statements_analyzer,
PhpParser\Node\Expr $stmt,
Type\Union $by_ref_type,
Type\Union $by_ref_out_type,
Context $context,
bool $constrain_type = true,
bool $prevent_null = false
@ -712,7 +713,7 @@ class ExpressionAnalyzer
}
}
$context->vars_in_scope[$var_id] = $by_ref_type;
$context->vars_in_scope[$var_id] = $by_ref_out_type;
if (!isset($stmt->inferredType) || $stmt->inferredType->isEmpty()) {
$stmt->inferredType = clone $by_ref_type;
@ -1294,7 +1295,7 @@ class ExpressionAnalyzer
if ($source instanceof FunctionLikeAnalyzer
&& !($source->getSource() instanceof TraitAnalyzer)
) {
$source->addPossibleParamTypes($context, $codebase);
$source->examineParamTypes($statements_analyzer, $context, $codebase);
$storage = $source->getFunctionLikeStorage($statements_analyzer);

View File

@ -124,7 +124,7 @@ class ReturnAnalyzer
$context
);
$source->addPossibleParamTypes($context, $codebase);
$source->examineParamTypes($statements_analyzer, $context, $codebase);
$storage = $source->getFunctionLikeStorage($statements_analyzer);

View File

@ -1529,8 +1529,6 @@ class TypeAnalyzer
* @param bool &$all_types_contain
*
* @return null|false
*
* @psalm-suppress ConflictingReferenceConstraint
*/
private static function compareCallable(
Codebase $codebase,

View File

@ -827,22 +827,6 @@ class AnnotationTest extends TestCase
}
}'
],
'paramOutChangeType' => [
'<?php
/**
* @param-out string $s
*/
function addFoo(?string &$s) : void {
if ($s === null) {
$s = "hello";
}
$s .= "foo";
}
addFoo($a);
echo strlen($a);',
],
'annotationOnForeachItems' => [
'<?php
function foo(array $arr) : void {

View File

@ -82,6 +82,40 @@ class ReferenceConstraintTest extends TestCase
'MixedOperand',
],
],
'paramOutRefineType' => [
'<?php
/**
* @param-out string $s
*/
function addFoo(?string &$s) : void {
if ($s === null) {
$s = "hello";
}
$s .= "foo";
}
addFoo($a);
echo strlen($a);',
],
'paramOutChangeType' => [
'<?php
/**
* @param-out int $s
*/
function addFoo(?string &$s) : void {
if ($s === null) {
$s = 5;
return;
}
$s = 4;
}
addFoo($a);',
'assertions' => [
'$a' => 'int',
],
],
];
}