1
0
mirror of https://github.com/danog/psalm.git synced 2024-12-17 03:47:04 +01:00
psalm/src/Psalm/Checker/Statements/Expression/Call/StaticCallChecker.php

552 lines
22 KiB
PHP
Raw Normal View History

2018-01-29 00:29:38 +01:00
<?php
namespace Psalm\Checker\Statements\Expression\Call;
use PhpParser;
use Psalm\Checker\ClassLikeChecker;
use Psalm\Checker\FunctionLikeChecker;
2018-01-29 00:29:38 +01:00
use Psalm\Checker\MethodChecker;
use Psalm\Checker\Statements\ExpressionChecker;
use Psalm\Checker\StatementsChecker;
use Psalm\CodeLocation;
use Psalm\Config;
use Psalm\Context;
use Psalm\FileManipulation\FileManipulationBuffer;
2018-01-29 00:29:38 +01:00
use Psalm\Issue\DeprecatedClass;
use Psalm\Issue\InvalidStringClass;
2018-01-29 00:29:38 +01:00
use Psalm\Issue\ParentNotFound;
use Psalm\Issue\UndefinedClass;
use Psalm\Issue\UndefinedMethod;
2018-01-29 00:29:38 +01:00
use Psalm\IssueBuffer;
use Psalm\Type;
use Psalm\Type\Atomic\TNamedObject;
class StaticCallChecker extends \Psalm\Checker\Statements\Expression\CallChecker
{
/**
* @param StatementsChecker $statements_checker
* @param PhpParser\Node\Expr\StaticCall $stmt
* @param Context $context
*
* @return false|null
*/
public static function analyze(
StatementsChecker $statements_checker,
PhpParser\Node\Expr\StaticCall $stmt,
Context $context
) {
$method_id = null;
$lhs_type = null;
$file_checker = $statements_checker->getFileChecker();
$project_checker = $file_checker->project_checker;
2018-02-01 06:50:01 +01:00
$codebase = $project_checker->codebase;
2018-01-29 00:29:38 +01:00
$source = $statements_checker->getSource();
$stmt->inferredType = null;
$config = $project_checker->config;
2018-01-29 00:29:38 +01:00
if ($stmt->class instanceof PhpParser\Node\Name) {
$fq_class_name = null;
if (count($stmt->class->parts) === 1
&& in_array(strtolower($stmt->class->parts[0]), ['self', 'static', 'parent'], true)
) {
if ($stmt->class->parts[0] === 'parent') {
$child_fq_class_name = $context->self;
$class_storage = $child_fq_class_name
? $project_checker->classlike_storage_provider->get($child_fq_class_name)
: null;
if (!$class_storage || !$class_storage->parent_classes) {
if (IssueBuffer::accepts(
new ParentNotFound(
'Cannot call method on parent as this class does not extend another',
new CodeLocation($statements_checker->getSource(), $stmt)
),
$statements_checker->getSuppressedIssues()
)) {
return false;
}
return;
}
2018-02-04 00:52:35 +01:00
$fq_class_name = reset($class_storage->parent_classes);
2018-01-29 00:29:38 +01:00
$class_storage = $project_checker->classlike_storage_provider->get($fq_class_name);
$fq_class_name = $class_storage->name;
if ($stmt->name instanceof PhpParser\Node\Identifier
&& $class_storage->user_defined
&& ($context->collect_mutations || $context->collect_initializations)
) {
$method_id = $fq_class_name . '::' . strtolower($stmt->name->name);
2018-01-29 00:29:38 +01:00
$appearing_method_id = $codebase->getAppearingMethodId($method_id);
if (!$appearing_method_id) {
if (IssueBuffer::accepts(
new UndefinedMethod(
'Method ' . $method_id . ' does not exist',
new CodeLocation($statements_checker->getSource(), $stmt),
$method_id
),
$statements_checker->getSuppressedIssues()
)) {
return false;
}
return;
}
list($appearing_method_class_name) = explode('::', $appearing_method_id);
2018-01-29 00:29:38 +01:00
$old_context_include_location = $context->include_location;
$old_self = $context->self;
$context->include_location = new CodeLocation($statements_checker->getSource(), $stmt);
$context->self = $appearing_method_class_name;
2018-01-29 00:29:38 +01:00
if ($context->collect_mutations) {
$file_checker->getMethodMutations($method_id, $context);
} else {
// collecting initializations
2018-01-29 00:29:38 +01:00
$local_vars_in_scope = [];
$local_vars_possibly_in_scope = [];
foreach ($context->vars_in_scope as $var => $_) {
if (strpos($var, '$this->') !== 0 && $var !== '$this') {
$local_vars_in_scope[$var] = $context->vars_in_scope[$var];
}
}
foreach ($context->vars_possibly_in_scope as $var => $_) {
if (strpos($var, '$this->') !== 0 && $var !== '$this') {
$local_vars_possibly_in_scope[$var] = $context->vars_possibly_in_scope[$var];
}
}
if (!isset($context->initialized_methods[$method_id])) {
if ($context->initialized_methods === null) {
$context->initialized_methods = [];
}
$context->initialized_methods[$method_id] = true;
$file_checker->getMethodMutations($method_id, $context);
foreach ($local_vars_in_scope as $var => $type) {
$context->vars_in_scope[$var] = $type;
}
foreach ($local_vars_possibly_in_scope as $var => $type) {
$context->vars_possibly_in_scope[$var] = $type;
}
}
}
$context->include_location = $old_context_include_location;
$context->self = $old_self;
if (isset($context->vars_in_scope['$this']) && $old_self) {
$context->vars_in_scope['$this'] = Type::parseString($old_self);
}
}
} elseif ($context->self) {
if ($stmt->class->parts[0] === 'static' && isset($context->vars_in_scope['$this'])) {
$fq_class_name = (string) $context->vars_in_scope['$this'];
$lhs_type = clone $context->vars_in_scope['$this'];
} else {
$fq_class_name = $context->self;
}
2018-01-29 00:29:38 +01:00
} else {
$namespace = $statements_checker->getNamespace()
? $statements_checker->getNamespace() . '\\'
: '';
$fq_class_name = $namespace . $statements_checker->getClassName();
2018-01-29 00:29:38 +01:00
}
if ($context->isPhantomClass($fq_class_name)) {
return null;
}
} elseif ($context->check_classes) {
$fq_class_name = ClassLikeChecker::getFQCLNFromNameObject(
$stmt->class,
$statements_checker->getAliases()
);
if ($context->isPhantomClass($fq_class_name)) {
return null;
}
$does_class_exist = false;
if ($context->self) {
$self_storage = $project_checker->classlike_storage_provider->get($context->self);
if (isset($self_storage->used_traits[strtolower($fq_class_name)])) {
$fq_class_name = $context->self;
$does_class_exist = true;
}
}
if (!$does_class_exist) {
$does_class_exist = ClassLikeChecker::checkFullyQualifiedClassLikeName(
$statements_checker,
$fq_class_name,
new CodeLocation($source, $stmt->class),
$statements_checker->getSuppressedIssues(),
false
);
}
if (!$does_class_exist) {
return $does_class_exist;
}
}
if ($fq_class_name && !$lhs_type) {
2018-01-29 00:29:38 +01:00
$lhs_type = new Type\Union([new TNamedObject($fq_class_name)]);
}
} else {
ExpressionChecker::analyze($statements_checker, $stmt->class, $context);
$lhs_type = $stmt->class->inferredType;
}
if (!$context->check_methods || !$lhs_type) {
if (self::checkFunctionArguments(
$statements_checker,
$stmt->args,
null,
null,
$context
) === false) {
return false;
}
2018-01-29 00:29:38 +01:00
return null;
}
$has_mock = false;
foreach ($lhs_type->getTypes() as $lhs_type_part) {
if (!$lhs_type_part instanceof TNamedObject) {
// this is always OK
if ($lhs_type_part instanceof Type\Atomic\TClassString) {
continue;
}
// ok for now
if ($lhs_type_part instanceof Type\Atomic\TLiteralClassString) {
continue;
}
if ($lhs_type_part instanceof Type\Atomic\TString) {
if ($config->allow_string_standin_for_class
&& !$lhs_type_part instanceof Type\Atomic\TNumericString
) {
continue;
}
if (IssueBuffer::accepts(
new InvalidStringClass(
'String cannot be used as a class',
new CodeLocation($statements_checker->getSource(), $stmt)
),
$statements_checker->getSuppressedIssues()
)) {
// fall through
}
continue;
}
if ($lhs_type_part instanceof Type\Atomic\TMixed
|| $lhs_type_part instanceof Type\Atomic\TGenericParam
) {
continue;
}
if ($lhs_type_part instanceof Type\Atomic\TNull
&& $lhs_type->ignore_nullable_issues
) {
continue;
}
if (IssueBuffer::accepts(
new UndefinedClass(
'Type ' . $lhs_type_part . ' cannot be called as a class',
new CodeLocation($statements_checker->getSource(), $stmt),
(string) $lhs_type_part
),
$statements_checker->getSuppressedIssues()
)) {
// fall through
}
2018-01-29 00:29:38 +01:00
continue;
}
$fq_class_name = $lhs_type_part->value;
$is_mock = ExpressionChecker::isMock($fq_class_name);
$has_mock = $has_mock || $is_mock;
$method_id = null;
if ($stmt->name instanceof PhpParser\Node\Identifier
&& !$codebase->methodExists($fq_class_name . '::__callStatic')
&& !$is_mock
2018-01-29 00:29:38 +01:00
) {
$method_id = $fq_class_name . '::' . strtolower($stmt->name->name);
$cased_method_id = $fq_class_name . '::' . $stmt->name->name;
2018-01-29 00:29:38 +01:00
$source_method_id = $source instanceof FunctionLikeChecker
? $source->getMethodId()
: null;
2018-01-29 00:29:38 +01:00
$does_method_exist = MethodChecker::checkMethodExists(
$project_checker,
$cased_method_id,
new CodeLocation($source, $stmt),
$statements_checker->getSuppressedIssues(),
$source_method_id
2018-01-29 00:29:38 +01:00
);
if (!$does_method_exist) {
return $does_method_exist;
}
$class_storage = $project_checker->classlike_storage_provider->get($fq_class_name);
if ($class_storage->deprecated) {
if (IssueBuffer::accepts(
new DeprecatedClass(
$fq_class_name . ' is marked deprecated',
new CodeLocation($statements_checker->getSource(), $stmt)
),
$statements_checker->getSuppressedIssues()
)) {
// fall through
}
}
if (MethodChecker::checkMethodVisibility(
$method_id,
$context->self,
$statements_checker->getSource(),
new CodeLocation($source, $stmt),
$statements_checker->getSuppressedIssues()
) === false) {
return false;
}
if ($stmt->class instanceof PhpParser\Node\Name
&& ($stmt->class->parts[0] !== 'parent' || $statements_checker->isStatic())
&& (
!$context->self
|| $statements_checker->isStatic()
2018-02-01 06:50:01 +01:00
|| !$codebase->classExtends($context->self, $fq_class_name)
2018-01-29 00:29:38 +01:00
)
) {
if (MethodChecker::checkStatic(
$method_id,
strtolower($stmt->class->parts[0]) === 'self' || $context->self === $fq_class_name,
!$statements_checker->isStatic(),
2018-01-29 00:29:38 +01:00
$project_checker,
new CodeLocation($source, $stmt),
$statements_checker->getSuppressedIssues(),
$is_dynamic_this_method
2018-01-29 00:29:38 +01:00
) === false) {
// fall through
}
if ($is_dynamic_this_method) {
$fake_method_call_expr = new PhpParser\Node\Expr\MethodCall(
new PhpParser\Node\Expr\Variable(
'this',
$stmt->class->getAttributes()
),
$stmt->name,
$stmt->args,
$stmt->getAttributes()
);
if (MethodCallChecker::analyze(
$statements_checker,
$fake_method_call_expr,
$context
) === false) {
return false;
}
if (isset($fake_method_call_expr->inferredType)) {
$stmt->inferredType = $fake_method_call_expr->inferredType;
}
return null;
}
2018-01-29 00:29:38 +01:00
}
if (MethodChecker::checkMethodNotDeprecated(
$project_checker,
$method_id,
new CodeLocation($statements_checker->getSource(), $stmt),
$statements_checker->getSuppressedIssues()
) === false) {
// fall through
}
if (self::checkMethodArgs(
$method_id,
$stmt->args,
$found_generic_params,
$context,
new CodeLocation($statements_checker->getSource(), $stmt),
$statements_checker
) === false) {
return false;
}
$fq_class_name = $stmt->class instanceof PhpParser\Node\Name && $stmt->class->parts === ['parent']
? (string) $statements_checker->getFQCLN()
: $fq_class_name;
$self_fq_class_name = $fq_class_name;
2018-02-04 00:52:35 +01:00
$return_type_candidate = $codebase->methods->getMethodReturnType(
2018-01-29 00:29:38 +01:00
$method_id,
$self_fq_class_name,
$stmt->args
2018-01-29 00:29:38 +01:00
);
if ($return_type_candidate) {
$return_type_candidate = clone $return_type_candidate;
if ($found_generic_params) {
$return_type_candidate->replaceTemplateTypesWithArgTypes(
$found_generic_params
);
}
$return_type_candidate = ExpressionChecker::fleshOutType(
$project_checker,
$return_type_candidate,
$self_fq_class_name,
$fq_class_name
);
2018-02-04 00:52:35 +01:00
$return_type_location = $codebase->methods->getMethodReturnTypeLocation(
2018-01-29 00:29:38 +01:00
$method_id,
$secondary_return_type_location
);
if ($secondary_return_type_location) {
$return_type_location = $secondary_return_type_location;
}
// only check the type locally if it's defined externally
if ($return_type_location && !$config->isInProjectDirs($return_type_location->file_path)) {
$return_type_candidate->check(
$statements_checker,
new CodeLocation($source, $stmt),
$statements_checker->getSuppressedIssues(),
$context->getPhantomClasses()
);
}
}
try {
$method_storage = $codebase->methods->getUserMethodStorage($method_id);
if ($method_storage->assertions) {
self::applyAssertionsToContext(
$method_storage->assertions,
$stmt->args,
$context,
$statements_checker
);
}
if ($method_storage->if_true_assertions) {
$stmt->ifTrueAssertions = $method_storage->if_true_assertions;
}
if ($method_storage->if_false_assertions) {
$stmt->ifFalseAssertions = $method_storage->if_false_assertions;
}
} catch (\UnexpectedValueException $e) {
// do nothing for non-user-defined methods
}
if ($config->after_method_checks) {
$file_manipulations = [];
$appearing_method_id = $codebase->methods->getAppearingMethodId($method_id);
$declaring_method_id = $codebase->methods->getDeclaringMethodId($method_id);
$code_location = new CodeLocation($source, $stmt);
foreach ($config->after_method_checks as $plugin_fq_class_name) {
$plugin_fq_class_name::afterMethodCallCheck(
$statements_checker,
$method_id,
$appearing_method_id,
$declaring_method_id,
null,
$stmt->args,
$code_location,
$context,
$file_manipulations,
$return_type_candidate
);
}
if ($file_manipulations) {
/** @psalm-suppress MixedTypeCoercion */
FileManipulationBuffer::add($statements_checker->getFilePath(), $file_manipulations);
}
}
2018-01-29 00:29:38 +01:00
if ($return_type_candidate) {
if (isset($stmt->inferredType)) {
$stmt->inferredType = Type::combineUnionTypes($stmt->inferredType, $return_type_candidate);
} else {
$stmt->inferredType = $return_type_candidate;
}
}
} else {
if (self::checkFunctionArguments(
$statements_checker,
$stmt->args,
null,
null,
$context
) === false) {
return false;
}
2018-01-29 00:29:38 +01:00
}
}
if ($method_id === null) {
return self::checkMethodArgs(
$method_id,
$stmt->args,
$found_generic_params,
$context,
new CodeLocation($statements_checker->getSource(), $stmt),
$statements_checker
);
}
if (!$config->remember_property_assignments_after_call && !$context->collect_initializations) {
$context->removeAllObjectVars();
}
}
}