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
70 changes: 56 additions & 14 deletions custom-standards/Flyeralarm/Sniffs/Docblock/ReturnTypeSniff.php
Original file line number Diff line number Diff line change
Expand Up @@ -58,22 +58,13 @@ public function process(File $phpcsFile, $stackPtr)
$tokens = $phpcsFile->getTokens();
$returnTypePtr = $this->getDocReturnTypePtr($phpcsFile, $stackPtr);
$returnTypeString = $tokens[$returnTypePtr]['content'];
$returnTypes = explode('|', $returnTypeString);

foreach ($returnTypes as $returnType) {
$returnType = trim($returnType);
if (in_array($returnType, $this->returnTypeScalarWhitelist, true)) {
continue;
}
if (in_array($returnType, $this->returnTypeClassWhitelist, true)) {
continue;
}
if ($this->isStartingWithUppercaseLetter($returnType)) {
continue;
}

try {
$this->checkReturnTypeShape($returnTypeString);
}
catch (\InvalidArgumentException $exception) {
$phpcsFile->addError(
sprintf('Return type "%s" is discouraged', $returnType),
$exception->getMessage(),
$returnTypePtr,
'ProhibitedReturnType'
);
Expand Down Expand Up @@ -145,4 +136,55 @@ private function isStartingWithUppercaseLetter($singleCharacter)

return false;
}

/**
* @param string $subject The return type string to check.
* @return void
*/
private function checkReturnTypeShape(string $subject)
{
$matched = preg_match_all('#(?<separator>\s*\|\s*)?(?<atom>[^<>\|]+)(?<generic><(?<nested>.*)>)?#', $subject, $matches);

if (!$matched || implode('', $matches[0]) !== $subject) {
throw new \InvalidArgumentException('Invalid structure in return type "' . $subject . '"');
}

if (strpos($matches['separator'][0], '|') !== false) {
throw new \InvalidArgumentException('Missing return type in first alternative of type "' . $subject . '"');
}

foreach ($matches['nested'] as $index => $match) {
if ($matches['generic'][$index] === '') {
if (trim($matches['atom'][$index]) !== 'array') {
throw new \InvalidArgumentException('Unexpected generic specification in type "' . $matches[0][$index] . '"');
}

$match = trim($match);
if (strpos($match, 'int,') === 0) {
// Allow numeric indexing in generics, e.g. `array<int, string>`
$match = substr($match, 4);
}

if ($match === '') {
throw new \InvalidArgumentException('Generic specification may not be empty in type "' . $matches[0][$index] . '"');
}

$this->checkReturnTypeShape($match);
}

// Check if atom is in whitelist.
$returnType = trim($matches['atom'][$index]);
if (in_array($returnType, $this->returnTypeScalarWhitelist, true)) {
continue;
}
if (in_array($returnType, $this->returnTypeClassWhitelist, true)) {
continue;
}
if ($this->isStartingWithUppercaseLetter($returnType)) {
continue;
}

throw new \InvalidArgumentException('Return type "' . $returnType . '" is discouraged');
}
}
}
57 changes: 57 additions & 0 deletions tests/rules/doc/allowed/ReturnTypeArrayGenerics.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
<?php

// @expectedPass

namespace flyeralarm\Test;

class FooTest
{
/**
* @return array
*/
public function testArray()
{
}

/**
* @return array<string>
*/
public function testWithGeneric()
{
}

/**
* @return array<int, string>
*/
public function testWithNumericIndex()
{
}

/**
* @return array<string | int>
*/
public function testWithAlternativeInside()
{
}

/**
* @return int | array<array<string>>
*/
public function testWithAlternativeOutside()
{
}

/**
* @return array<array<string>> | null
*/
public function testWithAlternativeAtTheEnd()
{
}

/**
* @return int | array<string | array<string>>
*/
public function testWithMultipleAlternatives()
{
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<?php

// @expectedError Missing return type in first alternative of type "| string"

class EmptyAlternativeInReturnTypeInDocComment
{
/**
* @return | string
*/
public function foo()
{
return true;
}
}
14 changes: 14 additions & 0 deletions tests/rules/doc/not-allowed/EmptyGenericReturnTypeInDocComment.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<?php

// @expectedError Generic specification may not be empty in type "array<>"

class EmptyGenericReturnTypeInDocComment
{
/**
* @return array<>
*/
public function foo()
{
return true;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<?php

// @expectedError Invalid structure in return type "array<int"

class InvalidStructureInReturnTypeInDocComment
{
/**
* @return array<int
*/
public function foo()
{
return true;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<?php

// @expectedError Return type "array int" is discouraged

class MissingAlternativeSeparatorInReturnTypeInDocComment
{
/**
* @return array int
*/
public function foo()
{
return true;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<?php

// @expectedError Unexpected generic specification in type "int<array>"

class UnknownGenericReturnTypeInDocComment
{
/**
* @return int<array>
*/
public function foo()
{
return true;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<?php

// @expectedError Return type "foo" is discouraged

class UnknownGenericSubReturnTypeInDocComment
{
/**
* @return array<foo>
*/
public function foo()
{
return true;
}
}