Skip to content

Commit 7d46b4c

Browse files
authored
Merge pull request #719 from PHPCSStandards/feature/616-add-getattributeopener-methods
✨ New `*::getAttributeOpeners()` methods
2 parents 70cabf6 + b51c5c1 commit 7d46b4c

21 files changed

+1841
-0
lines changed
Lines changed: 227 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,227 @@
1+
<?php
2+
/**
3+
* PHPCSUtils, utility functions and classes for PHP_CodeSniffer sniff developers.
4+
*
5+
* @package PHPCSUtils
6+
* @copyright 2025 PHPCSUtils Contributors
7+
* @license https://opensource.org/licenses/LGPL-3.0 LGPL3
8+
* @link https://github.com/PHPCSStandards/PHPCSUtils
9+
*/
10+
11+
namespace PHPCSUtils\Internal;
12+
13+
use PHP_CodeSniffer\Files\File;
14+
use PHP_CodeSniffer\Util\Tokens;
15+
use PHPCSUtils\Exceptions\OutOfBoundsStackPtr;
16+
use PHPCSUtils\Exceptions\TypeError;
17+
use PHPCSUtils\Exceptions\UnexpectedTokenType;
18+
use PHPCSUtils\Exceptions\ValueError;
19+
use PHPCSUtils\Internal\Cache;
20+
use PHPCSUtils\Tokens\Collections;
21+
use PHPCSUtils\Utils\FunctionDeclarations;
22+
use PHPCSUtils\Utils\Parentheses;
23+
use PHPCSUtils\Utils\Scopes;
24+
25+
/**
26+
* Helper methods for PHP attributes.
27+
*
28+
* ---------------------------------------------------------------------------------------------
29+
* This class is only intended for internal use by PHPCSUtils and is not part of the public API.
30+
* This also means that it has no promise of backward compatibility.
31+
*
32+
* End-users should use the {@see \PHPCSUtils\Utils\Constants::getAttributeOpeners()},
33+
* {@see \PHPCSUtils\Utils\FunctionDeclarations::getAttributeOpeners()},
34+
* {@see \PHPCSUtils\Utils\ObjectDeclarations::getAttributeOpeners()},
35+
* or the {@see \PHPCSUtils\Utils\Variables::getAttributeOpeners()} methods instead.
36+
* ---------------------------------------------------------------------------------------------
37+
*
38+
* @internal
39+
*
40+
* @since 1.2.0
41+
*/
42+
final class AttributeHelper
43+
{
44+
45+
/**
46+
* Retrieve a list of stack pointers to the attribute openers for any attributes
47+
* which apply to the current stack pointer.
48+
*
49+
* @since 1.2.0
50+
*
51+
* @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned.
52+
* @param int $stackPtr The position of the token for a construct which can take an attribute.
53+
* Currently, this means:
54+
* - All OO declaration tokens;
55+
* - All function declaration tokens;
56+
* - T_VARIABLE tokens for function parameters and OO properties;
57+
* - T_CONST tokens.
58+
* @param string $type The expected type of construct.
59+
* Should be one of the following values:
60+
* 'constant', 'function', 'OO', 'variable'.
61+
*
62+
* @return array<int>
63+
*
64+
* @throws \PHPCSUtils\Exceptions\TypeError If the $stackPtr parameter is not an integer.
65+
* @throws \PHPCSUtils\Exceptions\TypeError If the $type parameter is not a string.
66+
* @throws \PHPCSUtils\Exceptions\OutOfBoundsStackPtr If the token passed does not exist in the $phpcsFile.
67+
* @throws \PHPCSUtils\Exceptions\UnexpectedTokenType If the token passed is not of a token type accepted for $type.
68+
* @throws \PHPCSUtils\Exceptions\ValueError For T_VARIABLE tokens: if the token passed does not point
69+
* to an OO property token or a parameter in a function declaration.
70+
*/
71+
public static function getOpeners(File $phpcsFile, $stackPtr, $type)
72+
{
73+
$tokens = $phpcsFile->getTokens();
74+
75+
if (\is_int($stackPtr) === false) {
76+
throw TypeError::create(2, '$stackPtr', 'integer', $stackPtr);
77+
}
78+
79+
if (isset($tokens[$stackPtr]) === false) {
80+
throw OutOfBoundsStackPtr::create(2, '$stackPtr', $stackPtr);
81+
}
82+
83+
if (\is_string($type) === false) {
84+
throw TypeError::create(3, '$type', 'string', $type);
85+
}
86+
87+
$isOOProperty = false;
88+
$isFunctionParam = false;
89+
switch ($type) {
90+
case 'constant':
91+
if ($tokens[$stackPtr]['code'] !== \T_CONST) {
92+
throw UnexpectedTokenType::create(2, '$stackPtr', 'T_CONST', $tokens[$stackPtr]['type']);
93+
}
94+
break;
95+
96+
case 'function':
97+
if (isset(Collections::functionDeclarationTokens()[$tokens[$stackPtr]['code']]) === false) {
98+
$acceptedTokens = 'T_FUNCTION, T_CLOSURE or T_FN';
99+
throw UnexpectedTokenType::create(2, '$stackPtr', $acceptedTokens, $tokens[$stackPtr]['type']);
100+
}
101+
break;
102+
103+
case 'OO':
104+
if (isset(Tokens::$ooScopeTokens[$tokens[$stackPtr]['code']]) === false) {
105+
$acceptedTokens = 'T_CLASS, T_ANON_CLASS, T_INTERFACE, T_TRAIT or T_ENUM';
106+
throw UnexpectedTokenType::create(2, '$stackPtr', $acceptedTokens, $tokens[$stackPtr]['type']);
107+
}
108+
break;
109+
110+
case 'variable':
111+
if ($tokens[$stackPtr]['code'] !== \T_VARIABLE) {
112+
throw UnexpectedTokenType::create(2, '$stackPtr', 'T_VARIABLE', $tokens[$stackPtr]['type']);
113+
}
114+
115+
$isOOProperty = Scopes::isOOProperty($phpcsFile, $stackPtr);
116+
$isFunctionParam = Parentheses::lastOwnerIn($phpcsFile, $stackPtr, Collections::functionDeclarationTokens());
117+
118+
if ($isOOProperty === false && $isFunctionParam === false) {
119+
$message = 'must be the pointer to an OO property or a parameter in a function declaration';
120+
throw ValueError::create(2, '$stackPtr', $message);
121+
}
122+
123+
// Allow for multi-property declarations.
124+
if ($isOOProperty === true) {
125+
do {
126+
$prevNonEmpty = $phpcsFile->findPrevious(Tokens::$emptyTokens, ($stackPtr - 1), null, true);
127+
if ($tokens[$prevNonEmpty]['code'] !== \T_COMMA) {
128+
break;
129+
}
130+
131+
$stackPtr = $phpcsFile->findPrevious(T_VARIABLE, ($prevNonEmpty - 1), null, false, null, true);
132+
} while ($stackPtr !== false);
133+
134+
if ($stackPtr === false) {
135+
$message = 'must be the pointer to an OO property or a parameter in a function declaration';
136+
throw ValueError::create(2, '$stackPtr', $message);
137+
}
138+
}
139+
break;
140+
141+
default:
142+
throw ValueError::create(3, '$type', 'must be one of the following: constant, function, OO, variable');
143+
}
144+
145+
if (Cache::isCached($phpcsFile, __METHOD__, "$stackPtr-$type") === true) {
146+
return Cache::get($phpcsFile, __METHOD__, "$stackPtr-$type");
147+
}
148+
149+
$allowedBetween = Tokens::$emptyTokens;
150+
switch ($type) {
151+
case 'constant':
152+
if (Scopes::isOOConstant($phpcsFile, $stackPtr) === true) {
153+
$allowedBetween += Collections::constantModifierKeywords();
154+
}
155+
break;
156+
157+
case 'function':
158+
$allowedBetween += [\T_STATIC => \T_STATIC];
159+
if (Scopes::isOOMethod($phpcsFile, $stackPtr) === true) {
160+
$allowedBetween += Tokens::$methodPrefixes;
161+
}
162+
break;
163+
164+
case 'OO':
165+
if ($tokens[$stackPtr]['code'] === \T_CLASS) {
166+
$allowedBetween += Collections::classModifierKeywords();
167+
} elseif ($tokens[$stackPtr]['code'] === \T_ANON_CLASS) {
168+
$allowedBetween[\T_READONLY] = \T_READONLY;
169+
}
170+
break;
171+
172+
case 'variable':
173+
$allowedBetween += [\T_NULLABLE => \T_NULLABLE];
174+
if ($isOOProperty === true) {
175+
$allowedBetween += Collections::propertyModifierKeywords();
176+
$allowedBetween += Collections::propertyTypeTokens();
177+
} elseif ($isFunctionParam !== false) {
178+
$allowedBetween += Collections::parameterTypeTokens();
179+
$allowedBetween += [
180+
\T_BITWISE_AND => \T_BITWISE_AND,
181+
\T_ELLIPSIS => \T_ELLIPSIS,
182+
];
183+
184+
if ($tokens[$isFunctionParam]['code'] === \T_FUNCTION
185+
&& Scopes::isOOMethod($phpcsFile, $isFunctionParam) === true
186+
) {
187+
$functionName = FunctionDeclarations::getName($phpcsFile, $isFunctionParam);
188+
if (empty($functionName) === false && \strtolower($functionName) === '__construct') {
189+
$allowedBetween += Collections::propertyModifierKeywords();
190+
}
191+
}
192+
}
193+
194+
break;
195+
}
196+
197+
$seenAttributes = [];
198+
199+
for ($i = ($stackPtr - 1); $i >= 0; $i--) {
200+
if (isset($tokens[$i]['comment_opener'])) {
201+
// Skip over docblocks.
202+
$i = $tokens[$i]['comment_opener'];
203+
continue;
204+
}
205+
206+
if (isset($allowedBetween[$tokens[$i]['code']])) {
207+
continue;
208+
}
209+
210+
if (isset($tokens[$i]['attribute_opener'])) {
211+
$seenAttributes[] = $tokens[$i]['attribute_opener'];
212+
$i = $tokens[$i]['attribute_opener'];
213+
continue;
214+
}
215+
216+
// In all other cases, we've reached the end of our search.
217+
break;
218+
}
219+
220+
if ($seenAttributes !== []) {
221+
$seenAttributes = \array_reverse($seenAttributes);
222+
}
223+
224+
Cache::set($phpcsFile, __METHOD__, "$stackPtr-$type", $seenAttributes);
225+
return $seenAttributes;
226+
}
227+
}

PHPCSUtils/Utils/Constants.php

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
use PHPCSUtils\Exceptions\TypeError;
1717
use PHPCSUtils\Exceptions\UnexpectedTokenType;
1818
use PHPCSUtils\Exceptions\ValueError;
19+
use PHPCSUtils\Internal\AttributeHelper;
1920
use PHPCSUtils\Internal\Cache;
2021
use PHPCSUtils\Tokens\Collections;
2122
use PHPCSUtils\Utils\Scopes;
@@ -191,4 +192,26 @@ public static function getProperties(File $phpcsFile, $stackPtr)
191192
Cache::set($phpcsFile, __METHOD__, $stackPtr, $returnValue);
192193
return $returnValue;
193194
}
195+
196+
/**
197+
* Retrieve the stack pointers to the attribute openers for any attribute block
198+
* which applies to the constant declaration.
199+
*
200+
* @since 1.2.0
201+
*
202+
* @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned.
203+
* @param int $stackPtr The position in the stack of the `T_CONST` token
204+
* to acquire the attributes for.
205+
*
206+
* @return array<int> Array with the stack pointers to the applicable attribute openers
207+
* or an empty array if there are no attributes attached to the constant declaration.
208+
*
209+
* @throws \PHPCSUtils\Exceptions\TypeError If the $stackPtr parameter is not an integer.
210+
* @throws \PHPCSUtils\Exceptions\OutOfBoundsStackPtr If the token passed does not exist in the $phpcsFile.
211+
* @throws \PHPCSUtils\Exceptions\UnexpectedTokenType If the token passed is not a `T_CONST` token.
212+
*/
213+
public static function getAttributeOpeners(File $phpcsFile, $stackPtr)
214+
{
215+
return AttributeHelper::getOpeners($phpcsFile, $stackPtr, 'constant');
216+
}
194217
}

PHPCSUtils/Utils/FunctionDeclarations.php

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
use PHPCSUtils\Exceptions\TypeError;
1717
use PHPCSUtils\Exceptions\UnexpectedTokenType;
1818
use PHPCSUtils\Exceptions\ValueError;
19+
use PHPCSUtils\Internal\AttributeHelper;
1920
use PHPCSUtils\Internal\Cache;
2021
use PHPCSUtils\Tokens\Collections;
2122
use PHPCSUtils\Utils\GetTokensAsString;
@@ -665,6 +666,29 @@ public static function getParameters(File $phpcsFile, $stackPtr)
665666
return $vars;
666667
}
667668

669+
/**
670+
* Retrieve the stack pointers to the attribute openers for any attribute block
671+
* which applies to the function declaration.
672+
*
673+
* @since 1.2.0
674+
*
675+
* @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned.
676+
* @param int $stackPtr The position in the stack of the function token to
677+
* acquire the attributes for.
678+
*
679+
* @return array<int> Array with the stack pointers to the applicable attribute openers
680+
* or an empty array if there are no attributes attached to the function declaration.
681+
*
682+
* @throws \PHPCSUtils\Exceptions\TypeError If the $stackPtr parameter is not an integer.
683+
* @throws \PHPCSUtils\Exceptions\OutOfBoundsStackPtr If the token passed does not exist in the $phpcsFile.
684+
* @throws \PHPCSUtils\Exceptions\UnexpectedTokenType If the token passed is not a T_FUNCTION, T_CLOSURE
685+
* or T_FN token.
686+
*/
687+
public static function getAttributeOpeners(File $phpcsFile, $stackPtr)
688+
{
689+
return AttributeHelper::getOpeners($phpcsFile, $stackPtr, 'function');
690+
}
691+
668692
/**
669693
* Checks if a given function is a PHP magic function.
670694
*

PHPCSUtils/Utils/ObjectDeclarations.php

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
use PHPCSUtils\Exceptions\OutOfBoundsStackPtr;
1616
use PHPCSUtils\Exceptions\TypeError;
1717
use PHPCSUtils\Exceptions\UnexpectedTokenType;
18+
use PHPCSUtils\Internal\AttributeHelper;
1819
use PHPCSUtils\Internal\Cache;
1920
use PHPCSUtils\Tokens\Collections;
2021
use PHPCSUtils\Utils\FunctionDeclarations;
@@ -375,6 +376,28 @@ private static function findNames(File $phpcsFile, $stackPtr, $keyword, array $a
375376
return $names;
376377
}
377378

379+
/**
380+
* Retrieve the stack pointers to the attribute openers for any attribute block which applies to the OO declaration.
381+
*
382+
* @since 1.2.0
383+
*
384+
* @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned.
385+
* @param int $stackPtr The position in the stack of the OO token to
386+
* acquire the attributes for.
387+
*
388+
* @return array<int> Array with the stack pointers to the applicable attribute openers
389+
* or an empty array if there are no attributes attached to the OO declaration.
390+
*
391+
* @throws \PHPCSUtils\Exceptions\TypeError If the $stackPtr parameter is not an integer.
392+
* @throws \PHPCSUtils\Exceptions\OutOfBoundsStackPtr If the token passed does not exist in the $phpcsFile.
393+
* @throws \PHPCSUtils\Exceptions\UnexpectedTokenType If the token passed is not a `T_CLASS`, `T_ANON_CLASS`,
394+
* `T_TRAIT`, `T_ENUM` or `T_INTERFACE` token.
395+
*/
396+
public static function getAttributeOpeners(File $phpcsFile, $stackPtr)
397+
{
398+
return AttributeHelper::getOpeners($phpcsFile, $stackPtr, 'OO');
399+
}
400+
378401
/**
379402
* Retrieve all constants declared in an OO structure.
380403
*

PHPCSUtils/Utils/Variables.php

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
use PHPCSUtils\Exceptions\TypeError;
1717
use PHPCSUtils\Exceptions\UnexpectedTokenType;
1818
use PHPCSUtils\Exceptions\ValueError;
19+
use PHPCSUtils\Internal\AttributeHelper;
1920
use PHPCSUtils\Internal\Cache;
2021
use PHPCSUtils\Tokens\Collections;
2122
use PHPCSUtils\Utils\Scopes;
@@ -270,6 +271,31 @@ public static function getMemberProperties(File $phpcsFile, $stackPtr)
270271
return $returnValue;
271272
}
272273

274+
/**
275+
* Retrieve the stack pointers to the attribute openers for any attribute block which applies to an OO property
276+
* or function declaration parameters.
277+
*
278+
* @since 1.2.0
279+
*
280+
* @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned.
281+
* @param int $stackPtr The position in the stack of the variable token to
282+
* acquire the attributes for.
283+
*
284+
* @return array<int> Array with the stack pointers to the applicable attribute openers
285+
* or an empty array if there are no attributes attached to the OO property
286+
* or function declaration parameter.
287+
*
288+
* @throws \PHPCSUtils\Exceptions\TypeError If the $stackPtr parameter is not an integer.
289+
* @throws \PHPCSUtils\Exceptions\OutOfBoundsStackPtr If the token passed does not exist in the $phpcsFile.
290+
* @throws \PHPCSUtils\Exceptions\UnexpectedTokenType If the token passed is not a `T_VARIABLE` token.
291+
* @throws \PHPCSUtils\Exceptions\ValueError If the token passed does not point to an OO property token
292+
* or a parameter in a function declaration.
293+
*/
294+
public static function getAttributeOpeners(File $phpcsFile, $stackPtr)
295+
{
296+
return AttributeHelper::getOpeners($phpcsFile, $stackPtr, 'variable');
297+
}
298+
273299
/**
274300
* Verify if a given variable name is the name of a PHP reserved variable.
275301
*

0 commit comments

Comments
 (0)