• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

jojo1981 / json-ast-builder / 17555189188

08 Sep 2025 03:03PM UTC coverage: 17.664%. Remained the same
17555189188

push

github

jojo1981
Fix: Make compatible with PHP versions: ^8.0|^8.1|^8.2|^8.3|^8.4. Closes #1
https://github.com/jojo1981/json-ast-builder/issues/1

89 of 408 new or added lines in 17 files covered. (21.81%)

355 existing lines in 25 files now uncovered.

189 of 1070 relevant lines covered (17.66%)

1.71 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

0.0
/src/Parser.php
1
<?php
2
/*
3
 * This file is part of the jojo1981/json-ast-builder package
4
 *
5
 * Copyright (c) 2019 Joost Nijhuis <jnijhuis81@gmail.com>
6
 *
7
 * For the full copyright and license information, please view the LICENSE
8
 * file that was distributed in the root of the source code
9
 */
10
declare(strict_types=1);
11

12
namespace Jojo1981\JsonAstBuilder;
13

14
use Jojo1981\JsonAstBuilder\Ast\ArrayNode;
15
use Jojo1981\JsonAstBuilder\Ast\BooleanNode;
16
use Jojo1981\JsonAstBuilder\Ast\ElementNode;
17
use Jojo1981\JsonAstBuilder\Ast\IntegerNode;
18
use Jojo1981\JsonAstBuilder\Ast\JsonNode;
19
use Jojo1981\JsonAstBuilder\Ast\KeyNode;
20
use Jojo1981\JsonAstBuilder\Ast\MemberNode;
21
use Jojo1981\JsonAstBuilder\Ast\NullNode;
22
use Jojo1981\JsonAstBuilder\Ast\NumberNode;
23
use Jojo1981\JsonAstBuilder\Ast\ObjectNode;
24
use Jojo1981\JsonAstBuilder\Ast\StringNode;
25
use Jojo1981\JsonAstBuilder\Ast\ValueNode;
26
use Jojo1981\JsonAstBuilder\Exception\ParseException;
27
use Jojo1981\JsonAstBuilder\Lexer\LexerInterface;
28
use Jojo1981\JsonAstBuilder\Lexer\Token;
29
use Jojo1981\JsonAstBuilder\Lexer\TokenType;
30
use UnexpectedValueException;
31
use function chr;
32
use function hexdec;
33
use function in_array;
34
use function preg_match;
35
use function strlen;
36
use function substr;
37

38
/**
39
 * The parser is responsible for generating an AST from the tokens it will get from the lexer and performs the
40
 * semantic analysis.
41
 *
42
 * @package Jojo1981\JsonAstBuilder
43
 */
44
final class Parser
45
{
46
    /** @var int[] */
47
    private const WHITE_SPACE_TOKENS = [
48
        TokenType::TOKEN_WHITE_SPACE,
49
        TokenType::TOKEN_NEWLINE
50
    ];
51

52
    /** @var LexerInterface */
53
    private LexerInterface $lexer;
54

55
    /**
56
     * @param LexerInterface $lexer
57
     */
58
    public function __construct(LexerInterface $lexer)
59
    {
UNCOV
60
        $this->lexer = $lexer;
×
61
    }
62

63
    /**
64
     * @param string $input
65
     * @return void
66
     * @throws ParseException
67
     */
68
    public function setInput(string $input): void
69
    {
70
        $this->lexer->setInput($input);
×
71
    }
72

73
    /**
74
     * @return JsonNode
75
     * @throws UnexpectedValueException
76
     * @throws ParseException
77
     */
78
    public function parse(): JsonNode
79
    {
80
        $this->lexer->reset();
×
81

UNCOV
82
        return $this->json();
×
83
    }
84

85
    /**
86
     * @return JsonNode
87
     * @throws UnexpectedValueException
88
     * @throws ParseException
89
     */
90
    private function json(): JsonNode
91
    {
92
        $jsonNode = new JsonNode($this->element());
×
UNCOV
93
        $this->eatToken(TokenType::TOKEN_EOF);
×
94

UNCOV
95
        return $jsonNode;
×
96
    }
97

98
    /**
99
     * @return ElementNode
100
     * @throws UnexpectedValueException
101
     * @throws ParseException
102
     */
103
    private function element(): ElementNode
104
    {
105
        $this->eatWhiteSpace();
×
UNCOV
106
        $valueNode = $this->value();
×
UNCOV
107
        $this->eatWhiteSpace();
×
108

UNCOV
109
        return new ElementNode($valueNode);
×
110
    }
111

112
    /**
113
     * @return void
114
     * @throws UnexpectedValueException
115
     * @throws ParseException
116
     */
117
    private function eatWhiteSpace(): void
118
    {
NEW
119
        while (in_array($this->lexer->getCurrent()->getType(), self::WHITE_SPACE_TOKENS, true)) {
×
NEW
120
            $this->lexer->getNext();
×
121
        }
122
    }
123

124
    /**
125
     * @return ValueNode
126
     * @throws UnexpectedValueException
127
     * @throws ParseException
128
     */
129
    private function value(): ValueNode
130
    {
131
        $expectedTokens = [
×
UNCOV
132
            TokenType::getLiteral(TokenType::TOKEN_LEFT_CURLY_BRACKET),
×
UNCOV
133
            TokenType::getLiteral(TokenType::TOKEN_LEFT_SQUARE_BRACKET),
×
UNCOV
134
            TokenType::getLiteral(TokenType::TOKEN_STRING),
×
UNCOV
135
            TokenType::getLiteral(TokenType::TOKEN_NUMBER),
×
UNCOV
136
            TokenType::getLiteral(TokenType::TOKEN_INT),
×
UNCOV
137
            TokenType::getLiteral(TokenType::TOKEN_KEYWORD)
×
UNCOV
138
        ];
×
139

UNCOV
140
        $currentToken = $this->lexer->getCurrent();
×
141
        switch ($currentToken->getType()) {
×
NEW
142
            case (TokenType::TOKEN_EOF):
×
NEW
143
            {
×
UNCOV
144
                throw ParseException::unexpectedEndOfFile($this->lexer->getCurrent(), $expectedTokens);
×
UNCOV
145
            }
×
NEW
146
            case (TokenType::TOKEN_LEFT_CURLY_BRACKET):
×
NEW
147
            {
×
UNCOV
148
                $typeNode = $this->object();
×
UNCOV
149
                break;
×
UNCOV
150
            }
×
NEW
151
            case (TokenType::TOKEN_LEFT_SQUARE_BRACKET):
×
NEW
152
            {
×
UNCOV
153
                $typeNode = $this->array();
×
UNCOV
154
                break;
×
UNCOV
155
            }
×
NEW
156
            case (TokenType::TOKEN_STRING):
×
NEW
157
            {
×
158
                $typeNode = $this->string();
×
159
                break;
×
160
            }
×
NEW
161
            case (TokenType::TOKEN_NUMBER):
×
NEW
162
            {
×
163
                $typeNode = new NumberNode((float) $currentToken->getLexeme());
×
164
                $typeNode->setToken($currentToken);
×
165
                $this->eatToken(TokenType::TOKEN_NUMBER);
×
UNCOV
166
                break;
×
167
            }
×
NEW
168
            case (TokenType::TOKEN_INT):
×
NEW
169
            {
×
170
                $typeNode = new IntegerNode((int) $currentToken->getLexeme());
×
171
                $typeNode->setToken($currentToken);
×
172
                $this->eatToken(TokenType::TOKEN_INT);
×
173
                break;
×
174
            }
×
NEW
175
            case (TokenType::TOKEN_KEYWORD && in_array($currentToken->getLexeme(), ['true', 'false'], true)):
×
NEW
176
            {
×
177
                $typeNode = new BooleanNode('true' === $currentToken->getLexeme());
×
178
                $typeNode->setToken($currentToken);
×
179
                $this->eatToken(TokenType::TOKEN_KEYWORD);
×
180
                break;
×
181
            }
×
NEW
182
            case (TokenType::TOKEN_KEYWORD && 'null' === $currentToken->getLexeme()):
×
NEW
183
            {
×
184
                $typeNode = new NullNode();
×
185
                $typeNode->setToken($currentToken);
×
186
                $this->eatToken(TokenType::TOKEN_KEYWORD);
×
187
                break;
×
188
            }
×
189
            default:
190
                throw ParseException::unexpectedToken($currentToken, $expectedTokens);
×
191
        }
192

193
        return new ValueNode($typeNode);
×
194
    }
195

196
    /**
197
     * @return ObjectNode
198
     * @throws UnexpectedValueException
199
     * @throws ParseException
200
     */
201
    private function object(): ObjectNode
202
    {
203
        $currentToken = $this->lexer->getCurrent();
×
204
        $this->eatToken(TokenType::TOKEN_LEFT_CURLY_BRACKET);
×
205
        $this->eatWhiteSpace();
×
206
        $members = $this->members();
×
UNCOV
207
        $this->eatWhiteSpace();
×
208
        $this->eatToken(TokenType::TOKEN_RIGHT_CURLY_BRACKET);
×
209

UNCOV
210
        $objectNode = new ObjectNode($members);
×
211
        $objectNode->setToken($currentToken);
×
212

UNCOV
213
        return $objectNode;
×
214
    }
215

216
    /**
217
     * @param int $tokenType
218
     * @return void
219
     * @throws UnexpectedValueException
220
     * @throws ParseException
221
     */
222
    private function eatToken(int $tokenType): void
223
    {
NEW
224
        if ($this->lexer->getCurrent()->getType() !== $tokenType) {
×
NEW
225
            throw ParseException::unexpectedToken($this->lexer->getCurrent(), [TokenType::getLiteral($tokenType)]);
×
226
        }
NEW
227
        if (TokenType::TOKEN_EOF !== $tokenType) {
×
NEW
228
            $this->lexer->getNext();
×
229
        }
230
    }
231

232
    /**
233
     * @return MemberNode[]
234
     * @throws UnexpectedValueException
235
     * @throws ParseException
236
     */
237
    private function members(): array
238
    {
239
        $members = [];
×
UNCOV
240
        while ($this->lexer->getCurrent()->getType() !== TokenType::TOKEN_RIGHT_CURLY_BRACKET) {
×
241
            $members[] = $this->member();
×
242
            if ($this->lexer->getCurrent()->getType() !== TokenType::TOKEN_RIGHT_CURLY_BRACKET) {
×
UNCOV
243
                $commaToken = $this->lexer->getCurrent();
×
UNCOV
244
                $this->eatToken(TokenType::TOKEN_COMMA);
×
UNCOV
245
                $this->eatWhiteSpace();
×
UNCOV
246
                if ($this->lexer->getCurrent()->getType() === TokenType::TOKEN_RIGHT_CURLY_BRACKET) {
×
UNCOV
247
                    ParseException::illegalTrailingComma($commaToken);
×
248
                }
249
            }
250
        }
251

UNCOV
252
        return $members;
×
253
    }
254

255
    /**
256
     * @return MemberNode
257
     * @throws UnexpectedValueException
258
     * @throws ParseException
259
     */
260
    private function member(): MemberNode
261
    {
UNCOV
262
        $keyNode = $this->key();
×
UNCOV
263
        $this->eatWhiteSpace();
×
UNCOV
264
        $this->eatToken(TokenType::TOKEN_COLON);
×
UNCOV
265
        $this->eatWhiteSpace();
×
266
        $element = $this->element();
×
267

UNCOV
268
        return new MemberNode($keyNode, $element);
×
269
    }
270

271
    /**
272
     * @return KeyNode
273
     * @throws UnexpectedValueException
274
     * @throws ParseException
275
     */
276
    private function key(): KeyNode
277
    {
NEW
278
        $currentToken = $this->lexer->getCurrent();
×
NEW
279
        $this->eatToken(TokenType::TOKEN_STRING);
×
280

NEW
281
        $keyNode = new KeyNode(substr(self::parseStringValue($currentToken), 1, -1));
×
NEW
282
        $keyNode->setToken($currentToken);
×
283

NEW
284
        return $keyNode;
×
285
    }
286

287
    /**
288
     * @return ArrayNode
289
     * @throws UnexpectedValueException
290
     * @throws ParseException
291
     */
292
    private function array(): ArrayNode
293
    {
294
        $token = $this->lexer->getCurrent();
×
295
        $this->eatToken(TokenType::TOKEN_LEFT_SQUARE_BRACKET);
×
296
        $this->eatWhiteSpace();
×
UNCOV
297
        $elements = $this->elements();
×
298
        $this->eatToken(TokenType::TOKEN_RIGHT_SQUARE_BRACKET);
×
299

UNCOV
300
        $arrayNode = new ArrayNode($elements);
×
UNCOV
301
        $arrayNode->setToken($token);
×
302

UNCOV
303
        return $arrayNode;
×
304
    }
305

306
    /**
307
     * @return ElementNode[]
308
     * @throws UnexpectedValueException
309
     * @throws ParseException
310
     */
311
    private function elements(): array
312
    {
UNCOV
313
        $elements = [];
×
314
        while ($this->lexer->getCurrent()->getType() !== TokenType::TOKEN_RIGHT_SQUARE_BRACKET) {
×
UNCOV
315
            $elements[] = $this->element();
×
UNCOV
316
            if ($this->lexer->getCurrent()->getType() !== TokenType::TOKEN_RIGHT_SQUARE_BRACKET) {
×
UNCOV
317
                $commaToken = $this->lexer->getCurrent();
×
UNCOV
318
                $this->eatToken(TokenType::TOKEN_COMMA);
×
UNCOV
319
                $this->eatWhiteSpace();
×
UNCOV
320
                if ($this->lexer->getCurrent()->getType() === TokenType::TOKEN_RIGHT_SQUARE_BRACKET) {
×
UNCOV
321
                    throw ParseException::illegalTrailingComma($commaToken);
×
322
                }
323
            }
324
        }
325

326
        return $elements;
×
327
    }
328

329
    /**
330
     * @return StringNode
331
     * @throws UnexpectedValueException
332
     * @throws ParseException
333
     */
334
    private function string(): StringNode
335
    {
NEW
336
        $currentToken = $this->lexer->getCurrent();
×
NEW
337
        $this->eatToken(TokenType::TOKEN_STRING);
×
338

NEW
339
        $stringNode = new StringNode(self::parseStringValue($currentToken));
×
NEW
340
        $stringNode->setToken($currentToken);
×
341

NEW
342
        return $stringNode;
×
343
    }
344

345
    /**
346
     * @param Token $token
347
     * @return string
348
     * @throws UnexpectedValueException
349
     */
350
    private static function parseStringValue(Token $token): string
351
    {
NEW
352
        if ($token->getType() !== TokenType::TOKEN_STRING) {
×
NEW
353
            throw new UnexpectedValueException('Token is not a string');
×
354
        }
355

NEW
356
        $result = '';
×
NEW
357
        $rawString = substr($token->getLexeme(), 1, -1);
×
NEW
358
        $length = strlen($rawString);
×
NEW
359
        for ($i = 0; $i < $length; $i++) {
×
NEW
360
            $char = $rawString[$i];
×
NEW
361
            if ('\\' === $char) {
×
NEW
362
                $nextChar = $rawString[$i + 1] ?? '';
×
NEW
363
                if (in_array($nextChar, ['"', '\\', '/', 'b', 'f', 'n', 'r', 't'], true)) {
×
NEW
364
                    $result .= self::parseControlChar($char . $nextChar);
×
NEW
365
                    $i++;
×
NEW
366
                } elseif ('u' === $nextChar) {
×
NEW
367
                    $unicodeSequence = substr($rawString, $i, 6);
×
NEW
368
                    if (strlen($unicodeSequence) !== 6 || !preg_match('/\\\\u[0-9a-fA-F]{4}/', $unicodeSequence)) {
×
NEW
369
                        throw new UnexpectedValueException('Invalid Unicode escape sequence: ' . $unicodeSequence);
×
370
                    }
NEW
371
                    $result .= self::parseUnicode($unicodeSequence);
×
NEW
372
                    $i += 5;
×
373
                } else {
NEW
374
                    throw new UnexpectedValueException('Invalid escape sequence: \\' . $nextChar);
×
375
                }
376
            } else {
NEW
377
                $result .= $char;
×
378
            }
379
        }
380

NEW
381
        return $result;
×
382
    }
383

384
    /**
385
     * @param string $sequence
386
     * @return string
387
     * @throws UnexpectedValueException
388
     */
389
    private static function parseControlChar(string $sequence): string
390
    {
NEW
391
        return match ($sequence) {
×
NEW
392
            '\\"' => '"',
×
NEW
393
            '\\\\' => '\\',
×
NEW
394
            '\\/' => '/',
×
NEW
395
            '\\b' => "\b",
×
NEW
396
            '\\f' => "\f",
×
NEW
397
            '\\n' => "\n",
×
NEW
398
            '\\r' => "\r",
×
NEW
399
            '\\t' => "\t",
×
NEW
400
            default => throw new UnexpectedValueException('Invalid escape sequence: ' . $sequence)
×
NEW
401
        };
×
402
    }
403

404
    /**
405
     * @param string $sequence
406
     * @return string
407
     * @throws UnexpectedValueException
408
     */
409
    private static function parseUnicode(string $sequence): string
410
    {
NEW
411
        $codePoint = hexdec(substr($sequence, 2));
×
NEW
412
        if ($codePoint <= 0x7F) {
×
NEW
413
            return chr($codePoint);
×
NEW
414
        } elseif ($codePoint <= 0x7FF) {
×
NEW
415
            return chr(0xC0 | ($codePoint >> 6)) .
×
NEW
416
                   chr(0x80 | ($codePoint & 0x3F));
×
NEW
417
        } elseif ($codePoint <= 0xFFFF) {
×
NEW
418
            return chr(0xE0 | ($codePoint >> 12)) .
×
NEW
419
                   chr(0x80 | (($codePoint >> 6) & 0x3F)) .
×
NEW
420
                   chr(0x80 | ($codePoint & 0x3F));
×
NEW
421
        } elseif ($codePoint <= 0x10FFFF) {
×
NEW
422
            return chr(0xF0 | ($codePoint >> 18)) .
×
NEW
423
                   chr(0x80 | (($codePoint >> 12) & 0x3F)) .
×
NEW
424
                   chr(0x80 | (($codePoint >> 6) & 0x3F)) .
×
NEW
425
                   chr(0x80 | ($codePoint & 0x3F));
×
426
        } else {
NEW
427
            throw new UnexpectedValueException('Invalid Unicode code point: ' . $sequence);
×
428
        }
429
    }
430
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc