public function generateFileReport(XRef_IParsedFile $pf) { if ($pf->getFileType() != $this->supportedFileType) { return; } $this->lintEngine->addParsedFile($pf); }
public function getReport(XRef_IParsedFile $pf) { if ($pf->getFileType() != $this->supportedFileType) { return; } $this->report = array(); $tokens = $pf->getTokens(); if (!$tokens) { return; // skip empty php files } $skip_closing_tag_checks = false; // do we have text before the opening tag? $t = $tokens[0]; if ($t->kind != T_OPEN_TAG) { if ($t->kind != T_INLINE_HTML) { throw new XRef_ParseException($t); } // is there are spaces only or meaningful html? // regexp = optional byte order mark in the beginning + (spaces or nbsp)+ $regexp = "#^(\\xEF\\xBB\\xBF|\\xFE\\xFF|\\xFF\\xFE|\\x00\\x00\\xFE\\xFF|\\xFF\\xFE\\x00\\x00)?[\\s\\xa0]*\$#"; if (preg_match($regexp, $t->text)) { $this->addDefect($t, self::E_EXTRA_SPACES); } else { $skip_closing_tag_checks = true; } } if (!$skip_closing_tag_checks) { $count = count($tokens); for ($i = 0; $i < $count; ++$i) { $t = $tokens[$i]; // if there is only one closing tag, and there is nothing after it // (except maybe a space-only T_INLINE_HTML), report an error if ($t->kind == T_CLOSE_TAG) { if ($i == $count - 1) { $this->addDefect($t, self::E_CLOSING_TAG); } elseif ($i == $count - 2) { $n = $tokens[$count - 1]; if (preg_match('#^[\\s+\\xa0]*$#', $n->text)) { $this->addDefect($t, self::E_CLOSING_TAG); } } break; } } } return $this->report; }
public function generateFileReport(XRef_IParsedFile $pf) { if ($pf->getFileType() != $this->supportedFileType) { return; } $tokens = $pf->getTokens(); // Property declared: // (public|protected|private) $foo; for ($i = 0; $i < count($tokens); ++$i) { $t = $tokens[$i]; if ($t->kind == T_PUBLIC || $t->kind == T_PROTECTED || $t->kind == T_PRIVATE) { $n = $t->nextNS(); if ($n->kind == T_VARIABLE) { $name = $n->text; if (substr($name, 0, 1) == '$') { $name = substr($name, 1); } else { error_log("Strange property name: {$name}"); } $p = $this->getOrCreate($name); $filePos = new XRef_FilePosition($pf, $n->index); $p->declaredAt[] = $filePos; $this->xref->addSourceFileLink($filePos, $this->reportId, $name); } } } // property used: // $foo->bar->baz for ($i = 0; $i < count($tokens); ++$i) { $t = $tokens[$i]; if ($t->kind == T_OBJECT_OPERATOR) { $t = $t->nextNS(); $n = $t->nextNS(); if ($n->kind == XRef::T_ONE_CHAR && $n->text == '(') { // method call: $foo->bar() continue; } $name = $t->text; $p = $this->getOrCreate($name); $filePos = new XRef_FilePosition($pf, $t->index); $p->usedAt[] = $filePos; // link from source file HTML page to report page "reportId/objectId" $this->xref->addSourceFileLink($filePos, $this->reportId, $name); continue; } } }
public function generateFileReport(XRef_IParsedFile $pf) { if ($pf->getFileType() != $this->supportedFileType) { return; } // collect all declared methods $pf_methods = $pf->getMethods(); foreach ($pf_methods as $pfm) { if ($pfm->name) { $m = $this->getOrCreate($pfm->name); $definedAt = new XRef_FilePosition($pf, $pfm->nameIndex); $m->definedAt[] = $definedAt; // link from source file HTML page to report page "reportId/objectId" $this->xref->addSourceFileLink($definedAt, $this->reportId, $m->id, true); } } // collect all method calls $tokens = $pf->getTokens(); for ($i = 0; $i < count($tokens); ++$i) { $t = $tokens[$i]; // find something like: // <name> "(" // and not like // "new" <name> "(" // "function" <name> "(" // "function" ("get"|"set") <name> "(" // AS3 declaration of getter/setter function if ($t->kind == T_STRING) { $n = $t->nextNS(); if ($n != null && $n->kind == XRef::T_ONE_CHAR && $n->text == "(") { $p = $t->prevNS(); if ($p != null && $p->kind == XRef::T_ONE_CHAR && $p->text == '&') { // PHP - function &foo( $p = $p->prevNS(); } if ($p->kind != T_NEW && $p->kind != T_FUNCTION && $p->kind != XRef::T_GET && $p->kind != XRef::T_SET || $p == null) { $m = $this->getOrCreate($t->text); $calledFrom = new XRef_FilePosition($pf, $t->index); $m->calledFrom[] = $calledFrom; // link from source file HTML page to report page "reportId/objectId" $this->xref->addSourceFileLink($calledFrom, $this->reportId, $m->id); } } } } }
public function createFileSlice(XRef_IParsedFile $pf, $is_library_file = false) { // names of classes that has constructors and don't call parent class constructors // array of array(class_name, line_number); $slice = array(); foreach ($pf->getClasses() as $class) { foreach ($class->methods as $method) { if (strtolower($method->name) == '__construct' && $method->bodyStarts > 0) { // ok, this class has constructor $does_call_parent_constructor = false; $t = $pf->getTokenAt($method->bodyStarts); while ($t->index < $method->bodyEnds) { if ($t->text == 'parent') { $t = $t->nextNS(); if ($t->kind == T_DOUBLE_COLON) { $t = $t->nextNS(); if ($t->text == '__construct') { $t = $t->nextNS(); if ($t->text == '(') { // ok, this is a call for the parent class constructor $does_call_parent_constructor = true; break; } } } } $t = $t->nextNS(); } if (!$does_call_parent_constructor) { // this is suspicious and, // if there is a parent class with constructor, error-prone $slice[] = array($class->name, $class->lineNumber); } break; } } } return $slice; }
/** * Function to summarize content of parsed file. * The result will be added to database by addFileSlice() method * and can be serialized/stored meanwhile. * * @param XRef_ParsedFile $pf * @param bool $is_library_file * @return array file summary, */ public function createFileSlice(XRef_IParsedFile $pf, $is_library_file = false) { // TODO: add constants // filter functions that are not methods and not closures $functions = array(); foreach ($pf->getMethods() as $m) { if (!$m->className && $m->name) { $functions[] = $m; } } // check if function/method calls // - call_user_func()/call_user_func_array() // (then can't reliable say if child class calls constructor of its base class) // - func_get_args()/func_num_args() // (then can't tell the number of arguments) foreach ($pf->getMethods() as $m) { if ($m->bodyStarts > 0) { $t = $pf->getTokenAt($m->bodyStarts); // TODO: remove all token iteration; use a real parser that returns AST while ($t->index < $m->bodyEnds) { if ($t->kind == T_STRING) { $text = $t->text; $t = $t->nextNS(); if ($t->text == '(') { if ($text == 'call_user_func' || $text == 'call_user_func_array') { $m->flags |= self::FLAG_CALLS_USER_FUNC; } elseif ($text == 'func_get_args' || $text == 'func_get_arg' || $text == 'func_num_args') { $m->flags |= self::FLAG_CALLS_GET_ARGS; } } } $t = $t->nextNS(); } } } $slice = array("classes" => $pf->getClasses(), "functions" => $functions); return $slice; }
public function getReport(XRef_IParsedFile $pf) { if ($pf->getFileType() != $this->supportedFileType) { return; } $this->report = array(); $tokens = $pf->getTokens(); $count = count($tokens); for ($i = 0; $i < $count; ++$i) { $t = $tokens[$i]; // warnings: // if ($foo = $bar) ; // if ($bar = null) ; // if ($baz = 25 && $qux == 30) ; // ok: // if ($fh = fopen($file, "w")) ; // if ($a = $foo->next() ) ; if ($t->kind == T_IF || $t->kind == T_ELSEIF) { $n = $t->nextNS(); if ($n->text != '(') { throw new XRef_ParseException($n); } $last_index = $pf->getIndexOfPairedBracket($n->index); while ($n->index < $last_index) { $n = $n->nextNS(); // skip function/method calls inside 'if' statements // if (someFunc($paramName = value)) ... if ($n->text == '(') { $p = $n->prevNS(); if ($p->kind == T_STRING) { $n = $pf->getTokenAt($pf->getIndexOfPairedBracket($n->index)); continue; } } if ($n->text == '=') { $n = $n->nextNS(); $nn = $n->nextNS(); if ($nn->text == ')' || $nn->kind == T_BOOLEAN_AND || $nn->kind == T_BOOLEAN_OR) { $this->addDefect($n, self::E_ASSIGNMENT_IN_CONDITION); } } } $i = $last_index; continue; } } return $this->report; }
public function getReport(XRef_IParsedFile $pf) { if ($pf->getFileType() != $this->supportedFileType) { return; } $tokens = $pf->getTokens(); $tokens_count = count($tokens); // initialization/clean-up after previous parsed file, if any $this->listOfScopes = array(); $this->listOfCommands = array(); $this->report = array(); // create a global scope ... $global_scope = new XRef_Lint_UninitializedVars_Scope(0, $tokens_count); $global_scope->mode = self::MODE_RELAXED; $global_scope->isGlobal = true; $this->listOfScopes[] = $global_scope; $this->currentScope = $global_scope; foreach (self::$knownGlobals as $var_name) { $var = $global_scope->getOrCreateVarByName($var_name); $var->state = self::VAR_ASSIGNED; } // and create a separate scope for each function/method/closure foreach ($pf->getMethods() as $method) { // if the method/function has a body, create a scope if ($method->bodyStarts) { $scope = new XRef_Lint_UninitializedVars_Scope($method->bodyStarts, $method->bodyEnds); $this->listOfScopes[] = $scope; // copy function parameters to list of known variables of this scope foreach ($method->parameters as $param) { $var = $scope->getOrCreateVarByName($param->name); $var->status = self::VAR_ASSIGNED; } foreach ($method->usedVariables as $param) { $var = $scope->getOrCreateVarByName($param->name); $var->status = self::VAR_ASSIGNED; } // commands to switch the scope $this->addCommand(array($method->bodyStarts + 1, self::CMD_CHANGE_CURRENT_SCOPE)); $this->addCommand(array($method->bodyEnds + 1, self::CMD_CHANGE_CURRENT_SCOPE)); // special command to check used variables of a closure // right before T_FUNCTION token of the closure starts if ($method->usedVariables) { $this->addCommand(array($method->index - 1, self::CMD_CHECK_USED_VARIABLES, $method)); } } // for all method declaration/definitions, skip the part // "function" <name> (argument list...) from checks $skip_to_index = $method->bodyStarts ? $method->bodyStarts : $method->bodyEnds; $this->addCommand(array($method->index, self::CMD_SKIP_TO_TOKEN, $skip_to_index)); } // add functions of the current file - we'll use their signatures to find functions/methods // that can initialize passed-by-reference variables $this->functionsOfCurrentFile = array(); foreach ($pf->getMethods() as $function) { if ($function->name) { $full_name = $function->className ? $function->className . '::' . $function->name : $function->name; $this->functionsOfCurrentFile[strtolower($full_name)] = $function; } } // skip the content of class body from checks; check method bodies only // add commands: // skip from class start to start of body of first method // skip from end of body of first method to start of body of second method // ... // skip from end of the body of the last method till the end of the class foreach ($pf->getClasses() as $class) { // hm, there are no class declaration without a definition in PHP, right? if ($class->bodyStarts) { $start_skip_from = $class->index; foreach ($class->methods as $method) { if ($method->bodyStarts) { $this->addCommand(array($start_skip_from, self::CMD_SKIP_TO_TOKEN, $method->bodyStarts)); $start_skip_from = $method->bodyEnds; } } $this->addCommand(array($start_skip_from, self::CMD_SKIP_TO_TOKEN, $class->bodyEnds)); } } // to check or not to check variables in the global scope // variables in local scope (inside functions) will always be checked $checkGlobalScope = XRef::getConfigValue("lint.check-global-scope", true); for ($i = 0; $i < $tokens_count; ++$i) { $t = $tokens[$i]; // skip whitespaces but not doc comments (we'll process them later) // TODO: move doc comments parse code into XRef_ParsedFile (?) if ($t->kind == T_WHITESPACE || $t->kind == T_COMMENT) { continue; } // process the command(s) if ($this->listOfCommands && $this->listOfCommands[0][0] <= $i) { $cmd = array_shift($this->listOfCommands); switch ($cmd[1]) { case self::CMD_CHANGE_CURRENT_SCOPE: // there can be nested scopes, find the deepest one foreach ($this->listOfScopes as $scope) { if ($scope->start > $i) { break; } if ($scope->end > $i) { $this->currentScope = $scope; } } break; case self::CMD_SKIP_TO_TOKEN: if ($cmd[2] > $i) { $i = $cmd[2]; } break; case self::CMD_CHECK_USED_VARIABLES: $closure = $cmd[2]; foreach ($closure->usedVariables as $arg) { // check that this variable exists at outer scope // however, if it's passed by reference, it can be created by the closure: // $foo = function () using (&$x) { $x = 1; }; // $foo(); // now we have $x here $token = $pf->getTokenAt($arg->index); if ($arg->isPassedByReference) { $var = $this->currentScope->getOrCreateVar($token); $var->status = self::VAR_ASSIGNED; // not quite true - } else { $this->checkVarAndAddDefectIfMissing($token); } } break; case self::CMD_START_LOOP: $this->currentScope->isLoopMode = true; break; case self::CMD_END_LOOP: $this->currentScope->isLoopMode = false; foreach ($this->currentScope->loopVars as $variable_name => $v) { list($token, $error_code) = $v; $this->checkVarAndAddDefectIfMissing($token, $error_code); } $this->currentScope->loopVars = array(); break; case self::CMD_SWITCH_TO_RELAXED_MODE: $this->currentScope->mode = self::MODE_RELAXED; break; default: // shouldn't be here throw new XRef_ParseException($t); } // executing a command can change the current pointer, redo the loop // and maybe execute another command // however, don't increment $i --$i; continue; } // // Switch from strict mode to relaxed? // // use of extract() or $$foo notation // trick is: the mode should be switched AFTER the statement, e.g. // function foo() { extract($foo); echo $bar; } // $foo must be declared (still in strict scope); $bar - not (in relaxed mode) if ($this->currentScope->mode == self::MODE_STRICT) { // use of extract() if ($t->kind == T_STRING && $t->text == 'extract') { $n = $t->nextNS(); // next non-space token if ($n->text == '(') { $this->addCommand(array($pf->getIndexOfPairedBracket($n->index), self::CMD_SWITCH_TO_RELAXED_MODE, $t)); $this->addDefect($t, self::E_LOSS_OF_STRICT_MODE); continue; } } // use of eval(); if ($t->kind == T_EVAL) { $n = self::skipTillText($t, ';'); $this->addCommand(array($n->index, self::CMD_SWITCH_TO_RELAXED_MODE, $t)); $this->addDefect($t, self::E_LOSS_OF_STRICT_MODE); continue; } // $$var notation in assignment. // Non-assignment (read) operations doesn't cause mode switch // $$foo = // $$bar["baz"] = // TODO: other forms of assignment? $$foo++; ? if ($t->text == '$') { $n = $t->nextNS(); // next non-space token if ($n->kind == T_VARIABLE) { $nn = $n->nextNS(); while ($nn->text == '[') { // quick forward to closing ']' $nn = $pf->getTokenAt($pf->getIndexOfPairedBracket($nn->index)); $nn = $nn->nextNS(); } if ($nn->text == '=') { $s = self::skipTillText($n, ';'); // find the end of the statement $this->addCommand(array($s->index, self::CMD_SWITCH_TO_RELAXED_MODE, $n)); $this->addDefect($t, self::E_LOSS_OF_STRICT_MODE); } } } // include/require statements // if you use them inside functions, well, it's impossible to make any assertions about your code. if ($t->kind == T_INCLUDE || $t->kind == T_REQUIRE || $t->kind == T_INCLUDE_ONCE || $t->kind == T_REQUIRE_ONCE) { $s = self::skipTillText($t, ';'); // find the end of the statement if ($s) { $this->addCommand(array($s->index, self::CMD_SWITCH_TO_RELAXED_MODE, $t)); $this->addDefect($t, self::E_LOSS_OF_STRICT_MODE); } } } // loops: // in loop, a variable can be tested first, and then assigned and this in not an error: // foreach // on parsing this line, $this->loop_starts_at will be set // ($items as $item) // { // here $this->loop_ends_at will be set as marker // // that we are inside of the loop // if (isset($total)) { // $total += $item->cost; // } else { // $total = $item->cost; // } // } // here the loop scope will be dropped and all // // missing vars will be added to report // if (!$this->currentScope->isLoopMode) { // start a new loop scope? // for, foreach, while, do ... while if ($t->kind == T_FOR || $t->kind == T_FOREACH || $t->kind == T_DO || $t->kind == T_WHILE) { $n = $t->nextNS(); // skip condition, may be missing if token is T_DO if ($n->text == '(') { $n = $pf->getTokenAt($pf->getIndexOfPairedBracket($n->index)); $n = $n->nextNS(); } // is there a block or a single statement as loop's body? if ($n->text == '{') { $this->addCommand(array($n->index, self::CMD_START_LOOP)); $this->addCommand(array($pf->getIndexOfPairedBracket($n->index), self::CMD_END_LOOP)); } } } // // Part 1. // // Find "known" (declared or assigned) variables. // Variable is "known" in following cases: // 1. value is assigned to the variable: $foo = expr // 2. loop var: foreach (array() as $foo) // 3. parameter of a function: function bar($foo) // 4. catch(Exception $err) // 5. Array autovivification: $foo['index'] // 6. Scalar autovivification: $count++, $text .= // 7. superglobals // 8. list($foo) = array(); // 9. globals: global $foo; // 10. functions that modify arguments: // int preg_match ( string $pattern , string $subject [, array &$matches ...]) // 11. test for existence of var in "relaxed" mode: isset($foo), empty($bar) // 12. variables declared via annotations: @var ClassName $varName (relaxed mode only) // $foo = // $foo[...] = // $foo[...][...] = // $foo++ // $foo .= // exclude class variables: public $foo = 1; // special case: allow declarations of variables with undefined value: $foo; if ($t->kind == T_VARIABLE) { // skip variables declaration in classes // public $foo; // private static $bar; // var $baz; if ($pf->getClassAt($t->index) != null && $pf->getMethodAt($t->index) == null) { // shouldn't be here // remove this once it passes the tests throw new XRef_ParseException($t); } $n = $t->nextNS(); // next non-space token $p = $t->prevNS(); // prev non-space token // skip static class variables: // Foo::$bar, self::$foo if ($p->kind == T_DOUBLE_COLON) { continue; } $is_array = false; while ($n->text == '[') { // quick forward to closing ']' $n = $pf->getTokenAt($pf->getIndexOfPairedBracket($n->index)); $n = $n->nextNS(); $is_array = true; } if ($n->text == '=') { if ($is_array && !$this->currentScope->checkVar($t)) { // array autovivification? $this->checkVarAndAddDefectIfMissing($t, self::E_ARRAY_AUTOVIVIFICATION); } else { $var = $this->currentScope->getOrCreateVar($t); $var->status = self::VAR_ASSIGNED; } continue; } if ($n->kind == T_INC || $n->kind == T_DEC || $p->kind == T_INC || $p->kind == T_DEC || $n->kind == T_CONCAT_EQUAL || $n->kind == T_PLUS_EQUAL) { $error_code = $is_array ? self::E_ARRAY_AUTOVIVIFICATION : self::E_SCALAR_AUTOVIVIFICATION; $this->checkVarAndAddDefectIfMissing($t, $error_code); continue; } if ($n->text == ';' && !$is_array) { if ($p && ($p->text == ';' || $p->text == '{')) { $this->addDefect($t, self::E_EMPTY_STATEMENT); $var = $this->currentScope->getOrCreateVar($t); $var->status = self::VAR_ASSIGNED; continue; } } } // foreach (expr as $foo) // foreach (expr as $foo => & $var) if ($t->kind == T_FOREACH) { $n = $t->nextNS(); while ($n->kind != T_AS) { $n = $n->nextNS(); } $nn = $n->nextNS(); if ($nn->text == '&') { $nn = $nn->nextNS(); } $var = $this->currentScope->getOrCreateVar($nn); $var->status = self::VAR_ASSIGNED; $n = $nn->nextNS(); if ($n->kind == T_DOUBLE_ARROW) { $nn = $n->nextNS(); if ($nn->text == '&') { $nn = $nn->nextNS(); } $var = $this->currentScope->getOrCreateVar($nn); $var->status = self::VAR_ASSIGNED; $n = $nn->nextNS(); } if ($n->text == ")") { // ok } else { // PHP code generated by smarty: // foreach ($_from as $this->_tpl_vars['event']): } // TODO: can't skip to ")" of foreach(expr as ...), because expr will be unparsed // TODO: loop vars will be scanned again and counted as used even if they are not continue; } // function &asdf($foo, $bar = array()) // function asdf(&$foo); // function asdf(Foo $foo); // function ($x) use ($y) ... // here a new scope frame is created if ($t->kind == T_FUNCTION) { // shouldn't be here // function declaration parst should be skipped by CMD_SKIP_TO throw new XRef_ParseException($t); } // catch (Exception $foo) if ($t->kind == T_CATCH) { $n = $t->nextNS(); if ($n->text != '(') { throw new Exception("{$n} found instead of '('"); } $n = $n->nextNS(); list($type, $n) = self::parseType($n); if (!$type) { throw new Exception("No exception type found ({$n})"); } if ($n->kind == T_VARIABLE) { $var = $this->currentScope->getOrCreateVar($n); $var->isCatchVar = true; } else { throw new Exception("{$n} found instead of variable"); } $n = $n->nextNS(); // if ($n->text != ')') { throw new Exception("{$n} found instead of ')'"); } $i = $n->index; continue; } // list($a, $b) = ... // TODO: check that the list is used in the left side of the assinment operator // TODO: example from PHP documentation: list($a, list($b, $c)) = array(1, array(2, 3)); if ($t->kind == T_LIST) { $n = $t->nextNS(); if (!$n->text == "(") { throw new Exception("Invalid list declaration found: {$t}"); } $closingBraketIndex = $pf->getIndexOfPairedBracket($n->index); while ($n->index != $closingBraketIndex) { if ($n->kind == T_VARIABLE) { $this->currentScope->getOrCreateVar($n); } $n = $n->nextNS(); } $i = $n->index; continue; } // globals: // global $foo; // makes the variable $foo known // global $$bar; // uh-oh, the var $bar must be known and relaxed mode afterwards // // TODO: check that the variable does exist at global level // TODO: link this var to the var at global level if ($t->kind == T_GLOBAL) { $n = $t->nextNS(); while (true) { if ($n->kind == T_VARIABLE) { $var = $this->currentScope->getOrCreateVar($n); $var->isGlobal = true; $var->status = self::VAR_ASSIGNED; $n = $n->nextNS(); } elseif ($n->text == '$') { $n = $n->nextNS(); if ($n->kind == T_VARIABLE) { // check that this var is declared $this->checkVarAndAddDefectIfMissing($n); // turn the relaxed mode on beginning of the next statement $s = self::skipTillText($n, ';'); $token_caused_mode_switch = $n; $switch_to_relaxed_scope_at = $s->index; } else { throw new Exception("Invalid 'global' decalaraion found: {$nn}"); } $n = $n->nextNS(); } else { throw new Exception("Invalid 'global' decalaraion found: {$n}"); } if ($n->text == ',') { $n = $n->nextNS(); continue; // next variable in list } elseif ($n->text == ';') { break; // end of list } else { throw new Exception("Invalid 'global' declaration found: {$n}"); } } $i = $n->index; continue; } // static function variables // function foo() { // static $foo; // <-- well, strictly speaking this variable is not intilialized, // static $bar = 10, $baz; // but it's declared so let's hope that author knows what's going on // } // other usage of "static" keyword: // $foo = new static(); // $foo = new static; // $foo instanceof staic // $foo = static::methodName(); if ($t->kind == T_STATIC && $pf->getMethodAt($t->index) != null) { $n = $t->nextNS(); $p = $t->prevNS(); if ($n->kind != T_DOUBLE_COLON && $p->kind != T_NEW && $p->kind != T_INSTANCEOF) { $list = $pf->extractList($n, ',', ';'); foreach ($list as $n) { if ($n->kind == T_VARIABLE) { $var = $this->currentScope->getOrCreateVar($n); $var->status = self::VAR_ASSIGNED; $i = $n->index; } else { // oops? throw new Exception("Invalid 'static' decalaraion found: {$n}"); } } continue; } } // // Functions that can return values into passed-by-reference arguments, // e.g. preg_match, preg_match_all etc. // // foo($x); // Foo::foo($x); // $foo->foo($x); // // Checks for known functions: // 1. if function doesn't accept parameters by reference ( not function foo(&$x) ) // and can't therefore initialize a passed variable, check that the variable exists, // otherwise, report an error // 2. if function does accept &$vars, check that variable, not an expression is // actually passed // 3. if function does accept params-by-reference, but does not intialize them // (e.g. sort()), check that variable exists // // Unknown (user-defined) functions can accept vars by reference too, // but we don't know about them, so just produce a warning // // Summary: // known_function_that_assign_variable($unknown_var); // ok (processed here) // known_function_that_doesnt_assign_variable($unknown_var); // error/warning (processed later) // unknown_function($unknown_var); // warning (here) // unknown_function($unknown_var_in_expression*2); // error/warning (later) // if ($t->kind == T_STRING) { $n = $t->nextNS(); if ($n->text == '(') { $function = $this->getFunctionAtToken($pf, $t); $arguments = $pf->extractList($n->nextNS()); if ($function) { // For known functions: // - mark variables that are used as passed-by-reference return arguments as known // - do nothing with variables that are not returned by function - they will be checked later $function_name = $function->className ? $function->className . '::' . $function->name : $function->name; foreach ($function->parameters as $pos => $param) { if ($pos < count($arguments) && $param->isPassedByReference) { $n = $arguments[$pos]; if ($n->text == '&') { $n = $n->nextNS(); } if ($n->kind == T_VARIABLE) { if (isset(self::$internalFunctionsThatDoesntInitializePassedByReferenceParams[$function_name])) { // if the function takes parameters by reference, but they must be defined prior to that // (e.g. sort), than check that this var exists $this->checkVarAndAddDefectIfMissing($n); } else { // otherwise, just note that this var will be initialized by this method call $var = $this->currentScope->getOrCreateVar($n); $var->status = self::VAR_ASSIGNED; } } else { // warn about non-variable being passed by reference // allow static class variable: Foo::$bar, \Foo\Bar::$baz $is_class_variable = false; if ($n->kind == T_STRING || $n->kind == T_NS_SEPARATOR) { list($type, $t) = self::parseType($n); if ($t->kind == T_DOUBLE_COLON && $t->nextNS()->kind == T_VARIABLE) { $is_class_variable = true; } } if (!$is_class_variable) { $this->addDefect($n, self::E_NON_VAR_PASSED_BY_REF); } } } } } else { // For unknown functions: // If argument look like a single variable (not a part of a complex expression), // it too can be passed/returned/initialized by function. // Issue a warning if this variable is not known foreach ($arguments as $n) { if ($n->text == '&') { $n = $n->nextNS(); } if ($n->kind == T_VARIABLE) { $nn = $n->nextNS(); if ($nn->text == ',' || $nn->text == ')') { // single variable - check that it exists but if not issue a warning only, // cause it can be initialized by this unknown functiton $this->checkVarAndAddDefectIfMissing($n, self::E_UNKNOWN_VAR_ARGUMENT); } } } } } continue; } // test for variable in relaxed mode only: // if (isset($variable)) ... // this makes $variable "known" in relaxed mode // if (!empty($variable)) ... // No expressions as function argument: // isset( $foo["bar"] ); // doesn't make $foo "declared", it must exist or this is an error if ($t->kind == T_ISSET || $t->kind == T_EMPTY) { $n = $t->nextNS(); if ($n && $n->text == '(') { $nn = $n->nextNS(); if ($nn && $nn->kind == T_VARIABLE) { $nnn = $nn->nextNS(); if ($nnn && $nnn->text == ')') { // ok, this is a simple expression with a variable inside function call if ($this->currentScope->mode == self::MODE_RELAXED) { // mark this variable as "known" in relaxed mode $var = $this->currentScope->getOrCreateVar($nn); $var->status = self::VAR_ASSIGNED; } else { // skip till the end of statement in strict mode $i = $nnn->index; continue; } } } } continue; } // doc comment (/** */) annotations // 1. Type info about variables (/** @var Foo $bar */) // 2. Variable declaration in relaxed mode (/** @var $foo */) if ($t->kind == T_DOC_COMMENT) { $variables_list = self::parseDocComment($t->text); $is_relaxed_mode = $this->currentScope->mode == self::MODE_RELAXED; foreach ($variables_list as $var_name => $var_type) { if ($var_type) { $this->currentScope->setVarType($var_name, $var_type); } if ($is_relaxed_mode) { $this->currentScope->getOrCreateVarByName($var_name); } } continue; } // eval: a common pattern: // eval('$foo = ...'); // eval "\$foo = ...'; if ($t->kind == T_EVAL) { $n = $t->nextNS(); if ($n->text == '(') { $n = $n->nextNS(); } if ($n->kind == T_CONSTANT_ENCAPSED_STRING) { if (preg_match('#^\'\\s*(\\$\\w+)\\s*=#', $n->text, $matches) || preg_match('#^"\\s*\\\\(\\$\\w+)\\s*=#', $n->text, $matches)) { $this->currentScope->getOrCreateVarByName($matches[1]); } } continue; } // Part 2. // Check if a variable is defined // if ($t->kind == T_VARIABLE) { $skipVariable = false; // skip class static variables: // Foo::$foo // TODO: check that this class variable is really declared $p = $t->prevNS(); if ($p->kind == T_DOUBLE_COLON) { $skipVariable = true; } // skip variables in the global scope, because it's often polluted by vars // included from included/required files if ($checkGlobalScope == false && $this->currentScope->isGlobal) { $skipVariable = true; } if (!$skipVariable) { $this->checkVarAndAddDefectIfMissing($t); } } } // end of "for each token" loop return $this->report; }
public function getReport(XRef_IParsedFile $pf) { if ($pf->getFileType() != $this->supportedFileType) { return; } $this->report = array(); // config_constants are initialized here and not in constructor only for // unittest. TODO: make unittest reload plugins after config changes $this->config_constants = array(); foreach (XRef::getConfigValue("lint.add-constant", array()) as $const_name) { $this->config_constants[$const_name] = true; } // lower/mixed-case string literals: // for (foo=0; $foo<10; ) // $bar[x] - is it $bar['x'] or $bar[$x] ? $seen_strings = array(); // report every literal once per file, don't be noisy $global_constants = array(); // list of all constants defined in global space in this file $class_constants = array(); // list of all class constants names defined in this file // list of token indexes that contains allowed string literals $ignore_tokens = array(); foreach ($pf->getConstants() as $c) { if ($c->className) { $class_constants[$c->name] = 1; } else { $global_constants[$c->name] = 1; // TODO: this is a namespaced name } $ignore_tokens[$c->index] = 1; // don't parse/analyze this token } $tokens = $pf->getTokens(); $tokens_count = count($tokens); $is_inside_interpolated_string = false; for ($i = 0; $i < $tokens_count; ++$i) { $t = $tokens[$i]; if (isset($ignore_tokens[$i])) { continue; } // skip namespaces declarations and imports: // namespace foo\bar; // use foo\bar as foobar; if ($t->kind == T_NAMESPACE || $t->kind == T_USE) { do { $t = $t->nextNS(); } while ($t->text != ';' && $t->text != '{'); $i = $t->index; continue; } // skip class names completely // class Foo extends Bar implements Baz, Qux if ($t->kind == T_CLASS || $t->kind == T_INTERFACE || $t->kind == T_TRAIT) { do { $t = $t->nextNS(); } while ($t->text != ';' && $t->text != '{'); $i = $t->index; continue; } if ($t->kind == T_INSTANCEOF || $t->kind == T_NEW) { // ok, class name: // $foo instanceof Foo // $foo = new Foo; do { $t = $t->nextNS(); } while ($t->kind == T_STRING || $t->kind == T_NS_SEPARATOR); $i = $t->index; continue; } if ($t->kind == XRef::T_ONE_CHAR && $t->text == '"') { $is_inside_interpolated_string = !$is_inside_interpolated_string; } if ($t->kind == T_STRING) { if (isset($seen_strings[$t->text])) { // report each string only once continue; } // skip know constants defined in this file (const foo=1), // system constants (SORT_DESC) and config-defined constants if (isset($global_constants[$t->text]) || isset($this->system_constants[$t->text]) || isset($this->config_constants[$t->text])) { continue; } // PHP predefined case-insensitive constants? $str_upper = strtoupper($t->text); if ($str_upper == "TRUE" || $str_upper == "FALSE" || $str_upper == "NULL") { continue; } // skip all fully-qualified names (names with namespaces), if any // Foo\Bar\Baz $n = $t->nextNS(); while ($n->kind == T_STRING || $n->kind == T_NS_SEPARATOR) { $n = $n->nextNS(); } // add constants defined in this file to list of known strings and don't report them // define('foo', <expression>) if ($t->text == 'define') { if ($n->text == '(') { $nn = $n->nextNS(); if ($nn->kind == T_CONSTANT_ENCAPSED_STRING) { $string = $nn->text; $nn = $nn->nextNS(); if ($nn->text == ',' && strlen($string) > 2) { // remove the first and the last quotes $string = substr($string, 1, strlen($string) - 2); $global_constants[$string] = 1; continue; } } } } if ($n->text == '(') { // ok, function call: Foo(...) $i = $n->index; continue; } if ($n->kind == T_DOUBLE_COLON) { // ok, class name: foo::$something $i = $n->index; continue; } if ($n->text == ':') { // ok, label (e.g. goto foo; foo: ...); $i = $n->index; continue; } // some kind of variable declared with class? // catch (Foo $x); // function bar(Foo\Bar &$x) if ($n->text == '&') { $n = $n->nextNS(); } if ($n->kind == T_VARIABLE) { $i = $n->index; continue; } $p = $t->prevNS(); if ($p->kind == T_DOUBLE_COLON || $p->kind == T_OBJECT_OPERATOR) { // ok, static or instance member: $bar->foo, Bar::foo continue; } if ($p->kind == T_GOTO) { // ok, label for goto continue; } // declare(ticks=1) ? if ($t->text == 'ticks' || $t->text == 'encoding') { if ($p->text == '(') { $pp = $p->prevNS(); if (strtolower($pp->text) == 'declare') { // ok, skip this continue; } } } if ($is_inside_interpolated_string) { // exception: it's ok to index arrays inside interpolated (double-quoted) strings // with bare words: // "Foo is $data[foo]"; // ok // "Foo is {$data['foo']}"; // ok // "Foo is {$data[foo]}"; // NOT ok if ($p->text == '[') { $pp = $p->prev(); if ($pp && $pp->kind == T_VARIABLE) { $pp = $pp->prev(); if ($pp && $pp->kind != T_CURLY_OPEN) { // ok, allow "$data[something]" continue; } } } } // is it some known class constant used without prefix? // class Foo { const BAR = 1; } // echo BAR; // should be Foo::BAR (or self::BAR inside the class) if (isset($class_constants[$t->text])) { $this->addDefect($t, self::E_UNPREFIXED_CLASS_CONSTANT); $seen_strings[$t->text] = 1; continue; } if ($t->text == $str_upper) { // ok, all-uppercase, SOME_CONSTANT, I hope continue; } $this->addDefect($t, self::E_LOWER_CASE_STRING_LITERAL); $seen_strings[$t->text] = 1; } } return $this->report; }
public function getReport(XRef_IParsedFile $pf) { if ($pf->getFileType() != $this->supportedFileType) { return; } $this->report = array(); // special case: if file has no declarations of function/classes, it can be included into // body of some instance method, then use of $this is legit. // see also: joomla code. $allow_this_in_global_scope = false; if (count($pf->getClasses()) == 0 && count($pf->getMethods()) == 0) { $allow_this_in_global_scope = true; } // TOKEN: $tokens = $pf->getTokens(); $tokens_count = count($tokens); for ($i = 0; $i < $tokens_count; ++$i) { $t = $tokens[$i]; // ignore bodies of non-static functions declared inside classes // TODO: should we allow $this inside function arguments, e.g. // class Foo { public function bar($this, $baz = $this) {} } ?? if ($t->kind == T_FUNCTION && $t->prevNS()->kind != T_STATIC && $pf->getClassAt($t->index) != null) { $n = $t->nextNS(); while ($n->text != "(") { $n = $n->nextNS(); } // fast-forward to closing ")" of the function arguments $n = $pf->getTokenAt($pf->getIndexOfPairedBracket($n->index)); $n = $n->nextNS(); if ($n->kind == T_USE) { $n = $n->nextNS(); if ($n->text != '(') { throw new XRef_ParseException($n); } $n = $pf->getTokenAt($pf->getIndexOfPairedBracket($n->index)); $n = $n->nextNS(); } if ($n->text == ';') { // declaration only or abstract function: function foo(); // do nothing, skip main loop to the next token $i = $n->index; } elseif ($n->text == '{') { // fast-forward main loop to the end of the function body $i = $pf->getIndexOfPairedBracket($n->index); } else { throw new XRef_ParseException($n, "'{' or ';'"); } continue; } // if we found $this anywhere else, this is an error if ($t->text == '$this') { if ($allow_this_in_global_scope) { $this->addDefect($t, self::E_THIS_IN_GLOBAL_SCOPE); } else { $this->addDefect($t, self::E_THIS_OUTSIDE_OF_METHOD); } } // similar error: self is used outside of class declaration // however, self:: is allowed in static methods // also, allow $object->parent if (($t->text == 'self' || $t->text == 'parent') && !$pf->getClassAt($t->index)) { $n = $t->nextNS(); $p = $t->prevNS(); if ($n->text == '::' || $p->kind == T_NEW || $p->kind == T_INSTANCEOF) { if ($allow_this_in_global_scope) { $this->addDefect($t, self::E_CLASS_CONSTRUCT_IN_GLOBAL_SCOPE); } else { $this->addDefect($t, self::E_CLASS_CONSTRUCT_OUTSIDE_OF_METHOD); } } } } // end of TOKEN loop return $this->report; }
public function getFormattedText(XRef_IParsedFile $pf, $root) { $filename = $pf->getFileName(); $links = $this->xref->getSourceFileLinks($filename); $tokens = $pf->getTokens(); $lineNumber = 1; $ret = ''; $ret .= sprintf(self::LINE_NUMBER_FORMAT, $lineNumber, $lineNumber); for ($i = 0; $i < count($tokens); ++$i) { $token = $tokens[$i]; if ($links && array_key_exists($i, $links)) { // TODO: ugly, redo if (is_array($links[$i])) { list($reportName, $objectId) = $links[$i]; $link = $this->xref->getHtmlLinkFor($reportName, $objectId, $root); $ret .= "<a href='{$link}'>"; } else { $ret .= "</a>"; } } $text = htmlspecialchars($token->text); if ($token->kind == XRef::T_ONE_CHAR) { $ret .= $text; } else { $span_class = array_key_exists($token->kind, XRef::$tokenNames) ? XRef::$tokenNames[$token->kind] : token_name($token->kind); if ($span_class == 'UNKNOWN') { $span_class = $token->kind; } // special classes for php predefined constants if ($token->kind == T_STRING) { $str_upper = strtoupper($token->text); switch (strtoupper($token->text)) { case "NULL": $span_class == 'T_NULL'; break; case "TRUE": $span_class == 'T_TRUE'; break; case "FALSE": $span_class == 'T_FALSE'; break; } } if (strpos($text, "\n") === false) { // no new lines, simple $ret .= "<span class='{$span_class}'>{$text}</span>"; } else { // ugly code that does the following: // input: // some \n multiline text \n startig at line 10 // output: // <span class="text">some</span> // 11 <span class="text">multiline text </span> // 12 <span class="text"> starting at line 10 </span> $parts = explode("\n", $text); for ($j = 0; $j < count($parts); $j++) { if ($j != 0) { $lineNumber++; $ret .= sprintf(self::LINE_NUMBER_FORMAT, $lineNumber, $lineNumber); } $ret .= "<span class='{$span_class}'>{$parts[$j]}</span>"; if ($j != count($parts) - 1) { $ret .= "\n"; } } } } } return $ret; }
/** methods to use when parsed files are already available */ public function addParsedFile(XRef_IParsedFile $pf) { $this->report[$pf->getFileName()] = $this->getFileReport($pf); }
public function generateFileReport(XRef_IParsedFile $pf) { if ($pf->getFileType() != $this->supportedFileType) { return; } // collect all declared classes foreach ($pf->getClasses() as $pfc) { $c = $this->getOrCreate($pfc->name); $c->isInterface = $pfc->kind == T_INTERFACE; $definedAt = new XRef_FilePosition($pf, $pfc->nameIndex); $c->definedAt[] = $definedAt; // link from source file HTML page to report page "reportId/objectId" $this->xref->addSourceFileLink($definedAt, $this->reportId, $c->id); // extended classes/interfaces foreach ($pfc->extends as $ext_name) { $c->extends[] = $ext_name; $ext_class = $this->getOrCreate($ext_name); $ext_class->inheritedBy[] = $pfc->name; $extendsIndex = $pfc->extendsIndex[$ext_name]; $filePos = new XRef_FilePosition($pf, $extendsIndex[0], $extendsIndex[1]); $ext_class->usedAt[] = $filePos; // link from source file HTML page to report page "reportId/objectId" $this->xref->addSourceFileLink($filePos, $this->reportId, $ext_class->id); } // extended classes/interfaces foreach ($pfc->implements as $imp_name) { $c->implements[] = $imp_name; $imp_class = $this->getOrCreate($imp_name); $imp_class->inheritedBy[] = $pfc->name; $extendsIndex = $pfc->implementsIndex[$imp_name]; $filePos = new XRef_FilePosition($pf, $extendsIndex[0], $extendsIndex[1]); $imp_class->usedAt[] = $filePos; // link from source file HTML page to report page "reportId/objectId" $this->xref->addSourceFileLink($filePos, $this->reportId, $imp_class->id); } } // foreach declared class // collect all places where this class is instantiated or mentioned: $tokens = $pf->getTokens(); $tokens_count = count($tokens); for ($i = 0; $i < $tokens_count; ++$i) { $t = $tokens[$i]; // "new" <name> // "new" $className // "new" "(" <callable-name> ")" // AS3 // "new" "<" <int> ">" // AS3 if ($t->kind == T_NEW) { $t = $t->nextNS(); if ($t->kind != T_STRING) { continue; } // TODO: scan for namespaced name, e.g. new \Foo\Bar() $name = $pf->qualifyName($t->text, $t->index); $new = $this->getOrCreate($name); $filePos = new XRef_FilePosition($pf, $t->index); $new->instantiatedAt[] = $filePos; // link from source file HTML page to report page "reportId/objectId" $this->xref->addSourceFileLink($filePos, $this->reportId, $new->id); continue; } // T_NEW // // Class :: $foo // Class :: foo() // but not Class :: CONST if ($t->kind == T_DOUBLE_COLON) { $p = $t->prevNS(); if ($p->kind == T_STRING) { $n = $t->nextNS(); if ($n->kind == T_VARIABLE) { // ok, static field } elseif ($n->kind == T_STRING) { // method or constant $nn = $n->nextNS(); if ($nn->text != '(') { continue; } } elseif ($n->text == '$') { // rare case: Class::$$static_field_name continue; } else { throw new XRef_ParseException($n); } $className = $pf->qualifyName($p->text, $p->index); if ($pf->getFileType() == XRef::FILETYPE_PHP) { if ($className == 'self') { $class = $pf->getClassAt($p->index); if (!$class) { error_log("Reference to self:: class not inside a class method at " . $pf->getFileName() . ":{$t->lineNumber}"); continue; } $className = $class->name; } // TODO: super:: class resolution needs 2-pass parser } $c = $this->getOrCreate($className); $filePos = new XRef_FilePosition($pf, $p->index); $c->usedAt[] = $filePos; $this->xref->addSourceFileLink($filePos, $this->reportId, $c->id); } else { error_log("Unexpected token {$t->text} at " . $pf->getFileName() . ":{$t->lineNumber}"); } } // $foo isinstanceof Bar // $foo isinstanceof $bar if ($t->kind == T_INSTANCEOF) { $n = $t->nextNS(); if ($n->kind == T_STRING) { $className = $pf->qualifyName($n->text, $n->index); $c = $this->getOrCreate($className); $filePos = new XRef_FilePosition($pf, $n->index); $c->usedAt[] = $filePos; $this->xref->addSourceFileLink($filePos, $this->reportId, $c->id); } } // is_a($foo, "Bar") if ($t->kind == T_STRING && $t->text == "is_a") { $n = $t->nextNS(); if ($n->text != '(') { continue; } // TODO: add more checks below $n = $n->nextNS(); // var name. Actually, should skip any expression, e.g.: is_a($user->world, "World") $n = $n->nextNS(); // comma $n = $n->nextNS(); // class name literal? $className = $pf->qualifyName(preg_replace("#[\\'\"]#", '', $n->text), $n->index); $c = $this->getOrCreate($className); $filePos = new XRef_FilePosition($pf, $n->index); $c->usedAt[] = $filePos; $this->xref->addSourceFileLink($filePos, $this->reportId, $c->id); } } // foreach $token }
public function generateFileReport(XRef_IParsedFile $pf) { if ($pf->getFileType() != $this->supportedFileType) { return; } $tokens = $pf->getTokens(); for ($i = 0; $i < count($tokens); ++$i) { $t = $tokens[$i]; // Const declared: // const FOO = 10; if ($t->kind == T_CONST) { $n = $t->nextNS(); $class = $pf->getClassAt($n->index); if ($class) { $name = $class->name . "::" . $n->text; } else { $name = $n->text; } $c = $this->getOrCreate($name); $filePos = new XRef_FilePosition($pf, $n->index); $c->declaredAt[] = $filePos; // link from source file HTML page to report page "reportId/objectId" $this->xref->addSourceFileLink($filePos, $this->reportId, $c->id); } // Const used: // foo::bar, but not foo::bar() or foo::$bar\ if ($t->kind == T_DOUBLE_COLON) { $p = $t->prevNS(); if ($p->kind == T_STRING) { $n = $t->nextNS(); if ($n->kind == T_VARIABLE) { // static field continue; } elseif ($n->kind == T_STRING) { $nn = $n->nextNS(); if ($nn->kind == XRef::T_ONE_CHAR && $nn->text == '(') { // method call continue; } } elseif ($n->text == '$') { // rare case: Class::$$static_field_name continue; } else { error_log("What's this: {$n->text}"); continue; } $className = $p->text; if ($className == 'self') { $class = $pf->getClassAt($p->index); if (!$class) { error_log("Reference to self:: class not inside a class method at " . $pf->getFileName() . ":{$p->lineNumber}"); continue; } $className = $class->name; } $name = $className . "::" . $n->text; $c = $this->getOrCreate($name); $filePos = new XRef_FilePosition($pf, $n->index); $c->usedAt[] = $filePos; // link from source file HTML page to report page "reportId/objectId" $this->xref->addSourceFileLink($filePos, $this->reportId, $c->id); } } } // foreach token }
public function createFileSlice(XRef_IParsedFile $pf, $is_library_file = false) { $this->slice = array(); $this->already_seen = array(); $tokens = $pf->getTokens(); for ($i = 0; $i < count($tokens); ++$i) { $t = $tokens[$i]; // new Foo(); // new \Bar\Baz(); // new $class // new self() if ($t->kind == T_NEW) { $n = $t->nextNS(); $class_name = ''; while ($n->kind == T_NS_SEPARATOR || $n->kind == T_STRING) { $class_name = $class_name . $n->text; $n = $n->nextNS(); } if ($class_name) { $class_name = $pf->qualifyName($class_name, $t->index); if ($class_name == 'self' || $class_name == 'parent' || $class_name == 'static') { $class = $pf->getClassAt($t->index); if (!$class) { continue; } $class_name = $class->name; } $this->addUsedConstruct($class_name, 'method', '__construct', $t->lineNumber, $class_name, false, false); } continue; } // $this->foo(); // $this->bar; if ($t->kind == T_VARIABLE && $t->text == '$this') { $n = $t->nextNS(); if ($n->text == '->') { $n = $n->nextNS(); if ($n->kind == T_STRING) { $name = $n->text; $n = $n->nextNS(); $class = $pf->getClassAt($t->index); if (!$class) { continue; } $class_name = $class->name; $key = $n->text == '(' ? 'method' : 'property'; $this->addUsedConstruct($class_name, $key, $name, $t->lineNumber, $class_name, false, false); } } continue; } // Foo::bar(); // \Foo::bar(); // Foo\Bar::BAZ if ($t->kind == T_DOUBLE_COLON) { $class_name = ''; $p = $t->prevNS(); while ($p->kind == T_NS_SEPARATOR || $p->kind == T_STRING) { $class_name = $p->text . $class_name; $p = $p->prevNS(); } if ($class_name) { $class_name = $pf->qualifyName($class_name, $t->index); $check_parent_only = $class_name == 'parent'; $from_class = $pf->getClassAt($t->index); $from_class_name = $from_class ? $from_class->name : ''; if ($class_name == 'self' || $class_name == 'static' || $class_name == 'parent') { $check_parent_only = $class_name == 'parent'; $class_name = $from_class_name; $from_method = $pf->getMethodAt($t->index); $is_static_context = !$from_class || !$from_method || XRef::isStatic($from_method->attributes); } else { $is_static_context = true; } $n = $t->nextNS(); if ($n->kind == T_STRING) { $nn = $n->nextNS(); if ($nn->text == '(') { // Foo::bar() // self::bar() - this can be either static or instance access, depends on context $this->addUsedConstruct($class_name, 'method', $n->text, $t->lineNumber, $from_class_name, $is_static_context, $check_parent_only); } else { // Foo::BAR - constant $const_name = $n->text; $this->addUsedConstruct($class_name, 'constant', $n->text, $t->lineNumber, $from_class_name, true, $check_parent_only); } } elseif ($n->kind == T_VARIABLE) { // Foo::$bar $property_name = substr($n->text, 1); // skip '$' sign $this->addUsedConstruct($class_name, 'property', $property_name, $t->lineNumber, $from_class_name, true, $check_parent_only); } else { // e.g. self::$$keyName //error_log($n); } continue; } } } return $this->slice; }
public function __construct(XRef_IParsedFile $pf, $startIndex, $endIndex = 0) { $this->fileName = $pf->getFileName(); $this->lineNumber = $pf->getLineNumberAt($startIndex); $this->startIndex = $startIndex; $this->endIndex = $endIndex ? $endIndex : $startIndex; $class = $pf->getClassAt($startIndex); $method = $pf->getMethodAt($startIndex); $this->inClass = $class ? $class->name : null; $this->inMethod = $method ? $method->name : null; }
/** * @param XRef_ParsedFile $pf * @param bool $is_library_file * @return * any data */ public function createFileSlice(XRef_IParsedFile $pf, $is_library_file = false) { /** array of arrays with info about called functions and methods */ $called_functions = array(); /** map: (function call info => true), to report any call once only */ $uniqs = array(); /** map of function names, checked for existence by function_exists() */ $checked_for_functions = array(); $tokens = $pf->getTokens(); for ($i = 0; $i < count($tokens); ++$i) { $t = $tokens[$i]; // $this->foo(); // Foo\Barr::foo(); if ($t->text == '(') { $p = $t->prevNS(); if ($p->kind != T_STRING) { continue; } // array with parts of the name (T_NS_SEPARATOR && T_STRING) $function_name_parts = array($p->text); $class_name = ''; $pp = $p->prevNS(); if ($pp->kind == T_NS_SEPARATOR) { // Foo\bar(); // \bar(); while ($pp->kind == T_NS_SEPARATOR || $pp->kind == T_STRING) { array_unshift($function_name_parts, $pp->text); $pp = $pp->prevNS(); } } elseif ($pp->kind == T_OBJECT_OPERATOR) { // $var->bar() // $this->bar(); $pp = $pp->prevNS(); if ($pp->kind == T_VARIABLE && $pp->text == '$this') { $class_name = 'self'; // will be resolved later } else { // TODO continue; } } elseif ($pp->kind == T_DOUBLE_COLON) { // Foo::bar(); // Foo\Bar::bar(); // self::foo(); // static::bar(); $pp = $pp->prevNS(); while ($pp->kind == T_NS_SEPARATOR || $pp->kind == T_STRING || $pp->kind == T_STATIC) { $class_name = $pp->text . $class_name; $pp = $pp->prevNS(); } } if ($pp->text == '&') { $pp = $pp->prevNS(); } if ($pp->kind == T_FUNCTION) { // skip function declarations continue; } $arguments = $pf->extractList($t->nextNS()); $num_of_arguments = count($arguments); $from_class = $pf->getClassAt($t->index); $from_class_name = $from_class ? $from_class->name : ''; if ($pp->kind == T_NEW) { // new Something(); // new ns\Another\Something(); // new \Foo(); $class_name = implode('', $function_name_parts); if ($class_name == 'static' || $class_name == 'self') { $class_name = $from_class_name; } elseif ($class_name == 'parent') { $class_name = $from_class && $from_class->extends ? $from_class->extends[0] : null; } else { $class_name = $pf->qualifyName($class_name, $t->index); } if ($class_name) { $uniq = "new##{$class_name}#{$num_of_arguments}"; if (!isset($uniqs[$uniq])) { $uniqs[$uniq] = true; $called_functions[] = array(self::F_CLASS_CONSTRUCTOR, null, $class_name, $t->lineNumber, $num_of_arguments); } } } elseif ($class_name) { // method call: // $this->foo(); // self::foo(); // Foo::bar(); // \bar\Baz::foo(); if ($class_name == 'static' || $class_name == 'self') { $class_name = $from_class_name; } elseif ($class_name == 'parent') { $class_name = $from_class && $from_class->extends ? $from_class->extends[0] : null; } else { $class_name = $pf->qualifyName($class_name, $t->index); } if ($class_name) { if (count($function_name_parts) > 1) { throw new Exception("{$class_name}::" . implode('', $function_name_parts)); } $function_name = $function_name_parts[0]; if ($function_name == '__construct') { $uniq = "new##{$class_name}#{$num_of_arguments}"; if (!isset($uniqs[$uniq])) { $uniqs[$uniq] = true; $called_functions[] = array(self::F_CLASS_CONSTRUCTOR, null, $class_name, $t->lineNumber, $num_of_arguments); } } else { $uniq = "{$function_name}##{$class_name}#{$num_of_arguments}"; if (!isset($uniqs[$uniq])) { $uniqs[$uniq] = true; $called_functions[] = array(self::F_CLASS_METHOD, $function_name, $class_name, $t->lineNumber, $num_of_arguments); } } } } else { // function call: qualified or unqualified // foo(); // foo\bar(); // \foo\bar\baz(); if (count($function_name_parts) > 1) { $function_name = $pf->qualifyName(implode('', $function_name_parts), $t->index); $uniq = "{$function_name}##{$num_of_arguments}"; if (!isset($uniqs[$uniq])) { $uniqs[$uniq] = true; $called_functions[] = array(self::F_FULLY_QUALIFIED_FUNC, $function_name, null, $t->lineNumber, $num_of_arguments); } } else { $function_name = $function_name_parts[0]; $namespace = $pf->getNamespaceAt($t->index); $namespace_name = $namespace ? $namespace->name : ''; $uniq = "{$function_name}##{$namespace_name}#{$num_of_arguments}"; if (!isset($uniqs[$uniq])) { $uniqs[$uniq] = true; $called_functions[] = array(self::F_NOT_QUALIFIED_FUNC, $function_name, $namespace_name, $t->lineNumber, $num_of_arguments, $from_class_name); } } if ($function_name == 'function_exists') { $n = $t->nextNS(); if ($n->kind == T_CONSTANT_ENCAPSED_STRING) { $checked_for_function_name = trim($n->text, '\'"'); $checked_for_functions[$checked_for_function_name] = true; } } } } } $file_slice = array("called" => $called_functions, "checked" => $checked_for_functions); return $file_slice; }