Skip to content

Commit 018da15

Browse files
committed
Add token emulation support for asymmetric visibility modifiers
1 parent ba14437 commit 018da15

File tree

5 files changed

+153
-0
lines changed

5 files changed

+153
-0
lines changed

lib/PhpParser/Lexer/Emulative.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
use PhpParser\Error;
66
use PhpParser\ErrorHandler;
77
use PhpParser\Lexer;
8+
use PhpParser\Lexer\TokenEmulator\AsymmetricVisibilityTokenEmulator;
89
use PhpParser\Lexer\TokenEmulator\AttributeEmulator;
910
use PhpParser\Lexer\TokenEmulator\EnumTokenEmulator;
1011
use PhpParser\Lexer\TokenEmulator\ExplicitOctalEmulator;
@@ -45,6 +46,7 @@ public function __construct(?PhpVersion $phpVersion = null) {
4546
new ExplicitOctalEmulator(),
4647
new ReadonlyFunctionTokenEmulator(),
4748
new PropertyTokenEmulator(),
49+
new AsymmetricVisibilityTokenEmulator(),
4850
];
4951

5052
// Collect emulators that are relevant for the PHP version we're running
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
<?php declare(strict_types=1);
2+
3+
namespace PhpParser\Lexer\TokenEmulator;
4+
5+
use PhpParser\PhpVersion;
6+
use PhpParser\Token;
7+
8+
final class AsymmetricVisibilityTokenEmulator extends TokenEmulator {
9+
public function getPhpVersion(): PhpVersion {
10+
return PhpVersion::fromComponents(8, 4);
11+
}
12+
public function isEmulationNeeded(string $code): bool {
13+
$code = strtolower($code);
14+
return strpos($code, 'public(set)') !== false ||
15+
strpos($code, 'protected(set)') !== false ||
16+
strpos($code, 'private(set)') !== false;
17+
}
18+
19+
public function emulate(string $code, array $tokens): array {
20+
$map = [
21+
\T_PUBLIC => \T_PUBLIC_SET,
22+
\T_PROTECTED => \T_PROTECTED_SET,
23+
\T_PRIVATE => \T_PRIVATE_SET,
24+
];
25+
for ($i = 0, $c = count($tokens); $i < $c; ++$i) {
26+
$token = $tokens[$i];
27+
if (isset($map[$token->id]) && $i + 3 < $c && $tokens[$i + 1]->text === '(' &&
28+
$tokens[$i + 2]->id === \T_STRING && \strtolower($tokens[$i + 2]->text) === 'set' &&
29+
$tokens[$i + 3]->text === ')' &&
30+
$this->isKeywordContext($tokens, $i)
31+
) {
32+
array_splice($tokens, $i, 4, [
33+
new Token(
34+
$map[$token->id], $token->text . '(' . $tokens[$i + 2]->text . ')',
35+
$token->line, $token->pos),
36+
]);
37+
$c -= 3;
38+
}
39+
}
40+
41+
return $tokens;
42+
}
43+
44+
public function reverseEmulate(string $code, array $tokens): array {
45+
$reverseMap = [
46+
\T_PUBLIC_SET => \T_PUBLIC,
47+
\T_PROTECTED_SET => \T_PROTECTED,
48+
\T_PRIVATE_SET => \T_PRIVATE,
49+
];
50+
for ($i = 0, $c = count($tokens); $i < $c; ++$i) {
51+
$token = $tokens[$i];
52+
if (isset($reverseMap[$token->id]) &&
53+
\preg_match('/(public|protected|private)\((set)\)/i', $token->text, $matches)
54+
) {
55+
[, $modifier, $set] = $matches;
56+
$modifierLen = \strlen($modifier);
57+
array_splice($tokens, $i, 1, [
58+
new Token($reverseMap[$token->id], $modifier, $token->line, $token->pos),
59+
new Token(\ord('('), '(', $token->line, $token->pos + $modifierLen),
60+
new Token(\T_STRING, $set, $token->line, $token->pos + $modifierLen + 1),
61+
new Token(\ord(')'), ')', $token->line, $token->pos + $modifierLen + 4),
62+
]);
63+
$i += 3;
64+
$c += 3;
65+
}
66+
}
67+
68+
return $tokens;
69+
}
70+
71+
/** @param Token[] $tokens */
72+
protected function isKeywordContext(array $tokens, int $pos): bool {
73+
$prevToken = $this->getPreviousNonSpaceToken($tokens, $pos);
74+
if ($prevToken === null) {
75+
return false;
76+
}
77+
return $prevToken->id !== \T_OBJECT_OPERATOR
78+
&& $prevToken->id !== \T_NULLSAFE_OBJECT_OPERATOR;
79+
}
80+
81+
/** @param Token[] $tokens */
82+
private function getPreviousNonSpaceToken(array $tokens, int $start): ?Token {
83+
for ($i = $start - 1; $i >= 0; --$i) {
84+
if ($tokens[$i]->id === T_WHITESPACE) {
85+
continue;
86+
}
87+
88+
return $tokens[$i];
89+
}
90+
91+
return null;
92+
}
93+
}

lib/PhpParser/compatibility_tokens.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,9 @@ function defineCompatibilityTokens(): void {
1919
'T_READONLY',
2020
// PHP 8.4
2121
'T_PROPERTY_C',
22+
'T_PUBLIC_SET',
23+
'T_PROTECTED_SET',
24+
'T_PRIVATE_SET',
2225
];
2326

2427
// PHP-Parser might be used together with another library that also emulates some or all

phpstan-baseline.neon

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,21 @@ parameters:
9090
count: 1
9191
path: lib/PhpParser/Lexer/Emulative.php
9292

93+
-
94+
message: "#^Constant T_PRIVATE_SET not found\\.$#"
95+
count: 2
96+
path: lib/PhpParser/Lexer/TokenEmulator/AsymmetricVisibilityTokenEmulator.php
97+
98+
-
99+
message: "#^Constant T_PROTECTED_SET not found\\.$#"
100+
count: 2
101+
path: lib/PhpParser/Lexer/TokenEmulator/AsymmetricVisibilityTokenEmulator.php
102+
103+
-
104+
message: "#^Constant T_PUBLIC_SET not found\\.$#"
105+
count: 2
106+
path: lib/PhpParser/Lexer/TokenEmulator/AsymmetricVisibilityTokenEmulator.php
107+
93108
-
94109
message: "#^Constant T_PROPERTY_C not found\\.$#"
95110
count: 1

test/PhpParser/Lexer/EmulativeTest.php

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -393,6 +393,37 @@ public static function provideTestLexNewFeatures() {
393393
[\T_READONLY, 'readonly'],
394394
[ord('('), '('],
395395
]],
396+
397+
// PHP 8.4: Asymmetric visibility modifiers
398+
['private(set)', [
399+
[\T_PRIVATE_SET, 'private(set)']
400+
]],
401+
['PROTECTED(SET)', [
402+
[\T_PROTECTED_SET, 'PROTECTED(SET)']
403+
]],
404+
['Public(Set)', [
405+
[\T_PUBLIC_SET, 'Public(Set)']
406+
]],
407+
['public (set)', [
408+
[\T_PUBLIC, 'public'],
409+
[\ord('('), '('],
410+
[\T_STRING, 'set'],
411+
[\ord(')'), ')'],
412+
]],
413+
['->public(set)', [
414+
[\T_OBJECT_OPERATOR, '->'],
415+
[\T_STRING, 'public'],
416+
[\ord('('), '('],
417+
[\T_STRING, 'set'],
418+
[\ord(')'), ')'],
419+
]],
420+
['?-> public(set)', [
421+
[\T_NULLSAFE_OBJECT_OPERATOR, '?->'],
422+
[\T_STRING, 'public'],
423+
[\ord('('), '('],
424+
[\T_STRING, 'set'],
425+
[\ord(')'), ')'],
426+
]],
396427
];
397428
}
398429

@@ -431,6 +462,15 @@ public static function provideTestTargetVersion() {
431462
['8.3', '__PROPERTY__', [[\T_STRING, '__PROPERTY__']]],
432463
['8.4', '__property__', [[\T_PROPERTY_C, '__property__']]],
433464
['8.3', '__property__', [[\T_STRING, '__property__']]],
465+
['8.4', 'public(set)', [
466+
[\T_PUBLIC_SET, 'public(set)'],
467+
]],
468+
['8.3', 'public(set)', [
469+
[\T_PUBLIC, 'public'],
470+
[\ord('('), '('],
471+
[\T_STRING, 'set'],
472+
[\ord(')'), ')']
473+
]],
434474
];
435475
}
436476
}

0 commit comments

Comments
 (0)