Skip to content

Commit

Permalink
Add token emulation support for asymmetric visibility modifiers
Browse files Browse the repository at this point in the history
  • Loading branch information
nikic committed Aug 31, 2024
1 parent ba14437 commit 018da15
Show file tree
Hide file tree
Showing 5 changed files with 153 additions and 0 deletions.
2 changes: 2 additions & 0 deletions lib/PhpParser/Lexer/Emulative.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
use PhpParser\Error;
use PhpParser\ErrorHandler;
use PhpParser\Lexer;
use PhpParser\Lexer\TokenEmulator\AsymmetricVisibilityTokenEmulator;
use PhpParser\Lexer\TokenEmulator\AttributeEmulator;
use PhpParser\Lexer\TokenEmulator\EnumTokenEmulator;
use PhpParser\Lexer\TokenEmulator\ExplicitOctalEmulator;
Expand Down Expand Up @@ -45,6 +46,7 @@ public function __construct(?PhpVersion $phpVersion = null) {
new ExplicitOctalEmulator(),
new ReadonlyFunctionTokenEmulator(),
new PropertyTokenEmulator(),
new AsymmetricVisibilityTokenEmulator(),
];

// Collect emulators that are relevant for the PHP version we're running
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
<?php declare(strict_types=1);

namespace PhpParser\Lexer\TokenEmulator;

use PhpParser\PhpVersion;
use PhpParser\Token;

final class AsymmetricVisibilityTokenEmulator extends TokenEmulator {
public function getPhpVersion(): PhpVersion {
return PhpVersion::fromComponents(8, 4);
}
public function isEmulationNeeded(string $code): bool {
$code = strtolower($code);
return strpos($code, 'public(set)') !== false ||
strpos($code, 'protected(set)') !== false ||
strpos($code, 'private(set)') !== false;
}

public function emulate(string $code, array $tokens): array {
$map = [
\T_PUBLIC => \T_PUBLIC_SET,
\T_PROTECTED => \T_PROTECTED_SET,
\T_PRIVATE => \T_PRIVATE_SET,
];
for ($i = 0, $c = count($tokens); $i < $c; ++$i) {
$token = $tokens[$i];
if (isset($map[$token->id]) && $i + 3 < $c && $tokens[$i + 1]->text === '(' &&
$tokens[$i + 2]->id === \T_STRING && \strtolower($tokens[$i + 2]->text) === 'set' &&
$tokens[$i + 3]->text === ')' &&
$this->isKeywordContext($tokens, $i)
) {
array_splice($tokens, $i, 4, [
new Token(
$map[$token->id], $token->text . '(' . $tokens[$i + 2]->text . ')',
$token->line, $token->pos),
]);
$c -= 3;
}
}

return $tokens;
}

public function reverseEmulate(string $code, array $tokens): array {
$reverseMap = [
\T_PUBLIC_SET => \T_PUBLIC,
\T_PROTECTED_SET => \T_PROTECTED,
\T_PRIVATE_SET => \T_PRIVATE,
];
for ($i = 0, $c = count($tokens); $i < $c; ++$i) {
$token = $tokens[$i];
if (isset($reverseMap[$token->id]) &&
\preg_match('/(public|protected|private)\((set)\)/i', $token->text, $matches)
) {
[, $modifier, $set] = $matches;
$modifierLen = \strlen($modifier);
array_splice($tokens, $i, 1, [
new Token($reverseMap[$token->id], $modifier, $token->line, $token->pos),
new Token(\ord('('), '(', $token->line, $token->pos + $modifierLen),
new Token(\T_STRING, $set, $token->line, $token->pos + $modifierLen + 1),
new Token(\ord(')'), ')', $token->line, $token->pos + $modifierLen + 4),
]);
$i += 3;
$c += 3;
}
}

return $tokens;
}

/** @param Token[] $tokens */
protected function isKeywordContext(array $tokens, int $pos): bool {
$prevToken = $this->getPreviousNonSpaceToken($tokens, $pos);
if ($prevToken === null) {
return false;
}
return $prevToken->id !== \T_OBJECT_OPERATOR
&& $prevToken->id !== \T_NULLSAFE_OBJECT_OPERATOR;
}

/** @param Token[] $tokens */
private function getPreviousNonSpaceToken(array $tokens, int $start): ?Token {
for ($i = $start - 1; $i >= 0; --$i) {
if ($tokens[$i]->id === T_WHITESPACE) {
continue;
}

return $tokens[$i];
}

return null;
}
}
3 changes: 3 additions & 0 deletions lib/PhpParser/compatibility_tokens.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ function defineCompatibilityTokens(): void {
'T_READONLY',
// PHP 8.4
'T_PROPERTY_C',
'T_PUBLIC_SET',
'T_PROTECTED_SET',
'T_PRIVATE_SET',
];

// PHP-Parser might be used together with another library that also emulates some or all
Expand Down
15 changes: 15 additions & 0 deletions phpstan-baseline.neon
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,21 @@ parameters:
count: 1
path: lib/PhpParser/Lexer/Emulative.php

-
message: "#^Constant T_PRIVATE_SET not found\\.$#"
count: 2
path: lib/PhpParser/Lexer/TokenEmulator/AsymmetricVisibilityTokenEmulator.php

-
message: "#^Constant T_PROTECTED_SET not found\\.$#"
count: 2
path: lib/PhpParser/Lexer/TokenEmulator/AsymmetricVisibilityTokenEmulator.php

-
message: "#^Constant T_PUBLIC_SET not found\\.$#"
count: 2
path: lib/PhpParser/Lexer/TokenEmulator/AsymmetricVisibilityTokenEmulator.php

-
message: "#^Constant T_PROPERTY_C not found\\.$#"
count: 1
Expand Down
40 changes: 40 additions & 0 deletions test/PhpParser/Lexer/EmulativeTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -393,6 +393,37 @@ public static function provideTestLexNewFeatures() {
[\T_READONLY, 'readonly'],
[ord('('), '('],
]],

// PHP 8.4: Asymmetric visibility modifiers
['private(set)', [
[\T_PRIVATE_SET, 'private(set)']
]],
['PROTECTED(SET)', [
[\T_PROTECTED_SET, 'PROTECTED(SET)']
]],
['Public(Set)', [
[\T_PUBLIC_SET, 'Public(Set)']
]],
['public (set)', [
[\T_PUBLIC, 'public'],
[\ord('('), '('],
[\T_STRING, 'set'],
[\ord(')'), ')'],
]],
['->public(set)', [
[\T_OBJECT_OPERATOR, '->'],
[\T_STRING, 'public'],
[\ord('('), '('],
[\T_STRING, 'set'],
[\ord(')'), ')'],
]],
['?-> public(set)', [
[\T_NULLSAFE_OBJECT_OPERATOR, '?->'],
[\T_STRING, 'public'],
[\ord('('), '('],
[\T_STRING, 'set'],
[\ord(')'), ')'],
]],
];
}

Expand Down Expand Up @@ -431,6 +462,15 @@ public static function provideTestTargetVersion() {
['8.3', '__PROPERTY__', [[\T_STRING, '__PROPERTY__']]],
['8.4', '__property__', [[\T_PROPERTY_C, '__property__']]],
['8.3', '__property__', [[\T_STRING, '__property__']]],
['8.4', 'public(set)', [
[\T_PUBLIC_SET, 'public(set)'],
]],
['8.3', 'public(set)', [
[\T_PUBLIC, 'public'],
[\ord('('), '('],
[\T_STRING, 'set'],
[\ord(')'), ')']
]],
];
}
}

0 comments on commit 018da15

Please sign in to comment.