Skip to content

Commit

Permalink
Merge pull request #137 from PHPCSStandards/universal/new-constructor…
Browse files Browse the repository at this point in the history
…destructorreturn-sniff

✨ New `Universal.CodeAnalysis.ConstructorDestructorReturn` sniff
  • Loading branch information
jrfnl committed Oct 26, 2022
2 parents 7e1cae2 + 52456f6 commit 8304a55
Show file tree
Hide file tree
Showing 4 changed files with 392 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
<?xml version="1.0"?>
<documentation xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="https://phpcsstandards.github.io/PHPCSDevTools/phpcsdocs.xsd"
title="Constructor Destructor Return"
>
<standard>
<![CDATA[
A class constructor/destructor can not have a return type declarations. This would result in a fatal error.
]]>
</standard>
<code_comparison>
<code title="Valid: no return type declaration.">
<![CDATA[
class Foo {
public function __construct() {}
}
]]>
</code>
<code title="Invalid: return type declaration.">
<![CDATA[
class Foo {
public function __construct()<em>: int</em> {}
}
]]>
</code>
</code_comparison>

<standard>
<![CDATA[
A class constructor/destructor should not return anything.
]]>
</standard>
<code_comparison>
<code title="Valid: class constructor/destructor doesn't return anything.">
<![CDATA[
class Foo {
public function __construct() {
// Do something.
}
public function __destruct() {
// Do something.
return;
}
}
]]>
</code>
<code title="Invalid: class constructor/destructor returns a value.">
<![CDATA[
class Foo {
public function __construct() {
// Do something.
return <em>$this</em>;
}
public function __destruct() {
// Do something.
return <em>false</em>;
}
}
]]>
</code>
</code_comparison>
</documentation>
145 changes: 145 additions & 0 deletions Universal/Sniffs/CodeAnalysis/ConstructorDestructorReturnSniff.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
<?php
/**
* PHPCSExtra, a collection of sniffs and standards for use with PHP_CodeSniffer.
*
* @package PHPCSExtra
* @copyright 2020 PHPCSExtra Contributors
* @license https://opensource.org/licenses/LGPL-3.0 LGPL3
* @link https://github.com/PHPCSStandards/PHPCSExtra
*/

namespace PHPCSExtra\Universal\Sniffs\CodeAnalysis;

use PHP_CodeSniffer\Files\File;
use PHP_CodeSniffer\Sniffs\Sniff;
use PHP_CodeSniffer\Util\Tokens;
use PHPCSUtils\BackCompat\BCFile;
use PHPCSUtils\Utils\FunctionDeclarations;
use PHPCSUtils\Utils\GetTokensAsString;
use PHPCSUtils\Utils\NamingConventions;
use PHPCSUtils\Utils\ObjectDeclarations;
use PHPCSUtils\Utils\Scopes;

/**
* Verify that a class constructor/destructor does not return anything, nor has a
* return type declaration (fatal error).
*
* @since 1.0.0
*/
final class ConstructorDestructorReturnSniff implements Sniff
{

/**
* Registers the tokens that this sniff wants to listen for.
*
* @since 1.0.0
*
* @return int[]
*/
public function register()
{
return [\T_FUNCTION];
}

/**
* Processes this test, when one of its tokens is encountered.
*
* @since 1.0.0
*
* @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned.
* @param int $stackPtr The position of the current token
* in the stack passed in $tokens.
*
* @return void
*/
public function process(File $phpcsFile, $stackPtr)
{
$scopePtr = Scopes::validDirectScope($phpcsFile, $stackPtr, Tokens::$ooScopeTokens);
if ($scopePtr === false) {
// Not an OO method.
return;
}

$functionName = FunctionDeclarations::getName($phpcsFile, $stackPtr);
$functionNameLC = \strtolower($functionName);

if ($functionNameLC === '__construct' || $functionNameLC === '__destruct') {
$functionType = \sprintf('A "%s()" magic method', $functionNameLC);
} else {
// This may be a PHP 4-style constructor.
$OOName = ObjectDeclarations::getName($phpcsFile, $scopePtr);

if (empty($OOName) === true) {
// Anonymous class or parse error. The function can't be a PHP 4-style constructor.
return;
}

if (NamingConventions::isEqual($functionName, $OOName) === false) {
// Class and function name not the same, so not a PHP 4-style constructor.
return;
}

$functionType = 'A PHP 4-style constructor';
}

/*
* OK, so now we know for sure that this is a constructor/destructor method.
*/

// Check for a return type.
$properties = FunctionDeclarations::getProperties($phpcsFile, $stackPtr);
if ($properties['return_type'] !== '' && $properties['return_type_token'] !== false) {
$data = [
$functionType,
$properties['return_type'],
];

$phpcsFile->addError(
'%s can not declare a return type. Found: %s',
$properties['return_type_token'],
'ReturnTypeFound',
$data
);
}

$tokens = $phpcsFile->getTokens();
if (isset($tokens[$stackPtr]['scope_opener'], $tokens[$stackPtr]['scope_closer']) === false) {
// Abstract/interface method, live coding or parse error.
return;
}

// Check for a value being returned.
$current = $tokens[$stackPtr]['scope_opener'];
$end = $tokens[$stackPtr]['scope_closer'];

do {
$current = $phpcsFile->findNext(\T_RETURN, ($current + 1), $end);
if ($current === false) {
break;
}

$next = $phpcsFile->findNext(Tokens::$emptyTokens, ($current + 1), $end, true);
if ($next === false
|| $tokens[$next]['code'] === \T_SEMICOLON
|| $tokens[$next]['code'] === \T_CLOSE_TAG
) {
// Return statement without value.
continue;
}

$endOfStatement = BCFile::findEndOfStatement($phpcsFile, $next);

$data = [
$functionType,
GetTokensAsString::compact($phpcsFile, $current, $endOfStatement, true),
];

$phpcsFile->addWarning(
'%s can not return a value. Found: "%s"',
$current,
'ReturnValueFound',
$data
);
} while ($current < $end);
}
}
125 changes: 125 additions & 0 deletions Universal/Tests/CodeAnalysis/ConstructorDestructorReturnUnitTest.inc
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
<?php

/*
* OK.
*/

// Global function, not a constructor/destructor.
function __construct() : int {
return 123;
}

function __destruct() : float {
return 321;
}

// Methods which are not constructors can be disregarded.
class NotAConstructor {
public function __set($name, $value): void {
// Do something.
}

public function Foo(): void {
// Do something.
}

public function Bar() {
return $this;
}
}

// Constructor/Destructor without a return statement or return type.
class NoReturn {
protected function __construct()
{
// Do something.
}

protected function __destruct()
{
// Do something.
}

// Also applies to PHP4-style constructors/destructors.
function NoReturn()
{
// Do something.
}

function _NoReturn()
{
// Do something.
}
}

// Constructor/Destructor with return statement, but no value.
$anon = new class extends ReturnNoValue {
public function __construct() {
if ($foo) {
return ;
} else {
return /* comments are fine */
// Even when spread over multiple lines.
;
}

return;
}

public function __destruct() {
// Do something.
return;
}

// Non-constructor/destructor method returning.
public function returnsavalue() {
return 'php4style';
}
};


/*
* Not OK.
*/
class ReturnsAValue {
public function __construct(): self {
return $this;
}

public function __destruct():string {
return 'destructed';
}

function returnsavalue()
{
return 'php4style';
}
}

$anon = new class() extends ReturnsAValue {
public function __Construct()
: static|self
{
return $this;
}

public function __deStRucT() {
return 'destructed';
}
};

/*
* Return types are not allowed on constructor/destructor methods (fatal error).
*/

trait AbstractConstructorDestructorReturnTypes {
abstract public function __construct() : int;

abstract public function __destruct():string;
}

interface InterfaceMethodReturnTypes {
public function __construct(): void;

public function __destruct(): void;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
<?php
/**
* PHPCSExtra, a collection of sniffs and standards for use with PHP_CodeSniffer.
*
* @package PHPCSExtra
* @copyright 2020 PHPCSExtra Contributors
* @license https://opensource.org/licenses/LGPL-3.0 LGPL3
* @link https://github.com/PHPCSStandards/PHPCSExtra
*/

namespace PHPCSExtra\Universal\Tests\CodeAnalysis;

use PHP_CodeSniffer\Tests\Standards\AbstractSniffUnitTest;

/**
* Unit test class for the ConstructorDestructorReturn sniff.
*
* @covers PHPCSExtra\Universal\Sniffs\CodeAnalysis\ConstructorDestructorReturnSniff
*
* @since 1.0.0
*/
final class ConstructorDestructorReturnUnitTest extends AbstractSniffUnitTest
{

/**
* Returns the lines where errors should occur.
*
* @return array <int line number> => <int number of errors>
*/
public function getErrorList()
{
return [
85 => 1,
89 => 1,
101 => 1,
116 => 1,
118 => 1,
122 => 1,
124 => 1,
];
}

/**
* Returns the lines where warnings should occur.
*
* @return array <int line number> => <int number of warnings>
*/
public function getWarningList()
{
return [
86 => 1,
90 => 1,
95 => 1,
103 => 1,
107 => 1,
];
}
}

0 comments on commit 8304a55

Please sign in to comment.