diff --git a/src/lint/linter/ArcanistExternalLinter.php b/src/lint/linter/ArcanistExternalLinter.php --- a/src/lint/linter/ArcanistExternalLinter.php +++ b/src/lint/linter/ArcanistExternalLinter.php @@ -13,6 +13,7 @@ private $bin; private $interpreter; private $flags; + private $versionRequirement; /* -( Interpreters, Binaries and Flags )----------------------------------- */ @@ -43,6 +44,16 @@ abstract public function getInstallInstructions(); /** + * Return a human-readable string describing how to upgrade the linter. + * + * @return string Human readable upgrade instructions + * @task bin + */ + public function getUpgradeInstructions() { + return null; + } + + /** * Return true to continue when the external linter exits with an error code. * By default, linters which exit with an error code are assumed to have * failed. However, some linters exit with a specific code to indicate that @@ -103,6 +114,18 @@ } /** + * Set the binary's version requirement. + * + * @param string Version requirement. + * @return this + * @task bin + */ + final public function setVersionRequirement($version) { + $this->versionRequirement = trim($version); + return $this; + } + + /** * Return the binary or script to execute. This method synthesizes defaults * and configuration. You can override the binary with @{method:setBinary}. * @@ -259,6 +282,60 @@ } /** + * If a binary version requirement has been specified, compare the version + * of the configured binary to the required version, and if the binary's + * version is not supported, throw an exception. + * + * @param string Version string to check. + * @return void + */ + final protected function checkBinaryVersion($version) { + if (!$this->versionRequirement) { + return; + } + + if (!$version) { + $message = pht( + 'Linter %s requires %s version %s. Unable to determine the version '. + 'that you have installed.', + get_class($this), + $this->getBinary()); + + $instructions = $this->getUpgradeInstructions(); + if ($instructions) { + $message .= "\n".pht('TO UPGRADE: %s', $instructions); + } + + throw new ArcanistMissingLinterException($message); + } + + $operator = '=='; + $compare_to = $this->versionRequirement; + + $matches = null; + if (preg_match('/^([<>]=?|==)\s*(.*)$/', $compare_to, $matches)) { + $operator = $matches[1]; + $compare_to = $matches[2]; + } + + if (!version_compare($version, $compare_to, $operator)) { + $message = pht( + 'Linter %s requires %s version %s. You have version %s.', + get_class($this), + $this->getBinary(), + $this->versionRequirement, + $version); + + $instructions = $this->getUpgradeInstructions(); + if ($instructions) { + $message .= "\n".pht('TO UPGRADE: %s', $instructions); + } + + throw new ArcanistMissingLinterException($message); + } + } + + /** * Get the composed executable command, including the interpreter and binary * but without flags or paths. This can be used to execute `--version` * commands. @@ -308,6 +385,7 @@ $version = $this->getVersion(); if ($version) { + $this->checkBinaryVersion($version); return $version.'-'.json_encode($this->getCommandFlags()); } else { // Either we failed to parse the version number or the `getVersion` @@ -395,6 +473,13 @@ 'Provide a list of additional flags to pass to the linter on the '. 'command line.'), ), + 'version' => array( + 'type' => 'optional string', + 'help' => pht( + 'Specify a version requirement for the binary. The version number '. + 'may be prefixed with <, <=, >, >=, or == to specify the version '. + 'comparison operator (default: ==).'), + ), ); if ($this->shouldUseInterpreter()) { @@ -456,6 +541,9 @@ case 'flags': $this->setFlags($value); return; + case 'version': + $this->setVersionRequirement($value); + return; } return parent::setLinterConfigurationValue($key, $value);