src/Phan/Analysis/ScopeVisitor.php
<?php
declare(strict_types=1);
namespace Phan\Analysis;
use AssertionError;
use ast;
use ast\Node;
use Phan\AST\AnalysisVisitor;
use Phan\Config;
use Phan\Issue;
use Phan\Language\Context;
use Phan\Language\FQSEN;
use Phan\Language\FQSEN\FullyQualifiedClassName;
use Phan\Language\FQSEN\FullyQualifiedFunctionName;
use Phan\Language\FQSEN\FullyQualifiedGlobalConstantName;
use Phan\Language\FQSEN\FullyQualifiedGlobalStructuralElement;
use function implode;
use function rtrim;
/**
* An abstract visitor with methods to track elements in the current scope.
*
* This tracks the current namespace and adds namespace and `use` information to the current scope.
*
* @phan-file-suppress PhanPartialTypeMismatchArgument
* @phan-file-suppress PhanPartialTypeMismatchArgumentInternal
*/
abstract class ScopeVisitor extends AnalysisVisitor
{
/**
* @param CodeBase $code_base
* The global code base holding all state
*
* @param Context $context
* The context of the parser at the node for which we'd
* like to determine a type
*/
/*
public function __construct(
CodeBase $code_base,
Context $context
) {
parent::__construct($code_base, $context);
}
*/
/**
* Default visitor for node kinds that do not have
* an overriding method
*
* @param Node $node @phan-unused-param
* A node to parse
*
* @return Context
* A new or an unchanged context resulting from
* parsing the node
*/
public function visit(Node $node): Context
{
// Many nodes don't change the context and we
// don't need to read them.
return $this->context;
}
/**
* Visit a node with kind `\ast\AST_DECLARE`
*
* @param Node $node
* A node to parse
*
* @return Context
* A new or an unchanged context resulting from
* parsing the node
*/
public function visitDeclare(Node $node): Context
{
$declares = $node->children['declares'];
$context = $this->context;
foreach ($declares->children as $elem) {
if (!$elem instanceof Node) {
throw new AssertionError('Expected an array of declaration elements');
}
['name' => $name, 'value' => $value] = $elem->children;
if ('strict_types' === $name && \is_int($value)) {
$context = $context->withStrictTypes($value);
}
}
return $context;
}
/**
* Visit a node with kind `\ast\AST_NAMESPACE`
*
* @param Node $node
* A node to parse
*
* @return Context
* A new context resulting from parsing the node
*/
public function visitNamespace(Node $node): Context
{
$namespace = '\\' . (string)$node->children['name'];
return $this->context->withNamespace($namespace);
}
/**
* Visit a node with kind `\ast\AST_GROUP_USE`
* such as `use \ast\Node;`.
*
* @param Node $node
* A node to parse
*
* @return Context
* A new or an unchanged context resulting from
* parsing the node
*/
public function visitGroupUse(Node $node): Context
{
$children = $node->children;
$prefix = \array_shift($children);
$context = $this->context;
$alias_target_map = self::aliasTargetMapFromUseNode(
$children['uses'], // @phan-suppress-current-line PhanTypeMismatchArgumentNullable the key is also used by AST_CLOSURE
$prefix,
$node->flags ?? 0
);
foreach ($alias_target_map as $alias => [$flags, $target, $lineno]) {
$context = $context->withNamespaceMap(
$flags,
$alias,
$target,
$lineno,
$this->code_base
);
}
return $context;
}
/**
* Visit a node with kind `\ast\AST_USE`
* such as `use \ast\Node;`.
*
* @param Node $node
* A node to parse
*
* @return Context
* A new or an unchanged context resulting from
* parsing the node
*/
public function visitUse(Node $node): Context
{
$context = $this->context;
$minimum_target_php_version = Config::get_closest_minimum_target_php_version_id();
foreach (self::aliasTargetMapFromUseNode($node) as $alias => [$flags, $target, $lineno]) {
$flags = $node->flags ?: $flags;
if ($flags === \ast\flags\USE_NORMAL && $minimum_target_php_version < 70200) {
self::analyzeUseElemCompatibility($alias, $target, $minimum_target_php_version, $lineno);
}
if (\strcasecmp($target->getNamespace(), $context->getNamespace()) === 0) {
$this->maybeWarnSameNamespaceUse($alias, $target, $flags, $lineno);
}
$context = $context->withNamespaceMap(
$flags,
$alias,
$target,
$lineno,
$this->code_base
);
}
return $context;
}
private function maybeWarnSameNamespaceUse(string $alias, FullyQualifiedGlobalStructuralElement $target, int $flags, int $lineno): void
{
if (\strcasecmp($alias, $target->getName()) !== 0) {
return;
}
if ($flags === ast\flags\USE_FUNCTION) {
if ($target->getNamespace() !== '\\') {
return;
}
$issue_type = Issue::UseFunctionNoEffect;
} elseif ($flags === ast\flags\USE_CONST) {
if ($target->getNamespace() !== '\\') {
return;
}
$issue_type = Issue::UseConstantNoEffect;
} else {
if ($target->getNamespace() !== '\\') {
if (!Config::getValue('warn_about_relative_include_statement')) {
return;
}
$issue_type = Issue::UseNormalNamespacedNoEffect;
} else {
$issue_type = Issue::UseNormalNoEffect;
}
}
$this->emitIssue(
$issue_type,
$lineno,
$target
);
}
private const USE_ERRORS = [
'iterable' => Issue::CompatibleUseIterablePHP71,
'object' => Issue::CompatibleUseObjectPHP71,
'mixed' => Issue::CompatibleUseMixed,
];
private function analyzeUseElemCompatibility(
string $alias,
FQSEN $target,
int $minimum_target_php_version,
int $lineno
): void {
$alias_lower = \strtolower($alias);
if ($minimum_target_php_version < 70100) {
if ($alias_lower === 'void') {
Issue::maybeEmit(
$this->code_base,
$this->context,
Issue::CompatibleUseVoidPHP70,
$lineno,
$target
);
return;
}
}
$issue_name = self::USE_ERRORS[$alias_lower] ?? null;
if ($issue_name) {
Issue::maybeEmit(
$this->code_base,
$this->context,
$issue_name,
$lineno,
$target
);
}
}
/**
* @param Node $node
* The node with the use statement
*
* @param int $flags
* An optional node flag specifying the type
* of the use clause.
*
* @return array<string,array{0:int,1:FullyQualifiedGlobalStructuralElement,2:int}>
* A map from alias to target
*
* @suppress PhanPartialTypeMismatchReturn TODO: investigate
* @suppress PhanThrowTypeAbsentForCall
*/
public static function aliasTargetMapFromUseNode(
Node $node,
string $prefix = '',
int $flags = 0
): array {
if ($node->kind !== \ast\AST_USE) {
throw new AssertionError('Method takes AST_USE nodes');
}
$map = [];
foreach ($node->children as $child_node) {
if (!$child_node instanceof Node) {
throw new AssertionError('Expected array of AST_USE_ELEM nodes');
}
$target = $child_node->children['name'];
if (isset($child_node->children['alias'])) {
$alias = $child_node->children['alias'];
} else {
if (($pos = \strrpos($target, '\\')) !== false) {
$alias = \substr($target, $pos + 1);
} else {
$alias = $target;
}
}
if (!\is_string($alias)) {
// Should be impossible
continue;
}
// if AST_USE does not have any flags set, then its AST_USE_ELEM
// children will (this will be for AST_GROUP_USE)
// The 'use' type can be defined on the `AST_GROUP_USE` node, the
// `AST_USE_ELEM` or on the child element.
$use_flag = $flags ?: $node->flags ?: $child_node->flags;
if ($use_flag === \ast\flags\USE_FUNCTION) {
$parts = \explode('\\', $target);
$function_name = \array_pop($parts);
$target = FullyQualifiedFunctionName::make(
rtrim($prefix, '\\') . '\\' . implode('\\', $parts),
$function_name
);
} elseif ($use_flag === \ast\flags\USE_CONST) {
$parts = \explode('\\', $target);
$name = \array_pop($parts);
$target = FullyQualifiedGlobalConstantName::make(
rtrim($prefix, '\\') . '\\' . implode('\\', $parts),
$name
);
} elseif ($use_flag === \ast\flags\USE_NORMAL) {
$target = FullyQualifiedClassName::fromFullyQualifiedString(
rtrim($prefix, '\\') . '\\' . $target
);
} else {
// If we get to this spot and don't know what
// kind of a use clause we're dealing with, its
// likely that this is a `USE` node which is
// a child of a `GROUP_USE` and we already
// handled it when analyzing the parent
// node.
continue;
}
$map[$alias] = [$use_flag, $target, $child_node->lineno];
}
return $map;
}
}