diff --git a/.arc/arc-phpstan/__phutil_library_init__.php b/.arc/arc-phpstan/__phutil_library_init__.php new file mode 100755 --- /dev/null +++ b/.arc/arc-phpstan/__phutil_library_init__.php @@ -0,0 +1,18 @@ +<?php +/* + Copyright 2017-present Appsinet. All Rights Reserved. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ + +phutil_register_library('phpstan', __FILE__); diff --git a/.arc/arc-phpstan/__phutil_library_map__.php b/.arc/arc-phpstan/__phutil_library_map__.php new file mode 100755 --- /dev/null +++ b/.arc/arc-phpstan/__phutil_library_map__.php @@ -0,0 +1,18 @@ +<?php + +/** + * This file is automatically generated. Use 'arc liberate' to rebuild it. + * + * @generated + * @phutil-library-version 2 + */ +phutil_register_library_map(array( + '__library_version__' => 2, + 'class' => array( + 'PhpstanLinter' => 'lint/linter/PhpstanLinter.php', + ), + 'function' => array(), + 'xmap' => array( + 'PhpstanLinter' => 'ArcanistExternalLinter', + ), +)); diff --git a/.arc/arc-phpstan/lint/linter/PhpstanLinter.php b/.arc/arc-phpstan/lint/linter/PhpstanLinter.php new file mode 100755 --- /dev/null +++ b/.arc/arc-phpstan/lint/linter/PhpstanLinter.php @@ -0,0 +1,242 @@ +<?php +/** + * @copyright Copyright 2017-present Appsinet. All Rights Reserved. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ + +/** Uses phpstan to lint php files */ +final class PhpstanLinter extends ArcanistExternalLinter +{ + + /** + * @var string Config file path + */ + private $configFile = null; + + /** + * @var string Rule level + */ + private $level = null; + + /** + * @var string Autoload file path + */ + private $autoloadFile = null; + + public function getInfoName() + { + return 'phpstan'; + } + + public function getInfoURI() + { + return ''; + } + + public function getInfoDescription() + { + return pht('Use phpstan for processing specified files.'); + } + + public function getLinterConfigurationName() + { + return 'phpstan'; + } + + public function getDefaultBinary() + { + return 'phpstan'; + } + + public function getInstallInstructions() + { + return pht('Install phpstan following the official guide at https://github.com/phpstan/phpstan#installation'); + } + + public function shouldExpectCommandErrors() + { + return true; + } + + public function getVersion() + { + list($stdout) = execx('%C --version', $this->getExecutableCommand()); + + $matches = array(); + $regex = '/(?P<version>\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 <path> to phpstan.' + ), + ), + 'level' => array( + 'type' => 'optional string', + 'help' => pht( + 'Rule level used (0 loosest - max strictest). Will be provided as -l <level> to phpstan.' + ), + ), + 'autoload' => array( + 'type' => 'optional string', + 'help' => pht( + 'The path to the auto load file. Will be provided as -a <autoload_file> 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, '<?xml')); + $checkstyleOutpout = new SimpleXMLElement($stdout); + $errors = $checkstyleOutpout->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 + * + * <checkstyle> + * <file name="${sPath}"> + * <error line="12" column="10" severity="${sSeverity}" message="${sMessage}" source="${sSource}"> + * ... + * </file> + * </checkstyle> + * + * 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; + } +} diff --git a/.arc/src/extensions/PhpstanLinter.php b/.arc/src/extensions/PhpstanLinter.php new file mode 100755 --- /dev/null +++ b/.arc/src/extensions/PhpstanLinter.php @@ -0,0 +1,242 @@ +<?php +/** + * @copyright Copyright 2017-present Appsinet. All Rights Reserved. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ + +/** Uses phpstan to lint php files */ +final class PhpstanLinter extends ArcanistExternalLinter +{ + + /** + * @var string Config file path + */ + private $configFile = null; + + /** + * @var string Rule level + */ + private $level = null; + + /** + * @var string Autoload file path + */ + private $autoloadFile = null; + + public function getInfoName() + { + return 'phpstan'; + } + + public function getInfoURI() + { + return ''; + } + + public function getInfoDescription() + { + return pht('Use phpstan for processing specified files.'); + } + + public function getLinterConfigurationName() + { + return 'phpstan'; + } + + public function getDefaultBinary() + { + return 'phpstan'; + } + + public function getInstallInstructions() + { + return pht('Install phpstan following the official guide at https://github.com/phpstan/phpstan#installation'); + } + + public function shouldExpectCommandErrors() + { + return true; + } + + public function getVersion() + { + list($stdout) = execx('%C --version', $this->getExecutableCommand()); + + $matches = array(); + $regex = '/(?P<version>\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 <path> to phpstan.' + ), + ), + 'level' => array( + 'type' => 'optional string', + 'help' => pht( + 'Rule level used (0 loosest - max strictest). Will be provided as -l <level> to phpstan.' + ), + ), + 'autoload' => array( + 'type' => 'optional string', + 'help' => pht( + 'The path to the auto load file. Will be provided as -a <autoload_file> 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, '<?xml')); + $checkstyleOutpout = new SimpleXMLElement($stdout); + $errors = $checkstyleOutpout->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 + * + * <checkstyle> + * <file name="${sPath}"> + * <error line="12" column="10" severity="${sSeverity}" message="${sMessage}" source="${sSource}"> + * ... + * </file> + * </checkstyle> + * + * 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; + } +} 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": [".arc/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": "(^.arc/)" + }, + "phpstan": { + "type": "phpstan", + "include": "(\\.php$)", + "exclude": "(^.arc/)", + "config": "src/phpstan.neon", + "bin": "src/vendor/bin/phpstan", + "level": "4" + } + } +}