Completed
Branch master (939199)
by
unknown
39:35
created

includes/libs/jsminplus.php (7 issues)

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

1
<?php
2
// @codingStandardsIgnoreFile File external to MediaWiki. Ignore coding conventions checks.
3
/**
4
 * JSMinPlus version 1.4
5
 *
6
 * Minifies a javascript file using a javascript parser
7
 *
8
 * This implements a PHP port of Brendan Eich's Narcissus open source javascript engine (in javascript)
9
 * References: http://en.wikipedia.org/wiki/Narcissus_(JavaScript_engine)
10
 * Narcissus sourcecode: http://mxr.mozilla.org/mozilla/source/js/narcissus/
11
 * JSMinPlus weblog: http://crisp.tweakblogs.net/blog/cat/716
12
 *
13
 * Tino Zijdel <[email protected]>
14
 *
15
 * Usage: $minified = JSMinPlus::minify($script [, $filename])
16
 *
17
 * Versionlog (see also changelog.txt):
18
 * 23-07-2011 - remove dynamic creation of OP_* and KEYWORD_* defines and declare them on top
19
 *              reduce memory footprint by minifying by block-scope
20
 *              some small byte-saving and performance improvements
21
 * 12-05-2009 - fixed hook:colon precedence, fixed empty body in loop and if-constructs
22
 * 18-04-2009 - fixed crashbug in PHP 5.2.9 and several other bugfixes
23
 * 12-04-2009 - some small bugfixes and performance improvements
24
 * 09-04-2009 - initial open sourced version 1.0
25
 *
26
 * Latest version of this script: http://files.tweakers.net/jsminplus/jsminplus.zip
27
 *
28
 * @file
29
 */
30
31
/* ***** BEGIN LICENSE BLOCK *****
32
 * Version: MPL 1.1/GPL 2.0/LGPL 2.1
33
 *
34
 * The contents of this file are subject to the Mozilla Public License Version
35
 * 1.1 (the "License"); you may not use this file except in compliance with
36
 * the License. You may obtain a copy of the License at
37
 * http://www.mozilla.org/MPL/
38
 *
39
 * Software distributed under the License is distributed on an "AS IS" basis,
40
 * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
41
 * for the specific language governing rights and limitations under the
42
 * License.
43
 *
44
 * The Original Code is the Narcissus JavaScript engine.
45
 *
46
 * The Initial Developer of the Original Code is
47
 * Brendan Eich <[email protected]>.
48
 * Portions created by the Initial Developer are Copyright (C) 2004
49
 * the Initial Developer. All Rights Reserved.
50
 *
51
 * Contributor(s): Tino Zijdel <[email protected]>
52
 * PHP port, modifications and minifier routine are (C) 2009-2011
53
 *
54
 * Alternatively, the contents of this file may be used under the terms of
55
 * either the GNU General Public License Version 2 or later (the "GPL"), or
56
 * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
57
 * in which case the provisions of the GPL or the LGPL are applicable instead
58
 * of those above. If you wish to allow use of your version of this file only
59
 * under the terms of either the GPL or the LGPL, and not to allow others to
60
 * use your version of this file under the terms of the MPL, indicate your
61
 * decision by deleting the provisions above and replace them with the notice
62
 * and other provisions required by the GPL or the LGPL. If you do not delete
63
 * the provisions above, a recipient may use your version of this file under
64
 * the terms of any one of the MPL, the GPL or the LGPL.
65
 *
66
 * ***** END LICENSE BLOCK ***** */
67
68
define('TOKEN_END', 1);
69
define('TOKEN_NUMBER', 2);
70
define('TOKEN_IDENTIFIER', 3);
71
define('TOKEN_STRING', 4);
72
define('TOKEN_REGEXP', 5);
73
define('TOKEN_NEWLINE', 6);
74
define('TOKEN_CONDCOMMENT_START', 7);
75
define('TOKEN_CONDCOMMENT_END', 8);
76
77
define('JS_SCRIPT', 100);
78
define('JS_BLOCK', 101);
79
define('JS_LABEL', 102);
80
define('JS_FOR_IN', 103);
81
define('JS_CALL', 104);
82
define('JS_NEW_WITH_ARGS', 105);
83
define('JS_INDEX', 106);
84
define('JS_ARRAY_INIT', 107);
85
define('JS_OBJECT_INIT', 108);
86
define('JS_PROPERTY_INIT', 109);
87
define('JS_GETTER', 110);
88
define('JS_SETTER', 111);
89
define('JS_GROUP', 112);
90
define('JS_LIST', 113);
91
92
define('JS_MINIFIED', 999);
93
94
define('DECLARED_FORM', 0);
95
define('EXPRESSED_FORM', 1);
96
define('STATEMENT_FORM', 2);
97
98
/* Operators */
99
define('OP_SEMICOLON', ';');
100
define('OP_COMMA', ',');
101
define('OP_HOOK', '?');
102
define('OP_COLON', ':');
103
define('OP_OR', '||');
104
define('OP_AND', '&&');
105
define('OP_BITWISE_OR', '|');
106
define('OP_BITWISE_XOR', '^');
107
define('OP_BITWISE_AND', '&');
108
define('OP_STRICT_EQ', '===');
109
define('OP_EQ', '==');
110
define('OP_ASSIGN', '=');
111
define('OP_STRICT_NE', '!==');
112
define('OP_NE', '!=');
113
define('OP_LSH', '<<');
114
define('OP_LE', '<=');
115
define('OP_LT', '<');
116
define('OP_URSH', '>>>');
117
define('OP_RSH', '>>');
118
define('OP_GE', '>=');
119
define('OP_GT', '>');
120
define('OP_INCREMENT', '++');
121
define('OP_DECREMENT', '--');
122
define('OP_PLUS', '+');
123
define('OP_MINUS', '-');
124
define('OP_MUL', '*');
125
define('OP_DIV', '/');
126
define('OP_MOD', '%');
127
define('OP_NOT', '!');
128
define('OP_BITWISE_NOT', '~');
129
define('OP_DOT', '.');
130
define('OP_LEFT_BRACKET', '[');
131
define('OP_RIGHT_BRACKET', ']');
132
define('OP_LEFT_CURLY', '{');
133
define('OP_RIGHT_CURLY', '}');
134
define('OP_LEFT_PAREN', '(');
135
define('OP_RIGHT_PAREN', ')');
136
define('OP_CONDCOMMENT_END', '@*/');
137
138
define('OP_UNARY_PLUS', 'U+');
139
define('OP_UNARY_MINUS', 'U-');
140
141
/* Keywords */
142
define('KEYWORD_BREAK', 'break');
143
define('KEYWORD_CASE', 'case');
144
define('KEYWORD_CATCH', 'catch');
145
define('KEYWORD_CONST', 'const');
146
define('KEYWORD_CONTINUE', 'continue');
147
define('KEYWORD_DEBUGGER', 'debugger');
148
define('KEYWORD_DEFAULT', 'default');
149
define('KEYWORD_DELETE', 'delete');
150
define('KEYWORD_DO', 'do');
151
define('KEYWORD_ELSE', 'else');
152
define('KEYWORD_ENUM', 'enum');
153
define('KEYWORD_FALSE', 'false');
154
define('KEYWORD_FINALLY', 'finally');
155
define('KEYWORD_FOR', 'for');
156
define('KEYWORD_FUNCTION', 'function');
157
define('KEYWORD_IF', 'if');
158
define('KEYWORD_IN', 'in');
159
define('KEYWORD_INSTANCEOF', 'instanceof');
160
define('KEYWORD_NEW', 'new');
161
define('KEYWORD_NULL', 'null');
162
define('KEYWORD_RETURN', 'return');
163
define('KEYWORD_SWITCH', 'switch');
164
define('KEYWORD_THIS', 'this');
165
define('KEYWORD_THROW', 'throw');
166
define('KEYWORD_TRUE', 'true');
167
define('KEYWORD_TRY', 'try');
168
define('KEYWORD_TYPEOF', 'typeof');
169
define('KEYWORD_VAR', 'var');
170
define('KEYWORD_VOID', 'void');
171
define('KEYWORD_WHILE', 'while');
172
define('KEYWORD_WITH', 'with');
173
174
175
class JSMinPlus
176
{
177
	private $parser;
178
	private $reserved = array(
179
		'break', 'case', 'catch', 'continue', 'default', 'delete', 'do',
180
		'else', 'finally', 'for', 'function', 'if', 'in', 'instanceof',
181
		'new', 'return', 'switch', 'this', 'throw', 'try', 'typeof', 'var',
182
		'void', 'while', 'with',
183
		// Words reserved for future use
184
		'abstract', 'boolean', 'byte', 'char', 'class', 'const', 'debugger',
185
		'double', 'enum', 'export', 'extends', 'final', 'float', 'goto',
186
		'implements', 'import', 'int', 'interface', 'long', 'native',
187
		'package', 'private', 'protected', 'public', 'short', 'static',
188
		'super', 'synchronized', 'throws', 'transient', 'volatile',
189
		// These are not reserved, but should be taken into account
190
		// in isValidIdentifier (See jslint source code)
191
		'arguments', 'eval', 'true', 'false', 'Infinity', 'NaN', 'null', 'undefined'
192
	);
193
194
	private function __construct()
195
	{
196
		$this->parser = new JSParser($this);
197
	}
198
199
	public static function minify($js, $filename='')
200
	{
201
		static $instance;
202
203
		// this is a singleton
204
		if(!$instance)
205
			$instance = new JSMinPlus();
206
207
		return $instance->min($js, $filename);
208
	}
209
210
	private function min($js, $filename)
211
	{
212
		try
213
		{
214
			$n = $this->parser->parse($js, $filename, 1);
215
			return $this->parseTree($n);
216
		}
217
		catch(Exception $e)
218
		{
219
			echo $e->getMessage() . "\n";
220
		}
221
222
		return false;
223
	}
224
225
	public function parseTree($n, $noBlockGrouping = false)
226
	{
227
		$s = '';
228
229
		switch ($n->type)
230
		{
231
			case JS_MINIFIED:
232
				$s = $n->value;
233
			break;
234
235
			case JS_SCRIPT:
236
				// we do nothing yet with funDecls or varDecls
237
				$noBlockGrouping = true;
238
			// FALL THROUGH
239
240
			case JS_BLOCK:
241
				$childs = $n->treeNodes;
242
				$lastType = 0;
243
				for ($c = 0, $i = 0, $j = count($childs); $i < $j; $i++)
244
				{
245
					$type = $childs[$i]->type;
246
					$t = $this->parseTree($childs[$i]);
247
					if (strlen($t))
248
					{
249
						if ($c)
250
						{
251
							$s = rtrim($s, ';');
252
253
							if ($type == KEYWORD_FUNCTION && $childs[$i]->functionForm == DECLARED_FORM)
254
							{
255
								// put declared functions on a new line
256
								$s .= "\n";
257
							}
258
							elseif ($type == KEYWORD_VAR && $type == $lastType)
259
							{
260
								// multiple var-statements can go into one
261
								$t = ',' . substr($t, 4);
262
							}
263
							else
264
							{
265
								// add terminator
266
								$s .= ';';
267
							}
268
						}
269
270
						$s .= $t;
271
272
						$c++;
273
						$lastType = $type;
274
					}
275
				}
276
277
				if ($c > 1 && !$noBlockGrouping)
278
				{
279
					$s = '{' . $s . '}';
280
				}
281
			break;
282
283
			case KEYWORD_FUNCTION:
284
				$s .= 'function' . ($n->name ? ' ' . $n->name : '') . '(';
285
				$params = $n->params;
286
				for ($i = 0, $j = count($params); $i < $j; $i++)
287
					$s .= ($i ? ',' : '') . $params[$i];
288
				$s .= '){' . $this->parseTree($n->body, true) . '}';
289
			break;
290
291
			case KEYWORD_IF:
292
				$s = 'if(' . $this->parseTree($n->condition) . ')';
293
				$thenPart = $this->parseTree($n->thenPart);
294
				$elsePart = $n->elsePart ? $this->parseTree($n->elsePart) : null;
295
296
				// empty if-statement
297
				if ($thenPart == '')
298
					$thenPart = ';';
299
300
				if ($elsePart)
301
				{
302
					// be careful and always make a block out of the thenPart; could be more optimized but is a lot of trouble
303
					if ($thenPart != ';' && $thenPart[0] != '{')
304
						$thenPart = '{' . $thenPart . '}';
305
306
					$s .= $thenPart . 'else';
307
308
					// we could check for more, but that hardly ever applies so go for performance
309
					if ($elsePart[0] != '{')
310
						$s .= ' ';
311
312
					$s .= $elsePart;
313
				}
314
				else
315
				{
316
					$s .= $thenPart;
317
				}
318
			break;
319
320
			case KEYWORD_SWITCH:
321
				$s = 'switch(' . $this->parseTree($n->discriminant) . '){';
322
				$cases = $n->cases;
323
				for ($i = 0, $j = count($cases); $i < $j; $i++)
324
				{
325
					$case = $cases[$i];
326
					if ($case->type == KEYWORD_CASE)
327
						$s .= 'case' . ($case->caseLabel->type != TOKEN_STRING ? ' ' : '') . $this->parseTree($case->caseLabel) . ':';
328
					else
329
						$s .= 'default:';
330
331
					$statement = $this->parseTree($case->statements, true);
332
					if ($statement)
333
					{
334
						$s .= $statement;
335
						// no terminator for last statement
336
						if ($i + 1 < $j)
337
							$s .= ';';
338
					}
339
				}
340
				$s .= '}';
341
			break;
342
343
			case KEYWORD_FOR:
344
				$s = 'for(' . ($n->setup ? $this->parseTree($n->setup) : '')
345
					. ';' . ($n->condition ? $this->parseTree($n->condition) : '')
346
					. ';' . ($n->update ? $this->parseTree($n->update) : '') . ')';
347
348
				$body  = $this->parseTree($n->body);
349
				if ($body == '')
350
					$body = ';';
351
352
				$s .= $body;
353
			break;
354
355
			case KEYWORD_WHILE:
356
				$s = 'while(' . $this->parseTree($n->condition) . ')';
357
358
				$body  = $this->parseTree($n->body);
359
				if ($body == '')
360
					$body = ';';
361
362
				$s .= $body;
363
			break;
364
365
			case JS_FOR_IN:
366
				$s = 'for(' . ($n->varDecl ? $this->parseTree($n->varDecl) : $this->parseTree($n->iterator)) . ' in ' . $this->parseTree($n->object) . ')';
367
368
				$body  = $this->parseTree($n->body);
369
				if ($body == '')
370
					$body = ';';
371
372
				$s .= $body;
373
			break;
374
375
			case KEYWORD_DO:
376
				$s = 'do{' . $this->parseTree($n->body, true) . '}while(' . $this->parseTree($n->condition) . ')';
377
			break;
378
379
			case KEYWORD_BREAK:
380
			case KEYWORD_CONTINUE:
381
				$s = $n->value . ($n->label ? ' ' . $n->label : '');
382
			break;
383
384
			case KEYWORD_TRY:
385
				$s = 'try{' . $this->parseTree($n->tryBlock, true) . '}';
386
				$catchClauses = $n->catchClauses;
387
				for ($i = 0, $j = count($catchClauses); $i < $j; $i++)
388
				{
389
					$t = $catchClauses[$i];
390
					$s .= 'catch(' . $t->varName . ($t->guard ? ' if ' . $this->parseTree($t->guard) : '') . '){' . $this->parseTree($t->block, true) . '}';
391
				}
392
				if ($n->finallyBlock)
393
					$s .= 'finally{' . $this->parseTree($n->finallyBlock, true) . '}';
394
			break;
395
396
			case KEYWORD_THROW:
397
			case KEYWORD_RETURN:
398
				$s = $n->type;
399
				if ($n->value)
400
				{
401
					$t = $this->parseTree($n->value);
402
					if (strlen($t))
403
					{
404
						if ($this->isWordChar($t[0]) || $t[0] == '\\')
405
							$s .= ' ';
406
407
						$s .= $t;
408
					}
409
				}
410
			break;
411
412
			case KEYWORD_WITH:
413
				$s = 'with(' . $this->parseTree($n->object) . ')' . $this->parseTree($n->body);
414
			break;
415
416
			case KEYWORD_VAR:
417
			case KEYWORD_CONST:
418
				$s = $n->value . ' ';
419
				$childs = $n->treeNodes;
420
				for ($i = 0, $j = count($childs); $i < $j; $i++)
421
				{
422
					$t = $childs[$i];
423
					$s .= ($i ? ',' : '') . $t->name;
424
					$u = $t->initializer;
425
					if ($u)
426
						$s .= '=' . $this->parseTree($u);
427
				}
428
			break;
429
430
			case KEYWORD_IN:
431
			case KEYWORD_INSTANCEOF:
432
				$left = $this->parseTree($n->treeNodes[0]);
433
				$right = $this->parseTree($n->treeNodes[1]);
434
435
				$s = $left;
436
437
				if ($this->isWordChar(substr($left, -1)))
438
					$s .= ' ';
439
440
				$s .= $n->type;
441
442
				if ($this->isWordChar($right[0]) || $right[0] == '\\')
443
					$s .= ' ';
444
445
				$s .= $right;
446
			break;
447
448
			case KEYWORD_DELETE:
449
			case KEYWORD_TYPEOF:
450
				$right = $this->parseTree($n->treeNodes[0]);
451
452
				$s = $n->type;
453
454
				if ($this->isWordChar($right[0]) || $right[0] == '\\')
455
					$s .= ' ';
456
457
				$s .= $right;
458
			break;
459
460
			case KEYWORD_VOID:
461
				$s = 'void(' . $this->parseTree($n->treeNodes[0]) . ')';
462
			break;
463
464
			case KEYWORD_DEBUGGER:
465
				throw new Exception('NOT IMPLEMENTED: DEBUGGER');
466
			break;
467
468
			case TOKEN_CONDCOMMENT_START:
469
			case TOKEN_CONDCOMMENT_END:
470
				$s = $n->value . ($n->type == TOKEN_CONDCOMMENT_START ? ' ' : '');
471
				$childs = $n->treeNodes;
472
				for ($i = 0, $j = count($childs); $i < $j; $i++)
473
					$s .= $this->parseTree($childs[$i]);
474
			break;
475
476
			case OP_SEMICOLON:
477
				if ($expression = $n->expression)
478
					$s = $this->parseTree($expression);
479
			break;
480
481
			case JS_LABEL:
482
				$s = $n->label . ':' . $this->parseTree($n->statement);
483
			break;
484
485 View Code Duplication
			case OP_COMMA:
486
				$childs = $n->treeNodes;
487
				for ($i = 0, $j = count($childs); $i < $j; $i++)
488
					$s .= ($i ? ',' : '') . $this->parseTree($childs[$i]);
489
			break;
490
491
			case OP_ASSIGN:
492
				$s = $this->parseTree($n->treeNodes[0]) . $n->value . $this->parseTree($n->treeNodes[1]);
493
			break;
494
495 View Code Duplication
			case OP_HOOK:
496
				$s = $this->parseTree($n->treeNodes[0]) . '?' . $this->parseTree($n->treeNodes[1]) . ':' . $this->parseTree($n->treeNodes[2]);
497
			break;
498
499
			case OP_OR: case OP_AND:
500
			case OP_BITWISE_OR: case OP_BITWISE_XOR: case OP_BITWISE_AND:
501
			case OP_EQ: case OP_NE: case OP_STRICT_EQ: case OP_STRICT_NE:
502
			case OP_LT: case OP_LE: case OP_GE: case OP_GT:
503
			case OP_LSH: case OP_RSH: case OP_URSH:
504
			case OP_MUL: case OP_DIV: case OP_MOD:
505
				$s = $this->parseTree($n->treeNodes[0]) . $n->type . $this->parseTree($n->treeNodes[1]);
506
			break;
507
508
			case OP_PLUS:
509
			case OP_MINUS:
510
				$left = $this->parseTree($n->treeNodes[0]);
511
				$right = $this->parseTree($n->treeNodes[1]);
512
513
				switch ($n->treeNodes[1]->type)
514
				{
515
					case OP_PLUS:
516
					case OP_MINUS:
517
					case OP_INCREMENT:
518
					case OP_DECREMENT:
519
					case OP_UNARY_PLUS:
520
					case OP_UNARY_MINUS:
521
						$s = $left . $n->type . ' ' . $right;
522
					break;
523
524
					case TOKEN_STRING:
525
						//combine concatenated strings with same quote style
526
						if ($n->type == OP_PLUS && substr($left, -1) == $right[0])
527
						{
528
							$s = substr($left, 0, -1) . substr($right, 1);
529
							break;
530
						}
531
					// FALL THROUGH
532
533
					default:
534
						$s = $left . $n->type . $right;
535
				}
536
			break;
537
538
			case OP_NOT:
539
			case OP_BITWISE_NOT:
540
			case OP_UNARY_PLUS:
541
			case OP_UNARY_MINUS:
542
				$s = $n->value . $this->parseTree($n->treeNodes[0]);
543
			break;
544
545
			case OP_INCREMENT:
546
			case OP_DECREMENT:
547
				if ($n->postfix)
548
					$s = $this->parseTree($n->treeNodes[0]) . $n->value;
549
				else
550
					$s = $n->value . $this->parseTree($n->treeNodes[0]);
551
			break;
552
553
			case OP_DOT:
554
				$s = $this->parseTree($n->treeNodes[0]) . '.' . $this->parseTree($n->treeNodes[1]);
555
			break;
556
557
			case JS_INDEX:
558
				$s = $this->parseTree($n->treeNodes[0]);
559
				// See if we can replace named index with a dot saving 3 bytes
560
				if (	$n->treeNodes[0]->type == TOKEN_IDENTIFIER &&
561
					$n->treeNodes[1]->type == TOKEN_STRING &&
562
					$this->isValidIdentifier(substr($n->treeNodes[1]->value, 1, -1))
563
				)
564
					$s .= '.' . substr($n->treeNodes[1]->value, 1, -1);
565
				else
566
					$s .= '[' . $this->parseTree($n->treeNodes[1]) . ']';
567
			break;
568
569 View Code Duplication
			case JS_LIST:
570
				$childs = $n->treeNodes;
571
				for ($i = 0, $j = count($childs); $i < $j; $i++)
572
					$s .= ($i ? ',' : '') . $this->parseTree($childs[$i]);
573
			break;
574
575 View Code Duplication
			case JS_CALL:
576
				$s = $this->parseTree($n->treeNodes[0]) . '(' . $this->parseTree($n->treeNodes[1]) . ')';
577
			break;
578
579
			case KEYWORD_NEW:
580 View Code Duplication
			case JS_NEW_WITH_ARGS:
581
				$s = 'new ' . $this->parseTree($n->treeNodes[0]) . '(' . ($n->type == JS_NEW_WITH_ARGS ? $this->parseTree($n->treeNodes[1]) : '') . ')';
582
			break;
583
584
			case JS_ARRAY_INIT:
585
				$s = '[';
586
				$childs = $n->treeNodes;
587
				for ($i = 0, $j = count($childs); $i < $j; $i++)
588
				{
589
					$s .= ($i ? ',' : '') . $this->parseTree($childs[$i]);
590
				}
591
				$s .= ']';
592
			break;
593
594
			case JS_OBJECT_INIT:
595
				$s = '{';
596
				$childs = $n->treeNodes;
597
				for ($i = 0, $j = count($childs); $i < $j; $i++)
598
				{
599
					$t = $childs[$i];
600
					if ($i)
601
						$s .= ',';
602
					if ($t->type == JS_PROPERTY_INIT)
603
					{
604
						// Ditch the quotes when the index is a valid identifier
605
						if (	$t->treeNodes[0]->type == TOKEN_STRING &&
606
							$this->isValidIdentifier(substr($t->treeNodes[0]->value, 1, -1))
607
						)
608
							$s .= substr($t->treeNodes[0]->value, 1, -1);
609
						else
610
							$s .= $t->treeNodes[0]->value;
611
612
						$s .= ':' . $this->parseTree($t->treeNodes[1]);
613
					}
614
					else
615
					{
616
						$s .= $t->type == JS_GETTER ? 'get' : 'set';
617
						$s .= ' ' . $t->name . '(';
618
						$params = $t->params;
619
						for ($i = 0, $j = count($params); $i < $j; $i++)
620
							$s .= ($i ? ',' : '') . $params[$i];
621
						$s .= '){' . $this->parseTree($t->body, true) . '}';
622
					}
623
				}
624
				$s .= '}';
625
			break;
626
627
			case TOKEN_NUMBER:
628
				$s = $n->value;
629
				if (preg_match('/^([1-9]+)(0{3,})$/', $s, $m))
630
					$s = $m[1] . 'e' . strlen($m[2]);
631
			break;
632
633
			case KEYWORD_NULL: case KEYWORD_THIS: case KEYWORD_TRUE: case KEYWORD_FALSE:
634
			case TOKEN_IDENTIFIER: case TOKEN_STRING: case TOKEN_REGEXP:
635
				$s = $n->value;
636
			break;
637
638
			case JS_GROUP:
639
				if (in_array(
640
					$n->treeNodes[0]->type,
641
					array(
642
						JS_ARRAY_INIT, JS_OBJECT_INIT, JS_GROUP,
643
						TOKEN_NUMBER, TOKEN_STRING, TOKEN_REGEXP, TOKEN_IDENTIFIER,
644
						KEYWORD_NULL, KEYWORD_THIS, KEYWORD_TRUE, KEYWORD_FALSE
645
					)
646
				))
647
				{
648
					$s = $this->parseTree($n->treeNodes[0]);
649
				}
650
				else
651
				{
652
					$s = '(' . $this->parseTree($n->treeNodes[0]) . ')';
653
				}
654
			break;
655
656
			default:
657
				throw new Exception('UNKNOWN TOKEN TYPE: ' . $n->type);
658
		}
659
660
		return $s;
661
	}
662
663
	private function isValidIdentifier($string)
664
	{
665
		return preg_match('/^[a-zA-Z_][a-zA-Z0-9_]*$/', $string) && !in_array($string, $this->reserved);
666
	}
667
668
	private function isWordChar($char)
669
	{
670
		return $char == '_' || $char == '$' || ctype_alnum($char);
671
	}
672
}
673
674
class JSParser
675
{
676
	private $t;
677
	private $minifier;
678
679
	private $opPrecedence = array(
680
		';' => 0,
681
		',' => 1,
682
		'=' => 2, '?' => 2, ':' => 2,
683
		// The above all have to have the same precedence, see bug 330975
684
		'||' => 4,
685
		'&&' => 5,
686
		'|' => 6,
687
		'^' => 7,
688
		'&' => 8,
689
		'==' => 9, '!=' => 9, '===' => 9, '!==' => 9,
690
		'<' => 10, '<=' => 10, '>=' => 10, '>' => 10, 'in' => 10, 'instanceof' => 10,
691
		'<<' => 11, '>>' => 11, '>>>' => 11,
692
		'+' => 12, '-' => 12,
693
		'*' => 13, '/' => 13, '%' => 13,
694
		'delete' => 14, 'void' => 14, 'typeof' => 14,
695
		'!' => 14, '~' => 14, 'U+' => 14, 'U-' => 14,
696
		'++' => 15, '--' => 15,
697
		'new' => 16,
698
		'.' => 17,
699
		JS_NEW_WITH_ARGS => 0, JS_INDEX => 0, JS_CALL => 0,
700
		JS_ARRAY_INIT => 0, JS_OBJECT_INIT => 0, JS_GROUP => 0
701
	);
702
703
	private $opArity = array(
704
		',' => -2,
705
		'=' => 2,
706
		'?' => 3,
707
		'||' => 2,
708
		'&&' => 2,
709
		'|' => 2,
710
		'^' => 2,
711
		'&' => 2,
712
		'==' => 2, '!=' => 2, '===' => 2, '!==' => 2,
713
		'<' => 2, '<=' => 2, '>=' => 2, '>' => 2, 'in' => 2, 'instanceof' => 2,
714
		'<<' => 2, '>>' => 2, '>>>' => 2,
715
		'+' => 2, '-' => 2,
716
		'*' => 2, '/' => 2, '%' => 2,
717
		'delete' => 1, 'void' => 1, 'typeof' => 1,
718
		'!' => 1, '~' => 1, 'U+' => 1, 'U-' => 1,
719
		'++' => 1, '--' => 1,
720
		'new' => 1,
721
		'.' => 2,
722
		JS_NEW_WITH_ARGS => 2, JS_INDEX => 2, JS_CALL => 2,
723
		JS_ARRAY_INIT => 1, JS_OBJECT_INIT => 1, JS_GROUP => 1,
724
		TOKEN_CONDCOMMENT_START => 1, TOKEN_CONDCOMMENT_END => 1
725
	);
726
727
	public function __construct($minifier=null)
728
	{
729
		$this->minifier = $minifier;
730
		$this->t = new JSTokenizer();
731
	}
732
733
	public function parse($s, $f, $l)
734
	{
735
		// initialize tokenizer
736
		$this->t->init($s, $f, $l);
737
738
		$x = new JSCompilerContext(false);
739
		$n = $this->Script($x);
740
		if (!$this->t->isDone())
741
			throw $this->t->newSyntaxError('Syntax error');
742
743
		return $n;
744
	}
745
746
	private function Script($x)
747
	{
748
		$n = $this->Statements($x);
749
		$n->type = JS_SCRIPT;
750
		$n->funDecls = $x->funDecls;
751
		$n->varDecls = $x->varDecls;
752
753
		// minify by scope
754
		if ($this->minifier)
755
		{
756
			$n->value = $this->minifier->parseTree($n);
0 ignored issues
show
The property $value is declared private in JSNode. Since you implemented __set(), maybe consider adding a @property or @property-write annotation. This makes it easier for IDEs to provide auto-completion.

Since your code implements the magic setter _set, this function will be called for any write access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

Since the property has write access only, you can use the @property-write annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
757
758
			// clear tree from node to save memory
759
			$n->treeNodes = null;
0 ignored issues
show
Documentation Bug introduced by
It seems like null of type null is incompatible with the declared type array of property $treeNodes.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
760
			$n->funDecls = null;
0 ignored issues
show
Documentation Bug introduced by
It seems like null of type null is incompatible with the declared type array of property $funDecls.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
761
			$n->varDecls = null;
0 ignored issues
show
Documentation Bug introduced by
It seems like null of type null is incompatible with the declared type array of property $varDecls.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
762
763
			$n->type = JS_MINIFIED;
0 ignored issues
show
The property $type is declared private in JSNode. Since you implemented __set(), maybe consider adding a @property or @property-write annotation. This makes it easier for IDEs to provide auto-completion.

Since your code implements the magic setter _set, this function will be called for any write access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

Since the property has write access only, you can use the @property-write annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
764
		}
765
766
		return $n;
767
	}
768
769
	private function Statements($x)
770
	{
771
		$n = new JSNode($this->t, JS_BLOCK);
772
		array_push($x->stmtStack, $n);
773
774
		while (!$this->t->isDone() && $this->t->peek() != OP_RIGHT_CURLY)
775
			$n->addNode($this->Statement($x));
776
777
		array_pop($x->stmtStack);
778
779
		return $n;
780
	}
781
782
	private function Block($x)
783
	{
784
		$this->t->mustMatch(OP_LEFT_CURLY);
785
		$n = $this->Statements($x);
786
		$this->t->mustMatch(OP_RIGHT_CURLY);
787
788
		return $n;
789
	}
790
791
	private function Statement($x)
792
	{
793
		$tt = $this->t->get();
794
		$n2 = null;
795
796
		// Cases for statements ending in a right curly return early, avoiding the
797
		// common semicolon insertion magic after this switch.
798
		switch ($tt)
799
		{
800
			case KEYWORD_FUNCTION:
801
				return $this->FunctionDefinition(
802
					$x,
803
					true,
804
					count($x->stmtStack) > 1 ? STATEMENT_FORM : DECLARED_FORM
805
				);
806
			break;
807
808
			case OP_LEFT_CURLY:
809
				$n = $this->Statements($x);
810
				$this->t->mustMatch(OP_RIGHT_CURLY);
811
			return $n;
812
813
			case KEYWORD_IF:
814
				$n = new JSNode($this->t);
815
				$n->condition = $this->ParenExpression($x);
816
				array_push($x->stmtStack, $n);
817
				$n->thenPart = $this->Statement($x);
818
				$n->elsePart = $this->t->match(KEYWORD_ELSE) ? $this->Statement($x) : null;
819
				array_pop($x->stmtStack);
820
			return $n;
821
822
			case KEYWORD_SWITCH:
823
				$n = new JSNode($this->t);
824
				$this->t->mustMatch(OP_LEFT_PAREN);
825
				$n->discriminant = $this->Expression($x);
826
				$this->t->mustMatch(OP_RIGHT_PAREN);
827
				$n->cases = array();
828
				$n->defaultIndex = -1;
829
830
				array_push($x->stmtStack, $n);
831
832
				$this->t->mustMatch(OP_LEFT_CURLY);
833
834
				while (($tt = $this->t->get()) != OP_RIGHT_CURLY)
835
				{
836
					switch ($tt)
837
					{
838
						case KEYWORD_DEFAULT:
839
							if ($n->defaultIndex >= 0)
840
								throw $this->t->newSyntaxError('More than one switch default');
841
							// FALL THROUGH
842
						case KEYWORD_CASE:
843
							$n2 = new JSNode($this->t);
844
							if ($tt == KEYWORD_DEFAULT)
845
								$n->defaultIndex = count($n->cases);
846
							else
847
								$n2->caseLabel = $this->Expression($x, OP_COLON);
848
								break;
849
						default:
850
							throw $this->t->newSyntaxError('Invalid switch case');
851
					}
852
853
					$this->t->mustMatch(OP_COLON);
854
					$n2->statements = new JSNode($this->t, JS_BLOCK);
855
					while (($tt = $this->t->peek()) != KEYWORD_CASE && $tt != KEYWORD_DEFAULT && $tt != OP_RIGHT_CURLY)
856
						$n2->statements->addNode($this->Statement($x));
857
858
					array_push($n->cases, $n2);
859
				}
860
861
				array_pop($x->stmtStack);
862
			return $n;
863
864
			case KEYWORD_FOR:
865
				$n = new JSNode($this->t);
866
				$n->isLoop = true;
867
				$this->t->mustMatch(OP_LEFT_PAREN);
868
869
				if (($tt = $this->t->peek()) != OP_SEMICOLON)
870
				{
871
					$x->inForLoopInit = true;
872
					if ($tt == KEYWORD_VAR || $tt == KEYWORD_CONST)
873
					{
874
						$this->t->get();
875
						$n2 = $this->Variables($x);
876
					}
877
					else
878
					{
879
						$n2 = $this->Expression($x);
880
					}
881
					$x->inForLoopInit = false;
882
				}
883
884
				if ($n2 && $this->t->match(KEYWORD_IN))
885
				{
886
					$n->type = JS_FOR_IN;
887
					if ($n2->type == KEYWORD_VAR)
888
					{
889
						if (count($n2->treeNodes) != 1)
890
						{
891
							throw $this->t->SyntaxError(
892
								'Invalid for..in left-hand side',
893
								$this->t->filename,
894
								$n2->lineno
895
							);
896
						}
897
898
						// NB: n2[0].type == IDENTIFIER and n2[0].value == n2[0].name.
899
						$n->iterator = $n2->treeNodes[0];
900
						$n->varDecl = $n2;
901
					}
902
					else
903
					{
904
						$n->iterator = $n2;
905
						$n->varDecl = null;
906
					}
907
908
					$n->object = $this->Expression($x);
909
				}
910
				else
911
				{
912
					$n->setup = $n2 ? $n2 : null;
913
					$this->t->mustMatch(OP_SEMICOLON);
914
					$n->condition = $this->t->peek() == OP_SEMICOLON ? null : $this->Expression($x);
915
					$this->t->mustMatch(OP_SEMICOLON);
916
					$n->update = $this->t->peek() == OP_RIGHT_PAREN ? null : $this->Expression($x);
917
				}
918
919
				$this->t->mustMatch(OP_RIGHT_PAREN);
920
				$n->body = $this->nest($x, $n);
921
			return $n;
922
923
			case KEYWORD_WHILE:
924
			        $n = new JSNode($this->t);
925
			        $n->isLoop = true;
926
			        $n->condition = $this->ParenExpression($x);
927
			        $n->body = $this->nest($x, $n);
928
			return $n;
929
930
			case KEYWORD_DO:
931
				$n = new JSNode($this->t);
932
				$n->isLoop = true;
933
				$n->body = $this->nest($x, $n, KEYWORD_WHILE);
934
				$n->condition = $this->ParenExpression($x);
935
				if (!$x->ecmaStrictMode)
936
				{
937
					// <script language="JavaScript"> (without version hints) may need
938
					// automatic semicolon insertion without a newline after do-while.
939
					// See http://bugzilla.mozilla.org/show_bug.cgi?id=238945.
940
					$this->t->match(OP_SEMICOLON);
941
					return $n;
942
				}
943
			break;
944
945
			case KEYWORD_BREAK:
946
			case KEYWORD_CONTINUE:
947
				$n = new JSNode($this->t);
948
949
				if ($this->t->peekOnSameLine() == TOKEN_IDENTIFIER)
950
				{
951
					$this->t->get();
952
					$n->label = $this->t->currentToken()->value;
953
				}
954
955
				$ss = $x->stmtStack;
956
				$i = count($ss);
957
				$label = $n->label;
958
				if ($label)
959
				{
960
					do
961
					{
962
						if (--$i < 0)
963
							throw $this->t->newSyntaxError('Label not found');
964
					}
965
					while ($ss[$i]->label != $label);
966
				}
967
				else
968
				{
969
					do
970
					{
971
						if (--$i < 0)
972
							throw $this->t->newSyntaxError('Invalid ' . $tt);
973
					}
974
					while (!$ss[$i]->isLoop && ($tt != KEYWORD_BREAK || $ss[$i]->type != KEYWORD_SWITCH));
975
				}
976
977
				$n->target = $ss[$i];
978
			break;
979
980
			case KEYWORD_TRY:
981
				$n = new JSNode($this->t);
982
				$n->tryBlock = $this->Block($x);
983
				$n->catchClauses = array();
984
985
				while ($this->t->match(KEYWORD_CATCH))
986
				{
987
					$n2 = new JSNode($this->t);
988
					$this->t->mustMatch(OP_LEFT_PAREN);
989
					$n2->varName = $this->t->mustMatch(TOKEN_IDENTIFIER)->value;
990
991
					if ($this->t->match(KEYWORD_IF))
992
					{
993
						if ($x->ecmaStrictMode)
994
							throw $this->t->newSyntaxError('Illegal catch guard');
995
996
						if (count($n->catchClauses) && !end($n->catchClauses)->guard)
997
							throw $this->t->newSyntaxError('Guarded catch after unguarded');
998
999
						$n2->guard = $this->Expression($x);
1000
					}
1001
					else
1002
					{
1003
						$n2->guard = null;
1004
					}
1005
1006
					$this->t->mustMatch(OP_RIGHT_PAREN);
1007
					$n2->block = $this->Block($x);
1008
					array_push($n->catchClauses, $n2);
1009
				}
1010
1011
				if ($this->t->match(KEYWORD_FINALLY))
1012
					$n->finallyBlock = $this->Block($x);
1013
1014
				if (!count($n->catchClauses) && !$n->finallyBlock)
1015
					throw $this->t->newSyntaxError('Invalid try statement');
1016
			return $n;
1017
1018
			case KEYWORD_CATCH:
1019
			case KEYWORD_FINALLY:
1020
				throw $this->t->newSyntaxError($tt . ' without preceding try');
1021
1022
			case KEYWORD_THROW:
1023
				$n = new JSNode($this->t);
1024
				$n->value = $this->Expression($x);
0 ignored issues
show
The property $value is declared private in JSNode. Since you implemented __set(), maybe consider adding a @property or @property-write annotation. This makes it easier for IDEs to provide auto-completion.

Since your code implements the magic setter _set, this function will be called for any write access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

Since the property has write access only, you can use the @property-write annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
1025
			break;
1026
1027
			case KEYWORD_RETURN:
1028
				if (!$x->inFunction)
1029
					throw $this->t->newSyntaxError('Invalid return');
1030
1031
				$n = new JSNode($this->t);
1032
				$tt = $this->t->peekOnSameLine();
1033
				if ($tt != TOKEN_END && $tt != TOKEN_NEWLINE && $tt != OP_SEMICOLON && $tt != OP_RIGHT_CURLY)
1034
					$n->value = $this->Expression($x);
1035
				else
1036
					$n->value = null;
1037
			break;
1038
1039
			case KEYWORD_WITH:
1040
				$n = new JSNode($this->t);
1041
				$n->object = $this->ParenExpression($x);
1042
				$n->body = $this->nest($x, $n);
1043
			return $n;
1044
1045
			case KEYWORD_VAR:
1046
			case KEYWORD_CONST:
1047
			        $n = $this->Variables($x);
1048
			break;
1049
1050
			case TOKEN_CONDCOMMENT_START:
1051
			case TOKEN_CONDCOMMENT_END:
1052
				$n = new JSNode($this->t);
1053
			return $n;
1054
1055
			case KEYWORD_DEBUGGER:
1056
				$n = new JSNode($this->t);
1057
			break;
1058
1059
			case TOKEN_NEWLINE:
1060
			case OP_SEMICOLON:
1061
				$n = new JSNode($this->t, OP_SEMICOLON);
1062
				$n->expression = null;
1063
			return $n;
1064
1065
			default:
1066
				if ($tt == TOKEN_IDENTIFIER)
1067
				{
1068
					$this->t->scanOperand = false;
1069
					$tt = $this->t->peek();
1070
					$this->t->scanOperand = true;
1071
					if ($tt == OP_COLON)
1072
					{
1073
						$label = $this->t->currentToken()->value;
1074
						$ss = $x->stmtStack;
1075
						for ($i = count($ss) - 1; $i >= 0; --$i)
1076
						{
1077
							if ($ss[$i]->label == $label)
1078
								throw $this->t->newSyntaxError('Duplicate label');
1079
						}
1080
1081
						$this->t->get();
1082
						$n = new JSNode($this->t, JS_LABEL);
1083
						$n->label = $label;
1084
						$n->statement = $this->nest($x, $n);
1085
1086
						return $n;
1087
					}
1088
				}
1089
1090
				$n = new JSNode($this->t, OP_SEMICOLON);
1091
				$this->t->unget();
1092
				$n->expression = $this->Expression($x);
1093
				$n->end = $n->expression->end;
1094
			break;
1095
		}
1096
1097
		if ($this->t->lineno == $this->t->currentToken()->lineno)
1098
		{
1099
			$tt = $this->t->peekOnSameLine();
1100
			if ($tt != TOKEN_END && $tt != TOKEN_NEWLINE && $tt != OP_SEMICOLON && $tt != OP_RIGHT_CURLY)
1101
				throw $this->t->newSyntaxError('Missing ; before statement');
1102
		}
1103
1104
		$this->t->match(OP_SEMICOLON);
1105
1106
		return $n;
1107
	}
1108
1109
	private function FunctionDefinition($x, $requireName, $functionForm)
1110
	{
1111
		$f = new JSNode($this->t);
1112
1113
		if ($f->type != KEYWORD_FUNCTION)
1114
			$f->type = ($f->value == 'get') ? JS_GETTER : JS_SETTER;
1115
1116
		if ($this->t->match(TOKEN_IDENTIFIER))
1117
			$f->name = $this->t->currentToken()->value;
1118
		elseif ($requireName)
1119
			throw $this->t->newSyntaxError('Missing function identifier');
1120
1121
		$this->t->mustMatch(OP_LEFT_PAREN);
1122
			$f->params = array();
1123
1124
		while (($tt = $this->t->get()) != OP_RIGHT_PAREN)
1125
		{
1126
			if ($tt != TOKEN_IDENTIFIER)
1127
				throw $this->t->newSyntaxError('Missing formal parameter');
1128
1129
			array_push($f->params, $this->t->currentToken()->value);
1130
1131
			if ($this->t->peek() != OP_RIGHT_PAREN)
1132
				$this->t->mustMatch(OP_COMMA);
1133
		}
1134
1135
		$this->t->mustMatch(OP_LEFT_CURLY);
1136
1137
		$x2 = new JSCompilerContext(true);
1138
		$f->body = $this->Script($x2);
1139
1140
		$this->t->mustMatch(OP_RIGHT_CURLY);
1141
		$f->end = $this->t->currentToken()->end;
1142
1143
		$f->functionForm = $functionForm;
1144
		if ($functionForm == DECLARED_FORM)
1145
			array_push($x->funDecls, $f);
1146
1147
		return $f;
1148
	}
1149
1150
	private function Variables($x)
1151
	{
1152
		$n = new JSNode($this->t);
1153
1154
		do
1155
		{
1156
			$this->t->mustMatch(TOKEN_IDENTIFIER);
1157
1158
			$n2 = new JSNode($this->t);
1159
			$n2->name = $n2->value;
1160
1161
			if ($this->t->match(OP_ASSIGN))
1162
			{
1163
				if ($this->t->currentToken()->assignOp)
1164
					throw $this->t->newSyntaxError('Invalid variable initialization');
1165
1166
				$n2->initializer = $this->Expression($x, OP_COMMA);
1167
			}
1168
1169
			$n2->readOnly = $n->type == KEYWORD_CONST;
1170
1171
			$n->addNode($n2);
1172
			array_push($x->varDecls, $n2);
1173
		}
1174
		while ($this->t->match(OP_COMMA));
1175
1176
		return $n;
1177
	}
1178
1179
	private function Expression($x, $stop=false)
1180
	{
1181
		$operators = array();
1182
		$operands = array();
1183
		$n = false;
1184
1185
		$bl = $x->bracketLevel;
1186
		$cl = $x->curlyLevel;
1187
		$pl = $x->parenLevel;
1188
		$hl = $x->hookLevel;
1189
1190
		while (($tt = $this->t->get()) != TOKEN_END)
1191
		{
1192
			if ($tt == $stop &&
1193
				$x->bracketLevel == $bl &&
1194
				$x->curlyLevel == $cl &&
1195
				$x->parenLevel == $pl &&
1196
				$x->hookLevel == $hl
1197
			)
1198
			{
1199
				// Stop only if tt matches the optional stop parameter, and that
1200
				// token is not quoted by some kind of bracket.
1201
				break;
1202
			}
1203
1204
			switch ($tt)
1205
			{
1206
				case OP_SEMICOLON:
1207
					// NB: cannot be empty, Statement handled that.
1208
					break 2;
1209
1210
				case OP_HOOK:
1211
					if ($this->t->scanOperand)
1212
						break 2;
1213
1214 View Code Duplication
					while (	!empty($operators) &&
1215
						$this->opPrecedence[end($operators)->type] > $this->opPrecedence[$tt]
1216
					)
1217
						$this->reduce($operators, $operands);
1218
1219
					array_push($operators, new JSNode($this->t));
1220
1221
					++$x->hookLevel;
1222
					$this->t->scanOperand = true;
1223
					$n = $this->Expression($x);
1224
1225
					if (!$this->t->match(OP_COLON))
1226
						break 2;
1227
1228
					--$x->hookLevel;
1229
					array_push($operands, $n);
1230
				break;
1231
1232
				case OP_COLON:
1233
					if ($x->hookLevel)
1234
						break 2;
1235
1236
					throw $this->t->newSyntaxError('Invalid label');
1237
				break;
1238
1239
				case OP_ASSIGN:
1240
					if ($this->t->scanOperand)
1241
						break 2;
1242
1243
					// Use >, not >=, for right-associative ASSIGN
1244 View Code Duplication
					while (	!empty($operators) &&
1245
						$this->opPrecedence[end($operators)->type] > $this->opPrecedence[$tt]
1246
					)
1247
						$this->reduce($operators, $operands);
1248
1249
					array_push($operators, new JSNode($this->t));
1250
					end($operands)->assignOp = $this->t->currentToken()->assignOp;
1251
					$this->t->scanOperand = true;
1252
				break;
1253
1254 View Code Duplication
				case KEYWORD_IN:
1255
					// An in operator should not be parsed if we're parsing the head of
1256
					// a for (...) loop, unless it is in the then part of a conditional
1257
					// expression, or parenthesized somehow.
1258
					if ($x->inForLoopInit && !$x->hookLevel &&
1259
						!$x->bracketLevel && !$x->curlyLevel &&
1260
						!$x->parenLevel
1261
					)
1262
						break 2;
1263
				// FALL THROUGH
1264 View Code Duplication
				case OP_COMMA:
1265
					// A comma operator should not be parsed if we're parsing the then part
1266
					// of a conditional expression unless it's parenthesized somehow.
1267
					if ($tt == OP_COMMA && $x->hookLevel &&
1268
						!$x->bracketLevel && !$x->curlyLevel &&
1269
						!$x->parenLevel
1270
					)
1271
						break 2;
1272
				// Treat comma as left-associative so reduce can fold left-heavy
1273
				// COMMA trees into a single array.
1274
				// FALL THROUGH
1275
				case OP_OR:
1276
				case OP_AND:
1277
				case OP_BITWISE_OR:
1278
				case OP_BITWISE_XOR:
1279
				case OP_BITWISE_AND:
1280
				case OP_EQ: case OP_NE: case OP_STRICT_EQ: case OP_STRICT_NE:
1281
				case OP_LT: case OP_LE: case OP_GE: case OP_GT:
1282
				case KEYWORD_INSTANCEOF:
1283
				case OP_LSH: case OP_RSH: case OP_URSH:
1284
				case OP_PLUS: case OP_MINUS:
1285
				case OP_MUL: case OP_DIV: case OP_MOD:
1286
				case OP_DOT:
1287
					if ($this->t->scanOperand)
1288
						break 2;
1289
1290 View Code Duplication
					while (	!empty($operators) &&
1291
						$this->opPrecedence[end($operators)->type] >= $this->opPrecedence[$tt]
1292
					)
1293
						$this->reduce($operators, $operands);
1294
1295
					if ($tt == OP_DOT)
1296
					{
1297
						$this->t->mustMatch(TOKEN_IDENTIFIER);
1298
						array_push($operands, new JSNode($this->t, OP_DOT, array_pop($operands), new JSNode($this->t)));
1299
					}
1300
					else
1301
					{
1302
						array_push($operators, new JSNode($this->t));
1303
						$this->t->scanOperand = true;
1304
					}
1305
				break;
1306
1307
				case KEYWORD_DELETE: case KEYWORD_VOID: case KEYWORD_TYPEOF:
1308
				case OP_NOT: case OP_BITWISE_NOT: case OP_UNARY_PLUS: case OP_UNARY_MINUS:
1309 View Code Duplication
				case KEYWORD_NEW:
1310
					if (!$this->t->scanOperand)
1311
						break 2;
1312
1313
					array_push($operators, new JSNode($this->t));
1314
				break;
1315
1316
				case OP_INCREMENT: case OP_DECREMENT:
1317
					if ($this->t->scanOperand)
1318
					{
1319
						array_push($operators, new JSNode($this->t));  // prefix increment or decrement
1320
					}
1321
					else
1322
					{
1323
						// Don't cross a line boundary for postfix {in,de}crement.
1324
						$t = $this->t->tokens[($this->t->tokenIndex + $this->t->lookahead - 1) & 3];
1325
						if ($t && $t->lineno != $this->t->lineno)
1326
							break 2;
1327
1328
						if (!empty($operators))
1329
						{
1330
							// Use >, not >=, so postfix has higher precedence than prefix.
1331 View Code Duplication
							while ($this->opPrecedence[end($operators)->type] > $this->opPrecedence[$tt])
1332
								$this->reduce($operators, $operands);
1333
						}
1334
1335
						$n = new JSNode($this->t, $tt, array_pop($operands));
1336
						$n->postfix = true;
1337
						array_push($operands, $n);
1338
					}
1339
				break;
1340
1341
				case KEYWORD_FUNCTION:
1342
					if (!$this->t->scanOperand)
1343
						break 2;
1344
1345
					array_push($operands, $this->FunctionDefinition($x, false, EXPRESSED_FORM));
1346
					$this->t->scanOperand = false;
1347
				break;
1348
1349
				case KEYWORD_NULL: case KEYWORD_THIS: case KEYWORD_TRUE: case KEYWORD_FALSE:
1350 View Code Duplication
				case TOKEN_IDENTIFIER: case TOKEN_NUMBER: case TOKEN_STRING: case TOKEN_REGEXP:
1351
					if (!$this->t->scanOperand)
1352
						break 2;
1353
1354
					array_push($operands, new JSNode($this->t));
1355
					$this->t->scanOperand = false;
1356
				break;
1357
1358
				case TOKEN_CONDCOMMENT_START:
1359
				case TOKEN_CONDCOMMENT_END:
1360
					if ($this->t->scanOperand)
1361
						array_push($operators, new JSNode($this->t));
1362
					else
1363
						array_push($operands, new JSNode($this->t));
1364
				break;
1365
1366
				case OP_LEFT_BRACKET:
1367
					if ($this->t->scanOperand)
1368
					{
1369
						// Array initialiser.  Parse using recursive descent, as the
1370
						// sub-grammar here is not an operator grammar.
1371
						$n = new JSNode($this->t, JS_ARRAY_INIT);
1372
						while (($tt = $this->t->peek()) != OP_RIGHT_BRACKET)
1373
						{
1374
							if ($tt == OP_COMMA)
1375
							{
1376
								$this->t->get();
1377
								$n->addNode(null);
1378
								continue;
1379
							}
1380
1381
							$n->addNode($this->Expression($x, OP_COMMA));
1382
							if (!$this->t->match(OP_COMMA))
1383
								break;
1384
						}
1385
1386
						$this->t->mustMatch(OP_RIGHT_BRACKET);
1387
						array_push($operands, $n);
1388
						$this->t->scanOperand = false;
1389
					}
1390
					else
1391
					{
1392
						// Property indexing operator.
1393
						array_push($operators, new JSNode($this->t, JS_INDEX));
1394
						$this->t->scanOperand = true;
1395
						++$x->bracketLevel;
1396
					}
1397
				break;
1398
1399
				case OP_RIGHT_BRACKET:
1400
					if ($this->t->scanOperand || $x->bracketLevel == $bl)
1401
						break 2;
1402
1403
					while ($this->reduce($operators, $operands)->type != JS_INDEX)
1404
						continue;
1405
1406
					--$x->bracketLevel;
1407
				break;
1408
1409
				case OP_LEFT_CURLY:
1410
					if (!$this->t->scanOperand)
1411
						break 2;
1412
1413
					// Object initialiser.  As for array initialisers (see above),
1414
					// parse using recursive descent.
1415
					++$x->curlyLevel;
1416
					$n = new JSNode($this->t, JS_OBJECT_INIT);
1417
					while (!$this->t->match(OP_RIGHT_CURLY))
1418
					{
1419
						do
1420
						{
1421
							$tt = $this->t->get();
1422
							$tv = $this->t->currentToken()->value;
1423
							if (($tv == 'get' || $tv == 'set') && $this->t->peek() == TOKEN_IDENTIFIER)
1424
							{
1425
								if ($x->ecmaStrictMode)
1426
									throw $this->t->newSyntaxError('Illegal property accessor');
1427
1428
								$n->addNode($this->FunctionDefinition($x, true, EXPRESSED_FORM));
1429
							}
1430
							else
1431
							{
1432
								switch ($tt)
1433
								{
1434
									case TOKEN_IDENTIFIER:
1435
									case TOKEN_NUMBER:
1436
									case TOKEN_STRING:
1437
										$id = new JSNode($this->t);
1438
									break;
1439
1440
									case OP_RIGHT_CURLY:
1441
										if ($x->ecmaStrictMode)
1442
											throw $this->t->newSyntaxError('Illegal trailing ,');
1443
									break 3;
1444
1445
									default:
1446
										throw $this->t->newSyntaxError('Invalid property name');
1447
								}
1448
1449
								$this->t->mustMatch(OP_COLON);
1450
								$n->addNode(new JSNode($this->t, JS_PROPERTY_INIT, $id, $this->Expression($x, OP_COMMA)));
1451
							}
1452
						}
1453
						while ($this->t->match(OP_COMMA));
1454
1455
						$this->t->mustMatch(OP_RIGHT_CURLY);
1456
						break;
1457
					}
1458
1459
					array_push($operands, $n);
1460
					$this->t->scanOperand = false;
1461
					--$x->curlyLevel;
1462
				break;
1463
1464
				case OP_RIGHT_CURLY:
1465
					if (!$this->t->scanOperand && $x->curlyLevel != $cl)
1466
						throw new Exception('PANIC: right curly botch');
1467
				break 2;
1468
1469
				case OP_LEFT_PAREN:
1470
					if ($this->t->scanOperand)
1471
					{
1472
						array_push($operators, new JSNode($this->t, JS_GROUP));
1473
					}
1474
					else
1475
					{
1476 View Code Duplication
						while (	!empty($operators) &&
1477
							$this->opPrecedence[end($operators)->type] > $this->opPrecedence[KEYWORD_NEW]
1478
						)
1479
							$this->reduce($operators, $operands);
1480
1481
						// Handle () now, to regularize the n-ary case for n > 0.
1482
						// We must set scanOperand in case there are arguments and
1483
						// the first one is a regexp or unary+/-.
1484
						$n = end($operators);
1485
						$this->t->scanOperand = true;
1486
						if ($this->t->match(OP_RIGHT_PAREN))
1487
						{
1488
							if ($n && $n->type == KEYWORD_NEW)
1489
							{
1490
								array_pop($operators);
1491
								$n->addNode(array_pop($operands));
1492
							}
1493
							else
1494
							{
1495
								$n = new JSNode($this->t, JS_CALL, array_pop($operands), new JSNode($this->t, JS_LIST));
1496
							}
1497
1498
							array_push($operands, $n);
1499
							$this->t->scanOperand = false;
1500
							break;
1501
						}
1502
1503
						if ($n && $n->type == KEYWORD_NEW)
1504
							$n->type = JS_NEW_WITH_ARGS;
1505
						else
1506
							array_push($operators, new JSNode($this->t, JS_CALL));
1507
					}
1508
1509
					++$x->parenLevel;
1510
				break;
1511
1512
				case OP_RIGHT_PAREN:
1513
					if ($this->t->scanOperand || $x->parenLevel == $pl)
1514
						break 2;
1515
1516
					while (($tt = $this->reduce($operators, $operands)->type) != JS_GROUP &&
1517
						$tt != JS_CALL && $tt != JS_NEW_WITH_ARGS
1518
					)
1519
					{
1520
						continue;
1521
					}
1522
1523
					if ($tt != JS_GROUP)
1524
					{
1525
						$n = end($operands);
1526
						if ($n->treeNodes[1]->type != OP_COMMA)
1527
							$n->treeNodes[1] = new JSNode($this->t, JS_LIST, $n->treeNodes[1]);
1528
						else
1529
							$n->treeNodes[1]->type = JS_LIST;
1530
					}
1531
1532
					--$x->parenLevel;
1533
				break;
1534
1535
				// Automatic semicolon insertion means we may scan across a newline
1536
				// and into the beginning of another statement.  If so, break out of
1537
				// the while loop and let the t.scanOperand logic handle errors.
1538
				default:
1539
					break 2;
1540
			}
1541
		}
1542
1543
		if ($x->hookLevel != $hl)
1544
			throw $this->t->newSyntaxError('Missing : in conditional expression');
1545
1546
		if ($x->parenLevel != $pl)
1547
			throw $this->t->newSyntaxError('Missing ) in parenthetical');
1548
1549
		if ($x->bracketLevel != $bl)
1550
			throw $this->t->newSyntaxError('Missing ] in index expression');
1551
1552
		if ($this->t->scanOperand)
1553
			throw $this->t->newSyntaxError('Missing operand');
1554
1555
		// Resume default mode, scanning for operands, not operators.
1556
		$this->t->scanOperand = true;
1557
		$this->t->unget();
1558
1559
		while (count($operators))
1560
			$this->reduce($operators, $operands);
1561
1562
		return array_pop($operands);
1563
	}
1564
1565
	private function ParenExpression($x)
1566
	{
1567
		$this->t->mustMatch(OP_LEFT_PAREN);
1568
		$n = $this->Expression($x);
1569
		$this->t->mustMatch(OP_RIGHT_PAREN);
1570
1571
		return $n;
1572
	}
1573
1574
	// Statement stack and nested statement handler.
1575
	private function nest($x, $node, $end = false)
1576
	{
1577
		array_push($x->stmtStack, $node);
1578
		$n = $this->statement($x);
1579
		array_pop($x->stmtStack);
1580
1581
		if ($end)
1582
			$this->t->mustMatch($end);
1583
1584
		return $n;
1585
	}
1586
1587
	private function reduce(&$operators, &$operands)
1588
	{
1589
		$n = array_pop($operators);
1590
		$op = $n->type;
1591
		$arity = $this->opArity[$op];
1592
		$c = count($operands);
1593
		if ($arity == -2)
1594
		{
1595
			// Flatten left-associative trees
1596
			if ($c >= 2)
1597
			{
1598
				$left = $operands[$c - 2];
1599
				if ($left->type == $op)
1600
				{
1601
					$right = array_pop($operands);
1602
					$left->addNode($right);
1603
					return $left;
1604
				}
1605
			}
1606
			$arity = 2;
1607
		}
1608
1609
		// Always use push to add operands to n, to update start and end
1610
		$a = array_splice($operands, $c - $arity);
1611
		for ($i = 0; $i < $arity; $i++)
1612
			$n->addNode($a[$i]);
1613
1614
		// Include closing bracket or postfix operator in [start,end]
1615
		$te = $this->t->currentToken()->end;
1616
		if ($n->end < $te)
1617
			$n->end = $te;
1618
1619
		array_push($operands, $n);
1620
1621
		return $n;
1622
	}
1623
}
1624
1625
class JSCompilerContext
1626
{
1627
	public $inFunction = false;
1628
	public $inForLoopInit = false;
1629
	public $ecmaStrictMode = false;
1630
	public $bracketLevel = 0;
1631
	public $curlyLevel = 0;
1632
	public $parenLevel = 0;
1633
	public $hookLevel = 0;
1634
1635
	public $stmtStack = array();
1636
	public $funDecls = array();
1637
	public $varDecls = array();
1638
1639
	public function __construct($inFunction)
1640
	{
1641
		$this->inFunction = $inFunction;
1642
	}
1643
}
1644
1645
class JSNode
1646
{
1647
	private $type;
1648
	private $value;
1649
	private $lineno;
1650
	private $start;
1651
	private $end;
1652
1653
	public $treeNodes = array();
1654
	public $funDecls = array();
1655
	public $varDecls = array();
1656
1657
	public function __construct($t, $type=0)
1658
	{
1659
		if ($token = $t->currentToken())
1660
		{
1661
			$this->type = $type ? $type : $token->type;
1662
			$this->value = $token->value;
1663
			$this->lineno = $token->lineno;
1664
			$this->start = $token->start;
1665
			$this->end = $token->end;
1666
		}
1667
		else
1668
		{
1669
			$this->type = $type;
1670
			$this->lineno = $t->lineno;
1671
		}
1672
1673
		if (($numargs = func_num_args()) > 2)
1674
		{
1675
			$args = func_get_args();
1676
			for ($i = 2; $i < $numargs; $i++)
1677
				$this->addNode($args[$i]);
1678
		}
1679
	}
1680
1681
	// we don't want to bloat our object with all kind of specific properties, so we use overloading
1682
	public function __set($name, $value)
1683
	{
1684
		$this->$name = $value;
1685
	}
1686
1687
	public function __get($name)
1688
	{
1689
		if (isset($this->$name))
1690
			return $this->$name;
1691
1692
		return null;
1693
	}
1694
1695
	public function addNode($node)
1696
	{
1697
		if ($node !== null)
1698
		{
1699
			if ($node->start < $this->start)
1700
				$this->start = $node->start;
1701
			if ($this->end < $node->end)
1702
				$this->end = $node->end;
1703
		}
1704
1705
		$this->treeNodes[] = $node;
1706
	}
1707
}
1708
1709
class JSTokenizer
1710
{
1711
	private $cursor = 0;
1712
	private $source;
1713
1714
	public $tokens = array();
1715
	public $tokenIndex = 0;
1716
	public $lookahead = 0;
1717
	public $scanNewlines = false;
1718
	public $scanOperand = true;
1719
1720
	public $filename;
1721
	public $lineno;
1722
1723
	private $keywords = array(
1724
		'break',
1725
		'case', 'catch', 'const', 'continue',
1726
		'debugger', 'default', 'delete', 'do',
1727
		'else', 'enum',
1728
		'false', 'finally', 'for', 'function',
1729
		'if', 'in', 'instanceof',
1730
		'new', 'null',
1731
		'return',
1732
		'switch',
1733
		'this', 'throw', 'true', 'try', 'typeof',
1734
		'var', 'void',
1735
		'while', 'with'
1736
	);
1737
1738
	private $opTypeNames = array(
1739
		';', ',', '?', ':', '||', '&&', '|', '^',
1740
		'&', '===', '==', '=', '!==', '!=', '<<', '<=',
1741
		'<', '>>>', '>>', '>=', '>', '++', '--', '+',
1742
		'-', '*', '/', '%', '!', '~', '.', '[',
1743
		']', '{', '}', '(', ')', '@*/'
1744
	);
1745
1746
	private $assignOps = array('|', '^', '&', '<<', '>>', '>>>', '+', '-', '*', '/', '%');
1747
	private $opRegExp;
1748
1749
	public function __construct()
1750
	{
1751
		$this->opRegExp = '#^(' . implode('|', array_map('preg_quote', $this->opTypeNames)) . ')#';
1752
	}
1753
1754
	public function init($source, $filename = '', $lineno = 1)
1755
	{
1756
		$this->source = $source;
1757
		$this->filename = $filename ? $filename : '[inline]';
1758
		$this->lineno = $lineno;
1759
1760
		$this->cursor = 0;
1761
		$this->tokens = array();
1762
		$this->tokenIndex = 0;
1763
		$this->lookahead = 0;
1764
		$this->scanNewlines = false;
1765
		$this->scanOperand = true;
1766
	}
1767
1768
	public function getInput($chunksize)
1769
	{
1770
		if ($chunksize)
1771
			return substr($this->source, $this->cursor, $chunksize);
1772
1773
		return substr($this->source, $this->cursor);
1774
	}
1775
1776
	public function isDone()
1777
	{
1778
		return $this->peek() == TOKEN_END;
1779
	}
1780
1781
	public function match($tt)
1782
	{
1783
		return $this->get() == $tt || $this->unget();
1784
	}
1785
1786
	public function mustMatch($tt)
1787
	{
1788
	        if (!$this->match($tt))
1789
			throw $this->newSyntaxError('Unexpected token; token ' . $tt . ' expected');
1790
1791
		return $this->currentToken();
1792
	}
1793
1794
	public function peek()
1795
	{
1796
		if ($this->lookahead)
1797
		{
1798
			$next = $this->tokens[($this->tokenIndex + $this->lookahead) & 3];
1799
			if ($this->scanNewlines && $next->lineno != $this->lineno)
1800
				$tt = TOKEN_NEWLINE;
1801
			else
1802
				$tt = $next->type;
1803
		}
1804
		else
1805
		{
1806
			$tt = $this->get();
1807
			$this->unget();
1808
		}
1809
1810
		return $tt;
1811
	}
1812
1813
	public function peekOnSameLine()
1814
	{
1815
		$this->scanNewlines = true;
1816
		$tt = $this->peek();
1817
		$this->scanNewlines = false;
1818
1819
		return $tt;
1820
	}
1821
1822
	public function currentToken()
1823
	{
1824
		if (!empty($this->tokens))
1825
			return $this->tokens[$this->tokenIndex];
1826
	}
1827
1828
	public function get($chunksize = 1000)
1829
	{
1830
		while($this->lookahead)
1831
		{
1832
			$this->lookahead--;
1833
			$this->tokenIndex = ($this->tokenIndex + 1) & 3;
1834
			$token = $this->tokens[$this->tokenIndex];
1835
			if ($token->type != TOKEN_NEWLINE || $this->scanNewlines)
1836
				return $token->type;
1837
		}
1838
1839
		$conditional_comment = false;
1840
1841
		// strip whitespace and comments
1842
		while(true)
1843
		{
1844
			$input = $this->getInput($chunksize);
1845
1846
			// whitespace handling; gobble up \r as well (effectively we don't have support for MAC newlines!)
1847
			$re = $this->scanNewlines ? '/^[ \r\t]+/' : '/^\s+/';
1848
			if (preg_match($re, $input, $match))
1849
			{
1850
				$spaces = $match[0];
1851
				$spacelen = strlen($spaces);
1852
				$this->cursor += $spacelen;
1853
				if (!$this->scanNewlines)
1854
					$this->lineno += substr_count($spaces, "\n");
1855
1856
				if ($spacelen == $chunksize)
1857
					continue; // complete chunk contained whitespace
1858
1859
				$input = $this->getInput($chunksize);
1860
				if ($input == '' || $input[0] != '/')
1861
					break;
1862
			}
1863
1864
			// Comments
1865
			if (!preg_match('/^\/(?:\*(@(?:cc_on|if|elif|else|end))?.*?\*\/|\/[^\n]*)/s', $input, $match))
1866
			{
1867
				if (!$chunksize)
1868
					break;
1869
1870
				// retry with a full chunk fetch; this also prevents breakage of long regular expressions (which will never match a comment)
1871
				$chunksize = null;
1872
				continue;
1873
			}
1874
1875
			// check if this is a conditional (JScript) comment
1876
			if (!empty($match[1]))
1877
			{
1878
				$match[0] = '/*' . $match[1];
1879
				$conditional_comment = true;
1880
				break;
1881
			}
1882
			else
1883
			{
1884
				$this->cursor += strlen($match[0]);
1885
				$this->lineno += substr_count($match[0], "\n");
1886
			}
1887
		}
1888
1889
		if ($input == '')
1890
		{
1891
			$tt = TOKEN_END;
1892
			$match = array('');
1893
		}
1894
		elseif ($conditional_comment)
1895
		{
1896
			$tt = TOKEN_CONDCOMMENT_START;
1897
		}
1898
		else
1899
		{
1900
			switch ($input[0])
1901
			{
1902
				case '0':
1903
					// hexadecimal
1904
					if (($input[1] == 'x' || $input[1] == 'X') && preg_match('/^0x[0-9a-f]+/i', $input, $match))
1905
					{
1906
						$tt = TOKEN_NUMBER;
1907
						break;
1908
					}
1909
				// FALL THROUGH
1910
1911
				case '1': case '2': case '3': case '4': case '5':
1912
				case '6': case '7': case '8': case '9':
1913
					// should always match
1914
					preg_match('/^\d+(?:\.\d*)?(?:[eE][-+]?\d+)?/', $input, $match);
1915
					$tt = TOKEN_NUMBER;
1916
				break;
1917
1918 View Code Duplication
				case "'":
1919
					if (preg_match('/^\'(?:[^\\\\\'\r\n]++|\\\\(?:.|\r?\n))*\'/', $input, $match))
1920
					{
1921
						$tt = TOKEN_STRING;
1922
					}
1923
					else
1924
					{
1925
						if ($chunksize)
0 ignored issues
show
Bug Best Practice introduced by
The expression $chunksize of type integer|null is loosely compared to true; this is ambiguous if the integer can be zero. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
1926
							return $this->get(null); // retry with a full chunk fetch
1927
1928
						throw $this->newSyntaxError('Unterminated string literal');
1929
					}
1930
				break;
1931
1932 View Code Duplication
				case '"':
1933
					if (preg_match('/^"(?:[^\\\\"\r\n]++|\\\\(?:.|\r?\n))*"/', $input, $match))
1934
					{
1935
						$tt = TOKEN_STRING;
1936
					}
1937
					else
1938
					{
1939
						if ($chunksize)
1940
							return $this->get(null); // retry with a full chunk fetch
1941
1942
						throw $this->newSyntaxError('Unterminated string literal');
1943
					}
1944
				break;
1945
1946
				case '/':
1947
					if ($this->scanOperand && preg_match('/^\/((?:\\\\.|\[(?:\\\\.|[^\]])*\]|[^\/])+)\/([gimy]*)/', $input, $match))
1948
					{
1949
						$tt = TOKEN_REGEXP;
1950
						break;
1951
					}
1952
				// FALL THROUGH
1953
1954
				case '|':
1955
				case '^':
1956
				case '&':
1957
				case '<':
1958
				case '>':
1959
				case '+':
1960
				case '-':
1961
				case '*':
1962
				case '%':
1963
				case '=':
1964
				case '!':
1965
					// should always match
1966
					preg_match($this->opRegExp, $input, $match);
1967
					$op = $match[0];
1968
					if (in_array($op, $this->assignOps) && $input[strlen($op)] == '=')
1969
					{
1970
						$tt = OP_ASSIGN;
1971
						$match[0] .= '=';
1972
					}
1973
					else
1974
					{
1975
						$tt = $op;
1976
						if ($this->scanOperand)
1977
						{
1978
							if ($op == OP_PLUS)
1979
								$tt = OP_UNARY_PLUS;
1980
							elseif ($op == OP_MINUS)
1981
								$tt = OP_UNARY_MINUS;
1982
						}
1983
						$op = null;
1984
					}
1985
				break;
1986
1987
				case '.':
1988
					if (preg_match('/^\.\d+(?:[eE][-+]?\d+)?/', $input, $match))
1989
					{
1990
						$tt = TOKEN_NUMBER;
1991
						break;
1992
					}
1993
				// FALL THROUGH
1994
1995
				case ';':
1996
				case ',':
1997
				case '?':
1998
				case ':':
1999
				case '~':
2000
				case '[':
2001
				case ']':
2002
				case '{':
2003
				case '}':
2004
				case '(':
2005
				case ')':
2006
					// these are all single
2007
					$match = array($input[0]);
2008
					$tt = $input[0];
2009
				break;
2010
2011
				case '@':
2012
					// check end of conditional comment
2013
					if (substr($input, 0, 3) == '@*/')
2014
					{
2015
						$match = array('@*/');
2016
						$tt = TOKEN_CONDCOMMENT_END;
2017
					}
2018
					else
2019
						throw $this->newSyntaxError('Illegal token');
2020
				break;
2021
2022
				case "\n":
2023
					if ($this->scanNewlines)
2024
					{
2025
						$match = array("\n");
2026
						$tt = TOKEN_NEWLINE;
2027
					}
2028
					else
2029
						throw $this->newSyntaxError('Illegal token');
2030
				break;
2031
2032
				default:
2033
					// Fast path for identifiers: word chars followed by whitespace or various other tokens.
2034
					// Note we don't need to exclude digits in the first char, as they've already been found
2035
					// above.
2036
					if (!preg_match('/^[$\w]+(?=[\s\/\|\^\&<>\+\-\*%=!.;,\?:~\[\]\{\}\(\)@])/', $input, $match))
2037
					{
2038
						// Character classes per ECMA-262 edition 5.1 section 7.6
2039
						// Per spec, must accept Unicode 3.0, *may* accept later versions.
2040
						// We'll take whatever PCRE understands, which should be more recent.
2041
						$identifierStartChars = "\\p{L}\\p{Nl}" .  # UnicodeLetter
2042
						                        "\$" .
2043
						                        "_";
2044
						$identifierPartChars  = $identifierStartChars .
2045
						                        "\\p{Mn}\\p{Mc}" . # UnicodeCombiningMark
2046
						                        "\\p{Nd}" .        # UnicodeDigit
2047
						                        "\\p{Pc}";         # UnicodeConnectorPunctuation
2048
						$unicodeEscape = "\\\\u[0-9A-F-a-f]{4}";
2049
						$identifierRegex = "/^" .
2050
						                   "(?:[$identifierStartChars]|$unicodeEscape)" .
2051
						                   "(?:[$identifierPartChars]|$unicodeEscape)*" .
2052
						                   "/uS";
2053
						if (preg_match($identifierRegex, $input, $match))
2054
						{
2055
							if (strpos($match[0], '\\') !== false) {
2056
								// Per ECMA-262 edition 5.1, section 7.6 escape sequences should behave as if they were
2057
								// the original chars, but only within the boundaries of the identifier.
2058
								$decoded = preg_replace_callback('/\\\\u([0-9A-Fa-f]{4})/',
2059
										array(__CLASS__, 'unicodeEscapeCallback'),
2060
										$match[0]);
2061
2062
								// Since our original regex didn't de-escape the originals, we need to check for validity again.
2063
								// No need to worry about token boundaries, as anything outside the identifier is illegal!
2064
								if (!preg_match("/^[$identifierStartChars][$identifierPartChars]*$/u", $decoded)) {
2065
									throw $this->newSyntaxError('Illegal token');
2066
								}
2067
2068
								// Per spec it _ought_ to work to use these escapes for keywords words as well...
2069
								// but IE rejects them as invalid, while Firefox and Chrome treat them as identifiers
2070
								// that don't match the keyword.
2071
								if (in_array($decoded, $this->keywords)) {
2072
									throw $this->newSyntaxError('Illegal token');
2073
								}
2074
2075
								// TODO: save the decoded form for output?
2076
							}
2077
						}
2078
						else
2079
							throw $this->newSyntaxError('Illegal token');
2080
					}
2081
					$tt = in_array($match[0], $this->keywords) ? $match[0] : TOKEN_IDENTIFIER;
2082
			}
2083
		}
2084
2085
		$this->tokenIndex = ($this->tokenIndex + 1) & 3;
2086
2087
		if (!isset($this->tokens[$this->tokenIndex]))
2088
			$this->tokens[$this->tokenIndex] = new JSToken();
2089
2090
		$token = $this->tokens[$this->tokenIndex];
2091
		$token->type = $tt;
2092
2093
		if ($tt == OP_ASSIGN)
2094
			$token->assignOp = $op;
2095
2096
		$token->start = $this->cursor;
2097
2098
		$token->value = $match[0];
2099
		$this->cursor += strlen($match[0]);
2100
2101
		$token->end = $this->cursor;
2102
		$token->lineno = $this->lineno;
2103
2104
		return $tt;
2105
	}
2106
2107
	public function unget()
2108
	{
2109
		if (++$this->lookahead == 4)
2110
			throw $this->newSyntaxError('PANIC: too much lookahead!');
2111
2112
		$this->tokenIndex = ($this->tokenIndex - 1) & 3;
2113
	}
2114
2115
	public function newSyntaxError($m)
2116
	{
2117
		return new Exception('Parse error: ' . $m . ' in file \'' . $this->filename . '\' on line ' . $this->lineno);
2118
	}
2119
2120
	public static function unicodeEscapeCallback($m)
2121
	{
2122
		return html_entity_decode('&#x' . $m[1]. ';', ENT_QUOTES, 'UTF-8');
2123
	}
2124
}
2125
2126
class JSToken
2127
{
2128
	public $type;
2129
	public $value;
2130
	public $start;
2131
	public $end;
2132
	public $lineno;
2133
	public $assignOp;
2134
}
2135