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

Fix #2712 - allow __toString to have more specific type

This commit is contained in:
Matthew Brown 2020-01-29 22:28:40 -05:00
parent 4b7780905e
commit 933dff9e20
5 changed files with 394 additions and 354 deletions

View File

@ -125,7 +125,7 @@ class ErrorBaseline
$codeSamples = $issue->getElementsByTagName('code');
foreach ($codeSamples as $codeSample) {
$files[$fileName][$issueType]['s'][] = (string) $codeSample->textContent;
$files[$fileName][$issueType]['s'][] = $codeSample->textContent;
}
}
}

View File

@ -1070,12 +1070,6 @@ class MethodCallAnalyzer extends \Psalm\Internal\Analyzer\Statements\Expression\
return false;
}
switch (strtolower($stmt->name->name)) {
case '__tostring':
$return_type = Type::getString();
return;
}
$declaring_method_id = $codebase->methods->getDeclaringMethodId($method_id);
$call_map_id = strtolower(
@ -1084,9 +1078,6 @@ class MethodCallAnalyzer extends \Psalm\Internal\Analyzer\Statements\Expression\
$can_memoize = false;
if ($method_name_lc === '__tostring') {
$return_type_candidate = Type::getString();
} else {
$return_type_candidate = null;
if ($codebase->methods->return_type_provider->has($fq_class_name)) {
@ -1450,7 +1441,6 @@ class MethodCallAnalyzer extends \Psalm\Internal\Analyzer\Statements\Expression\
}
}
}
}
if ($old_node_data) {
$statements_analyzer->node_data = $old_node_data;
@ -1540,6 +1530,8 @@ class MethodCallAnalyzer extends \Psalm\Internal\Analyzer\Statements\Expression\
} else {
$return_type = Type::combineUnionTypes($all_intersection_return_type, $return_type);
}
} elseif (strtolower($stmt->name->name) === '__tostring') {
$return_type = Type::getString();
} else {
$return_type = Type::getMixed();
}

View File

@ -601,10 +601,10 @@ class ExpressionAnalyzer
}
if ($statements_analyzer->node_data->getType($stmt->expr)) {
self::castStringAttempt($statements_analyzer, $stmt->expr);
}
$stmt_type = self::castStringAttempt($statements_analyzer, $stmt->expr, true);
} else {
$stmt_type = Type::getString();
}
$statements_analyzer->node_data->setType($stmt, $stmt_type);
@ -1887,44 +1887,66 @@ class ExpressionAnalyzer
return null;
}
/**
* @return void
*/
private static function castStringAttempt(
StatementsAnalyzer $statements_analyzer,
PhpParser\Node\Expr $stmt
) {
PhpParser\Node\Expr $stmt,
bool $explicit_cast = false
) : Type\Union {
$codebase = $statements_analyzer->getCodebase();
if (!($stmt_type = $statements_analyzer->node_data->getType($stmt))) {
return;
return Type::getString();
}
$has_valid_cast = false;
$invalid_casts = [];
$valid_strings = [];
$castable_types = [];
foreach ($stmt_type->getAtomicTypes() as $atomic_type) {
$atomic_type_results = new \Psalm\Internal\Analyzer\TypeComparisonResult();
if (!$atomic_type instanceof TMixed
&& !$atomic_type instanceof Type\Atomic\TResource
&& !$atomic_type instanceof TNull
&& !TypeAnalyzer::isAtomicContainedBy(
$statements_analyzer->getCodebase(),
$atomic_type,
new TString(),
false,
true,
$atomic_type_results
)
&& !$atomic_type_results->scalar_type_match_found
) {
$invalid_casts[] = $atomic_type->getId();
} else {
$has_valid_cast = true;
if ($atomic_type instanceof TString) {
$valid_strings[] = $atomic_type;
continue;
}
if ($atomic_type instanceof TMixed
|| $atomic_type instanceof Type\Atomic\TResource
|| $atomic_type instanceof Type\Atomic\TNull
|| $atomic_type instanceof Type\Atomic\Scalar
) {
$castable_types[] = new TString();
continue;
}
if ($atomic_type instanceof TNamedObject
&& $codebase->methods->methodExists($atomic_type->value . '::__tostring')
) {
$return_type = $codebase->methods->getMethodReturnType($atomic_type->value . '::__tostring', $self_class);
if ($return_type) {
$castable_types = array_merge(
$castable_types,
array_values($return_type->getAtomicTypes())
);
} else {
$castable_types[] = new TString();
}
continue;
}
if ($atomic_type instanceof Type\Atomic\TObjectWithProperties
&& isset($atomic_type->methods['__toString'])
) {
$castable_types[] = new TString();
continue;
}
$invalid_casts[] = $atomic_type->getId();
}
if ($invalid_casts) {
if ($has_valid_cast) {
if ($valid_strings || $castable_types) {
if (IssueBuffer::accepts(
new PossiblyInvalidCast(
$invalid_casts[0] . ' cannot be cast to string',
@ -1945,7 +1967,14 @@ class ExpressionAnalyzer
// fall through
}
}
} elseif ($explicit_cast && !$castable_types) {
// todo: emit error here
}
return \Psalm\Internal\Type\TypeCombination::combineTypes(
array_merge($valid_strings, $castable_types),
$codebase
);
}
/**

View File

@ -65,7 +65,7 @@ class DocumentationTest extends TestCase
++$i;
} while (substr($file_lines[$i], 0, 3) !== '```' && $i < $j);
$issue_code[(string) $current_issue][] = trim($current_block);
$issue_code[$current_issue][] = trim($current_block);
}
}

View File

@ -100,6 +100,25 @@ class ToStringTest extends TestCase
return null;
}'
],
'refineToStringType' => [
'<?php
/** @psalm-return non-empty-string */
function doesCast() : string {
return (string) (new A());
}
/** @psalm-return non-empty-string */
function callsToString() : string {
return (new A())->__toString();
}
class A {
/** @psalm-return non-empty-string */
function __toString(): string {
return "ha";
}
}'
],
];
}