src/lint/linter/UberShellCheckLinter.php (148 lines of code) (raw):

<?php /** This linter invokes shellcheck to check on shell code standards */ final class UberShellCheckLinter extends ArcanistExternalLinter { private $excluded_rules = NULL; private $shell = 'bash'; private $warning_as_error = FALSE; private $defaultSeverityMap = array(); public function getInfoName() { return 'ShellCheck'; } public function getInfoURI() { return 'http://www.shellcheck.net/'; } public function getInfoDescription() { return pht( 'ShellCheck is a static analysis and linting tool for %s scripts.', 'sh/bash'); } public function getLinterName() { return 'SHELLCHECK'; } public function getLinterConfigurationName() { return 'shellcheck'; } public function getLinterConfigurationOptions() { $options = array( 'shellcheck.script' => array( 'type' => 'optional string', 'help' => pht('Shellcheck script to execute. Script must output shellcheck in XML to $stdout'), ), 'shellcheck.shell' => array( 'type' => 'optional string', 'help' => pht( 'Specify shell dialect (%s, %s, %s, %s).', 'bash', 'sh', 'ksh', 'zsh'), ), 'shellcheck.excluded_rules' => array( 'type' => 'optional list<string>', 'help' => pht('List of excluded shellcheck rule(s)'), ), 'shellcheck.warning_as_error' => array( 'type' => 'optional bool', 'help' => pht('Whether to treat warnings as errors'), ), ); return $options + parent::getLinterConfigurationOptions(); } public function setLinterConfigurationValue($key, $value) { switch ($key) { case 'shellcheck.script': $this->setBinary($value); return; case 'shellcheck.shell': $this->setShell($value); return; case 'shellcheck.excluded_rules': $this->setExcludedRules($value); return; case 'shellcheck.warning_as_error': $this->setWarningAsError($value); return; default: return parent::setLinterConfigurationValue($key, $value); } } public function setExcludedRules($excluded_rules) { $this->excluded_rules = $excluded_rules; return $this; } public function setShell($shell) { $this->shell = $shell; return $this; } public function setWarningAsError($warning_as_error) { $this->warning_as_error = $warning_as_error; return $this; } public function getDefaultBinary() { return 'shellcheck'; } public function getInstallInstructions() { return pht( 'Install ShellCheck with `%s`.', 'brew install shellcheck'); } protected function getMandatoryFlags() { $options = array(); if ($this->excluded_rules && count($this->excluded_rules) > 0) { $options[] = '--exclude='.implode(',', $this->excluded_rules); } $options[] = '--format=checkstyle'; if ($this->shell) { $options[] = '--shell='.$this->shell; } return $options; } public function getVersion() { list($stdout, $stderr) = execx( '%C --version', $this->getExecutableCommand()); $matches = null; if (preg_match('/^version: (\d(?:\.\d){2})$/', $stdout, $matches)) { return $matches[1]; } return null; } protected function getDefaultMessageSeverity($code) { switch ($this->defaultSeverityMap[$code]) { case 'error': return ArcanistLintSeverity::SEVERITY_ERROR; case 'warning': return $this->warning_as_error ? ArcanistLintSeverity::SEVERITY_ERROR : ArcanistLintSeverity::SEVERITY_WARNING; case 'info': return ArcanistLintSeverity::SEVERITY_ADVICE; default: return ArcanistLintSeverity::SEVERITY_ERROR; } } protected function parseLinterOutput($path, $err, $stdout, $stderr) { $report_dom = new DOMDocument(); $ok = @$report_dom->loadXML($stdout); if (!$ok) { return false; } $files = $report_dom->getElementsByTagName('file'); $messages = array(); foreach ($files as $file) { foreach ($file->getElementsByTagName('error') as $child) { $line = $child->getAttribute('line'); $code = str_replace('ShellCheck.', '', $child->getAttribute('source')); $this->defaultSeverityMap[$code] = $child->getAttribute('severity'); $message = id(new ArcanistLintMessage()) ->setPath($path) ->setLine($child->getAttribute('line')) ->setChar($child->getAttribute('column')) ->setName($this->getLinterName()) ->setCode($code) ->setDescription($child->getAttribute('message')) ->setSeverity($this->getLintMessageSeverity($code)); $messages[] = $message; } } return $messages; } }