Skip to content

Commit

Permalink
Merge branch 'master' into 3.0
Browse files Browse the repository at this point in the history
# Conflicts:
#	src/AbstractPhpdocToTypeDeclarationFixer.php
#	src/Fixer/Comment/CommentToPhpdocFixer.php
#	src/Fixer/Phpdoc/AlignMultilineCommentFixer.php
#	src/Fixer/Phpdoc/PhpdocAlignFixer.php
#	src/Fixer/Phpdoc/PhpdocIndentFixer.php
#	src/Fixer/Phpdoc/PhpdocScalarFixer.php
#	src/Fixer/Phpdoc/PhpdocToCommentFixer.php
#	src/Fixer/Phpdoc/PhpdocTypesFixer.php
  • Loading branch information
keradus committed May 3, 2021
2 parents fc683a2 + 5698474 commit 33e3db3
Show file tree
Hide file tree
Showing 17 changed files with 832 additions and 21 deletions.
67 changes: 67 additions & 0 deletions doc/rules/function_notation/phpdoc_to_property_type.rst
@@ -0,0 +1,67 @@
================================
Rule ``phpdoc_to_property_type``
================================

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

.. warning:: Using this rule is risky.

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
-------------

``scalar_types``
~~~~~~~~~~~~~~~~

Fix also scalar types; may have unexpected behaviour due to PHP bad type
coercion system.

Allowed types: ``bool``

Default value: ``true``

Examples
--------

Example #1
~~~~~~~~~~

*Default* configuration.

.. code-block:: diff
--- Original
+++ New
<?php
class Foo {
/** @var int */
- private $foo;
+ private int $foo;
/** @var \Traversable */
- private $bar;
+ private \Traversable $bar;
}
Example #2
~~~~~~~~~~

With configuration: ``['scalar_types' => false]``.

.. code-block:: diff
--- Original
+++ New
<?php
class Foo {
/** @var int */
private $foo;
/** @var \Traversable */
- private $bar;
+ private \Traversable $bar;
}
2 changes: 1 addition & 1 deletion doc/rules/function_notation/phpdoc_to_return_type.rst
Expand Up @@ -11,7 +11,7 @@ accordingly the function signature. Requires PHP >= 7.0.
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.
properly documented.

Configuration
-------------
Expand Down
2 changes: 2 additions & 0 deletions doc/rules/index.rst
Expand Up @@ -237,6 +237,8 @@ Function Notation
Adds or removes ``?`` before type declarations for parameters with a default ``null`` value.
- `phpdoc_to_param_type <./function_notation/phpdoc_to_param_type.rst>`_ *(risky)*
EXPERIMENTAL: Takes ``@param`` annotations of non-mixed types and adjusts accordingly the function signature. Requires PHP >= 7.0.
- `phpdoc_to_property_type <./function_notation/phpdoc_to_property_type.rst>`_ *(risky)*
EXPERIMENTAL: Takes ``@var`` annotation of non-mixed types and adjusts accordingly the property signature. Requires PHP >= 7.4.
- `phpdoc_to_return_type <./function_notation/phpdoc_to_return_type.rst>`_ *(risky)*
EXPERIMENTAL: Takes ``@return`` annotation of non-mixed types and adjusts accordingly the function signature. Requires PHP >= 7.0.
- `regular_callable_call <./function_notation/regular_callable_call.rst>`_ *(risky)*
Expand Down
22 changes: 14 additions & 8 deletions src/AbstractPhpdocToTypeDeclarationFixer.php
Expand Up @@ -85,11 +85,9 @@ protected function createConfigurationDefinition(): FixerConfigurationResolverIn
}

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

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

return null;
}

/**
* @return Annotation[]
*/
protected function getAnnotationsFromDocComment(string $name, Tokens $tokens, int $docCommentIndex): array
{
$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 @@ -58,7 +58,7 @@ public function isRisky(): bool
/**
* {@inheritdoc}
*
* Must run before GeneralPhpdocAnnotationRemoveFixer, GeneralPhpdocTagRenameFixer, NoBlankLinesAfterPhpdocFixer, NoEmptyPhpdocFixer, NoSuperfluousPhpdocTagsFixer, PhpdocAddMissingParamAnnotationFixer, PhpdocAlignFixer, PhpdocAlignFixer, PhpdocAnnotationWithoutDotFixer, PhpdocInlineTagNormalizerFixer, PhpdocLineSpanFixer, PhpdocNoAccessFixer, PhpdocNoAliasTagFixer, PhpdocNoEmptyReturnFixer, PhpdocNoPackageFixer, PhpdocNoUselessInheritdocFixer, PhpdocOrderByValueFixer, PhpdocOrderFixer, PhpdocReturnSelfReferenceFixer, PhpdocSeparationFixer, PhpdocSingleLineVarSpacingFixer, PhpdocSummaryFixer, PhpdocTagCasingFixer, PhpdocTagTypeFixer, PhpdocToCommentFixer, PhpdocToParamTypeFixer, PhpdocToReturnTypeFixer, PhpdocTrimConsecutiveBlankLineSeparationFixer, PhpdocTrimFixer, PhpdocTypesOrderFixer, PhpdocVarAnnotationCorrectOrderFixer, PhpdocVarWithoutNameFixer.
* Must run before GeneralPhpdocAnnotationRemoveFixer, GeneralPhpdocTagRenameFixer, NoBlankLinesAfterPhpdocFixer, NoEmptyPhpdocFixer, NoSuperfluousPhpdocTagsFixer, PhpdocAddMissingParamAnnotationFixer, PhpdocAlignFixer, PhpdocAlignFixer, PhpdocAnnotationWithoutDotFixer, PhpdocInlineTagNormalizerFixer, PhpdocLineSpanFixer, PhpdocNoAccessFixer, PhpdocNoAliasTagFixer, PhpdocNoEmptyReturnFixer, PhpdocNoPackageFixer, PhpdocNoUselessInheritdocFixer, PhpdocOrderByValueFixer, PhpdocOrderFixer, PhpdocReturnSelfReferenceFixer, PhpdocSeparationFixer, PhpdocSingleLineVarSpacingFixer, PhpdocSummaryFixer, PhpdocTagCasingFixer, PhpdocTagTypeFixer, PhpdocToCommentFixer, PhpdocToParamTypeFixer, PhpdocToPropertyTypeFixer, PhpdocToReturnTypeFixer, PhpdocTrimConsecutiveBlankLineSeparationFixer, PhpdocTrimFixer, PhpdocTypesOrderFixer, PhpdocVarAnnotationCorrectOrderFixer, PhpdocVarWithoutNameFixer.
* Must run after AlignMultilineCommentFixer.
*/
public function getPriority(): int
Expand Down
8 changes: 6 additions & 2 deletions src/Fixer/FunctionNotation/PhpdocToParamTypeFixer.php
Expand Up @@ -130,9 +130,13 @@ protected function applyFix(\SplFileInfo $file, Tokens $tokens): void
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, false);

if (null === $typeInfo) {
Expand Down
242 changes: 242 additions & 0 deletions src/Fixer/FunctionNotation/PhpdocToPropertyTypeFixer.php
@@ -0,0 +1,242 @@
<?php

declare(strict_types=1);

/*
* 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\FixerDefinitionInterface;
use PhpCsFixer\FixerDefinition\VersionSpecification;
use PhpCsFixer\FixerDefinition\VersionSpecificCodeSample;
use PhpCsFixer\Tokenizer\Token;
use PhpCsFixer\Tokenizer\Tokens;

final class PhpdocToPropertyTypeFixer extends AbstractPhpdocToTypeDeclarationFixer
{
/**
* @var array<string, true>
*/
private $skippedTypes = [
'mixed' => true,
'resource' => true,
'null' => true,
];

/**
* {@inheritdoc}
*/
public function getDefinition(): FixerDefinitionInterface
{
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): bool
{
return \PHP_VERSION_ID >= 70400 && $tokens->isTokenKindFound(T_DOC_COMMENT);
}

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

protected function isSkippedType(string $type): bool
{
return isset($this->skippedTypes[$type]);
}

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

private function fixClass(Tokens $tokens, int $index): void
{
$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);
}
}

/**
* @return array<string, int>
*/
private function findNextUntypedPropertiesDeclaration(Tokens $tokens, int $index): array
{
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): ?array
{
$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, false);

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;
}
}

0 comments on commit 33e3db3

Please sign in to comment.