2020-03-11 14:38:09 +01:00
|
|
|
<?php
|
2020-05-19 01:10:48 +02:00
|
|
|
namespace Psalm\Internal\Analyzer\Statements\Expression\Call\Method;
|
2020-03-11 14:38:09 +01:00
|
|
|
|
|
|
|
use PhpParser;
|
|
|
|
use Psalm\Internal\Analyzer\ClassLikeAnalyzer;
|
|
|
|
use Psalm\Internal\Analyzer\FunctionLikeAnalyzer;
|
|
|
|
use Psalm\Internal\Analyzer\MethodAnalyzer;
|
|
|
|
use Psalm\Internal\Analyzer\Statements\ExpressionAnalyzer;
|
2020-03-12 04:04:52 +01:00
|
|
|
use Psalm\Internal\Analyzer\Statements\Expression\CallAnalyzer;
|
2020-05-19 01:10:48 +02:00
|
|
|
use Psalm\Internal\Analyzer\Statements\Expression\Call\ClassTemplateParamCollector;
|
2020-05-19 04:57:00 +02:00
|
|
|
use Psalm\Internal\Analyzer\Statements\Expression\Call\ArgumentsAnalyzer;
|
2020-03-11 14:38:09 +01:00
|
|
|
use Psalm\Internal\Analyzer\StatementsAnalyzer;
|
2020-05-25 19:10:06 +02:00
|
|
|
use Psalm\Internal\Codebase\InternalCallMapHandler;
|
2021-03-21 02:45:38 +01:00
|
|
|
use Psalm\Internal\Codebase\VariableUseGraph;
|
2020-03-11 14:38:09 +01:00
|
|
|
use Psalm\Codebase;
|
|
|
|
use Psalm\CodeLocation;
|
|
|
|
use Psalm\Context;
|
2020-03-11 17:24:25 +01:00
|
|
|
use Psalm\Internal\MethodIdentifier;
|
2020-03-11 14:38:09 +01:00
|
|
|
use Psalm\Issue\MixedMethodCall;
|
|
|
|
use Psalm\IssueBuffer;
|
|
|
|
use Psalm\Type;
|
|
|
|
use Psalm\Type\Atomic\TNamedObject;
|
|
|
|
use function array_values;
|
|
|
|
use function array_shift;
|
|
|
|
use function get_class;
|
|
|
|
use function strtolower;
|
|
|
|
use function array_merge;
|
|
|
|
|
2020-11-08 19:08:45 +01:00
|
|
|
/**
|
|
|
|
* This is a bunch of complex logic to handle the potential for missing methods,
|
|
|
|
* use of intersection types and/or mixins, together with handling for fallback magic
|
|
|
|
* methods.
|
|
|
|
*
|
|
|
|
* The happy path (i.e 99% of method calls) is handled in ExistingAtomicMethodCallAnalyzer
|
|
|
|
*/
|
2020-03-12 04:04:52 +01:00
|
|
|
class AtomicMethodCallAnalyzer extends CallAnalyzer
|
2020-03-11 14:38:09 +01:00
|
|
|
{
|
|
|
|
/**
|
|
|
|
* @param Type\Atomic\TNamedObject|Type\Atomic\TTemplateParam $static_type
|
2021-01-06 01:03:50 +01:00
|
|
|
*
|
|
|
|
* @psalm-suppress ComplexMethod it's really complex, but unavoidably so
|
2020-03-11 14:38:09 +01:00
|
|
|
*/
|
|
|
|
public static function analyze(
|
|
|
|
StatementsAnalyzer $statements_analyzer,
|
|
|
|
PhpParser\Node\Expr\MethodCall $stmt,
|
|
|
|
Codebase $codebase,
|
|
|
|
Context $context,
|
2021-03-21 02:45:38 +01:00
|
|
|
Type\Union $lhs_type,
|
2020-03-11 14:38:09 +01:00
|
|
|
Type\Atomic $lhs_type_part,
|
|
|
|
?Type\Atomic $static_type,
|
|
|
|
bool $is_intersection,
|
2020-09-07 01:36:47 +02:00
|
|
|
?string $lhs_var_id,
|
2020-03-11 14:38:09 +01:00
|
|
|
AtomicMethodCallAnalysisResult $result
|
|
|
|
) : void {
|
|
|
|
if ($lhs_type_part instanceof Type\Atomic\TTemplateParam
|
|
|
|
&& !$lhs_type_part->as->isMixed()
|
|
|
|
) {
|
|
|
|
$extra_types = $lhs_type_part->extra_types;
|
|
|
|
|
|
|
|
$lhs_type_part = array_values(
|
|
|
|
$lhs_type_part->as->getAtomicTypes()
|
|
|
|
)[0];
|
|
|
|
|
|
|
|
$lhs_type_part->from_docblock = true;
|
|
|
|
|
|
|
|
if ($lhs_type_part instanceof TNamedObject) {
|
|
|
|
$lhs_type_part->extra_types = $extra_types;
|
|
|
|
} elseif ($lhs_type_part instanceof Type\Atomic\TObject && $extra_types) {
|
|
|
|
$lhs_type_part = array_shift($extra_types);
|
|
|
|
if ($extra_types) {
|
|
|
|
$lhs_type_part->extra_types = $extra_types;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
$result->has_mixed_method_call = true;
|
|
|
|
}
|
|
|
|
|
|
|
|
$source = $statements_analyzer->getSource();
|
|
|
|
|
|
|
|
if (!$lhs_type_part instanceof TNamedObject) {
|
2020-03-11 18:35:33 +01:00
|
|
|
self::handleInvalidClass(
|
|
|
|
$statements_analyzer,
|
|
|
|
$codebase,
|
|
|
|
$stmt,
|
2021-03-21 02:45:38 +01:00
|
|
|
$lhs_type,
|
2020-03-11 18:35:33 +01:00
|
|
|
$lhs_type_part,
|
|
|
|
$lhs_var_id,
|
|
|
|
$context,
|
|
|
|
$is_intersection,
|
|
|
|
$result
|
|
|
|
);
|
2020-03-11 14:38:09 +01:00
|
|
|
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!$context->collect_initializations
|
|
|
|
&& !$context->collect_mutations
|
|
|
|
&& $statements_analyzer->getFilePath() === $statements_analyzer->getRootFilePath()
|
|
|
|
&& (!(($parent_source = $statements_analyzer->getSource())
|
|
|
|
instanceof \Psalm\Internal\Analyzer\FunctionLikeAnalyzer)
|
|
|
|
|| !$parent_source->getSource() instanceof \Psalm\Internal\Analyzer\TraitAnalyzer)
|
|
|
|
) {
|
|
|
|
$codebase->analyzer->incrementNonMixedCount($statements_analyzer->getFilePath());
|
|
|
|
}
|
|
|
|
|
|
|
|
$result->has_valid_method_call_type = true;
|
|
|
|
|
|
|
|
$fq_class_name = $lhs_type_part->value;
|
|
|
|
|
|
|
|
$is_mock = ExpressionAnalyzer::isMock($fq_class_name);
|
|
|
|
|
|
|
|
$result->has_mock = $result->has_mock || $is_mock;
|
|
|
|
|
|
|
|
if ($fq_class_name === 'static') {
|
|
|
|
$fq_class_name = (string) $context->self;
|
|
|
|
}
|
|
|
|
|
|
|
|
if ($is_mock ||
|
|
|
|
$context->isPhantomClass($fq_class_name)
|
|
|
|
) {
|
|
|
|
$result->return_type = Type::getMixed();
|
|
|
|
|
2020-05-19 04:57:00 +02:00
|
|
|
ArgumentsAnalyzer::analyze(
|
2020-03-11 14:38:09 +01:00
|
|
|
$statements_analyzer,
|
|
|
|
$stmt->args,
|
|
|
|
null,
|
|
|
|
null,
|
2020-10-03 02:58:51 +02:00
|
|
|
true,
|
2020-03-11 14:38:09 +01:00
|
|
|
$context
|
|
|
|
);
|
|
|
|
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
if ($lhs_var_id === '$this') {
|
|
|
|
$does_class_exist = true;
|
|
|
|
} else {
|
|
|
|
$does_class_exist = ClassLikeAnalyzer::checkFullyQualifiedClassLikeName(
|
|
|
|
$statements_analyzer,
|
|
|
|
$fq_class_name,
|
|
|
|
new CodeLocation($source, $stmt->var),
|
|
|
|
$context->self,
|
2020-03-26 17:35:27 +01:00
|
|
|
$context->calling_method_id,
|
2020-03-11 14:38:09 +01:00
|
|
|
$statements_analyzer->getSuppressedIssues(),
|
|
|
|
true,
|
|
|
|
false,
|
|
|
|
true,
|
|
|
|
$lhs_type_part->from_docblock
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!$does_class_exist) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
$class_storage = $codebase->classlike_storage_provider->get($fq_class_name);
|
|
|
|
|
|
|
|
$result->check_visibility = $result->check_visibility && !$class_storage->override_method_visibility;
|
|
|
|
|
|
|
|
$intersection_types = $lhs_type_part->getIntersectionTypes();
|
|
|
|
|
|
|
|
if (!$stmt->name instanceof PhpParser\Node\Identifier) {
|
|
|
|
if (!$context->ignore_variable_method) {
|
|
|
|
$codebase->analyzer->addMixedMemberName(
|
|
|
|
strtolower($fq_class_name) . '::',
|
2020-03-26 17:35:27 +01:00
|
|
|
$context->calling_method_id ?: $statements_analyzer->getFileName()
|
2020-03-11 14:38:09 +01:00
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2020-09-28 06:45:02 +02:00
|
|
|
ArgumentsAnalyzer::analyze(
|
|
|
|
$statements_analyzer,
|
|
|
|
$stmt->args,
|
|
|
|
null,
|
|
|
|
null,
|
2020-10-03 02:58:51 +02:00
|
|
|
true,
|
2020-09-28 06:45:02 +02:00
|
|
|
$context
|
|
|
|
);
|
|
|
|
|
2020-03-11 14:38:09 +01:00
|
|
|
$result->return_type = Type::getMixed();
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
$method_name_lc = strtolower($stmt->name->name);
|
|
|
|
|
2020-03-11 17:24:25 +01:00
|
|
|
$method_id = new MethodIdentifier($fq_class_name, $method_name_lc);
|
2020-03-11 14:38:09 +01:00
|
|
|
|
|
|
|
$args = $stmt->args;
|
|
|
|
|
2020-03-28 21:30:56 +01:00
|
|
|
$naive_method_id = $method_id;
|
|
|
|
|
2021-01-06 01:03:50 +01:00
|
|
|
// this tells us whether or not we can stay on the happy path
|
2020-03-28 21:30:56 +01:00
|
|
|
$naive_method_exists = $codebase->methods->methodExists(
|
2020-03-11 14:38:09 +01:00
|
|
|
$method_id,
|
2020-03-26 17:35:27 +01:00
|
|
|
$context->calling_method_id,
|
2020-03-28 21:30:56 +01:00
|
|
|
$codebase->collect_locations
|
|
|
|
? new CodeLocation($source, $stmt->name)
|
|
|
|
: null,
|
2020-03-11 14:38:09 +01:00
|
|
|
!$context->collect_initializations
|
|
|
|
&& !$context->collect_mutations
|
|
|
|
? $statements_analyzer
|
|
|
|
: null,
|
2020-09-01 00:56:21 +02:00
|
|
|
$statements_analyzer->getFilePath(),
|
|
|
|
false
|
2020-03-28 21:30:56 +01:00
|
|
|
);
|
|
|
|
|
2020-09-01 00:56:21 +02:00
|
|
|
$fake_method_exists = false;
|
|
|
|
|
2020-10-15 15:21:44 +02:00
|
|
|
if (!$naive_method_exists) {
|
2021-01-06 01:03:50 +01:00
|
|
|
// if the method doesn't exist we check for any method existence providers
|
|
|
|
if ($codebase->methods->existence_provider->has($fq_class_name)) {
|
|
|
|
$method_exists = $codebase->methods->existence_provider->doesMethodExist(
|
2020-10-15 15:21:44 +02:00
|
|
|
$fq_class_name,
|
2021-01-06 01:03:50 +01:00
|
|
|
$method_id->method_name,
|
|
|
|
$source,
|
|
|
|
null
|
2020-05-27 05:32:01 +02:00
|
|
|
);
|
2021-01-06 01:03:50 +01:00
|
|
|
|
|
|
|
if ($method_exists) {
|
|
|
|
$fake_method_exists = true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
$naive_method_exists = false;
|
|
|
|
|
|
|
|
// @mixin attributes are an absolute pain! Lots of complexity here,
|
|
|
|
// as they can redefine the called class, method id etc.
|
|
|
|
if ($class_storage->templatedMixins
|
|
|
|
&& $lhs_type_part instanceof Type\Atomic\TGenericObject
|
|
|
|
&& $class_storage->template_types
|
|
|
|
) {
|
|
|
|
[$lhs_type_part, $class_storage, $naive_method_exists, $method_id, $fq_class_name]
|
|
|
|
= self::handleTemplatedMixins(
|
|
|
|
$class_storage,
|
|
|
|
$lhs_type_part,
|
|
|
|
$method_name_lc,
|
|
|
|
$codebase,
|
|
|
|
$context,
|
|
|
|
$method_id,
|
|
|
|
$source,
|
|
|
|
$stmt,
|
|
|
|
$statements_analyzer,
|
|
|
|
$fq_class_name
|
|
|
|
);
|
|
|
|
} elseif ($class_storage->mixin_declaring_fqcln
|
|
|
|
&& $class_storage->namedMixins
|
|
|
|
) {
|
|
|
|
[$lhs_type_part, $class_storage, $naive_method_exists, $method_id, $fq_class_name]
|
|
|
|
= self::handleRegularMixins(
|
|
|
|
$class_storage,
|
|
|
|
$lhs_type_part,
|
|
|
|
$method_name_lc,
|
|
|
|
$codebase,
|
|
|
|
$context,
|
|
|
|
$method_id,
|
|
|
|
$source,
|
|
|
|
$stmt,
|
|
|
|
$statements_analyzer,
|
|
|
|
$fq_class_name,
|
|
|
|
$lhs_var_id
|
|
|
|
);
|
|
|
|
}
|
2020-04-26 22:49:52 +02:00
|
|
|
}
|
|
|
|
|
2020-11-27 21:25:16 +01:00
|
|
|
$all_intersection_return_type = null;
|
|
|
|
$all_intersection_existent_method_ids = [];
|
|
|
|
|
2021-01-06 01:03:50 +01:00
|
|
|
// insersection types are also fun, they also complicate matters
|
2020-11-27 21:25:16 +01:00
|
|
|
if ($intersection_types) {
|
|
|
|
[$all_intersection_return_type, $all_intersection_existent_method_ids]
|
|
|
|
= self::getIntersectionReturnType(
|
|
|
|
$statements_analyzer,
|
|
|
|
$stmt,
|
|
|
|
$codebase,
|
|
|
|
$context,
|
2021-03-21 02:45:38 +01:00
|
|
|
$lhs_type,
|
2020-11-27 21:25:16 +01:00
|
|
|
$lhs_type_part,
|
|
|
|
$lhs_var_id,
|
|
|
|
$result,
|
|
|
|
$intersection_types
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2020-09-01 00:56:21 +02:00
|
|
|
if (($fake_method_exists
|
|
|
|
&& $codebase->methods->methodExists(new MethodIdentifier($fq_class_name, '__call')))
|
|
|
|
|| !$naive_method_exists
|
2020-03-11 14:38:09 +01:00
|
|
|
|| !MethodAnalyzer::isMethodVisible(
|
|
|
|
$method_id,
|
|
|
|
$context,
|
|
|
|
$statements_analyzer->getSource()
|
|
|
|
)
|
|
|
|
) {
|
|
|
|
$interface_has_method = false;
|
|
|
|
|
|
|
|
if ($class_storage->abstract && $class_storage->class_implements) {
|
|
|
|
foreach ($class_storage->class_implements as $interface_fqcln_lc => $_) {
|
|
|
|
$interface_storage = $codebase->classlike_storage_provider->get($interface_fqcln_lc);
|
|
|
|
|
|
|
|
if (isset($interface_storage->methods[$method_name_lc])) {
|
|
|
|
$interface_has_method = true;
|
|
|
|
$fq_class_name = $interface_storage->name;
|
2020-03-11 17:24:25 +01:00
|
|
|
$method_id = new MethodIdentifier(
|
2020-03-11 14:38:09 +01:00
|
|
|
$fq_class_name,
|
|
|
|
$method_name_lc
|
|
|
|
);
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!$interface_has_method
|
|
|
|
&& $codebase->methods->methodExists(
|
2020-03-11 17:24:25 +01:00
|
|
|
new MethodIdentifier($fq_class_name, '__call'),
|
2020-05-13 04:39:18 +02:00
|
|
|
$context->calling_method_id,
|
|
|
|
$codebase->collect_locations
|
|
|
|
? new CodeLocation($source, $stmt->name)
|
|
|
|
: null,
|
|
|
|
!$context->collect_initializations
|
|
|
|
&& !$context->collect_mutations
|
|
|
|
? $statements_analyzer
|
|
|
|
: null,
|
|
|
|
$statements_analyzer->getFilePath()
|
2020-03-11 14:38:09 +01:00
|
|
|
)
|
|
|
|
) {
|
2020-03-12 04:04:52 +01:00
|
|
|
$new_call_context = MissingMethodCallHandler::handleMagicMethod(
|
2020-03-11 18:35:33 +01:00
|
|
|
$statements_analyzer,
|
|
|
|
$codebase,
|
|
|
|
$stmt,
|
|
|
|
$method_id,
|
|
|
|
$class_storage,
|
|
|
|
$context,
|
2020-11-27 21:25:16 +01:00
|
|
|
$codebase->config,
|
2020-03-11 18:35:33 +01:00
|
|
|
$all_intersection_return_type,
|
|
|
|
$result
|
|
|
|
);
|
2020-03-11 14:38:09 +01:00
|
|
|
|
2020-03-11 18:35:33 +01:00
|
|
|
if ($new_call_context) {
|
|
|
|
if ($method_id === $new_call_context->method_id) {
|
2020-03-11 14:38:09 +01:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2020-03-11 18:35:33 +01:00
|
|
|
$method_id = $new_call_context->method_id;
|
|
|
|
$args = $new_call_context->args;
|
2020-03-11 23:42:41 +01:00
|
|
|
} else {
|
|
|
|
return;
|
2020-03-11 14:38:09 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-11-27 21:25:16 +01:00
|
|
|
$intersection_method_id = $intersection_types
|
|
|
|
? '(' . $lhs_type_part . ')' . '::' . $stmt->name->name
|
|
|
|
: null;
|
|
|
|
$cased_method_id = $fq_class_name . '::' . $stmt->name->name;
|
|
|
|
|
2020-03-11 14:38:09 +01:00
|
|
|
if ($lhs_var_id === '$this'
|
|
|
|
&& $context->self
|
|
|
|
&& $fq_class_name !== $context->self
|
|
|
|
&& $codebase->methods->methodExists(
|
2020-03-11 17:24:25 +01:00
|
|
|
new MethodIdentifier($context->self, $method_name_lc)
|
2020-03-11 14:38:09 +01:00
|
|
|
)
|
|
|
|
) {
|
2020-03-11 17:24:25 +01:00
|
|
|
$method_id = new MethodIdentifier($context->self, $method_name_lc);
|
2020-03-11 14:38:09 +01:00
|
|
|
$cased_method_id = $context->self . '::' . $stmt->name->name;
|
|
|
|
$fq_class_name = $context->self;
|
|
|
|
}
|
|
|
|
|
|
|
|
$source_method_id = $source instanceof FunctionLikeAnalyzer
|
|
|
|
? $source->getId()
|
|
|
|
: null;
|
|
|
|
|
2020-03-28 21:30:56 +01:00
|
|
|
$corrected_method_exists = ($naive_method_exists && $method_id === $naive_method_id)
|
|
|
|
|| ($method_id !== $naive_method_id
|
|
|
|
&& $codebase->methods->methodExists(
|
|
|
|
$method_id,
|
|
|
|
$context->calling_method_id,
|
|
|
|
$codebase->collect_locations && $method_id !== $source_method_id
|
|
|
|
? new CodeLocation($source, $stmt->name)
|
|
|
|
: null
|
|
|
|
));
|
|
|
|
|
|
|
|
if (!$corrected_method_exists
|
2020-11-27 21:25:16 +01:00
|
|
|
|| ($codebase->config->use_phpdoc_method_without_magic_or_parent
|
2020-03-11 14:38:09 +01:00
|
|
|
&& isset($class_storage->pseudo_methods[$method_name_lc]))
|
|
|
|
) {
|
2020-03-12 04:04:52 +01:00
|
|
|
MissingMethodCallHandler::handleMissingOrMagicMethod(
|
2020-03-11 14:38:09 +01:00
|
|
|
$statements_analyzer,
|
2020-03-11 18:35:33 +01:00
|
|
|
$codebase,
|
|
|
|
$stmt,
|
|
|
|
$method_id,
|
2021-01-06 01:03:50 +01:00
|
|
|
$codebase->interfaceExists($fq_class_name),
|
2020-03-11 18:35:33 +01:00
|
|
|
$context,
|
2020-11-27 21:25:16 +01:00
|
|
|
$codebase->config,
|
2020-03-11 18:35:33 +01:00
|
|
|
$all_intersection_return_type,
|
2020-11-27 21:25:16 +01:00
|
|
|
$all_intersection_existent_method_ids,
|
|
|
|
$intersection_method_id,
|
|
|
|
$cased_method_id,
|
2020-03-11 18:35:33 +01:00
|
|
|
$result
|
|
|
|
);
|
2020-03-11 14:38:09 +01:00
|
|
|
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2020-11-08 18:29:23 +01:00
|
|
|
$old_node_data = $statements_analyzer->node_data;
|
2020-03-11 14:38:09 +01:00
|
|
|
|
2020-11-08 18:29:23 +01:00
|
|
|
$return_type_candidate = ExistingAtomicMethodCallAnalyzer::analyze(
|
2020-05-22 23:05:39 +02:00
|
|
|
$statements_analyzer,
|
|
|
|
$stmt,
|
2020-11-08 18:29:23 +01:00
|
|
|
$stmt->name,
|
|
|
|
$args,
|
|
|
|
$codebase,
|
2020-05-22 23:05:39 +02:00
|
|
|
$context,
|
|
|
|
$lhs_type_part,
|
|
|
|
$static_type,
|
2020-11-08 18:29:23 +01:00
|
|
|
$lhs_var_id,
|
|
|
|
$method_id,
|
|
|
|
$result
|
2020-05-22 23:05:39 +02:00
|
|
|
);
|
2020-03-11 14:38:09 +01:00
|
|
|
|
2020-11-08 18:29:23 +01:00
|
|
|
$statements_analyzer->node_data = $old_node_data;
|
2020-03-15 18:44:00 +01:00
|
|
|
|
2020-11-27 21:25:16 +01:00
|
|
|
$declaring_method_id = $codebase->methods->getDeclaringMethodId($method_id);
|
|
|
|
|
|
|
|
$in_call_map = InternalCallMapHandler::inCallMap((string) ($declaring_method_id ?: $method_id));
|
|
|
|
|
2020-03-15 18:44:00 +01:00
|
|
|
if (!$in_call_map) {
|
2020-03-11 17:24:25 +01:00
|
|
|
if ($result->check_visibility) {
|
2020-11-08 18:29:23 +01:00
|
|
|
$name_code_location = new CodeLocation($statements_analyzer, $stmt->name);
|
|
|
|
|
|
|
|
MethodVisibilityAnalyzer::analyze(
|
2020-03-11 14:38:09 +01:00
|
|
|
$method_id,
|
2020-03-11 17:24:25 +01:00
|
|
|
$context,
|
|
|
|
$statements_analyzer->getSource(),
|
2020-03-11 14:38:09 +01:00
|
|
|
$name_code_location,
|
|
|
|
$statements_analyzer->getSuppressedIssues()
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-03-23 18:29:00 +01:00
|
|
|
self::updateResultReturnType(
|
|
|
|
$result,
|
|
|
|
$return_type_candidate,
|
|
|
|
$all_intersection_return_type,
|
2020-04-07 06:13:56 +02:00
|
|
|
$method_name_lc,
|
|
|
|
$codebase
|
2020-03-23 18:29:00 +01:00
|
|
|
);
|
2020-03-23 18:24:36 +01:00
|
|
|
}
|
|
|
|
|
2020-11-27 21:25:16 +01:00
|
|
|
/**
|
|
|
|
* @param Type\Atomic\TNamedObject|Type\Atomic\TTemplateParam $lhs_type_part
|
|
|
|
* @param array<string, Type\Atomic> $intersection_types
|
|
|
|
*
|
|
|
|
* @return array{?Type\Union, array<string>}
|
|
|
|
*/
|
|
|
|
private static function getIntersectionReturnType(
|
|
|
|
StatementsAnalyzer $statements_analyzer,
|
|
|
|
PhpParser\Node\Expr\MethodCall $stmt,
|
|
|
|
Codebase $codebase,
|
|
|
|
Context $context,
|
2021-03-21 02:45:38 +01:00
|
|
|
Type\Union $lhs_type,
|
2020-11-27 21:25:16 +01:00
|
|
|
Type\Atomic $lhs_type_part,
|
|
|
|
?string $lhs_var_id,
|
|
|
|
AtomicMethodCallAnalysisResult $result,
|
|
|
|
array $intersection_types
|
|
|
|
) : array {
|
|
|
|
$all_intersection_return_type = null;
|
|
|
|
$all_intersection_existent_method_ids = [];
|
|
|
|
|
|
|
|
foreach ($intersection_types as $intersection_type) {
|
|
|
|
$intersection_result = clone $result;
|
|
|
|
|
|
|
|
/** @var ?Type\Union */
|
|
|
|
$intersection_result->return_type = null;
|
|
|
|
|
|
|
|
self::analyze(
|
|
|
|
$statements_analyzer,
|
|
|
|
$stmt,
|
|
|
|
$codebase,
|
|
|
|
$context,
|
2021-03-21 02:45:38 +01:00
|
|
|
$lhs_type,
|
2020-11-27 21:25:16 +01:00
|
|
|
$intersection_type,
|
|
|
|
$lhs_type_part,
|
|
|
|
true,
|
|
|
|
$lhs_var_id,
|
|
|
|
$intersection_result
|
|
|
|
);
|
|
|
|
|
|
|
|
$result->returns_by_ref = $intersection_result->returns_by_ref;
|
|
|
|
$result->has_mock = $intersection_result->has_mock;
|
|
|
|
$result->has_valid_method_call_type = $intersection_result->has_valid_method_call_type;
|
|
|
|
$result->has_mixed_method_call = $intersection_result->has_mixed_method_call;
|
|
|
|
$result->invalid_method_call_types = $intersection_result->invalid_method_call_types;
|
|
|
|
$result->check_visibility = $intersection_result->check_visibility;
|
|
|
|
$result->too_many_arguments = $intersection_result->too_many_arguments;
|
|
|
|
|
|
|
|
$all_intersection_existent_method_ids = array_merge(
|
|
|
|
$all_intersection_existent_method_ids,
|
|
|
|
$intersection_result->existent_method_ids
|
|
|
|
);
|
|
|
|
|
|
|
|
if ($intersection_result->return_type) {
|
|
|
|
if (!$all_intersection_return_type || $all_intersection_return_type->isMixed()) {
|
|
|
|
$all_intersection_return_type = $intersection_result->return_type;
|
|
|
|
} else {
|
|
|
|
$all_intersection_return_type = Type::intersectUnionTypes(
|
|
|
|
$all_intersection_return_type,
|
|
|
|
$intersection_result->return_type,
|
|
|
|
$codebase
|
|
|
|
) ?: Type::getMixed();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return [$all_intersection_return_type, $all_intersection_existent_method_ids];
|
|
|
|
}
|
|
|
|
|
2020-03-23 18:24:36 +01:00
|
|
|
private static function updateResultReturnType(
|
|
|
|
AtomicMethodCallAnalysisResult $result,
|
|
|
|
?Type\Union $return_type_candidate,
|
2020-03-23 18:29:00 +01:00
|
|
|
?Type\Union $all_intersection_return_type,
|
2020-04-07 06:13:56 +02:00
|
|
|
string $method_name,
|
|
|
|
Codebase $codebase
|
2020-03-23 18:29:00 +01:00
|
|
|
) : void {
|
2020-03-11 14:38:09 +01:00
|
|
|
if ($return_type_candidate) {
|
|
|
|
if ($all_intersection_return_type) {
|
|
|
|
$return_type_candidate = Type::intersectUnionTypes(
|
|
|
|
$all_intersection_return_type,
|
2020-04-07 06:13:56 +02:00
|
|
|
$return_type_candidate,
|
|
|
|
$codebase
|
2020-03-11 14:38:09 +01:00
|
|
|
) ?: Type::getMixed();
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!$result->return_type) {
|
|
|
|
$result->return_type = $return_type_candidate;
|
|
|
|
} else {
|
|
|
|
$result->return_type = Type::combineUnionTypes($return_type_candidate, $result->return_type);
|
|
|
|
}
|
|
|
|
} elseif ($all_intersection_return_type) {
|
|
|
|
if (!$result->return_type) {
|
|
|
|
$result->return_type = $all_intersection_return_type;
|
|
|
|
} else {
|
|
|
|
$result->return_type = Type::combineUnionTypes($all_intersection_return_type, $result->return_type);
|
|
|
|
}
|
2020-03-23 18:29:00 +01:00
|
|
|
} elseif ($method_name === '__tostring') {
|
2020-03-11 14:38:09 +01:00
|
|
|
$result->return_type = Type::getString();
|
|
|
|
} else {
|
|
|
|
$result->return_type = Type::getMixed();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-03-11 18:35:33 +01:00
|
|
|
private static function handleInvalidClass(
|
|
|
|
StatementsAnalyzer $statements_analyzer,
|
|
|
|
Codebase $codebase,
|
|
|
|
PhpParser\Node\Expr\MethodCall $stmt,
|
2021-03-21 02:45:38 +01:00
|
|
|
Type\Union $lhs_type,
|
2020-03-11 18:35:33 +01:00
|
|
|
Type\Atomic $lhs_type_part,
|
|
|
|
?string $lhs_var_id,
|
|
|
|
Context $context,
|
|
|
|
bool $is_intersection,
|
|
|
|
AtomicMethodCallAnalysisResult $result
|
|
|
|
) : void {
|
|
|
|
switch (get_class($lhs_type_part)) {
|
|
|
|
case Type\Atomic\TNull::class:
|
|
|
|
case Type\Atomic\TFalse::class:
|
|
|
|
// handled above
|
|
|
|
return;
|
|
|
|
|
|
|
|
case Type\Atomic\TTemplateParam::class:
|
|
|
|
case Type\Atomic\TEmptyMixed::class:
|
|
|
|
case Type\Atomic\TEmpty::class:
|
|
|
|
case Type\Atomic\TMixed::class:
|
|
|
|
case Type\Atomic\TNonEmptyMixed::class:
|
|
|
|
case Type\Atomic\TObject::class:
|
|
|
|
case Type\Atomic\TObjectWithProperties::class:
|
|
|
|
if (!$context->collect_initializations
|
|
|
|
&& !$context->collect_mutations
|
|
|
|
&& $statements_analyzer->getFilePath() === $statements_analyzer->getRootFilePath()
|
|
|
|
&& (!(($parent_source = $statements_analyzer->getSource())
|
|
|
|
instanceof \Psalm\Internal\Analyzer\FunctionLikeAnalyzer)
|
|
|
|
|| !$parent_source->getSource() instanceof \Psalm\Internal\Analyzer\TraitAnalyzer)
|
|
|
|
) {
|
|
|
|
$codebase->analyzer->incrementMixedCount($statements_analyzer->getFilePath());
|
|
|
|
}
|
|
|
|
|
|
|
|
$result->has_mixed_method_call = true;
|
|
|
|
|
|
|
|
if ($lhs_type_part instanceof Type\Atomic\TObjectWithProperties
|
|
|
|
&& $stmt->name instanceof PhpParser\Node\Identifier
|
|
|
|
&& isset($lhs_type_part->methods[$stmt->name->name])
|
|
|
|
) {
|
|
|
|
$result->existent_method_ids[] = $lhs_type_part->methods[$stmt->name->name];
|
|
|
|
} elseif (!$is_intersection) {
|
|
|
|
if ($stmt->name instanceof PhpParser\Node\Identifier) {
|
|
|
|
$codebase->analyzer->addMixedMemberName(
|
|
|
|
strtolower($stmt->name->name),
|
2020-03-26 17:35:27 +01:00
|
|
|
$context->calling_method_id ?: $statements_analyzer->getFileName()
|
2020-03-11 18:35:33 +01:00
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
if ($context->check_methods) {
|
|
|
|
$message = 'Cannot determine the type of the object'
|
|
|
|
. ' on the left hand side of this expression';
|
|
|
|
|
|
|
|
if ($lhs_var_id) {
|
|
|
|
$message = 'Cannot determine the type of ' . $lhs_var_id;
|
|
|
|
|
|
|
|
if ($stmt->name instanceof PhpParser\Node\Identifier) {
|
|
|
|
$message .= ' when calling method ' . $stmt->name->name;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-03-21 02:45:38 +01:00
|
|
|
$origin_locations = [];
|
|
|
|
|
|
|
|
if ($statements_analyzer->data_flow_graph instanceof VariableUseGraph) {
|
|
|
|
foreach ($lhs_type->parent_nodes as $parent_node) {
|
|
|
|
$origin_locations = array_merge(
|
|
|
|
$origin_locations,
|
|
|
|
$statements_analyzer->data_flow_graph->getOriginLocations($parent_node)
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
$origin_location = count($origin_locations) === 1 ? reset($origin_locations) : null;
|
|
|
|
|
|
|
|
$name_code_location = new CodeLocation($statements_analyzer, $stmt->name);
|
|
|
|
|
|
|
|
if ($origin_location && $origin_location->getHash() === $name_code_location->getHash()) {
|
|
|
|
$origin_location = null;
|
|
|
|
}
|
|
|
|
|
2020-03-11 18:35:33 +01:00
|
|
|
if (IssueBuffer::accepts(
|
|
|
|
new MixedMethodCall(
|
|
|
|
$message,
|
2021-03-21 02:45:38 +01:00
|
|
|
$name_code_location,
|
|
|
|
$origin_location
|
2020-03-11 18:35:33 +01:00
|
|
|
),
|
|
|
|
$statements_analyzer->getSuppressedIssues()
|
|
|
|
)) {
|
|
|
|
// fall through
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-05-19 04:57:00 +02:00
|
|
|
if (ArgumentsAnalyzer::analyze(
|
2020-03-11 18:35:33 +01:00
|
|
|
$statements_analyzer,
|
|
|
|
$stmt->args,
|
|
|
|
null,
|
|
|
|
null,
|
2020-10-03 02:58:51 +02:00
|
|
|
true,
|
2020-03-11 18:35:33 +01:00
|
|
|
$context
|
|
|
|
) === false) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
$result->return_type = Type::getMixed();
|
|
|
|
return;
|
|
|
|
|
|
|
|
default:
|
|
|
|
$result->invalid_method_call_types[] = (string)$lhs_type_part;
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-10-15 15:21:44 +02:00
|
|
|
/**
|
|
|
|
* @param lowercase-string $method_name_lc
|
2020-11-27 21:25:16 +01:00
|
|
|
* @return array{Type\Atomic\TNamedObject, \Psalm\Storage\ClassLikeStorage, bool, MethodIdentifier, string}
|
2020-10-15 15:21:44 +02:00
|
|
|
*/
|
2021-01-06 01:03:50 +01:00
|
|
|
private static function handleTemplatedMixins(
|
2020-10-15 15:21:44 +02:00
|
|
|
\Psalm\Storage\ClassLikeStorage $class_storage,
|
2020-11-27 21:25:16 +01:00
|
|
|
Type\Atomic\TNamedObject $lhs_type_part,
|
2020-10-15 15:21:44 +02:00
|
|
|
string $method_name_lc,
|
|
|
|
Codebase $codebase,
|
|
|
|
Context $context,
|
|
|
|
MethodIdentifier $method_id,
|
|
|
|
\Psalm\StatementsSource $source,
|
|
|
|
PhpParser\Node\Expr\MethodCall $stmt,
|
|
|
|
StatementsAnalyzer $statements_analyzer,
|
2021-01-06 01:03:50 +01:00
|
|
|
string $fq_class_name
|
2020-10-15 15:21:44 +02:00
|
|
|
) {
|
|
|
|
$naive_method_exists = false;
|
|
|
|
|
|
|
|
if ($class_storage->templatedMixins
|
|
|
|
&& $lhs_type_part instanceof Type\Atomic\TGenericObject
|
|
|
|
&& $class_storage->template_types
|
|
|
|
) {
|
|
|
|
$template_type_keys = \array_keys($class_storage->template_types);
|
|
|
|
|
|
|
|
foreach ($class_storage->templatedMixins as $mixin) {
|
|
|
|
$param_position = \array_search(
|
|
|
|
$mixin->param_name,
|
|
|
|
$template_type_keys
|
|
|
|
);
|
|
|
|
|
|
|
|
if ($param_position !== false
|
|
|
|
&& isset($lhs_type_part->type_params[$param_position])
|
|
|
|
) {
|
|
|
|
$current_type_param = $lhs_type_part->type_params[$param_position];
|
|
|
|
if ($current_type_param->isSingle()) {
|
|
|
|
$lhs_type_part_new = array_values(
|
|
|
|
$current_type_param->getAtomicTypes()
|
|
|
|
)[0];
|
|
|
|
|
|
|
|
if ($lhs_type_part_new instanceof Type\Atomic\TNamedObject) {
|
|
|
|
$new_method_id = new MethodIdentifier(
|
|
|
|
$lhs_type_part_new->value,
|
|
|
|
$method_name_lc
|
|
|
|
);
|
|
|
|
|
|
|
|
$mixin_class_storage = $codebase->classlike_storage_provider->get(
|
|
|
|
$lhs_type_part_new->value
|
|
|
|
);
|
|
|
|
|
|
|
|
if ($codebase->methods->methodExists(
|
|
|
|
$new_method_id,
|
|
|
|
$context->calling_method_id,
|
|
|
|
$codebase->collect_locations
|
|
|
|
? new CodeLocation($source, $stmt->name)
|
|
|
|
: null,
|
|
|
|
!$context->collect_initializations
|
|
|
|
&& !$context->collect_mutations
|
|
|
|
? $statements_analyzer
|
|
|
|
: null,
|
|
|
|
$statements_analyzer->getFilePath()
|
|
|
|
)) {
|
|
|
|
$lhs_type_part = clone $lhs_type_part_new;
|
|
|
|
$class_storage = $mixin_class_storage;
|
|
|
|
|
|
|
|
$naive_method_exists = true;
|
|
|
|
$method_id = $new_method_id;
|
|
|
|
} elseif (isset($mixin_class_storage->pseudo_methods[$method_name_lc])) {
|
|
|
|
$lhs_type_part = clone $lhs_type_part_new;
|
|
|
|
$class_storage = $mixin_class_storage;
|
|
|
|
$method_id = $new_method_id;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2021-01-06 01:03:50 +01:00
|
|
|
}
|
2020-10-15 15:21:44 +02:00
|
|
|
|
2021-01-06 01:03:50 +01:00
|
|
|
return [
|
|
|
|
$lhs_type_part,
|
|
|
|
$class_storage,
|
|
|
|
$naive_method_exists,
|
|
|
|
$method_id,
|
|
|
|
$fq_class_name
|
|
|
|
];
|
|
|
|
}
|
2020-10-15 15:21:44 +02:00
|
|
|
|
2021-01-06 01:03:50 +01:00
|
|
|
/**
|
|
|
|
* @param lowercase-string $method_name_lc
|
|
|
|
* @return array{Type\Atomic\TNamedObject, \Psalm\Storage\ClassLikeStorage, bool, MethodIdentifier, string}
|
|
|
|
*/
|
|
|
|
private static function handleRegularMixins(
|
|
|
|
\Psalm\Storage\ClassLikeStorage $class_storage,
|
|
|
|
Type\Atomic\TNamedObject $lhs_type_part,
|
|
|
|
string $method_name_lc,
|
|
|
|
Codebase $codebase,
|
|
|
|
Context $context,
|
|
|
|
MethodIdentifier $method_id,
|
|
|
|
\Psalm\StatementsSource $source,
|
|
|
|
PhpParser\Node\Expr\MethodCall $stmt,
|
|
|
|
StatementsAnalyzer $statements_analyzer,
|
|
|
|
string $fq_class_name,
|
|
|
|
?string $lhs_var_id
|
|
|
|
) {
|
|
|
|
$naive_method_exists = false;
|
2020-10-15 15:21:44 +02:00
|
|
|
|
2021-01-06 01:03:50 +01:00
|
|
|
foreach ($class_storage->namedMixins as $mixin) {
|
|
|
|
if (!$class_storage->mixin_declaring_fqcln) {
|
|
|
|
continue;
|
|
|
|
}
|
2020-10-15 15:21:44 +02:00
|
|
|
|
2021-01-06 01:03:50 +01:00
|
|
|
$new_method_id = new MethodIdentifier(
|
|
|
|
$mixin->value,
|
|
|
|
$method_name_lc
|
|
|
|
);
|
2020-10-15 15:21:44 +02:00
|
|
|
|
2021-01-06 01:03:50 +01:00
|
|
|
if ($codebase->methods->methodExists(
|
|
|
|
$new_method_id,
|
|
|
|
$context->calling_method_id,
|
|
|
|
$codebase->collect_locations
|
|
|
|
? new CodeLocation($source, $stmt->name)
|
|
|
|
: null,
|
|
|
|
!$context->collect_initializations
|
|
|
|
&& !$context->collect_mutations
|
|
|
|
? $statements_analyzer
|
|
|
|
: null,
|
|
|
|
$statements_analyzer->getFilePath()
|
|
|
|
)) {
|
|
|
|
$mixin_declaring_class_storage = $codebase->classlike_storage_provider->get(
|
|
|
|
$class_storage->mixin_declaring_fqcln
|
|
|
|
);
|
2020-10-15 15:21:44 +02:00
|
|
|
|
2021-01-06 01:03:50 +01:00
|
|
|
$mixin_class_template_params = ClassTemplateParamCollector::collect(
|
|
|
|
$codebase,
|
|
|
|
$mixin_declaring_class_storage,
|
|
|
|
$codebase->classlike_storage_provider->get($fq_class_name),
|
|
|
|
null,
|
|
|
|
$lhs_type_part,
|
|
|
|
$lhs_var_id === '$this'
|
|
|
|
);
|
2020-10-15 15:21:44 +02:00
|
|
|
|
2021-01-06 01:03:50 +01:00
|
|
|
$lhs_type_part = clone $mixin;
|
2020-10-15 15:21:44 +02:00
|
|
|
|
2021-01-06 01:03:50 +01:00
|
|
|
$lhs_type_part->replaceTemplateTypesWithArgTypes(
|
|
|
|
new \Psalm\Internal\Type\TemplateResult([], $mixin_class_template_params ?: []),
|
|
|
|
$codebase
|
|
|
|
);
|
2020-10-15 15:21:44 +02:00
|
|
|
|
2021-01-06 01:03:50 +01:00
|
|
|
$lhs_type_expanded = \Psalm\Internal\Type\TypeExpander::expandUnion(
|
|
|
|
$codebase,
|
|
|
|
new Type\Union([$lhs_type_part]),
|
|
|
|
$mixin_declaring_class_storage->name,
|
|
|
|
$fq_class_name,
|
|
|
|
$class_storage->parent_class,
|
|
|
|
true,
|
|
|
|
false,
|
|
|
|
$class_storage->final
|
|
|
|
);
|
2020-10-15 15:21:44 +02:00
|
|
|
|
2021-01-06 01:03:50 +01:00
|
|
|
$new_lhs_type_part = array_values($lhs_type_expanded->getAtomicTypes())[0];
|
|
|
|
|
|
|
|
if ($new_lhs_type_part instanceof Type\Atomic\TNamedObject) {
|
|
|
|
$lhs_type_part = $new_lhs_type_part;
|
2020-10-15 15:21:44 +02:00
|
|
|
}
|
2021-01-06 01:03:50 +01:00
|
|
|
|
|
|
|
$mixin_class_storage = $codebase->classlike_storage_provider->get($mixin->value);
|
|
|
|
|
|
|
|
$fq_class_name = $mixin_class_storage->name;
|
|
|
|
$mixin_class_storage->mixin_declaring_fqcln = $class_storage->mixin_declaring_fqcln;
|
|
|
|
$class_storage = $mixin_class_storage;
|
|
|
|
$naive_method_exists = true;
|
|
|
|
$method_id = $new_method_id;
|
2020-10-15 15:21:44 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return [
|
|
|
|
$lhs_type_part,
|
|
|
|
$class_storage,
|
|
|
|
$naive_method_exists,
|
|
|
|
$method_id,
|
|
|
|
$fq_class_name
|
|
|
|
];
|
|
|
|
}
|
2020-03-11 14:38:09 +01:00
|
|
|
}
|