Skip to content

Commit 241ddec

Browse files
authored
Add support for enum methods (#289)
* Add tests for enums * Add enum support
1 parent 6270149 commit 241ddec

File tree

5 files changed

+213
-14
lines changed

5 files changed

+213
-14
lines changed
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
<?php
2+
3+
namespace VariableAnalysis\Tests\VariableAnalysisSniff;
4+
5+
use VariableAnalysis\Tests\BaseTestCase;
6+
7+
class EnumTest extends BaseTestCase
8+
{
9+
public function testEnum()
10+
{
11+
$fixtureFile = $this->getFixture('EnumFixture.php');
12+
$phpcsFile = $this->prepareLocalFileForSniffs($fixtureFile);
13+
$phpcsFile->process();
14+
$lines = $this->getWarningLineNumbersFromFile($phpcsFile);
15+
$expectedWarnings = [
16+
33,
17+
];
18+
$this->assertEquals($expectedWarnings, $lines);
19+
}
20+
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
<?php
2+
3+
enum Suit
4+
{
5+
case Hearts;
6+
case Diamonds;
7+
case Clubs;
8+
case Spades;
9+
}
10+
11+
enum BackedSuit: string
12+
{
13+
case Hearts = 'H';
14+
case Diamonds = 'D';
15+
case Clubs = 'C';
16+
case Spades = 'S';
17+
}
18+
19+
enum Numbers: string {
20+
case ONE = '1';
21+
case TWO = '2';
22+
case THREE = '3';
23+
case FOUR = '4';
24+
25+
public function divisibility(): string {
26+
return match ($this) {
27+
self::ONE, self::THREE => 'odd',
28+
self::TWO, self::FOUR => 'even',
29+
};
30+
}
31+
32+
public function foobar(): string {
33+
return match ($foo) { // undefined variable $foo
34+
'x' => 'first',
35+
'y' => 'second',
36+
default => 'unknown',
37+
};
38+
}
39+
}

VariableAnalysis/Lib/EnumInfo.php

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
<?php
2+
3+
namespace VariableAnalysis\Lib;
4+
5+
/**
6+
* Holds details of an enum.
7+
*/
8+
class EnumInfo
9+
{
10+
/**
11+
* The position of the `enum` token.
12+
*
13+
* @var int
14+
*/
15+
public $enumIndex;
16+
17+
/**
18+
* The position of the block opener (curly brace) for the enum.
19+
*
20+
* @var int
21+
*/
22+
public $blockStart;
23+
24+
/**
25+
* The position of the block closer (curly brace) for the enum.
26+
*
27+
* @var int
28+
*/
29+
public $blockEnd;
30+
31+
/**
32+
* @param int $enumIndex
33+
* @param int $blockStart
34+
* @param int $blockEnd
35+
*/
36+
public function __construct(
37+
$enumIndex,
38+
$blockStart,
39+
$blockEnd
40+
) {
41+
$this->enumIndex = $enumIndex;
42+
$this->blockStart = $blockStart;
43+
$this->blockEnd = $blockEnd;
44+
}
45+
}

VariableAnalysis/Lib/Helpers.php

Lines changed: 51 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
use PHP_CodeSniffer\Files\File;
66
use VariableAnalysis\Lib\ScopeInfo;
77
use VariableAnalysis\Lib\ForLoopInfo;
8+
use VariableAnalysis\Lib\EnumInfo;
89
use VariableAnalysis\Lib\ScopeType;
910
use VariableAnalysis\Lib\VariableInfo;
1011
use PHP_CodeSniffer\Util\Tokens;
@@ -79,14 +80,20 @@ public static function findContainingOpeningBracket(File $phpcsFile, $stackPtr)
7980
}
8081

8182
/**
82-
* @param (int|string)[] $conditions
83+
* @param array{conditions: (int|string)[], content: string} $token
8384
*
8485
* @return bool
8586
*/
86-
public static function areAnyConditionsAClass(array $conditions)
87+
public static function areAnyConditionsAClass(array $token)
8788
{
89+
$conditions = $token['conditions'];
90+
$classlikeCodes = [T_CLASS, T_ANON_CLASS, T_TRAIT];
91+
if (defined('T_ENUM')) {
92+
$classlikeCodes[] = T_ENUM;
93+
}
94+
$classlikeCodes[] = 'PHPCS_T_ENUM';
8895
foreach (array_reverse($conditions, true) as $scopeCode) {
89-
if ($scopeCode === T_CLASS || $scopeCode === T_ANON_CLASS || $scopeCode === T_TRAIT) {
96+
if (in_array($scopeCode, $classlikeCodes, true)) {
9097
return true;
9198
}
9299
}
@@ -97,15 +104,20 @@ public static function areAnyConditionsAClass(array $conditions)
97104
* Return true if the token conditions are within a function before they are
98105
* within a class.
99106
*
100-
* @param (int|string)[] $conditions
107+
* @param array{conditions: (int|string)[], content: string} $token
101108
*
102109
* @return bool
103110
*/
104-
public static function areConditionsWithinFunctionBeforeClass(array $conditions)
111+
public static function areConditionsWithinFunctionBeforeClass(array $token)
105112
{
106-
$classTypes = [T_CLASS, T_ANON_CLASS, T_TRAIT];
113+
$conditions = $token['conditions'];
114+
$classlikeCodes = [T_CLASS, T_ANON_CLASS, T_TRAIT];
115+
if (defined('T_ENUM')) {
116+
$classlikeCodes[] = T_ENUM;
117+
}
118+
$classlikeCodes[] = 'PHPCS_T_ENUM';
107119
foreach (array_reverse($conditions, true) as $scopeCode) {
108-
if (in_array($scopeCode, $classTypes)) {
120+
if (in_array($scopeCode, $classlikeCodes)) {
109121
return false;
110122
}
111123
if ($scopeCode === T_FUNCTION) {
@@ -1282,6 +1294,38 @@ public static function isTokenVariableVariable(File $phpcsFile, $stackPtr)
12821294
return false;
12831295
}
12841296

1297+
/**
1298+
* @param File $phpcsFile
1299+
* @param int $stackPtr
1300+
*
1301+
* @return EnumInfo
1302+
*/
1303+
public static function makeEnumInfo(File $phpcsFile, $stackPtr)
1304+
{
1305+
$tokens = $phpcsFile->getTokens();
1306+
$token = $tokens[$stackPtr];
1307+
1308+
if (isset($token['scope_opener'])) {
1309+
$blockStart = $token['scope_opener'];
1310+
$blockEnd = $token['scope_closer'];
1311+
} else {
1312+
// Enums before phpcs could detect them do not have scopes so we have to
1313+
// find them ourselves.
1314+
1315+
$blockStart = $phpcsFile->findNext([T_OPEN_CURLY_BRACKET], $stackPtr + 1);
1316+
if (! is_int($blockStart)) {
1317+
throw new \Exception("Cannot find enum start at position {$stackPtr}");
1318+
}
1319+
$blockEnd = $tokens[$blockStart]['bracket_closer'];
1320+
}
1321+
1322+
return new EnumInfo(
1323+
$stackPtr,
1324+
$blockStart,
1325+
$blockEnd
1326+
);
1327+
}
1328+
12851329
/**
12861330
* @param File $phpcsFile
12871331
* @param int $stackPtr

VariableAnalysis/Sniffs/CodeAnalysis/VariableAnalysisSniff.php

Lines changed: 58 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ class VariableAnalysisSniff implements Sniff
1717
/**
1818
* The current phpcsFile being checked.
1919
*
20-
* @var File|null phpcsFile
20+
* @var File|null
2121
*/
2222
protected $currentFile = null;
2323

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

36+
/**
37+
* A list of enum blocks, keyed by the index of their first token in this file.
38+
*
39+
* @var array<int, \VariableAnalysis\Lib\EnumInfo>
40+
*/
41+
private $enums = [];
42+
3643
/**
3744
* A list of custom functions which pass in variables to be initialized by
3845
* reference (eg `preg_match()`) and therefore should not require those
@@ -175,6 +182,9 @@ public function register()
175182
if (defined('T_FN')) {
176183
$types[] = T_FN;
177184
}
185+
if (defined('T_ENUM')) {
186+
$types[] = T_ENUM;
187+
}
178188
return $types;
179189
}
180190

@@ -226,6 +236,7 @@ public function process(File $phpcsFile, $stackPtr)
226236
if ($this->currentFile !== $phpcsFile) {
227237
$this->currentFile = $phpcsFile;
228238
$this->forLoops = [];
239+
$this->enums = [];
229240
}
230241

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

279+
// Record enums so we can detect them even before phpcs was able to.
280+
if ($token['content'] === 'enum') {
281+
$this->recordEnum($phpcsFile, $stackPtr);
282+
return;
283+
}
284+
268285
// If the current token is a call to `get_defined_vars()`, consider that a
269286
// usage of all variables in the current scope.
270287
if ($this->isGetDefinedVars($phpcsFile, $stackPtr)) {
@@ -286,6 +303,19 @@ public function process(File $phpcsFile, $stackPtr)
286303
}
287304
}
288305

306+
/**
307+
* Record the boundaries of an enum.
308+
*
309+
* @param File $phpcsFile
310+
* @param int $stackPtr
311+
*
312+
* @return void
313+
*/
314+
private function recordEnum($phpcsFile, $stackPtr)
315+
{
316+
$this->enums[$stackPtr] = Helpers::makeEnumInfo($phpcsFile, $stackPtr);
317+
}
318+
289319
/**
290320
* Record the boundaries of a for loop.
291321
*
@@ -857,9 +887,11 @@ protected function processVariableAsClassProperty(File $phpcsFile, $stackPtr)
857887
// define variables, so make sure we are not in a function before
858888
// assuming it's a property.
859889
$tokens = $phpcsFile->getTokens();
860-
$token = $tokens[$stackPtr];
861-
if ($token && !empty($token['conditions']) && !Helpers::areConditionsWithinFunctionBeforeClass($token['conditions'])) {
862-
return Helpers::areAnyConditionsAClass($token['conditions']);
890+
891+
/** @var array{conditions?: (int|string)[], content?: string}|null */
892+
$token = $tokens[$stackPtr];
893+
if ($token && !empty($token['conditions']) && !empty($token['content']) && !Helpers::areConditionsWithinFunctionBeforeClass($token)) {
894+
return Helpers::areAnyConditionsAClass($token);
863895
}
864896
return false;
865897
}
@@ -925,13 +957,30 @@ protected function processVariableAsThisWithinClass(File $phpcsFile, $stackPtr,
925957
return false;
926958
}
927959

960+
// Handle enums specially since their condition may not exist in old phpcs.
961+
$inEnum = false;
962+
foreach ($this->enums as $enum) {
963+
if ($stackPtr > $enum->blockStart && $stackPtr < $enum->blockEnd) {
964+
$inEnum = true;
965+
}
966+
}
967+
928968
$inFunction = false;
929969
foreach (array_reverse($token['conditions'], true) as $scopeCode) {
930970
// $this within a closure is valid
931971
if ($scopeCode === T_CLOSURE && $inFunction === false) {
932972
return true;
933973
}
934-
if ($scopeCode === T_CLASS || $scopeCode === T_ANON_CLASS || $scopeCode === T_TRAIT) {
974+
975+
$classlikeCodes = [T_CLASS, T_ANON_CLASS, T_TRAIT];
976+
if (defined('T_ENUM')) {
977+
$classlikeCodes[] = T_ENUM;
978+
}
979+
if (in_array($scopeCode, $classlikeCodes, true)) {
980+
return true;
981+
}
982+
983+
if ($scopeCode === T_FUNCTION && $inEnum) {
935984
return true;
936985
}
937986

@@ -1033,7 +1082,9 @@ protected function processVariableAsStaticOutsideClass(File $phpcsFile, $stackPt
10331082
// Are we refering to self:: outside a class?
10341083
10351084
$tokens = $phpcsFile->getTokens();
1036-
$token = $tokens[$stackPtr];
1085+
1086+
/** @var array{conditions?: (int|string)[], content?: string}|null */
1087+
$token = $tokens[$stackPtr];
10371088

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

0 commit comments

Comments
 (0)