Skip to content

Commit

Permalink
Merge pull request #243 from PHPCSStandards/feature/new-final-methods…
Browse files Browse the repository at this point in the history
…-in-traits-sniff

✨ New `Universal.OOStructures.RequireFinalMethodsInTraits` sniff
  • Loading branch information
jrfnl committed Jun 18, 2023
2 parents e5e767f + ab858ff commit 289f035
Show file tree
Hide file tree
Showing 5 changed files with 473 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
<?xml version="1.0"?>
<documentation xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="https://phpcsstandards.github.io/PHPCSDevTools/phpcsdocs.xsd"
title="Require Final Methods in Traits"
>
<standard>
<![CDATA[
Requires the use of the `final` keyword for non-abstract, non-private methods in traits.
By default, magic methods are exempt from this check. Magic methods can be enforced to be `final` too by setting the `includeMagicMethods` property to `true`.
]]>
</standard>
<code_comparison>
<code title="Valid: Final methods in a trait.">
<![CDATA[
trait Foo {
<em>final</em> public function bar() {}
<em>final</em> public static function baz() {}
// Also valid (out of scope):
protected abstract function overload() {}
private function okay() {}
}
]]>
</code>
<code title="Invalid: Non-final methods in a trait.">
<![CDATA[
trait Foo {
<em>public function</em> bar() {}
<em>protected static function</em> baz() {}
}
]]>
</code>
</code_comparison>
</documentation>
129 changes: 129 additions & 0 deletions Universal/Sniffs/OOStructures/RequireFinalMethodsInTraitsSniff.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
<?php
/**
* PHPCSExtra, a collection of sniffs and standards for use with PHP_CodeSniffer.
*
* @package PHPCSExtra
* @copyright 2023 PHPCSExtra Contributors
* @license https://opensource.org/licenses/LGPL-3.0 LGPL3
* @link https://github.com/PHPCSStandards/PHPCSExtra
*/

namespace PHPCSExtra\Universal\Sniffs\OOStructures;

use PHP_CodeSniffer\Files\File;
use PHP_CodeSniffer\Sniffs\Sniff;
use PHPCSUtils\Utils\FunctionDeclarations;
use PHPCSUtils\Utils\ObjectDeclarations;
use PHPCSUtils\Utils\Scopes;

/**
* Require non-abstract, non-private methods in traits to be declared as "final".
*
* @since 1.1.0
*/
final class RequireFinalMethodsInTraitsSniff implements Sniff
{

/**
* Name of the metric.
*
* @since 1.1.0
*
* @var string
*/
const METRIC_NAME = 'Non-private method in trait is abstract or final ?';

/**
* Whether or not this rule applies to magic methods.
*
* Defaults to `false`.
*
* @since 1.1.0
*
* @var bool
*/
public $includeMagicMethods = false;

/**
* Returns an array of tokens this test wants to listen for.
*
* @since 1.1.0
*
* @return array
*/
public function register()
{
return [\T_FUNCTION];
}

/**
* Processes this test, when one of its tokens is encountered.
*
* @since 1.1.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)
{
$tokens = $phpcsFile->getTokens();
if (isset($tokens[$stackPtr]['parenthesis_opener']) === false) {
// Parse error/live coding.
return;
}

$scopePtr = Scopes::validDirectScope($phpcsFile, $stackPtr, \T_TRAIT);
if ($scopePtr === false) {
// Not a trait method.
return;
}

$methodName = FunctionDeclarations::getName($phpcsFile, $stackPtr);
if ($this->includeMagicMethods === false
&& FunctionDeclarations::isMagicMethodName($methodName) === true
) {
// Magic methods are excluded. Bow out.
return;
}

$methodProps = FunctionDeclarations::getProperties($phpcsFile, $stackPtr);
if ($methodProps['scope'] === 'private') {
// Private methods can't be final.
return;
}

if ($methodProps['is_final'] === true) {
// Already final, nothing to do.
$phpcsFile->recordMetric($stackPtr, self::METRIC_NAME, 'final');
return;
}

if ($methodProps['is_abstract'] === true) {
// Abstract classes can't be final.
$phpcsFile->recordMetric($stackPtr, self::METRIC_NAME, 'abstract');
return;
}

$phpcsFile->recordMetric($stackPtr, self::METRIC_NAME, 'not abstract, not final');

$data = [
$methodProps['scope'],
$methodName,
ObjectDeclarations::getName($phpcsFile, $scopePtr),
];

$fix = $phpcsFile->addFixableError(
'The non-abstract, %s method "%s()" in trait %s should be declared as final.',
$stackPtr,
'NonFinalMethodFound',
$data
);

if ($fix === true) {
$phpcsFile->fixer->addContentBefore($stackPtr, 'final ');
}
}
}
120 changes: 120 additions & 0 deletions Universal/Tests/OOStructures/RequireFinalMethodsInTraitsUnitTest.inc
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
<?php

/*
* OK.
*/
function NotOurTarget() {}

class FooA {
protected function NotOurTarget() {}
}

interface FooB {
function NotOurTarget();
}

enum FooC {
public function NotOurTarget();
}

$anon = new class {
function anonClassIsFinalByNature() {}
}

trait MethodsAreAlreadyFinal {
final public function publicFinalMethod();
protected final function protectedFinalMethod() {}
}

trait TheseAreExempt {
public abstract function publicAbstractMethodShouldNotBeFinal();
abstract protected function protectedAbstractMethodShouldNotBeFinal() {}

private function privateMethodsCannotBeFinal();

public function parseError
}

trait FinalMagicMethodsAreNotFlagged {
final public function __construct() {}
public final function __destruct() {}
public final function __clone() {}
final public function __debugInfo() {}
final public function __invoke() {}
public final function __get($name) {}
public final function __set($name, $value) {}
final public function __isset($name) {}
final public function __unset($name) {}
public final function __call($name, $arguments) {}
public static final function __callStatic($name, $arguments) {}
final public function __sleep() {}
public final function __toString() {}
public final static function __set_state($properties) {}
final public function __serialize() {}
final public function __unserialize($data) {}
}

trait MagicMethodsAreNotFlaggedByDefault {
public function __construct() {}
public function __destruct() {}
public function __clone() {}
public function __debugInfo() {}
public function __invoke() {}
public function __get($name) {}
public function __set($name, $value) {}
public function __isset($name) {}
public function __unset($name) {}
public function __call($name, $arguments) {}
public static function __callStatic($name, $arguments) {}
public function __sleep() {}
public function __toString() {}
public static function __set_state($properties) {}
public function __serialize() {}
public function __unserialize($data) {}
}


/*
* Bad.
*/
trait FixMe {
public function publicMethod() {}
protected function protectedMethod() {}

public static function publicStaticMethod() {}
static protected function protectedStaticMethod() {}

protected function __doubleUnderscoresButNotMagic() {}

/**
* Docblock
*/
public function withDocblock() {}

static /*comment*/ protected function withCommentBetweenKeywords() {}

/*comment*/ protected function withCommentBeforeKeyword() {}
}

// phpcs:set Universal.OOStructures.RequireFinalMethodsInTraits includeMagicMethods true
trait MagicMethodsAreFlaggedOnRequest {
public function __construct() {}
public function __destruct() {}
public function __clone() {}
public function __debugInfo() {}
public function __invoke() {}
public function __get($name) {}
public function __set($name, $value) {}
public function __isset($name) {}
public function __unset($name) {}
public function __call($name, $arguments) {}
public static function __callStatic($name, $arguments) {}
public function __sleep() {}
public function __toString() {}
public static function __set_state($properties) {}
public function __serialize() {}
public function __unserialize($data) {}
}

// Reset property to default value.
// phpcs:set Universal.OOStructures.RequireFinalMethodsInTraits includeMagicMethods false
Loading

0 comments on commit 289f035

Please sign in to comment.