Skip to content

Commit

Permalink
Add PhpdocToPropertyTypeFixer
Browse files Browse the repository at this point in the history
  • Loading branch information
julienfalque committed Jun 25, 2020
1 parent 352cd22 commit 7b00b95
Show file tree
Hide file tree
Showing 13 changed files with 766 additions and 25 deletions.
14 changes: 13 additions & 1 deletion README.rst
Expand Up @@ -1756,12 +1756,24 @@ 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: this rule is EXPERIMENTAL and [1] 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) will not be fixed. [3] Manual actions might be required for newly typed properties that are read before initialization.*

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
accordingly the function signature. Requires PHP >= 7.0.

*Risky rule: this rule is EXPERIMENTAL and [1] is not covered with backward compatibility promise. [2] ``@return`` annotation is mandatory for the fixer to make changes, signatures of methods 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.*
*Risky rule: this rule is EXPERIMENTAL and [1] is not covered with backward compatibility promise. [2] ``@return`` annotation is mandatory for the fixer to make changes, signatures of methods without it (no docblock, inheritdocs) will not be fixed. [3] Manual actions are required if inherited signatures are not properly documented.*

Configuration options:

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
2 changes: 1 addition & 1 deletion src/Fixer/Comment/CommentToPhpdocFixer.php
Expand Up @@ -54,7 +54,7 @@ public function isRisky()
/**
* {@inheritdoc}
*
* Must run before GeneralPhpdocAnnotationRemoveFixer, NoBlankLinesAfterPhpdocFixer, NoEmptyPhpdocFixer, NoSuperfluousPhpdocTagsFixer, PhpdocAddMissingParamAnnotationFixer, PhpdocAlignFixer, PhpdocAlignFixer, PhpdocAnnotationWithoutDotFixer, PhpdocInlineTagFixer, PhpdocLineSpanFixer, PhpdocNoAccessFixer, PhpdocNoAliasTagFixer, PhpdocNoEmptyReturnFixer, PhpdocNoPackageFixer, PhpdocNoUselessInheritdocFixer, PhpdocOrderByValueFixer, PhpdocOrderFixer, PhpdocReturnSelfReferenceFixer, PhpdocSeparationFixer, PhpdocSingleLineVarSpacingFixer, PhpdocSummaryFixer, PhpdocToCommentFixer, PhpdocToParamTypeFixer, PhpdocToReturnTypeFixer, PhpdocTrimConsecutiveBlankLineSeparationFixer, PhpdocTrimFixer, PhpdocTypesOrderFixer, PhpdocVarAnnotationCorrectOrderFixer, PhpdocVarWithoutNameFixer.
* Must run before GeneralPhpdocAnnotationRemoveFixer, NoBlankLinesAfterPhpdocFixer, NoEmptyPhpdocFixer, NoSuperfluousPhpdocTagsFixer, PhpdocAddMissingParamAnnotationFixer, PhpdocAlignFixer, PhpdocAlignFixer, PhpdocAnnotationWithoutDotFixer, PhpdocInlineTagFixer, PhpdocLineSpanFixer, PhpdocNoAccessFixer, PhpdocNoAliasTagFixer, PhpdocNoEmptyReturnFixer, PhpdocNoPackageFixer, PhpdocNoUselessInheritdocFixer, PhpdocOrderByValueFixer, PhpdocOrderFixer, PhpdocReturnSelfReferenceFixer, PhpdocSeparationFixer, PhpdocSingleLineVarSpacingFixer, PhpdocSummaryFixer, PhpdocToCommentFixer, PhpdocToParamTypeFixer, PhpdocToPropertyTypeFixer, PhpdocToReturnTypeFixer, PhpdocTrimConsecutiveBlankLineSeparationFixer, PhpdocTrimFixer, PhpdocTypesOrderFixer, PhpdocVarAnnotationCorrectOrderFixer, PhpdocVarWithoutNameFixer.
*/
public function getPriority()
{
Expand Down
8 changes: 6 additions & 2 deletions src/Fixer/FunctionNotation/PhpdocToParamTypeFixer.php
Expand Up @@ -99,9 +99,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
230 changes: 230 additions & 0 deletions src/Fixer/FunctionNotation/PhpdocToPropertyTypeFixer.php
@@ -0,0 +1,230 @@
<?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,
'This rule is EXPERIMENTAL and [1] 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) will not be fixed. [3] Manual actions might be required for newly typed properties that are read before initialization.'
);
}

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

/**
* {@inheritdoc}
*
* Must run before PhpdocAlignFixer.
* Must run after CommentToPhpdocFixer, PhpdocIndentFixer, PhpdocScalarFixer, PhpdocToCommentFixer, PhpdocTypesFixer.
*/
public function getPriority()
{
return parent::getPriority();
}

/**
* {@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, ['{', ';']);

if ($tokens[$index]->equals('{')) {
$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;
$classEndIndex += \count($newTokens);
}
}

/**
* @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;
}
}
9 changes: 7 additions & 2 deletions src/Fixer/FunctionNotation/PhpdocToReturnTypeFixer.php
Expand Up @@ -76,7 +76,7 @@ function my_foo()
),
],
null,
'This rule is EXPERIMENTAL and [1] is not covered with backward compatibility promise. [2] `@return` annotation is mandatory for the fixer to make changes, signatures of methods 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.'
'This rule is EXPERIMENTAL and [1] is not covered with backward compatibility promise. [2] `@return` annotation is mandatory for the fixer to make changes, signatures of methods without it (no docblock, inheritdocs) will not be fixed. [3] Manual actions are required if inherited signatures are not properly documented.'
);
}

Expand Down Expand Up @@ -121,7 +121,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
2 changes: 1 addition & 1 deletion src/Fixer/Phpdoc/PhpdocAlignFixer.php
Expand Up @@ -146,7 +146,7 @@ public function getDefinition()
/**
* {@inheritdoc}
*
* Must run after CommentToPhpdocFixer, CommentToPhpdocFixer, GeneralPhpdocAnnotationRemoveFixer, NoBlankLinesAfterPhpdocFixer, NoEmptyPhpdocFixer, NoSuperfluousPhpdocTagsFixer, PhpdocAddMissingParamAnnotationFixer, PhpdocAddMissingParamAnnotationFixer, PhpdocAnnotationWithoutDotFixer, PhpdocIndentFixer, PhpdocIndentFixer, PhpdocInlineTagFixer, PhpdocLineSpanFixer, PhpdocNoAccessFixer, PhpdocNoAliasTagFixer, PhpdocNoEmptyReturnFixer, PhpdocNoPackageFixer, PhpdocNoUselessInheritdocFixer, PhpdocOrderByValueFixer, PhpdocOrderFixer, PhpdocReturnSelfReferenceFixer, PhpdocScalarFixer, PhpdocScalarFixer, PhpdocSeparationFixer, PhpdocSingleLineVarSpacingFixer, PhpdocSummaryFixer, PhpdocToCommentFixer, PhpdocToCommentFixer, PhpdocToParamTypeFixer, PhpdocToReturnTypeFixer, PhpdocTrimConsecutiveBlankLineSeparationFixer, PhpdocTrimFixer, PhpdocTypesFixer, PhpdocTypesFixer, PhpdocTypesOrderFixer, PhpdocVarAnnotationCorrectOrderFixer, PhpdocVarWithoutNameFixer.
* Must run after CommentToPhpdocFixer, CommentToPhpdocFixer, GeneralPhpdocAnnotationRemoveFixer, NoBlankLinesAfterPhpdocFixer, NoEmptyPhpdocFixer, NoSuperfluousPhpdocTagsFixer, PhpdocAddMissingParamAnnotationFixer, PhpdocAddMissingParamAnnotationFixer, PhpdocAnnotationWithoutDotFixer, PhpdocIndentFixer, PhpdocIndentFixer, PhpdocInlineTagFixer, PhpdocLineSpanFixer, PhpdocNoAccessFixer, PhpdocNoAliasTagFixer, PhpdocNoEmptyReturnFixer, PhpdocNoPackageFixer, PhpdocNoUselessInheritdocFixer, PhpdocOrderByValueFixer, PhpdocOrderFixer, PhpdocReturnSelfReferenceFixer, PhpdocScalarFixer, PhpdocScalarFixer, PhpdocSeparationFixer, PhpdocSingleLineVarSpacingFixer, PhpdocSummaryFixer, PhpdocToCommentFixer, PhpdocToCommentFixer, PhpdocToParamTypeFixer, PhpdocToPropertyTypeFixer, PhpdocToReturnTypeFixer, PhpdocTrimConsecutiveBlankLineSeparationFixer, PhpdocTrimFixer, PhpdocTypesFixer, PhpdocTypesFixer, PhpdocTypesOrderFixer, PhpdocVarAnnotationCorrectOrderFixer, PhpdocVarWithoutNameFixer.
*/
public function getPriority()
{
Expand Down
2 changes: 1 addition & 1 deletion src/Fixer/Phpdoc/PhpdocIndentFixer.php
Expand Up @@ -48,7 +48,7 @@ class DocBlocks
/**
* {@inheritdoc}
*
* Must run before GeneralPhpdocAnnotationRemoveFixer, NoBlankLinesAfterPhpdocFixer, NoEmptyPhpdocFixer, NoSuperfluousPhpdocTagsFixer, PhpdocAddMissingParamAnnotationFixer, PhpdocAlignFixer, PhpdocAlignFixer, PhpdocAnnotationWithoutDotFixer, PhpdocInlineTagFixer, PhpdocLineSpanFixer, PhpdocNoAccessFixer, PhpdocNoAliasTagFixer, PhpdocNoEmptyReturnFixer, PhpdocNoPackageFixer, PhpdocNoUselessInheritdocFixer, PhpdocOrderByValueFixer, PhpdocOrderFixer, PhpdocReturnSelfReferenceFixer, PhpdocSeparationFixer, PhpdocSingleLineVarSpacingFixer, PhpdocSummaryFixer, PhpdocToParamTypeFixer, PhpdocToReturnTypeFixer, PhpdocTrimConsecutiveBlankLineSeparationFixer, PhpdocTrimFixer, PhpdocTypesFixer, PhpdocTypesOrderFixer, PhpdocVarAnnotationCorrectOrderFixer, PhpdocVarWithoutNameFixer.
* Must run before GeneralPhpdocAnnotationRemoveFixer, NoBlankLinesAfterPhpdocFixer, NoEmptyPhpdocFixer, NoSuperfluousPhpdocTagsFixer, PhpdocAddMissingParamAnnotationFixer, PhpdocAlignFixer, PhpdocAlignFixer, PhpdocAnnotationWithoutDotFixer, PhpdocInlineTagFixer, PhpdocLineSpanFixer, PhpdocNoAccessFixer, PhpdocNoAliasTagFixer, PhpdocNoEmptyReturnFixer, PhpdocNoPackageFixer, PhpdocNoUselessInheritdocFixer, PhpdocOrderByValueFixer, PhpdocOrderFixer, PhpdocReturnSelfReferenceFixer, PhpdocSeparationFixer, PhpdocSingleLineVarSpacingFixer, PhpdocSummaryFixer, PhpdocToParamTypeFixer, PhpdocToPropertyTypeFixer, PhpdocToReturnTypeFixer, PhpdocTrimConsecutiveBlankLineSeparationFixer, PhpdocTrimFixer, PhpdocTypesFixer, PhpdocTypesOrderFixer, PhpdocVarAnnotationCorrectOrderFixer, PhpdocVarWithoutNameFixer.
* Must run after IndentationTypeFixer, PhpdocToCommentFixer.
*/
public function getPriority()
Expand Down
2 changes: 1 addition & 1 deletion src/Fixer/Phpdoc/PhpdocScalarFixer.php
Expand Up @@ -65,7 +65,7 @@ function sample($a, $b, $c)
/**
* {@inheritdoc}
*
* Must run before GeneralPhpdocAnnotationRemoveFixer, NoBlankLinesAfterPhpdocFixer, NoEmptyPhpdocFixer, NoSuperfluousPhpdocTagsFixer, PhpdocAddMissingParamAnnotationFixer, PhpdocAlignFixer, PhpdocAlignFixer, PhpdocInlineTagFixer, PhpdocLineSpanFixer, PhpdocNoAccessFixer, PhpdocNoAliasTagFixer, PhpdocNoEmptyReturnFixer, PhpdocNoPackageFixer, PhpdocNoUselessInheritdocFixer, PhpdocOrderByValueFixer, PhpdocOrderFixer, PhpdocReturnSelfReferenceFixer, PhpdocSeparationFixer, PhpdocSingleLineVarSpacingFixer, PhpdocSummaryFixer, PhpdocToParamTypeFixer, PhpdocToReturnTypeFixer, PhpdocToReturnTypeFixer, PhpdocTrimConsecutiveBlankLineSeparationFixer, PhpdocTrimFixer, PhpdocTypesOrderFixer, PhpdocVarAnnotationCorrectOrderFixer, PhpdocVarWithoutNameFixer.
* Must run before GeneralPhpdocAnnotationRemoveFixer, NoBlankLinesAfterPhpdocFixer, NoEmptyPhpdocFixer, NoSuperfluousPhpdocTagsFixer, PhpdocAddMissingParamAnnotationFixer, PhpdocAlignFixer, PhpdocAlignFixer, PhpdocInlineTagFixer, PhpdocLineSpanFixer, PhpdocNoAccessFixer, PhpdocNoAliasTagFixer, PhpdocNoEmptyReturnFixer, PhpdocNoPackageFixer, PhpdocNoUselessInheritdocFixer, PhpdocOrderByValueFixer, PhpdocOrderFixer, PhpdocReturnSelfReferenceFixer, PhpdocSeparationFixer, PhpdocSingleLineVarSpacingFixer, PhpdocSummaryFixer, PhpdocToParamTypeFixer, PhpdocToPropertyTypeFixer, PhpdocToReturnTypeFixer, PhpdocToReturnTypeFixer, PhpdocTrimConsecutiveBlankLineSeparationFixer, PhpdocTrimFixer, PhpdocTypesOrderFixer, PhpdocVarAnnotationCorrectOrderFixer, PhpdocVarWithoutNameFixer.
* Must run after PhpdocTypesFixer.
*/
public function getPriority()
Expand Down

0 comments on commit 7b00b95

Please sign in to comment.