diff --git a/src/lint/linter/ArcanistPhutilLibraryLinter.php b/src/lint/linter/ArcanistPhutilLibraryLinter.php index ba89e0c0..3cef927b 100644 --- a/src/lint/linter/ArcanistPhutilLibraryLinter.php +++ b/src/lint/linter/ArcanistPhutilLibraryLinter.php @@ -1,335 +1,370 @@ pht('Unknown Symbol'), self::LINT_DUPLICATE_SYMBOL => pht('Duplicate Symbol'), self::LINT_ONE_CLASS_PER_FILE => pht('One Class Per File'), self::LINT_NONCANONICAL_SYMBOL => pht('Noncanonical Symbol'), ); } public function getLinterPriority() { return 2.0; } public function willLintPaths(array $paths) { + + $libtype_map = array( + 'class' => 'class', + 'function' => 'function', + 'interface' => 'class', + 'class/interface' => 'class', + ); + // 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(); $libraries = $bootloader->getAllLibraries(); + // Load all the builtin symbols first. + $builtin_map = PhutilLibraryMapBuilder::newBuiltinMap(); + $builtin_map = $builtin_map['have']; + + $normal_symbols = array(); + $all_symbols = array(); + foreach ($builtin_map as $type => $builtin_symbols) { + $libtype = $libtype_map[$type]; + foreach ($builtin_symbols as $builtin_symbol => $ignored) { + $normal_symbol = $this->normalizeSymbol($builtin_symbol); + $normal_symbols[$type][$normal_symbol] = $builtin_symbol; + + $all_symbols[$libtype][$builtin_symbol] = array( + 'library' => null, + 'file' => null, + 'offset' => null, + ); + } + } + // 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. $symbols = array(); foreach ($libraries as $library) { $root = phutil_get_library_root($library); try { $symbols[$library] = id(new PhutilLibraryMapBuilder($root)) ->buildFileSymbolMap(); } catch (XHPASTSyntaxErrorException $ex) { // If the library contains a syntax error then there isn't much that we // can do. continue; } } - $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, pht( "File '%s' mixes function (%s) and class/interface (%s) ". "definitions in the same file. A file which declares a class ". "or an interface MUST declare nothing else.", $file, $function_list, $class_list)); } 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, pht( "File '%s' declares more than one class or interface (%s). ". "A file which declares a class or interface MUST declare ". "nothing else.", $file, $class_list)); } } - $libtype_map = array( - 'class' => 'class', - 'function' => 'function', - 'interface' => 'class', - 'class/interface' => 'class', - ); - // Check for duplicate symbols: two files providing the same class or // function. While doing this, we also build a map of normalized symbol // names to original symbol names: we want a definition of "idx()" to // collide with a definition of "IdX()", and want to perform spelling // corrections later. - $normal_symbols = array(); foreach ($map as $file => $spec) { $have = idx($spec, 'have', array()); foreach (array('class', 'function', 'interface') as $type) { $libtype = $libtype_map[$type]; foreach (idx($have, $type, array()) as $symbol => $offset) { $normal_symbol = $this->normalizeSymbol($symbol); if (empty($normal_symbols[$libtype][$normal_symbol])) { $normal_symbols[$libtype][$normal_symbol] = $symbol; $all_symbols[$libtype][$symbol] = array( 'library' => $library, 'file' => $file, 'offset' => $offset, ); continue; } $old_symbol = $normal_symbols[$libtype][$normal_symbol]; $old_src = $all_symbols[$libtype][$old_symbol]['file']; $old_lib = $all_symbols[$libtype][$old_symbol]['library']; - $this->raiseLintInLibrary( - $library, - $file, - $offset, - self::LINT_DUPLICATE_SYMBOL, - pht( + // If these values are "null", it means that the symbol is a + // builtin symbol provided by PHP or a PHP extension. + + if ($old_lib === null) { + $message = pht( + 'Definition of symbol "%s" (of type "%s") in file "%s" in '. + 'library "%s" duplicates builtin definition of the same '. + 'symbol.', + $symbol, + $type, + $file, + $library); + } else { + $message = pht( 'Definition of symbol "%s" (of type "%s") in file "%s" in '. 'library "%s" duplicates prior definition in file "%s" in '. 'library "%s".', $symbol, $type, $file, $library, $old_src, - $old_lib)); + $old_lib); + } + + $this->raiseLintInLibrary( + $library, + $file, + $offset, + self::LINT_DUPLICATE_SYMBOL, + $message); } } } } $types = array('class', 'function', 'interface', 'class/interface'); 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 ($types as $type) { $libtype = $libtype_map[$type]; foreach (idx($need, $type, array()) as $symbol => $offset) { if (!empty($all_symbols[$libtype][$symbol])) { // Symbol is defined somewhere. continue; } $normal_symbol = $this->normalizeSymbol($symbol); if (!empty($normal_symbols[$libtype][$normal_symbol])) { $proper_symbol = $normal_symbols[$libtype][$normal_symbol]; switch ($type) { case 'class': $summary = pht( 'Class symbol "%s" should be written as "%s".', $symbol, $proper_symbol); break; case 'function': $summary = pht( 'Function symbol "%s" should be written as "%s".', $symbol, $proper_symbol); break; case 'interface': $summary = pht( 'Interface symbol "%s" should be written as "%s".', $symbol, $proper_symbol); break; case 'class/interface': $summary = pht( 'Class or interface symbol "%s" should be written as "%s".', $symbol, $proper_symbol); break; default: throw new Exception( pht('Unknown symbol type "%s".', $type)); } $this->raiseLintInLibrary( $library, $file, $offset, self::LINT_NONCANONICAL_SYMBOL, $summary, $symbol, $proper_symbol); continue; } $arcanist_root = dirname(phutil_get_library_root('arcanist')); switch ($type) { case 'class': $summary = pht( 'Use of unknown class symbol "%s".', $symbol); break; case 'function': $summary = pht( 'Use of unknown function symbol "%s".', $symbol); break; case 'interface': $summary = pht( 'Use of unknown interface symbol "%s".', $symbol); break; case 'class/interface': $summary = pht( 'Use of unknown class or interface symbol "%s".', $symbol); break; } $details = pht( "Common causes are:\n". "\n". " - Your copy of Arcanist is out of date.\n". " This is the most common cause.\n". " Update this copy of Arcanist:\n". "\n". " %s\n". "\n". " - Some other library is out of date.\n". " Update the library this symbol appears in.\n". "\n". " - The symbol is misspelled.\n". " Spell the symbol name correctly.\n". "\n". " - You added the symbol recently, but have not updated\n". " the symbol map for the library.\n". " Run \"arc liberate\" in the library where the symbol is\n". " defined.\n". "\n". " - This symbol is defined in an external library.\n". " Use \"@phutil-external-symbol\" to annotate it.\n". " Use \"grep\" to find examples of usage.", $arcanist_root); $message = implode( "\n\n", array( $summary, $details, )); $this->raiseLintInLibrary( $library, $file, $offset, self::LINT_UNKNOWN_SYMBOL, $message); } } } } } private function raiseLintInLibrary( $library, $path, $offset, $code, $desc, $original = null, $replacement = null) { $root = phutil_get_library_root($library); $this->activePath = $root.'/'.$path; $this->raiseLintAtOffset($offset, $code, $desc, $original, $replacement); } public function lintPath($path) { return; } private function normalizeSymbol($symbol) { return phutil_utf8_strtolower($symbol); } } diff --git a/src/moduleutils/PhutilLibraryMapBuilder.php b/src/moduleutils/PhutilLibraryMapBuilder.php index c036ed4c..e590e092 100644 --- a/src/moduleutils/PhutilLibraryMapBuilder.php +++ b/src/moduleutils/PhutilLibraryMapBuilder.php @@ -1,514 +1,534 @@ 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; } /** * Get the map of symbols in this library, analyzing the library to build it * if necessary. * * @return map Information about symbols in this library. * * @task map */ public function buildMap() { if ($this->librarySymbolMap === null) { $this->analyzeLibrary(); } return $this->librarySymbolMap; } /** * Get the map of files in this library, analyzing the library to build it * if necessary. * * Returns a map of file paths to information about symbols used and defined * in the file. * * @return map Information about files in this library. * * @task map */ public function buildFileSymbolMap() { if ($this->fileSymbolMap === null) { $this->analyzeLibrary(); } return $this->fileSymbolMap; } /** * Build and update the library map. * * @return void * * @task map */ public function buildAndWriteMap() { $library_map = $this->buildMap(); $this->log(pht('Writing map...')); $this->writeLibraryMap($library_map); } /** * 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, "%s\n", $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 * `extract-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) { try { $symbol_cache = phutil_json_decode($cache); } catch (PhutilJSONParserException $ex) { $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); try { Filesystem::writeFile($cache_file, $json); } catch (FilesystemException $ex) { $this->log(pht('Unable to save the cache!')); } } /** * Drop the symbol cache, forcing a clean rebuild. * * @return this * * @task symbol */ public function dropSymbolCache() { $this->log(pht('Dropping symbol cache...')); Filesystem::remove($this->getPathForSymbolCache()); } /** * Build a future which returns a `extract-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); + return self::newExtractSymbolsFuture( + array(), + array($absolute_file)); + } + + private static function newExtractSymbolsFuture(array $flags, array $paths) { $bin = dirname(__FILE__).'/../../support/lib/extract-symbols.php'; - return new ExecFuture('php -f %R -- --ugly %R', $bin, $absolute_file); + return new ExecFuture( + 'php -f %R -- --ugly %Ls -- %Ls', + $bin, + $flags, + $paths); + } + + public static function newBuiltinMap() { + $future = self::newExtractSymbolsFuture( + array('--builtins'), + array()); + + list($json) = $future->resolvex(); + + return phutil_json_decode($json); } /* -( 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( pht( "Provided path '%s' is not a %s library.", $root, 'phutil')); } $files = id(new FileFinder($root)) ->withType('f') ->withSuffix('php') ->excludePath('*/.*') ->setGenerateChecksums(true) ->find(); $extensions_dir = 'extensions/'; $extensions_len = strlen($extensions_dir); $map = array(); foreach ($files as $file => $hash) { $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; } // Ignore files in the extensions/ directory. if (!strncmp($file, $extensions_dir, $extensions_len)) { continue; } // We include also filename in the hash to handle cases when the file is // moved without modifying its content. $map[$file] = md5($hash.$file); } 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( pht( "Definition of %s '%s' in file '%s' duplicates prior ". "definition in file '%s'. You can not declare the ". "same symbol twice.", $type, $symbol, $file, $prior)); } $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 = phutil_var_export($library_map); $at = '@'; $source_file = <<log(pht('Finding source files...')); $source_map = $this->loadSourceFileMap(); $this->log( pht('Found %s files.', new PhutilNumber(count($source_map)))); // Load the symbol cache with existing parsed symbols. This allows us // to remap libraries quickly by analyzing only changed files. $this->log(pht('Loading symbol cache...')); $symbol_cache = $this->loadSymbolCache(); // If the XHPAST binary is not up-to-date, build it now. Otherwise, // `extract-symbols.php` will attempt to build the binary and will fail // miserably because it will be trying to build the same file multiple // times in parallel. if (!PhutilXHPASTBinary::isAvailable()) { PhutilXHPASTBinary::build(); } // 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( pht('Found %s files in cache.', new PhutilNumber(count($symbol_map)))); // Run the analyzer on any files which need analysis. if ($futures) { $limit = $this->subprocessLimit; $this->log( pht( 'Analyzing %s file(s) with %s subprocess(es)...', phutil_count($futures), new PhutilNumber($limit))); $progress = new PhutilConsoleProgressBar(); if ($this->quiet) { $progress->setQuiet(true); } $progress->setTotal(count($futures)); $futures = id(new FutureIterator($futures)) ->limit($limit); foreach ($futures as $file => $future) { $result = $future->resolveJSON(); if (empty($result['error'])) { $symbol_map[$file] = $result; } else { $progress->done(false); throw new XHPASTSyntaxErrorException( $result['line'], $file.': '.$result['error']); } $progress->update(1); } $progress->done(); } $this->fileSymbolMap = $symbol_map; // 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); // Our map is up to date, so either show it on stdout or write it to disk. $this->log(pht('Building library map...')); $this->librarySymbolMap = $this->buildLibraryMap($symbol_map); } } diff --git a/support/lib/extract-symbols.php b/support/lib/extract-symbols.php index 7b9378b1..dac4ca71 100755 --- a/support/lib/extract-symbols.php +++ b/support/lib/extract-symbols.php @@ -1,587 +1,609 @@ #!/usr/bin/env php setTagline(pht('identify symbols in a PHP source file')); $args->setSynopsis(<<parseStandardArguments(); $args->parse( array( array( 'name' => 'all', 'help' => pht( - 'Report all symbols, including built-ins and declared externals.'), + 'Emit all symbols, including built-ins and declared externals.'), + ), + array( + 'name' => 'builtins', + 'help' => pht('Emit builtin symbols.'), ), array( 'name' => 'ugly', 'help' => pht('Do not prettify JSON output.'), ), array( 'name' => 'path', 'wildcard' => true, 'help' => pht('PHP Source file to analyze.'), ), )); $paths = $args->getArg('path'); -if (count($paths) !== 1) { - throw new Exception(pht('Specify exactly one path!')); -} -$path = Filesystem::resolvePath(head($paths)); $show_all = $args->getArg('all'); +$show_builtins = $args->getArg('builtins'); + +if ($show_all && $show_builtins) { + throw new PhutilArgumentUsageException( + pht( + 'Flags "--all" and "--builtins" are not compatible.')); +} -$source_code = Filesystem::readFile($path); +if ($show_builtins && $paths) { + throw new PhutilArgumentUsageException( + pht( + 'Flag "--builtins" may not be used with a path.')); +} + +if ($show_builtins) { + $path = ''; + $source_code = ''; +} else { + if (count($paths) !== 1) { + throw new Exception(pht('Specify exactly one path!')); + } + $path = Filesystem::resolvePath(head($paths)); + $source_code = Filesystem::readFile($path); +} try { $tree = XHPASTTree::newFromData($source_code); } catch (XHPASTSyntaxErrorException $ex) { $result = array( 'error' => $ex->getMessage(), 'line' => $ex->getErrorLine(), 'file' => $path, ); $json = new PhutilJSON(); echo $json->encodeFormatted($result); exit(0); } $root = $tree->getRootNode(); $root->buildSelectCache(); // -( Unsupported Constructs )------------------------------------------------ $namespaces = $root->selectDescendantsOfType('n_NAMESPACE'); foreach ($namespaces as $namespace) { phutil_fail_on_unsupported_feature($namespace, $path, pht('namespaces')); } $uses = $root->selectDescendantsOfType('n_USE'); foreach ($namespaces as $namespace) { phutil_fail_on_unsupported_feature( $namespace, $path, pht('namespace `%s` statements', 'use')); } $possible_traits = $root->selectDescendantsOfType('n_CLASS_DECLARATION'); foreach ($possible_traits as $possible_trait) { $attributes = $possible_trait->getChildByIndex(0); // Can't use getChildByIndex here because not all classes have attributes foreach ($attributes->getChildren() as $attribute) { if (strtolower($attribute->getConcreteString()) === 'trait') { phutil_fail_on_unsupported_feature($possible_trait, $path, pht('traits')); } } } // -( Marked Externals )------------------------------------------------------ // Identify symbols marked with "@phutil-external-symbol", so we exclude them // from the dependency list. $externals = array(); $doc_parser = new PhutilDocblockParser(); foreach ($root->getTokens() as $token) { if ($token->getTypeName() === 'T_DOC_COMMENT') { list($block, $special) = $doc_parser->parse($token->getValue()); $ext_list = idx($special, 'phutil-external-symbol'); $ext_list = (array)$ext_list; $ext_list = array_filter($ext_list); foreach ($ext_list as $ext_ref) { $matches = null; if (preg_match('/^\s*(\S+)\s+(\S+)/', $ext_ref, $matches)) { $externals[$matches[1]][$matches[2]] = true; } } } } // -( Declarations and Dependencies )----------------------------------------- // The first stage of analysis is to find all the symbols we declare in the // file (like functions and classes) and all the symbols we use in the file // (like calling functions and invoking classes). Later, we filter this list // to exclude builtins. $have = array(); // For symbols we declare. $need = array(); // For symbols we use. $xmap = array(); // For extended classes and implemented interfaces. // -( Functions )------------------------------------------------------------- // Find functions declared in this file. // This is "function f() { ... }". $functions = $root->selectDescendantsOfType('n_FUNCTION_DECLARATION'); foreach ($functions as $function) { $name = $function->getChildByIndex(2); if ($name->getTypeName() === 'n_EMPTY') { // This is an anonymous function; don't record it into the symbol // index. continue; } $have[] = array( 'type' => 'function', 'symbol' => $name, ); } // Find functions used by this file. Uses: // // - Explicit Call // - String literal passed to call_user_func() or call_user_func_array() // - String literal in array literal in call_user_func()/call_user_func_array() // // TODO: Possibly support these: // // - String literal in ReflectionFunction(). // This is "f();". $calls = $root->selectDescendantsOfType('n_FUNCTION_CALL'); foreach ($calls as $call) { $name = $call->getChildByIndex(0); if ($name->getTypeName() === 'n_VARIABLE' || $name->getTypeName() === 'n_VARIABLE_VARIABLE') { // Ignore these, we can't analyze them. continue; } if ($name->getTypeName() === 'n_CLASS_STATIC_ACCESS') { // These are "C::f()", we'll pick this up later on. continue; } $call_name = $name->getConcreteString(); if ($call_name === 'call_user_func' || $call_name === 'call_user_func_array') { $params = $call->getChildByIndex(1)->getChildren(); if (!count($params)) { // This is a bare call_user_func() with no arguments; just ignore it. continue; } $symbol = array_shift($params); $type = 'function'; $symbol_value = $symbol->getStringLiteralValue(); $pos = strpos($symbol_value, '::'); if ($pos) { $type = 'class'; $symbol_value = substr($symbol_value, 0, $pos); } else if ($symbol->getTypeName() === 'n_ARRAY_LITERAL') { try { $type = 'class'; $symbol_value = idx($symbol->evalStatic(), 0); } catch (Exception $ex) {} } if ($symbol_value && strpos($symbol_value, '$') === false) { $need[] = array( 'type' => $type, 'name' => $symbol_value, 'symbol' => $symbol, ); } } else { $need[] = array( 'type' => 'function', 'symbol' => $name, ); } } // -( Classes )--------------------------------------------------------------- // Find classes declared by this file. // This is "class X ... { ... }". $classes = $root->selectDescendantsOfType('n_CLASS_DECLARATION'); foreach ($classes as $class) { $class_name = $class->getChildByIndex(1); $have[] = array( 'type' => 'class', 'symbol' => $class_name, ); } // Find classes used by this file. We identify these: // // - class ... extends X // - new X // - Static method call // - Static property access // - Use of class constant // - typehints // - catch // - instanceof // - newv() // // TODO: Possibly support these: // // - String literal in ReflectionClass(). // This is "class X ... { ... }". $classes = $root->selectDescendantsOfType('n_CLASS_DECLARATION'); foreach ($classes as $class) { $class_name = $class->getChildByIndex(1)->getConcreteString(); $extends = $class->getChildByIndex(2); foreach ($extends->selectDescendantsOfType('n_CLASS_NAME') as $parent) { $need[] = array( 'type' => 'class', 'symbol' => $parent, ); // Track all 'extends' in the extension map. $xmap[$class_name][] = $parent->getConcreteString(); } } // This is "new X()". $uses_of_new = $root->selectDescendantsOfType('n_NEW'); foreach ($uses_of_new as $new_operator) { $name = $new_operator->getChildByIndex(0); if ($name->getTypeName() === 'n_VARIABLE' || $name->getTypeName() === 'n_VARIABLE_VARIABLE') { continue; } $need[] = array( 'type' => 'class', 'symbol' => $name, ); } // This covers all of "X::$y", "X::y()" and "X::CONST". $static_uses = $root->selectDescendantsOfType('n_CLASS_STATIC_ACCESS'); foreach ($static_uses as $static_use) { $name = $static_use->getChildByIndex(0); if ($name->getTypeName() !== 'n_CLASS_NAME') { continue; } $need[] = array( 'type' => 'class', 'symbol' => $name, ); } // This is "function (X $x)". $parameters = $root->selectDescendantsOfType('n_DECLARATION_PARAMETER'); foreach ($parameters as $parameter) { $hint = $parameter->getChildByIndex(0); if ($hint->getTypeName() !== 'n_CLASS_NAME') { continue; } $need[] = array( 'type' => 'class/interface', 'symbol' => $hint, ); } $returns = $root->selectDescendantsOfType('n_DECLARATION_RETURN'); foreach ($returns as $return) { $hint = $return->getChildByIndex(0); if ($hint->getTypeName() !== 'n_CLASS_NAME') { continue; } $need[] = array( 'type' => 'class/interface', 'symbol' => $hint, ); } // This is "catch (Exception $ex)". $catches = $root->selectDescendantsOfType('n_CATCH'); foreach ($catches as $catch) { $need[] = array( 'type' => 'class/interface', 'symbol' => $catch->getChildOfType(0, 'n_CLASS_NAME'), ); } // This is "$x instanceof X". $instanceofs = $root->selectDescendantsOfType('n_BINARY_EXPRESSION'); foreach ($instanceofs as $instanceof) { $operator = $instanceof->getChildOfType(1, 'n_OPERATOR'); if ($operator->getConcreteString() !== 'instanceof') { continue; } $class = $instanceof->getChildByIndex(2); if ($class->getTypeName() !== 'n_CLASS_NAME') { continue; } $need[] = array( 'type' => 'class/interface', 'symbol' => $class, ); } // This is "newv('X')". $calls = $root->selectDescendantsOfType('n_FUNCTION_CALL'); foreach ($calls as $call) { $call_name = $call->getChildByIndex(0)->getConcreteString(); if ($call_name !== 'newv') { continue; } $params = $call->getChildByIndex(1)->getChildren(); if (!count($params)) { continue; } $symbol = reset($params); $symbol_value = $symbol->getStringLiteralValue(); if ($symbol_value && strpos($symbol_value, '$') === false) { $need[] = array( 'type' => 'class', 'name' => $symbol_value, 'symbol' => $symbol, ); } } // -( Interfaces )------------------------------------------------------------ // Find interfaces declared in this file. // This is "interface X .. { ... }". $interfaces = $root->selectDescendantsOfType('n_INTERFACE_DECLARATION'); foreach ($interfaces as $interface) { $interface_name = $interface->getChildByIndex(1); $have[] = array( 'type' => 'interface', 'symbol' => $interface_name, ); } // Find interfaces used by this file. We identify these: // // - class ... implements X // - interface ... extends X // This is "class X ... { ... }". $classes = $root->selectDescendantsOfType('n_CLASS_DECLARATION'); foreach ($classes as $class) { $class_name = $class->getChildByIndex(1)->getConcreteString(); $implements = $class->getChildByIndex(3); $interfaces = $implements->selectDescendantsOfType('n_CLASS_NAME'); foreach ($interfaces as $interface) { $need[] = array( 'type' => 'interface', 'symbol' => $interface, ); // Track 'class ... implements' in the extension map. $xmap[$class_name][] = $interface->getConcreteString(); } } // This is "interface X ... { ... }". $interfaces = $root->selectDescendantsOfType('n_INTERFACE_DECLARATION'); foreach ($interfaces as $interface) { $interface_name = $interface->getChildByIndex(1)->getConcreteString(); $extends = $interface->getChildByIndex(2); foreach ($extends->selectDescendantsOfType('n_CLASS_NAME') as $parent) { $need[] = array( 'type' => 'interface', 'symbol' => $parent, ); // Track 'interface ... extends' in the extension map. $xmap[$interface_name][] = $parent->getConcreteString(); } } // -( Analysis )-------------------------------------------------------------- $declared_symbols = array(); foreach ($have as $key => $spec) { $name = $spec['symbol']->getConcreteString(); $declared_symbols[$spec['type']][$name] = $spec['symbol']->getOffset(); } $required_symbols = array(); foreach ($need as $key => $spec) { $name = idx($spec, 'name'); if (!$name) { $name = $spec['symbol']->getConcreteString(); } $type = $spec['type']; foreach (explode('/', $type) as $libtype) { if (!$show_all) { if (!empty($externals[$libtype][$name])) { // Ignore symbols declared as externals. continue 2; } if (!empty($builtins[$libtype][$name])) { // Ignore symbols declared as builtins. continue 2; } } if (!empty($declared_symbols[$libtype][$name])) { // We declare this symbol, so don't treat it as a requirement. continue 2; } } if (!empty($required_symbols[$type][$name])) { // Report only the first use of a symbol, since reporting all of them // isn't terribly informative. continue; } $required_symbols[$type][$name] = $spec['symbol']->getOffset(); } +if ($show_builtins) { + foreach ($builtins as $type => $builtin_symbols) { + foreach ($builtin_symbols as $builtin_symbol => $ignored) { + $declared_symbols[$type][$builtin_symbol] = null; + } + } +} + $result = array( 'have' => $declared_symbols, 'need' => $required_symbols, 'xmap' => $xmap, ); // -( Output )---------------------------------------------------------------- if ($args->getArg('ugly')) { echo json_encode($result); } else { $json = new PhutilJSON(); echo $json->encodeFormatted($result); } // -( Library )--------------------------------------------------------------- function phutil_fail_on_unsupported_feature(XHPASTNode $node, $file, $what) { $line = $node->getLineNumber(); $message = phutil_console_wrap( pht( '`%s` has limited support for features introduced after PHP 5.2.3. '. 'This library uses an unsupported feature (%s) on line %d of %s.', 'arc liberate', $what, $line, Filesystem::readablePath($file))); $result = array( 'error' => $message, 'line' => $line, 'file' => $file, ); $json = new PhutilJSON(); echo $json->encodeFormatted($result); exit(0); } function phutil_symbols_get_builtins() { $builtin = array(); $builtin['classes'] = get_declared_classes(); $builtin['interfaces'] = get_declared_interfaces(); $funcs = get_defined_functions(); $builtin['functions'] = $funcs['internal']; $compat = json_decode( file_get_contents( dirname(__FILE__).'/../../resources/php/symbol-information.json'), true); foreach (array('functions', 'classes', 'interfaces') as $type) { // Developers may not have every extension that a library potentially uses // installed. We supplement the list of declared functions and classes with // a list of known extension functions to avoid raising false positives just // because you don't have pcntl, etc. $extensions = array_keys($compat[$type]); $builtin[$type] = array_merge($builtin[$type], $extensions); } return array( 'class' => array_fill_keys($builtin['classes'], true) + array( 'static' => true, 'parent' => true, 'self' => true, - 'PhutilBootloader' => true, - // PHP7 defines these new parent classes of "Exception", but they do not // exist prior to PHP7. It's possible to use them safely in PHP5, in // some cases, to write code which is compatible with either PHP5 or // PHP7, but it's hard for us tell if a particular use is safe or not. // For now, assume users know what they're doing and that uses are safe. // For discussion, see T12855. 'Throwable' => true, 'Error' => true, 'ParseError' => true, // PHP7 types. 'bool' => true, 'float' => true, 'int' => true, 'string' => true, 'iterable' => true, 'object' => true, 'void' => true, ), 'function' => array_filter( array( 'empty' => true, 'isset' => true, 'die' => true, - // These are provided by libphutil but not visible in the map. - - 'phutil_is_windows' => true, - 'phutil_load_library' => true, - 'phutil_is_hiphop_runtime' => true, - // HPHP/i defines these functions as 'internal', but they are NOT // builtins and do not exist in vanilla PHP. Make sure we don't mark // them as builtin since we need to add dependencies for them. 'idx' => false, 'id' => false, ) + array_fill_keys($builtin['functions'], true)), 'interface' => array_fill_keys($builtin['interfaces'], true), ); }