2017-07-25 22:11:02 +02:00
|
|
|
<?php
|
|
|
|
namespace Psalm\Visitor;
|
|
|
|
|
|
|
|
use PhpParser;
|
|
|
|
use Psalm\Aliases;
|
|
|
|
use Psalm\Checker\ClassChecker;
|
|
|
|
use Psalm\Checker\ClassLikeChecker;
|
|
|
|
use Psalm\Checker\CommentChecker;
|
2018-03-02 05:33:21 +01:00
|
|
|
use Psalm\Checker\Statements\Expression\CallChecker;
|
2018-01-14 18:09:40 +01:00
|
|
|
use Psalm\Checker\Statements\Expression\IncludeChecker;
|
2017-07-25 22:11:02 +02:00
|
|
|
use Psalm\Checker\StatementsChecker;
|
2018-01-21 19:38:51 +01:00
|
|
|
use Psalm\Codebase;
|
2018-02-04 00:52:35 +01:00
|
|
|
use Psalm\Codebase\CallMap;
|
2018-02-04 18:34:08 +01:00
|
|
|
use Psalm\Codebase\PropertyMap;
|
2017-07-25 22:11:02 +02:00
|
|
|
use Psalm\CodeLocation;
|
|
|
|
use Psalm\Config;
|
|
|
|
use Psalm\Exception\DocblockParseException;
|
|
|
|
use Psalm\Exception\FileIncludeException;
|
2017-11-15 03:43:31 +01:00
|
|
|
use Psalm\Exception\IncorrectDocblockException;
|
2017-10-22 07:04:35 +02:00
|
|
|
use Psalm\Exception\TypeParseTreeException;
|
2018-02-23 21:39:33 +01:00
|
|
|
use Psalm\FileSource;
|
2018-04-07 17:38:41 +02:00
|
|
|
use Psalm\Issue\DuplicateClass;
|
2017-07-25 22:11:02 +02:00
|
|
|
use Psalm\Issue\DuplicateParam;
|
|
|
|
use Psalm\Issue\InvalidDocblock;
|
|
|
|
use Psalm\Issue\MisplacedRequiredParam;
|
2017-11-15 03:43:31 +01:00
|
|
|
use Psalm\Issue\MissingDocblockType;
|
2017-07-25 22:11:02 +02:00
|
|
|
use Psalm\IssueBuffer;
|
2018-01-21 18:44:46 +01:00
|
|
|
use Psalm\Scanner\FileScanner;
|
2017-07-25 22:11:02 +02:00
|
|
|
use Psalm\Storage\ClassLikeStorage;
|
2017-07-29 21:05:06 +02:00
|
|
|
use Psalm\Storage\FileStorage;
|
2018-05-12 00:35:02 +02:00
|
|
|
use Psalm\Storage\FunctionLikeParameter;
|
2017-07-25 22:11:02 +02:00
|
|
|
use Psalm\Storage\FunctionLikeStorage;
|
|
|
|
use Psalm\Storage\MethodStorage;
|
|
|
|
use Psalm\Storage\PropertyStorage;
|
|
|
|
use Psalm\Type;
|
|
|
|
|
2018-02-23 21:39:33 +01:00
|
|
|
class DependencyFinderVisitor extends PhpParser\NodeVisitorAbstract implements PhpParser\NodeVisitor, FileSource
|
2017-07-25 22:11:02 +02:00
|
|
|
{
|
|
|
|
/** @var Aliases */
|
2018-01-21 18:44:46 +01:00
|
|
|
private $aliases;
|
2017-07-25 22:11:02 +02:00
|
|
|
|
|
|
|
/** @var Aliases */
|
2018-01-21 18:44:46 +01:00
|
|
|
private $file_aliases;
|
2017-07-25 22:11:02 +02:00
|
|
|
|
|
|
|
/**
|
2017-08-14 21:46:01 +02:00
|
|
|
* @var string[]
|
2017-07-25 22:11:02 +02:00
|
|
|
*/
|
2018-01-21 18:44:46 +01:00
|
|
|
private $fq_classlike_names = [];
|
2017-07-25 22:11:02 +02:00
|
|
|
|
2018-01-21 18:44:46 +01:00
|
|
|
/** @var FileScanner */
|
|
|
|
private $file_scanner;
|
2017-07-25 22:11:02 +02:00
|
|
|
|
2018-01-21 19:38:51 +01:00
|
|
|
/** @var Codebase */
|
|
|
|
private $codebase;
|
|
|
|
|
2017-07-25 22:11:02 +02:00
|
|
|
/** @var string */
|
2018-01-21 18:44:46 +01:00
|
|
|
private $file_path;
|
2017-07-25 22:11:02 +02:00
|
|
|
|
|
|
|
/** @var bool */
|
2018-01-21 18:44:46 +01:00
|
|
|
private $scan_deep;
|
2017-07-25 22:11:02 +02:00
|
|
|
|
|
|
|
/** @var Config */
|
2018-01-21 18:44:46 +01:00
|
|
|
private $config;
|
2017-07-25 22:11:02 +02:00
|
|
|
|
|
|
|
/** @var bool */
|
2018-01-21 18:44:46 +01:00
|
|
|
private $queue_strings_as_possible_type = false;
|
2017-07-25 22:11:02 +02:00
|
|
|
|
|
|
|
/** @var array<string, string> */
|
2018-01-21 18:44:46 +01:00
|
|
|
private $class_template_types = [];
|
2017-07-25 22:11:02 +02:00
|
|
|
|
|
|
|
/** @var array<string, string> */
|
2018-01-21 18:44:46 +01:00
|
|
|
private $function_template_types = [];
|
2017-07-25 22:11:02 +02:00
|
|
|
|
2017-08-14 21:46:01 +02:00
|
|
|
/** @var FunctionLikeStorage[] */
|
2018-01-21 18:44:46 +01:00
|
|
|
private $functionlike_storages = [];
|
2017-07-25 22:11:02 +02:00
|
|
|
|
2017-07-29 21:05:06 +02:00
|
|
|
/** @var FileStorage */
|
2018-01-21 18:44:46 +01:00
|
|
|
private $file_storage;
|
2017-07-29 21:05:06 +02:00
|
|
|
|
2017-08-14 21:46:01 +02:00
|
|
|
/** @var ClassLikeStorage[] */
|
2018-01-21 18:44:46 +01:00
|
|
|
private $classlike_storages = [];
|
2017-07-29 21:05:06 +02:00
|
|
|
|
2018-02-12 02:56:34 +01:00
|
|
|
/** @var string[] */
|
2018-02-12 04:49:19 +01:00
|
|
|
private $after_classlike_check_plugins;
|
2017-11-07 20:46:53 +01:00
|
|
|
|
2018-07-15 23:23:17 +02:00
|
|
|
/**
|
|
|
|
* @var array<string, array<int, string>>
|
|
|
|
*/
|
|
|
|
private $type_aliases = [];
|
|
|
|
|
2018-02-23 04:22:31 +01:00
|
|
|
public function __construct(
|
|
|
|
Codebase $codebase,
|
|
|
|
FileStorage $file_storage,
|
2018-02-23 04:52:22 +01:00
|
|
|
FileScanner $file_scanner
|
2018-02-23 04:22:31 +01:00
|
|
|
) {
|
2018-01-21 19:38:51 +01:00
|
|
|
$this->codebase = $codebase;
|
2018-01-21 18:44:46 +01:00
|
|
|
$this->file_scanner = $file_scanner;
|
2018-01-21 19:38:51 +01:00
|
|
|
$this->file_path = $file_scanner->file_path;
|
2018-01-21 18:44:46 +01:00
|
|
|
$this->scan_deep = $file_scanner->will_analyze;
|
2018-01-21 19:38:51 +01:00
|
|
|
$this->config = $codebase->config;
|
2017-07-25 22:11:02 +02:00
|
|
|
$this->aliases = $this->file_aliases = new Aliases();
|
2018-01-21 18:44:46 +01:00
|
|
|
$this->file_storage = $file_storage;
|
2018-02-12 04:49:19 +01:00
|
|
|
$this->after_classlike_check_plugins = $this->config->after_visit_classlikes;
|
2017-07-25 22:11:02 +02:00
|
|
|
}
|
|
|
|
|
2017-11-26 22:03:17 +01:00
|
|
|
/**
|
|
|
|
* @param PhpParser\Node $node
|
|
|
|
*
|
2018-01-28 23:07:09 +01:00
|
|
|
* @return null|int
|
2017-11-26 22:03:17 +01:00
|
|
|
*/
|
2017-07-25 22:11:02 +02:00
|
|
|
public function enterNode(PhpParser\Node $node)
|
|
|
|
{
|
|
|
|
if ($node instanceof PhpParser\Node\Stmt\Namespace_) {
|
|
|
|
$this->file_aliases = $this->aliases;
|
|
|
|
$this->aliases = new Aliases(
|
|
|
|
$node->name ? implode('\\', $node->name->parts) : '',
|
|
|
|
$this->aliases->uses,
|
|
|
|
$this->aliases->functions,
|
|
|
|
$this->aliases->constants
|
|
|
|
);
|
|
|
|
} elseif ($node instanceof PhpParser\Node\Stmt\Use_) {
|
|
|
|
foreach ($node->uses as $use) {
|
|
|
|
$use_path = implode('\\', $use->name->parts);
|
|
|
|
|
2018-04-17 18:16:25 +02:00
|
|
|
$use_alias = $use->alias ? $use->alias->name : $use->name->getLast();
|
|
|
|
|
2017-07-25 22:11:02 +02:00
|
|
|
switch ($use->type !== PhpParser\Node\Stmt\Use_::TYPE_UNKNOWN ? $use->type : $node->type) {
|
|
|
|
case PhpParser\Node\Stmt\Use_::TYPE_FUNCTION:
|
2018-04-17 18:16:25 +02:00
|
|
|
$this->aliases->functions[strtolower($use_alias)] = $use_path;
|
2017-07-25 22:11:02 +02:00
|
|
|
break;
|
|
|
|
|
|
|
|
case PhpParser\Node\Stmt\Use_::TYPE_CONSTANT:
|
2018-04-17 18:16:25 +02:00
|
|
|
$this->aliases->constants[$use_alias] = $use_path;
|
2017-07-25 22:11:02 +02:00
|
|
|
break;
|
|
|
|
|
|
|
|
case PhpParser\Node\Stmt\Use_::TYPE_NORMAL:
|
2018-04-17 18:16:25 +02:00
|
|
|
$this->aliases->uses[strtolower($use_alias)] = $use_path;
|
2017-07-25 22:11:02 +02:00
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
} elseif ($node instanceof PhpParser\Node\Stmt\GroupUse) {
|
|
|
|
$use_prefix = implode('\\', $node->prefix->parts);
|
|
|
|
|
|
|
|
foreach ($node->uses as $use) {
|
|
|
|
$use_path = $use_prefix . '\\' . implode('\\', $use->name->parts);
|
2018-04-17 18:16:25 +02:00
|
|
|
$use_alias = $use->alias ? $use->alias->name : $use->name->getLast();
|
2017-07-25 22:11:02 +02:00
|
|
|
|
|
|
|
switch ($use->type !== PhpParser\Node\Stmt\Use_::TYPE_UNKNOWN ? $use->type : $node->type) {
|
|
|
|
case PhpParser\Node\Stmt\Use_::TYPE_FUNCTION:
|
2018-04-17 18:16:25 +02:00
|
|
|
$this->aliases->functions[strtolower($use_alias)] = $use_path;
|
2017-07-25 22:11:02 +02:00
|
|
|
break;
|
|
|
|
|
|
|
|
case PhpParser\Node\Stmt\Use_::TYPE_CONSTANT:
|
2018-04-17 18:16:25 +02:00
|
|
|
$this->aliases->constants[$use_alias] = $use_path;
|
2017-07-25 22:11:02 +02:00
|
|
|
break;
|
|
|
|
|
|
|
|
case PhpParser\Node\Stmt\Use_::TYPE_NORMAL:
|
2018-04-17 18:16:25 +02:00
|
|
|
$this->aliases->uses[strtolower($use_alias)] = $use_path;
|
2017-07-25 22:11:02 +02:00
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
} elseif ($node instanceof PhpParser\Node\Stmt\ClassLike) {
|
2018-06-29 21:39:17 +02:00
|
|
|
$class_location = new CodeLocation($this->file_scanner, $node, null, true);
|
|
|
|
|
2018-06-30 21:29:37 +02:00
|
|
|
$storage = null;
|
|
|
|
|
2017-07-25 22:11:02 +02:00
|
|
|
if ($node->name === null) {
|
|
|
|
if (!$node instanceof PhpParser\Node\Stmt\Class_) {
|
|
|
|
throw new \LogicException('Anonymous classes are always classes');
|
|
|
|
}
|
|
|
|
|
|
|
|
$fq_classlike_name = ClassChecker::getAnonymousClassName($node, $this->file_path);
|
|
|
|
} else {
|
2018-04-17 18:16:25 +02:00
|
|
|
$fq_classlike_name =
|
|
|
|
($this->aliases->namespace ? $this->aliases->namespace . '\\' : '') . $node->name->name;
|
2018-04-07 17:38:41 +02:00
|
|
|
|
2018-06-30 21:29:37 +02:00
|
|
|
if ($this->codebase->classlike_storage_provider->has($fq_classlike_name)) {
|
|
|
|
$duplicate_storage = $this->codebase->classlike_storage_provider->get($fq_classlike_name);
|
|
|
|
|
|
|
|
if (!$this->codebase->register_stub_files) {
|
|
|
|
if (!$duplicate_storage->location
|
|
|
|
|| $duplicate_storage->location->file_path !== $this->file_path
|
|
|
|
|| $class_location->getHash() !== $duplicate_storage->location->getHash()
|
|
|
|
) {
|
|
|
|
if (IssueBuffer::accepts(
|
|
|
|
new DuplicateClass(
|
|
|
|
'Class ' . $fq_classlike_name . ' has already been defined'
|
|
|
|
. ($duplicate_storage->location
|
|
|
|
? ' in ' . $duplicate_storage->location->file_path
|
|
|
|
: ''),
|
|
|
|
new CodeLocation($this->file_scanner, $node, null, true)
|
|
|
|
)
|
|
|
|
)) {
|
|
|
|
$this->file_storage->has_visitor_issues = true;
|
|
|
|
}
|
2018-06-29 21:28:45 +02:00
|
|
|
|
2018-06-30 21:29:37 +02:00
|
|
|
return PhpParser\NodeTraverser::STOP_TRAVERSAL;
|
2018-06-29 21:28:45 +02:00
|
|
|
}
|
2018-07-06 05:02:09 +02:00
|
|
|
} elseif (!$duplicate_storage->location
|
|
|
|
|| $duplicate_storage->location->file_path !== $this->file_path
|
|
|
|
|| $class_location->getHash() !== $duplicate_storage->location->getHash()
|
2018-06-30 21:29:37 +02:00
|
|
|
) {
|
|
|
|
// we're overwriting some methods
|
|
|
|
$storage = $duplicate_storage;
|
2018-06-29 21:28:45 +02:00
|
|
|
}
|
2018-04-07 17:38:41 +02:00
|
|
|
}
|
2017-07-25 22:11:02 +02:00
|
|
|
}
|
|
|
|
|
2018-02-20 15:02:51 +01:00
|
|
|
$fq_classlike_name_lc = strtolower($fq_classlike_name);
|
|
|
|
|
|
|
|
$this->file_storage->classlikes_in_file[$fq_classlike_name_lc] = $fq_classlike_name;
|
|
|
|
|
2017-08-14 21:46:01 +02:00
|
|
|
$this->fq_classlike_names[] = $fq_classlike_name;
|
2017-07-25 22:11:02 +02:00
|
|
|
|
2018-06-30 21:29:37 +02:00
|
|
|
if (!$storage) {
|
|
|
|
$storage = $this->codebase->createClassLikeStorage($fq_classlike_name);
|
|
|
|
}
|
2017-08-14 21:46:01 +02:00
|
|
|
|
2018-06-29 21:39:17 +02:00
|
|
|
$storage->location = $class_location;
|
2018-06-30 21:29:37 +02:00
|
|
|
$storage->user_defined = !$this->codebase->register_stub_files;
|
|
|
|
$storage->stubbed = $this->codebase->register_stub_files;
|
2018-02-23 04:35:48 +01:00
|
|
|
|
2018-07-15 23:23:17 +02:00
|
|
|
$comments = $node->getComments();
|
|
|
|
|
|
|
|
foreach ($comments as $comment) {
|
|
|
|
if ($comment instanceof PhpParser\Comment\Doc) {
|
|
|
|
try {
|
|
|
|
$type_alias_tokens = CommentChecker::getTypeAliasesFromComment(
|
|
|
|
(string) $comment,
|
|
|
|
$this->aliases,
|
|
|
|
$this->type_aliases
|
|
|
|
);
|
|
|
|
|
|
|
|
foreach ($type_alias_tokens as $type_tokens) {
|
|
|
|
// finds issues, if there are any
|
|
|
|
Type::parseTokens($type_tokens);
|
|
|
|
}
|
|
|
|
|
|
|
|
$this->type_aliases += $type_alias_tokens;
|
|
|
|
} catch (DocblockParseException $e) {
|
|
|
|
if (IssueBuffer::accepts(
|
|
|
|
new InvalidDocblock(
|
|
|
|
(string)$e->getMessage(),
|
|
|
|
new CodeLocation($this->file_scanner, $node, null, true)
|
|
|
|
)
|
|
|
|
)) {
|
|
|
|
// fall through
|
|
|
|
}
|
|
|
|
} catch (TypeParseTreeException $e) {
|
|
|
|
if (IssueBuffer::accepts(
|
|
|
|
new InvalidDocblock(
|
|
|
|
(string)$e->getMessage(),
|
|
|
|
new CodeLocation($this->file_scanner, $node, null, true)
|
|
|
|
)
|
|
|
|
)) {
|
|
|
|
$storage->has_visitor_issues = true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-02-23 04:35:48 +01:00
|
|
|
$doc_comment = $node->getDocComment();
|
|
|
|
|
2018-02-23 04:52:22 +01:00
|
|
|
$this->classlike_storages[] = $storage;
|
|
|
|
|
2017-07-25 22:11:02 +02:00
|
|
|
if ($doc_comment) {
|
2017-12-15 18:07:34 +01:00
|
|
|
$docblock_info = null;
|
2017-07-25 22:11:02 +02:00
|
|
|
try {
|
|
|
|
$docblock_info = CommentChecker::extractClassLikeDocblockInfo(
|
|
|
|
(string)$doc_comment,
|
|
|
|
$doc_comment->getLine()
|
|
|
|
);
|
|
|
|
} catch (DocblockParseException $e) {
|
2018-05-29 16:13:26 +02:00
|
|
|
if (IssueBuffer::accepts(
|
2017-07-25 22:11:02 +02:00
|
|
|
new InvalidDocblock(
|
2017-08-14 21:46:01 +02:00
|
|
|
$e->getMessage() . ' in docblock for ' . implode('.', $this->fq_classlike_names),
|
2018-01-21 18:44:46 +01:00
|
|
|
new CodeLocation($this->file_scanner, $node, null, true)
|
2017-07-25 22:11:02 +02:00
|
|
|
)
|
2018-05-29 16:13:26 +02:00
|
|
|
)) {
|
|
|
|
$storage->has_visitor_issues = true;
|
|
|
|
}
|
2017-07-25 22:11:02 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
if ($docblock_info) {
|
2018-06-19 19:19:34 +02:00
|
|
|
if ($docblock_info->template_type_names) {
|
2017-07-25 22:11:02 +02:00
|
|
|
$storage->template_types = [];
|
|
|
|
|
2018-06-19 19:19:34 +02:00
|
|
|
foreach ($docblock_info->template_type_names as $template_type) {
|
2017-07-25 22:11:02 +02:00
|
|
|
if (count($template_type) === 3) {
|
2018-06-19 19:19:34 +02:00
|
|
|
$storage->template_types[$template_type[0]] = Type::parseTokens(
|
|
|
|
Type::fixUpLocalType(
|
|
|
|
$template_type[2],
|
2018-07-15 23:23:17 +02:00
|
|
|
$this->aliases,
|
|
|
|
null,
|
|
|
|
$this->type_aliases
|
2018-06-19 19:19:34 +02:00
|
|
|
)
|
2017-07-25 22:11:02 +02:00
|
|
|
);
|
|
|
|
} else {
|
2018-06-19 19:19:34 +02:00
|
|
|
$storage->template_types[$template_type[0]] = Type::getMixed();
|
2017-07-25 22:11:02 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
$this->class_template_types = $storage->template_types;
|
2018-05-28 23:26:43 +02:00
|
|
|
|
|
|
|
if ($docblock_info->template_parents) {
|
|
|
|
$storage->template_parents = [];
|
|
|
|
|
|
|
|
foreach ($docblock_info->template_parents as $template_parent) {
|
|
|
|
$storage->template_parents[$template_parent] = $template_parent;
|
|
|
|
}
|
|
|
|
}
|
2017-07-25 22:11:02 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
if ($docblock_info->properties) {
|
|
|
|
foreach ($docblock_info->properties as $property) {
|
2018-05-20 23:19:53 +02:00
|
|
|
$pseudo_property_type_tokens = Type::fixUpLocalType(
|
2018-02-04 05:10:22 +01:00
|
|
|
$property['type'],
|
2018-07-15 23:23:17 +02:00
|
|
|
$this->aliases,
|
|
|
|
null,
|
|
|
|
$this->type_aliases
|
2018-02-04 05:10:22 +01:00
|
|
|
);
|
|
|
|
|
2018-05-20 23:19:53 +02:00
|
|
|
$pseudo_property_type = Type::parseTokens($pseudo_property_type_tokens);
|
2017-11-29 05:00:26 +01:00
|
|
|
$pseudo_property_type->setFromDocblock();
|
2017-07-25 22:11:02 +02:00
|
|
|
|
2017-12-14 20:22:27 +01:00
|
|
|
if ($property['tag'] !== 'property-read') {
|
|
|
|
$storage->pseudo_property_set_types[$property['name']] = $pseudo_property_type;
|
|
|
|
}
|
2018-02-04 05:10:22 +01:00
|
|
|
|
2017-12-14 20:22:27 +01:00
|
|
|
if ($property['tag'] !== 'property-write') {
|
|
|
|
$storage->pseudo_property_get_types[$property['name']] = $pseudo_property_type;
|
|
|
|
}
|
2017-07-25 22:11:02 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
$storage->deprecated = $docblock_info->deprecated;
|
2017-09-13 17:32:13 +02:00
|
|
|
|
2017-11-17 02:47:58 +01:00
|
|
|
$storage->sealed_properties = $docblock_info->sealed_properties;
|
2018-04-22 06:40:30 +02:00
|
|
|
$storage->sealed_methods = $docblock_info->sealed_methods;
|
2017-11-17 02:47:58 +01:00
|
|
|
|
2017-09-13 17:32:13 +02:00
|
|
|
$storage->suppressed_issues = $docblock_info->suppressed_issues;
|
2018-04-22 04:13:10 +02:00
|
|
|
|
|
|
|
foreach ($docblock_info->methods as $method) {
|
|
|
|
$storage->pseudo_methods[strtolower($method->name->name)]
|
|
|
|
= $this->registerFunctionLike($method, true);
|
|
|
|
}
|
2017-07-25 22:11:02 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if ($node instanceof PhpParser\Node\Stmt\Class_) {
|
|
|
|
$storage->abstract = (bool)$node->isAbstract();
|
2017-11-30 05:46:56 +01:00
|
|
|
$storage->final = (bool)$node->isFinal();
|
2017-07-25 22:11:02 +02:00
|
|
|
|
2018-02-04 00:52:35 +01:00
|
|
|
$this->codebase->classlikes->addFullyQualifiedClassName($fq_classlike_name, $this->file_path);
|
2017-07-25 22:11:02 +02:00
|
|
|
|
|
|
|
if ($node->extends) {
|
|
|
|
$parent_fqcln = ClassLikeChecker::getFQCLNFromNameObject($node->extends, $this->aliases);
|
2018-02-04 00:52:35 +01:00
|
|
|
$this->codebase->scanner->queueClassLikeForScanning(
|
2017-07-27 22:12:16 +02:00
|
|
|
$parent_fqcln,
|
|
|
|
$this->file_path,
|
|
|
|
$this->scan_deep
|
|
|
|
);
|
2018-02-04 00:52:35 +01:00
|
|
|
$parent_fqcln_lc = strtolower($parent_fqcln);
|
|
|
|
$storage->parent_classes[$parent_fqcln_lc] = $parent_fqcln_lc;
|
2018-02-23 04:22:31 +01:00
|
|
|
$this->file_storage->required_classes[strtolower($parent_fqcln)] = $parent_fqcln;
|
2017-07-25 22:11:02 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
foreach ($node->implements as $interface) {
|
|
|
|
$interface_fqcln = ClassLikeChecker::getFQCLNFromNameObject($interface, $this->aliases);
|
2018-02-04 00:52:35 +01:00
|
|
|
$this->codebase->scanner->queueClassLikeForScanning($interface_fqcln, $this->file_path);
|
2017-07-25 22:11:02 +02:00
|
|
|
$storage->class_implements[strtolower($interface_fqcln)] = $interface_fqcln;
|
2018-02-23 04:22:31 +01:00
|
|
|
$this->file_storage->required_interfaces[strtolower($interface_fqcln)] = $interface_fqcln;
|
2017-07-25 22:11:02 +02:00
|
|
|
}
|
|
|
|
} elseif ($node instanceof PhpParser\Node\Stmt\Interface_) {
|
2018-02-19 06:27:39 +01:00
|
|
|
$storage->is_interface = true;
|
2018-02-04 00:52:35 +01:00
|
|
|
$this->codebase->classlikes->addFullyQualifiedInterfaceName($fq_classlike_name, $this->file_path);
|
2017-07-25 22:11:02 +02:00
|
|
|
|
|
|
|
foreach ($node->extends as $interface) {
|
|
|
|
$interface_fqcln = ClassLikeChecker::getFQCLNFromNameObject($interface, $this->aliases);
|
2018-02-04 00:52:35 +01:00
|
|
|
$this->codebase->scanner->queueClassLikeForScanning($interface_fqcln, $this->file_path);
|
2017-07-25 22:11:02 +02:00
|
|
|
$storage->parent_interfaces[strtolower($interface_fqcln)] = $interface_fqcln;
|
2018-02-23 04:22:31 +01:00
|
|
|
$this->file_storage->required_interfaces[strtolower($interface_fqcln)] = $interface_fqcln;
|
2017-07-25 22:11:02 +02:00
|
|
|
}
|
|
|
|
} elseif ($node instanceof PhpParser\Node\Stmt\Trait_) {
|
|
|
|
$storage->is_trait = true;
|
2018-02-19 06:27:39 +01:00
|
|
|
$this->file_storage->has_trait = true;
|
2018-02-04 00:52:35 +01:00
|
|
|
$this->codebase->classlikes->addFullyQualifiedTraitName($fq_classlike_name, $this->file_path);
|
|
|
|
$this->codebase->classlikes->addTraitNode(
|
2018-01-21 18:44:46 +01:00
|
|
|
$fq_classlike_name,
|
2017-07-25 22:11:02 +02:00
|
|
|
$node,
|
2018-01-21 18:44:46 +01:00
|
|
|
$this->aliases
|
2017-07-25 22:11:02 +02:00
|
|
|
);
|
|
|
|
}
|
2018-01-21 18:44:46 +01:00
|
|
|
|
|
|
|
foreach ($node->stmts as $node_stmt) {
|
|
|
|
if ($node_stmt instanceof PhpParser\Node\Stmt\ClassConst) {
|
2018-05-10 20:12:50 +02:00
|
|
|
$this->visitClassConstDeclaration($node_stmt, $storage, $fq_classlike_name);
|
2018-01-21 18:44:46 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
foreach ($node->stmts as $node_stmt) {
|
|
|
|
if ($node_stmt instanceof PhpParser\Node\Stmt\Property) {
|
2018-05-10 20:12:50 +02:00
|
|
|
$this->visitPropertyDeclaration($node_stmt, $this->config, $storage, $fq_classlike_name);
|
2018-01-21 18:44:46 +01:00
|
|
|
}
|
|
|
|
}
|
2017-07-25 22:11:02 +02:00
|
|
|
} elseif (($node instanceof PhpParser\Node\Expr\New_
|
|
|
|
|| $node instanceof PhpParser\Node\Expr\Instanceof_
|
|
|
|
|| $node instanceof PhpParser\Node\Expr\StaticPropertyFetch
|
|
|
|
|| $node instanceof PhpParser\Node\Expr\ClassConstFetch
|
|
|
|
|| $node instanceof PhpParser\Node\Expr\StaticCall)
|
|
|
|
&& $node->class instanceof PhpParser\Node\Name
|
|
|
|
) {
|
|
|
|
$fq_classlike_name = ClassLikeChecker::getFQCLNFromNameObject($node->class, $this->aliases);
|
|
|
|
|
2017-11-15 03:56:29 +01:00
|
|
|
if (!in_array(strtolower($fq_classlike_name), ['self', 'static', 'parent'], true)) {
|
2018-05-22 17:59:57 +02:00
|
|
|
$this->codebase->scanner->queueClassLikeForScanning(
|
|
|
|
$fq_classlike_name,
|
|
|
|
$this->file_path,
|
|
|
|
false,
|
|
|
|
!($node instanceof PhpParser\Node\Expr\ClassConstFetch)
|
|
|
|
|| !($node->name instanceof PhpParser\Node\Identifier)
|
|
|
|
|| strtolower($node->name->name) !== 'class'
|
|
|
|
);
|
2018-02-23 04:22:31 +01:00
|
|
|
$this->file_storage->referenced_classlikes[strtolower($fq_classlike_name)] = $fq_classlike_name;
|
2017-07-25 22:11:02 +02:00
|
|
|
}
|
|
|
|
} elseif ($node instanceof PhpParser\Node\Stmt\TryCatch) {
|
|
|
|
foreach ($node->catches as $catch) {
|
|
|
|
foreach ($catch->types as $catch_type) {
|
|
|
|
$catch_fqcln = ClassLikeChecker::getFQCLNFromNameObject($catch_type, $this->aliases);
|
|
|
|
|
2017-11-15 03:56:29 +01:00
|
|
|
if (!in_array(strtolower($catch_fqcln), ['self', 'static', 'parent'], true)) {
|
2018-02-04 00:52:35 +01:00
|
|
|
$this->codebase->scanner->queueClassLikeForScanning($catch_fqcln, $this->file_path);
|
2018-02-23 04:22:31 +01:00
|
|
|
$this->file_storage->referenced_classlikes[strtolower($catch_fqcln)] = $catch_fqcln;
|
2017-07-25 22:11:02 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2017-11-28 00:07:38 +01:00
|
|
|
} elseif ($node instanceof PhpParser\Node\FunctionLike) {
|
2017-07-25 22:11:02 +02:00
|
|
|
$this->registerFunctionLike($node);
|
|
|
|
|
|
|
|
if (!$this->scan_deep) {
|
|
|
|
return PhpParser\NodeTraverser::DONT_TRAVERSE_CHILDREN;
|
|
|
|
}
|
2018-05-31 04:09:46 +02:00
|
|
|
} elseif ($node instanceof PhpParser\Node\Stmt\Global_) {
|
|
|
|
$function_like_storage = end($this->functionlike_storages);
|
|
|
|
|
|
|
|
if ($function_like_storage) {
|
|
|
|
foreach ($node->vars as $var) {
|
|
|
|
if ($var instanceof PhpParser\Node\Expr\Variable) {
|
2018-05-31 04:56:46 +02:00
|
|
|
if (is_string($var->name) && $var->name !== 'argv' && $var->name !== 'argc') {
|
2018-05-31 04:09:46 +02:00
|
|
|
$var_id = '$' . $var->name;
|
|
|
|
|
|
|
|
$function_like_storage->global_variables[$var_id] = true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2017-07-25 22:11:02 +02:00
|
|
|
} elseif ($node instanceof PhpParser\Node\Expr\FuncCall && $node->name instanceof PhpParser\Node\Name) {
|
|
|
|
$function_id = implode('\\', $node->name->parts);
|
2018-02-04 00:52:35 +01:00
|
|
|
if (CallMap::inCallMap($function_id)) {
|
|
|
|
$function_params = CallMap::getParamsFromCallMap($function_id);
|
2017-07-25 22:11:02 +02:00
|
|
|
|
|
|
|
if ($function_params) {
|
|
|
|
foreach ($function_params as $function_param_group) {
|
|
|
|
foreach ($function_param_group as $function_param) {
|
2017-09-02 17:18:56 +02:00
|
|
|
if ($function_param->type) {
|
|
|
|
$function_param->type->queueClassLikesForScanning(
|
2018-01-21 19:38:51 +01:00
|
|
|
$this->codebase,
|
2018-02-19 06:27:39 +01:00
|
|
|
$this->file_storage
|
2017-09-02 17:18:56 +02:00
|
|
|
);
|
|
|
|
}
|
2017-07-25 22:11:02 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-02-04 00:52:35 +01:00
|
|
|
$return_type = CallMap::getReturnTypeFromCallMap($function_id);
|
2017-07-25 22:11:02 +02:00
|
|
|
|
2018-02-19 06:27:39 +01:00
|
|
|
$return_type->queueClassLikesForScanning($this->codebase, $this->file_storage);
|
2017-07-25 22:11:02 +02:00
|
|
|
|
|
|
|
if ($function_id === 'get_class') {
|
|
|
|
$this->queue_strings_as_possible_type = true;
|
|
|
|
}
|
|
|
|
|
2017-07-28 16:42:30 +02:00
|
|
|
if ($function_id === 'define') {
|
2017-07-25 22:11:02 +02:00
|
|
|
$first_arg_value = isset($node->args[0]) ? $node->args[0]->value : null;
|
|
|
|
$second_arg_value = isset($node->args[1]) ? $node->args[1]->value : null;
|
|
|
|
if ($first_arg_value instanceof PhpParser\Node\Scalar\String_ && $second_arg_value) {
|
2018-06-28 03:53:25 +02:00
|
|
|
$const_type = StatementsChecker::getSimpleType(
|
|
|
|
$this->codebase,
|
|
|
|
$second_arg_value,
|
|
|
|
$this->aliases
|
|
|
|
) ?: Type::getMixed();
|
2017-07-28 16:42:30 +02:00
|
|
|
$const_name = $first_arg_value->value;
|
|
|
|
|
2018-06-03 15:13:14 +02:00
|
|
|
if ($this->functionlike_storages && !$this->config->hoist_constants) {
|
2017-08-14 21:46:01 +02:00
|
|
|
$functionlike_storage =
|
|
|
|
$this->functionlike_storages[count($this->functionlike_storages) - 1];
|
|
|
|
$functionlike_storage->defined_constants[$const_name] = $const_type;
|
2017-07-28 16:42:30 +02:00
|
|
|
} else {
|
2017-07-29 21:05:06 +02:00
|
|
|
$this->file_storage->constants[$const_name] = $const_type;
|
|
|
|
$this->file_storage->declaring_constants[$const_name] = $this->file_path;
|
2017-07-28 16:42:30 +02:00
|
|
|
}
|
2017-07-25 22:11:02 +02:00
|
|
|
}
|
|
|
|
}
|
2017-11-11 07:06:39 +01:00
|
|
|
|
2018-03-02 05:33:21 +01:00
|
|
|
$mapping_function_ids = [];
|
|
|
|
|
|
|
|
if (($function_id === 'array_map' && isset($node->args[0]))
|
|
|
|
|| ($function_id === 'array_filter' && isset($node->args[1]))
|
|
|
|
) {
|
|
|
|
$node_arg_value = $function_id = 'array_map' ? $node->args[0]->value : $node->args[1]->value;
|
|
|
|
|
|
|
|
if ($node_arg_value instanceof PhpParser\Node\Scalar\String_
|
|
|
|
|| $node_arg_value instanceof PhpParser\Node\Expr\Array_
|
|
|
|
) {
|
|
|
|
$mapping_function_ids = CallChecker::getFunctionIdsFromCallableArg(
|
|
|
|
$this->file_scanner,
|
|
|
|
$node_arg_value
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
foreach ($mapping_function_ids as $potential_method_id) {
|
|
|
|
if (strpos($potential_method_id, '::') === false) {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
list($callable_fqcln) = explode('::', $potential_method_id);
|
|
|
|
|
|
|
|
if (!in_array(strtolower($callable_fqcln), ['self', 'parent', 'static'], true)) {
|
|
|
|
$this->codebase->scanner->queueClassLikeForScanning(
|
|
|
|
$callable_fqcln,
|
|
|
|
$this->file_path
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-11-11 07:06:39 +01:00
|
|
|
if ($function_id === 'func_get_arg'
|
|
|
|
|| $function_id === 'func_get_args'
|
|
|
|
|| $function_id === 'func_num_args'
|
|
|
|
) {
|
|
|
|
$function_like_storage = end($this->functionlike_storages);
|
|
|
|
|
|
|
|
if ($function_like_storage) {
|
|
|
|
$function_like_storage->variadic = true;
|
|
|
|
}
|
|
|
|
}
|
2017-07-25 22:11:02 +02:00
|
|
|
}
|
|
|
|
} elseif ($node instanceof PhpParser\Node\Stmt\TraitUse) {
|
2017-08-14 21:46:01 +02:00
|
|
|
if (!$this->classlike_storages) {
|
|
|
|
throw new \LogicException('$this->classlike_storages should not be empty');
|
2017-07-25 22:11:02 +02:00
|
|
|
}
|
|
|
|
|
2017-08-14 21:46:01 +02:00
|
|
|
$storage = $this->classlike_storages[count($this->classlike_storages) - 1];
|
2017-07-25 22:11:02 +02:00
|
|
|
|
2018-03-13 04:37:21 +01:00
|
|
|
$method_map = $storage->trait_alias_map ?: [];
|
2017-07-25 22:11:02 +02:00
|
|
|
|
|
|
|
foreach ($node->adaptations as $adaptation) {
|
|
|
|
if ($adaptation instanceof PhpParser\Node\Stmt\TraitUseAdaptation\Alias) {
|
2018-04-17 18:16:25 +02:00
|
|
|
if ($adaptation->newName) {
|
|
|
|
$method_map[strtolower($adaptation->method->name)] = strtolower($adaptation->newName->name);
|
2017-07-25 22:11:02 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
$storage->trait_alias_map = $method_map;
|
|
|
|
|
|
|
|
foreach ($node->traits as $trait) {
|
|
|
|
$trait_fqcln = ClassLikeChecker::getFQCLNFromNameObject($trait, $this->aliases);
|
2018-02-04 00:52:35 +01:00
|
|
|
$this->codebase->scanner->queueClassLikeForScanning($trait_fqcln, $this->file_path, $this->scan_deep);
|
2017-07-25 22:11:02 +02:00
|
|
|
$storage->used_traits[strtolower($trait_fqcln)] = $trait_fqcln;
|
2018-02-23 04:22:31 +01:00
|
|
|
$this->file_storage->required_classes[strtolower($trait_fqcln)] = $trait_fqcln;
|
2017-07-25 22:11:02 +02:00
|
|
|
}
|
|
|
|
} elseif ($node instanceof PhpParser\Node\Expr\Include_) {
|
|
|
|
$this->visitInclude($node);
|
|
|
|
} elseif ($node instanceof PhpParser\Node\Scalar\String_ && $this->queue_strings_as_possible_type) {
|
|
|
|
if (preg_match('/^[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*$/', $node->value)) {
|
2018-02-04 00:52:35 +01:00
|
|
|
$this->codebase->scanner->queueClassLikeForScanning($node->value, $this->file_path, false, false);
|
2017-07-25 22:11:02 +02:00
|
|
|
}
|
|
|
|
} elseif ($node instanceof PhpParser\Node\Expr\Assign
|
|
|
|
|| $node instanceof PhpParser\Node\Expr\AssignOp
|
|
|
|
|| $node instanceof PhpParser\Node\Expr\AssignRef
|
|
|
|
) {
|
|
|
|
if ($doc_comment = $node->getDocComment()) {
|
2018-02-08 05:33:31 +01:00
|
|
|
$var_comments = [];
|
2017-11-15 03:43:31 +01:00
|
|
|
|
|
|
|
try {
|
2018-02-08 05:33:31 +01:00
|
|
|
$var_comments = CommentChecker::getTypeFromComment(
|
2017-11-15 03:43:31 +01:00
|
|
|
(string)$doc_comment,
|
2018-01-21 18:44:46 +01:00
|
|
|
$this->file_scanner,
|
2017-11-15 03:43:31 +01:00
|
|
|
$this->aliases,
|
|
|
|
null,
|
2018-07-15 23:23:17 +02:00
|
|
|
null,
|
|
|
|
null,
|
|
|
|
$this->type_aliases
|
2017-11-15 03:43:31 +01:00
|
|
|
);
|
|
|
|
} catch (DocblockParseException $e) {
|
|
|
|
// do nothing
|
|
|
|
}
|
2017-07-25 22:11:02 +02:00
|
|
|
|
2018-02-08 05:33:31 +01:00
|
|
|
foreach ($var_comments as $var_comment) {
|
2018-01-02 02:04:03 +01:00
|
|
|
$var_type = $var_comment->type;
|
2018-02-19 06:27:39 +01:00
|
|
|
$var_type->queueClassLikesForScanning($this->codebase, $this->file_storage);
|
2017-07-25 22:11:02 +02:00
|
|
|
}
|
|
|
|
}
|
2017-07-27 01:54:22 +02:00
|
|
|
} elseif ($node instanceof PhpParser\Node\Stmt\Const_) {
|
2017-07-28 16:42:30 +02:00
|
|
|
foreach ($node->consts as $const) {
|
2018-06-28 03:53:25 +02:00
|
|
|
$const_type = StatementsChecker::getSimpleType($this->codebase, $const->value, $this->aliases)
|
2018-05-10 20:12:50 +02:00
|
|
|
?: Type::getMixed();
|
2017-07-28 16:42:30 +02:00
|
|
|
|
2018-06-30 21:29:37 +02:00
|
|
|
if ($this->codebase->register_stub_files || $this->codebase->register_autoload_files) {
|
|
|
|
$this->codebase->addGlobalConstantType($const->name->name, $const_type);
|
2017-07-27 01:54:22 +02:00
|
|
|
}
|
2018-06-29 21:28:45 +02:00
|
|
|
|
|
|
|
$this->file_storage->constants[$const->name->name] = $const_type;
|
|
|
|
$this->file_storage->declaring_constants[$const->name->name] = $this->file_path;
|
2017-07-27 01:54:22 +02:00
|
|
|
}
|
2018-06-30 21:29:37 +02:00
|
|
|
} elseif ($this->codebase->register_autoload_files && $node instanceof PhpParser\Node\Stmt\If_) {
|
2018-04-27 21:00:22 +02:00
|
|
|
if ($node->cond instanceof PhpParser\Node\Expr\BooleanNot) {
|
|
|
|
if ($node->cond->expr instanceof PhpParser\Node\Expr\FuncCall
|
|
|
|
&& $node->cond->expr->name instanceof PhpParser\Node\Name
|
|
|
|
) {
|
|
|
|
if ($node->cond->expr->name->parts === ['function_exists']
|
|
|
|
&& isset($node->cond->expr->args[0])
|
|
|
|
&& $node->cond->expr->args[0]->value instanceof PhpParser\Node\Scalar\String_
|
|
|
|
&& function_exists($node->cond->expr->args[0]->value->value)
|
|
|
|
) {
|
|
|
|
return PhpParser\NodeTraverser::DONT_TRAVERSE_CHILDREN;
|
|
|
|
}
|
|
|
|
|
|
|
|
if ($node->cond->expr->name->parts === ['class_exists']
|
|
|
|
&& isset($node->cond->expr->args[0])
|
|
|
|
&& $node->cond->expr->args[0]->value instanceof PhpParser\Node\Scalar\String_
|
|
|
|
&& class_exists($node->cond->expr->args[0]->value->value, false)
|
|
|
|
) {
|
2018-07-04 21:33:48 +02:00
|
|
|
$reflection_class = new \ReflectionClass($node->cond->expr->args[0]->value->value);
|
2018-07-04 20:32:58 +02:00
|
|
|
|
2018-07-04 21:33:48 +02:00
|
|
|
if ($reflection_class->getFileName() !== $this->file_path) {
|
|
|
|
$this->codebase->scanner->queueClassLikeForScanning(
|
|
|
|
$node->cond->expr->args[0]->value->value,
|
|
|
|
$this->file_path
|
|
|
|
);
|
|
|
|
|
|
|
|
return PhpParser\NodeTraverser::DONT_TRAVERSE_CHILDREN;
|
|
|
|
}
|
2018-04-27 21:00:22 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2018-07-15 23:47:58 +02:00
|
|
|
} elseif ($node instanceof PhpParser\Node\Expr\Yield_) {
|
|
|
|
$function_like_storage = end($this->functionlike_storages);
|
|
|
|
|
|
|
|
if ($function_like_storage) {
|
|
|
|
$function_like_storage->has_yield = true;
|
|
|
|
}
|
2017-07-25 22:11:02 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-11-26 22:03:17 +01:00
|
|
|
/**
|
2018-03-07 17:16:56 +01:00
|
|
|
* @return null
|
2017-11-26 22:03:17 +01:00
|
|
|
*/
|
2017-07-25 22:11:02 +02:00
|
|
|
public function leaveNode(PhpParser\Node $node)
|
|
|
|
{
|
|
|
|
if ($node instanceof PhpParser\Node\Stmt\Namespace_) {
|
|
|
|
$this->aliases = $this->file_aliases;
|
|
|
|
} elseif ($node instanceof PhpParser\Node\Stmt\ClassLike) {
|
2017-08-14 21:46:01 +02:00
|
|
|
if (!$this->fq_classlike_names) {
|
|
|
|
throw new \LogicException('$this->fq_classlike_names should not be empty');
|
2017-07-25 22:11:02 +02:00
|
|
|
}
|
|
|
|
|
2017-10-19 20:40:38 +02:00
|
|
|
$fq_classlike_name = array_pop($this->fq_classlike_names);
|
2017-07-25 22:11:02 +02:00
|
|
|
|
2018-02-04 18:34:08 +01:00
|
|
|
if (PropertyMap::inPropertyMap($fq_classlike_name)) {
|
|
|
|
$public_mapped_properties = PropertyMap::getPropertyMap()[strtolower($fq_classlike_name)];
|
2017-07-25 22:11:02 +02:00
|
|
|
|
2017-08-14 21:46:01 +02:00
|
|
|
if (!$this->classlike_storages) {
|
|
|
|
throw new \UnexpectedValueException('$this->classlike_storages cannot be empty');
|
2017-07-29 21:05:06 +02:00
|
|
|
}
|
|
|
|
|
2017-08-14 21:46:01 +02:00
|
|
|
$storage = $this->classlike_storages[count($this->classlike_storages) - 1];
|
2017-07-25 22:11:02 +02:00
|
|
|
|
|
|
|
foreach ($public_mapped_properties as $property_name => $public_mapped_property) {
|
|
|
|
$property_type = Type::parseString($public_mapped_property);
|
|
|
|
|
2018-02-19 06:27:39 +01:00
|
|
|
$property_type->queueClassLikesForScanning($this->codebase, $this->file_storage);
|
2017-07-25 22:11:02 +02:00
|
|
|
|
|
|
|
if (!isset($storage->properties[$property_name])) {
|
|
|
|
$storage->properties[$property_name] = new PropertyStorage();
|
|
|
|
}
|
|
|
|
|
|
|
|
$storage->properties[$property_name]->type = $property_type;
|
|
|
|
$storage->properties[$property_name]->visibility = ClassLikeChecker::VISIBILITY_PUBLIC;
|
|
|
|
|
|
|
|
$property_id = $fq_classlike_name . '::$' . $property_name;
|
|
|
|
|
2018-07-22 01:56:26 +02:00
|
|
|
$storage->declaring_property_ids[$property_name] = $fq_classlike_name;
|
2017-07-25 22:11:02 +02:00
|
|
|
$storage->appearing_property_ids[$property_name] = $property_id;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-11-07 20:46:53 +01:00
|
|
|
$classlike_storage = array_pop($this->classlike_storages);
|
2017-07-29 21:05:06 +02:00
|
|
|
|
2018-05-29 16:13:26 +02:00
|
|
|
if ($classlike_storage->has_visitor_issues) {
|
|
|
|
$this->file_storage->has_visitor_issues = true;
|
|
|
|
}
|
|
|
|
|
2017-07-25 22:11:02 +02:00
|
|
|
$this->class_template_types = [];
|
2017-11-07 20:46:53 +01:00
|
|
|
|
2018-02-12 04:49:19 +01:00
|
|
|
if ($this->after_classlike_check_plugins) {
|
2017-11-07 20:46:53 +01:00
|
|
|
$file_manipulations = [];
|
|
|
|
|
2018-02-12 04:49:19 +01:00
|
|
|
foreach ($this->after_classlike_check_plugins as $plugin_fq_class_name) {
|
|
|
|
$plugin_fq_class_name::afterVisitClassLike(
|
2017-11-07 20:46:53 +01:00
|
|
|
$node,
|
|
|
|
$classlike_storage,
|
2018-01-21 18:44:46 +01:00
|
|
|
$this->file_scanner,
|
2017-11-07 20:46:53 +01:00
|
|
|
$this->aliases,
|
|
|
|
$file_manipulations
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
2018-02-19 06:27:39 +01:00
|
|
|
|
2018-05-29 16:13:26 +02:00
|
|
|
if (!$this->file_storage->has_visitor_issues) {
|
|
|
|
$this->codebase->cacheClassLikeStorage($classlike_storage, $this->file_path);
|
|
|
|
}
|
2017-07-25 22:11:02 +02:00
|
|
|
} elseif ($node instanceof PhpParser\Node\Stmt\Function_
|
|
|
|
|| $node instanceof PhpParser\Node\Stmt\ClassMethod
|
|
|
|
) {
|
|
|
|
$this->queue_strings_as_possible_type = false;
|
|
|
|
|
|
|
|
$this->function_template_types = [];
|
|
|
|
} elseif ($node instanceof PhpParser\Node\FunctionLike) {
|
2018-05-29 16:13:26 +02:00
|
|
|
$functionlike_storage = array_pop($this->functionlike_storages);
|
|
|
|
|
|
|
|
if ($functionlike_storage->has_visitor_issues) {
|
|
|
|
$this->file_storage->has_visitor_issues = true;
|
|
|
|
}
|
2017-07-25 22:11:02 +02:00
|
|
|
}
|
2017-11-26 22:03:17 +01:00
|
|
|
|
|
|
|
return null;
|
2017-07-25 22:11:02 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @param PhpParser\Node\FunctionLike $stmt
|
2018-04-22 04:13:10 +02:00
|
|
|
* @param bool $fake_method in the case of @method annotations we do something a little strange
|
2017-07-25 22:11:02 +02:00
|
|
|
*
|
2018-04-22 04:13:10 +02:00
|
|
|
* @return FunctionLikeStorage
|
2017-07-25 22:11:02 +02:00
|
|
|
*/
|
2018-04-22 04:13:10 +02:00
|
|
|
private function registerFunctionLike(PhpParser\Node\FunctionLike $stmt, $fake_method = false)
|
2017-07-25 22:11:02 +02:00
|
|
|
{
|
|
|
|
$class_storage = null;
|
|
|
|
|
2018-06-14 23:20:02 +02:00
|
|
|
if ($fake_method && $stmt instanceof PhpParser\Node\Stmt\ClassMethod) {
|
2018-04-22 04:13:10 +02:00
|
|
|
$cased_function_id = '@method ' . $stmt->name->name;
|
|
|
|
|
|
|
|
$storage = new FunctionLikeStorage();
|
|
|
|
} elseif ($stmt instanceof PhpParser\Node\Stmt\Function_) {
|
2018-04-17 18:16:25 +02:00
|
|
|
$cased_function_id =
|
|
|
|
($this->aliases->namespace ? $this->aliases->namespace . '\\' : '') . $stmt->name->name;
|
2017-07-25 22:11:02 +02:00
|
|
|
$function_id = strtolower($cased_function_id);
|
|
|
|
|
2018-06-29 21:28:45 +02:00
|
|
|
if (isset($this->file_storage->functions[$function_id])) {
|
2018-06-30 21:29:37 +02:00
|
|
|
if ($this->codebase->register_stub_files || $this->codebase->register_autoload_files) {
|
|
|
|
$this->codebase->functions->addGlobalFunction(
|
|
|
|
$function_id,
|
|
|
|
$this->file_storage->functions[$function_id]
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2018-06-29 21:28:45 +02:00
|
|
|
return $this->file_storage->functions[$function_id];
|
|
|
|
}
|
|
|
|
|
|
|
|
$storage = new FunctionLikeStorage();
|
|
|
|
|
2018-06-30 21:29:37 +02:00
|
|
|
if ($this->codebase->register_stub_files || $this->codebase->register_autoload_files) {
|
|
|
|
$this->codebase->functions->addGlobalFunction($function_id, $storage);
|
2017-07-25 22:11:02 +02:00
|
|
|
}
|
2018-06-29 21:28:45 +02:00
|
|
|
|
|
|
|
$this->file_storage->functions[$function_id] = $storage;
|
|
|
|
$this->file_storage->declaring_function_ids[$function_id] = strtolower($this->file_path);
|
2017-07-25 22:11:02 +02:00
|
|
|
} elseif ($stmt instanceof PhpParser\Node\Stmt\ClassMethod) {
|
2017-08-14 21:46:01 +02:00
|
|
|
if (!$this->fq_classlike_names) {
|
|
|
|
throw new \LogicException('$this->fq_classlike_names should not be null');
|
2017-07-25 22:11:02 +02:00
|
|
|
}
|
|
|
|
|
2017-08-14 21:46:01 +02:00
|
|
|
$fq_classlike_name = $this->fq_classlike_names[count($this->fq_classlike_names) - 1];
|
2017-07-25 22:11:02 +02:00
|
|
|
|
2018-04-17 18:16:25 +02:00
|
|
|
$function_id = $fq_classlike_name . '::' . strtolower($stmt->name->name);
|
|
|
|
$cased_function_id = $fq_classlike_name . '::' . $stmt->name->name;
|
2017-07-25 22:11:02 +02:00
|
|
|
|
2017-08-14 21:46:01 +02:00
|
|
|
if (!$this->classlike_storages) {
|
|
|
|
throw new \UnexpectedValueException('$class_storages cannot be empty for ' . $function_id);
|
2017-07-25 22:11:02 +02:00
|
|
|
}
|
|
|
|
|
2017-08-14 21:46:01 +02:00
|
|
|
$class_storage = $this->classlike_storages[count($this->classlike_storages) - 1];
|
2017-07-25 22:11:02 +02:00
|
|
|
|
2018-06-30 21:29:37 +02:00
|
|
|
$storage = null;
|
|
|
|
|
2018-04-17 18:16:25 +02:00
|
|
|
if (isset($class_storage->methods[strtolower($stmt->name->name)])) {
|
2018-06-30 21:29:37 +02:00
|
|
|
if (!$this->codebase->register_stub_files) {
|
|
|
|
throw new \InvalidArgumentException('Cannot re-register ' . $function_id);
|
|
|
|
}
|
|
|
|
|
|
|
|
$storage = $class_storage->methods[strtolower($stmt->name->name)];
|
2017-07-25 22:11:02 +02:00
|
|
|
}
|
|
|
|
|
2018-06-30 21:29:37 +02:00
|
|
|
if (!$storage) {
|
|
|
|
$storage = $class_storage->methods[strtolower($stmt->name->name)] = new MethodStorage();
|
|
|
|
}
|
2017-07-25 22:11:02 +02:00
|
|
|
|
|
|
|
$class_name_parts = explode('\\', $fq_classlike_name);
|
|
|
|
$class_name = array_pop($class_name_parts);
|
|
|
|
|
2018-04-17 18:16:25 +02:00
|
|
|
if (strtolower($stmt->name->name) === strtolower($class_name) &&
|
2017-07-25 22:11:02 +02:00
|
|
|
!isset($class_storage->methods['__construct']) &&
|
|
|
|
strpos($fq_classlike_name, '\\') === false
|
|
|
|
) {
|
2018-02-04 00:52:35 +01:00
|
|
|
$this->codebase->methods->setDeclaringMethodId(
|
2017-07-29 21:05:06 +02:00
|
|
|
$fq_classlike_name . '::__construct',
|
|
|
|
$function_id
|
|
|
|
);
|
2018-02-04 00:52:35 +01:00
|
|
|
$this->codebase->methods->setAppearingMethodId(
|
2017-07-29 21:05:06 +02:00
|
|
|
$fq_classlike_name . '::__construct',
|
|
|
|
$function_id
|
|
|
|
);
|
2017-07-25 22:11:02 +02:00
|
|
|
}
|
|
|
|
|
2018-04-17 18:16:25 +02:00
|
|
|
$class_storage->declaring_method_ids[strtolower($stmt->name->name)] = $function_id;
|
|
|
|
$class_storage->appearing_method_ids[strtolower($stmt->name->name)] = $function_id;
|
2017-07-25 22:11:02 +02:00
|
|
|
|
2018-04-17 18:16:25 +02:00
|
|
|
if (!$stmt->isPrivate() || $stmt->name->name === '__construct' || $class_storage->is_trait) {
|
|
|
|
$class_storage->inheritable_method_ids[strtolower($stmt->name->name)] = $function_id;
|
2017-11-09 03:27:23 +01:00
|
|
|
}
|
|
|
|
|
2018-04-17 18:16:25 +02:00
|
|
|
if (!isset($class_storage->overridden_method_ids[strtolower($stmt->name->name)])) {
|
|
|
|
$class_storage->overridden_method_ids[strtolower($stmt->name->name)] = [];
|
2017-07-25 22:11:02 +02:00
|
|
|
}
|
|
|
|
|
2018-03-17 05:19:55 +01:00
|
|
|
$storage->is_static = (bool) $stmt->isStatic();
|
|
|
|
$storage->abstract = (bool) $stmt->isAbstract();
|
2017-07-25 22:11:02 +02:00
|
|
|
|
2017-11-30 05:46:56 +01:00
|
|
|
$storage->final = $class_storage->final || $stmt->isFinal();
|
|
|
|
|
2017-07-25 22:11:02 +02:00
|
|
|
if ($stmt->isPrivate()) {
|
|
|
|
$storage->visibility = ClassLikeChecker::VISIBILITY_PRIVATE;
|
|
|
|
} elseif ($stmt->isProtected()) {
|
|
|
|
$storage->visibility = ClassLikeChecker::VISIBILITY_PROTECTED;
|
|
|
|
} else {
|
|
|
|
$storage->visibility = ClassLikeChecker::VISIBILITY_PUBLIC;
|
|
|
|
}
|
|
|
|
} else {
|
2018-05-25 10:21:54 +02:00
|
|
|
$function_id = $cased_function_id = $this->file_path
|
|
|
|
. ':' . $stmt->getLine()
|
|
|
|
. ':' . (int) $stmt->getAttribute('startFilePos') . ':-:closure';
|
2017-07-25 22:11:02 +02:00
|
|
|
|
2017-07-29 21:05:06 +02:00
|
|
|
$storage = $this->file_storage->functions[$function_id] = new FunctionLikeStorage();
|
2017-07-25 22:11:02 +02:00
|
|
|
}
|
|
|
|
|
2017-08-14 21:46:01 +02:00
|
|
|
$this->functionlike_storages[] = $storage;
|
2017-07-25 22:11:02 +02:00
|
|
|
|
2018-06-29 21:28:45 +02:00
|
|
|
if ($stmt instanceof PhpParser\Node\Stmt\ClassMethod) {
|
2018-04-17 18:16:25 +02:00
|
|
|
$storage->cased_name = $stmt->name->name;
|
2018-06-29 21:28:45 +02:00
|
|
|
} elseif ($stmt instanceof PhpParser\Node\Stmt\Function_) {
|
|
|
|
$storage->cased_name =
|
|
|
|
($this->aliases->namespace ? $this->aliases->namespace . '\\' : '') . $stmt->name->name;
|
2017-07-25 22:11:02 +02:00
|
|
|
}
|
|
|
|
|
2018-01-21 18:44:46 +01:00
|
|
|
$storage->location = new CodeLocation($this->file_scanner, $stmt, null, true);
|
2017-07-25 22:11:02 +02:00
|
|
|
|
|
|
|
$required_param_count = 0;
|
|
|
|
$i = 0;
|
|
|
|
$has_optional_param = false;
|
|
|
|
|
2017-09-02 17:18:56 +02:00
|
|
|
$existing_params = [];
|
2018-06-30 21:29:37 +02:00
|
|
|
$storage->params = [];
|
2017-09-02 17:18:56 +02:00
|
|
|
|
2017-07-25 22:11:02 +02:00
|
|
|
/** @var PhpParser\Node\Param $param */
|
|
|
|
foreach ($stmt->getParams() as $param) {
|
2018-06-03 16:05:50 +02:00
|
|
|
if ($param->var instanceof PhpParser\Node\Expr\Error) {
|
|
|
|
if (IssueBuffer::accepts(
|
|
|
|
new InvalidDocblock(
|
|
|
|
'Param' . ((int) $i + 1) . ' of ' . $cased_function_id . ' has invalid syntax',
|
|
|
|
new CodeLocation($this->file_scanner, $param, null, true)
|
|
|
|
)
|
|
|
|
)) {
|
|
|
|
$storage->has_visitor_issues = true;
|
2018-06-04 04:13:19 +02:00
|
|
|
}
|
2018-06-03 16:05:50 +02:00
|
|
|
|
2018-06-04 04:13:19 +02:00
|
|
|
++$i;
|
2018-06-03 16:05:50 +02:00
|
|
|
|
2018-06-04 04:13:19 +02:00
|
|
|
continue;
|
2018-06-03 16:05:50 +02:00
|
|
|
}
|
|
|
|
|
2017-07-25 22:11:02 +02:00
|
|
|
$param_array = $this->getTranslatedFunctionParam($param);
|
|
|
|
|
2018-02-23 21:39:33 +01:00
|
|
|
if (isset($existing_params['$' . $param_array->name])) {
|
2017-07-25 22:11:02 +02:00
|
|
|
if (IssueBuffer::accepts(
|
|
|
|
new DuplicateParam(
|
2018-04-17 18:16:25 +02:00
|
|
|
'Duplicate param $' . $param_array->name . ' in docblock for ' . $cased_function_id,
|
2018-01-21 18:44:46 +01:00
|
|
|
new CodeLocation($this->file_scanner, $param, null, true)
|
2017-07-25 22:11:02 +02:00
|
|
|
)
|
|
|
|
)) {
|
2018-05-29 16:13:26 +02:00
|
|
|
$storage->has_visitor_issues = true;
|
|
|
|
|
2018-06-03 16:05:50 +02:00
|
|
|
++$i;
|
|
|
|
|
2017-09-02 17:18:56 +02:00
|
|
|
continue;
|
2017-07-25 22:11:02 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-02-23 21:39:33 +01:00
|
|
|
$existing_params['$' . $param_array->name] = $i;
|
2017-07-25 22:11:02 +02:00
|
|
|
$storage->param_types[$param_array->name] = $param_array->type;
|
|
|
|
$storage->params[] = $param_array;
|
|
|
|
|
|
|
|
if (!$param_array->is_optional) {
|
|
|
|
$required_param_count = $i + 1;
|
|
|
|
|
2018-06-03 16:05:50 +02:00
|
|
|
if (!$param->variadic
|
|
|
|
&& $has_optional_param
|
|
|
|
&& is_string($param->var->name)
|
|
|
|
) {
|
2017-07-25 22:11:02 +02:00
|
|
|
if (IssueBuffer::accepts(
|
|
|
|
new MisplacedRequiredParam(
|
2018-04-17 18:16:25 +02:00
|
|
|
'Required param $' . $param->var->name . ' should come before any optional params in ' .
|
2017-07-25 22:11:02 +02:00
|
|
|
$cased_function_id,
|
2018-01-21 18:44:46 +01:00
|
|
|
new CodeLocation($this->file_scanner, $param, null, true)
|
2017-07-25 22:11:02 +02:00
|
|
|
)
|
|
|
|
)) {
|
2018-05-29 16:13:26 +02:00
|
|
|
$storage->has_visitor_issues = true;
|
2017-07-25 22:11:02 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
$has_optional_param = true;
|
|
|
|
}
|
|
|
|
|
|
|
|
++$i;
|
|
|
|
}
|
|
|
|
|
|
|
|
$storage->required_param_count = $required_param_count;
|
|
|
|
|
2017-09-21 22:31:19 +02:00
|
|
|
if (($stmt instanceof PhpParser\Node\Stmt\Function_
|
|
|
|
|| $stmt instanceof PhpParser\Node\Stmt\ClassMethod)
|
2018-04-17 18:16:25 +02:00
|
|
|
&& strpos($stmt->name->name, 'assert') === 0
|
2017-09-21 22:31:19 +02:00
|
|
|
&& $stmt->stmts
|
|
|
|
) {
|
2017-09-21 21:36:26 +02:00
|
|
|
$var_assertions = [];
|
|
|
|
|
|
|
|
foreach ($stmt->stmts as $function_stmt) {
|
2018-02-23 21:39:33 +01:00
|
|
|
if ($function_stmt instanceof PhpParser\Node\Stmt\If_) {
|
|
|
|
$final_actions = \Psalm\Checker\ScopeChecker::getFinalControlActions(
|
|
|
|
$function_stmt->stmts,
|
2018-07-13 05:26:08 +02:00
|
|
|
$this->config->exit_functions,
|
2018-02-23 21:39:33 +01:00
|
|
|
false,
|
|
|
|
false
|
|
|
|
);
|
|
|
|
|
|
|
|
if ($final_actions !== [\Psalm\Checker\ScopeChecker::ACTION_END]) {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
2018-05-07 07:26:06 +02:00
|
|
|
$if_clauses = \Psalm\Type\Algebra::getFormula(
|
2018-02-23 21:39:33 +01:00
|
|
|
$function_stmt->cond,
|
|
|
|
$this->fq_classlike_names
|
|
|
|
? $this->fq_classlike_names[count($this->fq_classlike_names) - 1]
|
|
|
|
: null,
|
|
|
|
$this->file_scanner
|
|
|
|
);
|
|
|
|
|
2018-05-07 07:26:06 +02:00
|
|
|
$negated_formula = \Psalm\Type\Algebra::negateFormula($if_clauses);
|
2017-09-21 21:36:26 +02:00
|
|
|
|
2018-05-07 07:26:06 +02:00
|
|
|
$rules = \Psalm\Type\Algebra::getTruthsFromFormula($negated_formula);
|
2018-02-23 21:39:33 +01:00
|
|
|
|
|
|
|
foreach ($rules as $var_id => $rule) {
|
2018-05-13 01:38:43 +02:00
|
|
|
foreach ($rule as $rule_part) {
|
|
|
|
if (count($rule_part) > 1) {
|
|
|
|
continue 2;
|
|
|
|
}
|
2018-02-25 17:30:45 +01:00
|
|
|
}
|
|
|
|
|
2018-02-23 21:39:33 +01:00
|
|
|
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
|
|
|
|
);
|
2017-09-21 21:36:26 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
$storage->assertions = $var_assertions;
|
|
|
|
}
|
|
|
|
|
2018-03-17 14:22:28 +01:00
|
|
|
if (!$this->scan_deep
|
|
|
|
&& ($stmt instanceof PhpParser\Node\Stmt\Function_
|
|
|
|
|| $stmt instanceof PhpParser\Node\Stmt\ClassMethod
|
|
|
|
|| $stmt instanceof PhpParser\Node\Expr\Closure)
|
|
|
|
&& $stmt->stmts
|
|
|
|
) {
|
|
|
|
// pick up func_get_args that would otherwise be missed
|
|
|
|
foreach ($stmt->stmts as $function_stmt) {
|
2018-04-17 18:16:25 +02:00
|
|
|
if ($function_stmt instanceof PhpParser\Node\Stmt\Expression
|
|
|
|
&& $function_stmt->expr instanceof PhpParser\Node\Expr\Assign
|
|
|
|
&& ($function_stmt->expr->expr instanceof PhpParser\Node\Expr\FuncCall)
|
|
|
|
&& ($function_stmt->expr->expr->name instanceof PhpParser\Node\Name)
|
2018-03-17 14:22:28 +01:00
|
|
|
) {
|
2018-04-17 18:16:25 +02:00
|
|
|
$function_id = implode('\\', $function_stmt->expr->expr->name->parts);
|
2018-03-17 14:22:28 +01:00
|
|
|
|
|
|
|
if ($function_id === 'func_get_arg'
|
|
|
|
|| $function_id === 'func_get_args'
|
|
|
|
|| $function_id === 'func_num_args'
|
|
|
|
) {
|
|
|
|
$storage->variadic = true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-11-26 22:03:17 +01:00
|
|
|
$parser_return_type = $stmt->getReturnType();
|
|
|
|
|
|
|
|
if ($parser_return_type) {
|
2017-07-25 22:11:02 +02:00
|
|
|
$suffix = '';
|
|
|
|
|
|
|
|
if ($parser_return_type instanceof PhpParser\Node\NullableType) {
|
|
|
|
$suffix = '|null';
|
|
|
|
$parser_return_type = $parser_return_type->type;
|
|
|
|
}
|
|
|
|
|
2018-04-17 18:16:25 +02:00
|
|
|
if ($parser_return_type instanceof PhpParser\Node\Identifier) {
|
|
|
|
$return_type_string = $parser_return_type->name . $suffix;
|
2017-07-25 22:11:02 +02:00
|
|
|
} else {
|
|
|
|
$return_type_fq_classlike_name = ClassLikeChecker::getFQCLNFromNameObject(
|
|
|
|
$parser_return_type,
|
|
|
|
$this->aliases
|
|
|
|
);
|
|
|
|
|
2017-11-22 03:50:39 +01:00
|
|
|
if (!in_array(strtolower($return_type_fq_classlike_name), ['self', 'parent'], true)) {
|
2018-02-04 00:52:35 +01:00
|
|
|
$this->codebase->scanner->queueClassLikeForScanning(
|
2017-07-27 22:12:16 +02:00
|
|
|
$return_type_fq_classlike_name,
|
|
|
|
$this->file_path
|
|
|
|
);
|
2017-07-25 22:11:02 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
$return_type_string = $return_type_fq_classlike_name . $suffix;
|
|
|
|
}
|
|
|
|
|
2018-01-10 06:07:47 +01:00
|
|
|
$storage->return_type = Type::parseString($return_type_string, true);
|
2017-07-25 22:11:02 +02:00
|
|
|
$storage->return_type_location = new CodeLocation(
|
2018-01-21 18:44:46 +01:00
|
|
|
$this->file_scanner,
|
2017-07-25 22:11:02 +02:00
|
|
|
$stmt,
|
|
|
|
null,
|
|
|
|
false,
|
2018-01-02 02:04:03 +01:00
|
|
|
CodeLocation::FUNCTION_RETURN_TYPE
|
2017-07-25 22:11:02 +02:00
|
|
|
);
|
|
|
|
|
2018-01-12 05:18:13 +01:00
|
|
|
if ($stmt->returnsByRef()) {
|
|
|
|
$storage->return_type->by_ref = true;
|
|
|
|
}
|
|
|
|
|
2017-07-25 22:11:02 +02:00
|
|
|
$storage->signature_return_type = $storage->return_type;
|
|
|
|
$storage->signature_return_type_location = $storage->return_type_location;
|
|
|
|
}
|
|
|
|
|
2018-01-13 07:25:13 +01:00
|
|
|
if ($stmt->returnsByRef()) {
|
|
|
|
$storage->returns_by_ref = true;
|
|
|
|
}
|
|
|
|
|
2017-07-25 22:11:02 +02:00
|
|
|
$doc_comment = $stmt->getDocComment();
|
|
|
|
|
|
|
|
if (!$doc_comment) {
|
2018-04-22 04:13:10 +02:00
|
|
|
return $storage;
|
2017-07-25 22:11:02 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
try {
|
|
|
|
$docblock_info = CommentChecker::extractFunctionDocblockInfo(
|
|
|
|
(string)$doc_comment,
|
2018-07-15 23:23:17 +02:00
|
|
|
$doc_comment->getLine(),
|
|
|
|
$this->aliases,
|
|
|
|
$this->type_aliases
|
2017-07-25 22:11:02 +02:00
|
|
|
);
|
2017-11-15 03:43:31 +01:00
|
|
|
} catch (IncorrectDocblockException $e) {
|
|
|
|
if (IssueBuffer::accepts(
|
|
|
|
new MissingDocblockType(
|
|
|
|
$e->getMessage() . ' in docblock for ' . $cased_function_id,
|
2018-01-21 18:44:46 +01:00
|
|
|
new CodeLocation($this->file_scanner, $stmt, null, true)
|
2017-11-15 03:43:31 +01:00
|
|
|
)
|
|
|
|
)) {
|
2018-05-29 16:13:26 +02:00
|
|
|
$storage->has_visitor_issues = true;
|
2017-11-15 03:43:31 +01:00
|
|
|
}
|
2017-11-28 06:46:41 +01:00
|
|
|
|
|
|
|
$docblock_info = null;
|
2017-07-25 22:11:02 +02:00
|
|
|
} catch (DocblockParseException $e) {
|
|
|
|
if (IssueBuffer::accepts(
|
|
|
|
new InvalidDocblock(
|
|
|
|
$e->getMessage() . ' in docblock for ' . $cased_function_id,
|
2018-01-21 18:44:46 +01:00
|
|
|
new CodeLocation($this->file_scanner, $stmt, null, true)
|
2017-07-25 22:11:02 +02:00
|
|
|
)
|
|
|
|
)) {
|
2018-05-29 16:13:26 +02:00
|
|
|
$storage->has_visitor_issues = true;
|
2017-07-25 22:11:02 +02:00
|
|
|
}
|
2017-11-28 06:46:41 +01:00
|
|
|
|
|
|
|
$docblock_info = null;
|
2017-07-25 22:11:02 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
if (!$docblock_info) {
|
2018-04-22 04:13:10 +02:00
|
|
|
return $storage;
|
2017-07-25 22:11:02 +02:00
|
|
|
}
|
|
|
|
|
2018-07-15 23:23:17 +02:00
|
|
|
if ($docblock_info->type_aliases) {
|
|
|
|
$this->type_aliases += $docblock_info->type_aliases;
|
|
|
|
}
|
|
|
|
|
2017-07-25 22:11:02 +02:00
|
|
|
if ($docblock_info->deprecated) {
|
|
|
|
$storage->deprecated = true;
|
|
|
|
}
|
|
|
|
|
|
|
|
if ($docblock_info->variadic) {
|
|
|
|
$storage->variadic = true;
|
|
|
|
}
|
|
|
|
|
|
|
|
if ($docblock_info->ignore_nullable_return && $storage->return_type) {
|
|
|
|
$storage->return_type->ignore_nullable_issues = true;
|
|
|
|
}
|
|
|
|
|
2018-01-25 00:52:58 +01:00
|
|
|
if ($docblock_info->ignore_falsable_return && $storage->return_type) {
|
|
|
|
$storage->return_type->ignore_falsable_issues = true;
|
|
|
|
}
|
|
|
|
|
2017-07-25 22:11:02 +02:00
|
|
|
$storage->suppressed_issues = $docblock_info->suppress;
|
|
|
|
|
2018-06-22 07:13:49 +02:00
|
|
|
if ($this->config->check_for_throws_docblock) {
|
|
|
|
foreach ($docblock_info->throws as $throw_class) {
|
|
|
|
$exception_fqcln = Type::getFQCLNFromString(
|
|
|
|
$throw_class,
|
|
|
|
$this->aliases
|
|
|
|
);
|
|
|
|
|
|
|
|
$storage->throws[$exception_fqcln] = true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-07-25 22:11:02 +02:00
|
|
|
if (!$this->config->use_docblock_types) {
|
2018-04-22 04:13:10 +02:00
|
|
|
return $storage;
|
2017-07-25 22:11:02 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
$template_types = $class_storage && $class_storage->template_types ? $class_storage->template_types : null;
|
|
|
|
|
2018-06-19 19:19:34 +02:00
|
|
|
if ($docblock_info->template_type_names) {
|
2017-07-25 22:11:02 +02:00
|
|
|
$storage->template_types = [];
|
|
|
|
|
2018-06-19 19:19:34 +02:00
|
|
|
foreach ($docblock_info->template_type_names as $template_type) {
|
2017-07-25 22:11:02 +02:00
|
|
|
if (count($template_type) === 3) {
|
2018-06-19 19:19:34 +02:00
|
|
|
$storage->template_types[$template_type[0]] = Type::parseTokens(
|
|
|
|
Type::fixUpLocalType(
|
|
|
|
$template_type[2],
|
2018-07-15 23:23:17 +02:00
|
|
|
$this->aliases,
|
|
|
|
null,
|
|
|
|
$this->type_aliases
|
2018-06-19 19:19:34 +02:00
|
|
|
)
|
|
|
|
);
|
2017-07-25 22:11:02 +02:00
|
|
|
} else {
|
2018-06-19 19:19:34 +02:00
|
|
|
$storage->template_types[$template_type[0]] = Type::getMixed();
|
2017-07-25 22:11:02 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
$template_types = array_merge($template_types ?: [], $storage->template_types);
|
|
|
|
|
|
|
|
$this->function_template_types = $template_types;
|
|
|
|
}
|
|
|
|
|
|
|
|
if ($docblock_info->template_typeofs) {
|
|
|
|
$storage->template_typeof_params = [];
|
|
|
|
|
|
|
|
foreach ($docblock_info->template_typeofs as $template_typeof) {
|
|
|
|
foreach ($storage->params as $i => $param) {
|
|
|
|
if ($param->name === $template_typeof['param_name']) {
|
|
|
|
$storage->template_typeof_params[$i] = $template_typeof['template_type'];
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-05-28 21:07:42 +02:00
|
|
|
if ($docblock_info->assertions) {
|
|
|
|
$storage->assertions = [];
|
|
|
|
|
|
|
|
foreach ($docblock_info->assertions as $assertion) {
|
|
|
|
foreach ($storage->params as $i => $param) {
|
|
|
|
if ($param->name === $assertion['param_name']) {
|
|
|
|
$storage->assertions[] = new \Psalm\Storage\Assertion(
|
|
|
|
$i,
|
|
|
|
[[$assertion['type']]]
|
|
|
|
);
|
|
|
|
continue 2;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
$storage->assertions[] = new \Psalm\Storage\Assertion(
|
|
|
|
$assertion['param_name'],
|
|
|
|
[[$assertion['type']]]
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if ($docblock_info->if_true_assertions) {
|
|
|
|
$storage->assertions = [];
|
|
|
|
|
|
|
|
foreach ($docblock_info->if_true_assertions as $assertion) {
|
|
|
|
foreach ($storage->params as $i => $param) {
|
|
|
|
if ($param->name === $assertion['param_name']) {
|
|
|
|
$storage->if_true_assertions[] = new \Psalm\Storage\Assertion(
|
|
|
|
$i,
|
|
|
|
[[$assertion['type']]]
|
|
|
|
);
|
|
|
|
continue 2;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
$storage->if_true_assertions[] = new \Psalm\Storage\Assertion(
|
|
|
|
$assertion['param_name'],
|
|
|
|
[[$assertion['type']]]
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if ($docblock_info->if_false_assertions) {
|
|
|
|
$storage->assertions = [];
|
|
|
|
|
|
|
|
foreach ($docblock_info->if_false_assertions as $assertion) {
|
|
|
|
foreach ($storage->params as $i => $param) {
|
|
|
|
if ($param->name === $assertion['param_name']) {
|
|
|
|
$storage->if_false_assertions[] = new \Psalm\Storage\Assertion(
|
|
|
|
$i,
|
|
|
|
[[$assertion['type']]]
|
|
|
|
);
|
|
|
|
continue 2;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
$storage->if_false_assertions[] = new \Psalm\Storage\Assertion(
|
|
|
|
$assertion['param_name'],
|
|
|
|
[[$assertion['type']]]
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-07-25 22:11:02 +02:00
|
|
|
if ($docblock_info->return_type) {
|
2018-01-09 21:44:31 +01:00
|
|
|
if (!$storage->return_type || $docblock_info->return_type !== $storage->return_type->getId()) {
|
2017-07-25 22:11:02 +02:00
|
|
|
$storage->has_template_return_type =
|
|
|
|
$template_types !== null &&
|
|
|
|
count(
|
|
|
|
array_intersect(
|
|
|
|
Type::tokenize($docblock_info->return_type),
|
|
|
|
array_keys($template_types)
|
|
|
|
)
|
|
|
|
) > 0;
|
|
|
|
|
|
|
|
$docblock_return_type = $docblock_info->return_type;
|
|
|
|
|
|
|
|
if (!$storage->return_type_location) {
|
2018-01-02 02:04:03 +01:00
|
|
|
$storage->return_type_location = new CodeLocation(
|
2018-01-21 18:44:46 +01:00
|
|
|
$this->file_scanner,
|
2018-01-02 02:04:03 +01:00
|
|
|
$stmt,
|
|
|
|
null,
|
|
|
|
false,
|
|
|
|
CodeLocation::FUNCTION_PHPDOC_RETURN_TYPE,
|
|
|
|
$docblock_info->return_type
|
|
|
|
);
|
2017-07-25 22:11:02 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
if ($docblock_return_type) {
|
|
|
|
try {
|
2018-05-20 23:19:53 +02:00
|
|
|
$fixed_type_tokens = Type::fixUpLocalType(
|
2018-03-27 14:07:44 +02:00
|
|
|
$docblock_return_type,
|
|
|
|
$this->aliases,
|
2018-07-15 23:23:17 +02:00
|
|
|
$this->function_template_types + $this->class_template_types,
|
|
|
|
$this->type_aliases
|
2018-03-27 14:07:44 +02:00
|
|
|
);
|
|
|
|
|
2018-05-20 23:19:53 +02:00
|
|
|
$storage->return_type = Type::parseTokens(
|
|
|
|
$fixed_type_tokens,
|
2018-04-20 16:52:23 +02:00
|
|
|
false,
|
|
|
|
$this->function_template_types + $this->class_template_types
|
|
|
|
);
|
2017-07-25 22:11:02 +02:00
|
|
|
$storage->return_type->setFromDocblock();
|
2018-01-07 22:11:51 +01:00
|
|
|
|
|
|
|
if ($storage->signature_return_type) {
|
2018-02-05 22:57:33 +01:00
|
|
|
$all_typehint_types_match = true;
|
2018-02-01 05:27:25 +01:00
|
|
|
$signature_return_atomic_types = $storage->signature_return_type->getTypes();
|
|
|
|
|
2018-01-09 21:05:48 +01:00
|
|
|
foreach ($storage->return_type->getTypes() as $key => $type) {
|
2018-02-01 05:27:25 +01:00
|
|
|
if (isset($signature_return_atomic_types[$key])) {
|
2018-01-07 22:11:51 +01:00
|
|
|
$type->from_docblock = false;
|
2018-01-08 23:17:49 +01:00
|
|
|
} else {
|
2018-02-05 22:57:33 +01:00
|
|
|
$all_typehint_types_match = false;
|
2018-01-08 23:17:49 +01:00
|
|
|
}
|
2018-02-01 05:27:25 +01:00
|
|
|
}
|
2018-01-08 23:17:49 +01:00
|
|
|
|
2018-02-05 22:57:33 +01:00
|
|
|
if ($all_typehint_types_match) {
|
2018-02-01 05:27:25 +01:00
|
|
|
$storage->return_type->from_docblock = false;
|
2018-01-07 22:11:51 +01:00
|
|
|
}
|
2018-05-29 15:44:38 +02:00
|
|
|
|
|
|
|
if ($storage->signature_return_type->isNullable()
|
|
|
|
&& !$storage->return_type->isNullable()
|
|
|
|
) {
|
|
|
|
$storage->return_type->addType(new Type\Atomic\TNull());
|
|
|
|
}
|
2018-01-07 22:11:51 +01:00
|
|
|
}
|
|
|
|
|
2018-02-19 06:27:39 +01:00
|
|
|
$storage->return_type->queueClassLikesForScanning($this->codebase, $this->file_storage);
|
2017-10-22 07:04:35 +02:00
|
|
|
} catch (TypeParseTreeException $e) {
|
2017-07-25 22:11:02 +02:00
|
|
|
if (IssueBuffer::accepts(
|
|
|
|
new InvalidDocblock(
|
|
|
|
$e->getMessage() . ' in docblock for ' . $cased_function_id,
|
2018-01-21 18:44:46 +01:00
|
|
|
new CodeLocation($this->file_scanner, $stmt, null, true)
|
2017-07-25 22:11:02 +02:00
|
|
|
)
|
|
|
|
)) {
|
2018-05-29 16:13:26 +02:00
|
|
|
$storage->has_visitor_issues = true;
|
2017-07-25 22:11:02 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if ($storage->return_type && $docblock_info->ignore_nullable_return) {
|
|
|
|
$storage->return_type->ignore_nullable_issues = true;
|
|
|
|
}
|
|
|
|
|
2018-01-25 00:52:58 +01:00
|
|
|
if ($storage->return_type && $docblock_info->ignore_falsable_return) {
|
|
|
|
$storage->return_type->ignore_falsable_issues = true;
|
|
|
|
}
|
|
|
|
|
2018-01-13 07:25:13 +01:00
|
|
|
if ($stmt->returnsByRef() && $storage->return_type) {
|
2018-01-12 05:18:13 +01:00
|
|
|
$storage->return_type->by_ref = true;
|
|
|
|
}
|
|
|
|
|
2017-07-25 22:11:02 +02:00
|
|
|
if ($docblock_info->return_type_line_number) {
|
|
|
|
$storage->return_type_location->setCommentLine($docblock_info->return_type_line_number);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-06-08 15:31:21 +02:00
|
|
|
foreach ($docblock_info->globals as $global) {
|
|
|
|
try {
|
|
|
|
$storage->global_types[$global['name']] = Type::parseTokens(
|
|
|
|
Type::fixUpLocalType(
|
|
|
|
$global['type'],
|
2018-07-15 23:23:17 +02:00
|
|
|
$this->aliases,
|
|
|
|
null,
|
|
|
|
$this->type_aliases
|
2018-06-08 15:31:21 +02:00
|
|
|
),
|
|
|
|
false
|
|
|
|
);
|
|
|
|
} catch (TypeParseTreeException $e) {
|
|
|
|
if (IssueBuffer::accepts(
|
|
|
|
new InvalidDocblock(
|
|
|
|
$e->getMessage() . ' in docblock for ' . $cased_function_id,
|
|
|
|
new CodeLocation($this->file_scanner, $stmt, null, true)
|
|
|
|
)
|
|
|
|
)) {
|
|
|
|
$storage->has_visitor_issues = true;
|
|
|
|
}
|
|
|
|
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-07-25 22:11:02 +02:00
|
|
|
if ($docblock_info->params) {
|
|
|
|
$this->improveParamsFromDocblock(
|
|
|
|
$storage,
|
|
|
|
$docblock_info->params,
|
2018-01-02 02:04:03 +01:00
|
|
|
$stmt
|
2017-07-25 22:11:02 +02:00
|
|
|
);
|
|
|
|
}
|
2018-04-22 04:13:10 +02:00
|
|
|
|
|
|
|
return $storage;
|
2017-07-25 22:11:02 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @param PhpParser\Node\Param $param
|
|
|
|
*
|
|
|
|
* @return FunctionLikeParameter
|
|
|
|
*/
|
|
|
|
public function getTranslatedFunctionParam(PhpParser\Node\Param $param)
|
|
|
|
{
|
|
|
|
$param_type = null;
|
|
|
|
|
|
|
|
$is_nullable = $param->default !== null &&
|
|
|
|
$param->default instanceof PhpParser\Node\Expr\ConstFetch &&
|
|
|
|
$param->default->name instanceof PhpParser\Node\Name &&
|
|
|
|
strtolower($param->default->name->parts[0]) === 'null';
|
|
|
|
|
|
|
|
$param_typehint = $param->type;
|
|
|
|
|
|
|
|
if ($param_typehint instanceof PhpParser\Node\NullableType) {
|
|
|
|
$is_nullable = true;
|
|
|
|
$param_typehint = $param_typehint->type;
|
|
|
|
}
|
|
|
|
|
|
|
|
if ($param_typehint) {
|
2018-04-17 18:16:25 +02:00
|
|
|
if ($param_typehint instanceof PhpParser\Node\Identifier) {
|
|
|
|
$param_type_string = $param_typehint->name;
|
2017-07-25 22:11:02 +02:00
|
|
|
} elseif ($param_typehint instanceof PhpParser\Node\Name\FullyQualified) {
|
|
|
|
$param_type_string = (string)$param_typehint;
|
2018-02-04 00:52:35 +01:00
|
|
|
$this->codebase->scanner->queueClassLikeForScanning($param_type_string, $this->file_path);
|
2017-11-15 03:56:29 +01:00
|
|
|
} elseif (strtolower($param_typehint->parts[0]) === 'self') {
|
2017-08-14 21:46:01 +02:00
|
|
|
$param_type_string = $this->fq_classlike_names[count($this->fq_classlike_names) - 1];
|
2017-07-25 22:11:02 +02:00
|
|
|
} else {
|
|
|
|
$param_type_string = ClassLikeChecker::getFQCLNFromNameObject($param_typehint, $this->aliases);
|
2018-04-17 18:16:25 +02:00
|
|
|
|
2017-11-15 03:56:29 +01:00
|
|
|
if (!in_array(strtolower($param_type_string), ['self', 'static', 'parent'], true)) {
|
2018-02-04 00:52:35 +01:00
|
|
|
$this->codebase->scanner->queueClassLikeForScanning($param_type_string, $this->file_path);
|
2018-02-23 04:22:31 +01:00
|
|
|
$this->file_storage->referenced_classlikes[strtolower($param_type_string)] = $param_type_string;
|
2017-07-25 22:11:02 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if ($param_type_string) {
|
|
|
|
if ($is_nullable) {
|
|
|
|
$param_type_string .= '|null';
|
|
|
|
}
|
|
|
|
|
2018-01-10 06:07:47 +01:00
|
|
|
$param_type = Type::parseString($param_type_string, true);
|
2017-07-25 22:11:02 +02:00
|
|
|
|
|
|
|
if ($param->variadic) {
|
|
|
|
$param_type = new Type\Union([
|
|
|
|
new Type\Atomic\TArray([
|
|
|
|
Type::getInt(),
|
|
|
|
$param_type,
|
|
|
|
]),
|
|
|
|
]);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
} elseif ($param->variadic) {
|
|
|
|
$param_type = new Type\Union([
|
|
|
|
new Type\Atomic\TArray([
|
|
|
|
Type::getInt(),
|
|
|
|
Type::getMixed(),
|
|
|
|
]),
|
|
|
|
]);
|
|
|
|
}
|
|
|
|
|
|
|
|
$is_optional = $param->default !== null;
|
|
|
|
|
2018-06-03 16:05:50 +02:00
|
|
|
if ($param->var instanceof PhpParser\Node\Expr\Error || !is_string($param->var->name)) {
|
2018-04-17 18:16:25 +02:00
|
|
|
throw new \UnexpectedValueException('Not expecting param name to be non-string');
|
|
|
|
}
|
|
|
|
|
2017-07-25 22:11:02 +02:00
|
|
|
return new FunctionLikeParameter(
|
2018-04-17 18:16:25 +02:00
|
|
|
$param->var->name,
|
2017-07-25 22:11:02 +02:00
|
|
|
$param->byRef,
|
2017-09-02 17:18:56 +02:00
|
|
|
$param_type,
|
2018-01-21 18:44:46 +01:00
|
|
|
new CodeLocation($this->file_scanner, $param, null, false, CodeLocation::FUNCTION_PARAM_VAR),
|
2017-12-30 03:28:21 +01:00
|
|
|
$param_typehint
|
2018-01-21 18:44:46 +01:00
|
|
|
? new CodeLocation($this->file_scanner, $param, null, false, CodeLocation::FUNCTION_PARAM_TYPE)
|
2017-12-30 03:28:21 +01:00
|
|
|
: null,
|
2017-07-25 22:11:02 +02:00
|
|
|
$is_optional,
|
|
|
|
$is_nullable,
|
2018-06-07 18:23:10 +02:00
|
|
|
$param->variadic,
|
2018-06-28 03:53:25 +02:00
|
|
|
$param->default ? StatementsChecker::getSimpleType($this->codebase, $param->default, $this->aliases) : null
|
2017-07-25 22:11:02 +02:00
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @param FunctionLikeStorage $storage
|
2018-02-18 23:55:11 +01:00
|
|
|
* @param array<int, array{type:string,name:string,line_number:int}> $docblock_params
|
2017-07-25 22:11:02 +02:00
|
|
|
* @param PhpParser\Node\FunctionLike $function
|
|
|
|
*
|
|
|
|
* @return void
|
|
|
|
*/
|
2018-01-21 19:38:51 +01:00
|
|
|
private function improveParamsFromDocblock(
|
2017-07-25 22:11:02 +02:00
|
|
|
FunctionLikeStorage $storage,
|
|
|
|
array $docblock_params,
|
2018-01-02 02:04:03 +01:00
|
|
|
PhpParser\Node\FunctionLike $function
|
2017-07-25 22:11:02 +02:00
|
|
|
) {
|
2017-08-14 21:46:01 +02:00
|
|
|
$base = $this->fq_classlike_names
|
|
|
|
? $this->fq_classlike_names[count($this->fq_classlike_names) - 1] . '::'
|
|
|
|
: '';
|
2017-07-25 22:11:02 +02:00
|
|
|
|
|
|
|
$cased_method_id = $base . $storage->cased_name;
|
|
|
|
|
|
|
|
foreach ($docblock_params as $docblock_param) {
|
|
|
|
$param_name = $docblock_param['name'];
|
|
|
|
$docblock_param_variadic = false;
|
|
|
|
|
|
|
|
if (substr($param_name, 0, 3) === '...') {
|
|
|
|
$docblock_param_variadic = true;
|
|
|
|
$param_name = substr($param_name, 3);
|
|
|
|
}
|
|
|
|
|
|
|
|
$param_name = substr($param_name, 1);
|
|
|
|
|
|
|
|
$storage_param = null;
|
|
|
|
|
|
|
|
foreach ($storage->params as $function_signature_param) {
|
|
|
|
if ($function_signature_param->name === $param_name) {
|
|
|
|
$storage_param = $function_signature_param;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if ($storage_param === null) {
|
2017-09-02 17:18:56 +02:00
|
|
|
continue;
|
2017-07-25 22:11:02 +02:00
|
|
|
}
|
|
|
|
|
2018-01-02 02:04:03 +01:00
|
|
|
$code_location = new CodeLocation(
|
2018-01-21 18:44:46 +01:00
|
|
|
$this->file_scanner,
|
2018-01-02 02:04:03 +01:00
|
|
|
$function,
|
|
|
|
null,
|
|
|
|
true,
|
|
|
|
CodeLocation::FUNCTION_PHPDOC_PARAM_TYPE,
|
|
|
|
$docblock_param['type']
|
|
|
|
);
|
|
|
|
|
|
|
|
$code_location->setCommentLine($docblock_param['line_number']);
|
|
|
|
|
2017-10-22 07:04:35 +02:00
|
|
|
try {
|
2018-05-20 23:19:53 +02:00
|
|
|
$new_param_type = Type::parseTokens(
|
2018-02-04 18:23:32 +01:00
|
|
|
Type::fixUpLocalType(
|
2017-10-22 07:04:35 +02:00
|
|
|
$docblock_param['type'],
|
|
|
|
$this->aliases,
|
2018-07-15 23:23:17 +02:00
|
|
|
$this->function_template_types + $this->class_template_types,
|
|
|
|
$this->type_aliases
|
2018-04-20 16:52:23 +02:00
|
|
|
),
|
|
|
|
false,
|
|
|
|
$this->function_template_types + $this->class_template_types
|
2017-10-22 07:04:35 +02:00
|
|
|
);
|
|
|
|
} catch (TypeParseTreeException $e) {
|
|
|
|
if (IssueBuffer::accepts(
|
|
|
|
new InvalidDocblock(
|
|
|
|
$e->getMessage() . ' in docblock for ' . $cased_method_id,
|
|
|
|
$code_location
|
|
|
|
)
|
|
|
|
)) {
|
2018-05-29 16:13:26 +02:00
|
|
|
$storage->has_visitor_issues = true;
|
2017-10-22 07:04:35 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
continue;
|
|
|
|
}
|
2017-07-25 22:11:02 +02:00
|
|
|
|
2018-04-14 19:41:25 +02:00
|
|
|
$new_param_type->setFromDocblock();
|
|
|
|
|
2017-07-27 22:12:16 +02:00
|
|
|
$new_param_type->queueClassLikesForScanning(
|
2018-01-21 19:38:51 +01:00
|
|
|
$this->codebase,
|
2018-02-19 06:27:39 +01:00
|
|
|
$this->file_storage,
|
2017-07-27 22:12:16 +02:00
|
|
|
$storage->template_types ?: []
|
|
|
|
);
|
2017-07-25 22:11:02 +02:00
|
|
|
|
|
|
|
if ($docblock_param_variadic) {
|
|
|
|
$new_param_type = new Type\Union([
|
|
|
|
new Type\Atomic\TArray([
|
|
|
|
Type::getInt(),
|
|
|
|
$new_param_type,
|
|
|
|
]),
|
|
|
|
]);
|
|
|
|
}
|
|
|
|
|
|
|
|
$existing_param_type_nullable = $storage_param->is_nullable;
|
|
|
|
|
2017-09-02 17:18:56 +02:00
|
|
|
if (!$storage_param->type || $storage_param->type->isMixed() || $storage->template_types) {
|
2017-07-25 22:11:02 +02:00
|
|
|
if ($existing_param_type_nullable && !$new_param_type->isNullable()) {
|
2018-01-09 21:05:48 +01:00
|
|
|
$new_param_type->addType(new Type\Atomic\TNull());
|
2017-07-25 22:11:02 +02:00
|
|
|
}
|
|
|
|
|
2018-06-07 18:23:10 +02:00
|
|
|
if ($this->config->add_param_default_to_docblock_type
|
|
|
|
&& $storage_param->default_type
|
|
|
|
&& !$storage_param->default_type->isMixed()
|
|
|
|
&& (!$storage_param->type || !$storage_param->type->isMixed())
|
|
|
|
) {
|
|
|
|
$new_param_type = Type::combineUnionTypes($new_param_type, $storage_param->default_type);
|
|
|
|
}
|
|
|
|
|
2017-07-25 22:11:02 +02:00
|
|
|
$storage_param->type = $new_param_type;
|
2017-12-30 03:28:21 +01:00
|
|
|
$storage_param->type_location = $code_location;
|
2017-07-25 22:11:02 +02:00
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
2018-02-01 05:27:25 +01:00
|
|
|
$storage_param_atomic_types = $storage_param->type->getTypes();
|
|
|
|
|
|
|
|
$all_types_match = true;
|
2018-02-05 22:57:33 +01:00
|
|
|
$all_typehint_types_match = true;
|
2018-02-01 05:27:25 +01:00
|
|
|
|
|
|
|
foreach ($new_param_type->getTypes() as $key => $type) {
|
|
|
|
if (isset($storage_param_atomic_types[$key])) {
|
|
|
|
if ($storage_param_atomic_types[$key]->getId() !== $type->getId()) {
|
|
|
|
$all_types_match = false;
|
|
|
|
}
|
|
|
|
|
|
|
|
$type->from_docblock = false;
|
|
|
|
} else {
|
|
|
|
$all_types_match = false;
|
2018-02-05 22:57:33 +01:00
|
|
|
$all_typehint_types_match = false;
|
2018-02-01 05:27:25 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if ($all_types_match) {
|
2017-07-25 22:11:02 +02:00
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
2018-02-05 22:57:33 +01:00
|
|
|
if ($all_typehint_types_match) {
|
|
|
|
$new_param_type->from_docblock = false;
|
|
|
|
}
|
|
|
|
|
2018-01-05 06:19:35 +01:00
|
|
|
if ($existing_param_type_nullable && !$new_param_type->isNullable()) {
|
2018-01-09 21:05:48 +01:00
|
|
|
$new_param_type->addType(new Type\Atomic\TNull());
|
2017-07-25 22:11:02 +02:00
|
|
|
}
|
|
|
|
|
2018-01-05 06:19:35 +01:00
|
|
|
$storage_param->type = $new_param_type;
|
2017-12-30 03:28:21 +01:00
|
|
|
$storage_param->type_location = $code_location;
|
2017-07-25 22:11:02 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @param PhpParser\Node\Stmt\Property $stmt
|
|
|
|
* @param Config $config
|
2018-05-10 20:12:50 +02:00
|
|
|
* @param string $fq_classlike_name
|
2017-07-25 22:11:02 +02:00
|
|
|
*
|
|
|
|
* @return void
|
|
|
|
*/
|
|
|
|
private function visitPropertyDeclaration(
|
|
|
|
PhpParser\Node\Stmt\Property $stmt,
|
2018-01-21 18:44:46 +01:00
|
|
|
Config $config,
|
2018-05-10 20:12:50 +02:00
|
|
|
ClassLikeStorage $storage,
|
|
|
|
$fq_classlike_name
|
2017-07-25 22:11:02 +02:00
|
|
|
) {
|
2017-08-14 21:46:01 +02:00
|
|
|
if (!$this->fq_classlike_names) {
|
|
|
|
throw new \LogicException('$this->fq_classlike_names should not be empty');
|
2017-07-25 22:11:02 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
$comment = $stmt->getDocComment();
|
|
|
|
$var_comment = null;
|
2017-07-29 21:05:06 +02:00
|
|
|
|
2017-12-27 12:27:59 +01:00
|
|
|
$property_is_initialized = false;
|
|
|
|
|
2018-01-21 18:44:46 +01:00
|
|
|
$existing_constants = $storage->protected_class_constants
|
|
|
|
+ $storage->private_class_constants
|
|
|
|
+ $storage->public_class_constants;
|
|
|
|
|
2018-01-13 06:32:20 +01:00
|
|
|
if ($comment && $comment->getText() && ($config->use_docblock_types || $config->use_docblock_property_types)) {
|
2017-12-27 12:27:59 +01:00
|
|
|
if (preg_match('/[ \t\*]+@psalm-suppress[ \t]+PropertyNotSetInConstructor/', (string)$comment)) {
|
|
|
|
$property_is_initialized = true;
|
|
|
|
}
|
|
|
|
|
2017-07-25 22:11:02 +02:00
|
|
|
try {
|
|
|
|
$property_type_line_number = $comment->getLine();
|
2018-02-08 05:33:31 +01:00
|
|
|
$var_comments = CommentChecker::getTypeFromComment(
|
2017-07-25 22:11:02 +02:00
|
|
|
$comment->getText(),
|
2018-01-21 18:44:46 +01:00
|
|
|
$this->file_scanner,
|
2017-07-25 22:11:02 +02:00
|
|
|
$this->aliases,
|
|
|
|
$this->function_template_types + $this->class_template_types,
|
2018-07-15 23:23:17 +02:00
|
|
|
$property_type_line_number,
|
|
|
|
null,
|
|
|
|
$this->type_aliases
|
2017-07-25 22:11:02 +02:00
|
|
|
);
|
2018-02-08 05:33:31 +01:00
|
|
|
|
|
|
|
$var_comment = array_pop($var_comments);
|
2017-11-15 03:43:31 +01:00
|
|
|
} catch (IncorrectDocblockException $e) {
|
|
|
|
if (IssueBuffer::accepts(
|
|
|
|
new MissingDocblockType(
|
|
|
|
$e->getMessage(),
|
2018-01-21 18:44:46 +01:00
|
|
|
new CodeLocation($this->file_scanner, $stmt, null, true)
|
2017-11-15 03:43:31 +01:00
|
|
|
)
|
|
|
|
)) {
|
2018-05-29 16:13:26 +02:00
|
|
|
$storage->has_visitor_issues = true;
|
2017-11-15 03:43:31 +01:00
|
|
|
}
|
2017-07-25 22:11:02 +02:00
|
|
|
} catch (DocblockParseException $e) {
|
|
|
|
if (IssueBuffer::accepts(
|
|
|
|
new InvalidDocblock(
|
2017-11-15 03:43:31 +01:00
|
|
|
$e->getMessage(),
|
2018-01-21 18:44:46 +01:00
|
|
|
new CodeLocation($this->file_scanner, $stmt, null, true)
|
2017-07-25 22:11:02 +02:00
|
|
|
)
|
|
|
|
)) {
|
2018-05-29 16:13:26 +02:00
|
|
|
$storage->has_visitor_issues = true;
|
2017-07-25 22:11:02 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-01-02 02:04:03 +01:00
|
|
|
$property_group_type = $var_comment ? $var_comment->type : null;
|
2017-07-25 22:11:02 +02:00
|
|
|
|
|
|
|
if ($property_group_type) {
|
2018-02-19 06:27:39 +01:00
|
|
|
$property_group_type->queueClassLikesForScanning($this->codebase, $this->file_storage);
|
2017-07-25 22:11:02 +02:00
|
|
|
$property_group_type->setFromDocblock();
|
|
|
|
}
|
|
|
|
|
|
|
|
foreach ($stmt->props as $property) {
|
|
|
|
$property_type_location = null;
|
|
|
|
$default_type = null;
|
|
|
|
|
|
|
|
if (!$property_group_type) {
|
|
|
|
if ($property->default) {
|
2018-05-10 20:12:50 +02:00
|
|
|
$default_type = StatementsChecker::getSimpleType(
|
2018-06-28 03:53:25 +02:00
|
|
|
$this->codebase,
|
2018-05-10 20:12:50 +02:00
|
|
|
$property->default,
|
2018-06-28 03:53:25 +02:00
|
|
|
$this->aliases,
|
|
|
|
null,
|
2018-05-10 20:12:50 +02:00
|
|
|
$existing_constants,
|
|
|
|
$fq_classlike_name
|
|
|
|
);
|
2017-07-25 22:11:02 +02:00
|
|
|
}
|
2018-01-13 06:32:20 +01:00
|
|
|
|
|
|
|
$property_type = false;
|
2017-07-25 22:11:02 +02:00
|
|
|
} else {
|
2018-01-02 02:04:03 +01:00
|
|
|
if ($var_comment && $var_comment->line_number) {
|
|
|
|
$property_type_location = new CodeLocation(
|
2018-01-21 18:44:46 +01:00
|
|
|
$this->file_scanner,
|
2018-01-02 02:04:03 +01:00
|
|
|
$stmt,
|
|
|
|
null,
|
|
|
|
false,
|
|
|
|
CodeLocation::VAR_TYPE,
|
|
|
|
$var_comment->original_type
|
|
|
|
);
|
|
|
|
$property_type_location->setCommentLine($var_comment->line_number);
|
2017-07-25 22:11:02 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
$property_type = count($stmt->props) === 1 ? $property_group_type : clone $property_group_type;
|
|
|
|
}
|
|
|
|
|
2018-04-17 18:16:25 +02:00
|
|
|
$property_storage = $storage->properties[$property->name->name] = new PropertyStorage();
|
2017-07-25 22:11:02 +02:00
|
|
|
$property_storage->is_static = (bool)$stmt->isStatic();
|
|
|
|
$property_storage->type = $property_type;
|
2018-04-17 18:16:25 +02:00
|
|
|
$property_storage->location = new CodeLocation($this->file_scanner, $property->name);
|
2017-07-25 22:11:02 +02:00
|
|
|
$property_storage->type_location = $property_type_location;
|
|
|
|
$property_storage->has_default = $property->default ? true : false;
|
|
|
|
$property_storage->suggested_type = $property_group_type ? null : $default_type;
|
|
|
|
$property_storage->deprecated = $var_comment ? $var_comment->deprecated : false;
|
|
|
|
|
|
|
|
if ($stmt->isPublic()) {
|
|
|
|
$property_storage->visibility = ClassLikeChecker::VISIBILITY_PUBLIC;
|
|
|
|
} elseif ($stmt->isProtected()) {
|
|
|
|
$property_storage->visibility = ClassLikeChecker::VISIBILITY_PROTECTED;
|
|
|
|
} elseif ($stmt->isPrivate()) {
|
|
|
|
$property_storage->visibility = ClassLikeChecker::VISIBILITY_PRIVATE;
|
|
|
|
}
|
|
|
|
|
2017-08-14 21:46:01 +02:00
|
|
|
$fq_classlike_name = $this->fq_classlike_names[count($this->fq_classlike_names) - 1];
|
|
|
|
|
2018-04-17 18:16:25 +02:00
|
|
|
$property_id = $fq_classlike_name . '::$' . $property->name->name;
|
2017-07-25 22:11:02 +02:00
|
|
|
|
2018-07-22 01:56:26 +02:00
|
|
|
$storage->declaring_property_ids[$property->name->name] = $fq_classlike_name;
|
2018-04-17 18:16:25 +02:00
|
|
|
$storage->appearing_property_ids[$property->name->name] = $property_id;
|
2017-07-25 22:11:02 +02:00
|
|
|
|
2017-12-27 12:27:59 +01:00
|
|
|
if ($property_is_initialized) {
|
2018-04-17 18:16:25 +02:00
|
|
|
$storage->initialized_properties[$property->name->name] = true;
|
2017-12-27 12:27:59 +01:00
|
|
|
}
|
|
|
|
|
2017-07-25 22:11:02 +02:00
|
|
|
if (!$stmt->isPrivate()) {
|
2018-04-17 18:16:25 +02:00
|
|
|
$storage->inheritable_property_ids[$property->name->name] = $property_id;
|
2017-07-25 22:11:02 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @param PhpParser\Node\Stmt\ClassConst $stmt
|
2018-05-10 20:12:50 +02:00
|
|
|
* @param string $fq_classlike_name
|
2017-07-25 22:11:02 +02:00
|
|
|
*
|
|
|
|
* @return void
|
|
|
|
*/
|
2018-05-10 20:12:50 +02:00
|
|
|
private function visitClassConstDeclaration(
|
|
|
|
PhpParser\Node\Stmt\ClassConst $stmt,
|
|
|
|
ClassLikeStorage $storage,
|
|
|
|
$fq_classlike_name
|
|
|
|
) {
|
2017-07-25 22:11:02 +02:00
|
|
|
$existing_constants = $storage->protected_class_constants
|
|
|
|
+ $storage->private_class_constants
|
|
|
|
+ $storage->public_class_constants;
|
|
|
|
|
|
|
|
foreach ($stmt->consts as $const) {
|
|
|
|
$const_type = StatementsChecker::getSimpleType(
|
2018-06-28 03:53:25 +02:00
|
|
|
$this->codebase,
|
2017-07-25 22:11:02 +02:00
|
|
|
$const->value,
|
2018-06-28 03:53:25 +02:00
|
|
|
$this->aliases,
|
|
|
|
null,
|
2018-05-10 20:12:50 +02:00
|
|
|
$existing_constants,
|
|
|
|
$fq_classlike_name
|
2018-06-28 03:53:25 +02:00
|
|
|
);
|
2017-07-25 22:11:02 +02:00
|
|
|
|
2018-06-28 03:53:25 +02:00
|
|
|
if ($const_type) {
|
|
|
|
$existing_constants[$const->name->name] = $const_type;
|
2017-07-25 22:11:02 +02:00
|
|
|
|
2018-06-28 03:53:25 +02:00
|
|
|
if ($stmt->isProtected()) {
|
|
|
|
$storage->protected_class_constants[$const->name->name] = $const_type;
|
|
|
|
} elseif ($stmt->isPrivate()) {
|
|
|
|
$storage->private_class_constants[$const->name->name] = $const_type;
|
|
|
|
} else {
|
|
|
|
$storage->public_class_constants[$const->name->name] = $const_type;
|
|
|
|
}
|
2017-07-25 22:11:02 +02:00
|
|
|
} else {
|
2018-06-28 03:53:25 +02:00
|
|
|
if ($stmt->isProtected()) {
|
|
|
|
$storage->protected_class_constant_nodes[$const->name->name] = $const->value;
|
|
|
|
} elseif ($stmt->isPrivate()) {
|
|
|
|
$storage->private_class_constant_nodes[$const->name->name] = $const->value;
|
|
|
|
} else {
|
|
|
|
$storage->public_class_constant_nodes[$const->name->name] = $const->value;
|
|
|
|
}
|
|
|
|
|
|
|
|
$storage->aliases = $this->aliases;
|
2017-07-25 22:11:02 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @param PhpParser\Node\Expr\Include_ $stmt
|
|
|
|
*
|
2018-03-07 17:16:56 +01:00
|
|
|
* @return void
|
2017-07-25 22:11:02 +02:00
|
|
|
*/
|
|
|
|
public function visitInclude(PhpParser\Node\Expr\Include_ $stmt)
|
|
|
|
{
|
|
|
|
$config = Config::getInstance();
|
|
|
|
|
|
|
|
if (!$config->allow_includes) {
|
|
|
|
throw new FileIncludeException(
|
|
|
|
'File includes are not allowed per your Psalm config - check the allowFileIncludes flag.'
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
if ($stmt->expr instanceof PhpParser\Node\Scalar\String_) {
|
|
|
|
$path_to_file = $stmt->expr->value;
|
|
|
|
|
|
|
|
// attempts to resolve using get_include_path dirs
|
2018-01-14 18:09:40 +01:00
|
|
|
$include_path = IncludeChecker::resolveIncludePath($path_to_file, dirname($this->file_path));
|
2017-07-25 22:11:02 +02:00
|
|
|
$path_to_file = $include_path ? $include_path : $path_to_file;
|
|
|
|
|
|
|
|
if ($path_to_file[0] !== DIRECTORY_SEPARATOR) {
|
|
|
|
$path_to_file = getcwd() . DIRECTORY_SEPARATOR . $path_to_file;
|
|
|
|
}
|
|
|
|
} else {
|
2018-01-14 18:09:40 +01:00
|
|
|
$path_to_file = IncludeChecker::getPathTo($stmt->expr, $this->file_path);
|
2017-07-25 22:11:02 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
if ($path_to_file) {
|
|
|
|
$reduce_pattern = '/\/[^\/]+\/\.\.\//';
|
|
|
|
|
|
|
|
while (preg_match($reduce_pattern, $path_to_file)) {
|
|
|
|
$path_to_file = preg_replace($reduce_pattern, DIRECTORY_SEPARATOR, $path_to_file);
|
|
|
|
}
|
|
|
|
|
|
|
|
if ($this->file_path === $path_to_file) {
|
2018-03-07 17:16:56 +01:00
|
|
|
return;
|
2017-07-25 22:11:02 +02:00
|
|
|
}
|
|
|
|
|
2018-01-21 19:38:51 +01:00
|
|
|
if ($this->codebase->fileExists($path_to_file)) {
|
2018-05-23 05:38:27 +02:00
|
|
|
if ($this->scan_deep) {
|
|
|
|
$this->codebase->scanner->addFileToDeepScan($path_to_file);
|
|
|
|
} else {
|
|
|
|
$this->codebase->scanner->addFileToShallowScan($path_to_file);
|
|
|
|
}
|
2017-07-25 22:11:02 +02:00
|
|
|
|
2018-05-30 18:23:53 +02:00
|
|
|
$this->file_storage->required_file_paths[strtolower($path_to_file)] = $path_to_file;
|
2017-07-25 22:11:02 +02:00
|
|
|
|
2018-03-07 17:16:56 +01:00
|
|
|
return;
|
2017-07-25 22:11:02 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-03-07 17:16:56 +01:00
|
|
|
return;
|
2017-07-25 22:11:02 +02:00
|
|
|
}
|
2018-02-23 21:39:33 +01:00
|
|
|
|
|
|
|
/**
|
|
|
|
* @return string
|
|
|
|
*/
|
|
|
|
public function getFilePath()
|
|
|
|
{
|
|
|
|
return $this->file_path;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @return string
|
|
|
|
*/
|
|
|
|
public function getFileName()
|
|
|
|
{
|
|
|
|
return $this->file_scanner->getFileName();
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @return string
|
|
|
|
*/
|
2018-05-30 22:19:18 +02:00
|
|
|
public function getRootFilePath()
|
2018-02-23 21:39:33 +01:00
|
|
|
{
|
2018-05-30 22:19:18 +02:00
|
|
|
return $this->file_scanner->getRootFilePath();
|
2018-02-23 21:39:33 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @return string
|
|
|
|
*/
|
2018-05-30 22:19:18 +02:00
|
|
|
public function getRootFileName()
|
2018-02-23 21:39:33 +01:00
|
|
|
{
|
2018-05-30 22:19:18 +02:00
|
|
|
return $this->file_scanner->getRootFileName();
|
2018-02-23 21:39:33 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @return Aliases
|
|
|
|
*/
|
|
|
|
public function getAliases()
|
|
|
|
{
|
|
|
|
return $this->aliases;
|
|
|
|
}
|
2017-07-25 22:11:02 +02:00
|
|
|
}
|