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
Original file line number Diff line number Diff line change
Expand Up @@ -237,7 +237,12 @@ protected function isPrimitiveGenerics(string $type, array $parts): bool
return false;
}

foreach ($iterableTypes as $iterableType) {
$docBlockIterableTypes = [...$iterableTypes, 'non-empty-array', 'non-empty-list'];
if (array_intersect($docBlockIterableTypes, $parts)) {
return true;
}

foreach ($docBlockIterableTypes as $iterableType) {
if ($this->containsTypeArray($parts, $iterableType)) {
return true;
}
Expand Down
75 changes: 69 additions & 6 deletions PhpCollective/Sniffs/Commenting/DocBlockThrowsSniff.php
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,7 @@ protected function extractExceptions(File $phpcsFile, int $stackPointer): array

// Handle the case where there's no namespace separator, string token, or fully qualified name token
// (e.g., for parenthesis after 'new')
if (!$this->isGivenKind([T_NS_SEPARATOR, T_STRING, T_NAME_FULLY_QUALIFIED], $tokens[$contentIndex])) {
if (!$this->isGivenKind([T_NS_SEPARATOR, T_STRING, T_NAME_FULLY_QUALIFIED, T_NAME_QUALIFIED], $tokens[$contentIndex])) {
continue;
}

Expand Down Expand Up @@ -248,12 +248,13 @@ protected function extractException(File $phpcsFile, int $contentIndex): array
$fullClass = $tokens[$position]['content'];
++$position;
} else {
while ($this->isGivenKind([T_NS_SEPARATOR, T_STRING], $tokens[$position])) {
while ($this->isGivenKind([T_NS_SEPARATOR, T_STRING, T_NAME_QUALIFIED], $tokens[$position])) {
$fullClass .= $tokens[$position]['content'];
++$position;
}
}

$fullyQualified = str_starts_with($fullClass, '\\');
$class = $fullClass = ltrim($fullClass, '\\');
$lastSeparator = strrpos($class, '\\');
if ($lastSeparator !== false) {
Expand All @@ -265,6 +266,7 @@ protected function extractException(File $phpcsFile, int $contentIndex): array
'end' => $position - 1,
'fullClass' => $fullClass,
'class' => $class,
'fullyQualified' => $fullyQualified,
];
}

Expand All @@ -283,6 +285,7 @@ protected function compareExceptionsAndAnnotations(
int $docBlockEndIndex,
): void {
$useStatements = $this->getUseStatements($phpcsFile);
$namespace = $this->getNamespaceAt($phpcsFile, $docBlockEndIndex);

foreach ($annotations as $annotation) {
if ($this->isInCode($annotation, $exceptions, $useStatements)) {
Expand All @@ -307,7 +310,7 @@ protected function compareExceptionsAndAnnotations(

$exceptionsToAdd = [];
foreach ($exceptions as $exception) {
$exception = $this->normalizeClassName($exception, $useStatements);
$exception = $this->normalizeClassName($exception, $useStatements, $namespace);
if (empty($exception['fullClass'])) {
// We skip for complex scenarios
$phpcsFile->addError('Doc Block @throw annotation missing', $docBlockEndIndex, 'ThrowMissingManual');
Expand All @@ -333,19 +336,79 @@ protected function compareExceptionsAndAnnotations(
$this->addAnnotationLines($phpcsFile, $exceptionsToAdd, $docBlockEndIndex);
}

/**
* Namespace the given position is declared in; a file can hold several namespace blocks.
*
* @param \PHP_CodeSniffer\Files\File $phpcsFile
* @param int $index
*
* @return string
*/
protected function getNamespaceAt(File $phpcsFile, int $index): string
{
$tokens = $phpcsFile->getTokens();

$namespaceIndex = $index;
do {
$namespaceIndex = $phpcsFile->findPrevious(T_NAMESPACE, $namespaceIndex - 1);
if ($namespaceIndex === false) {
return '';
}
$nextIndex = $phpcsFile->findNext(Tokens::$emptyTokens, $namespaceIndex + 1, null, true);
// `namespace\Foo` is the relative-name operator, not a declaration
} while ($nextIndex !== false && $tokens[$nextIndex]['code'] === T_NS_SEPARATOR);

if (isset($tokens[$namespaceIndex]['scope_closer']) && $tokens[$namespaceIndex]['scope_closer'] < $index) {
return '';
}

$endIndex = $phpcsFile->findNext([T_SEMICOLON, T_OPEN_CURLY_BRACKET], $namespaceIndex + 1);
if ($endIndex === false) {
return '';
}

$namespace = '';
for ($i = $namespaceIndex + 1; $i < $endIndex; $i++) {
if (!in_array($tokens[$i]['code'], Tokens::$emptyTokens, true)) {
$namespace .= $tokens[$i]['content'];
}
}

return $namespace;
}

/**
* @param array<string, mixed> $exception
* @param array<string, array<string, mixed>> $useStatements
* @param string $namespace Namespace of the file, to resolve names that are not imported.
*
* @return array<string, mixed> Exception
*/
protected function normalizeClassName(array $exception, array $useStatements): array
protected function normalizeClassName(array $exception, array $useStatements, string $namespace = ''): array
{
if (!empty($exception['fullyQualified']) || empty($exception['fullClass'])) {
return $exception;
}

/** @var string $fullClass */
$fullClass = $exception['fullClass'];
$segments = explode('\\', $fullClass);
$first = array_shift($segments);

foreach ($useStatements as $useStatement) {
if ($useStatement['alias'] === $exception['class'] || $useStatement['shortName'] === $exception['class']) {
if ($useStatement['alias'] !== $first && $useStatement['shortName'] !== $first) {
continue;
}
if (!$segments) {
$exception['class'] = $useStatement['shortName'];
$exception['fullClass'] = $useStatement['fullName'];
}
$exception['fullClass'] = implode('\\', [$useStatement['fullName'], ...$segments]);

return $exception;
}

if ($namespace !== '') {
$exception['fullClass'] = $namespace . '\\' . $fullClass;
}

return $exception;
Expand Down
9 changes: 9 additions & 0 deletions PhpCollective/Sniffs/PHP/PreferCastOverFunctionSniff.php
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,15 @@ public function process(File $phpcsFile, $stackPtr): void

$closingBraceIndex = $tokens[$openingBraceIndex]['parenthesis_closer'];

// First-class callable syntax `strval(...)` is not a call and has no cast equivalent
$firstArgumentIndex = $phpcsFile->findNext(T_WHITESPACE, $openingBraceIndex + 1, $closingBraceIndex, true);
if ($firstArgumentIndex !== false && $tokens[$firstArgumentIndex]['code'] === T_ELLIPSIS) {
$afterEllipsisIndex = $phpcsFile->findNext(T_WHITESPACE, $firstArgumentIndex + 1, $closingBraceIndex, true);
if ($afterEllipsisIndex === false) {
return;
}
}

// We must ignore when commas are encountered
if ($this->contains($phpcsFile, 'T_COMMA', $openingBraceIndex + 1, $closingBraceIndex - 1)) {
return;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,14 @@ class DocBlockThrowsSniffTest extends TestCase
*/
public function testDocBlockThrowsSniffer(): void
{
$this->assertSnifferFindsErrors(new DocBlockThrowsSniff(), 6);
$this->assertSnifferFindsErrors(new DocBlockThrowsSniff(), 9);
}

/**
* @return void
*/
public function testDocBlockThrowsFixer(): void
{
$this->assertSnifferCanFixErrors(new DocBlockThrowsSniff(), 6);
$this->assertSnifferCanFixErrors(new DocBlockThrowsSniff(), 9);
}
}
13 changes: 13 additions & 0 deletions tests/_data/DocBlockParamAllowDefaultValue/after.php
Original file line number Diff line number Diff line change
Expand Up @@ -99,4 +99,17 @@ public function arrayShape(array $array = null): void
public function partialDocBlock(array $row, int $accountId, array &$errors): void
{
}

/**
* Narrowed array types already cover the `array` typehint.
*
* @param non-empty-array<array{int, string}> $block
* @param non-empty-list<string> $names
* @param non-empty-array $rows
* @param list $items
* @return void
*/
public function narrowedArrays(array $block, array $names, array $rows, array $items): void
{
}
}
13 changes: 13 additions & 0 deletions tests/_data/DocBlockParamAllowDefaultValue/before.php
Original file line number Diff line number Diff line change
Expand Up @@ -99,4 +99,17 @@ public function arrayShape(array $array = null): void
public function partialDocBlock(array $row, int $accountId, array &$errors): void
{
}

/**
* Narrowed array types already cover the `array` typehint.
*
* @param non-empty-array<array{int, string}> $block
* @param non-empty-list<string> $names
* @param non-empty-array $rows
* @param list $items
* @return void
*/
public function narrowedArrays(array $block, array $names, array $rows, array $items): void
{
}
}
23 changes: 23 additions & 0 deletions tests/_data/DocBlockThrows/after.php
Original file line number Diff line number Diff line change
Expand Up @@ -71,4 +71,27 @@ public function staticCall(): void

new Parser();
}

/**
* @throws \PhpCollective\LocalException
* @return void
*/
public function sameNamespace(): void
{
throw new LocalException();
}

/**
* @throws \Foo\Baz\Exception\DeeperException
* @throws \PhpCollective\Sub\NestedException
* @return void
*/
public function relativeNames(): void
{
if ($this->something()) {
throw new Sub\NestedException();
}

throw new SomeAliasedException\DeeperException();
}
}
20 changes: 20 additions & 0 deletions tests/_data/DocBlockThrows/before.php
Original file line number Diff line number Diff line change
Expand Up @@ -65,4 +65,24 @@ public function staticCall(): void

new Parser();
}

/**
* @return void
*/
public function sameNamespace(): void
{
throw new LocalException();
}

/**
* @return void
*/
public function relativeNames(): void
{
if ($this->something()) {
throw new Sub\NestedException();
}

throw new SomeAliasedException\DeeperException();
}
}
11 changes: 11 additions & 0 deletions tests/_data/PreferCastOverFunction/after.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,15 @@ public function namespacedFunctionForms($value): array

return [$plain, $fullyQualified, $qualified, $relative];
}

/**
* First-class callable syntax must stay as is.
*/
public function firstClassCallable(array $values): array
{
$strings = array_map(strval(...), $values);
$ints = array_map(\intval( ... ), $values);

return [$strings, $ints];
}
}
11 changes: 11 additions & 0 deletions tests/_data/PreferCastOverFunction/before.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,15 @@ public function namespacedFunctionForms($value): array

return [$plain, $fullyQualified, $qualified, $relative];
}

/**
* First-class callable syntax must stay as is.
*/
public function firstClassCallable(array $values): array
{
$strings = array_map(strval(...), $values);
$ints = array_map(\intval( ... ), $values);

return [$strings, $ints];
}
}
Loading