Skip to content

Commit e09be25

Browse files
PHP 8.4 | Add tokenizer support for asymmetric visibility (#871)
1 parent 4667299 commit e09be25

25 files changed

+684
-43
lines changed

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

+12
Original file line numberDiff line numberDiff line change
@@ -160,3 +160,15 @@ class SkipOverPHP84FinalProperties {
160160
final MyType|FALSE $propA;
161161
private static final NULL|MyClass $propB;
162162
}
163+
164+
// PHP 8.4 asymmetric visibility
165+
class WithAsym {
166+
167+
private(set) NULL|TRUE $asym1 = TRUE;
168+
169+
public private(set) ?bool $asym2 = FALSE;
170+
171+
protected(set) FALSE|string|null $asym3 = NULL;
172+
173+
public protected(set) Type|NULL|bool $asym4 = TRUE;
174+
}

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

+12
Original file line numberDiff line numberDiff line change
@@ -160,3 +160,15 @@ class SkipOverPHP84FinalProperties {
160160
final MyType|FALSE $propA;
161161
private static final NULL|MyClass $propB;
162162
}
163+
164+
// PHP 8.4 asymmetric visibility
165+
class WithAsym {
166+
167+
private(set) NULL|TRUE $asym1 = true;
168+
169+
public private(set) ?bool $asym2 = false;
170+
171+
protected(set) FALSE|string|null $asym3 = null;
172+
173+
public protected(set) Type|NULL|bool $asym4 = true;
174+
}

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

+4
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,10 @@ public function getErrorList($testFile='')
6666
129 => 1,
6767
149 => 1,
6868
153 => 1,
69+
167 => 1,
70+
169 => 1,
71+
171 => 1,
72+
173 => 1,
6973
];
7074

7175
case 'LowerCaseConstantUnitTest.js':

src/Standards/PSR12/Sniffs/Properties/ConstantVisibilitySniff.php

+7-1
Original file line numberDiff line numberDiff line change
@@ -50,8 +50,14 @@ public function process(File $phpcsFile, $stackPtr)
5050
$ignore = Tokens::$emptyTokens;
5151
$ignore[] = T_FINAL;
5252

53+
$validVisibility = [
54+
T_PRIVATE => T_PRIVATE,
55+
T_PUBLIC => T_PUBLIC,
56+
T_PROTECTED => T_PROTECTED,
57+
];
58+
5359
$prev = $phpcsFile->findPrevious($ignore, ($stackPtr - 1), null, true);
54-
if (isset(Tokens::$scopeModifiers[$tokens[$prev]['code']]) === true) {
60+
if (isset($validVisibility[$tokens[$prev]['code']]) === true) {
5561
return;
5662
}
5763

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
<?php
2+
3+
// Intentional parse error - unsupported asym visibility used on class constant.
4+
// Testing that the sniff will flags this as the constant doesn't have a valid visibility.
5+
class Foo {
6+
public(set) const BAR = 'bar';
7+
}

src/Standards/PSR12/Tests/Properties/ConstantVisibilityUnitTest.php

+19-6
Original file line numberDiff line numberDiff line change
@@ -41,15 +41,28 @@ public function getErrorList()
4141
* The key of the array should represent the line number and the value
4242
* should represent the number of warnings that should occur on that line.
4343
*
44+
* @param string $testFile The name of the file being tested.
45+
*
4446
* @return array<int, int>
4547
*/
46-
public function getWarningList()
48+
public function getWarningList($testFile='')
4749
{
48-
return [
49-
4 => 1,
50-
12 => 1,
51-
21 => 1,
52-
];
50+
switch ($testFile) {
51+
case 'ConstantVisibilityUnitTest.1.inc':
52+
return [
53+
4 => 1,
54+
12 => 1,
55+
21 => 1,
56+
];
57+
58+
case 'ConstantVisibilityUnitTest.2.inc':
59+
return [
60+
6 => 1,
61+
];
62+
63+
default:
64+
return [];
65+
}
5366

5467
}//end getWarningList()
5568

src/Standards/PSR2/Tests/Classes/PropertyDeclarationUnitTest.inc

+14
Original file line numberDiff line numberDiff line change
@@ -96,3 +96,17 @@ class FinalProperties {
9696
public FINAL ?int $wrongOrder1;
9797
static protected final ?string $wrongOrder2;
9898
}
99+
100+
class AsymmetricVisibility {
101+
private(set) int $foo,
102+
$bar,
103+
$var = 5;
104+
105+
public private(set) readonly ?string $spaces;
106+
107+
protected(set) array $unfixed;
108+
109+
protected(set) public int $wrongOrder1;
110+
111+
private(set) protected ?string $wrongOrder2;
112+
}

src/Standards/PSR2/Tests/Classes/PropertyDeclarationUnitTest.inc.fixed

+14
Original file line numberDiff line numberDiff line change
@@ -93,3 +93,17 @@ class FinalProperties {
9393
FINAL public ?int $wrongOrder1;
9494
final protected static ?string $wrongOrder2;
9595
}
96+
97+
class AsymmetricVisibility {
98+
private(set) int $foo,
99+
$bar,
100+
$var = 5;
101+
102+
public private(set) readonly ?string $spaces;
103+
104+
protected(set) array $unfixed;
105+
106+
protected(set) public int $wrongOrder1;
107+
108+
private(set) protected ?string $wrongOrder2;
109+
}

src/Standards/PSR2/Tests/Classes/PropertyDeclarationUnitTest.php

+35-30
Original file line numberDiff line numberDiff line change
@@ -31,36 +31,41 @@ final class PropertyDeclarationUnitTest extends AbstractSniffUnitTest
3131
public function getErrorList()
3232
{
3333
return [
34-
7 => 1,
35-
9 => 2,
36-
10 => 1,
37-
11 => 1,
38-
17 => 1,
39-
18 => 1,
40-
23 => 1,
41-
38 => 1,
42-
41 => 1,
43-
42 => 1,
44-
50 => 2,
45-
51 => 1,
46-
55 => 1,
47-
56 => 1,
48-
61 => 1,
49-
62 => 1,
50-
68 => 1,
51-
69 => 1,
52-
71 => 1,
53-
72 => 1,
54-
76 => 1,
55-
80 => 1,
56-
82 => 1,
57-
84 => 1,
58-
86 => 1,
59-
90 => 1,
60-
94 => 1,
61-
95 => 1,
62-
96 => 1,
63-
97 => 2,
34+
7 => 1,
35+
9 => 2,
36+
10 => 1,
37+
11 => 1,
38+
17 => 1,
39+
18 => 1,
40+
23 => 1,
41+
38 => 1,
42+
41 => 1,
43+
42 => 1,
44+
50 => 2,
45+
51 => 1,
46+
55 => 1,
47+
56 => 1,
48+
61 => 1,
49+
62 => 1,
50+
68 => 1,
51+
69 => 1,
52+
71 => 1,
53+
72 => 1,
54+
76 => 1,
55+
80 => 1,
56+
82 => 1,
57+
84 => 1,
58+
86 => 1,
59+
90 => 1,
60+
94 => 1,
61+
95 => 1,
62+
96 => 1,
63+
97 => 2,
64+
101 => 2,
65+
105 => 1,
66+
107 => 1,
67+
109 => 1,
68+
111 => 1,
6469
];
6570

6671
}//end getErrorList()

src/Tokenizers/PHP.php

+47
Original file line numberDiff line numberDiff line change
@@ -403,8 +403,11 @@ class PHP extends Tokenizer
403403
T_PLUS_EQUAL => 2,
404404
T_PRINT => 5,
405405
T_PRIVATE => 7,
406+
T_PRIVATE_SET => 12,
406407
T_PUBLIC => 6,
408+
T_PUBLIC_SET => 11,
407409
T_PROTECTED => 9,
410+
T_PROTECTED_SET => 14,
408411
T_READONLY => 8,
409412
T_REQUIRE => 7,
410413
T_REQUIRE_ONCE => 12,
@@ -1265,6 +1268,49 @@ protected function tokenize($string)
12651268
}
12661269
}//end if
12671270

1271+
/*
1272+
Asymmetric visibility for PHP < 8.4
1273+
*/
1274+
1275+
if ($tokenIsArray === true
1276+
&& in_array($token[0], [T_PUBLIC, T_PROTECTED, T_PRIVATE], true) === true
1277+
&& ($stackPtr + 3) < $numTokens
1278+
&& $tokens[($stackPtr + 1)] === '('
1279+
&& is_array($tokens[($stackPtr + 2)]) === true
1280+
&& $tokens[($stackPtr + 2)][0] === T_STRING
1281+
&& strtolower($tokens[($stackPtr + 2)][1]) === 'set'
1282+
&& $tokens[($stackPtr + 3)] === ')'
1283+
) {
1284+
$newToken = [];
1285+
if ($token[0] === T_PUBLIC) {
1286+
$oldCode = 'T_PUBLIC';
1287+
$newToken['code'] = T_PUBLIC_SET;
1288+
$newToken['type'] = 'T_PUBLIC_SET';
1289+
} else if ($token[0] === T_PROTECTED) {
1290+
$oldCode = 'T_PROTECTED';
1291+
$newToken['code'] = T_PROTECTED_SET;
1292+
$newToken['type'] = 'T_PROTECTED_SET';
1293+
} else {
1294+
$oldCode = 'T_PRIVATE';
1295+
$newToken['code'] = T_PRIVATE_SET;
1296+
$newToken['type'] = 'T_PRIVATE_SET';
1297+
}
1298+
1299+
$newToken['content'] = $token[1].'('.$tokens[($stackPtr + 2)][1].')';
1300+
$finalTokens[$newStackPtr] = $newToken;
1301+
$newStackPtr++;
1302+
1303+
if (PHP_CODESNIFFER_VERBOSITY > 1) {
1304+
$newCode = $newToken['type'];
1305+
echo "\t\t* tokens from $stackPtr changed from $oldCode to $newCode".PHP_EOL;
1306+
}
1307+
1308+
// We processed an extra 3 tokens, for `(`, `set`, and `)`.
1309+
$stackPtr += 3;
1310+
1311+
continue;
1312+
}//end if
1313+
12681314
/*
12691315
As of PHP 8.0 fully qualified, partially qualified and namespace relative
12701316
identifier names are tokenized differently.
@@ -2189,6 +2235,7 @@ protected function tokenize($string)
21892235
if ($tokenType === T_FUNCTION
21902236
|| $tokenType === T_FN
21912237
|| isset(Tokens::$methodPrefixes[$tokenType]) === true
2238+
|| isset(Tokens::$scopeModifiers[$tokenType]) === true
21922239
|| $tokenType === T_VAR
21932240
|| $tokenType === T_READONLY
21942241
) {

src/Util/Tokens.php

+19-3
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,19 @@
180180
define('T_ENUM', 'PHPCS_T_ENUM');
181181
}
182182

183+
// Some PHP 8.4 tokens, replicated for lower versions.
184+
if (defined('T_PUBLIC_SET') === false) {
185+
define('T_PUBLIC_SET', 'PHPCS_T_PUBLIC_SET');
186+
}
187+
188+
if (defined('T_PROTECTED_SET') === false) {
189+
define('T_PROTECTED_SET', 'PHPCS_T_PROTECTED_SET');
190+
}
191+
192+
if (defined('T_PRIVATE_SET') === false) {
193+
define('T_PRIVATE_SET', 'PHPCS_T_PRIVATE_SET');
194+
}
195+
183196
// Tokens used for parsing doc blocks.
184197
define('T_DOC_COMMENT_STAR', 'PHPCS_T_DOC_COMMENT_STAR');
185198
define('T_DOC_COMMENT_WHITESPACE', 'PHPCS_T_DOC_COMMENT_WHITESPACE');
@@ -463,9 +476,12 @@ final class Tokens
463476
* @var array<int|string, int|string>
464477
*/
465478
public static $scopeModifiers = [
466-
T_PRIVATE => T_PRIVATE,
467-
T_PUBLIC => T_PUBLIC,
468-
T_PROTECTED => T_PROTECTED,
479+
T_PRIVATE => T_PRIVATE,
480+
T_PUBLIC => T_PUBLIC,
481+
T_PROTECTED => T_PROTECTED,
482+
T_PUBLIC_SET => T_PUBLIC_SET,
483+
T_PROTECTED_SET => T_PROTECTED_SET,
484+
T_PRIVATE_SET => T_PRIVATE_SET,
469485
];
470486

471487
/**
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
<?php
2+
3+
class PropertyDemo {
4+
/* testPublicSetProperty */ public(set) mixed $pub1;
5+
/* testPublicSetPropertyUC */ PUBLIC(SET) mixed $pub2;
6+
public /* testPublicPublicSetProperty */ public(set) mixed $pub3;
7+
public /* testPublicPublicSetPropertyUC */ PUBLIC(SET) mixed $pub4;
8+
9+
/* testProtectedSetProperty */ protected(set) mixed $prot1;
10+
/* testProtectedSetPropertyUC */ PROTECTED(SET) mixed $prot2;
11+
public /* testPublicProtectedSetProperty */ protected(set) mixed $prot3;
12+
public /* testPublicProtectedSetPropertyUC */ PROTECTED(SET) mixed $prot4;
13+
14+
/* testPrivateSetProperty */ private(set) mixed $priv1;
15+
/* testPrivateSetPropertyUC */ PRIVATE(SET) mixed $priv2;
16+
public /* testPublicPrivateSetProperty */ private(set) mixed $priv3;
17+
public /* testPublicPrivateSetPropertyUC */ PRIVATE(SET) mixed $priv4;
18+
19+
/* testInvalidUnsetProperty */ public(unset) mixed $invalid1;
20+
/* testInvalidSpaceProperty */ public (set) mixed $invalid2;
21+
/* testInvalidCommentProperty */ protected/* foo */(set) mixed $invalid3;
22+
/* testInvalidGetProperty */ private(get) mixed $invalid4;
23+
/* testInvalidNoParenProperty */ private set mixed $invalid5;
24+
}
25+
26+
class ConstructorPromotionDemo {
27+
public function __construct(
28+
/* testPublicSetCPP */ public(set) mixed $pub1,
29+
/* testPublicSetCPPUC */ PUBLIC(SET) mixed $pub2,
30+
public /* testPublicPublicSetCPP */ public(set) mixed $pub3,
31+
public /* testPublicPublicSetCPPUC */ PUBLIC(SET) mixed $pub4,
32+
33+
/* testProtectedSetCPP */ protected(set) mixed $prot1,
34+
/* testProtectedSetCPPUC */ PROTECTED(SET) mixed $prot2,
35+
public /* testPublicProtectedSetCPP */ protected(set) mixed $prot3,
36+
public /* testPublicProtectedSetCPPUC */ PROTECTED(SET) mixed $prot4,
37+
38+
/* testPrivateSetCPP */ private(set) mixed $priv1,
39+
/* testPrivateSetCPPUC */ PRIVATE(SET) mixed $priv2,
40+
public /* testPublicPrivateSetCPP */ private(set) mixed $priv3,
41+
public /* testPublicPrivateSetCPPUC */ PRIVATE(SET) mixed $priv4,
42+
43+
/* testInvalidUnsetCPP */ public(unset) mixed $invalid1,
44+
/* testInvalidSpaceCPP */ public (set) mixed $invalid2,
45+
/* testInvalidCommentCPP */ protected/* foo */(set) mixed $invalid3,
46+
/* testInvalidGetCPP */ private(get) mixed $invalid4,
47+
/* testInvalidNoParenCPP */ private set mixed $invalid5,
48+
) {}
49+
}
50+
51+
class NonVisibilityCases {
52+
function /* testProtectedFunctionName */ protected() {}
53+
function /* testPublicFunctionName */ public(
54+
/* testSetParameterType */ Set $setter
55+
) {}
56+
}
57+
58+
// Intentional parse error. This must be the last test in the file.
59+
class LiveCodingDemo {
60+
/* testLiveCoding */ private(set

0 commit comments

Comments
 (0)