/** * 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 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 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 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; }