diff --git a/.arcconfig b/.arcconfig --- a/.arcconfig +++ b/.arcconfig @@ -1,3 +1,4 @@ { - "phabricator.uri": "https://git.kolab.org" + "phabricator.uri": "https://git.kolab.org", + "load": ["phabricator/arc-phpstan"] } diff --git a/.arclint b/.arclint new file mode 100644 --- /dev/null +++ b/.arclint @@ -0,0 +1,21 @@ +{ + "linters": { + "php": { + "type": "php" + }, + "phpcs": { + "type": "phpcs", + "bin": "src/vendor/bin/phpcs", + "include": "(\\.php$)", + "exclude": "(^phabricator/)" + }, + "phpstan": { + "type": "phpstan", + "include": "(\\.php$)", + "exclude": "(^phabricator/)", + "config": "src/phpstan.neon", + "bin": "src/vendor/bin/phpstan", + "level": "6" + } + } +} diff --git a/phabricator/arc-phpstan/__phutil_library_init__.php b/phabricator/arc-phpstan/__phutil_library_init__.php new file mode 100755 --- /dev/null +++ b/phabricator/arc-phpstan/__phutil_library_init__.php @@ -0,0 +1,18 @@ + 2, + 'class' => array( + 'PhpstanLinter' => 'lint/linter/PhpstanLinter.php', + ), + 'function' => array(), + 'xmap' => array( + 'PhpstanLinter' => 'ArcanistExternalLinter', + ), +)); diff --git a/phabricator/arc-phpstan/lint/linter/PhpstanLinter.php b/phabricator/arc-phpstan/lint/linter/PhpstanLinter.php new file mode 100755 --- /dev/null +++ b/phabricator/arc-phpstan/lint/linter/PhpstanLinter.php @@ -0,0 +1,242 @@ +getExecutableCommand()); + + $matches = array(); + $regex = '/(?P\d+\.\d+\.\d+)/'; + if (preg_match($regex, $stdout, $matches)) { + return $matches['version']; + } else { + return false; + } + } + + protected function getMandatoryFlags() + { + $flags = array( + 'analyse', + '--no-progress', + '--error-format=checkstyle', + '--memory-limit=1G' + ); + if (null !== $this->configFile) { + array_push($flags, '-c', $this->configFile); + } + if (null !== $this->level) { + array_push($flags, '-l', $this->level); + } + if (null !== $this->autoloadFile) { + array_push($flags, '-a', $this->autoloadFile); + } + + return $flags; + } + + public function getLinterConfigurationOptions() + { + $options = array( + 'config' => array( + 'type' => 'optional string', + 'help' => pht( + 'The path to your phpstan.neon file. Will be provided as -c to phpstan.' + ), + ), + 'level' => array( + 'type' => 'optional string', + 'help' => pht( + 'Rule level used (0 loosest - max strictest). Will be provided as -l to phpstan.' + ), + ), + 'autoload' => array( + 'type' => 'optional string', + 'help' => pht( + 'The path to the auto load file. Will be provided as -a to phpstan.'), + ), + ); + return $options + parent::getLinterConfigurationOptions(); + } + + public function setLinterConfigurationValue($key, $value) + { + switch ($key) { + case 'config': + $this->configFile = $value; + return; + case 'level': + $this->level = $value; + return; + case 'autoload': + $this->autoloadFile = $value; + return; + default: + parent::setLinterConfigurationValue($key, $value); + return; + } + } + + protected function getDefaultMessageSeverity($code) + { + return ArcanistLintSeverity::SEVERITY_WARNING; + } + + protected function parseLinterOutput($path, $err, $stdout, $stderr) + { + $result = array(); + if (!empty($stdout)) { + $stdout = substr($stdout, strpos($stdout, 'xpath('//file/error'); + foreach($errors as $error) { + $violation = $this->parseViolation($error); + $violation['path'] = $path; + $result[] = ArcanistLintMessage::newFromDictionary($violation); + } + } + + return $result; + } + + /** + * Checkstyle returns output of the form + * + * + * + * + * ... + * + * + * + * Of this, we need to extract + * - Line + * - Column + * - Severity + * - Message + * - Source (name) + * + * @param SimpleXMLElement $violation The XML Entity containing the issue + * + * @return array of the form + * [ + * 'line' => {int}, + * 'column' => {int}, + * 'severity' => {string}, + * 'message' => {string} + * ] + */ + private function parseViolation(SimpleXMLElement $violation) + { + return array( + 'code' => $this->getLinterName(), + 'name' => (string)$violation['message'], + 'line' => (int)$violation['line'], + 'char' => (int)$violation['column'], + 'severity' => $this->getMatchSeverity((string)$violation['severity']), + 'description' => (string)$violation['message'] + ); + } + + /** + * @return string Linter name + */ + public function getLinterName() + { + return 'phpstan'; + } + + /** + * Map the regex matching groups to a message severity. We look for either + * a nonempty severity name group like 'error', or a group called 'severity' + * with a valid name. + * + * @param string $severity_name dict Captured groups from regex. + * + * @return string @{class:ArcanistLintSeverity} constant. + * + * @task parse + */ + private function getMatchSeverity($severity_name) + { + $map = array( + 'error' => ArcanistLintSeverity::SEVERITY_ERROR, + 'warning' => ArcanistLintSeverity::SEVERITY_WARNING, + 'info' => ArcanistLintSeverity::SEVERITY_ADVICE, + ); + foreach ($map as $name => $severity) { + if ($severity_name == $name) { + return $severity; + } + } + return ArcanistLintSeverity::SEVERITY_ERROR; + } +}