public function process(XHPASTNode $root) { $calls = $root->selectDescendantsOfTypes(array('n_FUNCTION_CALL', 'n_METHOD_CALL')); foreach ($calls as $call) { $params = $call->getChildOfType(1, 'n_CALL_PARAMETER_LIST'); $tokens = $params->getTokens(); $first = head($tokens); $leading = $first->getNonsemanticTokensBefore(); $leading_text = implode('', mpull($leading, 'getValue')); if (preg_match('/^\\s+$/', $leading_text)) { $this->raiseLintAtOffset($first->getOffset() - strlen($leading_text), pht('Convention: no spaces before opening parenthesis in calls.'), $leading_text, ''); } } foreach ($calls as $call) { // If the last parameter of a call is a HEREDOC, don't apply this rule. $params = $call->getChildOfType(1, 'n_CALL_PARAMETER_LIST')->getChildren(); if ($params) { $last_param = last($params); if ($last_param->getTypeName() === 'n_HEREDOC') { continue; } } $tokens = $call->getTokens(); $last = array_pop($tokens); $trailing = $last->getNonsemanticTokensBefore(); $trailing_text = implode('', mpull($trailing, 'getValue')); if (preg_match('/^\\s+$/', $trailing_text)) { $this->raiseLintAtOffset($last->getOffset() - strlen($trailing_text), pht('Convention: no spaces before closing parenthesis in calls.'), $trailing_text, ''); } } }
public function process(XHPASTNode $root) { $all_paren_groups = $root->selectDescendantsOfTypes(array('n_ARRAY_VALUE_LIST', 'n_ASSIGNMENT_LIST', 'n_CALL_PARAMETER_LIST', 'n_DECLARATION_PARAMETER_LIST', 'n_CONTROL_CONDITION', 'n_FOR_EXPRESSION', 'n_FOREACH_EXPRESSION')); foreach ($all_paren_groups as $group) { $tokens = $group->getTokens(); $token_o = array_shift($tokens); $token_c = array_pop($tokens); $nonsem_o = $token_o->getNonsemanticTokensAfter(); $nonsem_c = $token_c->getNonsemanticTokensBefore(); if (!$nonsem_o) { continue; } $raise = array(); $string_o = implode('', mpull($nonsem_o, 'getValue')); if (preg_match('/^[ ]+$/', $string_o)) { $raise[] = array($nonsem_o, $string_o); } if ($nonsem_o !== $nonsem_c) { $string_c = implode('', mpull($nonsem_c, 'getValue')); if (preg_match('/^[ ]+$/', $string_c)) { $raise[] = array($nonsem_c, $string_c); } } foreach ($raise as $warning) { list($tokens, $string) = $warning; $this->raiseLintAtOffset(reset($tokens)->getOffset(), pht('Parentheses should hug their contents.'), $string, ''); } } }
public function process(XHPASTNode $root) { foreach ($root->selectDescendantsOfType('n_STATEMENT_LIST') as $list) { $tokens = $list->getTokens(); if (!$tokens || head($tokens)->getValue() != '{') { continue; } list($before, $after) = $list->getSurroundingNonsemanticTokens(); if (!$before) { $first = head($tokens); // Only insert the space if we're after a closing parenthesis. If // we're in a construct like "else{}", other rules will insert space // after the 'else' correctly. $prev = $first->getPrevToken(); if (!$prev || $prev->getValue() !== ')') { continue; } $this->raiseLintAtToken($first, pht('Put opening braces on the same line as control statements and ' . 'declarations, with a single space before them.'), ' ' . $first->getValue()); } else { if (count($before) === 1) { $before = reset($before); if ($before->getValue() !== ' ') { $this->raiseLintAtToken($before, pht('Put opening braces on the same line as control statements and ' . 'declarations, with a single space before them.'), ' '); } } } } $nodes = $root->selectDescendantsOfType('n_STATEMENT'); foreach ($nodes as $node) { $parent = $node->getParentNode(); if (!$parent) { continue; } $type = $parent->getTypeName(); if ($type != 'n_STATEMENT_LIST' && $type != 'n_DECLARE') { $this->raiseLintAtNode($node, pht('Use braces to surround a statement block.')); } } $nodes = $root->selectDescendantsOfTypes(array('n_DO_WHILE', 'n_ELSE', 'n_ELSEIF')); foreach ($nodes as $list) { $tokens = $list->getTokens(); if (!$tokens || last($tokens)->getValue() != '}') { continue; } list($before, $after) = $list->getSurroundingNonsemanticTokens(); if (!$before) { $first = last($tokens); $this->raiseLintAtToken($first, pht('Put opening braces on the same line as control statements and ' . 'declarations, with a single space before them.'), ' ' . $first->getValue()); } else { if (count($before) === 1) { $before = reset($before); if ($before->getValue() !== ' ') { $this->raiseLintAtToken($before, pht('Put opening braces on the same line as control statements and ' . 'declarations, with a single space before them.'), ' '); } } } } }
public function process(XHPASTNode $root) { $nodes = $root->selectDescendantsOfTypes(array('n_CONCATENATION_LIST', 'n_STRING_SCALAR')); foreach ($nodes as $node) { $strings = array(); if ($node->getTypeName() === 'n_CONCATENATION_LIST') { $strings = $node->selectDescendantsOfType('n_STRING_SCALAR'); } else { if ($node->getTypeName() === 'n_STRING_SCALAR') { $strings = array($node); if ($node->getParentNode()->getTypeName() === 'n_CONCATENATION_LIST') { continue; } } } $valid = false; $invalid_nodes = array(); $fixes = array(); foreach ($strings as $string) { $concrete_string = $string->getConcreteString(); $single_quoted = $concrete_string[0] === "'"; $contents = substr($concrete_string, 1, -1); // Double quoted strings are allowed when the string contains the // following characters. static $allowed_chars = array('\\n', '\\r', '\\t', '\\v', '\\e', '\\f', '\'', '\\0', '\\1', '\\2', '\\3', '\\4', '\\5', '\\6', '\\7', '\\x'); $contains_special_chars = false; foreach ($allowed_chars as $allowed_char) { if (strpos($contents, $allowed_char) !== false) { $contains_special_chars = true; } } if (!$string->isConstantString()) { $valid = true; } else { if ($contains_special_chars && !$single_quoted) { $valid = true; } else { if (!$contains_special_chars && !$single_quoted) { $invalid_nodes[] = $string; $fixes[$string->getID()] = "'" . str_replace('\\"', '"', $contents) . "'"; } } } } if (!$valid) { foreach ($invalid_nodes as $invalid_node) { $this->raiseLintAtNode($invalid_node, pht('String does not require double quotes. For consistency, ' . 'prefer single quotes.'), $fixes[$invalid_node->getID()]); } } } }
public function process(XHPASTNode $root) { $decs = $root->selectDescendantsOfTypes(array('n_FUNCTION_DECLARATION', 'n_METHOD_DECLARATION')); foreach ($decs as $dec) { $params = $dec->getChildOfType(3, 'n_DECLARATION_PARAMETER_LIST'); $tokens = $params->getTokens(); $last = array_pop($tokens); $trailing = $last->getNonsemanticTokensBefore(); $trailing_text = implode('', mpull($trailing, 'getValue')); if (preg_match('/^\\s+$/', $trailing_text)) { $this->raiseLintAtOffset($last->getOffset() - strlen($trailing_text), pht('Convention: no spaces before closing parenthesis in ' . 'function and method declarations.'), $trailing_text, ''); } } }
public function process(XHPASTNode $root) { $methods = $root->selectDescendantsOfTypes(array('n_CLASS_MEMBER_MODIFIER_LIST', 'n_METHOD_MODIFIER_LIST')); foreach ($methods as $method) { $modifiers = $method->getChildren(); $is_abstract = false; $is_final = false; $is_static = false; $visibility = null; foreach ($modifiers as $modifier) { switch ($modifier->getConcreteString()) { case 'abstract': if ($method->getTypeName() == 'n_CLASS_MEMBER_MODIFIER_LIST') { $this->raiseLintAtNode($modifier, pht('Properties cannot be declared %s.', 'abstract')); } if ($is_abstract) { $this->raiseLintAtNode($modifier, pht('Multiple %s modifiers are not allowed.', 'abstract')); } if ($is_final) { $this->raiseLintAtNode($modifier, pht('Cannot use the %s modifier on an %s class member', 'final', 'abstract')); } $is_abstract = true; break; case 'final': if ($is_abstract) { $this->raiseLintAtNode($modifier, pht('Cannot use the %s modifier on an %s class member', 'final', 'abstract')); } if ($is_final) { $this->raiseLintAtNode($modifier, pht('Multiple %s modifiers are not allowed.', 'final')); } $is_final = true; break; case 'public': case 'protected': case 'private': if ($visibility) { $this->raiseLintAtNode($modifier, pht('Multiple access type modifiers are not allowed.')); } $visibility = $modifier->getConcreteString(); break; case 'static': if ($is_static) { $this->raiseLintAtNode($modifier, pht('Multiple %s modifiers are not allowed.', 'static')); } break; } } } }
public function process(XHPASTNode $root) { $nodes = $root->selectDescendantsOfTypes(array('n_ARRAY_LITERAL', 'n_FUNCTION_CALL', 'n_METHOD_CALL', 'n_LIST')); foreach ($nodes as $node) { switch ($node->getTypeName()) { case 'n_ARRAY_LITERAL': if (head($node->getTokens())->getTypeName() == '[') { // Short array syntax. continue 2; } $params = $node->getChildOfType(0, 'n_ARRAY_VALUE_LIST'); break; case 'n_FUNCTION_CALL': case 'n_METHOD_CALL': $params = $node->getChildOfType(1, 'n_CALL_PARAMETER_LIST'); break; case 'n_LIST': $params = $node->getChildOfType(0, 'n_ASSIGNMENT_LIST'); break; default: throw new Exception(pht("Unexpected node of type '%s'!", $node->getTypeName())); } $tokens = $params->getTokens(); $first = head($tokens); $leading = $first->getNonsemanticTokensBefore(); $leading_text = implode('', mpull($leading, 'getValue')); if (preg_match('/^\\s+$/', $leading_text)) { $this->raiseLintAtOffset($first->getOffset() - strlen($leading_text), pht('Convention: no spaces before opening parentheses.'), $leading_text, ''); } // If the last parameter of a call is a HEREDOC, don't apply this rule. $params = $params->getChildren(); if ($params) { $last_param = last($params); if ($last_param->getTypeName() === 'n_HEREDOC') { continue; } } $tokens = $node->getTokens(); $last = array_pop($tokens); if ($node->getTypeName() == 'n_ARRAY_LITERAL') { continue; } $trailing = $last->getNonsemanticTokensBefore(); $trailing_text = implode('', mpull($trailing, 'getValue')); if (preg_match('/^\\s+$/', $trailing_text)) { $this->raiseLintAtOffset($last->getOffset() - strlen($trailing_text), pht('Convention: no spaces before closing parentheses.'), $trailing_text, ''); } } }
public function process(XHPASTNode $root) { $nodes = $root->selectDescendantsOfTypes(array('n_INCLUDE_FILE', 'n_ECHO_LIST')); foreach ($nodes as $node) { $child = head($node->getChildren()); if ($child->getTypeName() === 'n_PARENTHETICAL_EXPRESSION') { list($before, $after) = $child->getSurroundingNonsemanticTokens(); $replace = preg_replace('/^\\((.*)\\)$/', '$1', $child->getConcreteString()); if (!$before) { $replace = ' ' . $replace; } $this->raiseLintAtNode($child, pht('Language constructs do not require parentheses.'), $replace); } } }
public function process(XHPASTNode $root) { $decs = $root->selectDescendantsOfTypes(array('n_FUNCTION_DECLARATION', 'n_METHOD_DECLARATION')); foreach ($decs as $dec) { $params = $dec->getChildOfType(3, 'n_DECLARATION_PARAMETER_LIST'); $tokens = $params->getTokens(); $first = head($tokens); $last = last($tokens); $leading = $first->getNonsemanticTokensBefore(); $leading_text = implode('', mpull($leading, 'getValue')); $trailing = $last->getNonsemanticTokensBefore(); $trailing_text = implode('', mpull($trailing, 'getValue')); if ($dec->getChildByIndex(2)->getTypeName() == 'n_EMPTY') { // Anonymous functions. if ($leading_text != ' ') { $this->raiseLintAtOffset($first->getOffset() - strlen($leading_text), pht('Convention: space before opening parenthesis in ' . 'anonymous function declarations.'), $leading_text, ' '); } } else { if (preg_match('/^\\s+$/', $leading_text)) { $this->raiseLintAtOffset($first->getOffset() - strlen($leading_text), pht('Convention: no spaces before opening parenthesis in ' . 'function and method declarations.'), $leading_text, ''); } } if (preg_match('/^\\s+$/', $trailing_text)) { $this->raiseLintAtOffset($last->getOffset() - strlen($trailing_text), pht('Convention: no spaces before closing parenthesis in ' . 'function and method declarations.'), $trailing_text, ''); } $use_list = $dec->getChildByIndex(4); if ($use_list->getTypeName() == 'n_EMPTY') { continue; } $use_token = $use_list->selectTokensOfType('T_USE'); foreach ($use_token as $use) { $before = $use->getNonsemanticTokensBefore(); $after = $use->getNonsemanticTokensAfter(); if (!$before) { $this->raiseLintAtOffset($use->getOffset(), pht('Convention: space before `%s` token.', 'use'), '', ' '); } if (!$after) { $this->raiseLintAtOffset($use->getOffset() + strlen($use->getValue()), pht('Convention: space after `%s` token.', 'use'), '', ' '); } } } }
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()); } // 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; } } }
private function lintPHP54Incompatibilities(XHPASTNode $root) { $breaks = $root->selectDescendantsOfTypes(array('n_BREAK', 'n_CONTINUE')); foreach ($breaks as $break) { $arg = $break->getChildByIndex(0); switch ($arg->getTypeName()) { case 'n_EMPTY': break; case 'n_NUMERIC_SCALAR': if ($arg->getConcreteString() != '0') { break; } default: $this->raiseLintAtNode($break->getChildByIndex(0), pht('The `%s` and `%s` statements no longer accept ' . 'variable arguments.', 'break', 'continue')); break; } } }
public function process(XHPASTNode $root) { $defs = $root->selectDescendantsOfTypes(array('n_FUNCTION_DECLARATION', 'n_METHOD_DECLARATION')); foreach ($defs as $def) { $body = $def->getChildByIndex(6); if ($body->getTypeName() === 'n_EMPTY') { // Abstract method declaration. continue; } $exclude = array(); // Exclude uses of variables, unsets, and foreach loops // within closures - they are checked on their own $func_defs = $body->selectDescendantsOfType('n_FUNCTION_DECLARATION'); foreach ($func_defs as $func_def) { $vars = $func_def->selectDescendantsOfType('n_VARIABLE'); foreach ($vars as $var) { $exclude[$var->getID()] = true; } $unset_lists = $func_def->selectDescendantsOfType('n_UNSET_LIST'); foreach ($unset_lists as $unset_list) { $exclude[$unset_list->getID()] = true; } $foreaches = $func_def->selectDescendantsOfType('n_FOREACH'); foreach ($foreaches as $foreach) { $exclude[$foreach->getID()] = true; } } // Find all variables that are unset within the scope $unset_vars = array(); $unset_lists = $body->selectDescendantsOfType('n_UNSET_LIST'); foreach ($unset_lists as $unset_list) { if (isset($exclude[$unset_list->getID()])) { continue; } $unset_list_vars = $unset_list->selectDescendantsOfType('n_VARIABLE'); foreach ($unset_list_vars as $var) { $concrete = $this->getConcreteVariableString($var); $unset_vars[$concrete][] = $var->getOffset(); $exclude[$var->getID()] = true; } } // Find all reference variables in foreach expressions $reference_vars = array(); $foreaches = $body->selectDescendantsOfType('n_FOREACH'); foreach ($foreaches as $foreach) { if (isset($exclude[$foreach->getID()])) { continue; } $foreach_expr = $foreach->getChildOfType(0, 'n_FOREACH_EXPRESSION'); $var = $foreach_expr->getChildByIndex(2); if ($var->getTypeName() !== 'n_VARIABLE_REFERENCE') { continue; } $reference = $var->getChildByIndex(0); if ($reference->getTypeName() !== 'n_VARIABLE') { continue; } $reference_name = $this->getConcreteVariableString($reference); $reference_vars[$reference_name][] = $reference->getOffset(); $exclude[$reference->getID()] = true; // Exclude uses of the reference variable within the foreach loop $foreach_vars = $foreach->selectDescendantsOfType('n_VARIABLE'); foreach ($foreach_vars as $var) { $name = $this->getConcreteVariableString($var); if ($name === $reference_name) { $exclude[$var->getID()] = true; } } } // Allow usage if the reference variable is assigned to another // reference 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') { continue; } $rval = $expr->getChildByIndex(2); if ($rval->getTypeName() !== 'n_VARIABLE_REFERENCE') { continue; } // Counts as unsetting a variable $concrete = $this->getConcreteVariableString($lval); $unset_vars[$concrete][] = $lval->getOffset(); $exclude[$lval->getID()] = true; } $all_vars = array(); $all = $body->selectDescendantsOfType('n_VARIABLE'); foreach ($all as $var) { if (isset($exclude[$var->getID()])) { continue; } $name = $this->getConcreteVariableString($var); if (!isset($reference_vars[$name])) { continue; } // Find the closest reference offset to this variable $reference_offset = null; foreach ($reference_vars[$name] as $offset) { if ($offset < $var->getOffset()) { $reference_offset = $offset; } else { break; } } if (!$reference_offset) { continue; } // Check if an unset exists between reference and usage of this // variable $warn = true; if (isset($unset_vars[$name])) { foreach ($unset_vars[$name] as $unset_offset) { if ($unset_offset > $reference_offset && $unset_offset < $var->getOffset()) { $warn = false; break; } } } if ($warn) { $this->raiseLintAtNode($var, pht('This variable was used already as a by-reference iterator ' . 'variable. Such variables survive outside the `%s` loop, ' . 'do not reuse.', 'foreach')); } } } }
public function process(XHPASTNode $root) { // We're going to build up a list of <type, name, token, error> tuples // and then try to instantiate a hook class which has the opportunity to // override us. $names = array(); $classes = $root->selectDescendantsOfType('n_CLASS_DECLARATION'); foreach ($classes as $class) { $name_token = $class->getChildByIndex(1); $name_string = $name_token->getConcreteString(); $names[] = array('class', $name_string, $name_token, ArcanistXHPASTLintNamingHook::isUpperCamelCase($name_string) ? null : pht('Follow naming conventions: classes should be named using `%s`.', 'UpperCamelCase')); } $ifaces = $root->selectDescendantsOfType('n_INTERFACE_DECLARATION'); foreach ($ifaces as $iface) { $name_token = $iface->getChildByIndex(1); $name_string = $name_token->getConcreteString(); $names[] = array('interface', $name_string, $name_token, ArcanistXHPASTLintNamingHook::isUpperCamelCase($name_string) ? null : pht('Follow naming conventions: interfaces should be named using `%s`.', 'UpperCamelCase')); } $functions = $root->selectDescendantsOfType('n_FUNCTION_DECLARATION'); foreach ($functions as $function) { $name_token = $function->getChildByIndex(2); if ($name_token->getTypeName() === 'n_EMPTY') { // Unnamed closure. continue; } $name_string = $name_token->getConcreteString(); $names[] = array('function', $name_string, $name_token, ArcanistXHPASTLintNamingHook::isLowercaseWithUnderscores(ArcanistXHPASTLintNamingHook::stripPHPFunction($name_string)) ? null : pht('Follow naming conventions: functions should be named using `%s`.', 'lowercase_with_underscores')); } $methods = $root->selectDescendantsOfType('n_METHOD_DECLARATION'); foreach ($methods as $method) { $name_token = $method->getChildByIndex(2); $name_string = $name_token->getConcreteString(); $names[] = array('method', $name_string, $name_token, ArcanistXHPASTLintNamingHook::isLowerCamelCase(ArcanistXHPASTLintNamingHook::stripPHPFunction($name_string)) ? null : pht('Follow naming conventions: methods should be named using `%s`.', 'lowerCamelCase')); } $param_tokens = array(); $params = $root->selectDescendantsOfType('n_DECLARATION_PARAMETER_LIST'); foreach ($params as $param_list) { foreach ($param_list->getChildren() as $param) { $name_token = $param->getChildByIndex(1); if ($name_token->getTypeName() === 'n_VARIABLE_REFERENCE') { $name_token = $name_token->getChildOfType(0, 'n_VARIABLE'); } $param_tokens[$name_token->getID()] = true; $name_string = $name_token->getConcreteString(); $names[] = array('parameter', $name_string, $name_token, ArcanistXHPASTLintNamingHook::isLowercaseWithUnderscores(ArcanistXHPASTLintNamingHook::stripPHPVariable($name_string)) ? null : pht('Follow naming conventions: parameters ' . 'should be named using `%s`', 'lowercase_with_underscores')); } } $constants = $root->selectDescendantsOfType('n_CLASS_CONSTANT_DECLARATION_LIST'); foreach ($constants as $constant_list) { foreach ($constant_list->getChildren() as $constant) { $name_token = $constant->getChildByIndex(0); $name_string = $name_token->getConcreteString(); $names[] = array('constant', $name_string, $name_token, ArcanistXHPASTLintNamingHook::isUppercaseWithUnderscores($name_string) ? null : pht('Follow naming conventions: class constants ' . 'should be named using `%s`', 'UPPERCASE_WITH_UNDERSCORES')); } } $member_tokens = array(); $props = $root->selectDescendantsOfType('n_CLASS_MEMBER_DECLARATION_LIST'); foreach ($props as $prop_list) { foreach ($prop_list->getChildren() as $token_id => $prop) { if ($prop->getTypeName() === 'n_CLASS_MEMBER_MODIFIER_LIST') { continue; } $name_token = $prop->getChildByIndex(0); $member_tokens[$name_token->getID()] = true; $name_string = $name_token->getConcreteString(); $names[] = array('member', $name_string, $name_token, ArcanistXHPASTLintNamingHook::isLowerCamelCase(ArcanistXHPASTLintNamingHook::stripPHPVariable($name_string)) ? null : pht('Follow naming conventions: class properties ' . 'should be named using `%s`.', 'lowerCamelCase')); } } $superglobal_map = array_fill_keys($this->getSuperGlobalNames(), true); $defs = $root->selectDescendantsOfTypes(array('n_FUNCTION_DECLARATION', 'n_METHOD_DECLARATION')); foreach ($defs as $def) { $globals = $def->selectDescendantsOfType('n_GLOBAL_DECLARATION_LIST'); $globals = $globals->selectDescendantsOfType('n_VARIABLE'); $globals_map = array(); foreach ($globals as $global) { $global_string = $global->getConcreteString(); $globals_map[$global_string] = true; $names[] = array('user', $global_string, $global, null); } // Exclude access of static properties, since lint will be raised at // their declaration if they're invalid and they may not conform to // variable rules. This is slightly overbroad (includes the entire // RHS of a "Class::..." token) to cover cases like "Class:$x[0]". These // variables are simply made exempt from naming conventions. $exclude_tokens = array(); $statics = $def->selectDescendantsOfType('n_CLASS_STATIC_ACCESS'); foreach ($statics as $static) { $rhs = $static->getChildByIndex(1); if ($rhs->getTypeName() == 'n_VARIABLE') { $exclude_tokens[$rhs->getID()] = true; } else { $rhs_vars = $rhs->selectDescendantsOfType('n_VARIABLE'); foreach ($rhs_vars as $var) { $exclude_tokens[$var->getID()] = true; } } } $vars = $def->selectDescendantsOfType('n_VARIABLE'); foreach ($vars as $token_id => $var) { if (isset($member_tokens[$token_id])) { continue; } if (isset($param_tokens[$token_id])) { continue; } if (isset($exclude_tokens[$token_id])) { continue; } $var_string = $var->getConcreteString(); // Awkward artifact of "$o->{$x}". $var_string = trim($var_string, '{}'); if (isset($superglobal_map[$var_string])) { continue; } if (isset($globals_map[$var_string])) { continue; } $names[] = array('variable', $var_string, $var, ArcanistXHPASTLintNamingHook::isLowercaseWithUnderscores(ArcanistXHPASTLintNamingHook::stripPHPVariable($var_string)) ? null : pht('Follow naming conventions: variables ' . 'should be named using `%s`.', 'lowercase_with_underscores')); } } // If a naming hook is configured, give it a chance to override the // default results for all the symbol names. $hook_class = $this->naminghook; if ($hook_class) { $hook_obj = newv($hook_class, array()); foreach ($names as $k => $name_attrs) { list($type, $name, $token, $default) = $name_attrs; $result = $hook_obj->lintSymbolName($type, $name, $default); $names[$k][3] = $result; } } // Raise anything we're left with. foreach ($names as $k => $name_attrs) { list($type, $name, $token, $result) = $name_attrs; if ($result) { $this->raiseLintAtNode($token, $result); } } // Lint constant declarations. $defines = $this->getFunctionCalls($root, array('define'))->add($root->selectDescendantsOfTypes(array('n_CLASS_CONSTANT_DECLARATION', 'n_CONSTANT_DECLARATION'))); foreach ($defines as $define) { switch ($define->getTypeName()) { case 'n_CLASS_CONSTANT_DECLARATION': case 'n_CONSTANT_DECLARATION': $constant = $define->getChildByIndex(0); if ($constant->getTypeName() !== 'n_STRING') { $constant = null; } break; case 'n_FUNCTION_CALL': $constant = $define->getChildOfType(1, 'n_CALL_PARAMETER_LIST')->getChildByIndex(0); if ($constant->getTypeName() !== 'n_STRING_SCALAR') { $constant = null; } break; default: $constant = null; break; } if (!$constant) { continue; } $constant_name = $constant->getConcreteString(); if ($constant_name !== strtoupper($constant_name)) { $this->raiseLintAtNode($constant, pht('Constants should be uppercase.')); } } }
public function process(XHPASTNode $root) { $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') { // Recursivey 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()); } // 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(); // If a variable was declared before a foreach() and is used after // it, raise a message. if (isset($declarations[$concrete])) { if ($declarations[$concrete] < $offset) { if (!empty($uses[$concrete]) && max($uses[$concrete]) > $offset) { $message = $this->raiseLintAtNode($var, pht('This iterator variable is a previously declared local ' . 'variable. To avoid overwriting locals, do not reuse them ' . 'as iterator variables.')); $message->setOtherLocations(array($this->getOtherLocation($declarations[$concrete]), $this->getOtherLocation(max($uses[$concrete])))); } } } // This is a declaration, exclude it from the "declare variables prior // to use" check below. unset($all[$var->getOffset()]); $vars[] = $var; } } }