Skip to content

Commit 779884c

Browse files
authored
Support constructor promotion with namespaced and union typehints (#333)
* Add test case for constructor property promotion with namespace type * Simplify isConstructorPromotion and add ignore typehints * Add test for union properties * Handle unions * Style fixes * Remove unused var * Remove unreachable return * Use union token type content rather than type
1 parent 40d4800 commit 779884c

File tree

2 files changed

+89
-35
lines changed

2 files changed

+89
-35
lines changed

Tests/VariableAnalysisSniff/fixtures/ClassWithMembersFixture.php

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -222,3 +222,14 @@ public function getMessage(): string {
222222
return $this->message;
223223
}
224224
}
225+
226+
class ClassWithNamespacedConstructorPropertyPromotion
227+
{
228+
public function __construct(
229+
public \App\Models\User $user,
230+
public readonly \App\Models\Blog $blog,
231+
private \App\Models\Game $game,
232+
protected ?\App\Models\Flag $flag,
233+
protected true|false|int|string|null|\App\Models\Favorite $favorite,
234+
) {}
235+
}

VariableAnalysis/Lib/Helpers.php

Lines changed: 78 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1535,62 +1535,105 @@ public static function getForLoopForIncrementVariable($stackPtr, $forLoops)
15351535
*/
15361536
public static function isConstructorPromotion(File $phpcsFile, $stackPtr)
15371537
{
1538+
// If we are not in a function's parameters, this is not promotion.
15381539
$functionIndex = self::getFunctionIndexForFunctionParameter($phpcsFile, $stackPtr);
15391540
if (! $functionIndex) {
15401541
return false;
15411542
}
15421543

15431544
$tokens = $phpcsFile->getTokens();
15441545

1545-
// If the previous token is a visibility keyword, this is constructor
1546-
// promotion. eg: `public $foobar`.
1547-
$prevIndex = $phpcsFile->findPrevious(Tokens::$emptyTokens, ($stackPtr - 1), $functionIndex, true);
1548-
if (! is_int($prevIndex)) {
1546+
// Move backwards from the token, ignoring whitespace, typehints, and the
1547+
// 'readonly' keyword, and return true if the previous token is a
1548+
// visibility keyword (eg: `public`).
1549+
for ($i = $stackPtr - 1; $i > $functionIndex; $i--) {
1550+
if (in_array($tokens[$i]['code'], Tokens::$scopeModifiers, true)) {
1551+
return true;
1552+
}
1553+
if (in_array($tokens[$i]['code'], Tokens::$emptyTokens, true)) {
1554+
continue;
1555+
}
1556+
if ($tokens[$i]['content'] === 'readonly') {
1557+
continue;
1558+
}
1559+
if (self::isTokenPartOfTypehint($phpcsFile, $i)) {
1560+
continue;
1561+
}
15491562
return false;
15501563
}
1551-
$prevToken = $tokens[$prevIndex];
1552-
if (in_array($prevToken['code'], Tokens::$scopeModifiers, true)) {
1564+
return false;
1565+
}
1566+
1567+
/**
1568+
* Return false if the token is definitely not part of a typehint
1569+
*
1570+
* @param File $phpcsFile
1571+
* @param int $stackPtr
1572+
*
1573+
* @return bool
1574+
*/
1575+
private static function isTokenPossiblyPartOfTypehint(File $phpcsFile, $stackPtr)
1576+
{
1577+
$tokens = $phpcsFile->getTokens();
1578+
$token = $tokens[$stackPtr];
1579+
if ($token['code'] === 'PHPCS_T_NULLABLE') {
15531580
return true;
15541581
}
1555-
1556-
// If the previous token is not a visibility keyword, but the one before it
1557-
// is, the previous token was probably a typehint and this is constructor
1558-
// promotion. eg: `public boolean $foobar`.
1559-
$prev2Index = $phpcsFile->findPrevious(Tokens::$emptyTokens, ($prevIndex - 1), $functionIndex, true);
1560-
if (! is_int($prev2Index)) {
1561-
return false;
1582+
if ($token['code'] === T_NS_SEPARATOR) {
1583+
return true;
15621584
}
1563-
$prev2Token = $tokens[$prev2Index];
1564-
// If the token that might be a visibility keyword is a nullable typehint,
1565-
// ignore it and move back one token further eg: `public ?boolean $foobar`.
1566-
if ($prev2Token['code'] === 'PHPCS_T_NULLABLE') {
1567-
$prev2Index = $phpcsFile->findPrevious(Tokens::$emptyTokens, ($prev2Index - 1), $functionIndex, true);
1568-
if (! is_int($prev2Index)) {
1569-
return false;
1570-
}
1585+
if ($token['code'] === T_STRING) {
1586+
return true;
15711587
}
1572-
$prev2Token = $tokens[$prev2Index];
1573-
if (in_array($prev2Token['code'], Tokens::$scopeModifiers, true)) {
1588+
if ($token['code'] === T_TRUE) {
15741589
return true;
15751590
}
1576-
1577-
// If the previous token is not a visibility keyword, but the one two
1578-
// before it is, and one of the tokens is `readonly`, the previous token
1579-
// was probably a typehint and this is constructor promotion. eg: `public
1580-
// readonly boolean $foobar`.
1581-
$prev3Index = $phpcsFile->findPrevious(Tokens::$emptyTokens, ($prev2Index - 1), $functionIndex, true);
1582-
if (! is_int($prev3Index)) {
1583-
return false;
1591+
if ($token['code'] === T_FALSE) {
1592+
return true;
15841593
}
1585-
$prev3Token = $tokens[$prev3Index];
1586-
$wasPreviousReadonly = $prevToken['content'] === 'readonly' || $prev2Token['content'] === 'readonly';
1587-
if (in_array($prev3Token['code'], Tokens::$scopeModifiers, true) && $wasPreviousReadonly) {
1594+
if ($token['code'] === T_NULL) {
1595+
return true;
1596+
}
1597+
if ($token['content'] === '|') {
1598+
return true;
1599+
}
1600+
if (in_array($token['code'], Tokens::$emptyTokens)) {
15881601
return true;
15891602
}
1590-
15911603
return false;
15921604
}
15931605

1606+
/**
1607+
* Return true if the token is inside a typehint
1608+
*
1609+
* @param File $phpcsFile
1610+
* @param int $stackPtr
1611+
*
1612+
* @return bool
1613+
*/
1614+
public static function isTokenPartOfTypehint(File $phpcsFile, $stackPtr)
1615+
{
1616+
$tokens = $phpcsFile->getTokens();
1617+
1618+
if (! self::isTokenPossiblyPartOfTypehint($phpcsFile, $stackPtr)) {
1619+
return false;
1620+
}
1621+
1622+
// Examine every following token, ignoring everything that might be part of
1623+
// a typehint. If we find a variable at the end, this is part of a
1624+
// typehint.
1625+
$i = $stackPtr;
1626+
while (true) {
1627+
$i += 1;
1628+
if (! isset($tokens[$i])) {
1629+
return false;
1630+
}
1631+
if (! self::isTokenPossiblyPartOfTypehint($phpcsFile, $i)) {
1632+
return ($tokens[$i]['code'] === T_VARIABLE);
1633+
}
1634+
}
1635+
}
1636+
15941637
/**
15951638
* Return true if the token is inside an abstract class.
15961639
*

0 commit comments

Comments
 (0)