diff --git a/src/__phutil_library_init__.php b/src/__phutil_library_init__.php --- a/src/__phutil_library_init__.php +++ b/src/__phutil_library_init__.php @@ -26,10 +26,9 @@ $class_name, pht('class or interface'), pht( - "the class or interface '%s' is not defined in the library ". - "map for any loaded %s library.", - $class_name, - 'phutil')); + 'The class or interface "%s" is not defined in the library '. + 'map of any loaded library.', + $class_name)); } } catch (PhutilMissingSymbolException $ex) { $should_throw = true; diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -542,6 +542,7 @@ 'phutil_date_format' => 'utils/viewutils.php', 'phutil_decode_mime_header' => 'utils/utils.php', 'phutil_deprecated' => 'moduleutils/moduleutils.php', + 'phutil_describe_type' => 'utils/utils.php', 'phutil_error_listener_example' => 'error/phlog.php', 'phutil_escape_html' => 'markup/render.php', 'phutil_escape_html_newlines' => 'markup/render.php', @@ -564,6 +565,7 @@ 'phutil_implode_html' => 'markup/render.php', 'phutil_ini_decode' => 'utils/utils.php', 'phutil_is_hiphop_runtime' => 'utils/utils.php', + 'phutil_is_natural_list' => 'utils/utils.php', 'phutil_is_system_locale_available' => 'utils/utf8.php', 'phutil_is_utf8' => 'utils/utf8.php', 'phutil_is_utf8_slowly' => 'utils/utf8.php', diff --git a/src/conduit/ConduitClient.php b/src/conduit/ConduitClient.php --- a/src/conduit/ConduitClient.php +++ b/src/conduit/ConduitClient.php @@ -348,7 +348,7 @@ $out = array(); if (is_array($data)) { - if (!$data || (array_keys($data) == range(0, count($data) - 1))) { + if (phutil_is_natural_list($data)) { $out[] = 'A'; $out[] = count($data); $out[] = ':'; diff --git a/src/moduleutils/PhutilBootloader.php b/src/moduleutils/PhutilBootloader.php --- a/src/moduleutils/PhutilBootloader.php +++ b/src/moduleutils/PhutilBootloader.php @@ -62,6 +62,15 @@ $this->registeredLibraries[$name] = $path; + // If we're loading libphutil itself, load the utility functions first so + // we can safely call functions like "id()" when handling errors. In + // particular, this improves error behavior when "utils.php" itself can + // not load. + if ($name === 'phutil') { + $root = $this->getLibraryRoot('phutil'); + $this->executeInclude($root.'/utils/utils.php'); + } + // For libphutil v2 libraries, load all functions when we load the library. if (!class_exists('PhutilSymbolLoader', false)) { diff --git a/src/parser/PhutilJSON.php b/src/parser/PhutilJSON.php --- a/src/parser/PhutilJSON.php +++ b/src/parser/PhutilJSON.php @@ -118,7 +118,7 @@ */ private function encodeFormattedValue($value, $depth) { if (is_array($value)) { - if (empty($value) || array_keys($value) === range(0, count($value) - 1)) { + if (phutil_is_natural_list($value)) { return $this->encodeFormattedArray($value, $depth); } else { return $this->encodeFormattedObject($value, $depth); diff --git a/src/parser/PhutilTypeSpec.php b/src/parser/PhutilTypeSpec.php --- a/src/parser/PhutilTypeSpec.php +++ b/src/parser/PhutilTypeSpec.php @@ -93,7 +93,7 @@ if (!is_array($value)) { throw new PhutilTypeCheckException($this, $value, $name); } - if ($value && (array_keys($value) !== range(0, count($value) - 1))) { + if ($value && !phutil_is_natural_list($value)) { throw new PhutilTypeCheckException($this, $value, $name); } try { @@ -209,7 +209,7 @@ return get_class($value); } else if (is_array($value)) { $vtype = self::getTypeOfVector($value); - if ($value && (array_keys($value) === range(0, count($value) - 1))) { + if ($value && phutil_is_natural_list($value)) { return 'list<'.$vtype.'>'; } else { $ktype = self::getTypeOfVector(array_keys($value)); diff --git a/src/symbols/PhutilSymbolLoader.php b/src/symbols/PhutilSymbolLoader.php --- a/src/symbols/PhutilSymbolLoader.php +++ b/src/symbols/PhutilSymbolLoader.php @@ -386,11 +386,11 @@ $load_failed = null; if ($is_function) { if (!function_exists($name)) { - $load_failed = pht('function'); + $load_failed = 'function'; } } else { if (!class_exists($name, false) && !interface_exists($name, false)) { - $load_failed = pht('class or interface'); + $load_failed = 'class/interface'; } } @@ -400,14 +400,13 @@ $name, $load_failed, pht( - "the symbol map for library '%s' (at '%s') claims this %s is ". - "defined in '%s', but loading that source file did not cause the ". - "%s to become defined.", + 'The symbol map for library "%s" (at "%s") claims this symbol '. + '(of type "%s") is defined in "%s", but loading that source file '. + 'did not cause the symbol to become defined.', $lib_name, $lib_path, $load_failed, - $where, - $load_failed)); + $where)); } } diff --git a/src/symbols/exception/PhutilMissingSymbolException.php b/src/symbols/exception/PhutilMissingSymbolException.php --- a/src/symbols/exception/PhutilMissingSymbolException.php +++ b/src/symbols/exception/PhutilMissingSymbolException.php @@ -5,22 +5,24 @@ public function __construct($symbol, $type, $reason) { parent::__construct( pht( - "Failed to load %s '%s': %s\n\n". - "If you are not a developer, this almost always means that a library ". - "is out of date. For example, you may have upgraded `phabricator` ". - "without upgrading `libphutil`, or vice versa. It might also mean ". - "that you need to restart Apache or PHP-FPM. Make sure all libraries ". - "are up to date and all services have been restarted.\n\n". - "If you are a developer and this symbol was recently added or moved, ". - "your library map may need to be rebuilt. You can rebuild the map by ". - "running '%s'. For more information, see:\n\n". - "%s", + 'Failed to load %s "%s".'. + "\n\n". + '%s'. + "\n\n". + 'If you are not a developer, this almost always means that a library '. + 'is out of date. For example, you may have upgraded "phabricator/" '. + 'without upgrading "libphutil/", or vice versa. It might also mean '. + 'that you need to restart Apache or PHP-FPM. Make sure all libraries '. + 'are up to date and all services have been restarted.'. + "\n\n". + 'If you are a developer and this symbol was recently added or '. + 'moved, your library map may need to be rebuilt. You can rebuild '. + 'the map by running "arc liberate".'. + "\n\n". + 'For more information, see: https://phurl.io/newclasses', $type, $symbol, - $reason, - 'arc liberate', - 'https://secure.phabricator.com/book/phabcontrib/article/'. - 'adding_new_classes/')); + $reason)); } } diff --git a/src/utils/__tests__/PhutilUtilsTestCase.php b/src/utils/__tests__/PhutilUtilsTestCase.php --- a/src/utils/__tests__/PhutilUtilsTestCase.php +++ b/src/utils/__tests__/PhutilUtilsTestCase.php @@ -918,4 +918,21 @@ } } + public function testNaturalList() { + $cases = array( + array(true, array()), + array(true, array(0 => true, 1 => true, 2 => true)), + array(true, array('a', 'b', 'c')), + array(false, array(0 => true, 2 => true, 1 => true)), + array(false, array(1 => true)), + array(false, array('sound' => 'quack')), + ); + + foreach ($cases as $case) { + list($expect, $value) = $case; + $this->assertEqual($expect, phutil_is_natural_list($value)); + } + } + + } diff --git a/src/utils/utils.php b/src/utils/utils.php --- a/src/utils/utils.php +++ b/src/utils/utils.php @@ -1476,7 +1476,7 @@ } // Don't show keys for non-associative arrays. - $show_keys = (array_keys($var) !== range(0, count($var) - 1)); + $show_keys = !phutil_is_natural_list($var); $output = array(); $output[] = 'array('; @@ -1757,3 +1757,40 @@ return (string)$value; } + + +/** + * Return a short, human-readable description of an object's type. + * + * This is mostly useful for raising errors like "expected x() to return a Y, + * but it returned a Z". + * + * This is similar to "get_type()", but describes objects and arrays in more + * detail. + * + * @param wild Anything. + * @return string Human-readable description of the value's type. + */ +function phutil_describe_type($value) { + return PhutilTypeSpec::getTypeOf($value); +} + + +/** + * Test if a list has the natural numbers (1, 2, 3, and so on) as keys, in + * order. + * + * @return bool True if the list is a natural list. + */ +function phutil_is_natural_list(array $list) { + $expect = 0; + + foreach ($list as $key => $item) { + if ($key !== $expect) { + return false; + } + $expect++; + } + + return true; +}