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 @@ -47,6 +47,15 @@ $args->parse( '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, @@ -68,6 +77,11 @@ if ($args->getArg('drop-cache')) { $builder->dropSymbolCache(); } +if ($args->getArg('show')) { + $builder->setShowMap(true); + $builder->setUgly($args->getArg('ugly')); +} + $builder->buildMap(); exit(0); @@ -89,6 +103,8 @@ 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; @@ -140,6 +156,36 @@ final class PhutilLibraryMapBuilder { } + /** + * 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. * @@ -196,11 +242,25 @@ final class PhutilLibraryMapBuilder { // 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"); diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index 77137341..c770c425 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -82,6 +82,7 @@ phutil_register_library_map(array( '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', @@ -174,6 +175,7 @@ phutil_register_library_map(array( 'ArcanistPasteWorkflow' => 'ArcanistBaseWorkflow', 'ArcanistPatchWorkflow' => 'ArcanistBaseWorkflow', 'ArcanistPhpcsLinter' => 'ArcanistLinter', + 'ArcanistPhutilLibraryLinter' => 'ArcanistLinter', 'ArcanistPhutilModuleLinter' => 'ArcanistLinter', 'ArcanistPyFlakesLinter' => 'ArcanistLinter', 'ArcanistPyLintLinter' => 'ArcanistLinter', 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 @@ +