Skip to content

Commit

Permalink
Generic/LowerCaseConstant: improve performance
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
jrfnl committed Dec 7, 2023
1 parent ca9b3f1 commit 2016a15
Show file tree
Hide file tree
Showing 5 changed files with 155 additions and 17 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Expand Up @@ -101,6 +101,7 @@ The file documents changes to the PHP_CodeSniffer project.
- Runtime performance improvement for PHPCS CLI users. The improvement should be most noticeable for users on Windows.
- Thanks to Juliette Reinders Folmer (@jrfnl) for the patch
- The following sniffs have received performance related improvements:
- Generic.PHP.LowerCaseConstant
- Generic.PHP.LowerCaseType
- PSR12.Files.OpenTag
- Thanks to Juliette Reinders Folmer (@jrfnl) for the patches
Expand Down
85 changes: 68 additions & 17 deletions src/Standards/Generic/Sniffs/PHP/LowerCaseConstantSniff.php
Expand Up @@ -37,6 +37,29 @@ class LowerCaseConstantSniff implements Sniff
T_NULL => T_NULL,
];

/**
* Token types which can be encountered in a property type declaration.
*
* @var array<int|string, int|string>
*/
private $propertyTypeTokens = [
T_CALLABLE => T_CALLABLE,
T_SELF => T_SELF,
T_PARENT => T_PARENT,
T_FALSE => T_FALSE,
T_TRUE => T_TRUE,
T_NULL => T_NULL,
T_STRING => T_STRING,
T_NAME_QUALIFIED => T_NAME_QUALIFIED,
T_NAME_FULLY_QUALIFIED => T_NAME_FULLY_QUALIFIED,
T_NAME_RELATIVE => T_NAME_RELATIVE,
T_NS_SEPARATOR => T_NS_SEPARATOR,
T_NAMESPACE => T_NAMESPACE,
T_TYPE_UNION => T_TYPE_UNION,
T_TYPE_INTERSECTION => T_TYPE_INTERSECTION,
T_NULLABLE => T_NULLABLE,
];


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

// Register function keywords to filter out type declarations.
// Register scope modifiers to filter out property type declarations.
$targets += Tokens::$scopeModifiers;
$targets[] = T_VAR;
$targets[] = T_STATIC;
$targets[] = T_READONLY;

// Register function keywords to filter out param/return type declarations.
$targets[] = T_FUNCTION;
$targets[] = T_CLOSURE;
$targets[] = T_FN;
Expand All @@ -64,12 +93,43 @@ public function register()
* @param int $stackPtr The position of the current token in the
* stack passed in $tokens.
*
* @return void|int
* @return void|int Optionally returns a stack pointer. The sniff will not be
* called again on the current file until the returned stack
* pointer is reached.
*/
public function process(File $phpcsFile, $stackPtr)
{
$tokens = $phpcsFile->getTokens();

/*
* Skip over type declarations for properties.
*
* Note: for other uses of the visibility modifiers (functions, constants, trait use),
* nothing relevant will be skipped as the next non-empty token will be an "non-skippable"
* one.
* Functions are handled separately below (and then skip to their scope opener), so
* this should also not cause any confusion for constructor property promotion.
*
* For other uses of the "static" keyword, it also shouldn't be problematic as the only
* time the next non-empty token will be a "skippable" token will be in return type
* declarations, in which case, it is correct to skip over them.
*/

if (isset(Tokens::$scopeModifiers[$tokens[$stackPtr]['code']]) === true
|| $tokens[$stackPtr]['code'] === T_VAR
|| $tokens[$stackPtr]['code'] === T_STATIC
|| $tokens[$stackPtr]['code'] === T_READONLY
) {
$skipOver = (Tokens::$emptyTokens + $this->propertyTypeTokens);
$skipTo = $phpcsFile->findNext($skipOver, ($stackPtr + 1), null, true);
if ($skipTo !== false) {
return $skipTo;
}

// If we're at the end of the file, just return.
return;
}

// Handle function declarations separately as they may contain the keywords in type declarations.
if ($tokens[$stackPtr]['code'] === T_FUNCTION
|| $tokens[$stackPtr]['code'] === T_CLOSURE
Expand All @@ -79,9 +139,15 @@ public function process(File $phpcsFile, $stackPtr)
return;
}

// Make sure to skip over return type declarations.
$end = $tokens[$stackPtr]['parenthesis_closer'];
if (isset($tokens[$stackPtr]['scope_opener']) === true) {
$end = $tokens[$stackPtr]['scope_opener'];
} else {
$skipTo = $phpcsFile->findNext([T_SEMICOLON, T_OPEN_CURLY_BRACKET], ($end + 1), null, false, null, true);
if ($skipTo !== false) {
$end = $skipTo;
}
}

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

// Handle property declarations separately as they may contain the keywords in type declarations.
if (isset($tokens[$stackPtr]['conditions']) === true) {
$conditions = $tokens[$stackPtr]['conditions'];
$lastCondition = end($conditions);
if (isset(Tokens::$ooScopeTokens[$lastCondition]) === true) {
// This can only be an OO constant or property declaration as methods are handled above.
$equals = $phpcsFile->findPrevious(T_EQUAL, ($stackPtr - 1), null, false, null, true);
if ($equals !== false) {
$this->processConstant($phpcsFile, $stackPtr);
}

return;
}
}

// Handle everything else.
$this->processConstant($phpcsFile, $stackPtr);

Expand Down
40 changes: 40 additions & 0 deletions src/Standards/Generic/Tests/PHP/LowerCaseConstantUnitTest.inc
Expand Up @@ -107,3 +107,43 @@ interface InterfaceMethodsWithReturnTypeNoScopeOpener {
abstract class ClassMethodsWithReturnTypeNoScopeOpener {
abstract public function typed($param = FALSE) : TRUE;
}

// Additional tests to safeguard improved property type skip logic.
readonly class Properties {
use SomeTrait {
sayHello as private myPrivateHello;
}

public Type|FALSE|NULL $propertyA = array(
'itemA' => TRUE,
'itemB' => FALSE,
'itemC' => NULL,
), $propertyB = FALSE;

protected \FullyQualified&Partially\Qualified&namespace\Relative $propertyC;
var ?TRUE $propertyD;
static array|callable|FALSE|self|parent $propertyE = TRUE;
private
// phpcs:ignore Stnd.Cat.Sniff -- for reasons.
TRUE /*comment*/
$propertyF = TRUE;

public function __construct(
public FALSE|NULL $promotedPropA,
readonly callable|TRUE $promotedPropB,
) {
static $var;
echo static::class;
static::foo();
$var = $var instanceof static;
$obj = new static();
}

public static function foo(): static|self|FALSE {
$callable = static function() {};
}
}

// Last coding/parse error.
// This has to be the last test in the file.
function UnclosedCurly (): FALSE {
Expand Up @@ -107,3 +107,43 @@ interface InterfaceMethodsWithReturnTypeNoScopeOpener {
abstract class ClassMethodsWithReturnTypeNoScopeOpener {
abstract public function typed($param = false) : TRUE;
}

// Additional tests to safeguard improved property type skip logic.
readonly class Properties {
use SomeTrait {
sayHello as private myPrivateHello;
}

public Type|FALSE|NULL $propertyA = array(
'itemA' => true,
'itemB' => false,
'itemC' => null,
), $propertyB = false;

protected \FullyQualified&Partially\Qualified&namespace\Relative $propertyC;
var ?TRUE $propertyD;
static array|callable|FALSE|self|parent $propertyE = true;
private
// phpcs:ignore Stnd.Cat.Sniff -- for reasons.
TRUE /*comment*/
$propertyF = true;

public function __construct(
public FALSE|NULL $promotedPropA,
readonly callable|TRUE $promotedPropB,
) {
static $var;
echo static::class;
static::foo();
$var = $var instanceof static;
$obj = new static();
}

public static function foo(): static|self|FALSE {
$callable = static function() {};
}
}

// Last coding/parse error.
// This has to be the last test in the file.
function UnclosedCurly (): FALSE {
6 changes: 6 additions & 0 deletions src/Standards/Generic/Tests/PHP/LowerCaseConstantUnitTest.php
Expand Up @@ -53,6 +53,12 @@ public function getErrorList($testFile='LowerCaseConstantUnitTest.inc')
100 => 2,
104 => 1,
108 => 1,
118 => 1,
119 => 1,
120 => 1,
121 => 1,
125 => 1,
129 => 1,
];

case 'LowerCaseConstantUnitTest.js':
Expand Down

0 comments on commit 2016a15

Please sign in to comment.