public function run()
 {
     $console = PhutilConsole::getConsole();
     $working_copy = $this->getWorkingCopy();
     $configuration_manager = $this->getConfigurationManager();
     $engine = $this->newLintEngine($this->getArgument('engine'));
     $rev = $this->getArgument('rev');
     $paths = $this->getArgument('paths');
     $use_cache = $this->getArgument('cache', null);
     $everything = $this->getArgument('everything');
     if ($everything && $paths) {
         throw new ArcanistUsageException(pht('You can not specify paths with %s. The %s flag lints every file.', '--everything', '--everything'));
     }
     if ($use_cache === null) {
         $use_cache = (bool) $configuration_manager->getConfigFromAnySource('arc.lint.cache', false);
     }
     if ($rev && $paths) {
         throw new ArcanistUsageException(pht('Specify either %s or paths.', '--rev'));
     }
     // NOTE: When the user specifies paths, we imply --lintall and show all
     // warnings for the paths in question. This is easier to deal with for
     // us and less confusing for users.
     $this->shouldLintAll = $paths ? true : false;
     if ($this->getArgument('lintall')) {
         $this->shouldLintAll = true;
     } else {
         if ($this->getArgument('only-changed')) {
             $this->shouldLintAll = false;
         }
     }
     if ($everything) {
         $paths = iterator_to_array($this->getRepositoryApi()->getAllFiles());
         $this->shouldLintAll = true;
     } else {
         $paths = $this->selectPathsForWorkflow($paths, $rev);
     }
     $this->engine = $engine;
     $engine->setMinimumSeverity($this->getArgument('severity', self::DEFAULT_SEVERITY));
     $file_hashes = array();
     if ($use_cache) {
         $engine->setRepositoryVersion($this->getRepositoryVersion());
         $cache = $this->readScratchJSONFile('lint-cache.json');
         $cache = idx($cache, $this->getCacheKey(), array());
         $cached = array();
         foreach ($paths as $path) {
             $abs_path = $engine->getFilePathOnDisk($path);
             if (!Filesystem::pathExists($abs_path)) {
                 continue;
             }
             $file_hashes[$abs_path] = md5_file($abs_path);
             if (!isset($cache[$path])) {
                 continue;
             }
             $messages = idx($cache[$path], $file_hashes[$abs_path]);
             if ($messages !== null) {
                 $cached[$path] = $messages;
             }
         }
         if ($cached) {
             $console->writeErr("%s\n", pht("Using lint cache, use '%s' to disable it.", '--cache 0'));
         }
         $engine->setCachedResults($cached);
     }
     // Propagate information about which lines changed to the lint engine.
     // This is used so that the lint engine can drop warning messages
     // concerning lines that weren't in the change.
     $engine->setPaths($paths);
     if (!$this->shouldLintAll) {
         foreach ($paths as $path) {
             // Note that getChangedLines() returns null to indicate that a file
             // is binary or a directory (i.e., changed lines are not relevant).
             $engine->setPathChangedLines($path, $this->getChangedLines($path, 'new'));
         }
     }
     // Enable possible async linting only for 'arc diff' not 'arc lint'
     if ($this->getParentWorkflow()) {
         $engine->setEnableAsyncLint(true);
     } else {
         $engine->setEnableAsyncLint(false);
     }
     if ($this->getArgument('only-new')) {
         $conduit = $this->getConduit();
         $api = $this->getRepositoryAPI();
         if ($rev) {
             $api->setBaseCommit($rev);
         }
         $svn_root = id(new PhutilURI($api->getSourceControlPath()))->getPath();
         $all_paths = array();
         foreach ($paths as $path) {
             $path = str_replace(DIRECTORY_SEPARATOR, '/', $path);
             $full_paths = array($path);
             $change = $this->getChange($path);
             $type = $change->getType();
             if (ArcanistDiffChangeType::isOldLocationChangeType($type)) {
                 $full_paths = $change->getAwayPaths();
             } else {
                 if (ArcanistDiffChangeType::isNewLocationChangeType($type)) {
                     continue;
                 } else {
                     if (ArcanistDiffChangeType::isDeleteChangeType($type)) {
                         continue;
                     }
                 }
             }
             foreach ($full_paths as $full_path) {
                 $all_paths[$svn_root . '/' . $full_path] = $path;
             }
         }
         $lint_future = $conduit->callMethod('diffusion.getlintmessages', array('repositoryPHID' => idx($this->loadProjectRepository(), 'phid'), 'branch' => '', 'commit' => $api->getBaseCommit(), 'files' => array_keys($all_paths)));
     }
     $failed = null;
     try {
         $engine->run();
     } catch (Exception $ex) {
         $failed = $ex;
     }
     $results = $engine->getResults();
     if ($this->getArgument('only-new')) {
         $total = 0;
         foreach ($results as $result) {
             $total += count($result->getMessages());
         }
         // Don't wait for response with default value of --only-new.
         $timeout = null;
         if ($this->getArgument('only-new') === null || !$total) {
             $timeout = 0;
         }
         $raw_messages = $this->resolveCall($lint_future, $timeout);
         if ($raw_messages && $total) {
             $old_messages = array();
             $line_maps = array();
             foreach ($raw_messages as $message) {
                 $path = $all_paths[$message['path']];
                 $line = $message['line'];
                 $code = $message['code'];
                 if (!isset($line_maps[$path])) {
                     $line_maps[$path] = $this->getChange($path)->buildLineMap();
                 }
                 $new_lines = idx($line_maps[$path], $line);
                 if (!$new_lines) {
                     // Unmodified lines after last hunk.
                     $last_old = $line_maps[$path] ? last_key($line_maps[$path]) : 0;
                     $news = array_filter($line_maps[$path]);
                     $last_new = $news ? last(end($news)) : 0;
                     $new_lines = array($line + $last_new - $last_old);
                 }
                 $error = array($code => array(true));
                 foreach ($new_lines as $new) {
                     if (isset($old_messages[$path][$new])) {
                         $old_messages[$path][$new][$code][] = true;
                         break;
                     }
                     $old_messages[$path][$new] =& $error;
                 }
                 unset($error);
             }
             foreach ($results as $result) {
                 foreach ($result->getMessages() as $message) {
                     $path = str_replace(DIRECTORY_SEPARATOR, '/', $message->getPath());
                     $line = $message->getLine();
                     $code = $message->getCode();
                     if (!empty($old_messages[$path][$line][$code])) {
                         $message->setObsolete(true);
                         array_pop($old_messages[$path][$line][$code]);
                     }
                 }
                 $result->sortAndFilterMessages();
             }
         }
     }
     if ($this->getArgument('never-apply-patches')) {
         $apply_patches = false;
     } else {
         $apply_patches = true;
     }
     if ($this->getArgument('apply-patches')) {
         $prompt_patches = false;
     } else {
         $prompt_patches = true;
     }
     if ($this->getArgument('amend-all')) {
         $this->shouldAmendChanges = true;
         $this->shouldAmendWithoutPrompt = true;
     }
     if ($this->getArgument('amend-autofixes')) {
         $prompt_autofix_patches = false;
         $this->shouldAmendChanges = true;
         $this->shouldAmendAutofixesWithoutPrompt = true;
     } else {
         $prompt_autofix_patches = true;
     }
     $repository_api = $this->getRepositoryAPI();
     if ($this->shouldAmendChanges) {
         $this->shouldAmendChanges = $repository_api->supportsAmend() && !$this->isHistoryImmutable();
     }
     $wrote_to_disk = false;
     switch ($this->getArgument('output')) {
         case 'json':
             $renderer = new ArcanistJSONLintRenderer();
             $prompt_patches = false;
             $apply_patches = $this->getArgument('apply-patches');
             break;
         case 'summary':
             $renderer = new ArcanistSummaryLintRenderer();
             break;
         case 'none':
             $prompt_patches = false;
             $apply_patches = $this->getArgument('apply-patches');
             $renderer = new ArcanistNoneLintRenderer();
             break;
         case 'compiler':
             $renderer = new ArcanistCompilerLintRenderer();
             $prompt_patches = false;
             $apply_patches = $this->getArgument('apply-patches');
             break;
         case 'xml':
             $renderer = new ArcanistCheckstyleXMLLintRenderer();
             $prompt_patches = false;
             $apply_patches = $this->getArgument('apply-patches');
             break;
         default:
             $renderer = new ArcanistConsoleLintRenderer();
             $renderer->setShowAutofixPatches($prompt_autofix_patches);
             break;
     }
     $all_autofix = true;
     $tmp = null;
     if ($this->getArgument('outfile') !== null) {
         $tmp = id(new TempFile())->setPreserveFile(true);
     }
     $preamble = $renderer->renderPreamble();
     if ($tmp) {
         Filesystem::appendFile($tmp, $preamble);
     } else {
         $console->writeOut('%s', $preamble);
     }
     foreach ($results as $result) {
         $result_all_autofix = $result->isAllAutofix();
         if (!$result->getMessages() && !$result_all_autofix) {
             continue;
         }
         if (!$result_all_autofix) {
             $all_autofix = false;
         }
         $lint_result = $renderer->renderLintResult($result);
         if ($lint_result) {
             if ($tmp) {
                 Filesystem::appendFile($tmp, $lint_result);
             } else {
                 $console->writeOut('%s', $lint_result);
             }
         }
         if ($apply_patches && $result->isPatchable()) {
             $patcher = ArcanistLintPatcher::newFromArcanistLintResult($result);
             $old_file = $result->getFilePathOnDisk();
             if ($prompt_patches && !($result_all_autofix && !$prompt_autofix_patches)) {
                 if (!Filesystem::pathExists($old_file)) {
                     $old_file = '/dev/null';
                 }
                 $new_file = new TempFile();
                 $new = $patcher->getModifiedFileContent();
                 Filesystem::writeFile($new_file, $new);
                 // TODO: Improve the behavior here, make it more like
                 // difference_render().
                 list(, $stdout, $stderr) = exec_manual('diff -u %s %s', $old_file, $new_file);
                 $console->writeOut('%s', $stdout);
                 $console->writeErr('%s', $stderr);
                 $prompt = pht('Apply this patch to %s?', phutil_console_format('__%s__', $result->getPath()));
                 if (!$console->confirm($prompt, $default = true)) {
                     continue;
                 }
             }
             $patcher->writePatchToDisk();
             $wrote_to_disk = true;
             $file_hashes[$old_file] = md5_file($old_file);
         }
     }
     $postamble = $renderer->renderPostamble();
     if ($tmp) {
         Filesystem::appendFile($tmp, $postamble);
         Filesystem::rename($tmp, $this->getArgument('outfile'));
     } else {
         $console->writeOut('%s', $postamble);
     }
     if ($wrote_to_disk && $this->shouldAmendChanges) {
         if ($this->shouldAmendWithoutPrompt || $this->shouldAmendAutofixesWithoutPrompt && $all_autofix) {
             $console->writeOut("<bg:yellow>** %s **</bg> %s\n", pht('LINT NOTICE'), pht('Automatically amending HEAD with lint patches.'));
             $amend = true;
         } else {
             $amend = $console->confirm(pht('Amend HEAD with lint patches?'));
         }
         if ($amend) {
             if ($repository_api instanceof ArcanistGitAPI) {
                 // Add the changes to the index before amending
                 $repository_api->execxLocal('add -u');
             }
             $repository_api->amendCommit();
         } else {
             throw new ArcanistUsageException(pht('Sort out the lint changes that were applied to the working ' . 'copy and relint.'));
         }
     }
     if ($this->getArgument('output') == 'json') {
         // NOTE: Required by save_lint.php in Phabricator.
         return 0;
     }
     if ($failed) {
         if ($failed instanceof ArcanistNoEffectException) {
             if ($renderer instanceof ArcanistNoneLintRenderer) {
                 return 0;
             }
         }
         throw $failed;
     }
     $unresolved = array();
     $has_warnings = false;
     $has_errors = false;
     foreach ($results as $result) {
         foreach ($result->getMessages() as $message) {
             if (!$message->isPatchApplied()) {
                 if ($message->isError()) {
                     $has_errors = true;
                 } else {
                     if ($message->isWarning()) {
                         $has_warnings = true;
                     }
                 }
                 $unresolved[] = $message;
             }
         }
     }
     $this->unresolvedMessages = $unresolved;
     $cache = $this->readScratchJSONFile('lint-cache.json');
     $cached = idx($cache, $this->getCacheKey(), array());
     if ($cached || $use_cache) {
         $stopped = $engine->getStoppedPaths();
         foreach ($results as $result) {
             $path = $result->getPath();
             if (!$use_cache) {
                 unset($cached[$path]);
                 continue;
             }
             $abs_path = $engine->getFilePathOnDisk($path);
             if (!Filesystem::pathExists($abs_path)) {
                 continue;
             }
             $version = $result->getCacheVersion();
             $cached_path = array();
             if (isset($stopped[$path])) {
                 $cached_path['stopped'] = $stopped[$path];
             }
             $cached_path['repository_version'] = $this->getRepositoryVersion();
             foreach ($result->getMessages() as $message) {
                 $granularity = $message->getGranularity();
                 if ($granularity == ArcanistLinter::GRANULARITY_GLOBAL) {
                     continue;
                 }
                 if (!$message->isPatchApplied()) {
                     $cached_path[] = $message->toDictionary();
                 }
             }
             $hash = idx($file_hashes, $abs_path);
             if (!$hash) {
                 $hash = md5_file($abs_path);
             }
             $cached[$path] = array($hash => array($version => $cached_path));
         }
         $cache[$this->getCacheKey()] = $cached;
         // TODO: Garbage collection.
         $this->writeScratchJSONFile('lint-cache.json', $cache);
     }
     // Take the most severe lint message severity and use that
     // as the result code.
     if ($has_errors) {
         $result_code = self::RESULT_ERRORS;
     } else {
         if ($has_warnings) {
             $result_code = self::RESULT_WARNINGS;
         } else {
             $result_code = self::RESULT_OKAY;
         }
     }
     if (!$this->getParentWorkflow()) {
         if ($result_code == self::RESULT_OKAY) {
             $console->writeOut('%s', $renderer->renderOkayResult());
         }
     }
     return $result_code;
 }
示例#2
0
 public function run()
 {
     $working_copy = $this->getWorkingCopy();
     $engine = $this->getArgument('engine');
     if (!$engine) {
         $engine = $working_copy->getConfigFromAnySource('lint.engine');
         if (!$engine) {
             throw new ArcanistNoEngineException("No lint engine configured for this project. Edit .arcconfig to " . "specify a lint engine.");
         }
     }
     $rev = $this->getArgument('rev');
     $paths = $this->getArgument('paths');
     if ($rev && $paths) {
         throw new ArcanistUsageException("Specify either --rev or paths.");
     }
     $should_lint_all = $this->getArgument('lintall');
     if ($paths) {
         // NOTE: When the user specifies paths, we imply --lintall and show all
         // warnings for the paths in question. This is easier to deal with for
         // us and less confusing for users.
         $should_lint_all = true;
     }
     $paths = $this->selectPathsForWorkflow($paths, $rev);
     // is_subclass_of() doesn't autoload under HPHP.
     if (!class_exists($engine) || !is_subclass_of($engine, 'ArcanistLintEngine')) {
         throw new ArcanistUsageException("Configured lint engine '{$engine}' is not a subclass of " . "'ArcanistLintEngine'.");
     }
     $engine = newv($engine, array());
     $this->engine = $engine;
     $engine->setWorkingCopy($working_copy);
     if ($this->getArgument('advice')) {
         $engine->setMinimumSeverity(ArcanistLintSeverity::SEVERITY_ADVICE);
     } else {
         $engine->setMinimumSeverity(ArcanistLintSeverity::SEVERITY_AUTOFIX);
     }
     // Propagate information about which lines changed to the lint engine.
     // This is used so that the lint engine can drop warning messages
     // concerning lines that weren't in the change.
     $engine->setPaths($paths);
     if (!$should_lint_all) {
         foreach ($paths as $path) {
             // Note that getChangedLines() returns null to indicate that a file
             // is binary or a directory (i.e., changed lines are not relevant).
             $engine->setPathChangedLines($path, $this->getChangedLines($path, 'new'));
         }
     }
     // Enable possible async linting only for 'arc diff' not 'arc unit'
     if ($this->getParentWorkflow()) {
         $engine->setEnableAsyncLint(true);
     } else {
         $engine->setEnableAsyncLint(false);
     }
     $results = $engine->run();
     // It'd be nice to just return a single result from the run method above
     // which contains both the lint messages and the postponed linters.
     // However, to maintain compatibility with existing lint subclasses, use
     // a separate method call to grab the postponed linters.
     $this->postponedLinters = $engine->getPostponedLinters();
     if ($this->getArgument('never-apply-patches')) {
         $apply_patches = false;
     } else {
         $apply_patches = true;
     }
     if ($this->getArgument('apply-patches')) {
         $prompt_patches = false;
     } else {
         $prompt_patches = true;
     }
     if ($this->getArgument('amend-all')) {
         $this->shouldAmendChanges = true;
         $this->shouldAmendWithoutPrompt = true;
     }
     if ($this->getArgument('amend-autofixes')) {
         $prompt_autofix_patches = false;
         $this->shouldAmendChanges = true;
         $this->shouldAmendAutofixesWithoutPrompt = true;
     } else {
         $prompt_autofix_patches = true;
     }
     $wrote_to_disk = false;
     switch ($this->getArgument('output')) {
         case 'json':
             $renderer = new ArcanistLintJSONRenderer();
             $prompt_patches = false;
             $apply_patches = $this->getArgument('apply-patches');
             break;
         case 'summary':
             $renderer = new ArcanistLintSummaryRenderer();
             break;
         case 'compiler':
             $renderer = new ArcanistLintLikeCompilerRenderer();
             $prompt_patches = false;
             $apply_patches = $this->getArgument('apply-patches');
             break;
         default:
             $renderer = new ArcanistLintConsoleRenderer();
             $renderer->setShowAutofixPatches($prompt_autofix_patches);
             break;
     }
     $all_autofix = true;
     foreach ($results as $result) {
         $result_all_autofix = $result->isAllAutofix();
         if (!$result->getMessages() && !$result_all_autofix) {
             continue;
         }
         if (!$result_all_autofix) {
             $all_autofix = false;
         }
         $lint_result = $renderer->renderLintResult($result);
         if ($lint_result) {
             echo $lint_result;
         }
         if ($apply_patches && $result->isPatchable()) {
             $patcher = ArcanistLintPatcher::newFromArcanistLintResult($result);
             if ($prompt_patches && !($result_all_autofix && !$prompt_autofix_patches)) {
                 $old_file = $result->getFilePathOnDisk();
                 if (!Filesystem::pathExists($old_file)) {
                     $old_file = '/dev/null';
                 }
                 $new_file = new TempFile();
                 $new = $patcher->getModifiedFileContent();
                 Filesystem::writeFile($new_file, $new);
                 // TODO: Improve the behavior here, make it more like
                 // difference_render().
                 passthru(csprintf("diff -u %s %s", $old_file, $new_file));
                 $prompt = phutil_console_format("Apply this patch to __%s__?", $result->getPath());
                 if (!phutil_console_confirm($prompt, $default_no = false)) {
                     continue;
                 }
             }
             $patcher->writePatchToDisk();
             $wrote_to_disk = true;
         }
     }
     $repository_api = $this->getRepositoryAPI();
     if ($wrote_to_disk && $repository_api instanceof ArcanistGitAPI && $this->shouldAmendChanges) {
         if ($this->shouldAmendWithoutPrompt || $this->shouldAmendAutofixesWithoutPrompt && $all_autofix) {
             echo phutil_console_format("<bg:yellow>** LINT NOTICE **</bg> Automatically amending HEAD " . "with lint patches.\n");
             $amend = true;
         } else {
             $amend = phutil_console_confirm("Amend HEAD with lint patches?");
         }
         if ($amend) {
             execx('(cd %s; git commit -a --amend -C HEAD)', $repository_api->getPath());
         } else {
             throw new ArcanistUsageException("Sort out the lint changes that were applied to the working " . "copy and relint.");
         }
     }
     $unresolved = array();
     $has_warnings = false;
     $has_errors = false;
     foreach ($results as $result) {
         foreach ($result->getMessages() as $message) {
             if (!$message->isPatchApplied()) {
                 if ($message->isError()) {
                     $has_errors = true;
                 } else {
                     if ($message->isWarning()) {
                         $has_warnings = true;
                     }
                 }
                 $unresolved[] = $message;
             }
         }
     }
     $this->unresolvedMessages = $unresolved;
     // Take the most severe lint message severity and use that
     // as the result code.
     if ($has_errors) {
         $result_code = self::RESULT_ERRORS;
     } else {
         if ($has_warnings) {
             $result_code = self::RESULT_WARNINGS;
         } else {
             if (!empty($this->postponedLinters)) {
                 $result_code = self::RESULT_POSTPONED;
             } else {
                 $result_code = self::RESULT_OKAY;
             }
         }
     }
     if (!$this->getParentWorkflow()) {
         if ($result_code == self::RESULT_OKAY) {
             echo $renderer->renderOkayResult();
         }
     }
     return $result_code;
 }
 private function lintFile($file, ArcanistLinter $linter)
 {
     $linter = clone $linter;
     $contents = Filesystem::readFile($file);
     $contents = preg_split('/^~{4,}\\n/m', $contents);
     if (count($contents) < 2) {
         throw new Exception(pht("Expected '%s' separating test case and results.", '~~~~~~~~~~'));
     }
     list($data, $expect, $xform, $config) = array_merge($contents, array(null, null));
     $basename = basename($file);
     if ($config) {
         $config = phutil_json_decode($config);
     } else {
         $config = array();
     }
     PhutilTypeSpec::checkMap($config, array('config' => 'optional map<string, wild>', 'path' => 'optional string', 'mode' => 'optional string', 'stopped' => 'optional bool'));
     $exception = null;
     $after_lint = null;
     $messages = null;
     $exception_message = false;
     $caught_exception = false;
     try {
         $tmp = new TempFile($basename);
         Filesystem::writeFile($tmp, $data);
         $full_path = (string) $tmp;
         $mode = idx($config, 'mode');
         if ($mode) {
             Filesystem::changePermissions($tmp, octdec($mode));
         }
         $dir = dirname($full_path);
         $path = basename($full_path);
         $working_copy = ArcanistWorkingCopyIdentity::newFromRootAndConfigFile($dir, null, pht('Unit Test'));
         $configuration_manager = new ArcanistConfigurationManager();
         $configuration_manager->setWorkingCopyIdentity($working_copy);
         $engine = new ArcanistUnitTestableLintEngine();
         $engine->setWorkingCopy($working_copy);
         $engine->setConfigurationManager($configuration_manager);
         $path_name = idx($config, 'path', $path);
         $engine->setPaths(array($path_name));
         $linter->addPath($path_name);
         $linter->addData($path_name, $data);
         foreach (idx($config, 'config', array()) as $key => $value) {
             $linter->setLinterConfigurationValue($key, $value);
         }
         $engine->addLinter($linter);
         $engine->addFileData($path_name, $data);
         $results = $engine->run();
         $this->assertEqual(1, count($results), pht('Expect one result returned by linter.'));
         $assert_stopped = idx($config, 'stopped');
         if ($assert_stopped !== null) {
             $this->assertEqual($assert_stopped, $linter->didStopAllLinters(), $assert_stopped ? pht('Expect linter to be stopped.') : pht('Expect linter to not be stopped.'));
         }
         $result = reset($results);
         $patcher = ArcanistLintPatcher::newFromArcanistLintResult($result);
         $after_lint = $patcher->getModifiedFileContent();
     } catch (PhutilTestTerminatedException $ex) {
         throw $ex;
     } catch (Exception $exception) {
         $caught_exception = true;
         if ($exception instanceof PhutilAggregateException) {
             $caught_exception = false;
             foreach ($exception->getExceptions() as $ex) {
                 if ($ex instanceof ArcanistUsageException || $ex instanceof ArcanistMissingLinterException) {
                     $this->assertSkipped($ex->getMessage());
                 } else {
                     $caught_exception = true;
                 }
             }
         } else {
             if ($exception instanceof ArcanistUsageException || $exception instanceof ArcanistMissingLinterException) {
                 $this->assertSkipped($exception->getMessage());
             }
         }
         $exception_message = $exception->getMessage() . "\n\n" . $exception->getTraceAsString();
     }
     $this->assertEqual(false, $caught_exception, $exception_message);
     $this->compareLint($basename, $expect, $result);
     $this->compareTransform($xform, $after_lint);
 }
 private function lintFile($file, $linter, $working_copy)
 {
     $linter = clone $linter;
     $contents = Filesystem::readFile($file);
     $contents = explode("~~~~~~~~~~\n", $contents);
     if (count($contents) < 2) {
         throw new Exception("Expected '~~~~~~~~~~' separating test case and results.");
     }
     list($data, $expect, $xform, $config) = array_merge($contents, array(null, null));
     $basename = basename($file);
     if ($config) {
         $config = json_decode($config, true);
         if (!is_array($config)) {
             throw new Exception("Invalid configuration in test '{$basename}', not valid JSON.");
         }
     } else {
         $config = array();
     }
     /* TODO: ?
        validate_parameter_list(
          $config,
          array(
          ),
          array(
            'project' => true,
            'path' => true,
            'hook' => true,
          ));
        */
     $exception = null;
     $after_lint = null;
     $messages = null;
     $exception_message = false;
     $caught_exception = false;
     try {
         $path = idx($config, 'path', 'lint/' . $basename . '.php');
         $engine = new UnitTestableArcanistLintEngine();
         $engine->setWorkingCopy($working_copy);
         $engine->setPaths(array($path));
         $engine->setCommitHookMode(idx($config, 'hook', false));
         $linter->addPath($path);
         $linter->addData($path, $data);
         $linter->setConfig(idx($config, 'config', array()));
         $engine->addLinter($linter);
         $engine->addFileData($path, $data);
         $results = $engine->run();
         $this->assertEqual(1, count($results), 'Expect one result returned by linter.');
         $result = reset($results);
         $patcher = ArcanistLintPatcher::newFromArcanistLintResult($result);
         $after_lint = $patcher->getModifiedFileContent();
     } catch (ArcanistPhutilTestTerminatedException $ex) {
         throw $ex;
     } catch (Exception $exception) {
         $caught_exception = true;
         if ($exception instanceof PhutilAggregateException) {
             $caught_exception = false;
             foreach ($exception->getExceptions() as $ex) {
                 if ($ex instanceof ArcanistUsageException) {
                     $this->assertSkipped($ex->getMessage());
                 } else {
                     $caught_exception = true;
                 }
             }
         }
         $exception_message = $exception->getMessage() . "\n\n" . $exception->getTraceAsString();
     }
     switch ($basename) {
         default:
             $this->assertEqual(false, $caught_exception, $exception_message);
             $this->compareLint($basename, $expect, $result);
             $this->compareTransform($xform, $after_lint);
             break;
     }
 }
 private function liberateWritePatches(array $results)
 {
     assert_instances_of($results, 'ArcanistLintResult');
     $wrote = array();
     foreach ($results as $result) {
         if ($result->isPatchable()) {
             $patcher = ArcanistLintPatcher::newFromArcanistLintResult($result);
             $patcher->writePatchToDisk();
             $wrote[] = $result->getPath();
         }
     }
     return $wrote;
 }