diff --git a/src/lint/linter/ArcanistXHPASTLinter.php b/src/lint/linter/ArcanistXHPASTLinter.php index 3a036f2f..985f0950 100644 --- a/src/lint/linter/ArcanistXHPASTLinter.php +++ b/src/lint/linter/ArcanistXHPASTLinter.php @@ -1,116 +1,123 @@ rules = ArcanistXHPASTLinterRule::loadAllRules(); } public function __clone() { $rules = $this->rules; $this->rules = array(); foreach ($rules as $rule) { $this->rules[] = clone $rule; } } public function getInfoName() { return pht('XHPAST Lint'); } public function getInfoDescription() { return pht('Use XHPAST to enforce coding conventions on PHP source files.'); } public function getLinterName() { return 'XHP'; } public function getLinterConfigurationName() { return 'xhpast'; } public function getLintNameMap() { if ($this->lintNameMap === null) { $this->lintNameMap = mpull( $this->rules, 'getLintName', 'getLintID'); } return $this->lintNameMap; } public function getLintSeverityMap() { if ($this->lintSeverityMap === null) { $this->lintSeverityMap = mpull( $this->rules, 'getLintSeverity', 'getLintID'); } return $this->lintSeverityMap; } public function getLinterConfigurationOptions() { return parent::getLinterConfigurationOptions() + array_mergev( mpull($this->rules, 'getLinterConfigurationOptions')); } public function setLinterConfigurationValue($key, $value) { + $matched = false; + foreach ($this->rules as $rule) { foreach ($rule->getLinterConfigurationOptions() as $k => $spec) { if ($k == $key) { - return $rule->setLinterConfigurationValue($key, $value); + $matched = true; + $rule->setLinterConfigurationValue($key, $value); } } } + if ($matched) { + return; + } + return parent::setLinterConfigurationValue($key, $value); } public function getVersion() { // TODO: Improve this. return count($this->rules); } protected function resolveFuture($path, Future $future) { $tree = $this->getXHPASTTreeForPath($path); if (!$tree) { $ex = $this->getXHPASTExceptionForPath($path); if ($ex instanceof XHPASTSyntaxErrorException) { $this->raiseLintAtLine( $ex->getErrorLine(), 1, ArcanistSyntaxErrorXHPASTLinterRule::ID, pht( 'This file contains a syntax error: %s', $ex->getMessage())); } else if ($ex instanceof Exception) { $this->raiseLintAtPath( ArcanistUnableToParseXHPASTLinterRule::ID, $ex->getMessage()); } return; } $root = $tree->getRootNode(); foreach ($this->rules as $rule) { if ($this->isCodeEnabled($rule->getLintID())) { $rule->setLinter($this); $rule->process($root); } } } } diff --git a/src/lint/linter/__tests__/xhpast/self-member-references-php53.lint-test b/src/lint/linter/__tests__/xhpast/self-member-references-php53.lint-test new file mode 100644 index 00000000..5d06f638 --- /dev/null +++ b/src/lint/linter/__tests__/xhpast/self-member-references-php53.lint-test @@ -0,0 +1,19 @@ +setAncestorClass(__CLASS__) ->setUniqueMethod('getLintID') ->execute(); } final public function getLintID() { if ($this->lintID === null) { $class = new ReflectionClass($this); $const = $class->getConstant('ID'); if ($const === false) { throw new Exception( pht( '`%s` class `%s` must define an ID constant.', __CLASS__, get_class($this))); } if (!is_int($const)) { throw new Exception( pht( '`%s` class `%s` has an invalid ID constant. '. 'ID must be an integer.', __CLASS__, get_class($this))); } $this->lintID = $const; } return $this->lintID; } abstract public function getLintName(); public function getLintSeverity() { return ArcanistLintSeverity::SEVERITY_ERROR; } public function getLinterConfigurationOptions() { - return array(); + return array( + 'xhpast.php-version' => array( + 'type' => 'optional string', + 'help' => pht('PHP version to target.'), + ), + 'xhpast.php-version.windows' => array( + 'type' => 'optional string', + 'help' => pht('PHP version to target on Windows.'), + ), + ); } - public function setLinterConfigurationValue($key, $value) {} + public function setLinterConfigurationValue($key, $value) { + switch ($key) { + case 'xhpast.php-version': + $this->version = $value; + return; + + case 'xhpast.php-version.windows': + $this->windowsVersion = $value; + return; + } + } abstract public function process(XHPASTNode $root); final public function setLinter(ArcanistXHPASTLinter $linter) { $this->linter = $linter; return $this; } /** * Statically evaluate a boolean value from an XHP tree. * * TODO: Improve this and move it to XHPAST proper? * * @param string The "semantic string" of a single value. * @return mixed `true` or `false` if the value could be evaluated * statically; `null` if static evaluation was not possible. */ protected function evaluateStaticBoolean($string) { switch (strtolower($string)) { case '0': case 'null': case 'false': return false; case '1': case 'true': return true; } return null; } protected function getConcreteVariableString(XHPASTNode $var) { $concrete = $var->getConcreteString(); // Strip off curly braces as in `$obj->{$property}`. $concrete = trim($concrete, '{}'); return $concrete; } // These methods are proxied to the @{class:ArcanistLinter}. final public function getActivePath() { return $this->linter->getActivePath(); } final public function getOtherLocation($offset, $path = null) { return $this->linter->getOtherLocation($offset, $path); } final protected function raiseLintAtNode( XHPASTNode $node, $desc, $replace = null) { return $this->linter->raiseLintAtNode( $node, $this->getLintID(), $desc, $replace); } final public function raiseLintAtOffset( $offset, $desc, $text = null, $replace = null) { return $this->linter->raiseLintAtOffset( $offset, $this->getLintID(), $desc, $text, $replace); } final protected function raiseLintAtToken( XHPASTToken $token, $desc, $replace = null) { return $this->linter->raiseLintAtToken( $token, $this->getLintID(), $desc, $replace); } /* -( Utility )------------------------------------------------------------ */ + /** + * Retrieve all anonymous closure(s). + * + * Returns all descendant nodes which represent an anonymous function + * declaration. + * + * @param XHPASTNode Root node. + * @return AASTNodeList + */ + protected function getAnonymousClosures(XHPASTNode $root) { + $func_decls = $root->selectDescendantsOfType('n_FUNCTION_DECLARATION'); + $nodes = array(); + + foreach ($func_decls as $func_decl) { + if ($func_decl->getChildByIndex(2)->getTypeName() == 'n_EMPTY') { + $nodes[] = $func_decl; + } + } + + return AASTNodeList::newFromTreeAndNodes($root->getTree(), $nodes); + } + /** * Retrieve all calls to some specified function(s). * * Returns all descendant nodes which represent a function call to one of the * specified functions. * * @param XHPASTNode Root node. * @param list Function names. * @return AASTNodeList */ protected function getFunctionCalls(XHPASTNode $root, array $function_names) { $calls = $root->selectDescendantsOfType('n_FUNCTION_CALL'); $nodes = array(); foreach ($calls as $call) { $node = $call->getChildByIndex(0); $name = strtolower($node->getConcreteString()); if (in_array($name, $function_names)) { $nodes[] = $call; } } return AASTNodeList::newFromTreeAndNodes($root->getTree(), $nodes); } public function getSuperGlobalNames() { return array( '$GLOBALS', '$_SERVER', '$_GET', '$_POST', '$_FILES', '$_COOKIE', '$_SESSION', '$_REQUEST', '$_ENV', ); } } diff --git a/src/lint/linter/xhpast/rules/ArcanistPHPCompatibilityXHPASTLinterRule.php b/src/lint/linter/xhpast/rules/ArcanistPHPCompatibilityXHPASTLinterRule.php index 7697356e..5ee6f3fe 100644 --- a/src/lint/linter/xhpast/rules/ArcanistPHPCompatibilityXHPASTLinterRule.php +++ b/src/lint/linter/xhpast/rules/ArcanistPHPCompatibilityXHPASTLinterRule.php @@ -1,481 +1,450 @@ array( - 'type' => 'optional string', - 'help' => pht('PHP version to target.'), - ), - 'xhpast.php-version.windows' => array( - 'type' => 'optional string', - 'help' => pht('PHP version to target on Windows.'), - ), - ); - } - - public function setLinterConfigurationValue($key, $value) { - switch ($key) { - case 'xhpast.php-version': - $this->version = $value; - return; - - case 'xhpast.php-version.windows': - $this->windowsVersion = $value; - return; - - default: - return parent::setLinterConfigurationValue($key, $value); - } - } - public function process(XHPASTNode $root) { static $compat_info; if (!$this->version) { return; } if ($compat_info === null) { $target = phutil_get_library_root('phutil'). '/../resources/php_compat_info.json'; $compat_info = phutil_json_decode(Filesystem::readFile($target)); } // Create a whitelist for symbols which are being used conditionally. $whitelist = array( 'class' => array(), 'function' => array(), ); $conditionals = $root->selectDescendantsOfType('n_IF'); foreach ($conditionals as $conditional) { $condition = $conditional->getChildOfType(0, 'n_CONTROL_CONDITION'); $function = $condition->getChildByIndex(0); if ($function->getTypeName() != 'n_FUNCTION_CALL') { continue; } $function_token = $function ->getChildByIndex(0); if ($function_token->getTypeName() != 'n_SYMBOL_NAME') { // This may be `Class::method(...)` or `$var(...)`. continue; } $function_name = $function_token->getConcreteString(); switch ($function_name) { case 'class_exists': case 'function_exists': case 'interface_exists': $type = null; switch ($function_name) { case 'class_exists': $type = 'class'; break; case 'function_exists': $type = 'function'; break; case 'interface_exists': $type = 'interface'; break; } $params = $function->getChildOfType(1, 'n_CALL_PARAMETER_LIST'); $symbol = $params->getChildByIndex(0); if (!$symbol->isStaticScalar()) { continue; } $symbol_name = $symbol->evalStatic(); if (!idx($whitelist[$type], $symbol_name)) { $whitelist[$type][$symbol_name] = array(); } $span = $conditional ->getChildByIndex(1) ->getTokens(); $whitelist[$type][$symbol_name][] = range( head_key($span), last_key($span)); break; } } $calls = $root->selectDescendantsOfType('n_FUNCTION_CALL'); foreach ($calls as $call) { $node = $call->getChildByIndex(0); $name = $node->getConcreteString(); $version = idx($compat_info['functions'], $name, array()); $min = idx($version, 'php.min'); $max = idx($version, 'php.max'); // Check if whitelisted. $whitelisted = false; foreach (idx($whitelist['function'], $name, array()) as $range) { if (array_intersect($range, array_keys($node->getTokens()))) { $whitelisted = true; break; } } if ($whitelisted) { continue; } if ($min && version_compare($min, $this->version, '>')) { $this->raiseLintAtNode( $node, pht( 'This codebase targets PHP %s, but `%s()` was not '. 'introduced until PHP %s.', $this->version, $name, $min)); } else if ($max && version_compare($max, $this->version, '<')) { $this->raiseLintAtNode( $node, pht( 'This codebase targets PHP %s, but `%s()` was '. 'removed in PHP %s.', $this->version, $name, $max)); } else if (array_key_exists($name, $compat_info['params'])) { $params = $call->getChildOfType(1, 'n_CALL_PARAMETER_LIST'); foreach (array_values($params->getChildren()) as $i => $param) { $version = idx($compat_info['params'][$name], $i); if ($version && version_compare($version, $this->version, '>')) { $this->raiseLintAtNode( $param, pht( 'This codebase targets PHP %s, but parameter %d '. 'of `%s()` was not introduced until PHP %s.', $this->version, $i + 1, $name, $version)); } } } if ($this->windowsVersion) { $windows = idx($compat_info['functions_windows'], $name); if ($windows === false) { $this->raiseLintAtNode( $node, pht( 'This codebase targets PHP %s on Windows, '. 'but `%s()` is not available there.', $this->windowsVersion, $name)); } else if (version_compare($windows, $this->windowsVersion, '>')) { $this->raiseLintAtNode( $node, pht( 'This codebase targets PHP %s on Windows, '. 'but `%s()` is not available there until PHP %s.', $this->windowsVersion, $name, $windows)); } } } $classes = $root->selectDescendantsOfType('n_CLASS_NAME'); foreach ($classes as $node) { $name = $node->getConcreteString(); $version = idx($compat_info['interfaces'], $name, array()); $version = idx($compat_info['classes'], $name, $version); $min = idx($version, 'php.min'); $max = idx($version, 'php.max'); // Check if whitelisted. $whitelisted = false; foreach (idx($whitelist['class'], $name, array()) as $range) { if (array_intersect($range, array_keys($node->getTokens()))) { $whitelisted = true; break; } } if ($whitelisted) { continue; } if ($min && version_compare($min, $this->version, '>')) { $this->raiseLintAtNode( $node, pht( 'This codebase targets PHP %s, but `%s` was not '. 'introduced until PHP %s.', $this->version, $name, $min)); } else if ($max && version_compare($max, $this->version, '<')) { $this->raiseLintAtNode( $node, pht( 'This codebase targets PHP %s, but `%s` was '. 'removed in PHP %s.', $this->version, $name, $max)); } } // TODO: Technically, this will include function names. This is unlikely to // cause any issues (unless, of course, there existed a function that had // the same name as some constant). $constants = $root->selectDescendantsOfTypes(array( 'n_SYMBOL_NAME', 'n_MAGIC_SCALAR', )); foreach ($constants as $node) { $name = $node->getConcreteString(); $version = idx($compat_info['constants'], $name, array()); $min = idx($version, 'php.min'); $max = idx($version, 'php.max'); if ($min && version_compare($min, $this->version, '>')) { $this->raiseLintAtNode( $node, pht( 'This codebase targets PHP %s, but `%s` was not '. 'introduced until PHP %s.', $this->version, $name, $min)); } else if ($max && version_compare($max, $this->version, '<')) { $this->raiseLintAtNode( $node, pht( 'This codebase targets PHP %s, but `%s` was '. 'removed in PHP %s.', $this->version, $name, $max)); } } if (version_compare($this->version, '5.3.0') < 0) { $this->lintPHP53Features($root); } else { $this->lintPHP53Incompatibilities($root); } if (version_compare($this->version, '5.4.0') < 0) { $this->lintPHP54Features($root); } else { $this->lintPHP54Incompatibilities($root); } } private function lintPHP53Features(XHPASTNode $root) { $functions = $root->selectTokensOfType('T_FUNCTION'); foreach ($functions as $function) { $next = $function->getNextToken(); while ($next) { if ($next->isSemantic()) { break; } $next = $next->getNextToken(); } if ($next) { if ($next->getTypeName() === '(') { $this->raiseLintAtToken( $function, pht( 'This codebase targets PHP %s, but anonymous '. 'functions were not introduced until PHP 5.3.', $this->version)); } } } $namespaces = $root->selectTokensOfType('T_NAMESPACE'); foreach ($namespaces as $namespace) { $this->raiseLintAtToken( $namespace, pht( 'This codebase targets PHP %s, but namespaces were not '. 'introduced until PHP 5.3.', $this->version)); } // NOTE: This is only "use x;", in anonymous functions the node type is // n_LEXICAL_VARIABLE_LIST even though both tokens are T_USE. // TODO: We parse n_USE in a slightly crazy way right now; that would be // a better selector once it's fixed. $uses = $root->selectDescendantsOfType('n_USE_LIST'); foreach ($uses as $use) { $this->raiseLintAtNode( $use, pht( 'This codebase targets PHP %s, but namespaces were not '. 'introduced until PHP 5.3.', $this->version)); } $statics = $root->selectDescendantsOfType('n_CLASS_STATIC_ACCESS'); foreach ($statics as $static) { $name = $static->getChildByIndex(0); if ($name->getTypeName() != 'n_CLASS_NAME') { continue; } if ($name->getConcreteString() === 'static') { $this->raiseLintAtNode( $name, pht( 'This codebase targets PHP %s, but `%s` was not '. 'introduced until PHP 5.3.', $this->version, 'static::')); } } $ternaries = $root->selectDescendantsOfType('n_TERNARY_EXPRESSION'); foreach ($ternaries as $ternary) { $yes = $ternary->getChildByIndex(1); if ($yes->getTypeName() === 'n_EMPTY') { $this->raiseLintAtNode( $ternary, pht( 'This codebase targets PHP %s, but short ternary was '. 'not introduced until PHP 5.3.', $this->version)); } } $heredocs = $root->selectDescendantsOfType('n_HEREDOC'); foreach ($heredocs as $heredoc) { if (preg_match('/^<<<[\'"]/', $heredoc->getConcreteString())) { $this->raiseLintAtNode( $heredoc, pht( 'This codebase targets PHP %s, but nowdoc was not '. 'introduced until PHP 5.3.', $this->version)); } } } private function lintPHP53Incompatibilities(XHPASTNode $root) {} private function lintPHP54Features(XHPASTNode $root) { $indexes = $root->selectDescendantsOfType('n_INDEX_ACCESS'); foreach ($indexes as $index) { switch ($index->getChildByIndex(0)->getTypeName()) { case 'n_FUNCTION_CALL': case 'n_METHOD_CALL': $this->raiseLintAtNode( $index->getChildByIndex(1), pht( 'The `%s` syntax was not introduced until PHP 5.4, but this '. 'codebase targets an earlier version of PHP. You can rewrite '. 'this expression using `%s`.', 'f()[...]', 'idx()')); break; } } $closures = $this->getAnonymousClosures($root); foreach ($closures as $closure) { $static_accesses = $closure ->selectDescendantsOfType('n_CLASS_STATIC_ACCESS'); foreach ($static_accesses as $static_access) { $class = $static_access->getChildByIndex(0); if ($class->getTypeName() != 'n_CLASS_NAME') { continue; } if (strtolower($class->getConcreteString()) != 'self') { continue; } $this->raiseLintAtNode( $class, pht( 'The use of `%s` in an anonymous closure is not '. 'available before PHP 5.4.', 'self')); } $property_accesses = $closure ->selectDescendantsOfType('n_OBJECT_PROPERTY_ACCESS'); foreach ($property_accesses as $property_access) { $variable = $property_access->getChildByIndex(0); if ($variable->getTypeName() != 'n_VARIABLE') { continue; } if ($variable->getConcreteString() != '$this') { continue; } $this->raiseLintAtNode( $variable, pht( 'The use of `%s` in an anonymous closure is not '. 'available before PHP 5.4.', '$this')); } } } 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; } } } } diff --git a/src/lint/linter/xhpast/rules/ArcanistSelfMemberReferenceXHPASTLinterRule.php b/src/lint/linter/xhpast/rules/ArcanistSelfMemberReferenceXHPASTLinterRule.php index 4c3399b7..bfec780e 100644 --- a/src/lint/linter/xhpast/rules/ArcanistSelfMemberReferenceXHPASTLinterRule.php +++ b/src/lint/linter/xhpast/rules/ArcanistSelfMemberReferenceXHPASTLinterRule.php @@ -1,86 +1,98 @@ selectDescendantsOfType('n_CLASS_DECLARATION'); foreach ($class_declarations as $class_declaration) { $class_name = $class_declaration ->getChildOfType(1, 'n_CLASS_NAME') ->getConcreteString(); $class_static_accesses = $class_declaration ->selectDescendantsOfType('n_CLASS_STATIC_ACCESS'); + $closures = $this->getAnonymousClosures($class_declaration); foreach ($class_static_accesses as $class_static_access) { $double_colons = $class_static_access ->selectTokensOfType('T_PAAMAYIM_NEKUDOTAYIM'); $class_ref = $class_static_access->getChildByIndex(0); if ($class_ref->getTypeName() != 'n_CLASS_NAME') { continue; } $class_ref_name = $class_ref->getConcreteString(); if (strtolower($class_name) == strtolower($class_ref_name)) { - $this->raiseLintAtNode( - $class_ref, - pht( - 'Use `%s` for local static member references.', - 'self::'), - 'self'); + $in_closure = false; + + foreach ($closures as $closure) { + if ($class_ref->isDescendantOf($closure)) { + $in_closure = true; + break; + } + } + + if (version_compare($this->version, '5.4.0', '>=') || !$in_closure) { + $this->raiseLintAtNode( + $class_ref, + pht( + 'Use `%s` for local static member references.', + 'self::'), + 'self'); + } } static $self_refs = array( 'parent', 'self', 'static', ); if (!in_array(strtolower($class_ref_name), $self_refs)) { continue; } if ($class_ref_name != strtolower($class_ref_name)) { $this->raiseLintAtNode( $class_ref, pht('PHP keywords should be lowercase.'), strtolower($class_ref_name)); } } } $double_colons = $root->selectTokensOfType('T_PAAMAYIM_NEKUDOTAYIM'); foreach ($double_colons as $double_colon) { $tokens = $double_colon->getNonsemanticTokensBefore() + $double_colon->getNonsemanticTokensAfter(); foreach ($tokens as $token) { if ($token->isAnyWhitespace()) { if (strpos($token->getValue(), "\n") !== false) { continue; } $this->raiseLintAtToken( $token, pht('Unnecessary whitespace around double colon operator.'), ''); } } } } }