Skip to content

Instantly share code, notes, and snippets.

@voku
Created July 7, 2020 21:27
Show Gist options
  • Save voku/3aba12eb898dfa209a787c398a331f9c to your computer and use it in GitHub Desktop.
Save voku/3aba12eb898dfa209a787c398a331f9c to your computer and use it in GitHub Desktop.
<?php
declare(strict_types=1);
use PhpCsFixer\Preg;
use PhpCsFixer\Tokenizer\Token;
use PhpCsFixer\Tokenizer\Tokens;
require_once __DIR__ . '/AbstractVdmgFixerHelper.php';
final class VdmgFactoryDoctypeFixer extends AbstractVdmgFixerHelper {
/**
* @var string
*/
private $className = '';
/**
* @var string
*/
private $parentClassName = '';
/**
* @var string[]
*/
private $foundMethods = [];
/**
* @param SplFileInfo $file
* @param Token[]|Tokens $tokens
*
* @return void
*/
public function applyFix(\SplFileInfo $file, Tokens $tokens): void {
// init
$this->className = null;
foreach ($tokens as $index => $token) {
if (!$token->isGivenKind(\T_EXTENDS)) {
continue;
}
$this->parentClassName = $this->getParentClassName($tokens, $index);
if (!$this->isFactory($tokens, $index)) {
continue;
}
$classNamePosition = (int)$tokens->getPrevMeaningfulToken($index);
$classNameToken = $tokens[$classNamePosition];
$this->className = $classNameToken->getContent();
}
if (!$this->className) {
return;
}
$specialClasses = [
ManagedFactoryVertragsDB::class,
ManagedTreeFactory::class,
ManagedTreeNetworkFactory::class,
];
if (in_array($this->className, $specialClasses, true)) {
return;
}
// init
$this->foundMethods = [];
if (
$this->parentClassName == ManagedTreeFactory::class
||
$this->parentClassName == ManagedTreeNetworkFactory::class
) {
$this->foundMethods['fetchRoots'] = false;
$this->foundMethods['fetchChildrenById'] = false;
$this->foundMethods['fetchAllChildrenById'] = false;
$this->foundMethods['fetchLeafsById'] = false;
$this->foundMethods['fetchLeafsByIdYield'] = false;
$this->foundMethods['fetchParentsById'] = false;
}
$this->foundMethods['fetchEmpty'] = false;
$this->foundMethods['fetchAll'] = false;
$this->foundMethods['fetchAllYield'] = false;
$this->foundMethods['fetchByIds'] = false;
$this->foundMethods['fetchById'] = false;
$this->foundMethods['fetchByIdWithStaticCache'] = false;
$this->foundMethods['fetchByIdIfExists'] = false;
$this->foundMethods['fetchByIdIfExistsWithStaticCache'] = false;
$this->foundMethods['fetchByQuery'] = false;
$this->foundMethods['fetchByQueryWithStaticCache'] = false;
$this->foundMethods['fetchByQueryYield'] = false;
$this->foundMethods['fetchByQueryPrimaryKeyAsArrayIndex'] = false;
$this->foundMethods['fetchOneByQuery'] = false;
$this->foundMethods['fetchOneOrThrowExceptionByQuery'] = false;
$this->foundMethods['fetchAllPrimaryKeyAsArrayIndex'] = false;
$this->foundMethods['fetchByIdsPrimaryKeyAsArrayIndex'] = false;
$this->foundMethods['createFromArray'] = false;
$this->foundMethods['fetchFirst'] = false;
$this->foundMethods['fetchLast'] = false;
$this->foundMethods['delete'] = false;
$this->foundMethods['insert'] = false;
$this->foundMethods['update'] = false;
$this->foundMethods['replace'] = false;
$this->foundMethods['construct'] = false;
foreach ($tokens as $index => $token) {
if (!$token->isGivenKind(\T_FUNCTION)) {
if (isset($tokens[$index - 1])) {
$prevContent = $tokens[$index - 1]->getContent();
if ($prevContent == '->') {
continue;
}
if ($prevContent == '::') {
continue;
}
}
switch ($tokens[(int)$index]->getContent()) {
case 'fetchEmpty':
$this->foundMethods['fetchEmpty'] = true;
break;
case 'fetchAll':
$this->foundMethods['fetchAll'] = true;
break;
case 'fetchAllYield':
$this->foundMethods['fetchAllYield'] = true;
break;
case 'fetchByIds':
$this->foundMethods['fetchByIds'] = true;
break;
case 'fetchById':
$this->foundMethods['fetchById'] = true;
break;
case 'fetchByIdWithStaticCache':
$this->foundMethods['fetchByIdWithStaticCache'] = true;
break;
case 'fetchByIdIfExists':
$this->foundMethods['fetchByIdIfExists'] = true;
break;
case 'fetchByIdIfExistsWithStaticCache':
$this->foundMethods['fetchByIdIfExistsWithStaticCache'] = true;
break;
case 'fetchByQuery':
$this->foundMethods['fetchByQuery'] = true;
break;
case 'fetchByQueryWithStaticCache':
$this->foundMethods['fetchByQueryWithStaticCache'] = true;
break;
case 'fetchByQueryYield':
$this->foundMethods['fetchByQueryYield'] = true;
break;
case 'fetchByQueryPrimaryKeyAsArrayIndex':
$this->foundMethods['fetchByQueryPrimaryKeyAsArrayIndex'] = true;
break;
case 'fetchOneByQuery':
$this->foundMethods['fetchOneByQuery'] = true;
break;
case 'fetchOneOrThrowExceptionByQuery':
$this->foundMethods['fetchOneOrThrowExceptionByQuery'] = true;
break;
case 'fetchAllPrimaryKeyAsArrayIndex':
$this->foundMethods['fetchAllPrimaryKeyAsArrayIndex'] = true;
break;
case 'fetchByIdsPrimaryKeyAsArrayIndex':
$this->foundMethods['fetchByIdsPrimaryKeyAsArrayIndex'] = true;
break;
case 'createFromArray':
$this->foundMethods['createFromArray'] = true;
break;
case 'fetchFirst':
$this->foundMethods['fetchFirst'] = true;
break;
case 'fetchLast':
$this->foundMethods['fetchLast'] = true;
break;
case 'delete':
$this->foundMethods['delete'] = true;
break;
case 'insert':
$this->foundMethods['insert'] = true;
break;
case 'update':
$this->foundMethods['update'] = true;
break;
case 'replace':
$this->foundMethods['replace'] = true;
break;
case 'construct':
$this->foundMethods['construct'] = true;
break;
default:
// nothing
break;
}
if (
$this->parentClassName == ManagedTreeFactory::class
||
$this->parentClassName == ManagedTreeNetworkFactory::class
) {
switch ($tokens[(int)$index]->getContent()) {
case 'fetchRoots':
$this->foundMethods['fetchRoots'] = true;
break;
case 'fetchChildrenById':
$this->foundMethods['fetchChildrenById'] = true;
break;
case 'fetchAllChildrenById':
$this->foundMethods['fetchAllChildrenById'] = true;
break;
case 'fetchLeafsById':
$this->foundMethods['fetchLeafsById'] = true;
break;
case 'fetchLeafsByIdYield':
$this->foundMethods['fetchLeafsByIdYield'] = true;
break;
case 'fetchParentsById':
$this->foundMethods['fetchParentsById'] = true;
break;
default:
// nothing
break;
}
}
}
}
// figure out where the comment should be placed
$headerNewIndex = $this->findHeaderCommentInsertionIndex($tokens);
// check if there is already a comment
$headerCurrentIndex = $this->findHeaderCommentCurrentIndex($tokens, $headerNewIndex - 1);
if ($headerCurrentIndex === null) {
$this->insertHeader($tokens, $headerNewIndex);
} elseif ($this->getHeaderAsComment() !== $tokens[$headerCurrentIndex]->getContent()) {
$tokens->clearTokenAndMergeSurroundingWhitespace($headerCurrentIndex);
$this->insertHeader($tokens, $headerNewIndex);
} else {
$headerNewIndex = $headerCurrentIndex;
}
$this->fixWhiteSpaceAroundHeader($tokens, $headerNewIndex);
}
/**
* {@inheritdoc}
*/
public function getDocumentation(): string {
return 'Extended "Factory" (& ManagedFactory ...) needs some extra @method phpdocs.';
}
/**
* Run after other class name fixes etc. ....
*/
public function getPriority(): int {
return -50;
}
/**
* {@inheritdoc}
*
* @noinspection AutoloadingIssuesInspection
* @noinspection EmptyClassInspection
*/
public function getSampleCode(): string {
return <<<'PHP'
<?php
class TenderFactory extends ManagedFactory {
}
PHP;
}
/**
* @param Token[]|Tokens $tokens
*
* @return bool
*/
public function isCandidate(Tokens $tokens): bool {
return $tokens->isAllTokenKindsFound([\T_CLASS, \T_EXTENDS, \T_STRING]);
}
/**
* @param Token[]|Tokens $tokens
* @param int $headerNewIndex
*
* @return null|int
*/
private function findHeaderCommentCurrentIndex(Tokens $tokens, $headerNewIndex) {
$index = $tokens->getNextNonWhitespace($headerNewIndex);
return $index === null || !$tokens[(int)$index]->isComment() ? null : $index;
}
/**
* Find the index where the header comment must be inserted.
*
* @param Token[]|Tokens $tokens
*
* @return int
*/
private function findHeaderCommentInsertionIndex(Tokens $tokens): int {
$index = $tokens->getNextMeaningfulToken(0);
if ($index === null) {
// file without meaningful tokens but an open tag, comment should always be placed directly after the open tag
return 1;
}
if (!$tokens[(int)$index]->isGivenKind(\T_DECLARE)) {
return 1;
}
$next = $tokens->getNextMeaningfulToken($index);
if ($next === null || !$tokens[(int)$next]->equals('(')) {
return 1;
}
$next = $tokens->getNextMeaningfulToken($next);
if ($next === null || !$tokens[(int)$next]->equals([\T_STRING, 'strict_types'], false)) {
return 1;
}
$next = $tokens->getNextMeaningfulToken($next);
if ($next === null || !$tokens[(int)$next]->equals('=')) {
return 1;
}
$next = $tokens->getNextMeaningfulToken($next);
if ($next === null || !$tokens[(int)$next]->isGivenKind(\T_LNUMBER)) {
return 1;
}
$next = $tokens->getNextMeaningfulToken($next);
if ($next === null || !$tokens[(int)$next]->equals(')')) {
return 1;
}
$next = $tokens->getNextMeaningfulToken($next);
if ($next === null || !$tokens[(int)$next]->equals(';')) { // don't insert after close tag
return 1;
}
return $next + 1;
}
/**
* @param Token[]|Tokens $tokens
* @param int $headerIndex
*/
private function fixWhiteSpaceAroundHeader(Tokens $tokens, $headerIndex): void {
$lineEnding = "\n";
// fix lines after header comment
$expectedLineCount = 2;
if ($headerIndex === \count($tokens) - 1) {
$tokens->insertAt(
$headerIndex + 1,
new Token(
[
\T_WHITESPACE,
\str_repeat($lineEnding, $expectedLineCount),
]
)
);
} else {
$afterCommentIndex = $tokens->getNextNonWhitespace($headerIndex);
$lineBreakCount = $this->getLineBreakCount($tokens, $headerIndex + 1, $afterCommentIndex ?? count($tokens));
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 > 2) {
// remove extra line endings
if ($tokens[$headerIndex + 1]->isWhitespace()) {
$tokens[$headerIndex + 1] = new Token([\T_WHITESPACE, $lineEnding . $lineEnding]);
}
}
}
// fix lines before header comment
$expectedLineCount = 2;
$prev = $tokens->getPrevNonWhitespace($headerIndex);
$regex = '/[\t ]$/';
if ($prev && $tokens[(int)$prev]->isGivenKind(\T_OPEN_TAG) && Preg::match($regex, $tokens[(int)$prev]->getContent())) {
$tokens[(int)$prev] = new Token(
[
\T_OPEN_TAG,
Preg::replace($regex, $lineEnding, $tokens[(int)$prev]->getContent()),
]
);
}
$lineBreakCount = $this->getLineBreakCount($tokens, $prev, $headerIndex);
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),
]
)
);
}
}
/**
* @return mixed
*/
private function getActiveRecordName() {
return \str_replace('Factory', '', $this->className);
}
/**
* Enclose the given text in a comment block.
*
* @return string
*/
private function getHeaderAsComment(): string {
$activeRecordName = $this->getActiveRecordName();
// init
$lines = [];
if ($this->foundMethods['fetchEmpty'] === false) {
$lines['fetchEmpty'] = ' * @method ' . $activeRecordName . ' fetchEmpty()';
}
if ($this->foundMethods['fetchAll'] === false) {
$lines['fetchAll'] = ' * @method ' . $activeRecordName . '[] fetchAll(int $limit = null, int $offset = null, bool $sharded = false)';
}
if ($this->foundMethods['fetchAllYield'] === false) {
$lines['fetchAllYield'] = ' * @method Generator|' . $activeRecordName . '[] fetchAllYield(int $limit = null, int $offset = null, bool $sharded = false)';
}
if ($this->foundMethods['fetchByIds'] === false) {
$lines['fetchByIds'] = ' * @method ' . $activeRecordName . '[] fetchByIds(int[]|string[] $ids)';
}
if ($this->foundMethods['fetchById'] === false) {
$lines['fetchById'] = ' * @method ' . $activeRecordName . ' fetchById(int|string $id)';
}
if ($this->foundMethods['fetchByIdWithStaticCache'] === false) {
$lines['fetchByIdWithStaticCache'] = ' * @method ' . $activeRecordName . ' fetchByIdWithStaticCache(int|string $id)';
}
if ($this->foundMethods['fetchByIdIfExists'] === false) {
$lines['fetchByIdIfExists'] = ' * @method null|' . $activeRecordName . ' fetchByIdIfExists(int|string $id)';
}
if ($this->foundMethods['fetchByIdIfExistsWithStaticCache'] === false) {
$lines['fetchByIdIfExistsWithStaticCache'] = ' * @method null|' . $activeRecordName . ' fetchByIdIfExistsWithStaticCache(int|string $id)';
}
if ($this->foundMethods['fetchByQuery'] === false) {
$lines['fetchByQuery'] = ' * @method ' . $activeRecordName . '[] fetchByQuery(string $query, bool $sharded = false, bool $remote = false, null|DatabaseConnection $db = null)';
}
if ($this->foundMethods['fetchByQueryWithStaticCache'] === false) {
$lines['fetchByQueryWithStaticCache'] = ' * @method ' . $activeRecordName . '[] fetchByQueryWithStaticCache(string $query, bool $sharded = false, bool $remote = false, null|DatabaseConnection $db = null)';
}
if ($this->foundMethods['fetchByQueryYield'] === false) {
$lines['fetchByQueryYield'] = ' * @method Generator|' . $activeRecordName . '[] fetchByQueryYield(string $query, bool $sharded = false, bool $remote = false, null|DatabaseConnection $db = null)';
}
if ($this->foundMethods['fetchByQueryPrimaryKeyAsArrayIndex'] === false) {
$lines['fetchByQueryPrimaryKeyAsArrayIndex'] = ' * @method ' . $activeRecordName . '[] fetchByQueryPrimaryKeyAsArrayIndex(string $query, string $primaryKeyName, bool $sharded = false)';
}
if ($this->foundMethods['fetchOneByQuery'] === false) {
$lines['fetchOneByQuery'] = ' * @method null|' . $activeRecordName . ' fetchOneByQuery(string $query, bool $sharded = false, bool $remote = false, null|DatabaseConnection $db = null)';
}
if ($this->foundMethods['fetchOneOrThrowExceptionByQuery'] === false) {
$lines['fetchOneOrThrowExceptionByQuery'] = ' * @method ' . $activeRecordName . ' fetchOneOrThrowExceptionByQuery(string $query, bool $sharded = false, bool $remote = false, null|DatabaseConnection $db = null)';
}
if ($this->foundMethods['fetchAllPrimaryKeyAsArrayIndex'] === false) {
$lines['fetchAllPrimaryKeyAsArrayIndex'] = ' * @method ' . $activeRecordName . '[] fetchAllPrimaryKeyAsArrayIndex(int $limit = null, int $offset = null, bool $sharded = false)';
}
if ($this->foundMethods['fetchByIdsPrimaryKeyAsArrayIndex'] === false) {
$lines['fetchByIdsPrimaryKeyAsArrayIndex'] = ' * @method ' . $activeRecordName . '[] fetchByIdsPrimaryKeyAsArrayIndex(int[]|string[] $ids)';
}
if ($this->foundMethods['createFromArray'] === false) {
$lines['createFromArray'] = ' * @method ' . $activeRecordName . ' createFromArray(array $data, bool $insert = true)';
}
if ($this->foundMethods['fetchFirst'] === false) {
$lines['fetchFirst'] = ' * @method null|' . $activeRecordName . ' fetchFirst()';
}
if ($this->foundMethods['fetchLast'] === false) {
$lines['fetchLast'] = ' * @method null|' . $activeRecordName . ' fetchLast()';
}
if ($this->foundMethods['delete'] === false) {
$lines['delete'] = ' * @method bool delete(' . $activeRecordName . ' $activeRow)';
}
if ($this->foundMethods['insert'] === false) {
$lines['insert'] = ' * @method int insert(' . $activeRecordName . ' $activeRow)';
}
if ($this->foundMethods['update'] === false) {
$lines['update'] = ' * @method bool update(' . $activeRecordName . ' $activeRow)';
}
if ($this->foundMethods['replace'] === false) {
$lines['replace'] = ' * @method int replace(' . $activeRecordName . ' $activeRow)';
}
if ($this->foundMethods['construct'] === false) {
$lines['construct'] = ' * @method ' . $activeRecordName . ' construct(array &$row)';
}
if (
$this->parentClassName == ManagedTreeFactory::class
||
$this->parentClassName == ManagedTreeNetworkFactory::class
) {
if ($this->foundMethods['fetchRoots'] === false) {
$lines['fetchRoots'] = ' * @method ' . $activeRecordName . ' fetchRoots()';
}
if ($this->foundMethods['fetchChildrenById'] === false) {
$lines['fetchChildrenById'] = ' * @method ' . $activeRecordName . '[] fetchChildrenById($id)';
}
if ($this->foundMethods['fetchAllChildrenById'] === false) {
$lines['fetchAllChildrenById'] = ' * @method ' . $activeRecordName . '[] fetchAllChildrenById($id, bool $includeRoot = false)';
}
if ($this->foundMethods['fetchLeafsById'] === false) {
$lines['fetchLeafsById'] = ' * @method ' . $activeRecordName . '[] fetchLeafsById($id)';
}
if ($this->foundMethods['fetchLeafsByIdYield'] === false) {
$lines['fetchLeafsByIdYield'] = ' * @method Generator|' . $activeRecordName . '[] fetchLeafsByIdYield($id)';
}
if ($this->foundMethods['fetchParentsById'] === false) {
$lines['fetchParentsById'] = ' * @method ' . $activeRecordName . '[] fetchParentsById($id)';
}
if ($this->foundMethods['delete'] === false) {
$lines['delete'] = ' * @method bool delete(' . $activeRecordName . ' $activeRow, bool $buildTree = true)';
}
if ($this->foundMethods['insert'] === false) {
$lines['insert'] = ' * @method int insert(' . $activeRecordName . ' $activeRow, bool $buildTree = true)';
}
if ($this->foundMethods['update'] === false) {
$lines['update'] = ' * @method bool update(' . $activeRecordName . ' $activeRow, bool $buildTree = true)';
}
}
return '/**
' . implode("\n", $lines) . '
*
* // info: description is in the ActiveRecord class
*
* @see ' . $activeRecordName . '
*
* // warning -> do not edit by hand, auto-generated via:
* // cd ~/vdmg/ && make apply-php-cs-factory-doc-type-fixer
*
* @extends ' . $this->parentClassName . '<' . $activeRecordName . '>
*/';
}
/**
* @param Token[]|Tokens $tokens
* @param int $indexStart
* @param int $indexEnd
*
* @return int
*/
private function getLineBreakCount(Tokens $tokens, $indexStart, $indexEnd): int {
$lineCount = 0;
for ($i = $indexStart; $i < $indexEnd; ++$i) {
$lineCount += \substr_count($tokens[$i]->getContent(), "\n");
}
return $lineCount;
}
/**
* @param Token[]|Tokens $tokens
* @param int $index
*
* @return null|string
*/
private function getParentClassName(Tokens $tokens, $index) {
$parentClassNamePosition = $tokens->getNextMeaningfulToken($index);
if ($parentClassNamePosition === null) {
return null;
}
$parentClassNameToken = $tokens[(int)$parentClassNamePosition];
return (string)$parentClassNameToken->getContent();
}
/**
* @param Token[]|Tokens $tokens
* @param int $index
*/
private function insertHeader(Tokens $tokens, $index): void {
$tokens->insertAt(
$index,
new Token(
[
\T_DOC_COMMENT,
$this->getHeaderAsComment(),
]
)
);
}
/**
* @param Token[]|Tokens $tokens
* @param int $index
*
* @return bool
*/
private function isFactory(Tokens $tokens, $index): bool {
$parentClassName = $this->getParentClassName($tokens, $index);
return \strpos($parentClassName, ManagedFactory::class) !== false
||
\strpos($parentClassName, ManagedTreeFactory::class) !== false
||
\strpos($parentClassName, ManagedTreeNetworkFactory::class) !== false;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment