1<?php declare(strict_types=1);
2
3namespace PhpParser\Lexer\TokenEmulator;
4
5use PhpParser\PhpVersion;
6use PhpParser\Token;
7
8final 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}
94