Skip to content

Commit

Permalink
minor #4591 Refactor conversion of PHPDoc to type declarations (julie…
Browse files Browse the repository at this point in the history
…nfalque, keradus)

This PR was squashed before being merged into the 2.18 branch.

Discussion
----------

Refactor conversion of PHPDoc to type declarations

This PR:
* refactors parts of `PhpdocToParamTypeFixer` and `PhpdocToReturnTypeFixer` to ease sharing common logic.
* implements some points of #4511

Commits
-------

462260d Refactor conversion of PHPDoc to type declarations
  • Loading branch information
keradus committed May 3, 2021
2 parents 4961df4 + 462260d commit db7e1f1
Show file tree
Hide file tree
Showing 16 changed files with 1,148 additions and 486 deletions.
33 changes: 33 additions & 0 deletions doc/rules/function_notation/phpdoc_to_param_type.rst
Expand Up @@ -13,12 +13,27 @@ accordingly the function signature. Requires PHP >= 7.0.
be fixed. [3] Manual actions are required if inherited signatures are not
properly documented.

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
Expand All @@ -33,6 +48,8 @@ Example #1
Example #2
~~~~~~~~~~

*Default* configuration.

.. code-block:: diff
--- Original
Expand All @@ -43,3 +60,19 @@ Example #2
-function my_foo($bar)
+function my_foo(?string $bar)
{}
Example #3
~~~~~~~~~~

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

.. code-block:: diff
--- Original
+++ New
<?php
/** @param Foo $foo */
-function foo($foo) {}
+function foo(Foo $foo) {}
/** @param string $foo */
function bar($foo) {}
194 changes: 193 additions & 1 deletion src/AbstractPhpdocToTypeDeclarationFixer.php
Expand Up @@ -12,18 +12,210 @@

namespace PhpCsFixer;

use PhpCsFixer\DocBlock\Annotation;
use PhpCsFixer\DocBlock\DocBlock;
use PhpCsFixer\Fixer\ConfigurationDefinitionFixerInterface;
use PhpCsFixer\FixerConfiguration\FixerConfigurationResolver;
use PhpCsFixer\FixerConfiguration\FixerOptionBuilder;
use PhpCsFixer\Tokenizer\Analyzer\NamespacesAnalyzer;
use PhpCsFixer\Tokenizer\Analyzer\NamespaceUsesAnalyzer;
use PhpCsFixer\Tokenizer\CT;
use PhpCsFixer\Tokenizer\Token;
use PhpCsFixer\Tokenizer\Tokens;

/**
* @internal
*/
abstract class AbstractPhpdocToTypeDeclarationFixer extends AbstractFixer
abstract class AbstractPhpdocToTypeDeclarationFixer extends AbstractFixer implements ConfigurationDefinitionFixerInterface
{
/**
* @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,
'mixed' => 80000,
];

/**
* @var array<string, bool>
*/
private $scalarTypes = [
'bool' => true,
'float' => true,
'int' => true,
'string' => true,
];

/**
* @var array<string, bool>
*/
private static $syntaxValidationCache = [];

/**
* {@inheritdoc}
*/
public function isRisky()
{
return true;
}

/**
* @param string $type
*
* @return bool
*/
abstract protected function isSkippedType($type);

/**
* {@inheritdoc}
*/
protected function createConfigurationDefinition()
{
return new FixerConfigurationResolver([
(new FixerOptionBuilder('scalar_types', 'Fix also scalar types; may have unexpected behaviour due to PHP bad type coercion system.'))
->setAllowedTypes(['bool'])
->setDefault(true)
->getOption(),
]);
}

/**
* Find all the annotations of given type in the function's PHPDoc comment.
*
* @param string $name
* @param int $index The index of the function token
*
* @return Annotation[]
*/
protected function findAnnotations($name, Tokens $tokens, $index)
{
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 [];
}

$namespacesAnalyzer = new NamespacesAnalyzer();
$namespace = $namespacesAnalyzer->getNamespaceAt($tokens, $index);

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

$doc = new DocBlock(
$tokens[$index]->getContent(),
$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'],
'static' => [T_STATIC, 'static'],
];

$newTokens = [];

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

if (isset($specialTypes[$type])) {
$newTokens[] = new Token($specialTypes[$type]);
} else {
$typeUnqualified = ltrim($type, '\\');

if (isset($this->scalarTypes[$typeUnqualified]) || isset($this->versionSpecificTypes[$typeUnqualified])) {
// 'scalar's, 'void', 'iterable' and 'object' must be unqualified
$newTokens[] = new Token([T_STRING, $typeUnqualified]);
} 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 bool $isReturnType
*
* @return null|array
*/
protected function getCommonTypeFromAnnotation(Annotation $annotation, $isReturnType)
{
$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 && (!$isReturnType || \PHP_VERSION_ID < 80000)) {
$commonType = 'self';
}

if ($this->isSkippedType($commonType)) {
return null;
}

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

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

return [$commonType, $isNullable];
}

final protected function isValidSyntax($code)
{
if (!isset(self::$syntaxValidationCache[$code])) {
Expand Down

0 comments on commit db7e1f1

Please sign in to comment.