Skip to content

Commit 1e7d737

Browse files
committed
WAIT - PR squizlabs#3867 tokenizer fix | Generic/LowerCaseConstant: improve performance
I noticed a PHPCS run on a particular code base being quite slow. Running the Performance report from PR 3810 showed the `Generic.PHP.LowerCaseConstant` sniff being the slowest sniff taking 27 seconds on a total sniff run time of 87 seconds (with a few hundred sniffs being run). A closer look at the sniff pointed to the "skip type declarations for typed properties" part of the sniff as the culprit - in combination with the code base in question having a lot of array properties containing large arrays with mostly `true/false/null` values. Every single one of those `true/false/null` values would trigger a condition check and then a check whether the token was in the property value, causing lots of token walking for those long arrays. This PR should fix that by changing the logic for skipping over the property type declaration. Instead of checking for each `true/false/null` whether it is a property type (or value), the sniff now listens to all property modifier keywords and skips over the type declaration from there, meaning that `true/false/null` found within property values will no longer need to do a conditions check/"am I a value or a type?" check. For this particular code base, with this change, the sniff run time goes down from 27 seconds to 0.102 seconds. Includes additional tests for the "property type skipping" code to verify it works correctly, but also that it won't cause issues with too much/the wrong things being skipped. > Note: an additional performance boost could be gained by not recording metrics and bowing out early for any `true/false/null` which are already lowercase, but that would change the functionality of the sniff.
1 parent b69629c commit 1e7d737

File tree

4 files changed

+154
-17
lines changed

4 files changed

+154
-17
lines changed

src/Standards/Generic/Sniffs/PHP/LowerCaseConstantSniff.php

Lines changed: 68 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,29 @@ class LowerCaseConstantSniff implements Sniff
3737
T_NULL => T_NULL,
3838
];
3939

40+
/**
41+
* Token types which can be encountered in a property type declaration.
42+
*
43+
* @var array<int|string, int|string>
44+
*/
45+
private $propertyTypeTokens = [
46+
T_CALLABLE => T_CALLABLE,
47+
T_SELF => T_SELF,
48+
T_PARENT => T_PARENT,
49+
T_FALSE => T_FALSE,
50+
T_TRUE => T_TRUE,
51+
T_NULL => T_NULL,
52+
T_STRING => T_STRING,
53+
T_NAME_QUALIFIED => T_NAME_QUALIFIED,
54+
T_NAME_FULLY_QUALIFIED => T_NAME_FULLY_QUALIFIED,
55+
T_NAME_RELATIVE => T_NAME_RELATIVE,
56+
T_NS_SEPARATOR => T_NS_SEPARATOR,
57+
T_NAMESPACE => T_NAMESPACE,
58+
T_TYPE_UNION => T_TYPE_UNION,
59+
T_TYPE_INTERSECTION => T_TYPE_INTERSECTION,
60+
T_NULLABLE => T_NULLABLE,
61+
];
62+
4063

4164
/**
4265
* Returns an array of tokens this test wants to listen for.
@@ -47,7 +70,13 @@ public function register()
4770
{
4871
$targets = $this->targets;
4972

50-
// Register function keywords to filter out type declarations.
73+
// Register scope modifiers to filter out property type declarations.
74+
$targets += Tokens::$scopeModifiers;
75+
$targets[] = T_VAR;
76+
$targets[] = T_STATIC;
77+
$targets[] = T_READONLY;
78+
79+
// Register function keywords to filter out param/return type declarations.
5180
$targets[] = T_FUNCTION;
5281
$targets[] = T_CLOSURE;
5382
$targets[] = T_FN;
@@ -64,12 +93,43 @@ public function register()
6493
* @param int $stackPtr The position of the current token in the
6594
* stack passed in $tokens.
6695
*
67-
* @return void
96+
* @return void|int Optionally returns a stack pointer. The sniff will not be
97+
* called again on the current file until the returned stack
98+
* pointer is reached.
6899
*/
69100
public function process(File $phpcsFile, $stackPtr)
70101
{
71102
$tokens = $phpcsFile->getTokens();
72103

104+
/*
105+
* Skip over type declarations for properties.
106+
*
107+
* Note: for other uses of the visibility modifiers (functions, constants, trait use),
108+
* nothing relevant will be skipped as the next non-empty token will be an "non-skippable"
109+
* one.
110+
* Functions are handled separately below (and then skip to their scope opener), so
111+
* this should also not cause any confusion for constructor property promotion.
112+
*
113+
* For other uses of the "static" keyword, it also shouldn't be problematic as the only
114+
* time the next non-empty token will be a "skippable" token will be in return type
115+
* declarations, in which case, it is correct to skip over them.
116+
*/
117+
118+
if (isset(Tokens::$scopeModifiers[$tokens[$stackPtr]['code']]) === true
119+
|| $tokens[$stackPtr]['code'] === T_VAR
120+
|| $tokens[$stackPtr]['code'] === T_STATIC
121+
|| $tokens[$stackPtr]['code'] === T_READONLY
122+
) {
123+
$skipOver = (Tokens::$emptyTokens + $this->propertyTypeTokens);
124+
$skipTo = $phpcsFile->findNext($skipOver, ($stackPtr + 1), null, true);
125+
if ($skipTo !== false) {
126+
return $skipTo;
127+
}
128+
129+
// If we're at the end of the file, just return.
130+
return;
131+
}
132+
73133
// Handle function declarations separately as they may contain the keywords in type declarations.
74134
if ($tokens[$stackPtr]['code'] === T_FUNCTION
75135
|| $tokens[$stackPtr]['code'] === T_CLOSURE
@@ -79,9 +139,15 @@ public function process(File $phpcsFile, $stackPtr)
79139
return;
80140
}
81141

142+
// Make sure to skip over return type declarations.
82143
$end = $tokens[$stackPtr]['parenthesis_closer'];
83144
if (isset($tokens[$stackPtr]['scope_opener']) === true) {
84145
$end = $tokens[$stackPtr]['scope_opener'];
146+
} else {
147+
$skipTo = $phpcsFile->findNext([T_SEMICOLON, T_OPEN_CURLY_BRACKET], ($end + 1), null, false, null, true);
148+
if ($skipTo !== false) {
149+
$end = $skipTo;
150+
}
85151
}
86152

87153
// Do a quick check if any of the targets exist in the declaration.
@@ -114,21 +180,6 @@ public function process(File $phpcsFile, $stackPtr)
114180
return $end;
115181
}//end if
116182

117-
// Handle property declarations separately as they may contain the keywords in type declarations.
118-
if (isset($tokens[$stackPtr]['conditions']) === true) {
119-
$conditions = $tokens[$stackPtr]['conditions'];
120-
$lastCondition = end($conditions);
121-
if (isset(Tokens::$ooScopeTokens[$lastCondition]) === true) {
122-
// This can only be an OO constant or property declaration as methods are handled above.
123-
$equals = $phpcsFile->findPrevious(T_EQUAL, ($stackPtr - 1), null, false, null, true);
124-
if ($equals !== false) {
125-
$this->processConstant($phpcsFile, $stackPtr);
126-
}
127-
128-
return;
129-
}
130-
}
131-
132183
// Handle everything else.
133184
$this->processConstant($phpcsFile, $stackPtr);
134185

src/Standards/Generic/Tests/PHP/LowerCaseConstantUnitTest.inc

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,3 +107,43 @@ interface InterfaceMethodsWithReturnTypeNoScopeOpener {
107107
abstract class ClassMethodsWithReturnTypeNoScopeOpener {
108108
abstract public function typed($param = FALSE) : TRUE;
109109
}
110+
111+
// Additional tests to safeguard improved property type skip logic.
112+
readonly class Properties {
113+
use SomeTrait {
114+
sayHello as private myPrivateHello;
115+
}
116+
117+
public Type|FALSE|NULL $propertyA = array(
118+
'itemA' => TRUE,
119+
'itemB' => FALSE,
120+
'itemC' => NULL,
121+
), $propertyB = FALSE;
122+
123+
protected \FullyQualified&Partially\Qualified&namespace\Relative $propertyC;
124+
var ?TRUE $propertyD;
125+
static array|callable|FALSE|self|parent $propertyE = TRUE;
126+
private
127+
// phpcs:ignore Stnd.Cat.Sniff -- for reasons.
128+
TRUE /*comment*/
129+
$propertyF = TRUE;
130+
131+
public function __construct(
132+
public FALSE|NULL $promotedPropA,
133+
readonly callable|TRUE $promotedPropB,
134+
) {
135+
static $var;
136+
echo static::class;
137+
static::foo();
138+
$var = $var instanceof static;
139+
$obj = new static();
140+
}
141+
142+
public static function foo(): static|self|FALSE {
143+
$callable = static function() {};
144+
}
145+
}
146+
147+
// Last coding/parse error.
148+
// This has to be the last test in the file.
149+
function UnclosedCurly (): FALSE {

src/Standards/Generic/Tests/PHP/LowerCaseConstantUnitTest.inc.fixed

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,3 +107,43 @@ interface InterfaceMethodsWithReturnTypeNoScopeOpener {
107107
abstract class ClassMethodsWithReturnTypeNoScopeOpener {
108108
abstract public function typed($param = false) : TRUE;
109109
}
110+
111+
// Additional tests to safeguard improved property type skip logic.
112+
readonly class Properties {
113+
use SomeTrait {
114+
sayHello as private myPrivateHello;
115+
}
116+
117+
public Type|FALSE|NULL $propertyA = array(
118+
'itemA' => true,
119+
'itemB' => false,
120+
'itemC' => null,
121+
), $propertyB = false;
122+
123+
protected \FullyQualified&Partially\Qualified&namespace\Relative $propertyC;
124+
var ?TRUE $propertyD;
125+
static array|callable|FALSE|self|parent $propertyE = true;
126+
private
127+
// phpcs:ignore Stnd.Cat.Sniff -- for reasons.
128+
TRUE /*comment*/
129+
$propertyF = true;
130+
131+
public function __construct(
132+
public FALSE|NULL $promotedPropA,
133+
readonly callable|TRUE $promotedPropB,
134+
) {
135+
static $var;
136+
echo static::class;
137+
static::foo();
138+
$var = $var instanceof static;
139+
$obj = new static();
140+
}
141+
142+
public static function foo(): static|self|FALSE {
143+
$callable = static function() {};
144+
}
145+
}
146+
147+
// Last coding/parse error.
148+
// This has to be the last test in the file.
149+
function UnclosedCurly (): FALSE {

src/Standards/Generic/Tests/PHP/LowerCaseConstantUnitTest.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,12 @@ public function getErrorList($testFile='LowerCaseConstantUnitTest.inc')
5353
100 => 2,
5454
104 => 1,
5555
108 => 1,
56+
118 => 1,
57+
119 => 1,
58+
120 => 1,
59+
121 => 1,
60+
125 => 1,
61+
129 => 1,
5662
];
5763
break;
5864
case 'LowerCaseConstantUnitTest.js':

0 commit comments

Comments
 (0)