Merge pull request #2768 from rectorphp/node-type-resolver

NodeTypeResolver improvements
This commit is contained in:
Tomas Votruba 2020-01-28 01:13:27 +01:00 committed by GitHub
commit 967e7768cb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 239 additions and 161 deletions

View File

@ -9,6 +9,7 @@
"symfony/process": "^4.4|^5.0",
"symfony/filesystem": "^4.4|^5.0",
"symfony/finder": "^4.4|^5.0",
"symplify/package-builder": "^7.2",
"nette/utils": "^3.0"
},
"autoload": {

View File

@ -6,11 +6,15 @@ namespace Rector\Compiler\Console;
use Nette\Utils\FileSystem as NetteFileSystem;
use Nette\Utils\Json;
use Rector\Console\Style\SymfonyStyleFactory;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
use Symfony\Component\Filesystem\Filesystem;
use Symfony\Component\Process\Process;
use Symplify\PackageBuilder\Console\ShellCode;
use Symplify\PackageBuilder\Reflection\PrivatesCaller;
/**
* Inspired by @see https://github.com/phpstan/phpstan-src/blob/f939d23155627b5c2ec6eef36d976dddea22c0c5/compiler/src/Console/CompileCommand.php
@ -37,12 +41,20 @@ final class CompileCommand extends Command
*/
private $originalComposerJsonFileContent;
/**
* @var SymfonyStyle
*/
private $symfonyStyle;
public function __construct(string $dataDir, string $buildDir)
{
parent::__construct();
$this->filesystem = new Filesystem();
$this->dataDir = $dataDir;
$this->buildDir = $buildDir;
$symfonyStyleFactory = new SymfonyStyleFactory(new PrivatesCaller());
$this->symfonyStyle = $symfonyStyleFactory->create();
}
protected function configure(): void
@ -55,6 +67,8 @@ final class CompileCommand extends Command
{
$composerJsonFile = $this->buildDir . '/composer.json';
$this->symfonyStyle->note('Loading ' . $composerJsonFile);
$this->fixComposerJson($composerJsonFile);
// @see https://github.com/dotherightthing/wpdtrt-plugin-boilerplate/issues/52
@ -79,7 +93,7 @@ final class CompileCommand extends Command
$this->restoreComposerJson($composerJsonFile);
return 0;
return ShellCode::SUCCESS;
}
private function fixComposerJson(string $composerJsonFile): void

View File

@ -1,14 +0,0 @@
<?php
declare(strict_types=1);
namespace Rector\NodeTypeResolver\Contract\Metadata;
use PhpParser\Node;
interface NodeDecoratorInterface
{
public function reset(): void;
public function decorateNode(Node $node): void;
}

View File

@ -0,0 +1,113 @@
<?php
declare(strict_types=1);
namespace Rector\NodeTypeResolver\NodeTypeCorrector;
use PhpParser\Node;
use PhpParser\Node\Arg;
use PhpParser\Node\Expr\FuncCall;
use PhpParser\Node\Expr\Variable;
use PHPStan\Type\ArrayType;
use PHPStan\Type\MixedType;
use PHPStan\Type\Type;
use Rector\NodeTypeResolver\Node\AttributeKey;
use Rector\PhpParser\Node\BetterNodeFinder;
use Rector\PhpParser\Node\Resolver\NameResolver;
use Rector\PhpParser\Printer\BetterStandardPrinter;
final class PregMatchTypeCorrector
{
/**
* @var BetterNodeFinder
*/
private $betterNodeFinder;
/**
* @var NameResolver
*/
private $nameResolver;
/**
* @var BetterStandardPrinter
*/
private $betterStandardPrinter;
public function __construct(
BetterNodeFinder $betterNodeFinder,
NameResolver $nameResolver,
BetterStandardPrinter $betterStandardPrinter
) {
$this->betterNodeFinder = $betterNodeFinder;
$this->nameResolver = $nameResolver;
$this->betterStandardPrinter = $betterStandardPrinter;
}
/**
* Special case for "preg_match(), preg_match_all()" - with 3rd argument
* @covers https://github.com/rectorphp/rector/issues/786
*/
public function correct(Node $node, Type $originalType): Type
{
if (! $node instanceof Variable) {
return $originalType;
}
if ($originalType instanceof ArrayType) {
return $originalType;
}
foreach ($this->getVariableUsages($node) as $usage) {
$possiblyArg = $usage->getAttribute(AttributeKey::PARENT_NODE);
if (! $possiblyArg instanceof Arg) {
continue;
}
$funcCallNode = $possiblyArg->getAttribute(AttributeKey::PARENT_NODE);
if (! $funcCallNode instanceof FuncCall) {
continue;
}
if (! $this->nameResolver->isNames($funcCallNode, ['preg_match', 'preg_match_all'])) {
continue;
}
if (! isset($funcCallNode->args[2])) {
continue;
}
// are the same variables
if (! $this->betterStandardPrinter->areNodesEqual($funcCallNode->args[2]->value, $node)) {
continue;
}
return new ArrayType(new MixedType(), new MixedType());
}
return $originalType;
}
/**
* @return Node[]
*/
private function getVariableUsages(Variable $variable): array
{
$scope = $this->getScopeNode($variable);
if ($scope === null) {
return [];
}
return $this->betterNodeFinder->find((array) $scope->stmts, function (Node $node) use ($variable): bool {
return $node instanceof Variable && $node->name === $variable->name;
});
}
private function getScopeNode(Node $node): ?Node
{
return $node->getAttribute(AttributeKey::METHOD_NODE)
?? $node->getAttribute(AttributeKey::FUNCTION_NODE)
?? $node->getAttribute(AttributeKey::NAMESPACE_NODE);
}
}

View File

@ -10,7 +10,6 @@ use PhpParser\Node;
use PhpParser\Node\Arg;
use PhpParser\Node\Expr;
use PhpParser\Node\Expr\Array_;
use PhpParser\Node\Expr\FuncCall;
use PhpParser\Node\Expr\MethodCall;
use PhpParser\Node\Expr\New_;
use PhpParser\Node\Expr\PropertyFetch;
@ -27,7 +26,6 @@ use PhpParser\Node\Stmt\Interface_;
use PhpParser\Node\Stmt\Nop;
use PhpParser\Node\Stmt\PropertyProperty;
use PhpParser\Node\Stmt\Trait_;
use PhpParser\NodeTraverser;
use PHPStan\Analyser\Scope;
use PHPStan\PhpDocParser\Ast\Type\TypeNode;
use PHPStan\Reflection\ReflectionProvider;
@ -56,12 +54,10 @@ use Rector\Exception\ShouldNotHappenException;
use Rector\NodeContainer\ParsedNodesByType;
use Rector\NodeTypeResolver\Contract\PerNodeTypeResolver\PerNodeTypeResolverInterface;
use Rector\NodeTypeResolver\Node\AttributeKey;
use Rector\NodeTypeResolver\NodeTypeCorrector\PregMatchTypeCorrector;
use Rector\NodeTypeResolver\PHPStan\Type\TypeFactory;
use Rector\NodeTypeResolver\Reflection\ClassReflectionTypesResolver;
use Rector\PhpParser\Node\BetterNodeFinder;
use Rector\PhpParser\Node\Resolver\NameResolver;
use Rector\PhpParser\NodeTraverser\CallableNodeTraverser;
use Rector\PhpParser\Printer\BetterStandardPrinter;
use Rector\TypeDeclaration\PHPStan\Type\ObjectTypeSpecifier;
use ReflectionProperty;
use Symfony\Component\Finder\SplFileInfo;
@ -78,16 +74,6 @@ final class NodeTypeResolver
*/
private $nameResolver;
/**
* @var BetterStandardPrinter
*/
private $betterStandardPrinter;
/**
* @var CallableNodeTraverser
*/
private $callableNodeTraverser;
/**
* @var ClassReflectionTypesResolver
*/
@ -108,11 +94,6 @@ final class NodeTypeResolver
*/
private $objectTypeSpecifier;
/**
* @var BetterNodeFinder
*/
private $betterNodeFinder;
/**
* @var ParsedNodesByType
*/
@ -128,39 +109,40 @@ final class NodeTypeResolver
*/
private $staticTypeMapper;
/**
* @var PregMatchTypeCorrector
*/
private $pregMatchTypeCorrector;
/**
* @param PerNodeTypeResolverInterface[] $perNodeTypeResolvers
*/
public function __construct(
BetterStandardPrinter $betterStandardPrinter,
BetterPhpDocParser $betterPhpDocParser,
NameResolver $nameResolver,
ParsedNodesByType $parsedNodesByType,
CallableNodeTraverser $callableNodeTraverser,
ClassReflectionTypesResolver $classReflectionTypesResolver,
ReflectionProvider $reflectionProvider,
TypeFactory $typeFactory,
StaticTypeMapper $staticTypeMapper,
ObjectTypeSpecifier $objectTypeSpecifier,
BetterNodeFinder $betterNodeFinder,
PregMatchTypeCorrector $pregMatchTypeCorrector,
array $perNodeTypeResolvers
) {
$this->betterStandardPrinter = $betterStandardPrinter;
$this->nameResolver = $nameResolver;
foreach ($perNodeTypeResolvers as $perNodeTypeResolver) {
$this->addPerNodeTypeResolver($perNodeTypeResolver);
}
$this->callableNodeTraverser = $callableNodeTraverser;
$this->classReflectionTypesResolver = $classReflectionTypesResolver;
$this->reflectionProvider = $reflectionProvider;
$this->typeFactory = $typeFactory;
$this->objectTypeSpecifier = $objectTypeSpecifier;
$this->betterNodeFinder = $betterNodeFinder;
$this->parsedNodesByType = $parsedNodesByType;
$this->betterPhpDocParser = $betterPhpDocParser;
$this->staticTypeMapper = $staticTypeMapper;
$this->pregMatchTypeCorrector = $pregMatchTypeCorrector;
}
/**
@ -260,7 +242,7 @@ final class NodeTypeResolver
{
$nodeType = $this->getStaticType($node);
$nodeType = $this->correctPregMatchType($node, $nodeType);
$nodeType = $this->pregMatchTypeCorrector->correct($node, $nodeType);
if ($nodeType instanceof ObjectType) {
if (is_a($nodeType->getClassName(), Countable::class, true)) {
return true;
@ -270,6 +252,7 @@ final class NodeTypeResolver
if (is_a($nodeType->getClassName(), 'SimpleXMLElement', true)) {
return true;
}
return is_a($nodeType->getClassName(), 'ResourceBundle', true);
}
@ -280,7 +263,7 @@ final class NodeTypeResolver
{
$nodeStaticType = $this->getStaticType($node);
$nodeStaticType = $this->correctPregMatchType($node, $nodeStaticType);
$nodeStaticType = $this->pregMatchTypeCorrector->correct($node, $nodeStaticType);
if ($this->isIntersectionArrayType($nodeStaticType)) {
return true;
}
@ -312,10 +295,7 @@ final class NodeTypeResolver
}
if ($node instanceof Param) {
$paramStaticType = $this->resolveParamStaticType($node);
if ($paramStaticType !== null) {
return $paramStaticType;
}
return $this->resolve($node);
}
/** @var Scope|null $nodeScope */
@ -553,71 +533,6 @@ final class NodeTypeResolver
return $this->typeFactory->createObjectTypeOrUnionType($allTypes);
}
/**
* Special case for "preg_match(), preg_match_all()" - with 3rd argument
* @covers https://github.com/rectorphp/rector/issues/786
*/
private function correctPregMatchType(Node $node, Type $originalType): Type
{
if (! $node instanceof Variable) {
return $originalType;
}
if ($originalType instanceof ArrayType) {
return $originalType;
}
foreach ($this->getVariableUsages($node) as $usage) {
$possiblyArg = $usage->getAttribute(AttributeKey::PARENT_NODE);
if (! $possiblyArg instanceof Arg) {
continue;
}
$funcCallNode = $possiblyArg->getAttribute(AttributeKey::PARENT_NODE);
if (! $funcCallNode instanceof FuncCall) {
continue;
}
if (! $this->nameResolver->isNames($funcCallNode, ['preg_match', 'preg_match_all'])) {
continue;
}
if (! isset($funcCallNode->args[2])) {
continue;
}
// are the same variables
if (! $this->betterStandardPrinter->areNodesEqual($funcCallNode->args[2]->value, $node)) {
continue;
}
return new ArrayType(new MixedType(), new MixedType());
}
return $originalType;
}
private function getScopeNode(Node $node): ?Node
{
return $node->getAttribute(AttributeKey::METHOD_NODE)
?? $node->getAttribute(AttributeKey::FUNCTION_NODE)
?? $node->getAttribute(AttributeKey::NAMESPACE_NODE);
}
/**
* @return Node[]
*/
private function getVariableUsages(Variable $variable): array
{
$scope = $this->getScopeNode($variable);
if ($scope === null) {
return [];
}
return $this->betterNodeFinder->find((array) $scope->stmts, function (Node $node) use ($variable): bool {
return $node instanceof Variable && $node->name === $variable->name;
});
}
private function isIntersectionArrayType(Type $nodeType): bool
{
if (! $nodeType instanceof IntersectionType) {
@ -670,37 +585,6 @@ final class NodeTypeResolver
return $propertyPropertyNode->default instanceof Array_;
}
private function resolveParamStaticType(Param $param): Type
{
$classMethod = $param->getAttribute(AttributeKey::METHOD_NODE);
if ($classMethod === null) {
return new MixedType();
}
/** @var string $paramName */
$paramName = $this->nameResolver->getName($param);
$paramStaticType = new MixedType();
// special case for param inside method/function
$this->callableNodeTraverser->traverseNodesWithCallable(
(array) $classMethod->stmts,
function (Node $node) use ($paramName, &$paramStaticType): ?int {
if (! $node instanceof Variable) {
return null;
}
if (! $this->nameResolver->isName($node, $paramName)) {
return null;
}
$paramStaticType = $this->getStaticType($node);
return NodeTraverser::STOP_TRAVERSAL;
}
);
return $paramStaticType;
}
private function isAnonymousClass(Node $node): bool
{
if (! $node instanceof Class_) {

View File

@ -5,14 +5,20 @@ declare(strict_types=1);
namespace Rector\NodeTypeResolver\PerNodeTypeResolver;
use PhpParser\Node;
use PhpParser\Node\Expr\Variable;
use PhpParser\Node\FunctionLike;
use PhpParser\Node\Identifier;
use PhpParser\Node\Param;
use PhpParser\NodeTraverser;
use PHPStan\Type\MixedType;
use PHPStan\Type\ObjectType;
use PHPStan\Type\Type;
use Rector\NodeTypeResolver\Contract\PerNodeTypeResolver\PerNodeTypeResolverInterface;
use Rector\NodeTypeResolver\Node\AttributeKey;
use Rector\NodeTypeResolver\NodeTypeResolver;
use Rector\NodeTypeResolver\PhpDoc\NodeAnalyzer\DocBlockManipulator;
use Rector\PhpParser\Node\Resolver\NameResolver;
use Rector\PhpParser\NodeTraverser\CallableNodeTraverser;
/**
* @see \Rector\NodeTypeResolver\Tests\PerNodeTypeResolver\ParamTypeResolver\ParamTypeResolverTest
@ -29,10 +35,32 @@ final class ParamTypeResolver implements PerNodeTypeResolverInterface
*/
private $docBlockManipulator;
public function __construct(NameResolver $nameResolver, DocBlockManipulator $docBlockManipulator)
{
/**
* @var CallableNodeTraverser
*/
private $callableNodeTraverser;
/**
* @var NodeTypeResolver
*/
private $nodeTypeResolver;
public function __construct(
NameResolver $nameResolver,
DocBlockManipulator $docBlockManipulator,
CallableNodeTraverser $callableNodeTraverser
) {
$this->nameResolver = $nameResolver;
$this->docBlockManipulator = $docBlockManipulator;
$this->callableNodeTraverser = $callableNodeTraverser;
}
/**
* @required
*/
public function autowirePropertyTypeResolver(NodeTypeResolver $nodeTypeResolver): void
{
$this->nodeTypeResolver = $nodeTypeResolver;
}
/**
@ -44,29 +72,76 @@ final class ParamTypeResolver implements PerNodeTypeResolverInterface
}
/**
* @param Param $paramNode
* @param Param $node
*/
public function resolve(Node $paramNode): Type
public function resolve(Node $node): Type
{
if ($paramNode->type !== null) {
$resolveTypeName = $this->nameResolver->getName($paramNode->type);
$paramType = $this->resolveFromType($node);
if (! $paramType instanceof MixedType) {
return $paramType;
}
$firstVariableUseType = $this->resolveFromFirstVariableUse($node);
if (! $firstVariableUseType instanceof MixedType) {
return $firstVariableUseType;
}
return $this->resolveFromFunctionDocBlock($node);
}
private function resolveFromFirstVariableUse(Param $param): Type
{
$classMethod = $param->getAttribute(AttributeKey::METHOD_NODE);
if ($classMethod === null) {
return new MixedType();
}
/** @var string $paramName */
$paramName = $this->nameResolver->getName($param);
$paramStaticType = new MixedType();
// special case for param inside method/function
$this->callableNodeTraverser->traverseNodesWithCallable(
(array) $classMethod->stmts,
function (Node $node) use ($paramName, &$paramStaticType): ?int {
if (! $node instanceof Variable) {
return null;
}
if (! $this->nameResolver->isName($node, $paramName)) {
return null;
}
$paramStaticType = $this->nodeTypeResolver->resolve($node);
return NodeTraverser::STOP_TRAVERSAL;
}
);
return $paramStaticType;
}
private function resolveFromFunctionDocBlock(Param $param): Type
{
/** @var FunctionLike $parentNode */
$parentNode = $param->getAttribute(AttributeKey::PARENT_NODE);
/** @var string $paramName */
$paramName = $this->nameResolver->getName($param);
return $this->docBlockManipulator->getParamTypeByName($parentNode, '$' . $paramName);
}
private function resolveFromType(Node $node)
{
if ($node->type !== null && ! $node->type instanceof Identifier) {
$resolveTypeName = $this->nameResolver->getName($node->type);
if ($resolveTypeName) {
// @todo map the other way every type :)
return new ObjectType($resolveTypeName);
}
}
/** @var FunctionLike $parentNode */
$parentNode = $paramNode->getAttribute(AttributeKey::PARENT_NODE);
return $this->resolveTypesFromFunctionDocBlock($paramNode, $parentNode);
}
private function resolveTypesFromFunctionDocBlock(Param $param, FunctionLike $functionLike): Type
{
/** @var string $paramName */
$paramName = $this->nameResolver->getName($param);
return $this->docBlockManipulator->getParamTypeByName($functionLike, '$' . $paramName);
return new MixedType();
}
}

View File

@ -62,6 +62,11 @@ final class RectorsFinder
$rector = $reflectionClass->newInstanceWithoutConstructor();
if (! $rector instanceof RectorInterface) {
// lowercase letter bug in RototLoader
if (Strings::endsWith($class, 'rector')) {
continue;
}
throw new ShouldNotHappenException(sprintf(
'"%s" found something that looks like Rector but does not implements "%s" interface.',
__METHOD__,