Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions PhpCollective/Sniffs/Commenting/DocBlockParamSniff.php
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,27 @@ public function process(File $phpcsFile, $stackPointer): void
continue;
}

// Check if this might be a multi-line type (has unclosed brackets)
$openBrackets = substr_count($content, '<') + substr_count($content, '{') + substr_count($content, '(');
$closeBrackets = substr_count($content, '>') + substr_count($content, '}') + substr_count($content, ')');

if ($openBrackets > $closeBrackets) {
// Multi-line type annotation - collect across lines
$multiLineResult = $this->collectMultiLineType($phpcsFile, $i, $docBlockEndIndex);
if ($multiLineResult !== null) {
$docBlockParams[] = [
'index' => $classNameIndex,
'type' => $multiLineResult['type'],
'variable' => $multiLineResult['variable'],
'appendix' => ' ' . $multiLineResult['variable'] . ($multiLineResult['description'] ? ' ' . $multiLineResult['description'] : ''),
];
// Skip to the end of the multi-line annotation
$i = $multiLineResult['endIndex'];

continue;
}
}

$appendix = '';
$spacePos = strpos($content, ' ');
if ($spacePos) {
Expand Down
98 changes: 98 additions & 0 deletions PhpCollective/Traits/CommentingTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,104 @@ protected function containsIterableSyntax(array $docBlockTypes): bool
return false;
}

/**
* Collects a potentially multi-line type annotation from a doc block.
*
* This handles complex types like:
* - array<string, array{msgid: string, msgid_plural: string|null}>
* - Multi-line array shapes with nested structures
*
* @param \PHP_CodeSniffer\Files\File $phpcsFile
* @param int $tagIndex The index of the @param/@return/@var tag
* @param int $docBlockEndIndex The end of the doc block
*
* @return array{type: string, variable: string, description: string, endIndex: int}|null
*/
protected function collectMultiLineType(File $phpcsFile, int $tagIndex, int $docBlockEndIndex): ?array
{
$tokens = $phpcsFile->getTokens();

// Find the first content token after the tag
$contentIndex = $tagIndex + 2;
if (!isset($tokens[$contentIndex]) || $tokens[$contentIndex]['type'] !== 'T_DOC_COMMENT_STRING') {
return null;
}

$collectedContent = '';
$bracketDepth = 0;
$endIndex = $contentIndex;

// Collect content across multiple lines if brackets are open
for ($i = $contentIndex; $i < $docBlockEndIndex; $i++) {
$token = $tokens[$i];

if ($token['type'] === 'T_DOC_COMMENT_STRING') {
$content = $token['content'];
$collectedContent .= $content;
$endIndex = $i;

// Count bracket depth
$bracketDepth += substr_count($content, '<') + substr_count($content, '{') + substr_count($content, '(');
$bracketDepth -= substr_count($content, '>') + substr_count($content, '}') + substr_count($content, ')');

// If brackets are balanced and we have content, check if we have the full type
if ($bracketDepth <= 0) {
break;
}
} elseif ($token['type'] === 'T_DOC_COMMENT_WHITESPACE') {
// Add a space for line continuations (replacing newlines and asterisks)
if ($bracketDepth > 0 && str_contains($token['content'], "\n")) {
$collectedContent .= ' ';
}
} elseif ($token['type'] === 'T_DOC_COMMENT_STAR') {
// Skip the leading asterisk on continuation lines
continue;
} elseif ($token['type'] === 'T_DOC_COMMENT_TAG') {
// Hit another tag, stop collecting
break;
}
}

// Normalize whitespace (collapse multiple spaces)
$collectedContent = (string)preg_replace('/\s+/', ' ', trim($collectedContent));

// Parse the collected content to extract type, variable, and description
return $this->parseCollectedTypeContent($collectedContent, $endIndex);
}

/**
* Parse the collected content to extract type, variable name, and description.
*
* @param string $content The collected content
* @param int $endIndex The ending token index
*
* @return array{type: string, variable: string, description: string, endIndex: int}|null
*/
protected function parseCollectedTypeContent(string $content, int $endIndex): ?array
{
// Find the variable name (starts with $)
if (!preg_match('/^(.+?)\s+(\$\S+)(?:\s+(.*))?$/', $content, $matches)) {
// Maybe just a type without variable (for @return)
if (preg_match('/^(\S+)(?:\s+(.*))?$/', $content, $matches)) {
return [
'type' => $matches[1],
'variable' => '',
'description' => $matches[2] ?? '',
'endIndex' => $endIndex,
];
}

return null;
}

return [
'type' => trim($matches[1]),
'variable' => $matches[2],
'description' => $matches[3] ?? '',
'endIndex' => $endIndex,
];
}

/**
* @param array<\PHPStan\PhpDocParser\Ast\Type\TypeNode|string> $typeNodes type nodes
*
Expand Down
51 changes: 51 additions & 0 deletions tests/_data/DocBlockParam/after.php
Original file line number Diff line number Diff line change
Expand Up @@ -110,4 +110,55 @@ public function withInheritDoc($param1, $param2): void
{
// Should not error due to @inheritDoc
}

/**
* Multi-line array shape annotation - should be parsed correctly
*
* @param array<string, array{
* msgid: string,
* msgid_plural: string|null,
* msgctxt: string|null,
* references: array<string>,
* comments: array<string>
* }> $strings Extracted strings
*
* @return void
*/
public function multiLineArrayShape(array $strings): void
{
// Should not error - multi-line array shape is valid
}

/**
* Multi-line with multiple params - should be parsed correctly
*
* @param string $name The name
* @param array{
* id: int,
* name: string,
* meta?: array<string, mixed>
* } $data The data object
* @param bool $flag Optional flag
*
* @return void
*/
public function multiLineWithMultipleParams(string $name, array $data, bool $flag = false): void
{
// Should not error - multi-line array shape with other params
}

/**
* Nested multi-line generics
*
* @param array<int, array<string, array{
* key: string,
* value: mixed
* }>> $nested Deeply nested structure
*
* @return void
*/
public function nestedMultiLineGenerics(array $nested): void
{
// Should not error - deeply nested multi-line type
}
}
51 changes: 51 additions & 0 deletions tests/_data/DocBlockParam/before.php
Original file line number Diff line number Diff line change
Expand Up @@ -111,4 +111,55 @@ public function withInheritDoc($param1, $param2): void
{
// Should not error due to @inheritDoc
}

/**
* Multi-line array shape annotation - should be parsed correctly
*
* @param array<string, array{
* msgid: string,
* msgid_plural: string|null,
* msgctxt: string|null,
* references: array<string>,
* comments: array<string>
* }> $strings Extracted strings
*
* @return void
*/
public function multiLineArrayShape(array $strings): void
{
// Should not error - multi-line array shape is valid
}

/**
* Multi-line with multiple params - should be parsed correctly
*
* @param string $name The name
* @param array{
* id: int,
* name: string,
* meta?: array<string, mixed>
* } $data The data object
* @param bool $flag Optional flag
*
* @return void
*/
public function multiLineWithMultipleParams(string $name, array $data, bool $flag = false): void
{
// Should not error - multi-line array shape with other params
}

/**
* Nested multi-line generics
*
* @param array<int, array<string, array{
* key: string,
* value: mixed
* }>> $nested Deeply nested structure
*
* @return void
*/
public function nestedMultiLineGenerics(array $nested): void
{
// Should not error - deeply nested multi-line type
}
}