diff --git a/packages/attribute-aware-php-doc/src/Ast/Type/AttributeAwareArrayShapeItemNode.php b/packages/attribute-aware-php-doc/src/Ast/Type/AttributeAwareArrayShapeItemNode.php index f7741a49712..9872efa1402 100644 --- a/packages/attribute-aware-php-doc/src/Ast/Type/AttributeAwareArrayShapeItemNode.php +++ b/packages/attribute-aware-php-doc/src/Ast/Type/AttributeAwareArrayShapeItemNode.php @@ -28,7 +28,9 @@ final class AttributeAwareArrayShapeItemNode extends ArrayShapeItemNode implemen { parent::__construct($keyName, $optional, $typeNode); - $this->hasSpaceAfterDoubleColon = (bool) Strings::match($docComment, '#\:\s+#'); + // spaces after double colon + $keyWithSpacePattern = $this->createKeyWithSpacePattern($keyName, $optional); + $this->hasSpaceAfterDoubleColon = (bool) Strings::matchAll($docComment, $keyWithSpacePattern); } public function __toString(): string @@ -45,4 +47,19 @@ final class AttributeAwareArrayShapeItemNode extends ArrayShapeItemNode implemen (string) $this->valueType ); } + + /** + * @param ConstExprIntegerNode|IdentifierTypeNode|null $keyName + */ + private function createKeyWithSpacePattern($keyName, bool $optional): string + { + $keyNameString = (string) $keyName; + if ($optional) { + $keyNameString .= '?'; + } + + $keyNameStringPregQuoted = preg_quote($keyNameString); + + return sprintf('#%s\:\s+#', $keyNameStringPregQuoted); + } } diff --git a/packages/better-php-doc-parser/src/PhpDocParser/AnnotationContentResolver.php b/packages/better-php-doc-parser/src/PhpDocParser/AnnotationContentResolver.php index 0a0da54fb19..b8283b42fc0 100644 --- a/packages/better-php-doc-parser/src/PhpDocParser/AnnotationContentResolver.php +++ b/packages/better-php-doc-parser/src/PhpDocParser/AnnotationContentResolver.php @@ -8,6 +8,7 @@ use Nette\Utils\Strings; use PHPStan\PhpDocParser\Lexer\Lexer; use PHPStan\PhpDocParser\Parser\TokenIterator; use Rector\BetterPhpDocParser\PhpDocInfo\TokenIteratorFactory; +use Symplify\PackageBuilder\Reflection\PrivatesAccessor; final class AnnotationContentResolver { @@ -16,9 +17,15 @@ final class AnnotationContentResolver */ private $tokenIteratorFactory; - public function __construct(TokenIteratorFactory $tokenIteratorFactory) + /** + * @var PrivatesAccessor + */ + private $privatesAccessor; + + public function __construct(TokenIteratorFactory $tokenIteratorFactory, PrivatesAccessor $privatesAccessor) { $this->tokenIteratorFactory = $tokenIteratorFactory; + $this->privatesAccessor = $privatesAccessor; } /** @@ -30,6 +37,7 @@ final class AnnotationContentResolver $annotationContent = ''; $unclosedOpenedBracketCount = 0; + // keep spaces? while (true) { if ($tokenIterator->currentTokenType() === Lexer::TOKEN_OPEN_PARENTHESES) { ++$unclosedOpenedBracketCount; @@ -44,6 +52,8 @@ final class AnnotationContentResolver } // remove new line "*" + $annotationContent = $this->appendPreviousWhitespace($tokenIterator, $annotationContent); + if (Strings::contains($tokenIterator->currentTokenValue(), '*')) { $tokenValueWithoutAsterisk = Strings::replace($tokenIterator->currentTokenValue(), '#\*#'); $annotationContent .= $tokenValueWithoutAsterisk; @@ -59,6 +69,9 @@ final class AnnotationContentResolver $tokenIterator->next(); } + // remove appended first space +// $annotationContent = ltrim($annotationContent); + return $this->cleanMultilineAnnotationContent($annotationContent); } @@ -127,4 +140,29 @@ final class AnnotationContentResolver return false; } + + private function appendPreviousWhitespace(TokenIterator $tokenIterator, string $annotationContent): string + { + // is space? + if (! $tokenIterator->isPrecededByHorizontalWhitespace()) { + return $annotationContent; + } + + $tokens = $this->privatesAccessor->getPrivateProperty($tokenIterator, 'tokens'); + $currentIndex = $this->privatesAccessor->getPrivateProperty($tokenIterator, 'index'); + + if (! isset($tokens[$currentIndex - 1])) { + return $annotationContent; + } + + $previousWhitespaceToken = $tokens[$currentIndex - 1]; + + // do not prepend whitespace without any content + if ($annotationContent === '') { + return $annotationContent; + } + + // get the space + return $annotationContent . $previousWhitespaceToken[0]; + } } diff --git a/packages/better-php-doc-parser/src/PhpDocParser/BetterPhpDocParser.php b/packages/better-php-doc-parser/src/PhpDocParser/BetterPhpDocParser.php index 527fb03e940..c73e6912a31 100644 --- a/packages/better-php-doc-parser/src/PhpDocParser/BetterPhpDocParser.php +++ b/packages/better-php-doc-parser/src/PhpDocParser/BetterPhpDocParser.php @@ -214,6 +214,9 @@ final class BetterPhpDocParser extends PhpDocParser $originalTokenIterator = clone $tokenIterator; $docContent = $this->annotationContentResolver->resolveFromTokenIterator($originalTokenIterator); + // @todo here space is missing, probably skipped in resolveFromTokenIterator(), or in PHPStan doc parser + // we need evne more original content :) + $tokenStart = $this->getTokenIteratorIndex($tokenIterator); $phpDocNode = $this->privatesCaller->callPrivateMethod($this, 'parseChild', $tokenIterator); $tokenEnd = $this->getTokenIteratorIndex($tokenIterator); diff --git a/packages/better-php-doc-parser/tests/PhpDocInfo/PhpDocInfoPrinter/FixtureBasic/psalm_return_array_shape.txt b/packages/better-php-doc-parser/tests/PhpDocInfo/PhpDocInfoPrinter/FixtureBasic/psalm_return_array_shape.txt new file mode 100644 index 00000000000..fe0b957e98c --- /dev/null +++ b/packages/better-php-doc-parser/tests/PhpDocInfo/PhpDocInfoPrinter/FixtureBasic/psalm_return_array_shape.txt @@ -0,0 +1,3 @@ +/** + * @psalm-param array{cache:string, callbacks?: mixed, plugin?:mixed} $options + */