Fix bin/publish: copy docs.dist from project root

Fix bin/publish: use correct .env path for rspade_system
Fix bin/publish script: prevent grep exit code 1 from terminating script

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
root
2025-10-21 02:08:33 +00:00
commit f6fac6c4bc
79758 changed files with 10547827 additions and 0 deletions

View File

@@ -0,0 +1,234 @@
<?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\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\Tokenizer\Analyzer\CommentsAnalyzer;
use PhpCsFixer\Tokenizer\Token;
use PhpCsFixer\Tokenizer\Tokens;
use PhpCsFixer\Utils;
/**
* @phpstan-type _AutogeneratedInputConfiguration array{
* ignored_tags?: list<string>,
* }
* @phpstan-type _AutogeneratedComputedConfiguration array{
* ignored_tags: list<string>,
* }
*
* @implements ConfigurableFixerInterface<_AutogeneratedInputConfiguration, _AutogeneratedComputedConfiguration>
*
* @author Kuba Werłos <werlos@gmail.com>
*/
final class CommentToPhpdocFixer extends AbstractFixer implements ConfigurableFixerInterface, WhitespacesAwareFixerInterface
{
/** @use ConfigurableFixerTrait<_AutogeneratedInputConfiguration, _AutogeneratedComputedConfiguration> */
use ConfigurableFixerTrait;
/**
* @var list<string>
*/
private array $ignoredTags = [];
public function isCandidate(Tokens $tokens): bool
{
return $tokens->isTokenKindFound(\T_COMMENT);
}
public function isRisky(): bool
{
return true;
}
/**
* {@inheritdoc}
*
* Must run before GeneralPhpdocAnnotationRemoveFixer, GeneralPhpdocTagRenameFixer, NoBlankLinesAfterPhpdocFixer, NoEmptyPhpdocFixer, NoSuperfluousPhpdocTagsFixer, PhpdocAddMissingParamAnnotationFixer, PhpdocAlignFixer, PhpdocAnnotationWithoutDotFixer, PhpdocArrayTypeFixer, PhpdocInlineTagNormalizerFixer, PhpdocLineSpanFixer, PhpdocListTypeFixer, PhpdocNoAccessFixer, PhpdocNoAliasTagFixer, PhpdocNoEmptyReturnFixer, PhpdocNoPackageFixer, PhpdocNoUselessInheritdocFixer, PhpdocOrderByValueFixer, PhpdocOrderFixer, PhpdocParamOrderFixer, PhpdocReadonlyClassCommentToKeywordFixer, PhpdocReturnSelfReferenceFixer, PhpdocSeparationFixer, PhpdocSingleLineVarSpacingFixer, PhpdocSummaryFixer, PhpdocTagCasingFixer, PhpdocTagTypeFixer, PhpdocToCommentFixer, PhpdocToParamTypeFixer, PhpdocToPropertyTypeFixer, PhpdocToReturnTypeFixer, PhpdocTrimConsecutiveBlankLineSeparationFixer, PhpdocTrimFixer, PhpdocTypesOrderFixer, PhpdocVarAnnotationCorrectOrderFixer, PhpdocVarWithoutNameFixer.
* Must run after AlignMultilineCommentFixer.
*/
public function getPriority(): int
{
// Should be run before all other PHPDoc fixers
return 26;
}
public function getDefinition(): FixerDefinitionInterface
{
return new FixerDefinition(
'Comments with annotation should be docblock when used on structural elements.',
[
new CodeSample("<?php /* header */ \$x = true; /* @var bool \$isFoo */ \$isFoo = true;\n"),
new CodeSample("<?php\n// @todo do something later\n\$foo = 1;\n\n// @var int \$a\n\$a = foo();\n", ['ignored_tags' => ['todo']]),
],
null,
'Risky as new docblocks might mean more, e.g. a Doctrine entity might have a new column in database.'
);
}
protected function configurePostNormalisation(): void
{
$this->ignoredTags = array_map(
static fn (string $tag): string => strtolower($tag),
$this->configuration['ignored_tags']
);
}
protected function createConfigurationDefinition(): FixerConfigurationResolverInterface
{
return new FixerConfigurationResolver([
(new FixerOptionBuilder('ignored_tags', 'List of ignored tags.'))
->setAllowedTypes(['string[]'])
->setDefault([])
->getOption(),
]);
}
protected function applyFix(\SplFileInfo $file, Tokens $tokens): void
{
$commentsAnalyzer = new CommentsAnalyzer();
for ($index = 0, $limit = \count($tokens); $index < $limit; ++$index) {
$token = $tokens[$index];
if (!$token->isGivenKind(\T_COMMENT)) {
continue;
}
if ($commentsAnalyzer->isHeaderComment($tokens, $index)) {
continue;
}
if (!$commentsAnalyzer->isBeforeStructuralElement($tokens, $index)) {
continue;
}
if (Preg::match('~(?:#|//|/\*+|\R(?:\s*\*)?)\s*\@(?=\w+-(ignore|suppress))([a-zA-Z0-9_\\\-]+)(?=\s|\(|$)~', $tokens[$index]->getContent())) {
continue;
}
$commentIndices = $commentsAnalyzer->getCommentBlockIndices($tokens, $index);
if ($this->isCommentCandidate($tokens, $commentIndices)) {
$this->fixComment($tokens, $commentIndices);
}
$index = max($commentIndices);
}
}
/**
* @param list<int> $indices
*/
private function isCommentCandidate(Tokens $tokens, array $indices): bool
{
return array_reduce(
$indices,
function (bool $carry, int $index) use ($tokens): bool {
if ($carry) {
return true;
}
if (!Preg::match('~(?:#|//|/\*+|\R(?:\s*\*)?)\s*\@([a-zA-Z0-9_\\\-]+)(?=\s|\(|$)~', $tokens[$index]->getContent(), $matches)) {
return false;
}
return !\in_array(strtolower($matches[1]), $this->ignoredTags, true);
},
false
);
}
/**
* @param non-empty-list<int> $indices
*/
private function fixComment(Tokens $tokens, array $indices): void
{
if (1 === \count($indices)) {
$this->fixCommentSingleLine($tokens, $indices[0]);
} else {
$this->fixCommentMultiLine($tokens, $indices);
}
}
private function fixCommentSingleLine(Tokens $tokens, int $index): void
{
$message = $this->getMessage($tokens[$index]->getContent());
if ('' !== trim(substr($message, 0, 1))) {
$message = ' '.$message;
}
if ('' !== trim(substr($message, -1))) {
$message .= ' ';
}
$tokens[$index] = new Token([\T_DOC_COMMENT, '/**'.$message.'*/']);
}
/**
* @param non-empty-list<int> $indices
*/
private function fixCommentMultiLine(Tokens $tokens, array $indices): void
{
$startIndex = $indices[0];
$indent = Utils::calculateTrailingWhitespaceIndent($tokens[$startIndex - 1]);
$newContent = '/**'.$this->whitespacesConfig->getLineEnding();
$count = max($indices);
for ($index = $startIndex; $index <= $count; ++$index) {
if (!$tokens[$index]->isComment()) {
continue;
}
if (str_contains($tokens[$index]->getContent(), '*/')) {
return;
}
$message = $this->getMessage($tokens[$index]->getContent());
if ('' !== trim(substr($message, 0, 1))) {
$message = ' '.$message;
}
$newContent .= $indent.' *'.$message.$this->whitespacesConfig->getLineEnding();
}
for ($index = $startIndex; $index <= $count; ++$index) {
$tokens->clearAt($index);
}
$newContent .= $indent.' */';
$tokens->insertAt($startIndex, new Token([\T_DOC_COMMENT, $newContent]));
}
private function getMessage(string $content): string
{
if (str_starts_with($content, '#')) {
return substr($content, 1);
}
if (str_starts_with($content, '//')) {
return substr($content, 2);
}
return rtrim(ltrim($content, '/*'), '*/');
}
}

View File

@@ -0,0 +1,538 @@
<?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>
*/
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
declare(strict_types=1);
namespace A\B;
echo 1;
',
[
'header' => 'Made with love.',
]
),
new CodeSample(
'<?php
declare(strict_types=1);
namespace A\B;
echo 1;
',
[
'header' => 'Made with love.',
'comment_type' => self::HEADER_PHPDOC,
'location' => 'after_open',
'separate' => 'bottom',
]
),
new CodeSample(
'<?php
declare(strict_types=1);
namespace A\B;
echo 1;
',
[
'header' => 'Made with love.',
'comment_type' => self::HEADER_COMMENT,
'location' => 'after_declare_strict',
]
),
new CodeSample(
'<?php
declare(strict_types=1);
/*
* Made with love.
*
* Extra content.
*/
namespace A\B;
echo 1;
',
[
'header' => 'Made with love.',
'validator' => '/Made with love(?P<EXTRA>.*)??/s',
'comment_type' => self::HEADER_COMMENT,
'location' => 'after_declare_strict',
]
),
new CodeSample(
'<?php
declare(strict_types=1);
/*
* Comment is not wanted here.
*/
namespace A\B;
echo 1;
',
[
'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);
}
}

View File

@@ -0,0 +1,89 @@
<?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\FixerDefinition\CodeSample;
use PhpCsFixer\FixerDefinition\FixerDefinition;
use PhpCsFixer\FixerDefinition\FixerDefinitionInterface;
use PhpCsFixer\Preg;
use PhpCsFixer\Tokenizer\Token;
use PhpCsFixer\Tokenizer\Tokens;
/**
* @author Filippo Tessarotto <zoeslam@gmail.com>
*/
final class MultilineCommentOpeningClosingFixer extends AbstractFixer
{
public function getDefinition(): FixerDefinitionInterface
{
return new FixerDefinition(
'DocBlocks must start with two asterisks, multiline comments must start with a single asterisk, after the opening slash. Both must end with a single asterisk before the closing slash.',
[
new CodeSample(
<<<'EOT'
<?php
/******
* Multiline comment with arbitrary asterisks count
******/
/**\
* Multiline comment that seems a DocBlock
*/
/**
* DocBlock with arbitrary asterisk count at the end
**/
EOT
),
]
);
}
public function isCandidate(Tokens $tokens): bool
{
return $tokens->isAnyTokenKindsFound([\T_COMMENT, \T_DOC_COMMENT]);
}
protected function applyFix(\SplFileInfo $file, Tokens $tokens): void
{
foreach ($tokens as $index => $token) {
$originalContent = $token->getContent();
if (
!$token->isGivenKind(\T_DOC_COMMENT)
&& !($token->isGivenKind(\T_COMMENT) && str_starts_with($originalContent, '/*'))
) {
continue;
}
$newContent = $originalContent;
// Fix opening
if ($token->isGivenKind(\T_COMMENT)) {
$newContent = Preg::replace('/^\/\*{2,}(?!\/)/', '/*', $newContent);
}
// Fix closing
$newContent = Preg::replace('/(?<!\/)\*{2,}\/$/', '*/', $newContent);
if ($newContent !== $originalContent) {
$tokens[$index] = new Token([$token->getId(), $newContent]);
}
}
}
}

View File

@@ -0,0 +1,160 @@
<?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\FixerDefinition\CodeSample;
use PhpCsFixer\FixerDefinition\FixerDefinition;
use PhpCsFixer\FixerDefinition\FixerDefinitionInterface;
use PhpCsFixer\Preg;
use PhpCsFixer\Tokenizer\Tokens;
final class NoEmptyCommentFixer extends AbstractFixer
{
private const TYPE_HASH = 1;
private const TYPE_DOUBLE_SLASH = 2;
private const TYPE_SLASH_ASTERISK = 3;
/**
* {@inheritdoc}
*
* Must run before NoExtraBlankLinesFixer, NoTrailingWhitespaceFixer, NoWhitespaceInBlankLineFixer.
* Must run after PhpdocToCommentFixer.
*/
public function getPriority(): int
{
return 2;
}
public function getDefinition(): FixerDefinitionInterface
{
return new FixerDefinition(
'There should not be any empty comments.',
[new CodeSample("<?php\n//\n#\n/* */\n")]
);
}
public function isCandidate(Tokens $tokens): bool
{
return $tokens->isTokenKindFound(\T_COMMENT);
}
protected function applyFix(\SplFileInfo $file, Tokens $tokens): void
{
for ($index = 1, $count = \count($tokens); $index < $count; ++$index) {
if (!$tokens[$index]->isGivenKind(\T_COMMENT)) {
continue;
}
$blockInfo = $this->getCommentBlock($tokens, $index);
$blockStart = $blockInfo['blockStart'];
$index = $blockInfo['blockEnd'];
$isEmpty = $blockInfo['isEmpty'];
if (false === $isEmpty) {
continue;
}
for ($i = $blockStart; $i <= $index; ++$i) {
$tokens->clearTokenAndMergeSurroundingWhitespace($i);
}
}
}
/**
* Return the start index, end index and a flag stating if the comment block is empty.
*
* @param int $index T_COMMENT index
*
* @return array{blockStart: int, blockEnd: int, isEmpty: bool}
*/
private function getCommentBlock(Tokens $tokens, int $index): array
{
$commentType = $this->getCommentType($tokens[$index]->getContent());
$empty = $this->isEmptyComment($tokens[$index]->getContent());
if (self::TYPE_SLASH_ASTERISK === $commentType) {
return [
'blockStart' => $index,
'blockEnd' => $index,
'isEmpty' => $empty,
];
}
$start = $index;
$count = \count($tokens);
++$index;
for (; $index < $count; ++$index) {
if ($tokens[$index]->isComment()) {
if ($commentType !== $this->getCommentType($tokens[$index]->getContent())) {
break;
}
if ($empty) { // don't retest if already known the block not being empty
$empty = $this->isEmptyComment($tokens[$index]->getContent());
}
continue;
}
if (!$tokens[$index]->isWhitespace() || $this->getLineBreakCount($tokens, $index, $index + 1) > 1) {
break;
}
}
return [
'blockStart' => $start,
'blockEnd' => $index - 1,
'isEmpty' => $empty,
];
}
private function getCommentType(string $content): int
{
if (str_starts_with($content, '#')) {
return self::TYPE_HASH;
}
if ('*' === $content[1]) {
return self::TYPE_SLASH_ASTERISK;
}
return self::TYPE_DOUBLE_SLASH;
}
private function getLineBreakCount(Tokens $tokens, int $whiteStart, int $whiteEnd): int
{
$lineCount = 0;
for ($i = $whiteStart; $i < $whiteEnd; ++$i) {
$lineCount += Preg::matchAll('/\R/u', $tokens[$i]->getContent());
}
return $lineCount;
}
private function isEmptyComment(string $content): bool
{
$type = $this->getCommentType($content);
return Preg::match([
self::TYPE_HASH => '|^#\s*$|', // single line comment starting with '#'
self::TYPE_SLASH_ASTERISK => '|^/\*[\s\*]*\*+/$|', // comment starting with '/*' and ending with '*/' (but not a PHPDoc)
self::TYPE_DOUBLE_SLASH => '|^//\s*$|', // single line comment starting with '//'
][$type], $content);
}
}

View File

@@ -0,0 +1,75 @@
<?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\FixerDefinition\CodeSample;
use PhpCsFixer\FixerDefinition\FixerDefinition;
use PhpCsFixer\FixerDefinition\FixerDefinitionInterface;
use PhpCsFixer\Preg;
use PhpCsFixer\Tokenizer\Token;
use PhpCsFixer\Tokenizer\Tokens;
/**
* @author Dariusz Rumiński <dariusz.ruminski@gmail.com>
*/
final class NoTrailingWhitespaceInCommentFixer extends AbstractFixer
{
public function getDefinition(): FixerDefinitionInterface
{
return new FixerDefinition(
'There MUST be no trailing spaces inside comment or PHPDoc.',
[new CodeSample('<?php
// This is '.'
// a comment. '.'
')]
);
}
/**
* {@inheritdoc}
*
* Must run after PhpdocNoUselessInheritdocFixer.
*/
public function getPriority(): int
{
return 0;
}
public function isCandidate(Tokens $tokens): bool
{
return $tokens->isAnyTokenKindsFound([\T_COMMENT, \T_DOC_COMMENT]);
}
protected function applyFix(\SplFileInfo $file, Tokens $tokens): void
{
foreach ($tokens as $index => $token) {
if ($token->isGivenKind(\T_DOC_COMMENT)) {
$tokens[$index] = new Token([\T_DOC_COMMENT, Preg::replace('/(*ANY)[\h]+$/m', '', $token->getContent())]);
continue;
}
if ($token->isGivenKind(\T_COMMENT)) {
if (str_starts_with($token->getContent(), '/*')) {
$tokens[$index] = new Token([\T_COMMENT, Preg::replace('/(*ANY)[\h]+$/m', '', $token->getContent())]);
} elseif (isset($tokens[$index + 1]) && $tokens[$index + 1]->isWhitespace()) {
$trimmedContent = ltrim($tokens[$index + 1]->getContent(), " \t");
$tokens->ensureWhitespaceAtIndex($index + 1, 0, $trimmedContent);
}
}
}
}
}

View File

@@ -0,0 +1,110 @@
<?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\FixerDefinition\CodeSample;
use PhpCsFixer\FixerDefinition\FixerDefinition;
use PhpCsFixer\FixerDefinition\FixerDefinitionInterface;
use PhpCsFixer\Preg;
use PhpCsFixer\Tokenizer\Token;
use PhpCsFixer\Tokenizer\Tokens;
final class SingleLineCommentSpacingFixer extends AbstractFixer
{
public function getDefinition(): FixerDefinitionInterface
{
return new FixerDefinition(
'Single-line comments must have proper spacing.',
[
new CodeSample(
'<?php
//comment 1
#comment 2
/*comment 3*/
'
),
]
);
}
/**
* {@inheritdoc}
*
* Must run after PhpdocToCommentFixer.
*/
public function getPriority(): int
{
return 1;
}
public function isCandidate(Tokens $tokens): bool
{
return $tokens->isTokenKindFound(\T_COMMENT);
}
protected function applyFix(\SplFileInfo $file, Tokens $tokens): void
{
for ($index = \count($tokens) - 1; 0 <= $index; --$index) {
$token = $tokens[$index];
if (!$token->isGivenKind(\T_COMMENT)) {
continue;
}
$content = $token->getContent();
$contentLength = \strlen($content);
if ('/' === $content[0]) {
if ($contentLength < 3) {
continue; // cheap check for "//"
}
if ('*' === $content[1]) { // slash asterisk comment
if ($contentLength < 5 || '*' === $content[2] || str_contains($content, "\n")) {
continue; // cheap check for "/**/", comment that looks like a PHPDoc, or multi line comment
}
$newContent = rtrim(substr($content, 0, -2)).' '.substr($content, -2);
$newContent = $this->fixCommentLeadingSpace($newContent, '/*');
} else { // double slash comment
$newContent = $this->fixCommentLeadingSpace($content, '//');
}
} else { // hash comment
if ($contentLength < 2 || '[' === $content[1]) { // cheap check for "#" or annotation (like) comment
continue;
}
$newContent = $this->fixCommentLeadingSpace($content, '#');
}
if ($newContent !== $content) {
$tokens[$index] = new Token([\T_COMMENT, $newContent]);
}
}
}
// fix space between comment open and leading text
private function fixCommentLeadingSpace(string $content, string $prefix): string
{
if (Preg::match(\sprintf('@^%s\h+.*$@', preg_quote($prefix, '@')), $content)) {
return $content;
}
$position = \strlen($prefix);
return substr($content, 0, $position).' '.substr($content, $position);
}
}

View File

@@ -0,0 +1,178 @@
<?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\Fixer\ConfigurableFixerInterface;
use PhpCsFixer\Fixer\ConfigurableFixerTrait;
use PhpCsFixer\FixerConfiguration\AllowedValueSubset;
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\Tokenizer\Token;
use PhpCsFixer\Tokenizer\Tokens;
/**
* @phpstan-type _AutogeneratedInputConfiguration array{
* comment_types?: list<'asterisk'|'hash'>,
* }
* @phpstan-type _AutogeneratedComputedConfiguration array{
* comment_types: list<'asterisk'|'hash'>,
* }
*
* @implements ConfigurableFixerInterface<_AutogeneratedInputConfiguration, _AutogeneratedComputedConfiguration>
*
* @author Filippo Tessarotto <zoeslam@gmail.com>
*/
final class SingleLineCommentStyleFixer extends AbstractFixer implements ConfigurableFixerInterface
{
/** @use ConfigurableFixerTrait<_AutogeneratedInputConfiguration, _AutogeneratedComputedConfiguration> */
use ConfigurableFixerTrait;
private bool $asteriskEnabled;
private bool $hashEnabled;
public function getDefinition(): FixerDefinitionInterface
{
return new FixerDefinition(
'Single-line comments and multi-line comments with only one line of actual content should use the `//` syntax.',
[
new CodeSample(
'<?php
/* asterisk comment */
$a = 1;
# hash comment
$b = 2;
/*
* multi-line
* comment
*/
$c = 3;
'
),
new CodeSample(
'<?php
/* first comment */
$a = 1;
/*
* second comment
*/
$b = 2;
/*
* third
* comment
*/
$c = 3;
',
['comment_types' => ['asterisk']]
),
new CodeSample(
"<?php # comment\n",
['comment_types' => ['hash']]
),
]
);
}
/**
* {@inheritdoc}
*
* Must run after HeaderCommentFixer, NoUselessReturnFixer, PhpdocToCommentFixer.
*/
public function getPriority(): int
{
return -31;
}
public function isCandidate(Tokens $tokens): bool
{
return $tokens->isTokenKindFound(\T_COMMENT);
}
protected function configurePostNormalisation(): void
{
$this->asteriskEnabled = \in_array('asterisk', $this->configuration['comment_types'], true);
$this->hashEnabled = \in_array('hash', $this->configuration['comment_types'], true);
}
protected function applyFix(\SplFileInfo $file, Tokens $tokens): void
{
foreach ($tokens as $index => $token) {
if (!$token->isGivenKind(\T_COMMENT)) {
continue;
}
$content = $token->getContent();
/** @TODO PHP 8.0 - no more need for `?: ''` */
$commentContent = substr($content, 2, -2) ?: ''; // @phpstan-ignore-line
if ($this->hashEnabled && str_starts_with($content, '#')) {
if (isset($content[1]) && '[' === $content[1]) {
continue; // This might be an attribute on PHP8, do not change
}
$tokens[$index] = new Token([$token->getId(), '//'.substr($content, 1)]);
continue;
}
if (
!$this->asteriskEnabled
|| str_contains($commentContent, '?>')
|| !str_starts_with($content, '/*')
|| Preg::match('/[^\s\*].*\R.*[^\s\*]/s', $commentContent)
) {
continue;
}
$nextTokenIndex = $index + 1;
if (isset($tokens[$nextTokenIndex])) {
$nextToken = $tokens[$nextTokenIndex];
if (!$nextToken->isWhitespace() || !Preg::match('/\R/', $nextToken->getContent())) {
continue;
}
$tokens[$nextTokenIndex] = new Token([$nextToken->getId(), ltrim($nextToken->getContent(), " \t")]);
}
$content = '//';
if (Preg::match('/[^\s\*]/', $commentContent)) {
$content = '// '.Preg::replace('/[\s\*]*([^\s\*](?:.+[^\s\*])?)[\s\*]*/', '\1', $commentContent);
}
$tokens[$index] = new Token([$token->getId(), $content]);
}
}
protected function createConfigurationDefinition(): FixerConfigurationResolverInterface
{
return new FixerConfigurationResolver([
(new FixerOptionBuilder('comment_types', 'List of comment types to fix.'))
->setAllowedTypes(['string[]'])
->setAllowedValues([new AllowedValueSubset(['asterisk', 'hash'])])
->setDefault(['asterisk', 'hash'])
->getOption(),
]);
}
}