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

NoSuperfluousPhpdocTags - Add remove_inheritdoc option #4500

Merged
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
1 change: 1 addition & 0 deletions README.rst
Expand Up @@ -1122,6 +1122,7 @@ Choose from the list of available rules:

- ``allow_mixed`` (``bool``): whether type ``mixed`` without description is allowed
(``true``) or considered superfluous (``false``); defaults to ``false``
- ``remove_inheritdoc`` (``bool``): remove ``@inheritDoc`` tags; defaults to ``false``
julienfalque marked this conversation as resolved.
Show resolved Hide resolved

* **no_trailing_comma_in_list_call** [@Symfony, @PhpCsFixer]

Expand Down
188 changes: 154 additions & 34 deletions src/Fixer/Phpdoc/NoSuperfluousPhpdocTagsFixer.php
Expand Up @@ -67,6 +67,14 @@ class Foo {
public function doFoo(Bar $bar, $baz): Baz {}
}
', new VersionSpecification(70000)),
new CodeSample('<?php
class Foo {
/**
* @inheritDoc
*/
public function doFoo(Bar $bar, $baz) {}
}
', ['remove_inheritdoc' => true]),
]
);
}
Expand Down Expand Up @@ -105,46 +113,27 @@ protected function applyFix(\SplFileInfo $file, Tokens $tokens)
continue;
}

$functionIndex = $this->findDocumentedFunction($tokens, $index);
if (null === $functionIndex) {
continue;
}

$docBlock = new DocBlock($token->getContent());
$content = $initialContent = $token->getContent();

$openingParenthesisIndex = $tokens->getNextTokenOfKind($functionIndex, ['(']);
$closingParenthesisIndex = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_PARENTHESIS_BRACE, $openingParenthesisIndex);
$documentedElementIndex = $this->findDocumentedElement($tokens, $index);

$argumentsInfo = $this->getArgumentsInfo(
$tokens,
$openingParenthesisIndex + 1,
$closingParenthesisIndex - 1
);

foreach ($docBlock->getAnnotationsOfType('param') as $annotation) {
if (0 === Preg::match('/@param(?:\s+[^\$]\S+)?\s+(\$\S+)/', $annotation->getContent(), $matches)) {
continue;
}
if (null === $documentedElementIndex) {
continue;
}

$argumentName = $matches[1];
$token = $tokens[$documentedElementIndex];

if (
!isset($argumentsInfo[$argumentName])
|| $this->annotationIsSuperfluous($annotation, $argumentsInfo[$argumentName], $shortNames)
) {
$annotation->remove();
}
if ($token->isGivenKind(T_FUNCTION)) {
$content = $this->fixFunctionDocComment($content, $tokens, $index, $shortNames);
}

$returnTypeInfo = $this->getReturnTypeInfo($tokens, $closingParenthesisIndex);

foreach ($docBlock->getAnnotationsOfType('return') as $annotation) {
if ($this->annotationIsSuperfluous($annotation, $returnTypeInfo, $shortNames)) {
$annotation->remove();
}
if ($this->configuration['remove_inheritdoc']) {
$content = $this->removeSuperfluousInheritDoc($content);
}

$tokens[$index] = new Token([T_DOC_COMMENT, $docBlock->getContent()]);
if ($content !== $initialContent) {
$tokens[$index] = new Token([T_DOC_COMMENT, $content]);
}
}
}

Expand All @@ -158,22 +147,97 @@ protected function createConfigurationDefinition()
->setAllowedTypes(['bool'])
->setDefault(false)
->getOption(),
(new FixerOptionBuilder('remove_inheritdoc', 'Remove `@inheritDoc` tags'))
->setAllowedTypes(['bool'])
->setDefault(false)
->getOption(),
]);
}

private function findDocumentedFunction(Tokens $tokens, $index)
/**
* @param Tokens $tokens
* @param int $docCommentIndex
*
* @return null|int
*/
private function findDocumentedElement(Tokens $tokens, $docCommentIndex)
{
$index = $docCommentIndex;

do {
$index = $tokens->getNextMeaningfulToken($index);

if (null === $index || $tokens[$index]->isGivenKind(T_FUNCTION)) {
if (null === $index || $tokens[$index]->isGivenKind([T_FUNCTION, T_CLASS, T_INTERFACE])) {
return $index;
}
} while ($tokens[$index]->isGivenKind([T_ABSTRACT, T_FINAL, T_STATIC, T_PRIVATE, T_PROTECTED, T_PUBLIC]));

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

$kindsBeforeProperty = [T_STATIC, T_PRIVATE, T_PROTECTED, T_PUBLIC];

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

do {
$index = $tokens->getNextMeaningfulToken($index);

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

return null;
}

/**
* @param string $content
* @param Tokens $tokens
* @param int $functionIndex
* @param array $shortNames
*
* @return string
*/
private function fixFunctionDocComment($content, Tokens $tokens, $functionIndex, array $shortNames)
{
$docBlock = new DocBlock($content);

$openingParenthesisIndex = $tokens->getNextTokenOfKind($functionIndex, ['(']);
$closingParenthesisIndex = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_PARENTHESIS_BRACE, $openingParenthesisIndex);

$argumentsInfo = $this->getArgumentsInfo(
$tokens,
$openingParenthesisIndex + 1,
$closingParenthesisIndex - 1
);

foreach ($docBlock->getAnnotationsOfType('param') as $annotation) {
if (0 === Preg::match('/@param(?:\s+[^\$]\S+)?\s+(\$\S+)/', $annotation->getContent(), $matches)) {
continue;
}

$argumentName = $matches[1];

if (
!isset($argumentsInfo[$argumentName])
|| $this->annotationIsSuperfluous($annotation, $argumentsInfo[$argumentName], $shortNames)
) {
$annotation->remove();
}
}

$returnTypeInfo = $this->getReturnTypeInfo($tokens, $closingParenthesisIndex);

foreach ($docBlock->getAnnotationsOfType('return') as $annotation) {
if ($this->annotationIsSuperfluous($annotation, $returnTypeInfo, $shortNames)) {
$annotation->remove();
}
}

return $docBlock->getContent();
}

/**
* @param Tokens $tokens
* @param int $start
Expand Down Expand Up @@ -327,4 +391,60 @@ function ($type) use ($symbolShortNames) {

return $normalized;
}

/**
* @param string $docComment
*
* @return string
*/
private function removeSuperfluousInheritDoc($docComment)
{
return Preg::replace('~
# $1: before @inheritDoc tag
(
# beginning of comment or a PHPDoc tag
(?:
^/\*\*
(?:
\R
[ \t]*(?:\*[ \t]*)?
)*?
|
@\N+
)

# empty comment lines
(?:
\R
[ \t]*(?:\*[ \t]*?)?
)*
)

# spaces before @inheritDoc tag
[ \t]*

# @inheritDoc tag
(?:@inheritDocs?|\{@inheritDocs?\})

# $2: after @inheritDoc tag
(
# empty comment lines
(?:
\R
[ \t]*(?:\*[ \t]*)?
)*

# a PHPDoc tag or end of comment
(?:
@\N+
|
(?:
\R
[ \t]*(?:\*[ \t]*)?
)*
[ \t]*\*/$
)
)
~ix', '$1$2', $docComment);
}
}