diff --git a/src/lint/linter/ArcanistBaseXHPASTLinter.php b/src/lint/linter/ArcanistBaseXHPASTLinter.php index c420738e..c31abcb3 100644 --- a/src/lint/linter/ArcanistBaseXHPASTLinter.php +++ b/src/lint/linter/ArcanistBaseXHPASTLinter.php @@ -1,246 +1,246 @@ getVersion(); $version = PhutilXHPASTBinary::getVersion(); if ($version) { $parts[] = $version; } return implode('-', $parts); } - final protected function raiseLintAtToken( + final public function raiseLintAtToken( XHPASTToken $token, $code, $desc, $replace = null) { return $this->raiseLintAtOffset( $token->getOffset(), $code, $desc, $token->getValue(), $replace); } - final protected function raiseLintAtNode( + final public function raiseLintAtNode( XHPASTNode $node, $code, $desc, $replace = null) { return $this->raiseLintAtOffset( $node->getOffset(), $code, $desc, $node->getConcreteString(), $replace); } final protected function buildFutures(array $paths) { return $this->getXHPASTLinter()->buildSharedFutures($paths); } protected function didResolveLinterFutures(array $futures) { $this->getXHPASTLinter()->releaseSharedFutures(array_keys($futures)); } /* -( Sharing Parse Trees )------------------------------------------------ */ /** * Get the linter object which is responsible for building parse trees. * * When the engine specifies that several XHPAST linters should execute, * we designate one of them as the one which will actually build parse trees. * The other linters share trees, so they don't have to recompute them. * * Roughly, the first linter to execute elects itself as the builder. * Subsequent linters request builds and retrieve results from it. * * @return ArcanistBaseXHPASTLinter Responsible linter. * @task sharing */ final protected function getXHPASTLinter() { $resource_key = 'xhpast.linter'; // If we're the first linter to run, share ourselves. Otherwise, grab the // previously shared linter. $engine = $this->getEngine(); $linter = $engine->getLinterResource($resource_key); if (!$linter) { $linter = $this; $engine->setLinterResource($resource_key, $linter); } $base_class = __CLASS__; if (!($linter instanceof $base_class)) { throw new Exception( pht( 'Expected resource "%s" to be an instance of "%s"!', $resource_key, $base_class)); } return $linter; } /** * Build futures on this linter, for use and to share with other linters. * * @param list Paths to build futures for. * @return list Futures. * @task sharing */ final protected function buildSharedFutures(array $paths) { foreach ($paths as $path) { if (!isset($this->futures[$path])) { $this->futures[$path] = PhutilXHPASTBinary::getParserFuture( $this->getData($path)); $this->refcount[$path] = 1; } else { $this->refcount[$path]++; } } return array_select_keys($this->futures, $paths); } /** * Release futures on this linter which are no longer in use elsewhere. * * @param list Paths to release futures for. * @return void * @task sharing */ final protected function releaseSharedFutures(array $paths) { foreach ($paths as $path) { if (empty($this->refcount[$path])) { throw new Exception( pht( 'Imbalanced calls to shared futures: each call to '. '%s for a path must be paired with a call to %s.', 'buildSharedFutures()', 'releaseSharedFutures()')); } $this->refcount[$path]--; if (!$this->refcount[$path]) { unset($this->refcount[$path]); unset($this->futures[$path]); unset($this->trees[$path]); unset($this->exceptions[$path]); } } } /** * Get a path's tree from the responsible linter. * * @param string Path to retrieve tree for. * @return XHPASTTree|null Tree, or null if unparseable. * @task sharing */ final protected function getXHPASTTreeForPath($path) { // If we aren't the linter responsible for actually building the parse // trees, go get the tree from that linter. if ($this->getXHPASTLinter() !== $this) { return $this->getXHPASTLinter()->getXHPASTTreeForPath($path); } if (!array_key_exists($path, $this->trees)) { if (!array_key_exists($path, $this->futures)) { return; } $this->trees[$path] = null; try { $this->trees[$path] = XHPASTTree::newFromDataAndResolvedExecFuture( $this->getData($path), $this->futures[$path]->resolve()); $root = $this->trees[$path]->getRootNode(); $root->buildSelectCache(); $root->buildTokenCache(); } catch (Exception $ex) { $this->exceptions[$path] = $ex; } } return $this->trees[$path]; } /** * Get a path's parse exception from the responsible linter. * * @param string Path to retrieve exception for. * @return Exeption|null Parse exception, if available. * @task sharing */ final protected function getXHPASTExceptionForPath($path) { if ($this->getXHPASTLinter() !== $this) { return $this->getXHPASTLinter()->getXHPASTExceptionForPath($path); } return idx($this->exceptions, $path); } /* -( Utility )------------------------------------------------------------ */ /** * 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/ArcanistLinter.php b/src/lint/linter/ArcanistLinter.php index 6a1af25c..9eb92aaa 100644 --- a/src/lint/linter/ArcanistLinter.php +++ b/src/lint/linter/ArcanistLinter.php @@ -1,624 +1,624 @@ getLinterName(), $this->getLinterConfigurationName(), get_class($this)); } /* -( Runtime State )------------------------------------------------------ */ /** * @task state */ final public function getActivePath() { return $this->activePath; } /** * @task state */ final public function setActivePath($path) { $this->stopAllLinters = false; $this->activePath = $path; return $this; } /** * @task state */ final public function setEngine(ArcanistLintEngine $engine) { $this->engine = $engine; return $this; } /** * @task state */ final protected function getEngine() { return $this->engine; } /** * Set the internal ID for this linter. * * This ID is assigned automatically by the @{class:ArcanistLintEngine}. * * @param string Unique linter ID. * @return this * @task state */ final public function setLinterID($id) { $this->id = $id; return $this; } /** * Get the internal ID for this linter. * * Retrieves an internal linter ID managed by the @{class:ArcanistLintEngine}. * This ID is a unique scalar which distinguishes linters in a list. * * @return string Unique linter ID. * @task state */ final public function getLinterID() { return $this->id; } /* -( Executing Linters )-------------------------------------------------- */ /** * Hook called before a list of paths are linted. * * Parallelizable linters can start multiple requests in parallel here, * to improve performance. They can implement @{method:didLintPaths} to * collect results. * * Linters which are not parallelizable should normally ignore this callback * and implement @{method:lintPath} instead. * * @param list A list of paths to be linted * @return void * @task exec */ public function willLintPaths(array $paths) { return; } /** * Hook called for each path to be linted. * * Linters which are not parallelizable can do work here. * * Linters which are parallelizable may want to ignore this callback and * implement @{method:willLintPaths} and @{method:didLintPaths} instead. * * @param string Path to lint. * @return void * @task exec */ public function lintPath($path) { return; } /** * Hook called after a list of paths are linted. * * Parallelizable linters can collect results here. * * Linters which are not paralleizable should normally ignore this callback * and implement @{method:lintPath} instead. * * @param list A list of paths which were linted. * @return void * @task exec */ public function didLintPaths(array $paths) { return; } /** * Obsolete hook which was invoked before a path was linted. * * WARNING: This is an obsolete hook which is not called. If you maintain * a linter which relies on it, update to use @{method:lintPath} instead. * * @task exec */ final public function willLintPath($path) { // TODO: Remove this method after some time. In the meantime, the "final" // will fatal subclasses which implement this hook and point at the API // change so maintainers get fewer surprises. throw new PhutilMethodNotImplementedException(); } /** * Obsolete hook which was invoked after linters ran. * * WARNING: This is an obsolete hook which is not called. If you maintain * a linter which relies on it, update to use @{method:didLintPaths} instead. * * @return void * @task exec */ final public function didRunLinters() { // TODO: Remove this method after some time. In the meantime, the "final" // will fatal subclasses which implement this hook and point at the API // change so maintainers get fewer surprises. throw new PhutilMethodNotImplementedException(); } public function getLinterPriority() { return 1.0; } /** * TODO: This should be `final`. */ public function setCustomSeverityMap(array $map) { $this->customSeverityMap = $map; return $this; } final public function setCustomSeverityRules(array $rules) { $this->customSeverityRules = $rules; return $this; } final public function getProjectRoot() { $engine = $this->getEngine(); if (!$engine) { throw new Exception( pht( 'You must call %s before you can call %s.', 'setEngine()', __FUNCTION__.'()')); } $working_copy = $engine->getWorkingCopy(); if (!$working_copy) { return null; } return $working_copy->getProjectRoot(); } final public function getOtherLocation($offset, $path = null) { if ($path === null) { $path = $this->getActivePath(); } list($line, $char) = $this->getEngine()->getLineAndCharFromOffset( $path, $offset); return array( 'path' => $path, 'line' => $line + 1, 'char' => $char, ); } final public function stopAllLinters() { $this->stopAllLinters = true; return $this; } final public function didStopAllLinters() { return $this->stopAllLinters; } final public function addPath($path) { $this->paths[$path] = $path; $this->filteredPaths = null; return $this; } final public function setPaths(array $paths) { $this->paths = $paths; $this->filteredPaths = null; return $this; } /** * Filter out paths which this linter doesn't act on (for example, because * they are binaries and the linter doesn't apply to binaries). * * @param list * @return list */ final private function filterPaths(array $paths) { $engine = $this->getEngine(); $keep = array(); foreach ($paths as $path) { if (!$this->shouldLintDeletedFiles() && !$engine->pathExists($path)) { continue; } if (!$this->shouldLintDirectories() && $engine->isDirectory($path)) { continue; } if (!$this->shouldLintBinaryFiles() && $engine->isBinaryFile($path)) { continue; } if (!$this->shouldLintSymbolicLinks() && $engine->isSymbolicLink($path)) { continue; } $keep[] = $path; } return $keep; } final public function getPaths() { if ($this->filteredPaths === null) { $this->filteredPaths = $this->filterPaths(array_values($this->paths)); } return $this->filteredPaths; } final public function addData($path, $data) { $this->data[$path] = $data; return $this; } final protected function getData($path) { if (!array_key_exists($path, $this->data)) { $this->data[$path] = $this->getEngine()->loadData($path); } return $this->data[$path]; } public function getCacheVersion() { return 0; } final public function getLintMessageFullCode($short_code) { return $this->getLinterName().$short_code; } final public function getLintMessageSeverity($code) { $map = $this->customSeverityMap; if (isset($map[$code])) { return $map[$code]; } foreach ($this->customSeverityRules as $rule => $severity) { if (preg_match($rule, $code)) { return $severity; } } $map = $this->getLintSeverityMap(); if (isset($map[$code])) { return $map[$code]; } return $this->getDefaultMessageSeverity($code); } protected function getDefaultMessageSeverity($code) { return ArcanistLintSeverity::SEVERITY_ERROR; } final public function isMessageEnabled($code) { return ($this->getLintMessageSeverity($code) !== ArcanistLintSeverity::SEVERITY_DISABLED); } final public function getLintMessageName($code) { $map = $this->getLintNameMap(); if (isset($map[$code])) { return $map[$code]; } return pht('Unknown lint message!'); } final protected function addLintMessage(ArcanistLintMessage $message) { $root = $this->getProjectRoot(); $path = Filesystem::resolvePath($message->getPath(), $root); $message->setPath(Filesystem::readablePath($path, $root)); $this->messages[] = $message; return $message; } final public function getLintMessages() { return $this->messages; } - final protected function raiseLintAtLine( + final public function raiseLintAtLine( $line, $char, $code, $desc, $original = null, $replacement = null) { $message = id(new ArcanistLintMessage()) ->setPath($this->getActivePath()) ->setLine($line) ->setChar($char) ->setCode($this->getLintMessageFullCode($code)) ->setSeverity($this->getLintMessageSeverity($code)) ->setName($this->getLintMessageName($code)) ->setDescription($desc) ->setOriginalText($original) ->setReplacementText($replacement); return $this->addLintMessage($message); } - final protected function raiseLintAtPath($code, $desc) { + final public function raiseLintAtPath($code, $desc) { return $this->raiseLintAtLine(null, null, $code, $desc, null, null); } - final protected function raiseLintAtOffset( + final public function raiseLintAtOffset( $offset, $code, $desc, $original = null, $replacement = null) { $path = $this->getActivePath(); $engine = $this->getEngine(); if ($offset === null) { $line = null; $char = null; } else { list($line, $char) = $engine->getLineAndCharFromOffset($path, $offset); } return $this->raiseLintAtLine( $line + 1, $char + 1, $code, $desc, $original, $replacement); } public function canRun() { return true; } abstract public function getLinterName(); public function getVersion() { return null; } final protected function isCodeEnabled($code) { $severity = $this->getLintMessageSeverity($code); return $this->getEngine()->isSeverityEnabled($severity); } public function getLintSeverityMap() { return array(); } public function getLintNameMap() { return array(); } public function getCacheGranularity() { return self::GRANULARITY_FILE; } /** * If this linter is selectable via `.arclint` configuration files, return * a short, human-readable name to identify it. For example, `"jshint"` or * `"pep8"`. * * If you do not implement this method, the linter will not be selectable * through `.arclint` files. */ public function getLinterConfigurationName() { return null; } public function getLinterConfigurationOptions() { if (!$this->canCustomizeLintSeverities()) { return array(); } return array( 'severity' => array( 'type' => 'optional map', 'help' => pht( 'Provide a map from lint codes to adjusted severity levels: error, '. 'warning, advice, autofix or disabled.'), ), 'severity.rules' => array( 'type' => 'optional map', 'help' => pht( 'Provide a map of regular expressions to severity levels. All '. 'matching codes have their severity adjusted.'), ), ); } public function setLinterConfigurationValue($key, $value) { $sev_map = array( 'error' => ArcanistLintSeverity::SEVERITY_ERROR, 'warning' => ArcanistLintSeverity::SEVERITY_WARNING, 'autofix' => ArcanistLintSeverity::SEVERITY_AUTOFIX, 'advice' => ArcanistLintSeverity::SEVERITY_ADVICE, 'disabled' => ArcanistLintSeverity::SEVERITY_DISABLED, ); switch ($key) { case 'severity': if (!$this->canCustomizeLintSeverities()) { break; } $custom = array(); foreach ($value as $code => $severity) { if (empty($sev_map[$severity])) { $valid = implode(', ', array_keys($sev_map)); throw new Exception( pht( 'Unknown lint severity "%s". Valid severities are: %s.', $severity, $valid)); } $code = $this->getLintCodeFromLinterConfigurationKey($code); $custom[$code] = $severity; } $this->setCustomSeverityMap($custom); return; case 'severity.rules': if (!$this->canCustomizeLintSeverities()) { break; } foreach ($value as $rule => $severity) { if (@preg_match($rule, '') === false) { throw new Exception( pht( 'Severity rule "%s" is not a valid regular expression.', $rule)); } if (empty($sev_map[$severity])) { $valid = implode(', ', array_keys($sev_map)); throw new Exception( pht( 'Unknown lint severity "%s". Valid severities are: %s.', $severity, $valid)); } } $this->setCustomSeverityRules($value); return; } throw new Exception(pht('Incomplete implementation: %s!', $key)); } protected function canCustomizeLintSeverities() { return true; } protected function shouldLintBinaryFiles() { return false; } protected function shouldLintDeletedFiles() { return false; } protected function shouldLintDirectories() { return false; } protected function shouldLintSymbolicLinks() { return false; } /** * Map a configuration lint code to an `arc` lint code. Primarily, this is * intended for validation, but can also be used to normalize case or * otherwise be more permissive in accepted inputs. * * If the code is not recognized, you should throw an exception. * * @param string Code specified in configuration. * @return string Normalized code to use in severity map. */ protected function getLintCodeFromLinterConfigurationKey($code) { return $code; } }