Skip to content

Commit

Permalink
Add PhpdocToPropertyTypeFixer
Browse files Browse the repository at this point in the history
  • Loading branch information
julienfalque committed Nov 7, 2019
1 parent 336a3d4 commit e1fbd78
Show file tree
Hide file tree
Showing 6 changed files with 658 additions and 13 deletions.
12 changes: 12 additions & 0 deletions README.rst
Expand Up @@ -1654,6 +1654,18 @@ Choose from the list of available rules:
- ``scalar_types`` (``bool``): fix also scalar types; may have unexpected
behaviour due to PHP bad type coercion system; defaults to ``true``

* **phpdoc_to_property_type**

EXPERIMENTAL: Takes ``@var`` annotation of non-mixed types and adjusts
accordingly the property signature. Requires PHP >= 7.4.

*Risky rule: [1] This rule is EXPERIMENTAL and is not covered with backward compatibility promise. [2] ``@var`` annotation is mandatory for the fixer to make changes, signatures of properties without it (no docblock, inheritdocs) will not be fixed. [3] Manual actions are required if inherited signatures are not properly documented. [4] ``@inheritdocs`` support is under construction.*

Configuration options:

- ``scalar_types`` (``bool``): fix also scalar types; may have unexpected
behaviour due to PHP bad type coercion system; defaults to ``true``

* **phpdoc_to_return_type**

EXPERIMENTAL: Takes ``@return`` annotation of non-mixed types and adjusts
Expand Down
28 changes: 18 additions & 10 deletions src/AbstractPhpdocToTypeDeclarationFixer.php
Expand Up @@ -83,14 +83,11 @@ protected function createConfigurationDefinition()
}

/**
* Find all the annotations of given type in the function's PHPDoc comment.
* @param int $index The index of the function token
*
* @param string $name
* @param int $index The index of the function token
*
* @return Annotation[]
* @return null|int
*/
protected function findAnnotations($name, Tokens $tokens, $index)
protected function findFunctionDocComment(Tokens $tokens, $index)
{
do {
$index = $tokens->getPrevNonWhitespace($index);
Expand All @@ -104,18 +101,29 @@ protected function findAnnotations($name, Tokens $tokens, $index)
T_STATIC,
]));

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

return null;
}

/**
* @param string $name
* @param int $docCommentIndex
*
* @return Annotation[]
*/
protected function getAnnotationsFromDocComment($name, Tokens $tokens, $docCommentIndex)
{
$namespacesAnalyzer = new NamespacesAnalyzer();
$namespace = $namespacesAnalyzer->getNamespaceAt($tokens, $index);
$namespace = $namespacesAnalyzer->getNamespaceAt($tokens, $docCommentIndex);

$namespaceUsesAnalyzer = new NamespaceUsesAnalyzer();
$namespaceUses = $namespaceUsesAnalyzer->getDeclarationsInNamespace($tokens, $namespace);

$doc = new DocBlock(
$tokens[$index]->getContent(),
$tokens[$docCommentIndex]->getContent(),
$namespace,
$namespaceUses
);
Expand Down
8 changes: 6 additions & 2 deletions src/Fixer/FunctionNotation/PhpdocToParamTypeFixer.php
Expand Up @@ -97,9 +97,13 @@ protected function applyFix(\SplFileInfo $file, Tokens $tokens)
continue;
}

$paramTypeAnnotations = $this->findAnnotations('param', $tokens, $index);
$docCommentIndex = $this->findFunctionDocComment($tokens, $index);

foreach ($paramTypeAnnotations as $paramTypeAnnotation) {
if (null === $docCommentIndex) {
continue;
}

foreach ($this->getAnnotationsFromDocComment('param', $tokens, $docCommentIndex) as $paramTypeAnnotation) {
$typeInfo = $this->getCommonTypeFromAnnotation($paramTypeAnnotation);

if (null === $typeInfo) {
Expand Down
215 changes: 215 additions & 0 deletions src/Fixer/FunctionNotation/PhpdocToPropertyTypeFixer.php
@@ -0,0 +1,215 @@
<?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\Fixer\FunctionNotation;

use PhpCsFixer\AbstractPhpdocToTypeDeclarationFixer;
use PhpCsFixer\DocBlock\Annotation;
use PhpCsFixer\FixerDefinition\FixerDefinition;
use PhpCsFixer\FixerDefinition\VersionSpecification;
use PhpCsFixer\FixerDefinition\VersionSpecificCodeSample;
use PhpCsFixer\Tokenizer\Token;
use PhpCsFixer\Tokenizer\Tokens;

final class PhpdocToPropertyTypeFixer extends AbstractPhpdocToTypeDeclarationFixer
{
/**
* {@inheritdoc}
*/
public function getDefinition()
{
return new FixerDefinition(
'EXPERIMENTAL: Takes `@var` annotation of non-mixed types and adjusts accordingly the property signature. Requires PHP >= 7.4.',
[
new VersionSpecificCodeSample(
'<?php
class Foo {
/** @var int */
private $foo;
/** @var \Traversable */
private $bar;
}
',
new VersionSpecification(70400)
),
new VersionSpecificCodeSample(
'<?php
class Foo {
/** @var int */
private $foo;
/** @var \Traversable */
private $bar;
}
',
new VersionSpecification(70400),
['scalar_types' => false]
),
],
null,
'[1] This rule is EXPERIMENTAL and is not covered with backward compatibility promise. [2] `@var` annotation is mandatory for the fixer to make changes, signatures of properties without it (no docblock, inheritdocs) will not be fixed. [3] Manual actions are required if inherited signatures are not properly documented. [4] `@inheritdocs` support is under construction.'
);
}

/**
* {@inheritdoc}
*/
public function isCandidate(Tokens $tokens)
{
return \PHP_VERSION_ID >= 70400 && $tokens->isTokenKindFound(T_DOC_COMMENT);
}

/**
* {@inheritdoc}
*/
protected function applyFix(\SplFileInfo $file, Tokens $tokens)
{
for ($index = $tokens->count() - 1; 0 < $index; --$index) {
if ($tokens[$index]->isGivenKind([T_CLASS, T_TRAIT])) {
$this->fixClass($tokens, $index);
}
}
}

/**
* @param int $index
*/
private function fixClass(Tokens $tokens, $index)
{
$index = $tokens->getNextTokenOfKind($index, ['{']);
$classEndIndex = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_CURLY_BRACE, $index);

for (; $index < $classEndIndex; ++$index) {
if ($tokens[$index]->isGivenKind(T_FUNCTION)) {
$index = $tokens->getNextTokenOfKind($index, ['{']);
$index = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_CURLY_BRACE, $index);

continue;
}

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

$docCommentIndex = $index;
$propertyIndexes = $this->findNextUntypedPropertiesDeclaration($tokens, $docCommentIndex);

if ([] === $propertyIndexes) {
continue;
}

$typeInfo = $this->resolveAppliableType(
$propertyIndexes,
$this->getAnnotationsFromDocComment('var', $tokens, $docCommentIndex)
);

if (null === $typeInfo) {
continue;
}

list($propertyType, $isNullable) = $typeInfo;

if (\in_array($propertyType, ['void', 'callable'], true)) {
continue;
}

$newTokens = array_merge(
$this->createTypeDeclarationTokens($propertyType, $isNullable),
[new Token([T_WHITESPACE, ' '])]
);

$tokens->insertAt(current($propertyIndexes), $newTokens);

$index = max($propertyIndexes) + \count($newTokens) + 1;
}
}

/**
* @param int $index
*
* @return array<string, int>
*/
private function findNextUntypedPropertiesDeclaration(Tokens $tokens, $index)
{
do {
$index = $tokens->getNextMeaningfulToken($index);
} while ($tokens[$index]->isGivenKind([
T_PRIVATE,
T_PROTECTED,
T_PUBLIC,
T_STATIC,
T_VAR,
]));

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

$properties = [];
while (!$tokens[$index]->equals(';')) {
if ($tokens[$index]->isGivenKind(T_VARIABLE)) {
$properties[$tokens[$index]->getContent()] = $index;
}

$index = $tokens->getNextMeaningfulToken($index);
}

return $properties;
}

/**
* @param array<string, int> $propertyIndexes
* @param Annotation[] $annotations
*/
private function resolveAppliableType(array $propertyIndexes, array $annotations)
{
$propertyTypes = [];

foreach ($annotations as $annotation) {
$propertyName = $annotation->getVariableName();

if (null === $propertyName) {
if (1 !== \count($propertyIndexes)) {
continue;
}

$propertyName = key($propertyIndexes);
}

if (!isset($propertyIndexes[$propertyName])) {
continue;
}

$typeInfo = $this->getCommonTypeFromAnnotation($annotation);

if (!isset($propertyTypes[$propertyName])) {
$propertyTypes[$propertyName] = [];
} elseif ($typeInfo !== $propertyTypes[$propertyName]) {
return null;
}

$propertyTypes[$propertyName] = $typeInfo;
}

if (\count($propertyTypes) !== \count($propertyIndexes)) {
return null;
}

$type = array_shift($propertyTypes);
foreach ($propertyTypes as $propertyType) {
if ($propertyType !== $type) {
return null;
}
}

return $type;
}
}
7 changes: 6 additions & 1 deletion src/Fixer/FunctionNotation/PhpdocToReturnTypeFixer.php
Expand Up @@ -115,7 +115,12 @@ protected function applyFix(\SplFileInfo $file, Tokens $tokens)
continue;
}

$returnTypeAnnotation = $this->findAnnotations('return', $tokens, $index);
$docCommentIndex = $this->findFunctionDocComment($tokens, $index);
if (null === $docCommentIndex) {
continue;
}

$returnTypeAnnotation = $this->getAnnotationsFromDocComment('return', $tokens, $docCommentIndex);
if (1 !== \count($returnTypeAnnotation)) {
continue;
}
Expand Down

0 comments on commit e1fbd78

Please sign in to comment.