Skip to content

Commit

Permalink
Add support for enum methods (#289)
Browse files Browse the repository at this point in the history
* Add tests for enums

* Add enum support
  • Loading branch information
sirbrillig authored Mar 12, 2023
1 parent 6270149 commit 241ddec
Show file tree
Hide file tree
Showing 5 changed files with 213 additions and 14 deletions.
20 changes: 20 additions & 0 deletions Tests/VariableAnalysisSniff/EnumTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<?php

namespace VariableAnalysis\Tests\VariableAnalysisSniff;

use VariableAnalysis\Tests\BaseTestCase;

class EnumTest extends BaseTestCase
{
public function testEnum()
{
$fixtureFile = $this->getFixture('EnumFixture.php');
$phpcsFile = $this->prepareLocalFileForSniffs($fixtureFile);
$phpcsFile->process();
$lines = $this->getWarningLineNumbersFromFile($phpcsFile);
$expectedWarnings = [
33,
];
$this->assertEquals($expectedWarnings, $lines);
}
}
39 changes: 39 additions & 0 deletions Tests/VariableAnalysisSniff/fixtures/EnumFixture.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
<?php

enum Suit
{
case Hearts;
case Diamonds;
case Clubs;
case Spades;
}

enum BackedSuit: string
{
case Hearts = 'H';
case Diamonds = 'D';
case Clubs = 'C';
case Spades = 'S';
}

enum Numbers: string {
case ONE = '1';
case TWO = '2';
case THREE = '3';
case FOUR = '4';

public function divisibility(): string {
return match ($this) {
self::ONE, self::THREE => 'odd',
self::TWO, self::FOUR => 'even',
};
}

public function foobar(): string {
return match ($foo) { // undefined variable $foo
'x' => 'first',
'y' => 'second',
default => 'unknown',
};
}
}
45 changes: 45 additions & 0 deletions VariableAnalysis/Lib/EnumInfo.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
<?php

namespace VariableAnalysis\Lib;

/**
* Holds details of an enum.
*/
class EnumInfo
{
/**
* The position of the `enum` token.
*
* @var int
*/
public $enumIndex;

/**
* The position of the block opener (curly brace) for the enum.
*
* @var int
*/
public $blockStart;

/**
* The position of the block closer (curly brace) for the enum.
*
* @var int
*/
public $blockEnd;

/**
* @param int $enumIndex
* @param int $blockStart
* @param int $blockEnd
*/
public function __construct(
$enumIndex,
$blockStart,
$blockEnd
) {
$this->enumIndex = $enumIndex;
$this->blockStart = $blockStart;
$this->blockEnd = $blockEnd;
}
}
58 changes: 51 additions & 7 deletions VariableAnalysis/Lib/Helpers.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
use PHP_CodeSniffer\Files\File;
use VariableAnalysis\Lib\ScopeInfo;
use VariableAnalysis\Lib\ForLoopInfo;
use VariableAnalysis\Lib\EnumInfo;
use VariableAnalysis\Lib\ScopeType;
use VariableAnalysis\Lib\VariableInfo;
use PHP_CodeSniffer\Util\Tokens;
Expand Down Expand Up @@ -79,14 +80,20 @@ public static function findContainingOpeningBracket(File $phpcsFile, $stackPtr)
}

/**
* @param (int|string)[] $conditions
* @param array{conditions: (int|string)[], content: string} $token
*
* @return bool
*/
public static function areAnyConditionsAClass(array $conditions)
public static function areAnyConditionsAClass(array $token)
{
$conditions = $token['conditions'];
$classlikeCodes = [T_CLASS, T_ANON_CLASS, T_TRAIT];
if (defined('T_ENUM')) {
$classlikeCodes[] = T_ENUM;
}
$classlikeCodes[] = 'PHPCS_T_ENUM';
foreach (array_reverse($conditions, true) as $scopeCode) {
if ($scopeCode === T_CLASS || $scopeCode === T_ANON_CLASS || $scopeCode === T_TRAIT) {
if (in_array($scopeCode, $classlikeCodes, true)) {
return true;
}
}
Expand All @@ -97,15 +104,20 @@ public static function areAnyConditionsAClass(array $conditions)
* Return true if the token conditions are within a function before they are
* within a class.
*
* @param (int|string)[] $conditions
* @param array{conditions: (int|string)[], content: string} $token
*
* @return bool
*/
public static function areConditionsWithinFunctionBeforeClass(array $conditions)
public static function areConditionsWithinFunctionBeforeClass(array $token)
{
$classTypes = [T_CLASS, T_ANON_CLASS, T_TRAIT];
$conditions = $token['conditions'];
$classlikeCodes = [T_CLASS, T_ANON_CLASS, T_TRAIT];
if (defined('T_ENUM')) {
$classlikeCodes[] = T_ENUM;
}
$classlikeCodes[] = 'PHPCS_T_ENUM';
foreach (array_reverse($conditions, true) as $scopeCode) {
if (in_array($scopeCode, $classTypes)) {
if (in_array($scopeCode, $classlikeCodes)) {
return false;
}
if ($scopeCode === T_FUNCTION) {
Expand Down Expand Up @@ -1282,6 +1294,38 @@ public static function isTokenVariableVariable(File $phpcsFile, $stackPtr)
return false;
}

/**
* @param File $phpcsFile
* @param int $stackPtr
*
* @return EnumInfo
*/
public static function makeEnumInfo(File $phpcsFile, $stackPtr)
{
$tokens = $phpcsFile->getTokens();
$token = $tokens[$stackPtr];

if (isset($token['scope_opener'])) {
$blockStart = $token['scope_opener'];
$blockEnd = $token['scope_closer'];
} else {
// Enums before phpcs could detect them do not have scopes so we have to
// find them ourselves.

$blockStart = $phpcsFile->findNext([T_OPEN_CURLY_BRACKET], $stackPtr + 1);
if (! is_int($blockStart)) {
throw new \Exception("Cannot find enum start at position {$stackPtr}");
}
$blockEnd = $tokens[$blockStart]['bracket_closer'];
}

return new EnumInfo(
$stackPtr,
$blockStart,
$blockEnd
);
}

/**
* @param File $phpcsFile
* @param int $stackPtr
Expand Down
65 changes: 58 additions & 7 deletions VariableAnalysis/Sniffs/CodeAnalysis/VariableAnalysisSniff.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ class VariableAnalysisSniff implements Sniff
/**
* The current phpcsFile being checked.
*
* @var File|null phpcsFile
* @var File|null
*/
protected $currentFile = null;

Expand All @@ -33,6 +33,13 @@ class VariableAnalysisSniff implements Sniff
*/
private $forLoops = [];

/**
* A list of enum blocks, keyed by the index of their first token in this file.
*
* @var array<int, \VariableAnalysis\Lib\EnumInfo>
*/
private $enums = [];

/**
* A list of custom functions which pass in variables to be initialized by
* reference (eg `preg_match()`) and therefore should not require those
Expand Down Expand Up @@ -175,6 +182,9 @@ public function register()
if (defined('T_FN')) {
$types[] = T_FN;
}
if (defined('T_ENUM')) {
$types[] = T_ENUM;
}
return $types;
}

Expand Down Expand Up @@ -226,6 +236,7 @@ public function process(File $phpcsFile, $stackPtr)
if ($this->currentFile !== $phpcsFile) {
$this->currentFile = $phpcsFile;
$this->forLoops = [];
$this->enums = [];
}

// Add the global scope for the current file to our scope indexes.
Expand Down Expand Up @@ -265,6 +276,12 @@ public function process(File $phpcsFile, $stackPtr)
return;
}

// Record enums so we can detect them even before phpcs was able to.
if ($token['content'] === 'enum') {
$this->recordEnum($phpcsFile, $stackPtr);
return;
}

// If the current token is a call to `get_defined_vars()`, consider that a
// usage of all variables in the current scope.
if ($this->isGetDefinedVars($phpcsFile, $stackPtr)) {
Expand All @@ -286,6 +303,19 @@ public function process(File $phpcsFile, $stackPtr)
}
}

/**
* Record the boundaries of an enum.
*
* @param File $phpcsFile
* @param int $stackPtr
*
* @return void
*/
private function recordEnum($phpcsFile, $stackPtr)
{
$this->enums[$stackPtr] = Helpers::makeEnumInfo($phpcsFile, $stackPtr);
}

/**
* Record the boundaries of a for loop.
*
Expand Down Expand Up @@ -857,9 +887,11 @@ protected function processVariableAsClassProperty(File $phpcsFile, $stackPtr)
// define variables, so make sure we are not in a function before
// assuming it's a property.
$tokens = $phpcsFile->getTokens();
$token = $tokens[$stackPtr];
if ($token && !empty($token['conditions']) && !Helpers::areConditionsWithinFunctionBeforeClass($token['conditions'])) {
return Helpers::areAnyConditionsAClass($token['conditions']);

/** @var array{conditions?: (int|string)[], content?: string}|null */
$token = $tokens[$stackPtr];
if ($token && !empty($token['conditions']) && !empty($token['content']) && !Helpers::areConditionsWithinFunctionBeforeClass($token)) {
return Helpers::areAnyConditionsAClass($token);
}
return false;
}
Expand Down Expand Up @@ -925,13 +957,30 @@ protected function processVariableAsThisWithinClass(File $phpcsFile, $stackPtr,
return false;
}

// Handle enums specially since their condition may not exist in old phpcs.
$inEnum = false;
foreach ($this->enums as $enum) {
if ($stackPtr > $enum->blockStart && $stackPtr < $enum->blockEnd) {
$inEnum = true;
}
}

$inFunction = false;
foreach (array_reverse($token['conditions'], true) as $scopeCode) {
// $this within a closure is valid
if ($scopeCode === T_CLOSURE && $inFunction === false) {
return true;
}
if ($scopeCode === T_CLASS || $scopeCode === T_ANON_CLASS || $scopeCode === T_TRAIT) {

$classlikeCodes = [T_CLASS, T_ANON_CLASS, T_TRAIT];
if (defined('T_ENUM')) {
$classlikeCodes[] = T_ENUM;
}
if (in_array($scopeCode, $classlikeCodes, true)) {
return true;
}

if ($scopeCode === T_FUNCTION && $inEnum) {
return true;
}

Expand Down Expand Up @@ -1033,7 +1082,9 @@ protected function processVariableAsStaticOutsideClass(File $phpcsFile, $stackPt
// Are we refering to self:: outside a class?
$tokens = $phpcsFile->getTokens();
$token = $tokens[$stackPtr];

/** @var array{conditions?: (int|string)[], content?: string}|null */
$token = $tokens[$stackPtr];

$doubleColonPtr = $phpcsFile->findPrevious(Tokens::$emptyTokens, $stackPtr - 1, null, true);
if ($doubleColonPtr === false || $tokens[$doubleColonPtr]['code'] !== T_DOUBLE_COLON) {
Expand All @@ -1053,7 +1104,7 @@ protected function processVariableAsStaticOutsideClass(File $phpcsFile, $stackPt
}
$errorClass = $code === T_SELF ? 'SelfOutsideClass' : 'StaticOutsideClass';
$staticRefType = $code === T_SELF ? 'self::' : 'static::';
if (!empty($token['conditions']) && Helpers::areAnyConditionsAClass($token['conditions'])) {
if (!empty($token['conditions']) && !empty($token['content']) && Helpers::areAnyConditionsAClass($token)) {
return false;
}
$phpcsFile->addError(
Expand Down

0 comments on commit 241ddec

Please sign in to comment.