File "HeaderCommentFixer.php"
Full Path: /var/www/html/back/vendor/friendsofphp/php-cs-fixer/src/Fixer/Comment/HeaderCommentFixer.php
File size: 19.32 KB
MIME-type: text/x-php
Charset: utf-8
<?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\Comment;
use PhpCsFixer\AbstractFixer;
use PhpCsFixer\ConfigurationException\InvalidFixerConfigurationException;
use PhpCsFixer\Fixer\ConfigurableFixerInterface;
use PhpCsFixer\Fixer\ConfigurableFixerTrait;
use PhpCsFixer\Fixer\WhitespacesAwareFixerInterface;
use PhpCsFixer\FixerConfiguration\FixerConfigurationResolver;
use PhpCsFixer\FixerConfiguration\FixerConfigurationResolverInterface;
use PhpCsFixer\FixerConfiguration\FixerOptionBuilder;
use PhpCsFixer\FixerDefinition\CodeSample;
use PhpCsFixer\FixerDefinition\FixerDefinition;
use PhpCsFixer\FixerDefinition\FixerDefinitionInterface;
use PhpCsFixer\Preg;
use PhpCsFixer\PregException;
use PhpCsFixer\Tokenizer\Token;
use PhpCsFixer\Tokenizer\Tokens;
use Symfony\Component\OptionsResolver\Options;
/**
* @phpstan-type _AutogeneratedInputConfiguration array{
* comment_type?: 'PHPDoc'|'comment',
* header: string,
* location?: 'after_declare_strict'|'after_open',
* separate?: 'both'|'bottom'|'none'|'top',
* validator?: null|string,
* }
* @phpstan-type _AutogeneratedComputedConfiguration array{
* comment_type: 'PHPDoc'|'comment',
* header: string,
* location: 'after_declare_strict'|'after_open',
* separate: 'both'|'bottom'|'none'|'top',
* validator: null|string,
* }
*
* @implements ConfigurableFixerInterface<_AutogeneratedInputConfiguration, _AutogeneratedComputedConfiguration>
*
* @author Antonio J. García Lagar <aj@garcialagar.es>
*
* @no-named-arguments Parameter names are not covered by the backward compatibility promise.
*/
final class HeaderCommentFixer extends AbstractFixer implements ConfigurableFixerInterface, WhitespacesAwareFixerInterface
{
/** @use ConfigurableFixerTrait<_AutogeneratedInputConfiguration, _AutogeneratedComputedConfiguration> */
use ConfigurableFixerTrait;
/**
* @internal
*/
public const HEADER_PHPDOC = 'PHPDoc';
/**
* @internal
*/
public const HEADER_COMMENT = 'comment';
public function getDefinition(): FixerDefinitionInterface
{
return new FixerDefinition(
'Add, replace or remove header comment.',
[
new CodeSample(
<<<'PHP'
<?php
declare(strict_types=1);
namespace A\B;
echo 1;
PHP,
[
'header' => 'Made with love.',
],
),
new CodeSample(
<<<'PHP'
<?php
declare(strict_types=1);
namespace A\B;
echo 1;
PHP,
[
'header' => 'Made with love.',
'comment_type' => self::HEADER_PHPDOC,
'location' => 'after_open',
'separate' => 'bottom',
],
),
new CodeSample(
<<<'PHP'
<?php
declare(strict_types=1);
namespace A\B;
echo 1;
PHP,
[
'header' => 'Made with love.',
'comment_type' => self::HEADER_COMMENT,
'location' => 'after_declare_strict',
],
),
new CodeSample(
<<<'PHP'
<?php
declare(strict_types=1);
/*
* Made with love.
*
* Extra content.
*/
namespace A\B;
echo 1;
PHP,
[
'header' => 'Made with love.',
'validator' => '/Made with love(?P<EXTRA>.*)??/s',
'comment_type' => self::HEADER_COMMENT,
'location' => 'after_declare_strict',
],
),
new CodeSample(
<<<'PHP'
<?php
declare(strict_types=1);
/*
* Comment is not wanted here.
*/
namespace A\B;
echo 1;
PHP,
[
'header' => '',
],
),
],
);
}
public function isCandidate(Tokens $tokens): bool
{
return $tokens->isMonolithicPhp() && !$tokens->isTokenKindFound(\T_OPEN_TAG_WITH_ECHO);
}
/**
* {@inheritdoc}
*
* Must run before BlankLinesBeforeNamespaceFixer, SingleBlankLineBeforeNamespaceFixer, SingleLineCommentStyleFixer.
* Must run after DeclareStrictTypesFixer, NoBlankLinesAfterPhpdocFixer.
*/
public function getPriority(): int
{
// When this fixer is configured with ["separate" => "bottom", "comment_type" => "PHPDoc"]
// and the target file has no namespace or declare() construct,
// the fixed header comment gets trimmed by NoBlankLinesAfterPhpdocFixer if we run before it.
return -30;
}
protected function applyFix(\SplFileInfo $file, Tokens $tokens): void
{
$headerAsComment = $this->getHeaderAsComment();
$location = $this->configuration['location'];
$locationIndices = [];
foreach (['after_open', 'after_declare_strict'] as $possibleLocation) {
$locationIndex = $this->findHeaderCommentInsertionIndex($tokens, $possibleLocation);
if (!isset($locationIndices[$locationIndex]) || $possibleLocation === $location) {
$locationIndices[$locationIndex] = $possibleLocation;
}
}
// pre-run to find existing comment, if dynamic content is allowed
if (null !== $this->configuration['validator']) {
foreach ($locationIndices as $possibleLocation) {
// figure out where the comment should be placed
$headerNewIndex = $this->findHeaderCommentInsertionIndex($tokens, $possibleLocation);
// check if there is already a comment
$headerCurrentIndex = $this->findHeaderCommentCurrentIndex($tokens, $headerAsComment, $headerNewIndex - 1);
if (null === $headerCurrentIndex) {
continue;
}
$currentHeaderComment = $tokens[$headerCurrentIndex]->getContent();
if ($this->doesTokenFulfillValidator($tokens[$headerCurrentIndex])) {
$headerAsComment = $currentHeaderComment;
}
}
}
foreach ($locationIndices as $possibleLocation) {
// figure out where the comment should be placed
$headerNewIndex = $this->findHeaderCommentInsertionIndex($tokens, $possibleLocation);
// check if there is already a comment
$headerCurrentIndex = $this->findHeaderCommentCurrentIndex($tokens, $headerAsComment, $headerNewIndex - 1);
if (null === $headerCurrentIndex) {
if ('' === $this->configuration['header'] || $possibleLocation !== $location) {
continue;
}
$this->insertHeader($tokens, $headerAsComment, $headerNewIndex);
continue;
}
$currentHeaderComment = $tokens[$headerCurrentIndex]->getContent();
$sameComment = $headerAsComment === $currentHeaderComment;
$expectedLocation = $possibleLocation === $location;
if (!$sameComment || !$expectedLocation) {
if ($expectedLocation xor $sameComment) {
$this->removeHeader($tokens, $headerCurrentIndex);
}
if ('' === $this->configuration['header']) {
continue;
}
if ($possibleLocation === $location) {
$this->insertHeader($tokens, $headerAsComment, $headerNewIndex);
}
continue;
}
$this->fixWhiteSpaceAroundHeader($tokens, $headerCurrentIndex);
}
}
protected function createConfigurationDefinition(): FixerConfigurationResolverInterface
{
$fixerName = $this->getName();
return new FixerConfigurationResolver([
(new FixerOptionBuilder('header', 'Proper header content.'))
->setAllowedTypes(['string'])
->setNormalizer(static function (Options $options, string $value) use ($fixerName): string {
if ('' === trim($value)) {
return '';
}
if (str_contains($value, '*/')) {
throw new InvalidFixerConfigurationException($fixerName, 'Cannot use \'*/\' in header.');
}
return $value;
})
->getOption(),
(new FixerOptionBuilder('validator', 'RegEx validator for header content.'))
->setAllowedTypes(['string', 'null'])
->setNormalizer(static function (Options $options, ?string $value) use ($fixerName): ?string {
if (null !== $value) {
try {
Preg::match($value, '');
} catch (PregException $exception) {
throw new InvalidFixerConfigurationException($fixerName, 'Provided RegEx is not valid.');
}
}
return $value;
})
->setDefault(null)
->getOption(),
(new FixerOptionBuilder('comment_type', 'Comment syntax type.'))
->setAllowedValues([self::HEADER_PHPDOC, self::HEADER_COMMENT])
->setDefault(self::HEADER_COMMENT)
->getOption(),
(new FixerOptionBuilder('location', 'The location of the inserted header.'))
->setAllowedValues(['after_open', 'after_declare_strict'])
->setDefault('after_declare_strict')
->getOption(),
(new FixerOptionBuilder('separate', 'Whether the header should be separated from the file content with a new line.'))
->setAllowedValues(['both', 'top', 'bottom', 'none'])
->setDefault('both')
->getOption(),
]);
}
private function doesTokenFulfillValidator(Token $token): bool
{
if (null === $this->configuration['validator']) {
throw new \LogicException(\sprintf("Cannot call '%s' method while missing config:validator.", __METHOD__));
}
$currentHeaderComment = $token->getContent();
$lines = implode("\n", array_map(
static fn (string $line): string => ' *' === $line ? '' : (str_starts_with($line, ' * ') ? substr($line, 3) : $line),
\array_slice(explode("\n", $currentHeaderComment), 1, -1),
));
return Preg::match($this->configuration['validator'], $lines);
}
/**
* Enclose the given text in a comment block.
*/
private function getHeaderAsComment(): string
{
$lineEnding = $this->whitespacesConfig->getLineEnding();
$comment = (self::HEADER_COMMENT === $this->configuration['comment_type'] ? '/*' : '/**').$lineEnding;
$lines = explode("\n", str_replace("\r", '', $this->configuration['header']));
foreach ($lines as $line) {
$comment .= rtrim(' * '.$line).$lineEnding;
}
return $comment.' */';
}
private function findHeaderCommentCurrentIndex(Tokens $tokens, string $headerAsComment, int $headerNewIndex): ?int
{
$index = $tokens->getNextNonWhitespace($headerNewIndex);
if (null === $index || !$tokens[$index]->isComment()) {
return null;
}
$next = $index + 1;
if (!isset($tokens[$next]) || \in_array($this->configuration['separate'], ['top', 'none'], true) || !$tokens[$index]->isGivenKind(\T_DOC_COMMENT)) {
return $index;
}
if ($tokens[$next]->isWhitespace()) {
if (!Preg::match('/^\h*\R\h*$/D', $tokens[$next]->getContent())) {
return $index;
}
++$next;
}
if (!isset($tokens[$next]) || !$tokens[$next]->isClassy() && !$tokens[$next]->isGivenKind(\T_FUNCTION)) {
return $index;
}
if (
$headerAsComment === $tokens[$index]->getContent()
|| (null !== $this->configuration['validator'] && $this->doesTokenFulfillValidator($tokens[$index]))
) {
return $index;
}
return null;
}
/**
* Find the index where the header comment must be inserted.
*/
private function findHeaderCommentInsertionIndex(Tokens $tokens, string $location): int
{
$openTagIndex = $tokens[0]->isGivenKind(\T_INLINE_HTML) ? 1 : 0;
if ('after_open' === $location) {
return $openTagIndex + 1;
}
$index = $tokens->getNextMeaningfulToken($openTagIndex);
if (null === $index) {
return $openTagIndex + 1; // file without meaningful tokens but an open tag, comment should always be placed directly after the open tag
}
if (!$tokens[$index]->isGivenKind(\T_DECLARE)) {
return $openTagIndex + 1;
}
$next = $tokens->getNextMeaningfulToken($index);
if (null === $next || !$tokens[$next]->equals('(')) {
return $openTagIndex + 1;
}
$next = $tokens->getNextMeaningfulToken($next);
if (null === $next || !$tokens[$next]->equals([\T_STRING, 'strict_types'], false)) {
return $openTagIndex + 1;
}
$next = $tokens->getNextMeaningfulToken($next);
if (null === $next || !$tokens[$next]->equals('=')) {
return $openTagIndex + 1;
}
$next = $tokens->getNextMeaningfulToken($next);
if (null === $next || !$tokens[$next]->isGivenKind(\T_LNUMBER)) {
return $openTagIndex + 1;
}
$next = $tokens->getNextMeaningfulToken($next);
if (null === $next || !$tokens[$next]->equals(')')) {
return $openTagIndex + 1;
}
$next = $tokens->getNextMeaningfulToken($next);
if (null === $next || !$tokens[$next]->equals(';')) { // don't insert after close tag
return $openTagIndex + 1;
}
return $next + 1;
}
private function fixWhiteSpaceAroundHeader(Tokens $tokens, int $headerIndex): void
{
$lineEnding = $this->whitespacesConfig->getLineEnding();
// fix lines after header comment
if (
('both' === $this->configuration['separate'] || 'bottom' === $this->configuration['separate'])
&& null !== $tokens->getNextMeaningfulToken($headerIndex)
) {
$expectedLineCount = 2;
} else {
$expectedLineCount = 1;
}
if ($headerIndex === \count($tokens) - 1) {
$tokens->insertAt($headerIndex + 1, new Token([\T_WHITESPACE, str_repeat($lineEnding, $expectedLineCount)]));
} else {
$lineBreakCount = $this->getLineBreakCount($tokens, $headerIndex, 1);
if ($lineBreakCount < $expectedLineCount) {
$missing = str_repeat($lineEnding, $expectedLineCount - $lineBreakCount);
if ($tokens[$headerIndex + 1]->isWhitespace()) {
$tokens[$headerIndex + 1] = new Token([\T_WHITESPACE, $missing.$tokens[$headerIndex + 1]->getContent()]);
} else {
$tokens->insertAt($headerIndex + 1, new Token([\T_WHITESPACE, $missing]));
}
} elseif ($lineBreakCount > $expectedLineCount && $tokens[$headerIndex + 1]->isWhitespace()) {
$newLinesToRemove = $lineBreakCount - $expectedLineCount;
$tokens[$headerIndex + 1] = new Token([
\T_WHITESPACE,
Preg::replace("/^\\R{{$newLinesToRemove}}/", '', $tokens[$headerIndex + 1]->getContent()),
]);
}
}
// fix lines before header comment
$expectedLineCount = 'both' === $this->configuration['separate'] || 'top' === $this->configuration['separate'] ? 2 : 1;
$prev = $tokens->getPrevNonWhitespace($headerIndex);
$regex = '/\h$/';
if ($tokens[$prev]->isGivenKind(\T_OPEN_TAG) && Preg::match($regex, $tokens[$prev]->getContent())) {
$tokens[$prev] = new Token([\T_OPEN_TAG, Preg::replace($regex, $lineEnding, $tokens[$prev]->getContent())]);
}
$lineBreakCount = $this->getLineBreakCount($tokens, $headerIndex, -1);
if ($lineBreakCount < $expectedLineCount) {
// because of the way the insert index was determined for header comment there cannot be an empty token here
$tokens->insertAt($headerIndex, new Token([\T_WHITESPACE, str_repeat($lineEnding, $expectedLineCount - $lineBreakCount)]));
}
}
private function getLineBreakCount(Tokens $tokens, int $index, int $direction): int
{
$whitespace = '';
for ($index += $direction; isset($tokens[$index]); $index += $direction) {
$token = $tokens[$index];
if ($token->isWhitespace()) {
$whitespace .= $token->getContent();
continue;
}
if (-1 === $direction && $token->isGivenKind(\T_OPEN_TAG)) {
$whitespace .= $token->getContent();
}
if ('' !== $token->getContent()) {
break;
}
}
return substr_count($whitespace, "\n");
}
private function removeHeader(Tokens $tokens, int $index): void
{
$prevIndex = $index - 1;
$prevToken = $tokens[$prevIndex];
$newlineRemoved = false;
if ($prevToken->isWhitespace()) {
$content = $prevToken->getContent();
if (Preg::match('/\R/', $content)) {
$newlineRemoved = true;
}
$content = Preg::replace('/\R?\h*$/', '', $content);
$tokens->ensureWhitespaceAtIndex($prevIndex, 0, $content);
}
$nextIndex = $index + 1;
$nextToken = $tokens[$nextIndex] ?? null;
if (!$newlineRemoved && null !== $nextToken && $nextToken->isWhitespace()) {
$content = Preg::replace('/^\R/', '', $nextToken->getContent());
$tokens->ensureWhitespaceAtIndex($nextIndex, 0, $content);
}
$tokens->clearTokenAndMergeSurroundingWhitespace($index);
}
private function insertHeader(Tokens $tokens, string $headerAsComment, int $index): void
{
$tokens->insertAt($index, new Token([self::HEADER_COMMENT === $this->configuration['comment_type'] ? \T_COMMENT : \T_DOC_COMMENT, $headerAsComment]));
$this->fixWhiteSpaceAroundHeader($tokens, $index);
}
}