1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
/* |
4
|
|
|
* This file is part of the colinodell/json5 package. |
5
|
|
|
* |
6
|
|
|
* (c) Colin O'Dell <[email protected]> |
7
|
|
|
* |
8
|
|
|
* Based on the official JSON5 implementation for JavaScript (https://github.com/json5/json5) |
9
|
|
|
* - (c) 2012-2016 Aseem Kishore and others (https://github.com/json5/json5/contributors) |
10
|
|
|
* |
11
|
|
|
* For the full copyright and license information, please view the LICENSE |
12
|
|
|
* file that was distributed with this source code. |
13
|
|
|
*/ |
14
|
|
|
|
15
|
|
|
namespace ColinODell\Json5; |
16
|
|
|
|
17
|
|
|
final class Json5Decoder |
18
|
|
|
{ |
19
|
|
|
const REGEX_WHITESPACE = '/[ \t\r\n\v\f\xA0\x{FEFF}]/u'; |
20
|
|
|
|
21
|
|
|
private $json; |
22
|
|
|
|
23
|
|
|
private $at = 0; |
24
|
|
|
|
25
|
|
|
private $lineNumber = 1; |
26
|
|
|
|
27
|
|
|
private $columnNumber = 1; |
28
|
|
|
|
29
|
|
|
private $ch; |
30
|
|
|
|
31
|
|
|
private $associative = false; |
32
|
|
|
|
33
|
|
|
private $maxDepth = 512; |
34
|
|
|
|
35
|
|
|
private $castBigIntToString = false; |
36
|
|
|
|
37
|
|
|
private $depth = 1; |
38
|
|
|
|
39
|
|
|
private $length; |
40
|
|
|
|
41
|
|
|
private $lineCache; |
42
|
|
|
|
43
|
|
|
/** |
44
|
|
|
* Private constructor. |
45
|
|
|
* |
46
|
|
|
* @param string $json |
47
|
|
|
* @param bool $associative |
48
|
|
|
* @param int $depth |
49
|
|
|
* @param bool $castBigIntToString |
50
|
|
|
*/ |
51
|
360 |
|
private function __construct($json, $associative = false, $depth = 512, $castBigIntToString = false) |
52
|
|
|
{ |
53
|
360 |
|
$this->json = $json; |
54
|
360 |
|
$this->associative = $associative; |
55
|
360 |
|
$this->maxDepth = $depth; |
56
|
360 |
|
$this->castBigIntToString = $castBigIntToString; |
57
|
|
|
|
58
|
360 |
|
$this->length = mb_strlen($json, 'utf-8'); |
59
|
|
|
|
60
|
360 |
|
$this->ch = $this->charAt(0); |
61
|
360 |
|
} |
62
|
|
|
|
63
|
|
|
/** |
64
|
|
|
* Takes a JSON encoded string and converts it into a PHP variable. |
65
|
|
|
* |
66
|
|
|
* The parameters exactly match PHP's json_decode() function - see |
67
|
|
|
* http://php.net/manual/en/function.json-decode.php for more information. |
68
|
|
|
* |
69
|
|
|
* @param string $source The JSON string being decoded. |
70
|
|
|
* @param bool $associative When TRUE, returned objects will be converted into associative arrays. |
71
|
|
|
* @param int $depth User specified recursion depth. |
72
|
|
|
* @param int $options Bitmask of JSON decode options. |
73
|
|
|
* |
74
|
|
|
* @return mixed |
75
|
|
|
*/ |
76
|
360 |
|
public static function decode($source, $associative = false, $depth = 512, $options = 0) |
77
|
|
|
{ |
78
|
360 |
|
$associative = $associative || ($options & JSON_OBJECT_AS_ARRAY); |
79
|
360 |
|
$castBigIntToString = $options & JSON_BIGINT_AS_STRING; |
80
|
|
|
|
81
|
360 |
|
$decoder = new self((string)$source, $associative, $depth, $castBigIntToString); |
82
|
|
|
|
83
|
360 |
|
$result = $decoder->value(); |
84
|
285 |
|
$decoder->white(); |
85
|
282 |
|
if ($decoder->ch) { |
86
|
18 |
|
$decoder->throwSyntaxError('Syntax error'); |
87
|
|
|
} |
88
|
|
|
|
89
|
264 |
|
return $result; |
90
|
|
|
} |
91
|
|
|
|
92
|
|
|
/** |
93
|
|
|
* @param int $at |
94
|
|
|
* |
95
|
|
|
* @return string|null |
96
|
|
|
*/ |
97
|
360 |
|
private function charAt($at) |
98
|
|
|
{ |
99
|
360 |
|
if ($at < 0 || $at >= $this->length) { |
100
|
276 |
|
return null; |
101
|
|
|
} |
102
|
|
|
|
103
|
357 |
|
return mb_substr($this->json, $at, 1, 'utf-8'); |
104
|
|
|
} |
105
|
|
|
|
106
|
|
|
/** |
107
|
|
|
* Parse the next character. |
108
|
|
|
* |
109
|
|
|
* If $c is given, the next char will only be parsed if the current |
110
|
|
|
* one matches $c. |
111
|
|
|
* |
112
|
|
|
* @param string|null $c |
113
|
|
|
* |
114
|
|
|
* @return null|string |
115
|
|
|
*/ |
116
|
330 |
|
private function next($c = null) |
117
|
|
|
{ |
118
|
|
|
// If a c parameter is provided, verify that it matches the current character. |
119
|
330 |
|
if ($c !== null && $c !== $this->ch) { |
120
|
9 |
|
$this->throwSyntaxError(sprintf( |
121
|
9 |
|
'Expected %s instead of %s', |
122
|
9 |
|
self::renderChar($c), |
123
|
9 |
|
self::renderChar($this->ch) |
124
|
6 |
|
)); |
125
|
|
|
} |
126
|
|
|
|
127
|
|
|
// Get the next character. When there are no more characters, |
128
|
|
|
// return the empty string. |
129
|
330 |
|
if ($this->ch === "\n" || ($this->ch === "\r" && $this->peek() !== "\n")) { |
130
|
264 |
|
$this->at++; |
131
|
264 |
|
$this->lineNumber++; |
132
|
264 |
|
$this->columnNumber = 1; |
133
|
176 |
|
} else { |
134
|
291 |
|
$this->at++; |
135
|
291 |
|
$this->columnNumber++; |
136
|
|
|
} |
137
|
|
|
|
138
|
330 |
|
$this->ch = $this->charAt($this->at); |
139
|
|
|
|
140
|
330 |
|
return $this->ch; |
141
|
|
|
} |
142
|
|
|
|
143
|
|
|
/** |
144
|
|
|
* Get the next character without consuming it or |
145
|
|
|
* assigning it to the ch variable. |
146
|
|
|
* |
147
|
|
|
* @return mixed |
148
|
|
|
*/ |
149
|
12 |
|
private function peek() |
150
|
|
|
{ |
151
|
12 |
|
return $this->charAt($this->at + 1); |
152
|
|
|
} |
153
|
|
|
|
154
|
|
|
/** |
155
|
|
|
* @return string |
156
|
|
|
*/ |
157
|
210 |
|
private function getLineRemainder() |
158
|
|
|
{ |
159
|
|
|
// Line are separated by "\n" or "\r" without an "\n" next |
160
|
210 |
|
if ($this->lineCache === null) { |
161
|
210 |
|
$this->lineCache = preg_split('/\n|\r\n?/u', $this->json); |
162
|
140 |
|
} |
163
|
|
|
|
164
|
210 |
|
$line = $this->lineCache[$this->lineNumber - 1]; |
165
|
|
|
|
166
|
210 |
|
return mb_substr($line, $this->columnNumber - 1, null, 'utf-8'); |
167
|
|
|
} |
168
|
|
|
|
169
|
|
|
/** |
170
|
|
|
* Attempt to match a regular expression at the current position on the current line. |
171
|
|
|
* |
172
|
|
|
* This function will not match across multiple lines. |
173
|
|
|
* |
174
|
|
|
* @param string $regex |
175
|
|
|
* |
176
|
|
|
* @return string|null |
177
|
|
|
*/ |
178
|
210 |
|
private function match($regex) |
179
|
|
|
{ |
180
|
210 |
|
$subject = $this->getLineRemainder(); |
181
|
|
|
|
182
|
210 |
|
$matches = array(); |
183
|
210 |
|
if (!preg_match($regex, $subject, $matches, PREG_OFFSET_CAPTURE)) { |
184
|
111 |
|
return null; |
185
|
|
|
} |
186
|
|
|
|
187
|
|
|
// PREG_OFFSET_CAPTURE always returns the byte offset, not the char offset, which is annoying |
188
|
198 |
|
$offset = mb_strlen(mb_strcut($subject, 0, $matches[0][1], 'utf-8'), 'utf-8'); |
189
|
|
|
|
190
|
|
|
// [0][0] contains the matched text |
|
|
|
|
191
|
|
|
// [0][1] contains the index of that match |
192
|
198 |
|
$advanceBy = $offset + mb_strlen($matches[0][0], 'utf-8'); |
193
|
|
|
|
194
|
198 |
|
$this->at += $advanceBy; |
195
|
198 |
|
$this->columnNumber += $advanceBy; |
196
|
198 |
|
$this->ch = $this->charAt($this->at); |
197
|
|
|
|
198
|
198 |
|
return $matches[0][0]; |
199
|
|
|
} |
200
|
|
|
|
201
|
|
|
/** |
202
|
|
|
* Parse an identifier. |
203
|
|
|
* |
204
|
|
|
* Normally, reserved words are disallowed here, but we |
205
|
|
|
* only use this for unquoted object keys, where reserved words are allowed, |
206
|
|
|
* so we don't check for those here. References: |
207
|
|
|
* - http://es5.github.com/#x7.6 |
208
|
|
|
* - https://developer.mozilla.org/en/Core_JavaScript_1.5_Guide/Core_Language_Features#Variables |
209
|
|
|
* - http://docstore.mik.ua/orelly/webprog/jscript/ch02_07.htm |
210
|
|
|
*/ |
211
|
39 |
|
private function identifier() |
212
|
|
|
{ |
213
|
|
|
// Be careful when editing this regex, there are a couple Unicode characters in between here -------------vv |
214
|
39 |
|
$match = $this->match('/^(?:[\$_\p{L}\p{Nl}]|\\\\u[0-9A-Fa-f]{4})(?:[\$_\p{L}\p{Nl}\p{Mn}\p{Mc}\p{Nd}\p{Pc}]|\\\\u[0-9A-Fa-f]{4})*/u'); |
215
|
|
|
|
216
|
39 |
|
if ($match === null) { |
217
|
9 |
|
$this->throwSyntaxError('Bad identifier as unquoted key'); |
218
|
|
|
} |
219
|
|
|
|
220
|
|
|
// Un-escape escaped Unicode chars |
221
|
30 |
|
$unescaped = preg_replace_callback('/\\\\u([0-9A-Fa-f]{4})/', function ($m) { |
222
|
3 |
|
return Json5Decoder::fromCharCode($m[1]); |
223
|
30 |
|
}, $match); |
224
|
|
|
|
225
|
30 |
|
return $unescaped; |
226
|
|
|
} |
227
|
|
|
|
228
|
210 |
|
private function number() |
229
|
|
|
{ |
230
|
210 |
|
$number = null; |
231
|
210 |
|
$sign = ''; |
232
|
210 |
|
$string = ''; |
233
|
210 |
|
$base = 10; |
234
|
|
|
|
235
|
210 |
|
if ($this->ch === '-' || $this->ch === '+') { |
236
|
93 |
|
$sign = $this->ch; |
237
|
93 |
|
$this->next($this->ch); |
238
|
62 |
|
} |
239
|
|
|
|
240
|
|
|
// support for Infinity |
241
|
210 |
|
if ($this->ch === 'I') { |
242
|
6 |
|
$number = $this->word(); |
243
|
6 |
|
if ($number === null) { |
244
|
|
|
$this->throwSyntaxError('Unexpected word for number'); |
245
|
|
|
} |
246
|
|
|
|
247
|
6 |
|
return ($sign === '-') ? -INF : INF; |
248
|
|
|
} |
249
|
|
|
|
250
|
|
|
// support for NaN |
251
|
204 |
|
if ($this->ch === 'N') { |
252
|
|
|
$number = $this->word(); |
253
|
|
|
if ($number !== NAN) { |
254
|
|
|
$this->throwSyntaxError('expected word to be NaN'); |
255
|
|
|
} |
256
|
|
|
|
257
|
|
|
// ignore sign as -NaN also is NaN |
258
|
|
|
return $number; |
259
|
|
|
} |
260
|
|
|
|
261
|
204 |
|
if ($this->ch === '0') { |
262
|
105 |
|
$string .= $this->ch; |
263
|
105 |
|
$this->next(); |
264
|
105 |
|
if ($this->ch === 'x' || $this->ch === 'X') { |
265
|
33 |
|
$string .= $this->ch; |
266
|
33 |
|
$this->next(); |
267
|
33 |
|
$base = 16; |
268
|
94 |
|
} elseif (is_numeric($this->ch)) { |
269
|
30 |
|
$this->throwSyntaxError('Octal literal'); |
270
|
|
|
} |
271
|
50 |
|
} |
272
|
|
|
|
273
|
|
|
switch ($base) { |
274
|
174 |
|
case 10: |
275
|
144 |
|
if (($match = $this->match('/^\d*\.?\d*/')) !== null) { |
276
|
144 |
|
$string .= $match; |
277
|
96 |
|
} |
278
|
144 |
|
if (($match = $this->match('/^[Ee][-+]?\d*/')) !== null) { |
279
|
45 |
|
$string .= $match; |
280
|
30 |
|
} |
281
|
144 |
|
$number = $string; |
282
|
144 |
|
break; |
283
|
33 |
|
case 16: |
284
|
33 |
|
if (($match = $this->match('/^[A-Fa-f0-9]+/')) !== null) { |
285
|
30 |
|
$string .= $match; |
286
|
30 |
|
$number = hexdec($string); |
287
|
30 |
|
break; |
288
|
|
|
} |
289
|
3 |
|
$this->throwSyntaxError('Bad hex number'); |
290
|
|
|
} |
291
|
|
|
|
292
|
171 |
|
if ($sign === '-') { |
293
|
33 |
|
$number = -$number; |
294
|
22 |
|
} |
295
|
|
|
|
296
|
171 |
|
if (!is_numeric($number) || !is_finite($number)) { |
297
|
3 |
|
$this->throwSyntaxError('Bad number'); |
298
|
|
|
} |
299
|
|
|
|
300
|
168 |
|
if ($this->castBigIntToString) { |
301
|
3 |
|
return $number; |
302
|
|
|
} |
303
|
|
|
|
304
|
|
|
// Adding 0 will automatically cast this to an int or float |
305
|
165 |
|
return $number + 0; |
306
|
|
|
} |
307
|
|
|
|
308
|
66 |
|
private function string() |
309
|
|
|
{ |
310
|
66 |
|
if (!($this->ch === '"' || $this->ch === "'")) { |
311
|
|
|
$this->throwSyntaxError('Bad string'); |
312
|
|
|
} |
313
|
|
|
|
314
|
66 |
|
$string = ''; |
315
|
|
|
|
316
|
66 |
|
$delim = $this->ch; |
317
|
66 |
|
while ($this->next() !== null) { |
318
|
66 |
|
if ($this->ch === $delim) { |
319
|
63 |
|
$this->next(); |
320
|
|
|
|
321
|
63 |
|
return $string; |
322
|
66 |
|
} elseif ($this->ch === '\\') { |
323
|
18 |
|
$this->next(); |
324
|
18 |
|
if ($this->ch === 'u') { |
325
|
|
|
$this->next(); |
326
|
|
|
$hex = $this->match('/^[A-Fa-f0-9]{4}/'); |
327
|
|
|
if ($hex === null) { |
328
|
|
|
break; |
329
|
|
|
} |
330
|
|
|
$string .= self::fromCharCode($hex); |
331
|
18 |
|
} elseif ($this->ch === "\r") { |
332
|
6 |
|
if ($this->peek() === "\n") { |
333
|
4 |
|
$this->next(); |
334
|
2 |
|
} |
335
|
16 |
|
} elseif (($escapee = self::getEscapee($this->ch)) !== null) { |
336
|
12 |
|
$string .= $escapee; |
337
|
8 |
|
} else { |
338
|
6 |
|
break; |
339
|
|
|
} |
340
|
66 |
|
} elseif ($this->ch === "\n") { |
341
|
|
|
// unescaped newlines are invalid; see: |
342
|
|
|
// https://github.com/json5/json5/issues/24 |
343
|
|
|
// @todo this feels special-cased; are there other invalid unescaped chars? |
344
|
3 |
|
break; |
345
|
|
|
} else { |
346
|
66 |
|
$string .= $this->ch; |
347
|
|
|
} |
348
|
44 |
|
} |
349
|
|
|
|
350
|
3 |
|
$this->throwSyntaxError('Bad string'); |
351
|
|
|
} |
352
|
|
|
|
353
|
|
|
/** |
354
|
|
|
* Skip an inline comment, assuming this is one. |
355
|
|
|
* |
356
|
|
|
* The current character should be the second / character in the // pair that begins this inline comment. |
357
|
|
|
* To finish the inline comment, we look for a newline or the end of the text. |
358
|
|
|
*/ |
359
|
36 |
|
private function inlineComment() |
360
|
|
|
{ |
361
|
36 |
|
if ($this->ch !== '/') { |
362
|
|
|
$this->throwSyntaxError('Not an inline comment'); |
363
|
|
|
} |
364
|
|
|
|
365
|
|
|
do { |
366
|
36 |
|
$this->next(); |
367
|
36 |
|
if ($this->ch === "\n" || $this->ch === "\r") { |
368
|
33 |
|
$this->next(); |
369
|
|
|
|
370
|
33 |
|
return; |
371
|
|
|
} |
372
|
36 |
|
} while ($this->ch !== null); |
373
|
3 |
|
} |
374
|
|
|
|
375
|
|
|
/** |
376
|
|
|
* Skip a block comment, assuming this is one. |
377
|
|
|
* |
378
|
|
|
* The current character should be the * character in the /* pair that begins this block comment. |
379
|
|
|
* To finish the block comment, we look for an ending */ pair of characters, |
380
|
|
|
* but we also watch for the end of text before the comment is terminated. |
381
|
|
|
*/ |
382
|
21 |
View Code Duplication |
private function blockComment() |
|
|
|
|
383
|
|
|
{ |
384
|
21 |
|
if ($this->ch !== '*') { |
385
|
|
|
$this->throwSyntaxError('Not a block comment'); |
386
|
|
|
} |
387
|
|
|
|
388
|
|
|
do { |
389
|
21 |
|
$this->next(); |
390
|
21 |
|
while ($this->ch === '*') { |
391
|
18 |
|
$this->next('*'); |
392
|
18 |
|
if ($this->ch === '/') { |
393
|
18 |
|
$this->next('/'); |
394
|
|
|
|
395
|
18 |
|
return; |
396
|
|
|
} |
397
|
2 |
|
} |
398
|
21 |
|
} while ($this->ch); |
|
|
|
|
399
|
|
|
|
400
|
3 |
|
$this->throwSyntaxError('Unterminated block comment'); |
401
|
|
|
} |
402
|
|
|
|
403
|
|
|
/** |
404
|
|
|
* Skip a comment, whether inline or block-level, assuming this is one. |
405
|
|
|
*/ |
406
|
54 |
View Code Duplication |
private function comment() |
|
|
|
|
407
|
|
|
{ |
408
|
|
|
// Comments always begin with a / character. |
409
|
54 |
|
if ($this->ch !== '/') { |
410
|
|
|
$this->throwSyntaxError('Not a comment'); |
411
|
|
|
} |
412
|
|
|
|
413
|
54 |
|
$this->next('/'); |
414
|
|
|
|
415
|
54 |
|
if ($this->ch === '/') { |
416
|
36 |
|
$this->inlineComment(); |
417
|
43 |
|
} elseif ($this->ch === '*') { |
418
|
21 |
|
$this->blockComment(); |
419
|
12 |
|
} else { |
420
|
|
|
$this->throwSyntaxError('Unrecognized comment'); |
421
|
|
|
} |
422
|
51 |
|
} |
423
|
|
|
|
424
|
|
|
/** |
425
|
|
|
* Skip whitespace and comments. |
426
|
|
|
* |
427
|
|
|
* Note that we're detecting comments by only a single / character. |
428
|
|
|
* This works since regular expressions are not valid JSON(5), but this will |
429
|
|
|
* break if there are other valid values that begin with a / character! |
430
|
|
|
*/ |
431
|
360 |
|
private function white() |
432
|
|
|
{ |
433
|
360 |
|
while ($this->ch) { |
|
|
|
|
434
|
342 |
|
if ($this->ch === '/') { |
435
|
54 |
|
$this->comment(); |
436
|
340 |
|
} elseif (preg_match(self::REGEX_WHITESPACE, $this->ch) === 1) { |
437
|
267 |
|
$this->next(); |
438
|
178 |
|
} else { |
439
|
306 |
|
return; |
440
|
|
|
} |
441
|
182 |
|
} |
442
|
288 |
|
} |
443
|
|
|
|
444
|
|
|
/** |
445
|
|
|
* Matches true, false, null, etc |
446
|
|
|
*/ |
447
|
78 |
|
private function word() |
448
|
|
|
{ |
449
|
78 |
|
switch ($this->ch) { |
450
|
78 |
|
case 't': |
451
|
36 |
|
$this->next('t'); |
452
|
36 |
|
$this->next('r'); |
453
|
36 |
|
$this->next('u'); |
454
|
36 |
|
$this->next('e'); |
455
|
36 |
|
return true; |
456
|
57 |
|
case 'f': |
457
|
18 |
|
$this->next('f'); |
458
|
18 |
|
$this->next('a'); |
459
|
18 |
|
$this->next('l'); |
460
|
18 |
|
$this->next('s'); |
461
|
18 |
|
$this->next('e'); |
462
|
18 |
|
return false; |
463
|
42 |
|
case 'n': |
464
|
18 |
|
$this->next('n'); |
465
|
18 |
|
$this->next('u'); |
466
|
18 |
|
$this->next('l'); |
467
|
18 |
|
$this->next('l'); |
468
|
18 |
|
return null; |
469
|
24 |
|
case 'I': |
470
|
12 |
|
$this->next('I'); |
471
|
12 |
|
$this->next('n'); |
472
|
12 |
|
$this->next('f'); |
473
|
12 |
|
$this->next('i'); |
474
|
12 |
|
$this->next('n'); |
475
|
12 |
|
$this->next('i'); |
476
|
12 |
|
$this->next('t'); |
477
|
12 |
|
$this->next('y'); |
478
|
12 |
|
return INF; |
479
|
12 |
|
case 'N': |
480
|
3 |
|
$this->next('N'); |
481
|
3 |
|
$this->next('a'); |
482
|
3 |
|
$this->next('N'); |
483
|
3 |
|
return NAN; |
484
|
6 |
|
} |
485
|
|
|
|
486
|
9 |
|
$this->throwSyntaxError('Unexpected ' . self::renderChar($this->ch)); |
487
|
|
|
} |
488
|
|
|
|
489
|
42 |
|
private function arr() |
490
|
|
|
{ |
491
|
42 |
|
$arr = array(); |
492
|
|
|
|
493
|
42 |
|
if ($this->ch === '[') { |
494
|
42 |
|
if (++$this->depth > $this->maxDepth) { |
495
|
3 |
|
$this->throwSyntaxError('Maximum stack depth exceeded'); |
496
|
|
|
} |
497
|
|
|
|
498
|
42 |
|
$this->next('['); |
499
|
42 |
|
$this->white(); |
500
|
42 |
|
while ($this->ch !== null) { |
501
|
42 |
|
if ($this->ch === ']') { |
502
|
12 |
|
$this->next(']'); |
503
|
12 |
|
$this->depth--; |
504
|
12 |
|
return $arr; // Potentially empty array |
505
|
|
|
} |
506
|
|
|
// ES5 allows omitting elements in arrays, e.g. [,] and |
507
|
|
|
// [,null]. We don't allow this in JSON5. |
508
|
39 |
|
if ($this->ch === ',') { |
509
|
6 |
|
$this->throwSyntaxError('Missing array element'); |
510
|
|
|
} else { |
511
|
33 |
|
$arr[] = $this->value(); |
512
|
|
|
} |
513
|
30 |
|
$this->white(); |
514
|
|
|
// If there's no comma after this value, this needs to |
515
|
|
|
// be the end of the array. |
516
|
30 |
|
if ($this->ch !== ',') { |
517
|
21 |
|
$this->next(']'); |
518
|
18 |
|
$this->depth--; |
519
|
18 |
|
return $arr; |
520
|
|
|
} |
521
|
15 |
|
$this->next(','); |
522
|
15 |
|
$this->white(); |
523
|
10 |
|
} |
524
|
|
|
} |
525
|
|
|
|
526
|
|
|
$this->throwSyntaxError('Bad array'); |
527
|
|
|
} |
528
|
|
|
|
529
|
|
|
/** |
530
|
|
|
* Parse an object value |
531
|
|
|
*/ |
532
|
75 |
|
private function obj() |
533
|
|
|
{ |
534
|
75 |
|
$key = null; |
|
|
|
|
535
|
75 |
|
$object = $this->associative ? array() : new \stdClass; |
536
|
|
|
|
537
|
75 |
|
if ($this->ch === '{') { |
538
|
75 |
|
if (++$this->depth > $this->maxDepth) { |
539
|
|
|
$this->throwSyntaxError('Maximum stack depth exceeded'); |
540
|
|
|
} |
541
|
|
|
|
542
|
75 |
|
$this->next('{'); |
543
|
75 |
|
$this->white(); |
544
|
75 |
|
while ($this->ch) { |
545
|
75 |
|
if ($this->ch === '}') { |
546
|
21 |
|
$this->next('}'); |
547
|
21 |
|
$this->depth--; |
548
|
21 |
|
return $object; // Potentially empty object |
549
|
|
|
} |
550
|
|
|
|
551
|
|
|
// Keys can be unquoted. If they are, they need to be |
552
|
|
|
// valid JS identifiers. |
553
|
63 |
|
if ($this->ch === '"' || $this->ch === "'") { |
554
|
27 |
|
$key = $this->string(); |
555
|
18 |
|
} else { |
556
|
39 |
|
$key = $this->identifier(); |
557
|
|
|
} |
558
|
|
|
|
559
|
54 |
|
$this->white(); |
560
|
54 |
|
$this->next(':'); |
561
|
51 |
|
if ($this->associative) { |
562
|
45 |
|
$object[$key] = $this->value(); |
563
|
30 |
|
} else { |
564
|
48 |
|
$object->{$key} = $this->value(); |
565
|
|
|
} |
566
|
51 |
|
$this->white(); |
567
|
|
|
// If there's no comma after this pair, this needs to be |
568
|
|
|
// the end of the object. |
569
|
51 |
|
if ($this->ch !== ',') { |
570
|
42 |
|
$this->next('}'); |
571
|
39 |
|
$this->depth--; |
572
|
39 |
|
return $object; |
573
|
|
|
} |
574
|
18 |
|
$this->next(','); |
575
|
18 |
|
$this->white(); |
576
|
12 |
|
} |
577
|
|
|
} |
578
|
|
|
|
579
|
|
|
$this->throwSyntaxError('Bad object'); |
580
|
|
|
} |
581
|
|
|
|
582
|
|
|
/** |
583
|
|
|
* Parse a JSON value. |
584
|
|
|
* |
585
|
|
|
* It could be an object, an array, a string, a number, |
586
|
|
|
* or a word. |
587
|
|
|
*/ |
588
|
360 |
|
private function value() |
589
|
|
|
{ |
590
|
360 |
|
$this->white(); |
591
|
360 |
|
switch ($this->ch) { |
592
|
360 |
|
case '{': |
593
|
75 |
|
return $this->obj(); |
594
|
336 |
|
case '[': |
595
|
42 |
|
return $this->arr(); |
596
|
324 |
|
case '"': |
597
|
315 |
|
case "'": |
598
|
54 |
|
return $this->string(); |
599
|
279 |
|
case '-': |
600
|
264 |
|
case '+': |
601
|
249 |
|
case '.': |
602
|
102 |
|
return $this->number(); |
603
|
120 |
|
default: |
604
|
180 |
|
return is_numeric($this->ch) ? $this->number() : $this->word(); |
605
|
120 |
|
} |
606
|
|
|
} |
607
|
|
|
|
608
|
96 |
|
private function throwSyntaxError($message) |
609
|
|
|
{ |
610
|
96 |
|
throw new SyntaxError($message, $this->at, $this->lineNumber, $this->columnNumber); |
611
|
|
|
} |
612
|
|
|
|
613
|
18 |
|
private static function renderChar($chr) |
614
|
|
|
{ |
615
|
18 |
|
return $chr === null ? 'EOF' : "'" . $chr . "'"; |
616
|
|
|
} |
617
|
|
|
|
618
|
|
|
/** |
619
|
|
|
* @param string $hex Hex code |
620
|
|
|
* |
621
|
|
|
* @return string Unicode character |
622
|
|
|
*/ |
623
|
3 |
|
private static function fromCharCode($hex) |
624
|
|
|
{ |
625
|
3 |
|
return mb_convert_encoding('&#' . hexdec($hex) . ';', 'UTF-8', 'HTML-ENTITIES'); |
626
|
|
|
} |
627
|
|
|
|
628
|
|
|
/** |
629
|
|
|
* @param string $ch |
630
|
|
|
* |
631
|
|
|
* @return string|null |
632
|
|
|
*/ |
633
|
12 |
|
private static function getEscapee($ch) |
634
|
|
|
{ |
635
|
|
|
switch ($ch) { |
636
|
|
|
// @codingStandardsIgnoreStart |
637
|
12 |
|
case "'": return "'"; |
638
|
9 |
|
case '"': return '"'; |
639
|
9 |
|
case '\\': return '\\'; |
640
|
9 |
|
case '/': return '/'; |
641
|
9 |
|
case "\n": return ''; |
642
|
|
|
case 'b': return '\b'; |
643
|
|
|
case 'f': return '\f'; |
644
|
|
|
case 'n': return '\n'; |
645
|
|
|
case 'r': return '\r'; |
646
|
|
|
case 't': return '\t'; |
647
|
|
|
default: return null; |
648
|
|
|
// @codingStandardsIgnoreEnd |
649
|
|
|
} |
650
|
|
|
} |
651
|
|
|
} |
652
|
|
|
|
Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.
The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.
This check looks for comments that seem to be mostly valid code and reports them.