use simple NodeTypeResolver according to NodeVisitor example

This commit is contained in:
TomasVotruba 2017-08-20 00:51:00 +02:00
parent 5a0ce3b7dc
commit 738ac001f9
24 changed files with 334 additions and 359 deletions

View File

@ -17,7 +17,8 @@
"require-dev": {
"phpunit/phpunit": "^6.2",
"tracy/tracy": "^2.4",
"symplify/easy-coding-standard": "^2.2"
"symplify/easy-coding-standard": "^2.2",
"slam/php-cs-fixer-extensions": "^1.4"
},
"autoload": {
"psr-4": {
@ -27,7 +28,8 @@
},
"autoload-dev": {
"psr-4": {
"Rector\\Tests\\": "tests"
"Rector\\Tests\\": "tests",
"Rector\\NodeTypeResolver\\Tests\\": "packages/NodeTypeResolver/tests"
}
},
"scripts": {

View File

@ -20,6 +20,9 @@ checkers:
PHP_CodeSniffer\Standards\Generic\Sniffs\Metrics\NestingLevelSniff:
absoluteNestingLevel: 3
# Class should be Final or Abstract
- SlamCsFixer\FinalInternalClassFixer
parameters:
exclude_checkers:
# Excluded from symfony-checkers.neon

View File

@ -1,95 +0,0 @@
<?php declare(strict_types=1);
namespace Rector\NodeTypeResolver\Broker;
use PHPStan\Broker\Broker;
use PHPStan\Broker\BrokerFactory as OriginalBrokerFactory;
use PHPStan\Reflection\Annotations\AnnotationsMethodsClassReflectionExtension;
use PHPStan\Reflection\Annotations\AnnotationsPropertiesClassReflectionExtension;
use PHPStan\Reflection\Php\PhpClassReflectionExtension;
use PHPStan\Reflection\PhpDefect\PhpDefectClassReflectionExtension;
use PHPStan\Type\FileTypeMapper;
use Rector\NodeTypeResolver\Reflection\FunctionReflectionFactory;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Only mimics original @see \PHPStan\Broker\BrokerFactory with Symfony DI Container.
*/
final class BrokerFactory
{
/**
* @var ContainerInterface|ContainerBuilder
*/
private $container;
/**
* @var FunctionReflectionFactory
*/
private $functionReflectionFactory;
/**
* @var FileTypeMapper
*/
private $fileTypeMapper;
public function __construct(
ContainerInterface $container,
FunctionReflectionFactory $functionReflectionFactory,
FileTypeMapper $fileTypeMapper
) {
$this->container = $container;
$this->functionReflectionFactory = $functionReflectionFactory;
$this->fileTypeMapper = $fileTypeMapper;
}
public function create(): Broker
{
$tagToService = function (array $tags) {
return array_map(function (string $serviceName) {
return $this->container->get($serviceName);
}, array_keys($tags));
};
// @todo: require in ctor
$phpClassReflectionExtension = $this->container->get(PhpClassReflectionExtension::class);
$annotationsPropertiesClassReflectionExtension = $this->container->get(AnnotationsPropertiesClassReflectionExtension::class);
$annotationsMethodsClassReflectionExtension = $this->container->get(AnnotationsMethodsClassReflectionExtension::class);
$phpDefectClassReflectionExtension = $this->container->get(PhpDefectClassReflectionExtension::class);
dump('EEA');
die;
dump($tagToService($this->container->findTaggedServiceIds(OriginalBrokerFactory::PROPERTIES_CLASS_REFLECTION_EXTENSION_TAG)));
die;
$propertiesClassReflectionExtensions = array_merge(
[$phpClassReflectionExtension, $phpDefectClassReflectionExtension],
$tagToService($this->container->findTaggedServiceIds(OriginalBrokerFactory::PROPERTIES_CLASS_REFLECTION_EXTENSION_TAG)),
[$annotationsPropertiesClassReflectionExtension]
);
$methodsClassReflectionExtensions = array_merge(
[$phpClassReflectionExtension],
$tagToService($this->container->findTaggedServiceIds(OriginalBrokerFactory::METHODS_CLASS_REFLECTION_EXTENSION_TAG)),
[$annotationsMethodsClassReflectionExtension]
);
$dynamicMethodReturnTypeExtensions = $tagToService(
$this->container->findTaggedServiceIds(OriginalBrokerFactory::DYNAMIC_METHOD_RETURN_TYPE_EXTENSION_TAG));
$dynamicStaticMethodReturnTypeExtensions = $tagToService($this->container->findTaggedServiceIds(OriginalBrokerFactory::DYNAMIC_STATIC_METHOD_RETURN_TYPE_EXTENSION_TAG)
);
return new Broker(
$propertiesClassReflectionExtensions,
$methodsClassReflectionExtensions,
$dynamicMethodReturnTypeExtensions,
$dynamicStaticMethodReturnTypeExtensions,
$this->functionReflectionFactory,
$this->fileTypeMapper
);
}
}

View File

@ -1,15 +0,0 @@
<?php declare(strict_types=1);
namespace Rector\NodeTypeResolver\Broker;
use PHPStan\Broker\Broker;
use PHPStan\Type\FileTypeMapper;
use Rector\NodeTypeResolver\Reflection\FunctionReflectionFactory;
final class DummyBroker extends Broker
{
public function __construct(FunctionReflectionFactory $functionReflection, FileTypeMapper $fileTypeMapper)
{
parent::__construct([], [], [], [], $functionReflection, $fileTypeMapper);
}
}

View File

@ -0,0 +1,15 @@
<?php declare(strict_types=1);
namespace Rector\NodeTypeResolver\NodeTraverser;
use PhpParser\NodeTraverser;
use Rector\NodeTypeResolver\NodeVisitor\TypeResolvingNodeVisitor;
// @todo: move to normal NodeTraverser as last one? try after setting up tests
final class TypeDetectingNodeTraverser extends NodeTraverser
{
public function __construct(TypeResolvingNodeVisitor $typeResolvingNodeVisitor)
{
$this->visitors[] = $typeResolvingNodeVisitor;
}
}

View File

@ -3,87 +3,26 @@
namespace Rector\NodeTypeResolver;
use PhpParser\Node;
use PhpParser\PrettyPrinter\Standard;
use PHPStan\Analyser\NodeScopeResolver;
use PHPStan\Analyser\Scope;
use PHPStan\Analyser\TypeSpecifier;
use PHPStan\Broker\Broker;
use SplObjectStorage;
use Rector\NodeTypeResolver\NodeTraverser\TypeDetectingNodeTraverser;
final class NodeTypeResolver
{
/**
* @var NodeScopeResolver
* @var TypeDetectingNodeTraverser
*/
private $nodeScopeResolver;
private $typeDetectingNodeTraverser;
/**
* @var Standard
*/
private $prettyPrinter;
/**
* @var TypeSpecifier
*/
private $typeSpecifier;
/**
* @var SplObjectStorage|Scope[]
*/
private $nodeWithScope = [];
/**
* @var Broker
*/
private $broker;
public function __construct(
NodeScopeResolver $nodeScopeResolver,
Standard $prettyPrinter,
TypeSpecifier $typeSpecifier
) {
$this->nodeScopeResolver = $nodeScopeResolver;
$this->prettyPrinter = $prettyPrinter;
$this->typeSpecifier = $typeSpecifier;
$this->nodeWithScope = new SplObjectStorage;
}
public function setBroker(Broker $broker): void
public function __construct(TypeDetectingNodeTraverser $typeDetectingNodeTraverser)
{
$this->broker = $broker;
$this->typeDetectingNodeTraverser = $typeDetectingNodeTraverser;
}
/**
* @param Node[] $nodes
*/
public function getTypeForNode(Node $node, array $nodes): Scope
public function getTypeForNode(Node $activeNode, array $nodes): void
{
if (! isset($this->nodeWithScope[$node])) {
$this->prepareForNodes($nodes);
}
// variable
$scope = $this->nodeWithScope[$node];
dump($scope->getType($node));
// should be 'Nette\Utils\Html'
die;
return $this->nodeWithScope[$node];
}
/**
* @param Node[] $nodes
*/
private function prepareForNodes(array $nodes): void
{
$this->nodeScopeResolver->processNodes($nodes, $this->createScope(), function (Node $node, Scope $scope) {
$this->nodeWithScope[$node] = $scope;
});
}
private function createScope(): Scope
{
return new Scope($this->broker, $this->prettyPrinter, $this->typeSpecifier, '');
$this->typeDetectingNodeTraverser->traverse($nodes);
}
}

View File

@ -0,0 +1,66 @@
<?php declare(strict_types=1);
namespace Rector\NodeTypeResolver\NodeVisitor;
use PhpParser\Node;
use PhpParser\Node\Expr\Assign;
use PhpParser\Node\Expr\New_;
use PhpParser\Node\Expr\Variable;
use PhpParser\Node\FunctionLike;
use PhpParser\Node\Stmt\ClassLike;
use PhpParser\NodeVisitorAbstract;
use Rector\NodeTypeResolver\TypeContext;
// @todo: rename to ClassLikeType, noother types are here
final class TypeResolvingNodeVisitor extends NodeVisitorAbstract
{
/**
* @var TypeContext
*/
private $typeContext;
public function __construct(TypeContext $typeContext)
{
$this->typeContext = $typeContext;
}
/**
* @param Node[] $nodes
*/
public function beforeTraverse(array $nodes): void
{
$this->typeContext->startFile();
}
public function enterNode(Node $node): void
{
if ($node instanceof ClassLike) {
$this->typeContext->enterClass($node);
}
if ($node instanceof FunctionLike) {
$this->typeContext->enterFunction($node);
}
$variableType = null;
if ($node instanceof Variable) {
$nextNode = $node->getAttribute('next');
if ($nextNode instanceof New_) {
$variableType = $nextNode->class->toString();
$variableName = $node->name;
$this->typeContext->addLocalVariable($variableName, $variableType);
} else {
$variableType = $this->typeContext->getTypeForVariable((string) $node->name);
}
}
if ($variableType) {
$node->setAttribute('type', $variableType);
}
if ($node instanceof Assign && $node->var instanceof Variable && $node->expr instanceof Variable) {
$this->typeContext->addAssign($node->var->name, $node->expr->name);
}
}
}

View File

@ -1,22 +0,0 @@
<?php declare(strict_types=1);
namespace Rector\NodeTypeResolver\Reflection;
use PHPStan\Reflection\FunctionReflection;
use PHPStan\Reflection\FunctionReflectionFactory as FunctionReflectionFactoryInterface;
use PHPStan\Type\Type;
use ReflectionFunction;
final class FunctionReflectionFactory implements FunctionReflectionFactoryInterface
{
/**
* @param mixed[] $phpDocParameterTypes
*/
public function create(
ReflectionFunction $reflection,
array $phpDocParameterTypes,
?Type $phpDocReturnType = null
): FunctionReflection {
return new FunctionReflection($reflection, $phpDocParameterTypes, $phpDocReturnType);
}
}

View File

@ -1,72 +0,0 @@
<?php declare(strict_types=1);
namespace Rector\NodeTypeResolver\Reflection\Php;
use PHPStan\Broker\Broker;
use PHPStan\Cache\Cache;
use PHPStan\Parser\FunctionCallStatementFinder;
use PHPStan\Parser\Parser;
use PHPStan\Reflection\ClassReflection;
use PHPStan\Reflection\Php\PhpMethodReflection;
use PHPStan\Reflection\Php\PhpMethodReflectionFactory as PhpMethodReflectionFactoryInterface;
use PHPStan\Type\Type;
use ReflectionMethod;
final class PhpMethodReflectionFactory implements PhpMethodReflectionFactoryInterface
{
/**
* @var Broker
*/
private $broker;
/**
* @var Parser
*/
private $parser;
/**
* @var FunctionCallStatementFinder
*/
private $functionCallStatementFinder;
/**
* @var Cache
*/
private $cache;
public function __construct(
Parser $parser,
FunctionCallStatementFinder $functionCallStatementFinder,
Cache $cache
) {
$this->parser = $parser;
$this->functionCallStatementFinder = $functionCallStatementFinder;
$this->cache = $cache;
}
public function setBroker(Broker $broker): void
{
$this->broker = $broker;
}
/**
* @param Type[] $phpDocParameterTypes
*/
public function create(
ClassReflection $declaringClass,
ReflectionMethod $reflectionMethod,
array $phpDocParameterTypes,
?Type $phpDocReturnType = null
): PhpMethodReflection {
return new PhpMethodReflection(
$declaringClass,
$reflectionMethod,
$this->broker,
$this->parser,
$this->functionCallStatementFinder,
$this->cache,
$phpDocParameterTypes,
$phpDocReturnType
);
}
}

View File

@ -0,0 +1,78 @@
<?php declare(strict_types=1);
namespace Rector\NodeTypeResolver;
use PhpParser\Node\FunctionLike;
use PhpParser\Node\Stmt\ClassLike;
use ReflectionFunction;
use ReflectionMethod;
final class TypeContext
{
/**
* @var mixed[]
*/
private $types = [];
/**
* @var mixed[]
*/
private $localTypes;
/**
* @var ClassLike|null
*/
private $classLikeNode;
public function startFile(): void
{
$this->types = [];
$this->classLikeNode = [];
}
public function addLocalVariable(string $variableName, string $variableType): void
{
$this->localTypes[$variableName] = $variableType;
}
public function enterClass(ClassLike $classLikeNode): void
{
$this->classLikeNode = $classLikeNode;
}
public function enterFunction(FunctionLike $functionLikeNode): void
{
$this->localTypes = [];
$functionReflection = $this->getFunctionReflection($functionLikeNode);
foreach ($functionReflection->getParameters() as $parameterReflection) {
$this->localTypes[$parameterReflection->getName()] = $parameterReflection->getType();
}
}
public function getTypeForVariable(string $name): string
{
return $this->localTypes[$name] ?? '';
}
public function addAssign(string $newVariable, string $oldVariable): void
{
$type = $this->getTypeForVariable($oldVariable);
$this->addLocalVariable($newVariable, $type);
}
/**
* @return ReflectionFunction|ReflectionMethod
*/
private function getFunctionReflection(FunctionLike $functionLikeNode)
{
if ($this->classLikeNode) {
$className = $this->classLikeNode->namespacedName->toString();
$methodName = (string) $functionLikeNode->name;
return new ReflectionMethod($className, $methodName);
}
return new ReflectionFunction((string) $functionLikeNode->name);
}
}

View File

@ -4,54 +4,3 @@ services:
Rector\NodeTypeResolver\:
resource: '../../src'
# PHPStan
# PHPStan\Analyser\TypeSpecifier: ~
# PHPStan\Analyser\NodeScopeResolver:
# arguments:
# $polluteScopeWithLoopInitialAssignments: false
# $polluteCatchScopeWithTryAssignments: false
# $earlyTerminatingMethodCalls: []
# Broker
# Rector\NodeTypeResolver\Broker\BrokerFactory: ~
# PHPStan\Broker\Broker:
# factory: ['@Rector\NodeTypeResolver\Broker\BrokerFactory', 'create']
# required by Broker
# this causes circular referene issue
# PHPStan\Reflection\Php\PhpClassReflectionExtension:
# calls:
# # prevents circular reference
# - ['setBroker', ['@PHPStan\Broker\Broker']]
# PHPStan\Parser\FunctionCallStatementFinder: ~
#
# Rector\NodeTypeResolver\NodeTypeResolver:
# calls:
# # prevents circular reference
# - ['setBroker', ['@PHPStan\Broker\Broker']]
#
# Rector\NodeTypeResolver\Reflection\Php\PhpMethodReflectionFactory:
# calls:
# # prevents circular reference
# - ['setBroker', ['@PHPStan\Broker\Broker']]
#
# # Parser
# PHPStan\Parser\DirectParser: ~
# PHPStan\Parser\CachedParser:
# arguments:
# - '@PHPStan\Parser\DirectParser'
#
# # prefered service for interface
# PHPStan\Parser\Parser:
# alias: PHPStan\Parser\CachedParser
#
# PHPStan\Cache\Cache: ~
# PHPStan\Cache\MemoryCacheStorage: ~
# PHPStan\File\FileHelper:
# arguments:
# $workingDirectory: %kernel.root_dir%/../../
#
# # required for Broker
# PHPStan\Type\FileTypeMapper: ~

View File

@ -1,15 +1,12 @@
<?php declare(strict_types=1);
namespace Rector\NodeTypeResolver\Tests;
namespace Rector\NodeTypeResolver\Tests\NodeTypeResolverSource;
use Nette\Utils\Html;
final class VariableType
{
/**
* @return Html
*/
public function prepare(): \Nette\Utils\Html
public function prepare(): Html
{
$html = new Html;
$assignedHtml = $html;

View File

@ -2,7 +2,10 @@
namespace Rector\NodeTypeResolver\Tests;
use PhpParser\Parser;
use PhpParser\Node\Expr\Variable;
use PhpParser\NodeTraverser;
use Rector\Contract\Parser\ParserInterface;
use Rector\NodeTypeResolver\NodeTraverser\TypeDetectingNodeTraverser;
use Rector\NodeTypeResolver\NodeTypeResolver;
use Rector\Tests\AbstractContainerAwareTestCase;
@ -14,27 +17,42 @@ final class NodeTypeResolverTest extends AbstractContainerAwareTestCase
private $nodeTypeResolver;
/**
* @var Parser
* @var ParserInterface
*/
private $parser;
/**
* @var TypeDetectingNodeTraverser
*/
private $typeDetectingNodeTraverser;
/**
* @var NodeTraverser
*/
private $nodeTraverser;
protected function setUp(): void
{
$this->nodeTypeResolver = $this->container->get(NodeTypeResolver::class);
$this->parser = $this->container->get(Parser::class);
$this->parser = $this->container->get(ParserInterface::class);
$this->typeDetectingNodeTraverser = $this->container->get(TypeDetectingNodeTraverser::class);
$this->nodeTraverser = $this->container->get(NodeTraverser::class);
}
public function test(): void
{
$code = file_get_contents(__DIR__ . '/NodeTypeResolverSource/VariableType.php');
$nodes = $this->parser->parse($code);
$nodes = $this->parser->parseFile(__DIR__ . '/NodeTypeResolverSource/VariableType.php');
$variableNode = $nodes[1]->stmts[1]->stmts[0]->stmts[0]->expr->var;
$resolvedType = $this->nodeTypeResolver->getTypeForNode($variableNode, $nodes);
$this->assertSame('Nette\Utils\Html', $resolvedType);
// run basic traverser here
$this->nodeTraverser->traverse($nodes);
$this->typeDetectingNodeTraverser->traverse($nodes);
// $assignedVariableNode = $nodes[1]->stmts[1]->stmts[0]->stmts[2]->expr;
// $resolvedType = $this->nodeTypeResolver->getTypeForNode($assignedVariableNode, $nodes);
// $this->assertSame('Nette\Utils\Html', $resolvedType);
/** @var Variable $htmlVariableNode */
$htmlVariableNode = $nodes[1]->stmts[1]->stmts[0]->stmts[0]->expr->var;
$this->assertSame(
'Nette\Utils\Html',
$htmlVariableNode->getAttribute('type')
);
}
}

View File

@ -0,0 +1,13 @@
<?php declare(strict_types=1);
namespace Rector\Contract\Parser;
use PhpParser\Node;
interface ParserInterface
{
/**
* @return Node[]
*/
public function parseFile(string $filePath): array;
}

View File

@ -6,5 +6,4 @@ use Exception;
final class FileNotFoundException extends Exception
{
}

View File

@ -0,0 +1,43 @@
<?php declare(strict_types=1);
namespace Rector\NodeVisitor;
use PhpParser\Node;
use PhpParser\NodeVisitorAbstract;
/**
* See https://github.com/nikic/PHP-Parser/blob/master/doc/5_FAQ.markdown#how-can-the-nextprevious-sibling-of-a-node-be-obtained
*/
final class NodeConnector extends NodeVisitorAbstract
{
private $stack;
private $prev;
/**
* @param Node[] $nodes
*/
public function beforeTraverse(array $nodes)
{
$this->stack = [];
$this->prev = null;
}
public function enterNode(Node $node)
{
if (!empty($this->stack)) {
$node->setAttribute('parent', $this->stack[count($this->stack)-1]);
}
if ($this->prev && $this->prev->getAttribute('parent') == $node->getAttribute('parent')) {
$node->setAttribute('prev', $this->prev);
$this->prev->setAttribute('next', $node);
}
$this->stack[] = $node;
}
public function leaveNode(Node $node)
{
$this->prev = $node;
array_pop($this->stack);
}
}

41
src/Parser/Parser.php Normal file
View File

@ -0,0 +1,41 @@
<?php declare(strict_types=1);
namespace Rector\Parser;
use PhpParser\Node;
use PhpParser\Parser as NikicParser;
use Rector\Contract\Parser\ParserInterface;
final class Parser implements ParserInterface
{
/**
* @var NikicParser
*/
private $nikicParser;
/**
* @var Node[][]
*/
private $nodesByFile = [];
public function __construct(NikicParser $nikicParser)
{
$this->nikicParser = $nikicParser;
}
/**
* @return Node[]
*/
public function parseFile(string $filePath): array
{
if (isset($this->nodesByFile[$filePath])) {
return $this->nodesByFile[$filePath];
}
$fileContent = file_get_contents($filePath);
$this->nodesByFile[$filePath] = $this->nikicParser->parse($fileContent);
return $this->nodesByFile[$filePath];
}
}

View File

@ -3,7 +3,7 @@
namespace Rector\Parser;
use PhpParser\Lexer;
use PhpParser\Parser;
use PhpParser\Parser as NikicParser;
use PhpParser\ParserFactory as NikicParserFactory;
final class ParserFactory
@ -24,7 +24,7 @@ final class ParserFactory
$this->nikicParserFactory = $nikicParserFactory;
}
public function create(): Parser
public function create(): NikicParser
{
return $this->nikicParserFactory->create(NikicParserFactory::PREFER_PHP7, $this->lexer, [
'useIdentifierNodes' => true,

View File

@ -113,7 +113,17 @@ final class HtmlAddMethodRector extends AbstractRector
return false;
}
$type = $this->nodeTypeResolver->getTypeForNode($node->var, $this->fileNodes);
$this->nodeTypeResolver->getTypeForNode($node->var, $this->fileNodes);
dump($node->var);
die;
dump($type);
die;
dump($type->getType($node->var));
die;

View File

@ -10,26 +10,32 @@ services:
Rector\:
resource: '../../src'
# autowire by interface
Rector\Contract\Parser\ParserInterface:
alias: Rector\Parser\Parser
# 3rd party services
Symfony\Component\Console\Application:
arguments:
$name: "Rector"
# PhpParser - Parser
PhpParser\Parser:
factory: ['@Rector\Parser\ParserFactory', 'create']
PhpParser\Lexer:
factory: ['@Rector\Parser\LexerFactory', 'create']
PhpParser\BuilderFactory: ~
# PhpParser - NodeTraverser
# to allow $namespacedName
# see https://github.com/nikic/PHP-Parser/blob/7b36ca3b6cc1b99210c6699074d6091061e73eea/lib/PhpParser/Node/Stmt/ClassLike.php#L8
PhpParser\NodeVisitor\NameResolver: ~
PhpParser\NodeTraverser:
calls:
- ['addVisitor', ['@PhpParser\NodeVisitor\NameResolver']]
- ['addVisitor', ['@Rector\NodeVisitor\DependencyInjection\AddPropertiesToClassNodeVisitor']]
# Traverser
- ['addVisitor', ['@Rector\NodeVisitor\NodeConnector']]
PhpParser\ParserFactory: ~
# Printer
# PhpParser - Printer
PhpParser\PrettyPrinter\Standard: ~

View File

@ -20,7 +20,7 @@ abstract class AbstractContainerAwareTestCase extends TestCase
* @param mixed[] $data
* @param string $dataName
*/
public function __construct($name = null, array $data = [], $dataName = '')
public function __construct(string $name = null, array $data = [], string $dataName = '')
{
parent::__construct($name, $data, $dataName);

View File

@ -4,11 +4,11 @@ namespace Rector\Tests\Rector\Contrib\Nette\HtmlAddMethodRector\Correct;
use Nette\Utils\Html;
class SomeClass
final class SomeClass
{
private function createHtml()
private function createHtml(): void
{
$html = new Html();
$html = new Html;
$anotherHtml = $html;
$anotherHtml->addHtml('someContent');
}

View File

@ -4,11 +4,11 @@ namespace Rector\Tests\Rector\Contrib\Nette\HtmlAddMethodRector\Wrong;
use Nette\Utils\Html;
class SomeClass
final class SomeClass
{
private function createHtml()
private function createHtml(): void
{
$html = new Html();
$html = new Html;
$anotherHtml = $html;
$anotherHtml->add('someContent');
}