flags = $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 string Composed flags. * @task exec */ protected function getCommandFlags() { return csprintf( '%C %C', $this->getMandatoryFlags(), coalesce($this->flags, $this->getDefaultFlags())); } protected function buildFutures(array $paths) { $executable = $this->getExecutableCommand(); $bin = csprintf('%C %C', $executable, $this->getCommandFlags()); $futures = array(); foreach ($paths as $path) { if ($this->supportsReadDataFromStdin()) { $future = new ExecFuture( '%C %C', $bin, $this->getReadDataFromStdinFilename()); $future->write($this->getFileData($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); } $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) { $future->resolvex(); return; } foreach ($messages as $message) { $this->addLintMessage($message); } } public function getLinterConfigurationOptions() { $options = array( 'bin' => 'optional string | list', 'flags' => 'optional string', ); if ($this->shouldUseInterpreter()) { $options['interpreter'] = 'optional string | list'; } return $options; } 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 (strlen($value)) { $this->setFlags($value); } break; } return parent::setLinterConfigurationValue($key, $value); } }