Skip to content

Commit de3b745

Browse files
authored
Merge pull request #321 from PHPCSStandards/php-8.3/tokenizer-php-allow-for-typed-constants
PHP 8.3 | Tokenizer/PHP: add support for typed OO constants
2 parents 10249a2 + 53dd20c commit de3b745

13 files changed

+963
-8
lines changed

src/Tokenizers/PHP.php

Lines changed: 98 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -526,8 +526,9 @@ protected function tokenize($string)
526526
$numTokens = count($tokens);
527527
$lastNotEmptyToken = 0;
528528

529-
$insideInlineIf = [];
530-
$insideUseGroup = false;
529+
$insideInlineIf = [];
530+
$insideUseGroup = false;
531+
$insideConstDeclaration = false;
531532

532533
$commentTokenizer = new Comment();
533534

@@ -608,7 +609,8 @@ protected function tokenize($string)
608609
if ($tokenIsArray === true
609610
&& isset(Util\Tokens::$contextSensitiveKeywords[$token[0]]) === true
610611
&& (isset($this->tstringContexts[$finalTokens[$lastNotEmptyToken]['code']]) === true
611-
|| $finalTokens[$lastNotEmptyToken]['content'] === '&')
612+
|| $finalTokens[$lastNotEmptyToken]['content'] === '&'
613+
|| $insideConstDeclaration === true)
612614
) {
613615
if (isset($this->tstringContexts[$finalTokens[$lastNotEmptyToken]['code']]) === true) {
614616
$preserveKeyword = false;
@@ -665,6 +667,30 @@ protected function tokenize($string)
665667
}
666668
}//end if
667669

670+
// Types in typed constants should not be touched, but the constant name should be.
671+
if ((isset($this->tstringContexts[$finalTokens[$lastNotEmptyToken]['code']]) === true
672+
&& $finalTokens[$lastNotEmptyToken]['code'] === T_CONST)
673+
|| $insideConstDeclaration === true
674+
) {
675+
$preserveKeyword = true;
676+
677+
// Find the next non-empty token.
678+
for ($i = ($stackPtr + 1); $i < $numTokens; $i++) {
679+
if (is_array($tokens[$i]) === true
680+
&& isset(Util\Tokens::$emptyTokens[$tokens[$i][0]]) === true
681+
) {
682+
continue;
683+
}
684+
685+
break;
686+
}
687+
688+
if ($tokens[$i] === '=' || $tokens[$i] === ';') {
689+
$preserveKeyword = false;
690+
$insideConstDeclaration = false;
691+
}
692+
}//end if
693+
668694
if ($finalTokens[$lastNotEmptyToken]['content'] === '&') {
669695
$preserveKeyword = true;
670696

@@ -698,6 +724,26 @@ protected function tokenize($string)
698724
}
699725
}//end if
700726

727+
/*
728+
Mark the start of a constant declaration to allow for handling keyword to T_STRING
729+
convertion for constant names using reserved keywords.
730+
*/
731+
732+
if ($tokenIsArray === true && $token[0] === T_CONST) {
733+
$insideConstDeclaration = true;
734+
}
735+
736+
/*
737+
Close an open "inside constant declaration" marker when no keyword convertion was needed.
738+
*/
739+
740+
if ($insideConstDeclaration === true
741+
&& $tokenIsArray === false
742+
&& ($token[0] === '=' || $token[0] === ';')
743+
) {
744+
$insideConstDeclaration = false;
745+
}
746+
701747
/*
702748
Special case for `static` used as a function name, i.e. `static()`.
703749
*/
@@ -1869,6 +1915,20 @@ protected function tokenize($string)
18691915
$newToken = [];
18701916
$newToken['content'] = '?';
18711917

1918+
// For typed constants, we only need to check the token before the ? to be sure.
1919+
if ($finalTokens[$lastNotEmptyToken]['code'] === T_CONST) {
1920+
$newToken['code'] = T_NULLABLE;
1921+
$newToken['type'] = 'T_NULLABLE';
1922+
1923+
if (PHP_CODESNIFFER_VERBOSITY > 1) {
1924+
echo "\t\t* token $stackPtr changed from ? to T_NULLABLE".PHP_EOL;
1925+
}
1926+
1927+
$finalTokens[$newStackPtr] = $newToken;
1928+
$newStackPtr++;
1929+
continue;
1930+
}
1931+
18721932
/*
18731933
* Check if the next non-empty token is one of the tokens which can be used
18741934
* in type declarations. If not, it's definitely a ternary.
@@ -2236,7 +2296,30 @@ function return types. We want to keep the parenthesis map clean,
22362296
if ($tokenIsArray === true && $token[0] === T_STRING) {
22372297
$preserveTstring = false;
22382298

2239-
if (isset($this->tstringContexts[$finalTokens[$lastNotEmptyToken]['code']]) === true) {
2299+
// True/false/parent/self/static in typed constants should be fixed to their own token,
2300+
// but the constant name should not be.
2301+
if ((isset($this->tstringContexts[$finalTokens[$lastNotEmptyToken]['code']]) === true
2302+
&& $finalTokens[$lastNotEmptyToken]['code'] === T_CONST)
2303+
|| $insideConstDeclaration === true
2304+
) {
2305+
// Find the next non-empty token.
2306+
for ($i = ($stackPtr + 1); $i < $numTokens; $i++) {
2307+
if (is_array($tokens[$i]) === true
2308+
&& isset(Util\Tokens::$emptyTokens[$tokens[$i][0]]) === true
2309+
) {
2310+
continue;
2311+
}
2312+
2313+
break;
2314+
}
2315+
2316+
if ($tokens[$i] === '=') {
2317+
$preserveTstring = true;
2318+
$insideConstDeclaration = false;
2319+
}
2320+
} else if (isset($this->tstringContexts[$finalTokens[$lastNotEmptyToken]['code']]) === true
2321+
&& $finalTokens[$lastNotEmptyToken]['code'] !== T_CONST
2322+
) {
22402323
$preserveTstring = true;
22412324

22422325
// Special case for syntax like: return new self/new parent
@@ -3008,6 +3091,12 @@ protected function processAdditional()
30083091
$suspectedType = 'return';
30093092
}
30103093

3094+
if ($this->tokens[$x]['code'] === T_EQUAL) {
3095+
// Possible constant declaration, the `T_STRING` name will have been skipped over already.
3096+
$suspectedType = 'constant';
3097+
break;
3098+
}
3099+
30113100
break;
30123101
}//end for
30133102

@@ -3049,6 +3138,11 @@ protected function processAdditional()
30493138
break;
30503139
}
30513140

3141+
if ($suspectedType === 'constant' && $this->tokens[$x]['code'] === T_CONST) {
3142+
$confirmed = true;
3143+
break;
3144+
}
3145+
30523146
if ($suspectedType === 'property or parameter'
30533147
&& (isset(Util\Tokens::$scopeModifiers[$this->tokens[$x]['code']]) === true
30543148
|| $this->tokens[$x]['code'] === T_VAR

tests/Core/Tokenizer/ArrayKeywordTest.inc

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,15 +21,21 @@ $var = array(
2121
);
2222

2323
/* testFunctionDeclarationParamType */
24-
function foo(array $a) {}
24+
function typedParam(array $a) {}
2525

2626
/* testFunctionDeclarationReturnType */
27-
function foo($a) : int|array|null {}
27+
function returnType($a) : int|array|null {}
2828

2929
class Bar {
3030
/* testClassConst */
3131
const ARRAY = [];
3232

3333
/* testClassMethod */
3434
public function array() {}
35+
36+
/* testOOConstType */
37+
const array /* testTypedOOConstName */ ARRAY = /* testOOConstDefault */ array();
38+
39+
/* testOOPropertyType */
40+
protected array $property;
3541
}

tests/Core/Tokenizer/ArrayKeywordTest.php

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,9 @@ public static function dataArrayKeyword()
6868
'nested: inner array' => [
6969
'testMarker' => '/* testNestedArray */',
7070
],
71+
'OO constant default value' => [
72+
'testMarker' => '/* testOOConstDefault */',
73+
],
7174
];
7275

7376
}//end dataArrayKeyword()
@@ -122,6 +125,12 @@ public static function dataArrayType()
122125
'function union return type' => [
123126
'testMarker' => '/* testFunctionDeclarationReturnType */',
124127
],
128+
'OO constant type' => [
129+
'testMarker' => '/* testOOConstType */',
130+
],
131+
'OO property type' => [
132+
'testMarker' => '/* testOOPropertyType */',
133+
],
125134
];
126135

127136
}//end dataArrayType()
@@ -167,13 +176,17 @@ public function testNotArrayKeyword($testMarker, $testContent='array')
167176
public static function dataNotArrayKeyword()
168177
{
169178
return [
170-
'class-constant-name' => [
179+
'class-constant-name' => [
171180
'testMarker' => '/* testClassConst */',
172181
'testContent' => 'ARRAY',
173182
],
174-
'class-method-name' => [
183+
'class-method-name' => [
175184
'testMarker' => '/* testClassMethod */',
176185
],
186+
'class-constant-name-after-type' => [
187+
'testMarker' => '/* testTypedOOConstName */',
188+
'testContent' => 'ARRAY',
189+
],
177190
];
178191

179192
}//end dataNotArrayKeyword()

tests/Core/Tokenizer/BitwiseOrTest.inc

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,30 @@ $result = $value | $test /* testBitwiseOr2 */ | $another;
99

1010
class TypeUnion
1111
{
12+
/* testTypeUnionOOConstSimple */
13+
public const Foo|Bar SIMPLE = new Foo;
14+
15+
/* testTypeUnionOOConstReverseModifierOrder */
16+
protected final const int|float MODIFIERS_REVERSED /* testBitwiseOrOOConstDefaultValue */ = E_WARNING | E_NOTICE;
17+
18+
const
19+
/* testTypeUnionOOConstMulti1 */
20+
array |
21+
/* testTypeUnionOOConstMulti2 */
22+
Traversable | // phpcs:ignore Stnd.Cat.Sniff
23+
false
24+
/* testTypeUnionOOConstMulti3 */
25+
| null MULTI_UNION = false;
26+
27+
/* testTypeUnionOOConstNamespaceRelative */
28+
final protected const namespace\Sub\NameA|namespace\Sub\NameB NAMESPACE_RELATIVE = new namespace\Sub\NameB;
29+
30+
/* testTypeUnionOOConstPartiallyQualified */
31+
const Partially\Qualified\NameA|Partially\Qualified\NameB PARTIALLY_QUALIFIED = new Partially\Qualified\NameA;
32+
33+
/* testTypeUnionOOConstFullyQualified */
34+
const \Fully\Qualified\NameA|\Fully\Qualified\NameB FULLY_QUALIFIED = new \Fully\Qualified\NameB();
35+
1236
/* testTypeUnionPropertySimple */
1337
public static Foo|Bar $obj;
1438

tests/Core/Tokenizer/BitwiseOrTest.php

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ public static function dataBitwiseOr()
4747
return [
4848
'in simple assignment 1' => ['/* testBitwiseOr1 */'],
4949
'in simple assignment 2' => ['/* testBitwiseOr2 */'],
50+
'in OO constant default value' => ['/* testBitwiseOrOOConstDefaultValue */'],
5051
'in property default value' => ['/* testBitwiseOrPropertyDefaultValue */'],
5152
'in method parameter default value' => ['/* testBitwiseOrParamDefaultValue */'],
5253
'in return statement' => ['/* testBitwiseOr3 */'],
@@ -97,6 +98,14 @@ public function testTypeUnion($testMarker)
9798
public static function dataTypeUnion()
9899
{
99100
return [
101+
'type for OO constant' => ['/* testTypeUnionOOConstSimple */'],
102+
'type for OO constant, reversed modifier order' => ['/* testTypeUnionOOConstReverseModifierOrder */'],
103+
'type for OO constant, first of multi-union' => ['/* testTypeUnionOOConstMulti1 */'],
104+
'type for OO constant, middle of multi-union + comments' => ['/* testTypeUnionOOConstMulti2 */'],
105+
'type for OO constant, last of multi-union' => ['/* testTypeUnionOOConstMulti3 */'],
106+
'type for OO constant, using namespace relative names' => ['/* testTypeUnionOOConstNamespaceRelative */'],
107+
'type for OO constant, using partially qualified names' => ['/* testTypeUnionOOConstPartiallyQualified */'],
108+
'type for OO constant, using fully qualified names' => ['/* testTypeUnionOOConstFullyQualified */'],
100109
'type for static property' => ['/* testTypeUnionPropertySimple */'],
101110
'type for static property, reversed modifier order' => ['/* testTypeUnionPropertyReverseModifierOrder */'],
102111
'type for property, first of multi-union' => ['/* testTypeUnionPropertyMulti1 */'],

tests/Core/Tokenizer/ContextSensitiveKeywordsTest.inc

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,12 @@ class ContextSensitiveKeywords
7676
const /* testAnd */ AND = 'LOGICAL_AND';
7777
const /* testOr */ OR = 'LOGICAL_OR';
7878
const /* testXor */ XOR = 'LOGICAL_XOR';
79+
80+
const /* testArrayIsTstringInConstType */ array /* testArrayNameForTypedConstant */ ARRAY = /* testArrayIsKeywordInConstDefault */ array();
81+
const /* testStaticIsKeywordAsConstType */ static /* testStaticIsNameForTypedConstant */ STATIC = new /* testStaticIsKeywordAsConstDefault */ static;
82+
83+
const int|bool /* testPrivateNameForUnionTypedConstant */ PRIVATE = 'PRIVATE';
84+
const Foo&Bar /* testFinalNameForIntersectionTypedConstant */ FINAL = 'FINAL';
7985
}
8086

8187
namespace /* testKeywordAfterNamespaceShouldBeString */ class;

tests/Core/Tokenizer/ContextSensitiveKeywordsTest.php

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,12 @@ public static function dataStrings()
118118
'constant declaration: or' => ['/* testOr */'],
119119
'constant declaration: xor' => ['/* testXor */'],
120120

121+
'constant declaration: array in type' => ['/* testArrayIsTstringInConstType */'],
122+
'constant declaration: array, name after type' => ['/* testArrayNameForTypedConstant */'],
123+
'constant declaration: static, name after type' => ['/* testStaticIsNameForTypedConstant */'],
124+
'constant declaration: private, name after type' => ['/* testPrivateNameForUnionTypedConstant */'],
125+
'constant declaration: final, name after type' => ['/* testFinalNameForIntersectionTypedConstant */'],
126+
121127
'namespace declaration: class' => ['/* testKeywordAfterNamespaceShouldBeString */'],
122128
'namespace declaration (partial): my' => ['/* testNamespaceNameIsString1 */'],
123129
'namespace declaration (partial): class' => ['/* testNamespaceNameIsString2 */'],
@@ -179,6 +185,19 @@ public static function dataKeywords()
179185
'testMarker' => '/* testNamespaceIsKeyword */',
180186
'expectedTokenType' => 'T_NAMESPACE',
181187
],
188+
'array: default value in const decl' => [
189+
'testMarker' => '/* testArrayIsKeywordInConstDefault */',
190+
'expectedTokenType' => 'T_ARRAY',
191+
],
192+
'static: type in constant declaration' => [
193+
'testMarker' => '/* testStaticIsKeywordAsConstType */',
194+
'expectedTokenType' => 'T_STATIC',
195+
],
196+
'static: value in constant declaration' => [
197+
'testMarker' => '/* testStaticIsKeywordAsConstDefault */',
198+
'expectedTokenType' => 'T_STATIC',
199+
],
200+
182201
'abstract: class declaration' => [
183202
'testMarker' => '/* testAbstractIsKeyword */',
184203
'expectedTokenType' => 'T_ABSTRACT',

tests/Core/Tokenizer/OtherContextSensitiveKeywordsTest.inc

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,3 +51,17 @@ function standAloneFalseTrueNullTypesAndMore(
5151
|| $a === /* testNullIsKeywordInComparison */ null
5252
) {}
5353
}
54+
55+
class TypedConstProp {
56+
const /* testFalseIsKeywordAsConstType */ false /* testFalseIsNameForTypedConstant */ FALSE = /* testFalseIsKeywordAsConstDefault */ false;
57+
const /* testTrueIsKeywordAsConstType */ true /* testTrueIsNameForTypedConstant */ TRUE = /* testTrueIsKeywordAsConstDefault */ true;
58+
const /* testNullIsKeywordAsConstType */ null /* testNullIsNameForTypedConstant */ NULL = /* testNullIsKeywordAsConstDefault */ null;
59+
const /* testSelfIsKeywordAsConstType */ self /* testSelfIsNameForTypedConstant */ SELF = new /* testSelfIsKeywordAsConstDefault */ self;
60+
const /* testParentIsKeywordAsConstType */ parent /* testParentIsNameForTypedConstant */ PARENT = new /* testParentIsKeywordAsConstDefault */ parent;
61+
62+
public /* testFalseIsKeywordAsPropertyType */ false $false = /* testFalseIsKeywordAsPropertyDefault */ false;
63+
protected readonly /* testTrueIsKeywordAsPropertyType */ true $true = /* testTrueIsKeywordAsPropertyDefault */ true;
64+
static private /* testNullIsKeywordAsPropertyType */ null $null = /* testNullIsKeywordAsPropertyDefault */ null;
65+
var /* testSelfIsKeywordAsPropertyType */ self $self = new /* testSelfIsKeywordAsPropertyDefault */ self;
66+
protected /* testParentIsKeywordAsPropertyType */ parent $parent = new /* testParentIsKeywordAsPropertyDefault */ parent;
67+
}

0 commit comments

Comments
 (0)