Skip to content

Commit

Permalink
Refactor conversion of PHPDoc to type declarations
Browse files Browse the repository at this point in the history
  • Loading branch information
julienfalque committed Oct 13, 2019
1 parent 18e95e4 commit 777cb40
Show file tree
Hide file tree
Showing 11 changed files with 937 additions and 481 deletions.
209 changes: 209 additions & 0 deletions src/AbstractPhpdocToTypeDeclarationFixer.php
@@ -0,0 +1,209 @@
<?php

/*
* This file is part of PHP CS Fixer.
*
* (c) Fabien Potencier <fabien@symfony.com>
* Dariusz Rumiński <dariusz.ruminski@gmail.com>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/

namespace PhpCsFixer;

use PhpCsFixer\DocBlock\Annotation;
use PhpCsFixer\DocBlock\DocBlock;
use PhpCsFixer\Tokenizer\Analyzer\Analysis\NamespaceAnalysis;
use PhpCsFixer\Tokenizer\Analyzer\Analysis\NamespaceUseAnalysis;
use PhpCsFixer\Tokenizer\CT;
use PhpCsFixer\Tokenizer\Token;
use PhpCsFixer\Tokenizer\Tokens;

/**
* @internal
*/
abstract class AbstractPhpdocToTypeDeclarationFixer extends AbstractFixer
{
/**
* @var array<string, bool>
*/
protected $scalarTypes = [
'bool' => true,
'float' => true,
'int' => true,
'string' => true,
];
/**
* @var string
*/
private $classRegex = '/^\\\\?[a-zA-Z_\\x7f-\\xff](?:\\\\?[a-zA-Z0-9_\\x7f-\\xff]+)*$/';

/**
* @var array<string, int>
*/
private $versionSpecificTypes = [
'void' => 70100,
'iterable' => 70100,
'object' => 70200,
];

/**
* @var array<string, bool>
*/
private $skippedTypes = [
'mixed' => true,
'resource' => true,
'null' => true,
];

/**
* Find all the annotations of given type in the function's PHPDoc comment.
*
* @param string $name
* @param Tokens $tokens
* @param int $index The index of the function token
* @param NamespaceAnalysis[] $namespaces
* @param NamespaceUseAnalysis[] $namespaceUses
*
* @return Annotation[]
*/
protected function findAnnotations($name, Tokens $tokens, $index, array $namespaces, array $namespaceUses)
{
do {
$index = $tokens->getPrevNonWhitespace($index);
} while ($tokens[$index]->isGivenKind([
T_COMMENT,
T_ABSTRACT,
T_FINAL,
T_PRIVATE,
T_PROTECTED,
T_PUBLIC,
T_STATIC,
]));

if (!$tokens[$index]->isGivenKind(T_DOC_COMMENT)) {
return [];
}

$namespace = $this->getNamespace($namespaces, $index);

$doc = new DocBlock(
$tokens[$index]->getContent(),
$namespace,
$this->getNamespaceUses($namespace, $namespaceUses)
);

return $doc->getAnnotationsOfType($name);
}

/**
* @param string $type
* @param bool $isNullable
*
* @return Token[]
*/
protected function createTypeDeclarationTokens($type, $isNullable)
{
static $specialTypes = [
'array' => [CT::T_ARRAY_TYPEHINT, 'array'],
'callable' => [T_CALLABLE, 'callable'],
];

if (true === $isNullable) {
$newTokens[] = new Token([CT::T_NULLABLE_TYPE, '?']);
}

if (isset($specialTypes[$type])) {
$newTokens[] = new Token($specialTypes[$type]);
} else {
foreach (explode('\\', $type) as $nsIndex => $value) {
if (0 === $nsIndex && '' === $value) {
continue;
}

if (0 < $nsIndex) {
$newTokens[] = new Token([T_NS_SEPARATOR, '\\']);
}
$newTokens[] = new Token([T_STRING, $value]);
}
}

return $newTokens;
}

/**
* @param Annotation $annotation
*
* @return null|array
*/
protected function getCommonTypeFromAnnotation(Annotation $annotation)
{
$typesExpression = $annotation->getTypeExpression();

$commonType = $typesExpression->getCommonType();
$isNullable = $typesExpression->allowsNull();

if (null === $commonType) {
return null;
}

if ($isNullable && (\PHP_VERSION_ID < 70100 || 'void' === $commonType)) {
return null;
}

if ('static' === $commonType) {
$commonType = 'self';
}

if (isset($this->skippedTypes[$commonType])) {
return null;
}

if (isset($this->versionSpecificTypes[$commonType]) && \PHP_VERSION_ID < $this->versionSpecificTypes[$commonType]) {
return null;
}

if (!isset($this->scalarTypes[$commonType]) && 1 !== Preg::match($this->classRegex, $commonType)) {
return null;
}

return [$commonType, $isNullable];
}

/**
* @param NamespaceAnalysis[] $namespaces $namespaces
* @param int $index
*
* @return NamespaceAnalysis
*/
private function getNamespace(array $namespaces, $index)
{
foreach ($namespaces as $namespace) {
if ($namespace->getScopeStartIndex() <= $index && $namespace->getScopeEndIndex() >= $index) {
return $namespace;
}
}

throw new \RuntimeException("Unable to get the namespace at index {$index}.");
}

/**
* @param NamespaceAnalysis $namespace
* @param NamespaceUseAnalysis[] $namespaceUses
*
* @return NamespaceUseAnalysis[]
*/
private function getNamespaceUses(NamespaceAnalysis $namespace, array $namespaceUses)
{
$result = [];

foreach ($namespaceUses as $namespaceUse) {
if ($namespaceUse->getStartIndex() >= $namespace->getScopeStartIndex() && $namespaceUse->getStartIndex() <= $namespace->getScopeEndIndex()) {
$result[] = $namespaceUse;
}
}

return $result;
}
}
95 changes: 43 additions & 52 deletions src/DocBlock/Annotation.php
Expand Up @@ -13,6 +13,8 @@
namespace PhpCsFixer\DocBlock;

use PhpCsFixer\Preg;
use PhpCsFixer\Tokenizer\Analyzer\Analysis\NamespaceAnalysis;
use PhpCsFixer\Tokenizer\Analyzer\Analysis\NamespaceUseAnalysis;

/**
* This represents an entire annotation from a docblock.
Expand All @@ -22,41 +24,6 @@
*/
class Annotation
{
/**
* Regex to match any types, shall be used with `x` modifier.
*
* @internal
*/
const REGEX_TYPES = '
# <simple> is any non-array, non-generic, non-alternated type, eg `int` or `\Foo`
# <array> is array of <simple>, eg `int[]` or `\Foo[]`
# <generic> is generic collection type, like `array<string, int>`, `Collection<Item>` and more complex like `Collection<int, \null|SubCollection<string>>`
# <type> is <simple>, <array> or <generic> type, like `int`, `bool[]` or `Collection<ItemKey, ItemVal>`
# <types> is one or more types alternated via `|`, like `int|bool[]|Collection<ItemKey, ItemVal>`
(?<types>
(?<type>
(?<array>
(?&simple)(\[\])*
)
|
(?<simple>
[@$?]?[\\\\\w]+
)
|
(?<generic>
(?&simple)
<
(?:(?&types),\s*)?(?:(?&types)|(?&generic))
>
)
)
(?:
\|
(?:(?&simple)|(?&array)|(?&generic))
)*
)
';

/**
* All the annotation tag names with types.
*
Expand Down Expand Up @@ -116,14 +83,28 @@ class Annotation
*/
private $types;

/**
* @var null|NamespaceAnalysis
*/
private $namespace;

/**
* @var NamespaceUseAnalysis[]
*/
private $namespaceUses;

/**
* Create a new line instance.
*
* @param Line[] $lines
* @param Line[] $lines
* @param null|NamespaceAnalysis $namespace
* @param NamespaceUseAnalysis[] $namespaceUses
*/
public function __construct(array $lines)
public function __construct(array $lines, $namespace = null, array $namespaceUses = [])
{
$this->lines = array_values($lines);
$this->namespace = $namespace;
$this->namespaceUses = $namespaceUses;

$keys = array_keys($lines);

Expand Down Expand Up @@ -185,6 +166,29 @@ public function getTag()
return $this->tag;
}

/**
* @return TypeExpression
*/
public function getTypeExpression()
{
return new TypeExpression($this->getTypesContent(), $this->namespace, $this->namespaceUses);
}

/**
* @return null|string
*/
public function getVariableName()
{
$type = preg_quote($this->getTypesContent(), '/');
$regex = "/@{$this->tag->getName()}\\s+{$type}\\s+(?<variable>\\$.+?)(?:[\\s*]|$)/";

if (Preg::match($regex, $this->lines[0]->getContent(), $matches)) {
return $matches['variable'];
}

return null;
}

/**
* Get the types associated with this annotation.
*
Expand All @@ -193,20 +197,7 @@ public function getTag()
public function getTypes()
{
if (null === $this->types) {
$this->types = [];

$content = $this->getTypesContent();

while ('' !== $content && false !== $content) {
Preg::match(
'{^'.self::REGEX_TYPES.'$}x',
$content,
$matches
);

$this->types[] = $matches['type'];
$content = substr($content, \strlen($matches['type']) + 1);
}
$this->types = $this->getTypeExpression()->getTypes();
}

return $this->types;
Expand Down Expand Up @@ -286,7 +277,7 @@ private function getTypesContent()
}

$matchingResult = Preg::match(
'{^(?:\s*\*|/\*\*)\s*@'.$name.'\s+'.self::REGEX_TYPES.'(?:[ \t].*)?$}sx',
'{^(?:\s*\*|/\*\*)\s*@'.$name.'\s+'.TypeExpression::REGEX_TYPES.'(?:[ \t].*)?$}sx',
$this->lines[0]->getContent(),
$matches
);
Expand Down

0 comments on commit 777cb40

Please sign in to comment.