diff --git a/.arcconfig b/.arcconfig --- a/.arcconfig +++ b/.arcconfig @@ -6,5 +6,6 @@ "git:arc.feature.start.default" : "origin/master", "arc.feature.start.default" : "master", "history.immutable" : false, - "load" : ["arcanist"] + "load" : ["arcanist"], + "lint.engine" : "BitcoinABCConfigurationDrivenLintEngine" } diff --git a/arcanist/.phutil_module_cache b/arcanist/.phutil_module_cache --- a/arcanist/.phutil_module_cache +++ b/arcanist/.phutil_module_cache @@ -1 +1 @@ -{"__symbol_cache_version__":11,"90a8b110dc475955f15bb81d37268cb5":{"have":{"class":{"AutoPEP8FormatLinter":75}},"need":{"function":{"pht":297,"execx":769,"id":1903},"class":{"ArcanistExternalLinter":104,"ArcanistLintMessage":1910,"Filesystem":1754,"ArcanistLinter":2017,"ArcanistLintSeverity":2095}},"xmap":{"AutoPEP8FormatLinter":["ArcanistExternalLinter"]}},"38f0c676bff5192a344464142caaa253":{"have":{"class":{"CHeaderLinter":99}},"need":{"function":{"pht":611},"class":{"ArcanistLinter":121,"ArcanistLintSeverity":1060,"Filesystem":1307}},"xmap":{"CHeaderLinter":["ArcanistLinter"]}},"a30e4e25376ca05d4ae719915441be9e":{"have":{"class":{"CheckDocLinter":106}},"need":{"function":{"pht":323,"id":1848},"class":{"ArcanistExternalLinter":129,"ArcanistLintMessage":1855,"Filesystem":731,"ArcanistLinter":1903,"ArcanistLintSeverity":1989}},"xmap":{"CheckDocLinter":["ArcanistExternalLinter"]}},"6af7410cfea496ff1d4dcc2624b6b8ea":{"have":{"class":{"ClangFormatLinter":79}},"need":{"function":{"pht":302,"execx":781,"id":1653},"class":{"ArcanistExternalLinter":105,"ArcanistLintMessage":1660,"Filesystem":1504,"ArcanistLinter":1767,"ArcanistLintSeverity":1845}},"xmap":{"ClangFormatLinter":["ArcanistExternalLinter"]}},"9285ad9415f8ebe564f7119e5a72c559":{"have":{"class":{"FormatStringLinter":146}},"need":{"function":{"pht":377,"csprintf":1492,"id":1872},"class":{"ArcanistExternalLinter":173,"ArcanistLintMessage":1879,"Filesystem":827,"ArcanistLinter":1956,"ArcanistLintSeverity":2044}},"xmap":{"FormatStringLinter":["ArcanistExternalLinter"]}},"224d394856b17878058b4c14acb7178b":{"have":{"class":{"LocaleDependenceLinter":160}},"need":{"function":{"pht":5400},"class":{"ArcanistLinter":191,"ArcanistLintSeverity":5903,"Filesystem":6149}},"xmap":{"LocaleDependenceLinter":["ArcanistLinter"]}},"6f2f22dd0f259fb2eaa284b4fab3bc29":{"have":{"class":{"PythonFormatLinter":123}},"need":{"function":{"pht":353,"id":1838},"class":{"ArcanistExternalLinter":150,"ArcanistLintMessage":1845,"Filesystem":776,"ArcanistLinter":1970,"ArcanistLintSeverity":2053}},"xmap":{"PythonFormatLinter":["ArcanistExternalLinter"]}},"25781df78f6eebfb223296b8265e9d19":{"have":{"class":{"TestsLinter":103}},"need":{"function":{"pht":318,"id":2629},"class":{"ArcanistExternalLinter":123,"ArcanistLintMessage":2636,"Filesystem":776,"ArcanistLinter":2684,"ArcanistLintSeverity":2792}},"xmap":{"TestsLinter":["ArcanistExternalLinter"]}},"2809b09d2021203b43c57da33d1fe8bf":{"have":{"class":{"AssertWithSideEffectsLinter":210}},"need":{"function":{"pht":439},"class":{"ArcanistLinter":246,"ArcanistLintSeverity":926,"Filesystem":1170}},"xmap":{"AssertWithSideEffectsLinter":["ArcanistLinter"]}}} \ No newline at end of file +{"__symbol_cache_version__":11,"2809b09d2021203b43c57da33d1fe8bf":{"have":{"class":{"AssertWithSideEffectsLinter":210}},"need":{"function":{"pht":439},"class":{"ArcanistLinter":246,"ArcanistLintSeverity":926,"Filesystem":1170}},"xmap":{"AssertWithSideEffectsLinter":["ArcanistLinter"]}},"90a8b110dc475955f15bb81d37268cb5":{"have":{"class":{"AutoPEP8FormatLinter":75}},"need":{"function":{"pht":297,"execx":769,"id":1903},"class":{"ArcanistExternalLinter":104,"ArcanistLintMessage":1910,"Filesystem":1754,"ArcanistLinter":2017,"ArcanistLintSeverity":2095}},"xmap":{"AutoPEP8FormatLinter":["ArcanistExternalLinter"]}},"38f0c676bff5192a344464142caaa253":{"have":{"class":{"CHeaderLinter":99}},"need":{"function":{"pht":611},"class":{"ArcanistLinter":121,"ArcanistLintSeverity":1060,"Filesystem":1307}},"xmap":{"CHeaderLinter":["ArcanistLinter"]}},"a30e4e25376ca05d4ae719915441be9e":{"have":{"class":{"CheckDocLinter":106}},"need":{"function":{"pht":323,"id":1848},"class":{"ArcanistExternalLinter":129,"ArcanistLintMessage":1855,"Filesystem":731,"ArcanistLinter":1903,"ArcanistLintSeverity":1989}},"xmap":{"CheckDocLinter":["ArcanistExternalLinter"]}},"6af7410cfea496ff1d4dcc2624b6b8ea":{"have":{"class":{"ClangFormatLinter":79}},"need":{"function":{"pht":302,"execx":781,"id":1653},"class":{"ArcanistExternalLinter":105,"ArcanistLintMessage":1660,"Filesystem":1504,"ArcanistLinter":1767,"ArcanistLintSeverity":1845}},"xmap":{"ClangFormatLinter":["ArcanistExternalLinter"]}},"9285ad9415f8ebe564f7119e5a72c559":{"have":{"class":{"FormatStringLinter":146}},"need":{"function":{"pht":377,"csprintf":1492,"id":1872},"class":{"ArcanistExternalLinter":173,"ArcanistLintMessage":1879,"Filesystem":827,"ArcanistLinter":1956,"ArcanistLintSeverity":2044}},"xmap":{"FormatStringLinter":["ArcanistExternalLinter"]}},"224d394856b17878058b4c14acb7178b":{"have":{"class":{"LocaleDependenceLinter":160}},"need":{"function":{"pht":5400},"class":{"ArcanistLinter":191,"ArcanistLintSeverity":5903,"Filesystem":6149}},"xmap":{"LocaleDependenceLinter":["ArcanistLinter"]}},"6f2f22dd0f259fb2eaa284b4fab3bc29":{"have":{"class":{"PythonFormatLinter":123}},"need":{"function":{"pht":353,"id":1838},"class":{"ArcanistExternalLinter":150,"ArcanistLintMessage":1845,"Filesystem":776,"ArcanistLinter":1970,"ArcanistLintSeverity":2053}},"xmap":{"PythonFormatLinter":["ArcanistExternalLinter"]}},"25781df78f6eebfb223296b8265e9d19":{"have":{"class":{"TestsLinter":103}},"need":{"function":{"pht":318,"id":2629},"class":{"ArcanistExternalLinter":123,"ArcanistLintMessage":2636,"Filesystem":776,"ArcanistLinter":2684,"ArcanistLintSeverity":2792}},"xmap":{"TestsLinter":["ArcanistExternalLinter"]}},"0ab29329c5371d373da9d1ad4ff7db97":{"have":{"class":{"BitcoinABCConfigurationDrivenLintEngine":13}},"need":{"function":{"pht":383,"phutil_json_decode":700,"idx":1440,"id":4148},"class":{"ArcanistLintEngine":61,"ArcanistUsageException":351,"PhutilProxyException":789,"PhutilClassMapQuery":4155,"Filesystem":295,"PhutilTypeSpec":1067,"PhutilConsole":3620},"class\/interface":{"PhutilJSONParserException":740,"PhutilTypeCheckException":1262}},"xmap":{"BitcoinABCConfigurationDrivenLintEngine":["ArcanistLintEngine"]}}} \ No newline at end of file diff --git a/arcanist/__phutil_library_map__.php b/arcanist/__phutil_library_map__.php --- a/arcanist/__phutil_library_map__.php +++ b/arcanist/__phutil_library_map__.php @@ -11,6 +11,7 @@ 'class' => array( 'AssertWithSideEffectsLinter' => 'linter/AssertWithSideEffectsLinter.php', 'AutoPEP8FormatLinter' => 'linter/AutoPEP8Linter.php', + 'BitcoinABCConfigurationDrivenLintEngine' => 'linter/engine/BitcoinABCConfigurationDrivenLintEngine.php', 'CHeaderLinter' => 'linter/CHeaderLinter.php', 'CheckDocLinter' => 'linter/CheckDocLinter.php', 'ClangFormatLinter' => 'linter/ClangFormatLinter.php', @@ -23,6 +24,7 @@ 'xmap' => array( 'AssertWithSideEffectsLinter' => 'ArcanistLinter', 'AutoPEP8FormatLinter' => 'ArcanistExternalLinter', + 'BitcoinABCConfigurationDrivenLintEngine' => 'ArcanistLintEngine', 'CHeaderLinter' => 'ArcanistLinter', 'CheckDocLinter' => 'ArcanistExternalLinter', 'ClangFormatLinter' => 'ArcanistExternalLinter', diff --git a/arcanist/linter/engine/BitcoinABCConfigurationDrivenLintEngine.php b/arcanist/linter/engine/BitcoinABCConfigurationDrivenLintEngine.php new file mode 100644 --- /dev/null +++ b/arcanist/linter/engine/BitcoinABCConfigurationDrivenLintEngine.php @@ -0,0 +1,195 @@ +getWorkingCopy(); + $config_path = $working_copy->getProjectPath($this->configurationFile); + + if (!Filesystem::pathExists($config_path)) { + throw new ArcanistUsageException( + pht( + "Unable to find '%s' file to configure linters. Create an ". + "'%s' file in the root directory of the working copy.", + $this->configurationFile, + $this->configurationFile)); + } + + $data = Filesystem::readFile($config_path); + $config = null; + try { + $config = phutil_json_decode($data); + } catch (PhutilJSONParserException $ex) { + throw new PhutilProxyException( + pht( + "Expected '%s' file to be a valid JSON file, but ". + "failed to decode '%s'.", + $this->configurationFile, + $config_path), + $ex); + } + + $linters = $this->loadAvailableLinters(); + + try { + PhutilTypeSpec::checkMap( + $config, + array( + 'exclude' => 'optional regex | list', + 'linters' => 'map>', + )); + } catch (PhutilTypeCheckException $ex) { + throw new PhutilProxyException( + pht("Error in parsing '%s' file.", $config_path), + $ex); + } + + $global_exclude = (array)idx($config, 'exclude', array()); + + $built_linters = array(); + $all_paths = $this->getPaths(); + foreach ($config['linters'] as $name => $spec) { + $type = idx($spec, 'type'); + if ($type !== null) { + if (empty($linters[$type])) { + throw new ArcanistUsageException( + pht( + "Linter '%s' specifies invalid type '%s'. ". + "Available linters are: %s.", + $name, + $type, + implode(', ', array_keys($linters)))); + } + + $linter = clone $linters[$type]; + $linter->setEngine($this); + $more = $linter->getLinterConfigurationOptions(); + + foreach ($more as $key => $option_spec) { + PhutilTypeSpec::checkMap( + $option_spec, + array( + 'type' => 'string', + 'help' => 'string', + )); + $more[$key] = $option_spec['type']; + } + } else { + // We'll raise an error below about the invalid "type" key. + $linter = null; + $more = array(); + } + + try { + PhutilTypeSpec::checkMap( + $spec, + array( + 'type' => 'string', + 'include' => 'optional regex | list', + 'exclude' => 'optional regex | list', + ) + $more); + } catch (PhutilTypeCheckException $ex) { + throw new PhutilProxyException( + pht( + "Error in parsing '%s' file, for linter '%s'.", + $this->configurationFile, + $name), + $ex); + } + + foreach ($more as $key => $value) { + if (array_key_exists($key, $spec)) { + try { + $linter->setLinterConfigurationValue($key, $spec[$key]); + } catch (Exception $ex) { + throw new PhutilProxyException( + pht( + "Error in parsing '%s' file, in key '%s' for linter '%s'.", + $this->configurationFile, + $key, + $name), + $ex); + } + } + } + + $include = (array)idx($spec, 'include', array()); + $exclude = (array)idx($spec, 'exclude', array()); + + $console = PhutilConsole::getConsole(); + $console->writeLog( + "%s\n", + pht("Examining paths for linter '%s'.", $name)); + $paths = $this->matchPaths( + $all_paths, + $include, + $exclude, + $global_exclude); + $console->writeLog( + "%s\n", + pht("Found %d matching paths for linter '%s'.", count($paths), $name)); + + $linter->setPaths($paths); + $built_linters[] = $linter; + } + + return $built_linters; + } + + private function loadAvailableLinters() { + return id(new PhutilClassMapQuery()) + ->setAncestorClass('ArcanistLinter') + ->setUniqueMethod('getLinterConfigurationName', true) + ->execute(); + } + + protected function matchPaths( + array $paths, + array $include, + array $exclude, + array $global_exclude) { + + $console = PhutilConsole::getConsole(); + + $match = array(); + foreach ($paths as $path) { + $keep = false; + if (!$include) { + $keep = true; + } else { + foreach ($include as $rule) { + if (preg_match($rule, $path)) { + $keep = true; + break; + } + } + } + + if (!$keep) { + continue; + } + + if ($exclude) { + foreach ($exclude as $rule) { + if (preg_match($rule, $path)) { + continue 2; + } + } + } + + if ($global_exclude) { + foreach ($global_exclude as $rule) { + if (preg_match($rule, $path)) { + continue 2; + } + } + } + + $match[] = $path; + } + + return $match; + } +}