Unit test for #149.
[akelos.git] / lib / AkPhpParser.php
blob572ce184b460828a2fa210e1fb1347c77b568a22
1 <?php
2 /* vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4: */
4 // +----------------------------------------------------------------------+
5 // | Akelos Framework - http://www.akelos.org |
6 // +----------------------------------------------------------------------+
7 // | Copyright (c) 2002-2006, Akelos Media, S.L. & Bermi Ferrer Martinez |
8 // | Released under the GNU Lesser General Public License, see LICENSE.txt|
9 // +----------------------------------------------------------------------+
11 require_once(AK_VENDOR_DIR.DS.'pear'.DS.'PHP'.DS.'Compat'.DS.'Constant'.DS.'T.php');
13 /**
14 * @todo Avoid the ussage of globals in the PHP parser
17 /**
18 * This is a modified version of the pear/PHP_Shell package by Jan Kneschke
19 * and is used for validating PHP in the interactive shell
20 * before terminating execution with fatal errors.
22 * @package ActiveSupport
23 * @subpackage Console
24 * @author Jan Kneschke <jan@kneschke.de>
25 * @author Bermi Ferrer <bermi a.t akelos c.om>
26 * @copyright Copyright (c) 2002-2006, Akelos Media, S.L. http://www.akelos.org
27 * @license GNU Lesser General Public License <http://www.gnu.org/copyleft/lesser.html>
29 class AkPhpParser
31 var $errors = array();
32 var $code = '';
34 function AkPhpParser($code)
36 $this->code = trim($code);
38 /**
39 * parse the PHP code
41 * we parse before we eval() the code to
42 * - fetch fatal errors before they come up
43 * - know about where we have to wait for closing braces
45 * @return int 0 if a executable statement is in the code-buffer, non-zero otherwise
47 function parse()
50 $this->code = trim($this->code);
51 if (empty($this->code)){
52 return 1;
55 $t = token_get_all('<?php '.$this->code.' ?>');
57 $need_semicolon = 1; /* do we need a semicolon to complete the statement ? */
58 $need_return = 1; /* can we prepend a return to the eval-string ? */
59 $eval = ''; /* code to be eval()'ed later */
60 $braces = array(); /* to track if we need more closing braces */
62 $methods = array(); /* to track duplicate methods in a class declaration */
63 $ts = array(); /* tokens without whitespaces */
65 foreach ($t as $ndx => $token) {
66 if (is_array($token)) {
67 $ignore = 0;
69 switch($token[0]) {
70 case T_WHITESPACE:
71 case T_OPEN_TAG:
72 case T_CLOSE_TAG:
73 $ignore = 1;
74 break;
75 case T_FOREACH:
76 case T_DO:
77 case T_WHILE:
78 case T_FOR:
80 case T_IF:
81 case T_RETURN:
83 case T_CLASS:
84 case T_FUNCTION:
85 case T_INTERFACE:
87 case T_PRINT:
88 case T_ECHO:
90 case T_COMMENT:
91 case T_UNSET:
93 case T_INCLUDE:
94 case T_REQUIRE:
95 case T_INCLUDE_ONCE:
96 case T_REQUIRE_ONCE:
97 case T_TRY:
98 $need_return = 0;
99 break;
100 case T_VARIABLE:
101 case T_STRING:
102 case T_NEW:
103 case T_EXTENDS:
104 case T_IMPLEMENTS:
105 case T_OBJECT_OPERATOR:
106 case T_DOUBLE_COLON:
107 case T_INSTANCEOF:
109 case T_CATCH:
111 case T_ELSE:
112 case T_AS:
113 case T_LNUMBER:
114 case T_DNUMBER:
115 case T_CONSTANT_ENCAPSED_STRING:
116 case T_ENCAPSED_AND_WHITESPACE:
117 case T_CHARACTER:
118 case T_ARRAY:
119 case T_DOUBLE_ARROW:
121 case T_CONST:
122 case T_PUBLIC:
123 case T_PROTECTED:
124 case T_PRIVATE:
125 case T_ABSTRACT:
126 case T_STATIC:
127 case T_VAR:
129 case T_INC:
130 case T_DEC:
131 case T_SL:
132 case T_SL_EQUAL:
133 case T_SR:
134 case T_SR_EQUAL:
136 case T_IS_EQUAL:
137 case T_IS_IDENTICAL:
138 case T_IS_GREATER_OR_EQUAL:
139 case T_IS_SMALLER_OR_EQUAL:
141 case T_BOOLEAN_OR:
142 case T_LOGICAL_OR:
143 case T_BOOLEAN_AND:
144 case T_LOGICAL_AND:
145 case T_LOGICAL_XOR:
146 case T_MINUS_EQUAL:
147 case T_PLUS_EQUAL:
148 case T_MUL_EQUAL:
149 case T_DIV_EQUAL:
150 case T_MOD_EQUAL:
151 case T_XOR_EQUAL:
152 case T_AND_EQUAL:
153 case T_OR_EQUAL:
155 case T_FUNC_C:
156 case T_CLASS_C:
157 case T_LINE:
158 case T_FILE:
160 /* just go on */
161 break;
162 default:
163 /* debug unknown tags*/
164 error_log(sprintf("unknown tag: %d (%s): %s".PHP_EOL, $token[0], token_name($token[0]), $token[1]));
166 break;
168 if (!$ignore) {
169 $eval .= $token[1]." ";
170 $ts[] = array("token" => $token[0], "value" => $token[1]);
172 } else {
173 $ts[] = array("token" => $token, "value" => '');
175 $last = count($ts) - 1;
177 switch ($token) {
178 case '(':
179 /* walk backwards through the tokens */
181 if ($last >= 3 &&
182 $ts[$last - 1]['token'] == T_STRING &&
183 $ts[$last - 2]['token'] == T_OBJECT_OPERATOR &&
184 $ts[$last - 3]['token'] == T_VARIABLE ) {
186 /* $object->method( */
188 /* $object has to exist and has to be a object */
189 $objname = $ts[$last - 3]['value'];
191 if (!isset($GLOBALS[ltrim($objname, '$')])) {
192 $this->addError(sprintf('Variable \'%s\' is not set', $objname));
195 $k = ltrim($objname, '$');
197 if(isset($GLOBALS[$k])){
198 $object = $GLOBALS[$k];
200 if (!is_object($object)) {
201 $this->addError(sprintf('Variable \'%s\' is not a class', $objname));
204 $method = $ts[$last - 1]['value'];
206 /* obj */
208 if (!method_exists($object, $method)) {
209 $this->addError(sprintf("Variable %s (Class '%s') doesn't have a method named '%s'",
210 $objname, get_class($object), $method));
213 } else if ($last >= 3 &&
214 $ts[$last - 1]['token'] == T_VARIABLE &&
215 $ts[$last - 2]['token'] == T_OBJECT_OPERATOR &&
216 $ts[$last - 3]['token'] == T_VARIABLE ) {
218 /* $object->$method( */
220 /* $object has to exist and has to be a object */
221 $objname = $ts[$last - 3]['value'];
223 if (!isset($GLOBALS[ltrim($objname, '$')])) {
224 $this->addError(sprintf('Variable \'%s\' is not set', $objname));
226 $object = $GLOBALS[ltrim($objname, '$')];
228 if (!is_object($object)) {
229 $this->addError(sprintf('Variable \'%s\' is not a class', $objname));
232 $methodname = $ts[$last - 1]['value'];
234 if (!isset($GLOBALS[ltrim($methodname, '$')])) {
235 $this->addError(sprintf('Variable \'%s\' is not set', $methodname));
237 $method = $GLOBALS[ltrim($methodname, '$')];
239 /* obj */
241 if (!method_exists($object, $method)) {
242 $this->addError(sprintf("Variable %s (Class '%s') doesn't have a method named '%s'",
243 $objname, get_class($object), $method));
246 } else if ($last >= 6 &&
247 $ts[$last - 1]['token'] == T_STRING &&
248 $ts[$last - 2]['token'] == T_OBJECT_OPERATOR &&
249 $ts[$last - 3]['token'] == ']' &&
250 /* might be anything as index */
251 $ts[$last - 5]['token'] == '[' &&
252 $ts[$last - 6]['token'] == T_VARIABLE ) {
254 /* $object[...]->method( */
256 /* $object has to exist and has to be a object */
257 $objname = $ts[$last - 6]['value'];
259 if (!isset($GLOBALS[ltrim($objname, '$')])) {
260 $this->addError(sprintf('Variable \'%s\' is not set', $objname));
262 $array = $GLOBALS[ltrim($objname, '$')];
264 if (!is_array($array)) {
265 $this->addError(sprintf('Variable \'%s\' is not a array', $objname));
268 $andx = $ts[$last - 4]['value'];
270 if (!isset($array[$andx])) {
271 $this->addError(sprintf('%s[\'%s\'] is not set', $objname, $andx));
274 $object = $array[$andx];
276 if (!is_object($object)) {
277 $this->addError(sprintf('Variable \'%s\' is not a class', $objname));
280 $method = $ts[$last - 1]['value'];
282 /* obj */
284 if (!method_exists($object, $method)) {
285 $this->addError(sprintf("Variable %s (Class '%s') doesn't have a method named '%s'",
286 $objname, get_class($object), $method));
289 } else if ($last >= 3 &&
290 $ts[$last - 1]['token'] == T_STRING &&
291 $ts[$last - 2]['token'] == T_DOUBLE_COLON &&
292 $ts[$last - 3]['token'] == T_STRING ) {
294 /* Class::method() */
296 /* $object has to exist and has to be a object */
297 $classname = $ts[$last - 3]['value'];
299 if (!class_exists($classname)) {
300 $this->addError(sprintf('Class \'%s\' doesn\'t exist', $classname));
303 $method = $ts[$last - 1]['value'];
305 if (!empty($method) && !in_array($method, (array)get_class_methods($classname))) {
306 $this->addError(sprintf("Class '%s' doesn't have a method named '%s'",
307 $classname, $method));
309 } else if ($last >= 3 &&
310 $ts[$last - 1]['token'] == T_VARIABLE &&
311 $ts[$last - 2]['token'] == T_DOUBLE_COLON &&
312 $ts[$last - 3]['token'] == T_STRING ) {
314 /* Class::method() */
316 /* $object has to exist and has to be a object */
317 $classname = $ts[$last - 3]['value'];
319 if (!class_exists($classname)) {
320 $this->addError(sprintf('Class \'%s\' doesn\'t exist', $classname));
323 $methodname = $ts[$last - 1]['value'];
325 if (!isset($GLOBALS[ltrim($methodname, '$')])) {
326 $this->addError(sprintf('Variable \'%s\' is not set', $methodname));
328 $method = $GLOBALS[ltrim($methodname, '$')];
330 if (!in_array($method, get_class_methods($classname))) {
331 $this->addError(sprintf("Class '%s' doesn't have a method named '%s'",
332 $classname, $method));
335 } else if ($last >= 2 &&
336 $ts[$last - 1]['token'] == T_STRING &&
337 $ts[$last - 2]['token'] == T_NEW ) {
339 /* new Class() */
341 $classname = $ts[$last - 1]['value'];
343 if (!class_exists($classname)) {
344 $this->addError(sprintf('Class \'%s\' doesn\'t exist', $classname));
347 if(AK_PHP5){
348 $r = new ReflectionClass($classname);
350 if ($r->isAbstract()) {
351 $this->addError(sprintf("Can't instantiate abstract Class '%s'", $classname));
354 if (!$r->isInstantiable()) {
355 $this->addError(sprintf('Class \'%s\' can\'t be instantiated. Is the class abstract ?', $classname));
359 } else if ($last >= 2 &&
360 $ts[0]['token'] != T_CLASS &&
361 $ts[$last - 1]['token'] == T_STRING &&
362 $ts[$last - 2]['token'] == T_FUNCTION ) {
364 /* make sure we are not a in class definition */
366 /* function a() */
368 $func = $ts[$last - 1]['value'];
370 if (function_exists($func)) {
371 $this->addError(sprintf('Function \'%s\' is already defined', $func));
373 } else if ($last >= 4 &&
374 $ts[0]['token'] == T_CLASS &&
375 $ts[1]['token'] == T_STRING &&
376 $ts[$last - 1]['token'] == T_STRING &&
377 $ts[$last - 2]['token'] == T_FUNCTION ) {
379 /* make sure we are not a in class definition */
381 /* class a { .. function a() ... } */
383 $func = $ts[$last - 1]['value'];
384 $classname = $ts[1]['value'];
386 if (isset($methods[$func])) {
387 $this->addError(sprintf("Can't redeclare method '%s' in Class '%s'", $func, $classname));
390 $methods[$func] = 1;
392 } else if ($last >= 1 &&
393 $ts[$last - 1]['token'] == T_STRING ) {
394 /* func() */
395 $funcname = $ts[$last - 1]['value'];
397 if (!function_exists($funcname)) {
398 $this->addError(sprintf("Function %s() doesn't exist", $funcname));
400 } else if ($last >= 1 &&
401 $ts[$last - 1]['token'] == T_VARIABLE ) {
403 /* $object has to exist and has to be a object */
404 $funcname = $ts[$last - 1]['value'];
406 if (!isset($GLOBALS[ltrim($funcname, '$')])) {
407 $this->addError(sprintf('Variable \'%s\' is not set', $funcname));
409 $k = ltrim($funcname, '$');
411 if(isset($GLOBALS[$k])){
412 $func = $GLOBALS[$k];
414 if (!function_exists($func)) {
415 $this->addError(sprintf("Function %s() doesn't exist", $func));
421 array_push($braces, $token);
422 break;
423 case '{':
424 $need_return = 0;
426 if ($last >= 2 &&
427 $ts[$last - 1]['token'] == T_STRING &&
428 $ts[$last - 2]['token'] == T_CLASS ) {
430 /* class name { */
432 $classname = $ts[$last - 1]['value'];
434 if (class_exists($classname)) {
435 $this->addError(sprintf("Class '%s' can't be redeclared", $classname));
437 } else if ($last >= 4 &&
438 $ts[$last - 1]['token'] == T_STRING &&
439 $ts[$last - 2]['token'] == T_EXTENDS &&
440 $ts[$last - 3]['token'] == T_STRING &&
441 $ts[$last - 4]['token'] == T_CLASS ) {
443 /* class classname extends classname { */
445 $classname = $ts[$last - 3]['value'];
446 $extendsname = $ts[$last - 1]['value'];
448 if (class_exists($classname, false)) {
449 $this->addError(sprintf("Class '%s' can't be redeclared",
450 $classname));
452 if (!class_exists($extendsname, false)) {
453 $this->addError(sprintf("Can't extend '%s' from not existing Class '%s'",
454 $classname, $extendsname));
456 } else if ($last >= 4 &&
457 $ts[$last - 1]['token'] == T_STRING &&
458 $ts[$last - 2]['token'] == T_IMPLEMENTS &&
459 $ts[$last - 3]['token'] == T_STRING &&
460 $ts[$last - 4]['token'] == T_CLASS ) {
462 /* class name implements interface { */
464 $classname = $ts[$last - 3]['value'];
465 $implements = $ts[$last - 1]['value'];
467 if (class_exists($classname, false)) {
468 $this->addError(sprintf("Class '%s' can't be redeclared",
469 $classname));
471 if (!interface_exists($implements, false)) {
472 $this->addError(sprintf("Can't implement not existing Interface '%s' for Class '%s'",
473 $implements, $classname));
477 array_push($braces, $token);
478 break;
479 case '}':
480 $need_return = 0;
481 case ')':
482 array_pop($braces);
483 break;
486 $eval .= $token;
490 $last = count($ts) - 1;
491 if ($last >= 2 &&
492 $ts[$last - 0]['token'] == T_STRING &&
493 $ts[$last - 1]['token'] == T_DOUBLE_COLON &&
494 $ts[$last - 2]['token'] == T_STRING ) {
496 /* Class::constant */
498 /* $object has to exist and has to be a object */
499 $classname = $ts[$last - 2]['value'];
501 if (!class_exists($classname)) {
502 $this->addError(sprintf('Class \'%s\' doesn\'t exist', $classname));
505 $constname = $ts[$last - 0]['value'];
507 if(AK_PHP5){
508 $c = new ReflectionClass($classname);
509 if (!$c->hasConstant($constname)) {
510 $this->addError(sprintf("Class '%s' doesn't have a constant named '%s'",
511 $classname, $constname));
514 } else if ($last == 0 &&
515 $ts[$last - 0]['token'] == T_VARIABLE ) {
517 /* $var */
519 $varname = $ts[$last - 0]['value'];
521 if (!isset($GLOBALS[ltrim($varname, '$')])) {
522 $this->addError(sprintf('Variable \'%s\' is not set', $varname));
527 $need_more = count($braces);
529 if ($need_more || ';' === $token) {
530 $need_semicolon = 0;
533 if ($need_return) {
534 $eval = "return ".$eval;
537 /* add a traling ; if necessary */
538 if ($need_semicolon){
539 $eval .= ';';
542 if (!$need_more) {
543 $this->code = $eval;
546 return $need_more;
549 function addError($error)
551 $this->errors[$error] = '';
554 function hasErrors()
556 return !empty($this->errors);
559 function getErrors()
561 return array_keys($this->errors);