diff --git a/src/lint/linter/__tests__/xhpast/undeclared-variables.lint-test b/src/lint/linter/__tests__/xhpast/undeclared-variables.lint-test index 1bedf0fa..ad03c44c 100644 --- a/src/lint/linter/__tests__/xhpast/undeclared-variables.lint-test +++ b/src/lint/linter/__tests__/xhpast/undeclared-variables.lint-test @@ -1,203 +1,210 @@ $m) {} $a++; $b++; $c++; $d++; $e++; $f++; $g++; $h++; $i++; $j++; $k++; $l++; $m++; $this++; $n++; // Only one that isn't declared. extract(z()); $o++; } function g($q) { $$q = x(); $r = y(); } final class C { public function m() { $a++; x($b); $c[] = 3; $d->v = 4; $a = $f; } } function worst() { global $$x; $y++; } function superglobals() { $GLOBALS[$_FILES[$_POST[$this]]]++; } function ref_foreach($x) { foreach ($x as &$z) {} $z++; } function has_default($x = 0) { $x++; } function declparse( $a, Q $b, Q &$c, Q $d = null, Q &$e = null, $f = null, $g = null, &$h = null, &$i = null) { $a++; $b++; $c++; $d++; $e++; $f++; $g++; $h++; $i++; $j++; } function declparse_a(Q $a) { $a++; } function declparse_b(Q &$a) { $a++; } function declparse_c(Q $a = null) { $a++; } function declparse_d(Q &$a = null) { $a++; } function declparse_e($a) { $a++; } function declparse_f(&$a) { $a++; } function declparse_g($a = null) { $a++; } function declparse_h(&$a = null) { $a++; } function static_class() { SomeClass::$x; } function instance_class() { $a = $this->$x; } function exception_vars() { try { // This is intentionally left blank. } catch (Exception $y) { $y++; } } function nonuse() { isset($x); empty($y); $x++; } function twice() { $y++; $y++; } function more_exceptions() { try { // This is intentionally left blank. } catch (Exception $a) { $a++; } catch (Exception $b) { $b++; } } abstract class P { abstract public function q(); } function x() { $lib = $_SERVER['PHP_ROOT'].'/lib/titan/display/read/init.php'; require_once $lib; f(((($lib)))); // Tests for paren expressions. f(((($lub)))); } final class A { public function foo($a) { $im_service = foo($a); if ($im_servce === false) { return 1; } return 2; } } function arrow($o, $x) { echo $o->{$x->{$x->{$x.$x->{$x}}.$x}}; } function strings() { $a = 1; echo "$a"; echo "$b"; } function catchy() { try { dangerous(); } catch (Exception $ex) { $y->z(); } } function some_func($x, $y) { $func = function ($z) use ($x) { echo $x; echo $y; echo $z; }; } + +function some_func($x, $y) { + $func = function ($z) use ($x) { + echo "$x/$y/$z"; + }; +} ~~~~~~~~~~ warning:9:3 error:28:3 error:30:3 error:36:3 error:42:5 error:43:7 error:44:5 error:45:5 error:46:10 warning:51:3 error:51:10 worst ever warning:61:3 error:87:3 This stuff is basically testing the lexer/parser for function decls. error:104:15 Variables in instance derefs should be checked, static should not. error:118:3 isset() and empty() should not trigger errors. error:122:3 Should only warn once in this function. error:144:8 error:150:9 error:164:9 error:171:5 error:178:10 +error:185:14 diff --git a/src/lint/linter/xhpast/rules/ArcanistUndeclaredVariableXHPASTLinterRule.php b/src/lint/linter/xhpast/rules/ArcanistUndeclaredVariableXHPASTLinterRule.php index 05464d41..2a389e34 100644 --- a/src/lint/linter/xhpast/rules/ArcanistUndeclaredVariableXHPASTLinterRule.php +++ b/src/lint/linter/xhpast/rules/ArcanistUndeclaredVariableXHPASTLinterRule.php @@ -1,358 +1,373 @@ selectDescendantsOfTypes(array( 'n_FUNCTION_DECLARATION', 'n_METHOD_DECLARATION', )); foreach ($defs as $def) { // We keep track of the first offset where scope becomes unknowable, and // silence any warnings after that. Default it to INT_MAX so we can min() // it later to keep track of the first problem we encounter. $scope_destroyed_at = PHP_INT_MAX; $declarations = array( '$this' => 0, ) + array_fill_keys($this->getSuperGlobalNames(), 0); $declaration_tokens = array(); $exclude_tokens = array(); + $exclude_strings = array(); $vars = array(); // First up, find all the different kinds of declarations, as explained // above. Put the tokens into the $vars array. $param_list = $def->getChildOfType(3, 'n_DECLARATION_PARAMETER_LIST'); $param_vars = $param_list->selectDescendantsOfType('n_VARIABLE'); foreach ($param_vars as $var) { $vars[] = $var; } // This is PHP5.3 closure syntax: function () use ($x) {}; $lexical_vars = $def ->getChildByIndex(4) ->selectDescendantsOfType('n_VARIABLE'); foreach ($lexical_vars as $var) { $vars[] = $var; } $body = $def->getChildByIndex(5); if ($body->getTypeName() === 'n_EMPTY') { // Abstract method declaration. continue; } $static_vars = $body ->selectDescendantsOfType('n_STATIC_DECLARATION') ->selectDescendantsOfType('n_VARIABLE'); foreach ($static_vars as $var) { $vars[] = $var; } $global_vars = $body ->selectDescendantsOfType('n_GLOBAL_DECLARATION_LIST'); foreach ($global_vars as $var_list) { foreach ($var_list->getChildren() as $var) { if ($var->getTypeName() === 'n_VARIABLE') { $vars[] = $var; } else { // Dynamic global variable, i.e. "global $$x;". $scope_destroyed_at = min($scope_destroyed_at, $var->getOffset()); // An error is raised elsewhere, no need to raise here. } } } // Include "catch (Exception $ex)", but not variables in the body of the // catch block. $catches = $body->selectDescendantsOfType('n_CATCH'); foreach ($catches as $catch) { $vars[] = $catch->getChildOfType(1, 'n_VARIABLE'); } $binary = $body->selectDescendantsOfType('n_BINARY_EXPRESSION'); foreach ($binary as $expr) { if ($expr->getChildByIndex(1)->getConcreteString() !== '=') { continue; } $lval = $expr->getChildByIndex(0); if ($lval->getTypeName() === 'n_VARIABLE') { $vars[] = $lval; } else if ($lval->getTypeName() === 'n_LIST') { // Recursively grab everything out of list(), since the grammar // permits list() to be nested. Also note that list() is ONLY valid // as an lval assignments, so we could safely lift this out of the // n_BINARY_EXPRESSION branch. $assign_vars = $lval->selectDescendantsOfType('n_VARIABLE'); foreach ($assign_vars as $var) { $vars[] = $var; } } if ($lval->getTypeName() === 'n_VARIABLE_VARIABLE') { $scope_destroyed_at = min($scope_destroyed_at, $lval->getOffset()); // No need to raise here since we raise an error elsewhere. } } $calls = $body->selectDescendantsOfType('n_FUNCTION_CALL'); foreach ($calls as $call) { $name = strtolower($call->getChildByIndex(0)->getConcreteString()); if ($name === 'empty' || $name === 'isset') { $params = $call ->getChildOfType(1, 'n_CALL_PARAMETER_LIST') ->selectDescendantsOfType('n_VARIABLE'); foreach ($params as $var) { $exclude_tokens[$var->getID()] = true; } continue; } if ($name !== 'extract') { continue; } $scope_destroyed_at = min($scope_destroyed_at, $call->getOffset()); } $func_decls = $body->selectDescendantsOfType('n_FUNCTION_DECLARATION'); foreach ($func_decls as $func_decl) { if ($func_decl->getChildByIndex(2)->getTypeName() != 'n_EMPTY') { continue; } foreach ($func_decl->selectDescendantsOfType('n_VARIABLE') as $var) { $exclude_tokens[$var->getID()] = true; } + + foreach (array('n_STRING_SCALAR', 'n_HEREDOC') as $type) { + foreach ($func_decl->selectDescendantsOfType($type) as $string) { + $exclude_strings[$string->getID()] = array(); + + foreach ($string->getStringVariables() as $offset => $var) { + $exclude_strings[$string->getID()][$var] = true; + } + } + } } // Now we have every declaration except foreach(), handled below. Build // two maps, one which just keeps track of which tokens are part of // declarations ($declaration_tokens) and one which has the first offset // where a variable is declared ($declarations). foreach ($vars as $var) { $concrete = $this->getConcreteVariableString($var); $declarations[$concrete] = min( idx($declarations, $concrete, PHP_INT_MAX), $var->getOffset()); $declaration_tokens[$var->getID()] = true; } // Excluded tokens are ones we don't "count" as being used, described // above. Put them into $exclude_tokens. $class_statics = $body ->selectDescendantsOfType('n_CLASS_STATIC_ACCESS'); $class_static_vars = $class_statics ->selectDescendantsOfType('n_VARIABLE'); foreach ($class_static_vars as $var) { $exclude_tokens[$var->getID()] = true; } // Find all the variables in scope, and figure out where they are used. // We want to find foreach() iterators which are both declared before and // used after the foreach() loop. $uses = array(); $all_vars = $body->selectDescendantsOfType('n_VARIABLE'); $all = array(); // NOTE: $all_vars is not a real array so we can't unset() it. foreach ($all_vars as $var) { // Be strict since it's easier; we don't let you reuse an iterator you // declared before a loop after the loop, even if you're just assigning // to it. $concrete = $this->getConcreteVariableString($var); $uses[$concrete][$var->getID()] = $var->getOffset(); if (isset($declaration_tokens[$var->getID()])) { // We know this is part of a declaration, so it's fine. continue; } if (isset($exclude_tokens[$var->getID()])) { // We know this is part of isset() or similar, so it's fine. continue; } $all[$var->getOffset()] = $concrete; } // Do foreach() last, we want to handle implicit redeclaration of a // variable already in scope since this probably means we're ovewriting a // local. // NOTE: Processing foreach expressions in order allows programs which // reuse iterator variables in other foreach() loops -- this is fine. We // have a separate warning to prevent nested loops from reusing the same // iterators. $foreaches = $body->selectDescendantsOfType('n_FOREACH'); $all_foreach_vars = array(); foreach ($foreaches as $foreach) { $foreach_expr = $foreach->getChildOfType(0, 'n_FOREACH_EXPRESSION'); $foreach_vars = array(); // Determine the end of the foreach() loop. $foreach_tokens = $foreach->getTokens(); $last_token = end($foreach_tokens); $foreach_end = $last_token->getOffset(); $key_var = $foreach_expr->getChildByIndex(1); if ($key_var->getTypeName() === 'n_VARIABLE') { $foreach_vars[] = $key_var; } $value_var = $foreach_expr->getChildByIndex(2); if ($value_var->getTypeName() === 'n_VARIABLE') { $foreach_vars[] = $value_var; } else { // The root-level token may be a reference, as in: // foreach ($a as $b => &$c) { ... } // Reach into the n_VARIABLE_REFERENCE node to grab the n_VARIABLE // node. $var = $value_var->getChildByIndex(0); if ($var->getTypeName() === 'n_VARIABLE_VARIABLE') { $var = $var->getChildByIndex(0); } $foreach_vars[] = $var; } // Remove all uses of the iterators inside of the foreach() loop from // the $uses map. foreach ($foreach_vars as $var) { $concrete = $this->getConcreteVariableString($var); $offset = $var->getOffset(); foreach ($uses[$concrete] as $id => $use_offset) { if (($use_offset >= $offset) && ($use_offset < $foreach_end)) { unset($uses[$concrete][$id]); } } $all_foreach_vars[] = $var; } } foreach ($all_foreach_vars as $var) { $concrete = $this->getConcreteVariableString($var); $offset = $var->getOffset(); // This is a declaration, exclude it from the "declare variables prior // to use" check below. unset($all[$var->getOffset()]); $vars[] = $var; } // Now rebuild declarations to include foreach(). foreach ($vars as $var) { $concrete = $this->getConcreteVariableString($var); $declarations[$concrete] = min( idx($declarations, $concrete, PHP_INT_MAX), $var->getOffset()); $declaration_tokens[$var->getID()] = true; } foreach (array('n_STRING_SCALAR', 'n_HEREDOC') as $type) { foreach ($body->selectDescendantsOfType($type) as $string) { foreach ($string->getStringVariables() as $offset => $var) { + if (isset($exclude_strings[$string->getID()][$var])) { + continue; + } + $all[$string->getOffset() + $offset - 1] = '$'.$var; } } } // Issue a warning for every variable token, unless it appears in a // declaration, we know about a prior declaration, we have explicitly // excluded it, or scope has been made unknowable before it appears. $issued_warnings = array(); foreach ($all as $offset => $concrete) { if ($offset >= $scope_destroyed_at) { // This appears after an extract() or $$var so we have no idea // whether it's legitimate or not. We raised a harshly-worded warning // when scope was made unknowable, so just ignore anything we can't // figure out. continue; } if ($offset >= idx($declarations, $concrete, PHP_INT_MAX)) { // The use appears after the variable is declared, so it's fine. continue; } if (!empty($issued_warnings[$concrete])) { // We've already issued a warning for this variable so we don't need // to issue another one. continue; } $this->raiseLintAtOffset( $offset, pht( 'Declare variables prior to use (even if you are passing them '. 'as reference parameters). You may have misspelled this '. 'variable name.'), $concrete); $issued_warnings[$concrete] = true; } } } }