Page MenuHomePhorge

No OneTemporary

diff --git a/src/lint/linter/__tests__/xhpast/undeclared-variables.lint-test b/src/lint/linter/__tests__/xhpast/undeclared-variables.lint-test
index 3b07c19c..903e620a 100644
--- a/src/lint/linter/__tests__/xhpast/undeclared-variables.lint-test
+++ b/src/lint/linter/__tests__/xhpast/undeclared-variables.lint-test
@@ -1,193 +1,201 @@
<?php
function() use ($c) {
$c++;
};
function f($a, $b) {
static $c, $d;
global $e, $f;
$g = $h = x();
list($i, list($j, $k)) = y();
foreach (q() as $l => $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;
+ };
+}
~~~~~~~~~~
error:28:3
error:30:3
error:36:3
error:42:5
error:43:7
error:44:5
error:45:5
error:46:10
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
diff --git a/src/lint/linter/xhpast/rules/ArcanistUndeclaredVariableXHPASTLinterRule.php b/src/lint/linter/xhpast/rules/ArcanistUndeclaredVariableXHPASTLinterRule.php
index afb07968..c60b216c 100644
--- a/src/lint/linter/xhpast/rules/ArcanistUndeclaredVariableXHPASTLinterRule.php
+++ b/src/lint/linter/xhpast/rules/ArcanistUndeclaredVariableXHPASTLinterRule.php
@@ -1,345 +1,356 @@
<?php
final class ArcanistUndeclaredVariableXHPASTLinterRule
extends ArcanistXHPASTLinterRule {
const ID = 5;
public function getLintName() {
return pht('Use of Undeclared Variable');
}
public function process(XHPASTNode $root) {
// These things declare variables in a function:
// Explicit parameters
// Assignment
// Assignment via list()
// Static
// Global
// Lexical vars
// Builtins ($this)
// foreach()
// catch
//
// These things make lexical scope unknowable:
// Use of extract()
// Assignment to variable variables ($$x)
// Global with variable variables
//
// These things don't count as "using" a variable:
// isset()
// empty()
// Static class variables
//
// The general approach here is to find each function/method declaration,
// then:
//
// 1. Identify all the variable declarations, and where they first occur
// in the function/method declaration.
// 2. Identify all the uses that don't really count (as above).
// 3. Everything else must be a use of a variable.
// 4. For each variable, check if any uses occur before the declaration
// and warn about them.
//
// We also keep track of where lexical scope becomes unknowable (e.g.,
// because the function calls extract() or uses dynamic variables,
// preventing us from keeping track of which variables are defined) so we
// can stop issuing warnings after that.
//
// TODO: Support functions defined inside other functions which is commonly
// used with anonymous functions.
$defs = $root->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();
$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;
+ }
+ }
+
// 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) {
$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;
}
}
}
}

File Metadata

Mime Type
text/x-diff
Expires
Sun, Jan 19, 15:56 (2 w, 6 d ago)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
1126227
Default Alt Text
(16 KB)

Event Timeline