From f0170a81c757c6ee45cda93458b209cefa865495 Mon Sep 17 00:00:00 2001 From: Abdul Malik Ikhsan Date: Thu, 12 Dec 2024 22:11:59 +0700 Subject: [PATCH 1/4] [DowngradePhp73] Fix complex next arg on DowngradeTrailingCommasInFunctionCallsRector --- .../DowngradeTrailingCommasInFunctionCallsRector.php | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/rules/DowngradePhp73/Rector/FuncCall/DowngradeTrailingCommasInFunctionCallsRector.php b/rules/DowngradePhp73/Rector/FuncCall/DowngradeTrailingCommasInFunctionCallsRector.php index 9bee1977..5afdfc3e 100644 --- a/rules/DowngradePhp73/Rector/FuncCall/DowngradeTrailingCommasInFunctionCallsRector.php +++ b/rules/DowngradePhp73/Rector/FuncCall/DowngradeTrailingCommasInFunctionCallsRector.php @@ -93,6 +93,14 @@ public function refactor(Node $node): ?Node $iteration = 1; while (isset($tokens[$args[$lastArgKey]->getEndTokenPos() + $iteration])) { + if (trim($tokens[$args[$lastArgKey]->getEndTokenPos() + $iteration]->text) === '') { + continue; + } + + if (trim($tokens[$args[$lastArgKey]->getEndTokenPos() + $iteration]->text) !== ',') { + break; + } + if (trim($tokens[$args[$lastArgKey]->getEndTokenPos() + $iteration]->text) === ',') { $tokens[$args[$lastArgKey]->getEndTokenPos() + $iteration]->text = ''; break; From 923461f6f338842f224f209420b4c19ca947e6dc Mon Sep 17 00:00:00 2001 From: Abdul Malik Ikhsan Date: Thu, 12 Dec 2024 22:12:03 +0700 Subject: [PATCH 2/4] [DowngradePhp73] Fix complex next arg on DowngradeTrailingCommasInFunctionCallsRector --- .../DowngradeHeredoc/Fixture/next_arg.php.inc | 486 ++++++++++++++++++ 1 file changed, 486 insertions(+) create mode 100644 tests/Issues/DowngradeHeredoc/Fixture/next_arg.php.inc diff --git a/tests/Issues/DowngradeHeredoc/Fixture/next_arg.php.inc b/tests/Issues/DowngradeHeredoc/Fixture/next_arg.php.inc new file mode 100644 index 00000000..1f117df8 --- /dev/null +++ b/tests/Issues/DowngradeHeredoc/Fixture/next_arg.php.inc @@ -0,0 +1,486 @@ + + */ +final class NextArg +{ + /** + * @var list + */ + private array $lines = []; + + /** + * @var null|list + */ + private ?array $annotations = null; + + private ?NamespaceAnalysis $namespace; + + /** + * @var list + */ + private array $namespaceUses; + + /** + * @param list $namespaceUses + */ + public function __construct(string $content, ?NamespaceAnalysis $namespace = null, array $namespaceUses = []) + { + foreach (Preg::split('/([^\n\r]+\R*)/', $content, -1, PREG_SPLIT_NO_EMPTY | PREG_SPLIT_DELIM_CAPTURE) as $line) { + $this->lines[] = new Line($line); + } + + $this->namespace = $namespace; + $this->namespaceUses = $namespaceUses; + } + + public function __toString(): string + { + return $this->getContent(); + } + + /** + * Get this docblock's lines. + * + * @return list + */ + public function getLines(): array + { + return $this->lines; + } + + /** + * Get a single line. + */ + public function getLine(int $pos): ?Line + { + return $this->lines[$pos] ?? null; + } + + /** + * Get this docblock's annotations. + * + * @return list + */ + public function getAnnotations(): array + { + if (null !== $this->annotations) { + return $this->annotations; + } + + $this->annotations = []; + $total = \count($this->lines); + + for ($index = 0; $index < $total; ++$index) { + if ($this->lines[$index]->containsATag()) { + // get all the lines that make up the annotation + $lines = \array_slice($this->lines, $index, $this->findAnnotationLength($index), true); + $annotation = new Annotation($lines, $this->namespace, $this->namespaceUses); + // move the index to the end of the annotation to avoid + // checking it again because we know the lines inside the + // current annotation cannot be part of another annotation + $index = $annotation->getEnd(); + // add the current annotation to the list of annotations + $this->annotations[] = $annotation; + } + } + + return $this->annotations; + } + + public function isMultiLine(): bool + { + return 1 !== \count($this->lines); + } + + /** + * Take a one line doc block, and turn it into a multi line doc block. + */ + public function makeMultiLine(string $indent, string $lineEnd): void + { + if ($this->isMultiLine()) { + return; + } + + $lineContent = $this->getSingleLineDocBlockEntry($this->lines[0]); + + if ('' === $lineContent) { + $this->lines = [ + new Line('/**'.$lineEnd), + new Line($indent.' *'.$lineEnd), + new Line($indent.' */'), + ]; + + return; + } + + $this->lines = [ + new Line('/**'.$lineEnd), + new Line($indent.' * '.$lineContent.$lineEnd), + new Line($indent.' */'), + ]; + } + + public function makeSingleLine(): void + { + if (!$this->isMultiLine()) { + return; + } + + $usefulLines = array_filter( + $this->lines, + static fn (Line $line): bool => $line->containsUsefulContent() + ); + + if (1 < \count($usefulLines)) { + return; + } + + $lineContent = ''; + if (\count($usefulLines) > 0) { + $lineContent = $this->getSingleLineDocBlockEntry(array_shift($usefulLines)); + } + + $this->lines = [new Line('/** '.$lineContent.' */')]; + } + + public function getAnnotation(int $pos): ?Annotation + { + $annotations = $this->getAnnotations(); + + return $annotations[$pos] ?? null; + } + + /** + * Get specific types of annotations only. + * + * @param list|string $types + * + * @return list + */ + public function getAnnotationsOfType($types): array + { + $typesToSearchFor = (array) $types; + + $annotations = []; + + foreach ($this->getAnnotations() as $annotation) { + $tagName = $annotation->getTag()->getName(); + if (\in_array($tagName, $typesToSearchFor, true)) { + $annotations[] = $annotation; + } + } + + return $annotations; + } + + /** + * Get the actual content of this docblock. + */ + public function getContent(): string + { + return implode('', $this->lines); + } + + private function findAnnotationLength(int $start): int + { + $index = $start; + + while ($line = $this->getLine(++$index)) { + if ($line->containsATag()) { + // we've 100% reached the end of the description if we get here + break; + } + + if (!$line->containsUsefulContent()) { + // if next line is also non-useful, or contains a tag, then we're done here + $next = $this->getLine($index + 1); + if (null === $next || !$next->containsUsefulContent() || $next->containsATag()) { + break; + } + // otherwise, continue, the annotation must have contained a blank line in its description + } + } + + return $index - $start; + } + + private function getSingleLineDocBlockEntry(Line $line): string + { + $lineString = $line->getContent(); + + if ('' === $lineString) { + return $lineString; + } + + $lineString = str_replace('*/', '', $lineString); + $lineString = trim($lineString); + + if (str_starts_with($lineString, '/**')) { + $lineString = substr($lineString, 3); + } elseif (str_starts_with($lineString, '*')) { + $lineString = substr($lineString, 1); + } + + return trim($lineString); + } +} + +?> +----- + + */ +final class NextArg +{ + /** + * @var list + */ + private $lines = []; + + /** + * @var null|list + */ + private $annotations; + + /** + * @var \PhpCsFixer\Tokenizer\Analyzer\Analysis\NamespaceAnalysis|null + */ + private $namespace; + + /** + * @var list + */ + private $namespaceUses; + + /** + * @param list $namespaceUses + */ + public function __construct(string $content, ?NamespaceAnalysis $namespace = null, array $namespaceUses = []) + { + foreach (Preg::split('/([^\n\r]+\R*)/', $content, -1, PREG_SPLIT_NO_EMPTY | PREG_SPLIT_DELIM_CAPTURE) as $line) { + $this->lines[] = new Line($line); + } + + $this->namespace = $namespace; + $this->namespaceUses = $namespaceUses; + } + + public function __toString(): string + { + return $this->getContent(); + } + + /** + * Get this docblock's lines. + * + * @return list + */ + public function getLines(): array + { + return $this->lines; + } + + /** + * Get a single line. + */ + public function getLine(int $pos): ?Line + { + return $this->lines[$pos] ?? null; + } + + /** + * Get this docblock's annotations. + * + * @return list + */ + public function getAnnotations(): array + { + if (null !== $this->annotations) { + return $this->annotations; + } + + $this->annotations = []; + $total = \count($this->lines); + + for ($index = 0; $index < $total; ++$index) { + if ($this->lines[$index]->containsATag()) { + // get all the lines that make up the annotation + $lines = \array_slice($this->lines, $index, $this->findAnnotationLength($index), true); + $annotation = new Annotation($lines, $this->namespace, $this->namespaceUses); + // move the index to the end of the annotation to avoid + // checking it again because we know the lines inside the + // current annotation cannot be part of another annotation + $index = $annotation->getEnd(); + // add the current annotation to the list of annotations + $this->annotations[] = $annotation; + } + } + + return $this->annotations; + } + + public function isMultiLine(): bool + { + return 1 !== \count($this->lines); + } + + /** + * Take a one line doc block, and turn it into a multi line doc block. + */ + public function makeMultiLine(string $indent, string $lineEnd): void + { + if ($this->isMultiLine()) { + return; + } + + $lineContent = $this->getSingleLineDocBlockEntry($this->lines[0]); + + if ('' === $lineContent) { + $this->lines = [ + new Line('/**'.$lineEnd), + new Line($indent.' *'.$lineEnd), + new Line($indent.' */'), + ]; + + return; + } + + $this->lines = [ + new Line('/**'.$lineEnd), + new Line($indent.' * '.$lineContent.$lineEnd), + new Line($indent.' */'), + ]; + } + + public function makeSingleLine(): void + { + if (!$this->isMultiLine()) { + return; + } + + $usefulLines = array_filter( + $this->lines, + static function (Line $line): bool { + return $line->containsUsefulContent(); + } + ); + + if (1 < \count($usefulLines)) { + return; + } + + $lineContent = ''; + if (\count($usefulLines) > 0) { + $lineContent = $this->getSingleLineDocBlockEntry(array_shift($usefulLines)); + } + + $this->lines = [new Line('/** '.$lineContent.' */')]; + } + + public function getAnnotation(int $pos): ?Annotation + { + $annotations = $this->getAnnotations(); + + return $annotations[$pos] ?? null; + } + + /** + * Get specific types of annotations only. + * + * @param list|string $types + * + * @return list + */ + public function getAnnotationsOfType($types): array + { + $typesToSearchFor = (array) $types; + + $annotations = []; + + foreach ($this->getAnnotations() as $annotation) { + $tagName = $annotation->getTag()->getName(); + if (\in_array($tagName, $typesToSearchFor, true)) { + $annotations[] = $annotation; + } + } + + return $annotations; + } + + /** + * Get the actual content of this docblock. + */ + public function getContent(): string + { + return implode('', $this->lines); + } + + private function findAnnotationLength(int $start): int + { + $index = $start; + + while ($line = $this->getLine(++$index)) { + if ($line->containsATag()) { + // we've 100% reached the end of the description if we get here + break; + } + + if (!$line->containsUsefulContent()) { + // if next line is also non-useful, or contains a tag, then we're done here + $next = $this->getLine($index + 1); + if (null === $next || !$next->containsUsefulContent() || $next->containsATag()) { + break; + } + // otherwise, continue, the annotation must have contained a blank line in its description + } + } + + return $index - $start; + } + + private function getSingleLineDocBlockEntry(Line $line): string + { + $lineString = $line->getContent(); + + if ('' === $lineString) { + return $lineString; + } + + $lineString = str_replace('*/', '', $lineString); + $lineString = trim($lineString); + + if (strncmp($lineString, '/**', strlen('/**')) === 0) { + $lineString = substr($lineString, 3); + } elseif (strncmp($lineString, '*', strlen('*')) === 0) { + $lineString = substr($lineString, 1); + } + + return trim($lineString); + } +} + +?> From 2ae0b28ddb223f6ffbcd6592ca20bcac2b31aec9 Mon Sep 17 00:00:00 2001 From: Abdul Malik Ikhsan Date: Thu, 12 Dec 2024 22:13:44 +0700 Subject: [PATCH 3/4] [DowngradePhp73] Fix complex next arg on DowngradeTrailingCommasInFunctionCallsRector --- .../FuncCall/DowngradeTrailingCommasInFunctionCallsRector.php | 1 + 1 file changed, 1 insertion(+) diff --git a/rules/DowngradePhp73/Rector/FuncCall/DowngradeTrailingCommasInFunctionCallsRector.php b/rules/DowngradePhp73/Rector/FuncCall/DowngradeTrailingCommasInFunctionCallsRector.php index 5afdfc3e..6ae6c2bd 100644 --- a/rules/DowngradePhp73/Rector/FuncCall/DowngradeTrailingCommasInFunctionCallsRector.php +++ b/rules/DowngradePhp73/Rector/FuncCall/DowngradeTrailingCommasInFunctionCallsRector.php @@ -94,6 +94,7 @@ public function refactor(Node $node): ?Node while (isset($tokens[$args[$lastArgKey]->getEndTokenPos() + $iteration])) { if (trim($tokens[$args[$lastArgKey]->getEndTokenPos() + $iteration]->text) === '') { + ++$iteration; continue; } From 549f620ab16512800fad2146a8e007a1466c7325 Mon Sep 17 00:00:00 2001 From: Abdul Malik Ikhsan Date: Thu, 12 Dec 2024 22:17:29 +0700 Subject: [PATCH 4/4] fix phsptan --- .../DowngradeTrailingCommasInFunctionCallsRector.php | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/rules/DowngradePhp73/Rector/FuncCall/DowngradeTrailingCommasInFunctionCallsRector.php b/rules/DowngradePhp73/Rector/FuncCall/DowngradeTrailingCommasInFunctionCallsRector.php index 6ae6c2bd..d1f2daf6 100644 --- a/rules/DowngradePhp73/Rector/FuncCall/DowngradeTrailingCommasInFunctionCallsRector.php +++ b/rules/DowngradePhp73/Rector/FuncCall/DowngradeTrailingCommasInFunctionCallsRector.php @@ -102,12 +102,8 @@ public function refactor(Node $node): ?Node break; } - if (trim($tokens[$args[$lastArgKey]->getEndTokenPos() + $iteration]->text) === ',') { - $tokens[$args[$lastArgKey]->getEndTokenPos() + $iteration]->text = ''; - break; - } - - ++$iteration; + $tokens[$args[$lastArgKey]->getEndTokenPos() + $iteration]->text = ''; + break; } return $node;