diff --git a/scripts/phutil_rebuild_map.php b/scripts/phutil_rebuild_map.php index a5f1a5e2..bd9a6621 100755 --- a/scripts/phutil_rebuild_map.php +++ b/scripts/phutil_rebuild_map.php @@ -1,518 +1,578 @@ #!/usr/bin/env php setTagline('rebuild the library map file'); $args->setSynopsis(<<parseStandardArguments(); $args->parse( array( array( 'name' => 'quiet', 'help' => 'Do not write status messages to stderr.', ), array( 'name' => 'drop-cache', 'help' => 'Drop the symbol cache and rebuild the entire map from '. 'scratch.', ), array( 'name' => 'limit', 'param' => 'N', 'default' => 8, 'help' => 'Controls the number of symbol mapper subprocesses run '. 'at once. Defaults to 8.', ), + array( + 'name' => 'show', + 'help' => 'Print symbol map to stdout instead of writing it to the '. + 'map file.', + ), + array( + 'name' => 'ugly', + 'help' => 'Use faster but less readable serialization for --show.', + ), array( 'name' => 'root', 'wildcard' => true, ) )); $root = $args->getArg('root'); if (count($root) !== 1) { throw new Exception("Provide exactly one library root!"); } $root = Filesystem::resolvePath(head($root)); $builder = new PhutilLibraryMapBuilder($root); $builder->setQuiet($args->getArg('quiet')); $builder->setSubprocessLimit($args->getArg('limit')); if ($args->getArg('drop-cache')) { $builder->dropSymbolCache(); } +if ($args->getArg('show')) { + $builder->setShowMap(true); + $builder->setUgly($args->getArg('ugly')); +} + $builder->buildMap(); exit(0); /** * Build maps of libphutil libraries. libphutil uses the library map to locate * and load classes and functions in the library. * * @task map Mapping libphutil Libraries * @task path Path Management * @task symbol Symbol Analysis and Caching * @task source Source Management */ final class PhutilLibraryMapBuilder { private $root; private $quiet; private $subprocessLimit = 8; + private $ugly; + private $showMap; const LIBRARY_MAP_VERSION_KEY = '__library_version__'; const LIBRARY_MAP_VERSION = 2; const SYMBOL_CACHE_VERSION_KEY = '__symbol_cache_version__'; const SYMBOL_CACHE_VERSION = 2; /* -( Mapping libphutil Libraries )---------------------------------------- */ /** * Create a new map builder for a library. * * @param string Path to the library root. * * @task map */ public function __construct($root) { $this->root = $root; } /** * Control status output. Use --quiet to set this. * * @param bool If true, don't show status output. * @return this * * @task map */ public function setQuiet($quiet) { $this->quiet = $quiet; return $this; } /** * Control subprocess parallelism limit. Use --limit to set this. * * @param int Maximum number of subprocesses to run in parallel. * @return this * * @task map */ public function setSubprocessLimit($limit) { $this->subprocessLimit = $limit; return $this; } + /** + * Control whether the ugly (but fast) or pretty (but slower) JSON formatter + * is used. + * + * @param bool If true, use the fastest formatter. + * @return this + * + * @task map + */ + public function setUgly($ugly) { + $this->ugly = $ugly; + return $this; + } + + + /** + * Control whether the map should be rebuilt, or just shown (printed to + * stdout in JSON). + * + * @param bool If true, show map instead of updating. + * @return this + * + * @task map + */ + public function setShowMap($show_map) { + $this->showMap = $show_map; + return $this; + } + + /** * Build or rebuild the library map. * * @return this * * @task map */ public function buildMap() { // Identify all the ".php" source files in the library. $this->log("Finding source files...\n"); $source_map = $this->loadSourceFileMap(); $this->log("Found ".number_format(count($source_map))." files.\n"); // Load the symbol cache with existing parsed symbols. This allows us // to remap libraries quickly by analyzing only changed files. $this->log("Loading symbol cache...\n"); $symbol_cache = $this->loadSymbolCache(); // Build out the symbol analysis for all the files in the library. For // each file, check if it's in cache. If we miss in the cache, do a fresh // analysis. $symbol_map = array(); $futures = array(); foreach ($source_map as $file => $hash) { if (!empty($symbol_cache[$hash])) { $symbol_map[$file] = $symbol_cache[$hash]; continue; } $futures[$file] = $this->buildSymbolAnalysisFuture($file); } $this->log("Found ".number_format(count($symbol_map))." files in cache.\n"); // Run the analyzer on any files which need analysis. if ($futures) { $limit = $this->subprocessLimit; $count = number_format(count($futures)); $this->log("Analyzing {$count} files with {$limit} subprocesses...\n"); foreach (Futures($futures)->limit($limit) as $file => $future) { $this->log("."); $symbol_map[$file] = $future->resolveJSON(); } $this->log("\nDone.\n"); } // We're done building the cache, so write it out immediately. Note that // we've only retained entries for files we found, so this implicitly cleans // out old cache entries. $this->writeSymbolCache($symbol_map, $source_map); - $this->log("Building library map...\n"); - $library_map = $this->buildLibraryMap($symbol_map, $source_map); - $this->log("Writing map...\n"); - $this->writeLibraryMap($library_map); + // Our map is up to date, so either show it on stdout or write it to disk. + + if ($this->showMap) { + $this->log("Showing map...\n"); + + if ($this->ugly) { + echo json_encode($symbol_map); + } else { + $json = new PhutilJSON(); + echo $json->encodeFormatted($symbol_map); + } + } else { + $this->log("Building library map...\n"); + $library_map = $this->buildLibraryMap($symbol_map, $source_map); + + $this->log("Writing map...\n"); + $this->writeLibraryMap($library_map); + } $this->log("Done.\n"); return $this; } /** * Write a status message to the user, if not running in quiet mode. * * @param string Message to write. * @return this * * @task map */ private function log($message) { if (!$this->quiet) { @fwrite(STDERR, $message); } return $this; } /* -( Path Management )---------------------------------------------------- */ /** * Get the path to some file in the library. * * @param string A library-relative path. If omitted, returns the library * root path. * @return string An absolute path. * * @task path */ private function getPath($path = '') { return $this->root.'/'.$path; } /** * Get the path to the symbol cache file. * * @return string Absolute path to symbol cache. * * @task path */ private function getPathForSymbolCache() { return $this->getPath('.phutil_module_cache'); } /** * Get the path to the map file. * * @return string Absolute path to the library map. * * @task path */ private function getPathForLibraryMap() { return $this->getPath('__phutil_library_map__.php'); } /** * Get the path to the library init file. * * @return string Absolute path to the library init file * * @task path */ private function getPathForLibraryInit() { return $this->getPath('__phutil_library_init__.php'); } /* -( Symbol Analysis and Caching )---------------------------------------- */ /** * Load the library symbol cache, if it exists and is readable and valid. * * @return dict Map of content hashes to cache of output from * `phutil_symbols.php`. * * @task symbol */ private function loadSymbolCache() { $cache_file = $this->getPathForSymbolCache(); try { $cache = Filesystem::readFile($cache_file); } catch (Exception $ex) { $cache = null; } $symbol_cache = array(); if ($cache) { $symbol_cache = json_decode($cache, true); if (!is_array($symbol_cache)) { $symbol_cache = array(); } } $version = idx($symbol_cache, self::SYMBOL_CACHE_VERSION_KEY); if ($version != self::SYMBOL_CACHE_VERSION) { // Throw away caches from a different version of the library. $symbol_cache = array(); } unset($symbol_cache[self::SYMBOL_CACHE_VERSION_KEY]); return $symbol_cache; } /** * Write a symbol map to disk cache. * * @param dict Symbol map of relative paths to symbols. * @param dict Source map (like @{method:loadSourceFileMap}). * @return void * * @task symbol */ private function writeSymbolCache(array $symbol_map, array $source_map) { $cache_file = $this->getPathForSymbolCache(); $cache = array( self::SYMBOL_CACHE_VERSION_KEY => self::SYMBOL_CACHE_VERSION, ); foreach ($symbol_map as $file => $symbols) { $cache[$source_map[$file]] = $symbols; } $json = json_encode($cache); Filesystem::writeFile($cache_file, $json); } /** * Drop the symbol cache, forcing a clean rebuild. * * @return this * * @task symbol */ public function dropSymbolCache() { $this->log("Dropping symbol cache...\n"); Filesystem::remove($this->getPathForSymbolCache()); } /** * Build a future which returns a `phutil_symbols.php` analysis of a source * file. * * @param string Relative path to the source file to analyze. * @return Future Analysis future. * * @task symbol */ private function buildSymbolAnalysisFuture($file) { $absolute_file = $this->getPath($file); $bin = dirname(__FILE__).'/phutil_symbols.php'; return new ExecFuture('%s --ugly -- %s', $bin, $absolute_file); } /* -( Source Management )-------------------------------------------------- */ /** * Build a map of all source files in a library to hashes of their content. * Returns an array like this: * * array( * 'src/parser/ExampleParser.php' => '60b725f10c9c85c70d97880dfe8191b3', * // ... * ); * * @return dict Map of library-relative paths to content hashes. * @task source */ private function loadSourceFileMap() { $root = $this->getPath(); $init = $this->getPathForLibraryInit(); if (!Filesystem::pathExists($init)) { throw new Exception("Provided path '{$root}' is not a phutil library."); } $files = id(new FileFinder($root)) ->withType('f') ->withSuffix('php') ->excludePath('*/.*') ->setGenerateChecksums(true) ->find(); $map = array(); foreach ($files as $file => $hash) { if (basename($file) == '__init__.php') { // TODO: Remove this once we kill __init__.php. This just makes the // script run faster until we do, so testing and development is less // annoying. continue; } $file = Filesystem::readablePath($file, $root); $file = ltrim($file, '/'); if (dirname($file) == '.') { // We don't permit normal source files at the root level, so just ignore // them; they're special library files. continue; } $map[$file] = $hash; } return $map; } /** * Convert the symbol analysis of all the source files in the library into * a library map. * * @param dict Symbol analysis of all source files. * @return dict Library map. * @task source */ private function buildLibraryMap(array $symbol_map) { $library_map = array( 'class' => array(), 'function' => array(), 'xmap' => array(), ); // Detect duplicate symbols within the library. foreach ($symbol_map as $file => $info) { foreach ($info['have'] as $type => $symbols) { foreach ($symbols as $symbol => $declaration) { $lib_type = ($type == 'interface') ? 'class' : $type; if (!empty($library_map[$lib_type][$symbol])) { $prior = $library_map[$lib_type][$symbol]; throw new Exception( "Definition of {$type} '{$symbol}' in file '{$file}' duplicates ". "prior definition in file '{$prior}'. You can not declare the ". "same symbol twice."); } $library_map[$lib_type][$symbol] = $file; } } $library_map['xmap'] += $info['xmap']; } // Simplify the common case (one parent) to make the file a little easier // to deal with. foreach ($library_map['xmap'] as $class => $extends) { if (count($extends) == 1) { $library_map['xmap'][$class] = reset($extends); } } // Sort the map so it is relatively stable across changes. foreach ($library_map as $key => $symbols) { ksort($symbols); $library_map[$key] = $symbols; } ksort($library_map); return $library_map; } /** * Write a finalized library map. * * @param dict Library map structure to write. * @return void * * @task source */ private function writeLibraryMap(array $library_map) { $map_file = $this->getPathForLibraryMap(); $version = self::LIBRARY_MAP_VERSION; $library_map = array( self::LIBRARY_MAP_VERSION_KEY => $version, ) + $library_map; $library_map = var_export($library_map, $return_string = true); $library_map = preg_replace('/\s+$/m', '', $library_map); $library_map = preg_replace('/array \(/', 'array(', $library_map); $at = '@'; $source_file = << array( 'ArcanistAliasWorkflow' => 'workflow/alias', 'ArcanistAmendWorkflow' => 'workflow/amend', 'ArcanistApacheLicenseLinter' => 'lint/linter/apachelicense', 'ArcanistApacheLicenseLinterTestCase' => 'lint/linter/apachelicense/__tests__', 'ArcanistBaseUnitTestEngine' => 'unit/engine/base', 'ArcanistBaseWorkflow' => 'workflow/base', 'ArcanistBranchWorkflow' => 'workflow/branch', 'ArcanistBundle' => 'parser/bundle', 'ArcanistBundleTestCase' => 'parser/bundle/__tests__', 'ArcanistCallConduitWorkflow' => 'workflow/call-conduit', 'ArcanistCapabilityNotSupportedException' => 'workflow/exception/notsupported', 'ArcanistChooseInvalidRevisionException' => 'exception', 'ArcanistChooseNoRevisionsException' => 'exception', 'ArcanistCloseRevisionWorkflow' => 'workflow/close-revision', 'ArcanistCloseWorkflow' => 'workflow/close', 'ArcanistCommentRemover' => 'parser/commentremover', 'ArcanistCommentRemoverTestCase' => 'parser/commentremover/__tests__', 'ArcanistCommitWorkflow' => 'workflow/commit', 'ArcanistConduitLinter' => 'lint/linter/conduit', 'ArcanistConfiguration' => 'configuration', 'ArcanistCoverWorkflow' => 'workflow/cover', 'ArcanistDiffChange' => 'parser/diff/change', 'ArcanistDiffChangeType' => 'parser/diff/changetype', 'ArcanistDiffHunk' => 'parser/diff/hunk', 'ArcanistDiffParser' => 'parser/diff', 'ArcanistDiffParserTestCase' => 'parser/diff/__tests__', 'ArcanistDiffUtils' => 'difference', 'ArcanistDiffUtilsTestCase' => 'difference/__tests__', 'ArcanistDiffWorkflow' => 'workflow/diff', 'ArcanistDifferentialCommitMessage' => 'differential/commitmessage', 'ArcanistDifferentialCommitMessageParserException' => 'differential/commitmessage', 'ArcanistDifferentialRevisionHash' => 'differential/constants/revisionhash', 'ArcanistDifferentialRevisionStatus' => 'differential/constants/revisionstatus', 'ArcanistDownloadWorkflow' => 'workflow/download', 'ArcanistEventType' => 'events/constant/type', 'ArcanistExportWorkflow' => 'workflow/export', 'ArcanistFilenameLinter' => 'lint/linter/filename', 'ArcanistGeneratedLinter' => 'lint/linter/generated', 'ArcanistGetConfigWorkflow' => 'workflow/get-config', 'ArcanistGitAPI' => 'repository/api/git', 'ArcanistGitHookPreReceiveWorkflow' => 'workflow/git-hook-pre-receive', 'ArcanistHelpWorkflow' => 'workflow/help', 'ArcanistHookAPI' => 'repository/hookapi/base', 'ArcanistInstallCertificateWorkflow' => 'workflow/install-certificate', 'ArcanistJSHintLinter' => 'lint/linter/jshint', 'ArcanistLandWorkflow' => 'workflow/land', 'ArcanistLiberateLintEngine' => 'lint/engine/liberate', 'ArcanistLiberateWorkflow' => 'workflow/liberate', 'ArcanistLicenseLinter' => 'lint/linter/license', 'ArcanistLintEngine' => 'lint/engine/base', 'ArcanistLintJSONRenderer' => 'lint/renderer', 'ArcanistLintLikeCompilerRenderer' => 'lint/renderer', 'ArcanistLintMessage' => 'lint/message', 'ArcanistLintPatcher' => 'lint/patcher', 'ArcanistLintRenderer' => 'lint/renderer', 'ArcanistLintResult' => 'lint/result', 'ArcanistLintSeverity' => 'lint/severity', 'ArcanistLintSummaryRenderer' => 'lint/renderer', 'ArcanistLintWorkflow' => 'workflow/lint', 'ArcanistLinter' => 'lint/linter/base', 'ArcanistLinterTestCase' => 'lint/linter/base/test', 'ArcanistListWorkflow' => 'workflow/list', 'ArcanistMarkCommittedWorkflow' => 'workflow/mark-committed', 'ArcanistMercurialAPI' => 'repository/api/mercurial', 'ArcanistMercurialParser' => 'repository/parser/mercurial', 'ArcanistMercurialParserTestCase' => 'repository/parser/mercurial/__tests__', 'ArcanistNoEffectException' => 'exception/usage/noeffect', 'ArcanistNoEngineException' => 'exception/usage/noengine', 'ArcanistNoLintLinter' => 'lint/linter/nolint', 'ArcanistNoLintTestCaseMisnamed' => 'lint/linter/nolint/__tests__', 'ArcanistPEP8Linter' => 'lint/linter/pep8', 'ArcanistPasteWorkflow' => 'workflow/paste', 'ArcanistPatchWorkflow' => 'workflow/patch', 'ArcanistPhpcsLinter' => 'lint/linter/phpcs', + 'ArcanistPhutilLibraryLinter' => 'lint/linter/phutillibrary', 'ArcanistPhutilModuleLinter' => 'lint/linter/phutilmodule', 'ArcanistPhutilTestCase' => 'unit/engine/phutil/testcase', 'ArcanistPhutilTestSkippedException' => 'unit/engine/phutil/testcase/exception', 'ArcanistPhutilTestTerminatedException' => 'unit/engine/phutil/testcase/exception', 'ArcanistPyFlakesLinter' => 'lint/linter/pyflakes', 'ArcanistPyLintLinter' => 'lint/linter/pylint', 'ArcanistRepositoryAPI' => 'repository/api/base', 'ArcanistSetConfigWorkflow' => 'workflow/set-config', 'ArcanistShellCompleteWorkflow' => 'workflow/shell-complete', 'ArcanistSpellingDefaultData' => 'lint/linter/spelling', 'ArcanistSpellingLinter' => 'lint/linter/spelling', 'ArcanistSpellingLinterTestCase' => 'lint/linter/spelling/__tests__', 'ArcanistSubversionAPI' => 'repository/api/subversion', 'ArcanistSubversionHookAPI' => 'repository/hookapi/subversion', 'ArcanistSvnHookPreCommitWorkflow' => 'workflow/svn-hook-pre-commit', 'ArcanistTasksWorkflow' => 'workflow/tasks', 'ArcanistTextLinter' => 'lint/linter/text', 'ArcanistTextLinterTestCase' => 'lint/linter/text/__tests__', 'ArcanistUncommittedChangesException' => 'exception/usage/uncommittedchanges', 'ArcanistUnitTestResult' => 'unit/result', 'ArcanistUnitWorkflow' => 'workflow/unit', 'ArcanistUpgradeWorkflow' => 'workflow/upgrade', 'ArcanistUploadWorkflow' => 'workflow/upload', 'ArcanistUsageException' => 'exception/usage', 'ArcanistUserAbortException' => 'exception/usage/userabort', 'ArcanistWhichWorkflow' => 'workflow/which', 'ArcanistWorkingCopyIdentity' => 'workingcopyidentity', 'ArcanistXHPASTLintNamingHook' => 'lint/linter/xhpast/naminghook', 'ArcanistXHPASTLintNamingHookTestCase' => 'lint/linter/xhpast/naminghook/__tests__', 'ArcanistXHPASTLinter' => 'lint/linter/xhpast', 'ArcanistXHPASTLinterTestCase' => 'lint/linter/xhpast/__tests__', 'BranchInfo' => 'branch', 'ComprehensiveLintEngine' => 'lint/engine/comprehensive', 'ExampleLintEngine' => 'lint/engine/example', 'NoseTestEngine' => 'unit/engine/nose', 'PhpunitTestEngine' => 'unit/engine/phpunit', 'PhutilLintEngine' => 'lint/engine/phutil', 'PhutilModuleRequirements' => 'parser/phutilmodule', 'PhutilUnitTestEngine' => 'unit/engine/phutil', 'PhutilUnitTestEngineTestCase' => 'unit/engine/phutil/__tests__', 'UnitTestableArcanistLintEngine' => 'lint/engine/test', ), 'function' => array( ), 'requires_class' => array( 'ArcanistAliasWorkflow' => 'ArcanistBaseWorkflow', 'ArcanistAmendWorkflow' => 'ArcanistBaseWorkflow', 'ArcanistApacheLicenseLinter' => 'ArcanistLicenseLinter', 'ArcanistApacheLicenseLinterTestCase' => 'ArcanistLinterTestCase', 'ArcanistBranchWorkflow' => 'ArcanistBaseWorkflow', 'ArcanistBundleTestCase' => 'ArcanistPhutilTestCase', 'ArcanistCallConduitWorkflow' => 'ArcanistBaseWorkflow', 'ArcanistCloseRevisionWorkflow' => 'ArcanistBaseWorkflow', 'ArcanistCloseWorkflow' => 'ArcanistBaseWorkflow', 'ArcanistCommentRemoverTestCase' => 'ArcanistPhutilTestCase', 'ArcanistCommitWorkflow' => 'ArcanistBaseWorkflow', 'ArcanistConduitLinter' => 'ArcanistLinter', 'ArcanistCoverWorkflow' => 'ArcanistBaseWorkflow', 'ArcanistDiffParserTestCase' => 'ArcanistPhutilTestCase', 'ArcanistDiffUtilsTestCase' => 'ArcanistPhutilTestCase', 'ArcanistDiffWorkflow' => 'ArcanistBaseWorkflow', 'ArcanistDownloadWorkflow' => 'ArcanistBaseWorkflow', 'ArcanistEventType' => 'PhutilEventType', 'ArcanistExportWorkflow' => 'ArcanistBaseWorkflow', 'ArcanistFilenameLinter' => 'ArcanistLinter', 'ArcanistGeneratedLinter' => 'ArcanistLinter', 'ArcanistGetConfigWorkflow' => 'ArcanistBaseWorkflow', 'ArcanistGitAPI' => 'ArcanistRepositoryAPI', 'ArcanistGitHookPreReceiveWorkflow' => 'ArcanistBaseWorkflow', 'ArcanistHelpWorkflow' => 'ArcanistBaseWorkflow', 'ArcanistInstallCertificateWorkflow' => 'ArcanistBaseWorkflow', 'ArcanistJSHintLinter' => 'ArcanistLinter', 'ArcanistLandWorkflow' => 'ArcanistBaseWorkflow', 'ArcanistLiberateLintEngine' => 'ArcanistLintEngine', 'ArcanistLiberateWorkflow' => 'ArcanistBaseWorkflow', 'ArcanistLicenseLinter' => 'ArcanistLinter', 'ArcanistLintWorkflow' => 'ArcanistBaseWorkflow', 'ArcanistLinterTestCase' => 'ArcanistPhutilTestCase', 'ArcanistListWorkflow' => 'ArcanistBaseWorkflow', 'ArcanistMarkCommittedWorkflow' => 'ArcanistBaseWorkflow', 'ArcanistMercurialAPI' => 'ArcanistRepositoryAPI', 'ArcanistMercurialParserTestCase' => 'ArcanistPhutilTestCase', 'ArcanistNoEffectException' => 'ArcanistUsageException', 'ArcanistNoEngineException' => 'ArcanistUsageException', 'ArcanistNoLintLinter' => 'ArcanistLinter', 'ArcanistNoLintTestCaseMisnamed' => 'ArcanistLinterTestCase', 'ArcanistPEP8Linter' => 'ArcanistLinter', 'ArcanistPasteWorkflow' => 'ArcanistBaseWorkflow', 'ArcanistPatchWorkflow' => 'ArcanistBaseWorkflow', 'ArcanistPhpcsLinter' => 'ArcanistLinter', + 'ArcanistPhutilLibraryLinter' => 'ArcanistLinter', 'ArcanistPhutilModuleLinter' => 'ArcanistLinter', 'ArcanistPyFlakesLinter' => 'ArcanistLinter', 'ArcanistPyLintLinter' => 'ArcanistLinter', 'ArcanistSetConfigWorkflow' => 'ArcanistBaseWorkflow', 'ArcanistShellCompleteWorkflow' => 'ArcanistBaseWorkflow', 'ArcanistSpellingLinter' => 'ArcanistLinter', 'ArcanistSpellingLinterTestCase' => 'ArcanistLinterTestCase', 'ArcanistSubversionAPI' => 'ArcanistRepositoryAPI', 'ArcanistSubversionHookAPI' => 'ArcanistHookAPI', 'ArcanistSvnHookPreCommitWorkflow' => 'ArcanistBaseWorkflow', 'ArcanistTasksWorkflow' => 'ArcanistBaseWorkflow', 'ArcanistTextLinter' => 'ArcanistLinter', 'ArcanistTextLinterTestCase' => 'ArcanistLinterTestCase', 'ArcanistUncommittedChangesException' => 'ArcanistUsageException', 'ArcanistUnitWorkflow' => 'ArcanistBaseWorkflow', 'ArcanistUpgradeWorkflow' => 'ArcanistBaseWorkflow', 'ArcanistUploadWorkflow' => 'ArcanistBaseWorkflow', 'ArcanistUserAbortException' => 'ArcanistUsageException', 'ArcanistWhichWorkflow' => 'ArcanistBaseWorkflow', 'ArcanistXHPASTLintNamingHookTestCase' => 'ArcanistPhutilTestCase', 'ArcanistXHPASTLinter' => 'ArcanistLinter', 'ArcanistXHPASTLinterTestCase' => 'ArcanistLinterTestCase', 'ComprehensiveLintEngine' => 'ArcanistLintEngine', 'ExampleLintEngine' => 'ArcanistLintEngine', 'NoseTestEngine' => 'ArcanistBaseUnitTestEngine', 'PhpunitTestEngine' => 'ArcanistBaseUnitTestEngine', 'PhutilLintEngine' => 'ArcanistLintEngine', 'PhutilUnitTestEngine' => 'ArcanistBaseUnitTestEngine', 'PhutilUnitTestEngineTestCase' => 'ArcanistPhutilTestCase', 'UnitTestableArcanistLintEngine' => 'ArcanistLintEngine', ), 'requires_interface' => array( ), )); diff --git a/src/lint/linter/phutillibrary/ArcanistPhutilLibraryLinter.php b/src/lint/linter/phutillibrary/ArcanistPhutilLibraryLinter.php new file mode 100644 index 00000000..028eedb4 --- /dev/null +++ b/src/lint/linter/phutillibrary/ArcanistPhutilLibraryLinter.php @@ -0,0 +1,190 @@ + 'Unknown Symbol', + self::LINT_DUPLICATE_SYMBOL => 'Duplicate Symbol', + self::LINT_ONE_CLASS_PER_FILE => 'One Class Per File', + ); + } + + public function getLinterName() { + return 'PHL'; + } + + public function getLintSeverityMap() { + return array(); + } + + public function willLintPaths(array $paths) { + if (!xhpast_is_available()) { + throw new Exception(xhpast_get_build_instructions()); + } + + // NOTE: For now, we completely ignore paths and just lint every library in + // its entirety. This is simpler and relatively fast because we don't do any + // detailed checks and all the data we need for this comes out of module + // caches. + + $bootloader = PhutilBootloader::getInstance(); + $libs = $bootloader->getAllLibraries(); + + // Load the up-to-date map for each library, without loading the library + // itself. This means lint results will accurately reflect the state of + // the working copy. + + $arc_root = dirname(phutil_get_library_root('arcanist')); + $bin = "{$arc_root}/scripts/phutil_rebuild_map.php"; + + $symbols = array(); + foreach ($libs as $lib) { + // Do these one at a time since they individually fanout to saturate + // available system resources. + $future = new ExecFuture( + '%s --show --quiet --ugly -- %s', + $bin, + phutil_get_library_root($lib)); + $symbols[$lib] = $future->resolveJSON(); + } + + $all_symbols = array(); + foreach ($symbols as $library => $map) { + // Check for files which declare more than one class/interface in the same + // file, or mix function definitions with class/interface definitions. We + // must isolate autoloadable symbols to one per file so the autoloader + // can't end up in an unresolvable cycle. + foreach ($map as $file => $spec) { + $have = idx($spec, 'have', array()); + + $have_classes = + idx($have, 'class', array()) + + idx($have, 'interface', array()); + $have_functions = idx($have, 'function'); + + if ($have_functions && $have_classes) { + $function_list = implode(', ', array_keys($have_functions)); + $class_list = implode(', ', array_keys($have_classes)); + $this->raiseLintInLibrary( + $library, + $file, + end($have_functions), + self::LINT_ONE_CLASS_PER_FILE, + "File '{$file}' mixes function ({$function_list}) and ". + "class/interface ({$class_list}) definitions in the same file. ". + "A file which declares a class or an interface MUST ". + "declare nothing else."); + } else if (count($have_classes) > 1) { + $class_list = implode(', ', array_keys($have_classes)); + $this->raiseLintInLibrary( + $library, + $file, + end($have_classes), + self::LINT_ONE_CLASS_PER_FILE, + "File '{$file}' declares more than one class or interface ". + "({$class_list}). A file which declares a class or interface MUST ". + "declare nothing else."); + } + } + + // Check for duplicate symbols: two files providing the same class or + // function. + foreach ($map as $file => $spec) { + $have = idx($spec, 'have', array()); + foreach (array('class', 'function', 'interface') as $type) { + $libtype = ($type == 'interface') ? 'class' : $type; + foreach (idx($have, $type, array()) as $symbol => $offset) { + if (empty($all_symbols[$libtype][$symbol])) { + $all_symbols[$libtype][$symbol] = array( + 'library' => $library, + 'file' => $file, + 'offset' => $offset, + ); + continue; + } + + $osrc = $all_symbols[$libtype][$symbol]['file']; + $olib = $all_symbols[$libtype][$symbol]['library']; + + $this->raiseLintInLibrary( + $library, + $file, + $offset, + self::LINT_DUPLICATE_SYMBOL, + "Definition of {$type} '{$symbol}' in '{$file}' in library ". + "'{$library}' duplicates prior definition in '{$osrc}' in ". + "library '{$olib}'."); + } + } + } + } + + foreach ($symbols as $library => $map) { + // Check for unknown symbols: uses of classes, functions or interfaces + // which are not defined anywhere. We reference the list of all symbols + // we built up earlier. + foreach ($map as $file => $spec) { + $need = idx($spec, 'need', array()); + foreach (array('class', 'function', 'interface') as $type) { + $libtype = ($type == 'interface') ? 'class' : $type; + foreach (idx($need, $type, array()) as $symbol => $offset) { + if (!empty($all_symbols[$libtype][$symbol])) { + // Symbol is defined somewhere. + continue; + } + + $this->raiseLintInLibrary( + $library, + $file, + $offset, + self::LINT_UNKNOWN_SYMBOL, + "Use of unknown {$type} '{$symbol}'. This symbol is not defined ". + "in any loaded phutil library."); + } + } + } + } + } + + private function raiseLintInLibrary($library, $path, $offset, $code, $desc) { + $root = phutil_get_library_root($library); + + $this->activePath = $root.'/'.$path; + $this->raiseLintAtOffset($offset, $code, $desc); + } + + public function lintPath($path) { + return; + } +} diff --git a/src/lint/linter/phutillibrary/__init__.php b/src/lint/linter/phutillibrary/__init__.php new file mode 100644 index 00000000..e22eb549 --- /dev/null +++ b/src/lint/linter/phutillibrary/__init__.php @@ -0,0 +1,17 @@ +