rector/packages/BetterPhpDocParser/src/PhpDocParser/BetterPhpDocParser.php

280 lines
9.5 KiB
PHP
Raw Normal View History

2019-02-28 21:50:53 +00:00
<?php declare(strict_types=1);
namespace Rector\BetterPhpDocParser\PhpDocParser;
use Nette\Utils\Strings;
2019-02-28 21:50:53 +00:00
use PHPStan\PhpDocParser\Ast\Node;
use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocNode;
use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocTagNode;
2019-02-28 21:50:53 +00:00
use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocTagValueNode;
use PHPStan\PhpDocParser\Lexer\Lexer;
use PHPStan\PhpDocParser\Parser\ConstExprParser;
use PHPStan\PhpDocParser\Parser\ParserException;
use PHPStan\PhpDocParser\Parser\PhpDocParser;
use PHPStan\PhpDocParser\Parser\TokenIterator;
use PHPStan\PhpDocParser\Parser\TypeParser;
use Rector\BetterPhpDocParser\Attributes\Ast\AttributeAwareNodeFactory;
use Rector\BetterPhpDocParser\Attributes\Ast\PhpDoc\AttributeAwarePhpDocNode;
use Rector\BetterPhpDocParser\Attributes\Attribute\Attribute;
2019-09-26 14:26:44 +00:00
use Rector\BetterPhpDocParser\Contract\PhpDocNodeFactoryInterface;
use Rector\BetterPhpDocParser\Contract\PhpDocParserAwareInterface;
use Rector\BetterPhpDocParser\Printer\MultilineSpaceFormatPreserver;
use Rector\BetterPhpDocParser\ValueObject\StartEndValueObject;
2019-09-26 14:26:44 +00:00
use Rector\Configuration\CurrentNodeProvider;
2019-02-28 21:50:53 +00:00
use Symplify\PackageBuilder\Reflection\PrivatesAccessor;
use Symplify\PackageBuilder\Reflection\PrivatesCaller;
2019-09-26 14:26:44 +00:00
/**
2019-10-02 18:45:33 +00:00
* @see \Rector\BetterPhpDocParser\Tests\PhpDocParser\OrmTagParser\Class_\DoctrinePhpDocParserTest
2019-09-26 14:26:44 +00:00
* @see \Rector\BetterPhpDocParser\Tests\PhpDocParser\OrmTagParser\Property_\OrmTagParserPropertyTest
*/
2019-02-28 21:50:53 +00:00
final class BetterPhpDocParser extends PhpDocParser
{
/**
* @var bool
*/
private $isComment = false;
/**
* @var PrivatesCaller
*/
private $privatesCaller;
/**
* @var PrivatesAccessor
*/
private $privatesAccessor;
/**
* @var AttributeAwareNodeFactory
*/
private $attributeAwareNodeFactory;
/**
* @var MultilineSpaceFormatPreserver
*/
private $multilineSpaceFormatPreserver;
2019-09-26 14:26:44 +00:00
/**
* @var PhpDocNodeFactoryInterface[]
*/
private $phpDocNodeFactories = [];
/**
* @var CurrentNodeProvider
*/
private $currentNodeProvider;
/**
2019-09-26 14:26:44 +00:00
* @param PhpDocNodeFactoryInterface[] $phpDocNodeFactories
*/
2019-02-28 21:50:53 +00:00
public function __construct(
TypeParser $typeParser,
ConstExprParser $constExprParser,
AttributeAwareNodeFactory $attributeAwareNodeFactory,
MultilineSpaceFormatPreserver $multilineSpaceFormatPreserver,
2019-09-26 14:26:44 +00:00
CurrentNodeProvider $currentNodeProvider,
array $phpDocNodeFactories = []
2019-02-28 21:50:53 +00:00
) {
parent::__construct($typeParser, $constExprParser);
$this->privatesCaller = new PrivatesCaller();
$this->privatesAccessor = new PrivatesAccessor();
$this->attributeAwareNodeFactory = $attributeAwareNodeFactory;
$this->multilineSpaceFormatPreserver = $multilineSpaceFormatPreserver;
2019-09-26 14:26:44 +00:00
$this->phpDocNodeFactories = $phpDocNodeFactories;
$this->currentNodeProvider = $currentNodeProvider;
2019-02-28 21:50:53 +00:00
}
/**
* @return AttributeAwarePhpDocNode|PhpDocNode
*/
public function parse(TokenIterator $tokenIterator): PhpDocNode
{
$this->isComment = false;
try {
$tokenIterator->consumeTokenType(Lexer::TOKEN_OPEN_PHPDOC);
} catch (ParserException $parserException) {
// probably "//" start
$this->isComment = true;
$tokenIterator->consumeTokenType(Lexer::TOKEN_OTHER);
}
$tokenIterator->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL);
$children = [];
if (! $tokenIterator->isCurrentTokenType(Lexer::TOKEN_CLOSE_PHPDOC)) {
$children[] = $this->parseChildAndStoreItsPositions($tokenIterator);
while ($tokenIterator->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL) && ! $tokenIterator->isCurrentTokenType(
Lexer::TOKEN_CLOSE_PHPDOC
)) {
$children[] = $this->parseChildAndStoreItsPositions($tokenIterator);
}
}
if (! $this->isComment) {
2019-09-26 14:26:44 +00:00
// might be in the middle of annotations
$tokenIterator->tryConsumeTokenType(Lexer::TOKEN_CLOSE_PHPDOC);
2019-02-28 21:50:53 +00:00
}
$phpDocNode = new PhpDocNode(array_values($children));
return $this->attributeAwareNodeFactory->createFromNode($phpDocNode);
}
public function parseTag(TokenIterator $tokenIterator): PhpDocTagNode
{
2019-10-02 19:18:45 +00:00
$tag = $this->resolveTag($tokenIterator);
$phpDocTagValueNode = $this->parseTagValue($tokenIterator, $tag);
return new PhpDocTagNode($tag, $phpDocTagValueNode);
}
2019-02-28 21:50:53 +00:00
public function parseTagValue(TokenIterator $tokenIterator, string $tag): PhpDocTagValueNode
{
// needed for reference support in params, see https://github.com/rectorphp/rector/issues/1734
2019-09-26 14:26:44 +00:00
$tagValueNode = null;
foreach ($this->phpDocNodeFactories as $phpDocNodeFactory) {
// to prevent circular reference of this service
if ($phpDocNodeFactory instanceof PhpDocParserAwareInterface) {
$phpDocNodeFactory->setPhpDocParser($this);
}
2019-09-27 19:53:34 +00:00
// compare regardless sensitivity
2019-09-27 20:09:00 +00:00
if ($this->isTagMatchingPhpDocNodeFactory($tag, $phpDocNodeFactory)) {
2019-09-26 14:26:44 +00:00
$currentNode = $this->currentNodeProvider->getNode();
$tagValueNode = $phpDocNodeFactory->createFromNodeAndTokens($currentNode, $tokenIterator);
}
}
// fallback to original parser
2019-09-26 14:26:44 +00:00
if ($tagValueNode === null) {
2019-09-17 09:14:06 +00:00
$tagValueNode = parent::parseTagValue($tokenIterator, $tag);
}
2019-02-28 21:50:53 +00:00
return $this->attributeAwareNodeFactory->createFromNode($tagValueNode);
}
private function parseChildAndStoreItsPositions(TokenIterator $tokenIterator): Node
{
$tokenStart = $this->getTokenIteratorIndex($tokenIterator);
$phpDocNode = $this->privatesCaller->callPrivateMethod($this, 'parseChild', $tokenIterator);
$tokenEnd = $this->getTokenIteratorIndex($tokenIterator);
$startEndValueObject = new StartEndValueObject($tokenStart, $tokenEnd);
2019-02-28 21:50:53 +00:00
$attributeAwareNode = $this->attributeAwareNodeFactory->createFromNode($phpDocNode);
$attributeAwareNode->setAttribute(Attribute::PHP_DOC_NODE_INFO, $startEndValueObject);
2019-02-28 21:50:53 +00:00
$possibleMultilineText = $this->multilineSpaceFormatPreserver->resolveCurrentPhpDocNodeText(
$attributeAwareNode
);
if ($possibleMultilineText) {
// add original text, for keeping trimmed spaces
$originalContent = $this->getOriginalContentFromTokenIterator($tokenIterator);
// we try to match original content without trimmed spaces
$currentTextPattern = '#' . preg_quote($possibleMultilineText, '#') . '#s';
$currentTextPattern = Strings::replace($currentTextPattern, '#(\s)+#', '\s+');
$match = Strings::match($originalContent, $currentTextPattern);
if (isset($match[0])) {
$attributeAwareNode->setAttribute(Attribute::ORIGINAL_CONTENT, $match[0]);
}
}
2019-02-28 21:50:53 +00:00
return $attributeAwareNode;
}
private function getOriginalContentFromTokenIterator(TokenIterator $tokenIterator): string
{
2019-09-17 11:45:12 +00:00
// @todo iterate through tokens...
$originalTokens = $this->privatesAccessor->getPrivateProperty($tokenIterator, 'tokens');
$originalContent = '';
foreach ($originalTokens as $originalToken) {
// skip opening
2019-09-26 14:26:44 +00:00
if ($originalToken[1] === Lexer::TOKEN_OPEN_PHPDOC) {
continue;
}
// skip closing
2019-09-26 14:26:44 +00:00
if ($originalToken[1] === Lexer::TOKEN_CLOSE_PHPDOC) {
continue;
}
2019-09-26 14:26:44 +00:00
if ($originalToken[1] === Lexer::TOKEN_PHPDOC_EOL) {
$originalToken[0] = PHP_EOL;
}
$originalContent .= $originalToken[0];
}
return trim($originalContent);
}
private function getTokenIteratorIndex(TokenIterator $tokenIterator): int
{
return (int) $this->privatesAccessor->getPrivateProperty($tokenIterator, 'index');
}
2019-09-27 20:09:00 +00:00
private function isTagMatchingPhpDocNodeFactory(string $tag, PhpDocNodeFactoryInterface $phpDocNodeFactory): bool
{
if (Strings::lower($phpDocNodeFactory->getName()) === Strings::lower($tag)) {
return true;
}
if (in_array($tag, ['@param'], true)) {
return false;
}
// possible short import
if (Strings::contains($phpDocNodeFactory->getName(), '\\')) {
$lastNamePart = Strings::after($phpDocNodeFactory->getName(), '\\', -1);
if (Strings::lower('@' . $lastNamePart) === Strings::lower($tag)) {
return true;
}
}
return false;
}
2019-10-02 19:18:45 +00:00
private function resolveTag(TokenIterator $tokenIterator): string
{
$tag = $tokenIterator->currentTokenValue();
$tokenIterator->next();
// is not e.g "@var "
// join tags like "@ORM\Column" etc.
if ($tokenIterator->currentTokenType() !== Lexer::TOKEN_IDENTIFIER) {
return $tag;
}
$oldTag = $tag;
$tag .= $tokenIterator->currentTokenValue();
$isTagMatchedByFactories = $this->isTagMatchedByFactories($tag);
if (! $isTagMatchedByFactories) {
return $oldTag;
}
$tokenIterator->next();
return $tag;
}
private function isTagMatchedByFactories(string $tag): bool
{
foreach ($this->phpDocNodeFactories as $phpDocNodeFactory) {
if ($this->isTagMatchingPhpDocNodeFactory($tag, $phpDocNodeFactory)) {
return true;
}
}
return false;
}
2019-02-28 21:50:53 +00:00
}