diff --git a/src/lint/linter/ArcanistExternalLinter.php b/src/lint/linter/ArcanistExternalLinter.php index fe05f0c5..70612d5a 100644 --- a/src/lint/linter/ArcanistExternalLinter.php +++ b/src/lint/linter/ArcanistExternalLinter.php @@ -1,518 +1,532 @@ Mandatory flags, like `"--format=xml"`. * @task bin */ protected function getMandatoryFlags() { return array(); } /** * Provide default, overridable flags to the linter. Generally these are * configuration flags which affect behavior but aren't critical. Flags * which are required should be provided in @{method:getMandatoryFlags} * instead. * * Default flags can be overridden with @{method:setFlags}. * * @return list Overridable default flags. * @task bin */ protected function getDefaultFlags() { return array(); } /** * Override default flags with custom flags. If not overridden, flags provided * by @{method:getDefaultFlags} are used. * * @param list New flags. * @return this * @task bin */ final public function setFlags($flags) { $this->flags = (array) $flags; return $this; } /** * Return the binary or script to execute. This method synthesizes defaults * and configuration. You can override the binary with @{method:setBinary}. * * @return string Binary to execute. * @task bin */ final public function getBinary() { return coalesce($this->bin, $this->getDefaultBinary()); } /** * Override the default binary with a new one. * * @param string New binary. * @return this * @task bin */ final public function setBinary($bin) { $this->bin = $bin; return $this; } /** * Return true if this linter should use an interpreter (like "python" or * "node") in addition to the script. * * After overriding this method to return `true`, override * @{method:getDefaultInterpreter} to set a default. * * @return bool True to use an interpreter. * @task bin */ public function shouldUseInterpreter() { return false; } /** * Return the default interpreter, like "python" or "node". This method is * only invoked if @{method:shouldUseInterpreter} has been overridden to * return `true`. * * @return string Default interpreter. * @task bin */ public function getDefaultInterpreter() { throw new Exception("Incomplete implementation!"); } /** * Get the effective interpreter. This method synthesizes configuration and * defaults. * * @return string Effective interpreter. * @task bin */ final public function getInterpreter() { return coalesce($this->interpreter, $this->getDefaultInterpreter()); } /** * Set the interpreter, overriding any default. * * @param string New interpreter. * @return this * @task bin */ final public function setInterpreter($interpreter) { $this->interpreter = $interpreter; return $this; } /* -( Parsing Linter Output )---------------------------------------------- */ /** * Parse the output of the external lint program into objects of class * @{class:ArcanistLintMessage} which `arc` can consume. Generally, this * means examining the output and converting each warning or error into a * message. * * If parsing fails, returning `false` will cause the caller to throw an * appropriate exception. (You can also throw a more specific exception if * you're able to detect a more specific condition.) Otherwise, return a list * of messages. * * @param string Path to the file being linted. * @param int Exit code of the linter. * @param string Stdout of the linter. * @param string Stderr of the linter. * @return list|false List of lint messages, or false * to indicate parser failure. * @task parse */ abstract protected function parseLinterOutput($path, $err, $stdout, $stderr); /* -( Executing the Linter )----------------------------------------------- */ /** * Check that the binary and interpreter (if applicable) exist, and throw * an exception with a message about how to install them if they do not. * * @return void */ final public function checkBinaryConfiguration() { $interpreter = null; if ($this->shouldUseInterpreter()) { $interpreter = $this->getInterpreter(); } $binary = $this->getBinary(); // NOTE: If we have an interpreter, we don't require the script to be // executable (so we just check that the path exists). Otherwise, the // binary must be executable. if ($interpreter) { if (!Filesystem::binaryExists($interpreter)) { throw new ArcanistUsageException( pht( 'Unable to locate interpreter "%s" to run linter %s. You may '. 'need to install the intepreter, or adjust your linter '. 'configuration.'. "\nTO INSTALL: %s", $interpreter, get_class($this), $this->getInstallInstructions())); } if (!Filesystem::pathExists($binary)) { throw new ArcanistUsageException( pht( 'Unable to locate script "%s" to run linter %s. You may need '. 'to install the script, or adjust your linter configuration. '. "\nTO INSTALL: %s", $binary, get_class($this), $this->getInstallInstructions())); } } else { if (!Filesystem::binaryExists($binary)) { throw new ArcanistUsageException( pht( 'Unable to locate binary "%s" to run linter %s. You may need '. 'to install the binary, or adjust your linter configuration. '. "\nTO INSTALL: %s", $binary, get_class($this), $this->getInstallInstructions())); } } } /** * Get the composed executable command, including the interpreter and binary * but without flags or paths. This can be used to execute `--version` * commands. * * @return string Command to execute the raw linter. * @task exec */ protected function getExecutableCommand() { $this->checkBinaryConfiguration(); $interpreter = null; if ($this->shouldUseInterpreter()) { $interpreter = $this->getInterpreter(); } $binary = $this->getBinary(); if ($interpreter) { $bin = csprintf('%s %s', $interpreter, $binary); } else { $bin = csprintf('%s', $binary); } return $bin; } /** * Get the composed flags for the executable, including both mandatory and * configured flags. * * @return list Composed flags. * @task exec */ protected function getCommandFlags() { $mandatory_flags = $this->getMandatoryFlags(); if (!is_array($mandatory_flags)) { phutil_deprecated( 'String support for flags.', 'You should use list instead.'); $mandatory_flags = (array) $mandatory_flags; } $flags = nonempty($this->flags, $this->getDefaultFlags()); if (!is_array($flags)) { phutil_deprecated( 'String support for flags.', 'You should use list instead.'); $flags = (array) $flags; } return array_merge($mandatory_flags, $flags); } public function getCacheVersion() { $version = $this->getVersion(); if ($version) { return $version.'-'.json_encode($this->getCommandFlags()); } else { // Either we failed to parse the version number or the `getVersion` // function hasn't been implemented. return json_encode($this->getCommandFlags()); } } + + /** + * Prepare the path to be added to the command string. + * + * This method is expected to return an already escaped string. + * + * @param string Path to the file being linted + * @return string The command-ready file argument + */ + protected function getPathArgumentForLinterFuture($path) { + return csprintf('%s', $path); + } + protected function buildFutures(array $paths) { $executable = $this->getExecutableCommand(); $bin = csprintf('%C %Ls', $executable, $this->getCommandFlags()); $futures = array(); foreach ($paths as $path) { if ($this->supportsReadDataFromStdin()) { $future = new ExecFuture( '%C %C', $bin, $this->getReadDataFromStdinFilename()); $future->write($this->getEngine()->loadData($path)); } else { // TODO: In commit hook mode, we need to do more handling here. $disk_path = $this->getEngine()->getFilePathOnDisk($path); - $future = new ExecFuture('%C %s', $bin, $disk_path); + $path_argument = $this->getPathArgumentForLinterFuture($disk_path); + $future = new ExecFuture('%C %C', $bin, $path_argument); } $futures[$path] = $future; } return $futures; } protected function resolveFuture($path, Future $future) { list($err, $stdout, $stderr) = $future->resolve(); if ($err && !$this->shouldExpectCommandErrors()) { $future->resolvex(); } $messages = $this->parseLinterOutput($path, $err, $stdout, $stderr); if ($messages === false) { if ($err) { $future->resolvex(); } else { throw new Exception( "Linter failed to parse output!\n\n{$stdout}\n\n{$stderr}"); } } foreach ($messages as $message) { $this->addLintMessage($message); } } public function getLinterConfigurationOptions() { $options = array( 'bin' => 'optional string | list', 'flags' => 'optional list', ); if ($this->shouldUseInterpreter()) { $options['interpreter'] = 'optional string | list'; } return $options + parent::getLinterConfigurationOptions(); } public function setLinterConfigurationValue($key, $value) { switch ($key) { case 'interpreter': $working_copy = $this->getEngine()->getWorkingCopy(); $root = $working_copy->getProjectRoot(); foreach ((array)$value as $path) { if (Filesystem::binaryExists($path)) { $this->setInterpreter($path); return; } $path = Filesystem::resolvePath($path, $root); if (Filesystem::binaryExists($path)) { $this->setInterpreter($path); return; } } throw new Exception( pht('None of the configured interpreters can be located.')); case 'bin': $is_script = $this->shouldUseInterpreter(); $working_copy = $this->getEngine()->getWorkingCopy(); $root = $working_copy->getProjectRoot(); foreach ((array)$value as $path) { if (!$is_script && Filesystem::binaryExists($path)) { $this->setBinary($path); return; } $path = Filesystem::resolvePath($path, $root); if ((!$is_script && Filesystem::binaryExists($path)) || ($is_script && Filesystem::pathExists($path))) { $this->setBinary($path); return; } } throw new Exception( pht('None of the configured binaries can be located.')); case 'flags': if (!is_array($value)) { phutil_deprecated( 'String support for flags.', 'You should use list instead.'); $value = (array) $value; } $this->setFlags($value); return; } return parent::setLinterConfigurationValue($key, $value); } /** * 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; } }