diff --git a/src/lint/engine/ArcanistLintEngine.php b/src/lint/engine/ArcanistLintEngine.php index b98a4514..8a4d1212 100644 --- a/src/lint/engine/ArcanistLintEngine.php +++ b/src/lint/engine/ArcanistLintEngine.php @@ -1,654 +1,654 @@ configurationManager = $configuration_manager; return $this; } final public function getConfigurationManager() { return $this->configurationManager; } final public function setWorkingCopy( ArcanistWorkingCopyIdentity $working_copy) { $this->workingCopy = $working_copy; return $this; } final public function getWorkingCopy() { return $this->workingCopy; } final public function setPaths($paths) { $this->paths = $paths; return $this; } public function getPaths() { return $this->paths; } final public function setPathChangedLines($path, $changed) { if ($changed === null) { $this->changedLines[$path] = null; } else { $this->changedLines[$path] = array_fill_keys($changed, true); } return $this; } final public function getPathChangedLines($path) { return idx($this->changedLines, $path); } final public function setFileData($data) { $this->fileData = $data + $this->fileData; return $this; } final public function setCommitHookMode($mode) { $this->commitHookMode = $mode; return $this; } final public function setHookAPI(ArcanistHookAPI $hook_api) { $this->hookAPI = $hook_api; return $this; } final public function getHookAPI() { return $this->hookAPI; } final public function setEnableAsyncLint($enable_async_lint) { $this->enableAsyncLint = $enable_async_lint; return $this; } final public function getEnableAsyncLint() { return $this->enableAsyncLint; } final public function loadData($path) { if (!isset($this->fileData[$path])) { if ($this->getCommitHookMode()) { $this->fileData[$path] = $this->getHookAPI() ->getCurrentFileData($path); } else { - $disk_path = $this->getFilePathOnDisk($path); - $this->fileData[$path] = Filesystem::readFile($disk_path); + $disk_path = $this->getFilePathOnDisk($path); + $this->fileData[$path] = Filesystem::readFile($disk_path); } } return $this->fileData[$path]; } public function pathExists($path) { if ($this->getCommitHookMode()) { $file_data = $this->loadData($path); return ($file_data !== null); } else { $disk_path = $this->getFilePathOnDisk($path); return Filesystem::pathExists($disk_path); } } final public function isDirectory($path) { if ($this->getCommitHookMode()) { // TODO: This won't get the right result in every case (we need more // metadata) but should almost always be correct. try { $this->loadData($path); return false; } catch (Exception $ex) { return true; } } else { $disk_path = $this->getFilePathOnDisk($path); return is_dir($disk_path); } } final public function isBinaryFile($path) { try { $data = $this->loadData($path); } catch (Exception $ex) { return false; } return ArcanistDiffUtils::isHeuristicBinaryFile($data); } final public function isSymbolicLink($path) { return is_link($this->getFilePathOnDisk($path)); } final public function getFilePathOnDisk($path) { return Filesystem::resolvePath( $path, $this->getWorkingCopy()->getProjectRoot()); } final public function setMinimumSeverity($severity) { $this->minimumSeverity = $severity; return $this; } final public function getCommitHookMode() { return $this->commitHookMode; } final public function run() { $linters = $this->buildLinters(); if (!$linters) { throw new ArcanistNoEffectException('No linters to run.'); } foreach ($linters as $key => $linter) { $linter->setLinterID($key); } $linters = msort($linters, 'getLinterPriority'); foreach ($linters as $linter) { $linter->setEngine($this); } $have_paths = false; foreach ($linters as $linter) { if ($linter->getPaths()) { $have_paths = true; break; } } if (!$have_paths) { throw new ArcanistNoEffectException('No paths are lintable.'); } $versions = array($this->getCacheVersion()); foreach ($linters as $linter) { $version = get_class($linter).':'.$linter->getCacheVersion(); $symbols = id(new PhutilSymbolLoader()) ->setType('class') ->setName(get_class($linter)) ->selectSymbolsWithoutLoading(); $symbol = idx($symbols, 'class$'.get_class($linter)); if ($symbol) { $version .= ':'.md5_file( phutil_get_library_root($symbol['library']).'/'.$symbol['where']); } $versions[] = $version; } $this->cacheVersion = crc32(implode("\n", $versions)); $runnable = $this->getRunnableLinters($linters); $this->stopped = array(); $exceptions = $this->executeLinters($runnable); foreach ($runnable as $linter) { foreach ($linter->getLintMessages() as $message) { if (!$this->isSeverityEnabled($message->getSeverity())) { continue; } if (!$this->isRelevantMessage($message)) { continue; } $message->setGranularity($linter->getCacheGranularity()); $result = $this->getResultForPath($message->getPath()); $result->addMessage($message); } } if ($this->cachedResults) { foreach ($this->cachedResults as $path => $messages) { $messages = idx($messages, $this->cacheVersion, array()); $repository_version = idx($messages, 'repository_version'); unset($messages['stopped']); unset($messages['repository_version']); foreach ($messages as $message) { $use_cache = $this->shouldUseCache( idx($message, 'granularity'), $repository_version); if ($use_cache) { $this->getResultForPath($path)->addMessage( ArcanistLintMessage::newFromDictionary($message)); } } } } foreach ($this->results as $path => $result) { $disk_path = $this->getFilePathOnDisk($path); $result->setFilePathOnDisk($disk_path); if (isset($this->fileData[$path])) { $result->setData($this->fileData[$path]); } else if ($disk_path && Filesystem::pathExists($disk_path)) { // TODO: this may cause us to, e.g., load a large binary when we only // raised an error about its filename. We could refine this by looking // through the lint messages and doing this load only if any of them // have original/replacement text or something like that. try { $this->fileData[$path] = Filesystem::readFile($disk_path); $result->setData($this->fileData[$path]); } catch (FilesystemException $ex) { // Ignore this, it's noncritical that we access this data and it // might be unreadable or a directory or whatever else for plenty // of legitimate reasons. } } } if ($exceptions) { throw new PhutilAggregateException('Some linters failed:', $exceptions); } return $this->results; } final public function isSeverityEnabled($severity) { $minimum = $this->minimumSeverity; return ArcanistLintSeverity::isAtLeastAsSevere($severity, $minimum); } final private function shouldUseCache( $cache_granularity, $repository_version) { if ($this->commitHookMode) { return false; } switch ($cache_granularity) { case ArcanistLinter::GRANULARITY_FILE: return true; case ArcanistLinter::GRANULARITY_DIRECTORY: case ArcanistLinter::GRANULARITY_REPOSITORY: return ($this->repositoryVersion == $repository_version); default: return false; } } /** * @param dict>> * @return this */ final public function setCachedResults(array $results) { $this->cachedResults = $results; return $this; } final public function getResults() { return $this->results; } final public function getStoppedPaths() { return $this->stopped; } abstract public function buildLinters(); final public function setRepositoryVersion($version) { $this->repositoryVersion = $version; return $this; } final private function isRelevantMessage(ArcanistLintMessage $message) { // When a user runs "arc lint", we default to raising only warnings on // lines they have changed (errors are still raised anywhere in the // file). The list of $changed lines may be null, to indicate that the // path is a directory or a binary file so we should not exclude // warnings. if (!$this->changedLines || $message->isError() || $message->shouldBypassChangedLineFiltering()) { return true; } $locations = $message->getOtherLocations(); $locations[] = $message->toDictionary(); foreach ($locations as $location) { $path = idx($location, 'path', $message->getPath()); if (!array_key_exists($path, $this->changedLines)) { continue; } $changed = $this->getPathChangedLines($path); if ($changed === null || !$location['line']) { return true; } $last_line = $location['line']; if (isset($location['original'])) { $last_line += substr_count($location['original'], "\n"); } for ($l = $location['line']; $l <= $last_line; $l++) { if (!empty($changed[$l])) { return true; } } } return false; } final protected function getResultForPath($path) { if (empty($this->results[$path])) { $result = new ArcanistLintResult(); $result->setPath($path); $result->setCacheVersion($this->cacheVersion); $this->results[$path] = $result; } return $this->results[$path]; } final public function getLineAndCharFromOffset($path, $offset) { if (!isset($this->charToLine[$path])) { $char_to_line = array(); $line_to_first_char = array(); $lines = explode("\n", $this->loadData($path)); $line_number = 0; $line_start = 0; foreach ($lines as $line) { $len = strlen($line) + 1; // Account for "\n". $line_to_first_char[] = $line_start; $line_start += $len; for ($ii = 0; $ii < $len; $ii++) { $char_to_line[] = $line_number; } $line_number++; } $this->charToLine[$path] = $char_to_line; $this->lineToFirstChar[$path] = $line_to_first_char; } $line = $this->charToLine[$path][$offset]; $char = $offset - $this->lineToFirstChar[$path][$line]; return array($line, $char); } final public function getPostponedLinters() { return $this->postponedLinters; } final public function setPostponedLinters(array $linters) { $this->postponedLinters = $linters; return $this; } protected function getCacheVersion() { return 1; } /** * Get a named linter resource shared by another linter. * * This mechanism allows linters to share arbitrary resources, like the * results of computation. If several linters need to perform the same * expensive computation step, they can use a named resource to synchronize * construction of the result so it doesn't need to be built multiple * times. * * @param string Resource identifier. * @param wild Optionally, default value to return if resource does not * exist. * @return wild Resource, or default value if not present. */ public function getLinterResource($key, $default = null) { return idx($this->linterResources, $key, $default); } /** * Set a linter resource that other linters can access. * * See @{method:getLinterResource} for a description of this mechanism. * * @param string Resource identifier. * @param wild Resource. * @return this */ public function setLinterResource($key, $value) { $this->linterResources[$key] = $value; return $this; } private function getRunnableLinters(array $linters) { assert_instances_of($linters, 'ArcanistLinter'); // TODO: The canRun() mechanism is only used by one linter, and just // silently disables the linter. Almost every other linter handles this // by throwing `ArcanistMissingLinterException`. Both mechanisms are not // ideal; linters which can not run should emit a message, get marked as // "skipped", and allow execution to continue. See T7045. $runnable = array(); foreach ($linters as $key => $linter) { if ($linter->canRun()) { $runnable[$key] = $linter; } } return $runnable; } private function executeLinters(array $runnable) { $all_paths = $this->getPaths(); $path_chunks = array_chunk($all_paths, 32, $preserve_keys = true); $exception_lists = array(); foreach ($path_chunks as $chunk) { $exception_lists[] = $this->executeLintersOnChunk($runnable, $chunk); } return array_mergev($exception_lists); } private function executeLintersOnChunk(array $runnable, array $path_list) { assert_instances_of($runnable, 'ArcanistLinter'); $path_map = array_fuse($path_list); $exceptions = array(); $did_lint = array(); foreach ($runnable as $linter) { $linter_id = $linter->getLinterID(); $paths = $linter->getPaths(); foreach ($paths as $key => $path) { // If we aren't running this path in the current chunk of paths, // skip it completely. if (empty($path_map[$path])) { unset($paths[$key]); continue; } // Make sure each path has a result generated, even if it is empty // (i.e., the file has no lint messages). $result = $this->getResultForPath($path); // If a linter has stopped all other linters for this path, don't // actually run the linter. if (isset($this->stopped[$path])) { unset($paths[$key]); continue; } // If we have a cached result for this path, don't actually run the // linter. if (isset($this->cachedResults[$path][$this->cacheVersion])) { $cached_result = $this->cachedResults[$path][$this->cacheVersion]; $use_cache = $this->shouldUseCache( $linter->getCacheGranularity(), idx($cached_result, 'repository_version')); if ($use_cache) { unset($paths[$key]); if (idx($cached_result, 'stopped') == $linter_id) { $this->stopped[$path] = $linter_id; } } } } $paths = array_values($paths); if (!$paths) { continue; } try { $this->executeLinterOnPaths($linter, $paths); $did_lint[] = array($linter, $paths); } catch (Exception $ex) { $exceptions[] = $ex; } } foreach ($did_lint as $info) { list($linter, $paths) = $info; try { $this->executeDidLintOnPaths($linter, $paths); } catch (Exception $ex) { $exceptions[] = $ex; } } return $exceptions; } private function beginLintServiceCall(ArcanistLinter $linter, array $paths) { $profiler = PhutilServiceProfiler::getInstance(); return $profiler->beginServiceCall( array( 'type' => 'lint', 'linter' => $linter->getInfoName(), 'paths' => $paths, )); } private function endLintServiceCall($call_id) { $profiler = PhutilServiceProfiler::getInstance(); $profiler->endServiceCall($call_id, array()); } private function executeLinterOnPaths(ArcanistLinter $linter, array $paths) { $call_id = $this->beginLintServiceCall($linter, $paths); try { $linter->willLintPaths($paths); foreach ($paths as $path) { $linter->setActivePath($path); $linter->lintPath($path); if ($linter->didStopAllLinters()) { $this->stopped[$path] = $linter->getLinterID(); } } } catch (Exception $ex) { $this->endLintServiceCall($call_id); throw $ex; } $this->endLintServiceCall($call_id); } private function executeDidLintOnPaths(ArcanistLinter $linter, array $paths) { $call_id = $this->beginLintServiceCall($linter, $paths); try { $linter->didLintPaths($paths); } catch (Exception $ex) { $this->endLintServiceCall($call_id); throw $ex; } $this->endLintServiceCall($call_id); } } diff --git a/src/lint/linter/ArcanistClosureLinter.php b/src/lint/linter/ArcanistClosureLinter.php index e6f77187..78d4d428 100644 --- a/src/lint/linter/ArcanistClosureLinter.php +++ b/src/lint/linter/ArcanistClosureLinter.php @@ -1,66 +1,62 @@ setPath($path) ->setLine($matches[1]) ->setSeverity(ArcanistLintSeverity::SEVERITY_ERROR) ->setCode($this->getLinterName().$matches[2]) ->setDescription($matches[3]); $messages[] = $message; } return $messages; } } diff --git a/src/lint/linter/ArcanistCoffeeLintLinter.php b/src/lint/linter/ArcanistCoffeeLintLinter.php index 66319037..3301356a 100644 --- a/src/lint/linter/ArcanistCoffeeLintLinter.php +++ b/src/lint/linter/ArcanistCoffeeLintLinter.php @@ -1,147 +1,139 @@ getExecutableCommand()); $matches = array(); if (preg_match('/^(?P\d+\.\d+\.\d+)$/', $stdout, $matches)) { return $matches['version']; } else { return false; } } public function getInstallInstructions() { return pht( 'Install CoffeeLint using `%s`.', 'npm install -g coffeelint'); } - public function supportsReadDataFromStdin() { - return true; - } - - public function getReadDataFromStdinFilename() { - return '--stdin'; - } - protected function getMandatoryFlags() { $options = array( '--reporter=raw', ); if ($this->config) { $options[] = '--file='.$this->config; } return $options; } public function getLinterConfigurationOptions() { $options = array( 'coffeelint.config' => array( 'type' => 'optional string', 'help' => pht('A custom configuration file.'), ), ); return $options + parent::getLinterConfigurationOptions(); } public function setLinterConfigurationValue($key, $value) { switch ($key) { case 'coffeelint.config': $this->config = $value; return; } return parent::setLinterConfigurationValue($key, $value); } protected function parseLinterOutput($path, $err, $stdout, $stderr) { $messages = array(); $output = phutil_json_decode($stdout); // We are only linting a single file. if (count($output) != 1) { return false; } foreach ($output as $reports) { foreach ($reports as $report) { // Column number is not provided in the output. // See https://github.com/clutchski/coffeelint/issues/87 $message = id(new ArcanistLintMessage()) ->setPath($path) ->setLine($report['lineNumber']) ->setCode($this->getLinterName()) ->setName(ucwords(str_replace('_', ' ', $report['name']))) ->setDescription($report['message']) ->setOriginalText(idx($report, 'line')); switch ($report['level']) { case 'warn': $message->setSeverity(ArcanistLintSeverity::SEVERITY_WARNING); break; case 'error': $message->setSeverity(ArcanistLintSeverity::SEVERITY_ERROR); break; default: $message->setSeverity(ArcanistLintSeverity::SEVERITY_ADVICE); break; } $messages[] = $message; } } if ($err && !$messages) { return false; } return $messages; } protected function getLintCodeFromLinterConfigurationKey($code) { // NOTE: We can't figure out which rule generated each message, so we // can not customize severities. throw new Exception( pht( "CoffeeLint does not currently support custom severity levels, ". "because rules can't be identified from messages in output.")); } } diff --git a/src/lint/linter/ArcanistCpplintLinter.php b/src/lint/linter/ArcanistCpplintLinter.php index 0e3f5b66..7d8b4bbc 100644 --- a/src/lint/linter/ArcanistCpplintLinter.php +++ b/src/lint/linter/ArcanistCpplintLinter.php @@ -1,90 +1,82 @@ getDeprecatedConfiguration('lint.cpplint.prefix'); $bin = $this->getDeprecatedConfiguration('lint.cpplint.bin', 'cpplint.py'); if ($prefix) { return $prefix.'/'.$bin; } else { return $bin; } } public function getInstallInstructions() { return pht('Install cpplint.py using `wget http://google-styleguide.'. 'googlecode.com/svn/trunk/cpplint/cpplint.py`.'); } - public function supportsReadDataFromStdin() { - return true; - } - - public function getReadDataFromStdinFilename() { - return '-'; - } - protected function getDefaultFlags() { return $this->getDeprecatedConfiguration('lint.cpplint.options', array()); } protected function parseLinterOutput($path, $err, $stdout, $stderr) { $lines = explode("\n", $stderr); $messages = array(); foreach ($lines as $line) { $line = trim($line); $matches = null; $regex = '/^-:(\d+):\s*(.*)\s*\[(.*)\] \[(\d+)\]$/'; if (!preg_match($regex, $line, $matches)) { continue; } foreach ($matches as $key => $match) { $matches[$key] = trim($match); } $message = new ArcanistLintMessage(); $message->setPath($path); $message->setLine($matches[1]); $message->setCode($matches[3]); $message->setName($matches[3]); $message->setDescription($matches[2]); $message->setSeverity(ArcanistLintSeverity::SEVERITY_WARNING); $messages[] = $message; } if ($err && !$messages) { return false; } return $messages; } protected function getLintCodeFromLinterConfigurationKey($code) { if (!preg_match('@^[a-z_]+/[a-z_]+$@', $code)) { throw new Exception( pht( 'Unrecognized lint message code "%s". Expected a valid cpplint '. 'lint code like "%s" or "%s".', $code, 'build/include_order', 'whitespace/braces')); } return $code; } } diff --git a/src/lint/linter/ArcanistExternalLinter.php b/src/lint/linter/ArcanistExternalLinter.php index 08f3e5cb..b8aceb8e 100644 --- a/src/lint/linter/ArcanistExternalLinter.php +++ b/src/lint/linter/ArcanistExternalLinter.php @@ -1,520 +1,467 @@ Mandatory flags, like `"--format=xml"`. * @task bin */ protected function getMandatoryFlags() { return array(); } /** * Provide default, overridable flags to the linter. Generally these are * configuration flags which affect behavior but aren't critical. Flags * which are required should be provided in @{method:getMandatoryFlags} * instead. * * Default flags can be overridden with @{method:setFlags}. * * @return list Overridable default flags. * @task bin */ protected function getDefaultFlags() { return array(); } /** * Override default flags with custom flags. If not overridden, flags provided * by @{method:getDefaultFlags} are used. * * @param list New flags. * @return this * @task bin */ final public function setFlags(array $flags) { $this->flags = $flags; return $this; } /** * Return the binary or script to execute. This method synthesizes defaults * and configuration. You can override the binary with @{method:setBinary}. * * @return string Binary to execute. * @task bin */ final public function getBinary() { return coalesce($this->bin, $this->getDefaultBinary()); } /** * Override the default binary with a new one. * * @param string New binary. * @return this * @task bin */ final public function setBinary($bin) { $this->bin = $bin; return $this; } /** * Return true if this linter should use an interpreter (like "python" or * "node") in addition to the script. * * After overriding this method to return `true`, override * @{method:getDefaultInterpreter} to set a default. * * @return bool True to use an interpreter. * @task bin */ public function shouldUseInterpreter() { return false; } /** * Return the default interpreter, like "python" or "node". This method is * only invoked if @{method:shouldUseInterpreter} has been overridden to * return `true`. * * @return string Default interpreter. * @task bin */ public function getDefaultInterpreter() { throw new PhutilMethodNotImplementedException(); } /** * Get the effective interpreter. This method synthesizes configuration and * defaults. * * @return string Effective interpreter. * @task bin */ final public function getInterpreter() { return coalesce($this->interpreter, $this->getDefaultInterpreter()); } /** * Set the interpreter, overriding any default. * * @param string New interpreter. * @return this * @task bin */ final public function setInterpreter($interpreter) { $this->interpreter = $interpreter; return $this; } /* -( Parsing Linter Output )---------------------------------------------- */ /** * Parse the output of the external lint program into objects of class * @{class:ArcanistLintMessage} which `arc` can consume. Generally, this * means examining the output and converting each warning or error into a * message. * * If parsing fails, returning `false` will cause the caller to throw an * appropriate exception. (You can also throw a more specific exception if * you're able to detect a more specific condition.) Otherwise, return a list * of messages. * * @param string Path to the file being linted. * @param int Exit code of the linter. * @param string Stdout of the linter. * @param string Stderr of the linter. * @return list|false List of lint messages, or false * to indicate parser failure. * @task parse */ abstract protected function parseLinterOutput($path, $err, $stdout, $stderr); /* -( Executing the Linter )----------------------------------------------- */ /** * Check that the binary and interpreter (if applicable) exist, and throw * an exception with a message about how to install them if they do not. * * @return void */ final public function checkBinaryConfiguration() { $interpreter = null; if ($this->shouldUseInterpreter()) { $interpreter = $this->getInterpreter(); } $binary = $this->getBinary(); // NOTE: If we have an interpreter, we don't require the script to be // executable (so we just check that the path exists). Otherwise, the // binary must be executable. if ($interpreter) { if (!Filesystem::binaryExists($interpreter)) { throw new ArcanistMissingLinterException( pht( 'Unable to locate interpreter "%s" to run linter %s. You may need '. 'to install the interpreter, or adjust your linter configuration.', $interpreter, get_class($this))); } if (!Filesystem::pathExists($binary)) { throw new ArcanistMissingLinterException( sprintf( "%s\n%s", pht( 'Unable to locate script "%s" to run linter %s. You may need '. 'to install the script, or adjust your linter configuration.', $binary, get_class($this)), pht( 'TO INSTALL: %s', $this->getInstallInstructions()))); } } else { if (!Filesystem::binaryExists($binary)) { throw new ArcanistMissingLinterException( sprintf( "%s\n%s", pht( 'Unable to locate binary "%s" to run linter %s. You may need '. 'to install the binary, or adjust your linter configuration.', $binary, get_class($this)), pht( 'TO INSTALL: %s', $this->getInstallInstructions()))); } } } /** * Get the composed executable command, including the interpreter and binary * but without flags or paths. This can be used to execute `--version` * commands. * * @return string Command to execute the raw linter. * @task exec */ final protected function getExecutableCommand() { $this->checkBinaryConfiguration(); $interpreter = null; if ($this->shouldUseInterpreter()) { $interpreter = $this->getInterpreter(); } $binary = $this->getBinary(); if ($interpreter) { $bin = csprintf('%s %s', $interpreter, $binary); } else { $bin = csprintf('%s', $binary); } return $bin; } /** * Get the composed flags for the executable, including both mandatory and * configured flags. * * @return list Composed flags. * @task exec */ final protected function getCommandFlags() { return array_merge( $this->getMandatoryFlags(), nonempty($this->flags, $this->getDefaultFlags())); } public function getCacheVersion() { $version = $this->getVersion(); if ($version) { return $version.'-'.json_encode($this->getCommandFlags()); } else { // Either we failed to parse the version number or the `getVersion` // function hasn't been implemented. return json_encode($this->getCommandFlags()); } } /** * Prepare the path to be added to the command string. * * This method is expected to return an already escaped string. * * @param string Path to the file being linted * @return string The command-ready file argument */ protected function getPathArgumentForLinterFuture($path) { return csprintf('%s', $path); } final protected function buildFutures(array $paths) { $executable = $this->getExecutableCommand(); $bin = csprintf('%C %Ls', $executable, $this->getCommandFlags()); $futures = array(); foreach ($paths as $path) { - if ($this->supportsReadDataFromStdin()) { - $future = new ExecFuture( - '%C %C', - $bin, - $this->getReadDataFromStdinFilename()); - $future->write($this->getEngine()->loadData($path)); - } else { - // TODO: In commit hook mode, we need to do more handling here. - $disk_path = $this->getEngine()->getFilePathOnDisk($path); - $path_argument = $this->getPathArgumentForLinterFuture($disk_path); - $future = new ExecFuture('%C %C', $bin, $path_argument); - } + $disk_path = $this->getEngine()->getFilePathOnDisk($path); + $path_argument = $this->getPathArgumentForLinterFuture($disk_path); + $future = new ExecFuture('%C %C', $bin, $path_argument); $future->setCWD($this->getEngine()->getWorkingCopy()->getProjectRoot()); $futures[$path] = $future; } return $futures; } final protected function resolveFuture($path, Future $future) { list($err, $stdout, $stderr) = $future->resolve(); if ($err && !$this->shouldExpectCommandErrors()) { $future->resolvex(); } $messages = $this->parseLinterOutput($path, $err, $stdout, $stderr); if ($messages === false) { if ($err) { $future->resolvex(); } else { throw new Exception( sprintf( "%s\n\nSTDOUT\n%s\n\nSTDERR\n%s", pht('Linter failed to parse output!'), $stdout, $stderr)); } } foreach ($messages as $message) { $this->addLintMessage($message); } } public function getLinterConfigurationOptions() { $options = array( 'bin' => array( 'type' => 'optional string | list', 'help' => pht( 'Specify a string (or list of strings) identifying the binary '. 'which should be invoked to execute this linter. This overrides '. 'the default binary. If you provide a list of possible binaries, '. 'the first one which exists will be used.'), ), 'flags' => array( 'type' => 'optional list', 'help' => pht( 'Provide a list of additional flags to pass to the linter on the '. 'command line.'), ), ); if ($this->shouldUseInterpreter()) { $options['interpreter'] = array( 'type' => 'optional string | list', 'help' => pht( 'Specify a string (or list of strings) identifying the interpreter '. 'which should be used to invoke the linter binary. If you provide '. 'a list of possible interpreters, the first one that exists '. 'will be used.'), ); } return $options + parent::getLinterConfigurationOptions(); } public function setLinterConfigurationValue($key, $value) { switch ($key) { case 'interpreter': $working_copy = $this->getEngine()->getWorkingCopy(); $root = $working_copy->getProjectRoot(); foreach ((array)$value as $path) { if (Filesystem::binaryExists($path)) { $this->setInterpreter($path); return; } $path = Filesystem::resolvePath($path, $root); if (Filesystem::binaryExists($path)) { $this->setInterpreter($path); return; } } throw new Exception( pht('None of the configured interpreters can be located.')); case 'bin': $is_script = $this->shouldUseInterpreter(); $working_copy = $this->getEngine()->getWorkingCopy(); $root = $working_copy->getProjectRoot(); foreach ((array)$value as $path) { if (!$is_script && Filesystem::binaryExists($path)) { $this->setBinary($path); return; } $path = Filesystem::resolvePath($path, $root); if ((!$is_script && Filesystem::binaryExists($path)) || ($is_script && Filesystem::pathExists($path))) { $this->setBinary($path); return; } } throw new Exception( pht('None of the configured binaries can be located.')); case 'flags': $this->setFlags($value); return; } return parent::setLinterConfigurationValue($key, $value); } /** * Map a configuration lint code to an `arc` lint code. Primarily, this is * intended for validation, but can also be used to normalize case or * otherwise be more permissive in accepted inputs. * * If the code is not recognized, you should throw an exception. * * @param string Code specified in configuration. * @return string Normalized code to use in severity map. */ protected function getLintCodeFromLinterConfigurationKey($code) { return $code; } } diff --git a/src/lint/linter/ArcanistHLintLinter.php b/src/lint/linter/ArcanistHLintLinter.php index df22c25b..6274e045 100644 --- a/src/lint/linter/ArcanistHLintLinter.php +++ b/src/lint/linter/ArcanistHLintLinter.php @@ -1,107 +1,99 @@ getExecutableCommand()); $matches = null; if (preg_match('@HLint v(.*),@', $stdout, $matches)) { return $matches[1]; } return null; } protected function parseLinterOutput($path, $err, $stdout, $stderr) { $json = phutil_json_decode($stdout); $messages = array(); foreach ($json as $fix) { if ($fix === null) { return; } $message = new ArcanistLintMessage(); $message->setCode($this->getLinterName()); $message->setPath($path); $message->setLine($fix['startLine']); $message->setChar($fix['startColumn']); $message->setName($fix['hint']); $message->setOriginalText($fix['from']); $message->setReplacementText($fix['to']); /* Some improvements may slightly change semantics, so attach all necessary notes too. */ $notes = ''; foreach ($fix['note'] as $note) { $notes .= ' **NOTE**: '.trim($note, '"').'.'; } $message->setDescription( pht( 'In module `%s`, declaration `%s`.%s', $fix['module'], $fix['decl'], $notes)); switch ($fix['severity']) { case 'Error': $message->setSeverity(ArcanistLintSeverity::SEVERITY_ERROR); break; case 'Warning': $message->setSeverity(ArcanistLintSeverity::SEVERITY_WARNING); break; default: $message->setSeverity(ArcanistLintSeverity::SEVERITY_ADVICE); break; } $messages[] = $message; } return $messages; } } diff --git a/src/lint/linter/ArcanistJSONLintLinter.php b/src/lint/linter/ArcanistJSONLintLinter.php index 064782f8..b4a65bdf 100644 --- a/src/lint/linter/ArcanistJSONLintLinter.php +++ b/src/lint/linter/ArcanistJSONLintLinter.php @@ -1,93 +1,89 @@ getExecutableCommand()); $matches = array(); if (preg_match('/^(?P\d+\.\d+\.\d+)$/', $stdout, $matches)) { return $matches['version']; } else { return false; } } public function getInstallInstructions() { return pht('Install jsonlint using `npm install -g jsonlint`.'); } - public function supportsReadDataFromStdin() { - return true; - } - protected function getMandatoryFlags() { return array( '--compact', ); } protected function parseLinterOutput($path, $err, $stdout, $stderr) { $lines = phutil_split_lines($stderr, false); $messages = array(); foreach ($lines as $line) { $matches = null; $match = preg_match( '/^(?:(?.+): )?'. 'line (?\d+), col (?\d+), '. '(?.*)$/', $line, $matches); if ($match) { $message = new ArcanistLintMessage(); $message->setPath($path); $message->setLine($matches['line']); $message->setChar($matches['column']); $message->setCode($this->getLinterName()); $message->setDescription(ucfirst($matches['description'])); $message->setSeverity(ArcanistLintSeverity::SEVERITY_ERROR); $messages[] = $message; } } if ($err && !$messages) { return false; } return $messages; } } diff --git a/src/lint/linter/ArcanistLesscLinter.php b/src/lint/linter/ArcanistLesscLinter.php index f225bb35..5516540b 100644 --- a/src/lint/linter/ArcanistLesscLinter.php +++ b/src/lint/linter/ArcanistLesscLinter.php @@ -1,195 +1,184 @@ array( 'type' => 'optional bool', 'help' => pht( 'Enable strict math, which only processes mathematical expressions '. 'inside extraneous parentheses.'), ), 'lessc.strict-units' => array( 'type' => 'optional bool', 'help' => pht('Enable strict handling of units in expressions.'), ), ); } public function setLinterConfigurationValue($key, $value) { switch ($key) { case 'lessc.strict-math': $this->strictMath = $value; return; case 'lessc.strict-units': $this->strictUnits = $value; return; } return parent::setLinterConfigurationValue($key, $value); } public function getLintNameMap() { return array( self::LINT_RUNTIME_ERROR => pht('Runtime Error'), self::LINT_ARGUMENT_ERROR => pht('Argument Error'), self::LINT_FILE_ERROR => pht('File Error'), self::LINT_NAME_ERROR => pht('Name Error'), self::LINT_OPERATION_ERROR => pht('Operation Error'), self::LINT_PARSE_ERROR => pht('Parse Error'), self::LINT_SYNTAX_ERROR => pht('Syntax Error'), ); } public function getDefaultBinary() { return 'lessc'; } public function getVersion() { list($stdout) = execx('%C --version', $this->getExecutableCommand()); $matches = array(); $regex = '/^lessc (?P\d+\.\d+\.\d+)\b/'; if (preg_match($regex, $stdout, $matches)) { $version = $matches['version']; } else { return false; } } public function getInstallInstructions() { return pht('Install lessc using `npm install -g less`.'); } - public function supportsReadDataFromStdin() { - // Technically `lessc` can read data from standard input however, when doing - // so, relative imports cannot be resolved. Therefore, this functionality is - // disabled. - return false; - } - - public function getReadDataFromStdinFilename() { - return '-'; - } - protected function getMandatoryFlags() { return array( '--lint', '--no-color', '--strict-math='.($this->strictMath ? 'on' : 'off'), '--strict-units='.($this->strictUnits ? 'on' : 'off'), ); } protected function parseLinterOutput($path, $err, $stdout, $stderr) { $lines = phutil_split_lines($stderr, false); $messages = array(); foreach ($lines as $line) { $matches = null; $match = preg_match( '/^(?P\w+): (?P.+) '. 'in (?P.+|-) '. 'on line (?P\d+), column (?P\d+):$/', $line, $matches); if ($match) { switch ($matches['name']) { case 'RuntimeError': $code = self::LINT_RUNTIME_ERROR; break; case 'ArgumentError': $code = self::LINT_ARGUMENT_ERROR; break; case 'FileError': $code = self::LINT_FILE_ERROR; break; case 'NameError': $code = self::LINT_NAME_ERROR; break; case 'OperationError': $code = self::LINT_OPERATION_ERROR; break; case 'ParseError': $code = self::LINT_PARSE_ERROR; break; case 'SyntaxError': $code = self::LINT_SYNTAX_ERROR; break; default: throw new RuntimeException(pht( 'Unrecognized lint message code "%s".', $code)); } $code = $this->getLintCodeFromLinterConfigurationKey($matches['name']); $message = new ArcanistLintMessage(); $message->setPath($path); $message->setLine($matches['line']); $message->setChar($matches['column']); $message->setCode($this->getLintMessageFullCode($code)); $message->setSeverity($this->getLintMessageSeverity($code)); $message->setName($this->getLintMessageName($code)); $message->setDescription(ucfirst($matches['description'])); $messages[] = $message; } } if ($err && !$messages) { return false; } return $messages; } } diff --git a/src/lint/linter/ArcanistPhpLinter.php b/src/lint/linter/ArcanistPhpLinter.php index 328e87ce..bd6b8222 100644 --- a/src/lint/linter/ArcanistPhpLinter.php +++ b/src/lint/linter/ArcanistPhpLinter.php @@ -1,105 +1,101 @@ pht('Parse Error'), self::LINT_FATAL_ERROR => pht('Fatal Error'), ); } protected function getMandatoryFlags() { return array('-l'); } public function getInstallInstructions() { return pht('Install PHP.'); } public function getDefaultBinary() { return 'php'; } public function getVersion() { list($stdout) = execx( '%C --run %s', $this->getExecutableCommand(), 'echo phpversion();'); return $stdout; } - public function supportsReadDataFromStdin() { - return false; - } - protected function canCustomizeLintSeverities() { return false; } protected function parseLinterOutput($path, $err, $stdout, $stderr) { // Older versions of PHP had both on stdout, newer ones split it. // Combine stdout and stderr for consistency. $stdout = $stderr."\n".$stdout; $matches = array(); $regex = '/^(PHP )?(?.+) error: +(?.+) in (?.+) '. 'on line (?\d+)$/m'; if (preg_match($regex, $stdout, $matches)) { $code = $this->getLintCodeFromLinterConfigurationKey($matches['type']); $message = id(new ArcanistLintMessage()) ->setPath($path) ->setLine($matches['line']) ->setCode($this->getLinterName().$code) ->setName($this->getLintMessageName($code)) ->setSeverity(ArcanistLintSeverity::SEVERITY_ERROR) ->setDescription($matches['error']); // `php -l` only returns the first error. return array($message); } return array(); } protected function getLintCodeFromLinterConfigurationKey($code) { switch (phutil_utf8_strtolower($code)) { case 'parse': return self::LINT_PARSE_ERROR; case 'fatal': return self::LINT_FATAL_ERROR; default: throw new Exception(pht('Unrecognized lint message code "%s"', $code)); } } } diff --git a/src/lint/linter/ArcanistPhpcsLinter.php b/src/lint/linter/ArcanistPhpcsLinter.php index 8475c55a..68d4d7f3 100644 --- a/src/lint/linter/ArcanistPhpcsLinter.php +++ b/src/lint/linter/ArcanistPhpcsLinter.php @@ -1,164 +1,160 @@ array( 'type' => 'optional string', 'help' => pht('The name or path of the coding standard to use.'), ), ); return $options + parent::getLinterConfigurationOptions(); } public function setLinterConfigurationValue($key, $value) { switch ($key) { case 'phpcs.standard': $this->standard = $value; return; default: return parent::setLinterConfigurationValue($key, $value); } } protected function getMandatoryFlags() { $options = array('--report=xml'); if ($this->standard) { $options[] = '--standard='.$this->standard; } return $options; } protected function getDefaultFlags() { $options = $this->getDeprecatedConfiguration('lint.phpcs.options', array()); $standard = $this->getDeprecatedConfiguration('lint.phpcs.standard'); if (!empty($standard)) { if (is_array($options)) { $options[] = '--standard='.$standard; } else { $options .= ' --standard='.$standard; } } return $options; } public function getDefaultBinary() { return $this->getDeprecatedConfiguration('lint.phpcs.bin', 'phpcs'); } public function getVersion() { list($stdout) = execx('%C --version', $this->getExecutableCommand()); $matches = array(); $regex = '/^PHP_CodeSniffer version (?P\d+\.\d+\.\d+)\b/'; if (preg_match($regex, $stdout, $matches)) { return $matches['version']; } else { return false; } } - public function supportsReadDataFromStdin() { - return true; - } - protected function parseLinterOutput($path, $err, $stdout, $stderr) { // NOTE: Some version of PHPCS after 1.4.6 stopped printing a valid, empty // XML document to stdout in the case of no errors. If PHPCS exits with // error 0, just ignore output. if (!$err) { return array(); } $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->childNodes as $child) { if (!($child instanceof DOMElement)) { continue; } if ($child->tagName == 'error') { $prefix = 'E'; } else { $prefix = 'W'; } $code = 'PHPCS.'.$prefix.'.'.$child->getAttribute('source'); $message = new ArcanistLintMessage(); $message->setPath($path); $message->setLine($child->getAttribute('line')); $message->setChar($child->getAttribute('column')); $message->setCode($code); $message->setDescription($child->nodeValue); $message->setSeverity($this->getLintMessageSeverity($code)); $messages[] = $message; } } return $messages; } protected function getDefaultMessageSeverity($code) { if (preg_match('/^PHPCS\\.W\\./', $code)) { return ArcanistLintSeverity::SEVERITY_WARNING; } else { return ArcanistLintSeverity::SEVERITY_ERROR; } } protected function getLintCodeFromLinterConfigurationKey($code) { if (!preg_match('/^PHPCS\\.(E|W)\\./', $code)) { throw new Exception( "Invalid severity code '{$code}', should begin with 'PHPCS.'."); } return $code; } } diff --git a/src/lint/linter/ArcanistPyFlakesLinter.php b/src/lint/linter/ArcanistPyFlakesLinter.php index 4180fc90..59d91995 100644 --- a/src/lint/linter/ArcanistPyFlakesLinter.php +++ b/src/lint/linter/ArcanistPyFlakesLinter.php @@ -1,102 +1,98 @@ getDeprecatedConfiguration('lint.pyflakes.prefix'); $bin = $this->getDeprecatedConfiguration('lint.pyflakes.bin', 'pyflakes'); if ($prefix) { return $prefix.'/'.$bin; } else { return $bin; } } public function getVersion() { list($stdout) = execx('%C --version', $this->getExecutableCommand()); $matches = array(); if (preg_match('/^(?P\d+\.\d+\.\d+)$/', $stdout, $matches)) { return $matches['version']; } else { return false; } } public function getInstallInstructions() { return pht('Install pyflakes with `pip install pyflakes`.'); } - public function supportsReadDataFromStdin() { - return true; - } - protected function parseLinterOutput($path, $err, $stdout, $stderr) { $lines = phutil_split_lines($stdout, false); $messages = array(); foreach ($lines as $line) { $matches = null; if (!preg_match('/^(.*?):(\d+): (.*)$/', $line, $matches)) { continue; } foreach ($matches as $key => $match) { $matches[$key] = trim($match); } $severity = ArcanistLintSeverity::SEVERITY_WARNING; $description = $matches[3]; $error_regexp = '/(^undefined|^duplicate|before assignment$)/'; if (preg_match($error_regexp, $description)) { $severity = ArcanistLintSeverity::SEVERITY_ERROR; } $message = new ArcanistLintMessage(); $message->setPath($path); $message->setLine($matches[2]); $message->setCode($this->getLinterName()); $message->setDescription($description); $message->setSeverity($severity); $messages[] = $message; } if ($err && !$messages) { return false; } return $messages; } protected function canCustomizeLintSeverities() { return false; } } diff --git a/src/lint/linter/ArcanistRubyLinter.php b/src/lint/linter/ArcanistRubyLinter.php index bdf3ab82..b2d76567 100644 --- a/src/lint/linter/ArcanistRubyLinter.php +++ b/src/lint/linter/ArcanistRubyLinter.php @@ -1,98 +1,94 @@ getDeprecatedConfiguration('lint.ruby.prefix'); if ($prefix !== null) { $ruby_bin = $prefix.'ruby'; } return 'ruby'; } public function getVersion() { list($stdout) = execx('%C --version', $this->getExecutableCommand()); $matches = array(); $regex = '/^ruby (?P\d+\.\d+\.\d+)p\d+/'; if (preg_match($regex, $stdout, $matches)) { return $matches['version']; } else { return false; } } public function getInstallInstructions() { return pht('Install `ruby` from .'); } - public function supportsReadDataFromStdin() { - return true; - } - protected function getMandatoryFlags() { // -w: turn on warnings // -c: check syntax return array('-w', '-c'); } protected function parseLinterOutput($path, $err, $stdout, $stderr) { $lines = phutil_split_lines($stderr, false); $messages = array(); foreach ($lines as $line) { $matches = null; if (!preg_match('/(.*?):(\d+): (.*?)$/', $line, $matches)) { continue; } foreach ($matches as $key => $match) { $matches[$key] = trim($match); } $code = head(explode(',', $matches[3])); $message = new ArcanistLintMessage(); $message->setPath($path); $message->setLine($matches[2]); $message->setCode($this->getLinterName()); $message->setName(pht('Syntax Error')); $message->setDescription($matches[3]); $message->setSeverity($this->getLintMessageSeverity($code)); $messages[] = $message; } if ($err && !$messages) { return false; } return $messages; } }