Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor conversion of PHPDoc to type declarations #4591

Merged
merged 1 commit into from May 3, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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