diff --git a/.divinerconfig b/.divinerconfig index 0cb6628..bd68660 100644 --- a/.divinerconfig +++ b/.divinerconfig @@ -1,28 +1,30 @@ { "name" : "libphutil", "src_base" : "https://github.com/facebook/libphutil/blob/master", "groups" : { "overview" : "Overview", "contrib" : "Contributing to libphutil", "working" : "Working with libphutil", "util" : "Core Utilities", "library" : "Phutil Module System", "utf8" : "UTF-8", "filesystem" : "Filesystem", "exec" : "Command Execution", "futures" : "Futures", "error" : "Error Handling", "markup" : "Markup", "console" : "Console Utilities", + "aast" : "Abstract Abstract Syntax Tree", "xhpast" : "XHPAST (PHP/XHP Parser)", "conduit" : "Conduit (Service API)", + "event" : "Events", "daemon" : "Daemons", "parser" : "Other Parsers", "testcase" : "Test Cases" }, "engines" : [ ["DivinerArticleEngine", {}], ["DivinerXHPEngine", {}] ] } diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index b2fd1bc..1de57ba 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -1,232 +1,232 @@ array( 'AASTNode' => 'parser/aast/api/node', 'AASTNodeList' => 'parser/aast/api/list', 'AASTToken' => 'parser/aast/api/token', 'AASTTree' => 'parser/aast/api/tree', 'AbstractDirectedGraph' => 'utils/abstractgraph', 'AbstractDirectedGraphTestCase' => 'utils/abstractgraph/__tests__', 'BaseHTTPFuture' => 'future/http/base', 'CommandException' => 'future/exec', 'ConduitClient' => 'conduit/client', 'ConduitClientException' => 'conduit/client', 'ConduitFuture' => 'conduit/client', 'ExecFuture' => 'future/exec', 'FileFinder' => 'filesystem/filefinder', 'FileList' => 'filesystem/filelist', 'Filesystem' => 'filesystem', 'FilesystemException' => 'filesystem', 'Future' => 'future', 'FutureIterator' => 'future', 'FutureProxy' => 'future/proxy', 'HTTPFuture' => 'future/http/http', 'HTTPFutureResponseStatus' => 'future/http/status/base', 'HTTPFutureResponseStatusCURL' => 'future/http/status/curl', 'HTTPFutureResponseStatusHTTP' => 'future/http/status/http', 'HTTPFutureResponseStatusParse' => 'future/http/status/parse', 'HTTPFutureResponseStatusTransport' => 'future/http/status/transport', 'HTTPSFuture' => 'future/http/https', 'ImmediateFuture' => 'future/immediate', - 'JSONReadableSerializerTestCase' => 'parser/json/__tests__', 'LinesOfALargeFile' => 'filesystem/linesofalargefile', 'MFilterTestHelper' => 'utils/__tests__', 'PhutilConsoleFormatter' => 'console', 'PhutilConsoleStdinNotInteractiveException' => 'console/exception', 'PhutilDaemon' => 'daemon/base', 'PhutilDaemonOverseer' => 'daemon/overseer', 'PhutilDefaultSyntaxHighlighter' => 'markup/syntax/highlighter/default', 'PhutilDefaultSyntaxHighlighterEngine' => 'markup/syntax/engine/default', 'PhutilDefaultSyntaxHighlighterEnginePygmentsFuture' => 'markup/syntax/highlighter/pygments/future', 'PhutilDefaultSyntaxHighlighterEngineTestCase' => 'markup/syntax/engine/default/__tests__', 'PhutilDivinerSyntaxHighlighter' => 'markup/syntax/highlighter/diviner', 'PhutilDocblockParser' => 'parser/docblock', 'PhutilDocblockParserTestCase' => 'parser/docblock/__tests__', 'PhutilEmailAddress' => 'parser/emailaddress', 'PhutilEmailAddressTestCase' => 'parser/emailaddress/__tests__', 'PhutilErrorHandler' => 'error', 'PhutilEvent' => 'events/event', 'PhutilEventConstants' => 'events/constant/base', 'PhutilEventEngine' => 'events/engine', 'PhutilEventListener' => 'events/listener', 'PhutilEventType' => 'events/constant/type', 'PhutilExcessiveServiceCallsDaemon' => 'daemon/torture/excessiveservicecalls', 'PhutilFatalDaemon' => 'daemon/torture/fatal', 'PhutilHangForeverDaemon' => 'daemon/torture/hangforever', 'PhutilInteractiveEditor' => 'console/editor', 'PhutilJSON' => 'parser/json', + 'PhutilJSONTestCase' => 'parser/json/__tests__', 'PhutilMarkupEngine' => 'markup/engine', 'PhutilMarkupTestCase' => 'markup/__tests__', 'PhutilMissingSymbolException' => 'symbols/exception/missing', 'PhutilNiceDaemon' => 'daemon/torture/nice', 'PhutilProcessGroupDaemon' => 'daemon/torture/processgroup', 'PhutilPygmentsSyntaxHighlighter' => 'markup/syntax/highlighter/pygments', 'PhutilRainbowSyntaxHighlighter' => 'markup/syntax/highlighter/rainbow', 'PhutilReadableSerializer' => 'readableserializer', 'PhutilRemarkupBlockStorage' => 'markup/engine/remarkup/blockstorage', 'PhutilRemarkupEngine' => 'markup/engine/remarkup', 'PhutilRemarkupEngineBlockRule' => 'markup/engine/remarkup/blockrule/base', 'PhutilRemarkupEngineRemarkupCodeBlockRule' => 'markup/engine/remarkup/blockrule/remarkupcode', 'PhutilRemarkupEngineRemarkupDefaultBlockRule' => 'markup/engine/remarkup/blockrule/remarkupdefault', 'PhutilRemarkupEngineRemarkupHeaderBlockRule' => 'markup/engine/remarkup/blockrule/remarkupheader', 'PhutilRemarkupEngineRemarkupInlineBlockRule' => 'markup/engine/remarkup/blockrule/remarkupinline', 'PhutilRemarkupEngineRemarkupListBlockRule' => 'markup/engine/remarkup/blockrule/remarkuplist', 'PhutilRemarkupEngineRemarkupLiteralBlockRule' => 'markup/engine/remarkup/blockrule/remarkupliteral', 'PhutilRemarkupEngineRemarkupNoteBlockRule' => 'markup/engine/remarkup/blockrule/remarkupnote', 'PhutilRemarkupEngineRemarkupQuotesBlockRule' => 'markup/engine/remarkup/blockrule/remarkupquotes', 'PhutilRemarkupEngineTestCase' => 'markup/engine/remarkup/__tests__', 'PhutilRemarkupRule' => 'markup/engine/remarkup/markuprule/base', 'PhutilRemarkupRuleBold' => 'markup/engine/remarkup/markuprule/bold', 'PhutilRemarkupRuleEscapeHTML' => 'markup/engine/remarkup/markuprule/escapehtml', 'PhutilRemarkupRuleEscapeRemarkup' => 'markup/engine/remarkup/markuprule/escaperemarkup', 'PhutilRemarkupRuleHyperlink' => 'markup/engine/remarkup/markuprule/hyperlink', 'PhutilRemarkupRuleItalic' => 'markup/engine/remarkup/markuprule/italics', 'PhutilRemarkupRuleLinebreaks' => 'markup/engine/remarkup/markuprule/linebreaks', 'PhutilRemarkupRuleMonospace' => 'markup/engine/remarkup/markuprule/monospace', 'PhutilSaturateStdoutDaemon' => 'daemon/torture/saturatestdout', 'PhutilServiceProfiler' => 'serviceprofiler', 'PhutilSimpleOptions' => 'parser/simpleoptions', 'PhutilSimpleOptionsTestCase' => 'parser/simpleoptions/__tests__', 'PhutilSymbolLoader' => 'symbols', 'PhutilSyntaxHighlighter' => 'markup/syntax/highlighter/base', 'PhutilSyntaxHighlighterEngine' => 'markup/syntax/engine/base', 'PhutilTortureTestDaemon' => 'daemon/torture/base', 'PhutilURI' => 'parser/uri', 'PhutilURITestCase' => 'parser/uri/__tests__', 'PhutilUTF8TestCase' => 'utils/__tests__', 'PhutilUtilsTestCase' => 'utils/__tests__', 'PhutilXHPASTSyntaxHighlighter' => 'markup/syntax/highlighter/xhpast', 'PhutilXHPASTSyntaxHighlighterTestCase' => 'markup/syntax/highlighter/xhpast/__tests__', 'TempFile' => 'filesystem/tempfile', 'TestAbstractDirectedGraph' => 'utils/abstractgraph/__tests__', 'XHPASTNode' => 'parser/xhpast/api/node', 'XHPASTSyntaxErrorException' => 'parser/xhpast/api/exception', 'XHPASTToken' => 'parser/xhpast/api/token', 'XHPASTTree' => 'parser/xhpast/api/tree', 'XHPASTTreeTestCase' => 'parser/xhpast/api/tree/__tests__', ), 'function' => array( 'Futures' => 'future', 'array_mergev' => 'utils', 'array_select_keys' => 'utils', 'coalesce' => 'utils', 'csprintf' => 'xsprintf/csprintf', 'exec_manual' => 'future/exec', 'execx' => 'future/exec', 'head' => 'utils', 'id' => 'utils', 'idx' => 'utils', 'ifilter' => 'utils', 'igroup' => 'utils', 'ipull' => 'utils', 'isort' => 'utils', 'jsprintf' => 'xsprintf/jsprintf', 'last' => 'utils', 'mfilter' => 'utils', 'mgroup' => 'utils', 'mpull' => 'utils', 'msort' => 'utils', 'newv' => 'utils', 'nonempty' => 'utils', 'phlog' => 'error', 'phutil_console_confirm' => 'console', 'phutil_console_format' => 'console', 'phutil_console_prompt' => 'console', 'phutil_console_require_tty' => 'console', 'phutil_console_wrap' => 'console', 'phutil_deprecated' => 'moduleutils', 'phutil_error_listener_example' => 'error', 'phutil_escape_html' => 'markup', 'phutil_escape_uri' => 'markup', 'phutil_get_library_name_for_root' => 'moduleutils', 'phutil_get_library_root' => 'moduleutils', 'phutil_get_library_root_for_path' => 'moduleutils', 'phutil_is_utf8' => 'utils', 'phutil_passthru' => 'future/exec', 'phutil_render_tag' => 'markup', 'phutil_utf8_shorten' => 'utils', 'phutil_utf8_strlen' => 'utils', 'phutil_utf8ize' => 'utils', 'phutil_utf8v' => 'utils', 'vcsprintf' => 'xsprintf/csprintf', 'vjsprintf' => 'xsprintf/jsprintf', 'xhp_parser_node_constants' => 'parser/xhpast/constants', 'xhpast_get_binary_path' => 'parser/xhpast/bin', 'xhpast_get_build_instructions' => 'parser/xhpast/bin', 'xhpast_get_parser_future' => 'parser/xhpast/bin', 'xhpast_is_available' => 'parser/xhpast/bin', 'xhpast_parser_token_constants' => 'parser/xhpast/constants', 'xsprintf' => 'xsprintf', 'xsprintf_callback_example' => 'xsprintf', 'xsprintf_command' => 'xsprintf/csprintf', 'xsprintf_javascript' => 'xsprintf/jsprintf', ), 'requires_class' => array( 'AbstractDirectedGraphTestCase' => 'ArcanistPhutilTestCase', 'BaseHTTPFuture' => 'Future', 'ConduitFuture' => 'FutureProxy', 'ExecFuture' => 'Future', 'FutureProxy' => 'Future', 'HTTPFuture' => 'BaseHTTPFuture', 'HTTPFutureResponseStatusCURL' => 'HTTPFutureResponseStatus', 'HTTPFutureResponseStatusHTTP' => 'HTTPFutureResponseStatus', 'HTTPFutureResponseStatusParse' => 'HTTPFutureResponseStatus', 'HTTPFutureResponseStatusTransport' => 'HTTPFutureResponseStatus', 'HTTPSFuture' => 'BaseHTTPFuture', 'ImmediateFuture' => 'Future', - 'JSONReadableSerializerTestCase' => 'ArcanistPhutilTestCase', 'PhutilDefaultSyntaxHighlighterEngine' => 'PhutilSyntaxHighlighterEngine', 'PhutilDefaultSyntaxHighlighterEnginePygmentsFuture' => 'FutureProxy', 'PhutilDefaultSyntaxHighlighterEngineTestCase' => 'ArcanistPhutilTestCase', 'PhutilDocblockParserTestCase' => 'ArcanistPhutilTestCase', 'PhutilEmailAddressTestCase' => 'ArcanistPhutilTestCase', 'PhutilEventType' => 'PhutilEventConstants', 'PhutilExcessiveServiceCallsDaemon' => 'PhutilTortureTestDaemon', 'PhutilFatalDaemon' => 'PhutilTortureTestDaemon', 'PhutilHangForeverDaemon' => 'PhutilTortureTestDaemon', + 'PhutilJSONTestCase' => 'ArcanistPhutilTestCase', 'PhutilMarkupTestCase' => 'ArcanistPhutilTestCase', 'PhutilNiceDaemon' => 'PhutilTortureTestDaemon', 'PhutilProcessGroupDaemon' => 'PhutilTortureTestDaemon', 'PhutilRemarkupEngine' => 'PhutilMarkupEngine', 'PhutilRemarkupEngineRemarkupCodeBlockRule' => 'PhutilRemarkupEngineBlockRule', 'PhutilRemarkupEngineRemarkupDefaultBlockRule' => 'PhutilRemarkupEngineBlockRule', 'PhutilRemarkupEngineRemarkupHeaderBlockRule' => 'PhutilRemarkupEngineBlockRule', 'PhutilRemarkupEngineRemarkupInlineBlockRule' => 'PhutilRemarkupEngineBlockRule', 'PhutilRemarkupEngineRemarkupListBlockRule' => 'PhutilRemarkupEngineBlockRule', 'PhutilRemarkupEngineRemarkupLiteralBlockRule' => 'PhutilRemarkupEngineBlockRule', 'PhutilRemarkupEngineRemarkupNoteBlockRule' => 'PhutilRemarkupEngineBlockRule', 'PhutilRemarkupEngineRemarkupQuotesBlockRule' => 'PhutilRemarkupEngineBlockRule', 'PhutilRemarkupEngineTestCase' => 'ArcanistPhutilTestCase', 'PhutilRemarkupRuleBold' => 'PhutilRemarkupRule', 'PhutilRemarkupRuleEscapeHTML' => 'PhutilRemarkupRule', 'PhutilRemarkupRuleEscapeRemarkup' => 'PhutilRemarkupRule', 'PhutilRemarkupRuleHyperlink' => 'PhutilRemarkupRule', 'PhutilRemarkupRuleItalic' => 'PhutilRemarkupRule', 'PhutilRemarkupRuleLinebreaks' => 'PhutilRemarkupRule', 'PhutilRemarkupRuleMonospace' => 'PhutilRemarkupRule', 'PhutilSaturateStdoutDaemon' => 'PhutilTortureTestDaemon', 'PhutilSimpleOptionsTestCase' => 'ArcanistPhutilTestCase', 'PhutilTortureTestDaemon' => 'PhutilDaemon', 'PhutilURITestCase' => 'ArcanistPhutilTestCase', 'PhutilUTF8TestCase' => 'ArcanistPhutilTestCase', 'PhutilUtilsTestCase' => 'ArcanistPhutilTestCase', 'PhutilXHPASTSyntaxHighlighterTestCase' => 'ArcanistPhutilTestCase', 'TestAbstractDirectedGraph' => 'AbstractDirectedGraph', 'XHPASTNode' => 'AASTNode', 'XHPASTToken' => 'AASTToken', 'XHPASTTree' => 'AASTTree', 'XHPASTTreeTestCase' => 'ArcanistPhutilTestCase', ), 'requires_interface' => array( ), )); diff --git a/src/events/constant/base/PhutilEventConstants.php b/src/events/constant/base/PhutilEventConstants.php index 1415355..5e8921f 100644 --- a/src/events/constant/base/PhutilEventConstants.php +++ b/src/events/constant/base/PhutilEventConstants.php @@ -1,21 +1,24 @@ } public static function getInstance() { if (!self::$instance) { self::$instance = new PhutilEventEngine(); } return self::$instance; } public function addListener(PhutilEventListener $listener, $type) { $this->listeners[$type][] = $listener; return $this; } /** * Get all the objects currently listening to any event. */ public function getAllListeners() { $listeners = array_mergev($this->listeners); $listeners = mpull($listeners, null, 'getListenerID'); return $listeners; } public static function dispatchEvent(PhutilEvent $event) { $instance = self::getInstance(); $listeners = idx($instance->listeners, $event->getType(), array()); $global_listeners = idx( $instance->listeners, PhutilEventType::TYPE_ALL, array()); // Merge and deduplicate listeners (we want to send the event to each // listener only once, even if it satisfies multiple criteria for the // event). $listeners = array_merge($listeners, $global_listeners); $listeners = mpull($listeners, null, 'getListenerID'); foreach ($listeners as $listener) { if ($event->isStopped()) { // Do this first so if someone tries to dispatch a stopped event it // doesn't go anywhere. Silly but less surprising. break; } $listener->handleEvent($event); } } } diff --git a/src/events/event/PhutilEvent.php b/src/events/event/PhutilEvent.php index ba8f357..e483bd9 100644 --- a/src/events/event/PhutilEvent.php +++ b/src/events/event/PhutilEvent.php @@ -1,57 +1,60 @@ type = $type; $this->data = $data; } public function getType() { return $this->type; } public function getValue($key, $default = null) { return idx($this->data, $key, $default); } public function setValue($key, $value) { $this->data[$key] = $value; return $this; } public function stop() { $this->stop = true; return $this; } public function isStopped() { return $this->stop; } } diff --git a/src/events/listener/PhutilEventListener.php b/src/events/listener/PhutilEventListener.php index 8940970..6197af6 100644 --- a/src/events/listener/PhutilEventListener.php +++ b/src/events/listener/PhutilEventListener.php @@ -1,53 +1,56 @@ } abstract public function register(); abstract public function handleEvent(PhutilEvent $event); final public function listen($type) { $engine = PhutilEventEngine::getInstance(); $engine->addListener($this, $type); } /** * Return a scalar ID unique to this listener. This is used to deduplicate * listeners which match events on multiple rules, so they are invoked only * once. * * @return int A scalar unique to this object instance. */ final public function getListenerID() { if (!$this->listenerID) { $this->listenerID = self::$nextListenerID; self::$nextListenerID++; } return $this->listenerID; } } diff --git a/src/future/http/status/base/HTTPFutureResponseStatus.php b/src/future/http/status/base/HTTPFutureResponseStatus.php index ab19851..f27e986 100644 --- a/src/future/http/status/base/HTTPFutureResponseStatus.php +++ b/src/future/http/status/base/HTTPFutureResponseStatus.php @@ -1,46 +1,49 @@ statusCode = $status_code; $type = $this->getErrorCodeType($status_code); $description = $this->getErrorCodeDescription($status_code); $message = rtrim("[{$type}/{$status_code}] {$description}"); parent::__construct($message); } final public function getStatusCode() { return $this->statusCode; } abstract public function isError(); abstract public function isTimeout(); abstract protected function getErrorCodeType($code); abstract protected function getErrorCodeDescription($code); } diff --git a/src/future/http/status/curl/HTTPFutureResponseStatusCURL.php b/src/future/http/status/curl/HTTPFutureResponseStatusCURL.php index 93fa79a..9a79c7f 100644 --- a/src/future/http/status/curl/HTTPFutureResponseStatusCURL.php +++ b/src/future/http/status/curl/HTTPFutureResponseStatusCURL.php @@ -1,65 +1,68 @@ getStatusCode() == CURLE_OPERATION_TIMEOUTED); } protected function getErrorCodeDescription($code) { $constants = get_defined_constants(); $constant_name = null; foreach ($constants as $constant => $value) { if ($value == $code && preg_match('/^CURLE_/', $constant)) { $constant_name = '<'.$constant.'> '; break; } } $map = array( CURLE_SSL_CONNECT_ERROR => 'There was an error negotiating the SSL connection. This usually '. 'indicates that the remote host has a bad SSL certificate, or your '. 'local host has some sort of SSL misconfiguration which prevents it '. 'from accepting the CA.', CURLE_OPERATION_TIMEOUTED => 'The request took too long to complete.', ); return $constant_name.idx($map, $code); } } diff --git a/src/future/http/status/http/HTTPFutureResponseStatusHTTP.php b/src/future/http/status/http/HTTPFutureResponseStatusHTTP.php index e2240e8..1b6a6cb 100644 --- a/src/future/http/status/http/HTTPFutureResponseStatusHTTP.php +++ b/src/future/http/status/http/HTTPFutureResponseStatusHTTP.php @@ -1,41 +1,44 @@ getStatusCode() < 200) || ($this->getStatusCode() > 299); } public function isTimeout() { return false; } protected function getErrorCodeDescription($code) { static $map = array( 404 => 'Not Found', ); return idx($map, $code); } } diff --git a/src/future/http/status/parse/HTTPFutureResponseStatusParse.php b/src/future/http/status/parse/HTTPFutureResponseStatusParse.php index cf1237a..64d4558 100644 --- a/src/future/http/status/parse/HTTPFutureResponseStatusParse.php +++ b/src/future/http/status/parse/HTTPFutureResponseStatusParse.php @@ -1,47 +1,50 @@ rawResponse = $raw_response; parent::__construct($code); } protected function getErrorCodeType($code) { return 'Parse'; } public function isError() { return true; } public function isTimeout() { return false; } protected function getErrorCodeDescription($code) { return "The remote host returned something other than an HTTP response: ". $this->rawResponse; } } diff --git a/src/future/http/status/transport/HTTPFutureResponseStatusTransport.php b/src/future/http/status/transport/HTTPFutureResponseStatusTransport.php index 3702f0f..746b202 100644 --- a/src/future/http/status/transport/HTTPFutureResponseStatusTransport.php +++ b/src/future/http/status/transport/HTTPFutureResponseStatusTransport.php @@ -1,59 +1,62 @@ getStatusCode() == self::ERROR_TIMEOUT); } protected function getErrorCodeDescription($code) { $map = array( self::ERROR_TIMEOUT => 'The request took too long to complete.', self::ERROR_CONNECTION_ABORTED => 'The remote host closed the connection before the request completed.', self::ERROR_CONNECTION_REFUSED => 'The remote host refused the connection. This usually means the '. 'host is not running an HTTP server, or the network is blocking '. 'connections from this machine. Verify you can connect to the '. 'remote host from this host.', self::ERROR_CONNECTION_FAILED => 'Connection could not be initiated. This usually indicates a DNS '. 'problem: verify the domain name is correct, that you can '. 'perform a DNS lookup for it from this machine. (Did you add the '. 'domain to `/etc/hosts` on some other machine, but not this one?)', ); return idx($map, $code); } } diff --git a/src/markup/syntax/highlighter/xhpast/__tests__/PhutilXHPASTSyntaxHighlighterTestCase.php b/src/markup/syntax/highlighter/xhpast/__tests__/PhutilXHPASTSyntaxHighlighterTestCase.php index 0855dab..7fdd407 100644 --- a/src/markup/syntax/highlighter/xhpast/__tests__/PhutilXHPASTSyntaxHighlighterTestCase.php +++ b/src/markup/syntax/highlighter/xhpast/__tests__/PhutilXHPASTSyntaxHighlighterTestCase.php @@ -1,39 +1,43 @@ getHighlightFuture($source); return $future->resolve(); } private function read($file) { $path = dirname(__FILE__).'/data/'.$file; return Filesystem::readFile($path); } public function testBuiltinClassnames() { $this->assertEqual( $this->read('builtin-classname.expect'), $this->highlight($this->read('builtin-classname.source')), 'Builtin classnames should not be marked as linkable symbols.'); } } diff --git a/src/parser/json/PhutilJSON.php b/src/parser/json/PhutilJSON.php index 487a92b..05d5fb9 100644 --- a/src/parser/json/PhutilJSON.php +++ b/src/parser/json/PhutilJSON.php @@ -1,106 +1,154 @@ encodeFormattedObject($object, 0)."\n"; } + +/* -( Internals )---------------------------------------------------------- */ + + + /** + * Pretty-print a JSON object. + * + * @param dict Object to format. + * @param int Current depth, for indentation. + * @return string Pretty-printed value. + * @task internal + */ private function encodeFormattedObject($object, $depth) { if (empty($object)) { return '{}'; } $pre = $this->getIndent($depth); $key_pre = $this->getIndent($depth + 1); $keys = array(); $vals = array(); $max = 0; foreach ($object as $key => $val) { $ekey = $this->encodeFormattedValue((string)$key, 0); $max = max($max, strlen($ekey)); $keys[] = $ekey; $vals[] = $this->encodeFormattedValue($val, $depth + 1); } $key_lines = array(); foreach ($keys as $k => $key) { $key_lines[] = $key_pre.str_pad($key, $max).' : '.$vals[$k]; } $key_lines = implode(",\n", $key_lines); $out = "{\n"; $out .= $key_lines; $out .= "\n"; $out .= $pre.'}'; return $out; } + + /** + * Pretty-print a JSON list. + * + * @param list List to format. + * @param int Current depth, for indentation. + * @return string Pretty-printed value. + * @task internal + */ private function encodeFormattedArray($array, $depth) { if (empty($array)) { return '[]'; } $pre = $this->getIndent($depth); $val_pre = $this->getIndent($depth + 1); $vals = array(); foreach ($array as $val) { $vals[] = $val_pre.$this->encodeFormattedValue($val, $depth + 1); } $val_lines = implode(",\n", $vals); $out = "[\n"; $out .= $val_lines; $out .= "\n"; $out .= $pre.']'; return $out; } + + /** + * Pretty-print a JSON value. + * + * @param dict Value to format. + * @param int Current depth, for indentation. + * @return string Pretty-printed value. + * @task internal + */ private function encodeFormattedValue($value, $depth) { if (is_array($value)) { if (empty($value) || array_keys($value) === range(0, count($value) - 1)) { return $this->encodeFormattedArray($value, $depth); } else { return $this->encodeFormattedObject($value, $depth); } } else { return json_encode($value); } } + + /** + * Render a string corresponding to the current indent depth. + * + * @param int Current depth. + * @return string Indentation. + * @task internal + */ private function getIndent($depth) { if (!$depth) { return ''; } else { return str_repeat(' ', $depth); } } + } diff --git a/src/parser/json/__tests__/JSONReadableSerializerTestCase.php b/src/parser/json/__tests__/PhutilJSONTestCase.php similarity index 87% rename from src/parser/json/__tests__/JSONReadableSerializerTestCase.php rename to src/parser/json/__tests__/PhutilJSONTestCase.php index 25d4e10..d4907a9 100644 --- a/src/parser/json/__tests__/JSONReadableSerializerTestCase.php +++ b/src/parser/json/__tests__/PhutilJSONTestCase.php @@ -1,37 +1,40 @@ assertEqual( $expect, $serializer->encodeFormatted(array('x' => array())), 'Empty arrays should serialize as [], not {}.'); } } diff --git a/src/parser/json/__tests__/__init__.php b/src/parser/json/__tests__/__init__.php index 043158d..b3c9c3d 100644 --- a/src/parser/json/__tests__/__init__.php +++ b/src/parser/json/__tests__/__init__.php @@ -1,14 +1,14 @@ '4', * 'eyes' => '2', * ); * * @param string Input option list. * @return dict Parsed dictionary. * @task parse */ public static function parse($input) { $result = array(); $vars = explode(',', $input); foreach ($vars as $var) { if (strpos($var, '=') !== false) { list($key, $value) = explode('=', $var, 2); $value = trim($value); } else { list($key, $value) = array($var, true); } $key = trim($key); $key = strtolower($key); if (!self::isValidKey($key)) { continue; } if (!strlen($value)) { unset($result[$key]); continue; } $result[$key] = $value; } return $result; } /* -( Unparsing Simple Options )------------------------------------------- */ /** * Convert a dictionary into a simple option list. For example: * * array( * 'legs' => '4', * 'eyes' => '2', * ); * * ...becomes: * * legs=4, eyes=2 * * @param dict Input dictionary. * @return string Unparsed option list. */ public static function unparse(array $options) { $result = array(); foreach ($options as $name => $value) { if (!self::isValidKey($name)) { throw new Exception( "SimpleOptions: keys must contain only lowercase letters."); } if (!strlen($value)) { continue; } if ($value === true) { $result[] = $name; } else { $result[] = $name.'='.$value; } } return implode(', ', $result); } /* -( Internals )---------------------------------------------------------- */ private static function isValidKey($key) { return (bool)preg_match('/^[a-z]+$/', $key); } } diff --git a/src/parser/simpleoptions/__tests__/PhutilSimpleOptionsTestCase.php b/src/parser/simpleoptions/__tests__/PhutilSimpleOptionsTestCase.php index 6382eeb..2509a43 100644 --- a/src/parser/simpleoptions/__tests__/PhutilSimpleOptionsTestCase.php +++ b/src/parser/simpleoptions/__tests__/PhutilSimpleOptionsTestCase.php @@ -1,96 +1,99 @@ array(), // Basic parsing. 'legs=4' => array('legs' => '4'), 'legs=4,eyes=2' => array('legs' => '4', 'eyes' => '2'), // Repeated keys mean last specification wins. 'legs=4,legs=3' => array('legs' => '3'), // Keys with no value should map to true. 'flag' => array('flag' => true), 'legs=4,flag' => array('legs' => '4', 'flag' => true), // Spaces should be ignored. ' flag ' => array('flag' => true), ' legs = 4 , eyes = 2' => array('legs' => '4', 'eyes' => '2'), // Case should be ignored. 'LEGS=4' => array('legs' => '4'), 'legs=4, LEGS=4' => array('legs' => '4'), // Empty values should be absent. 'legs=' => array(), 'legs=4,legs=,eyes=2' => array('eyes' => '2'), ); foreach ($map as $string => $expect) { $this->assertEqual( $expect, PhutilSimpleOptions::parse($string), "Correct parse of '{$string}'"); } } public function testSimpleOptionsUnparse() { $map = array( '' => array(), 'legs=4' => array('legs' => '4'), 'legs=4, eyes=2' => array('legs' => '4', 'eyes' => '2'), 'eyes=2, legs=4' => array('eyes' => '2', 'legs' => '4'), 'legs=4, head' => array('legs' => '4', 'head' => true), 'eyes=2' => array('legs' => '', 'eyes' => '2'), ); foreach ($map as $expect => $dict) { $this->assertEqual( $expect, PhutilSimpleOptions::unparse($dict), "Correct unparse of ".print_r($dict, true)); } $bogus = array( array('LEGS' => true), array('LEGS' => 4), array('!' => '!'), array('' => '2'), ); foreach ($bogus as $bad_input) { $caught = null; try { PhutilSimpleOptions::unparse($bad_input); } catch (Exception $ex) { $caught = $ex; } $this->assertEqual( true, $caught instanceof Exception, "Correct throw on unparse of '{$bad_input}'"); } } } diff --git a/src/serviceprofiler/PhutilServiceProfiler.php b/src/serviceprofiler/PhutilServiceProfiler.php index 11937cc..b16fd8b 100644 --- a/src/serviceprofiler/PhutilServiceProfiler.php +++ b/src/serviceprofiler/PhutilServiceProfiler.php @@ -1,124 +1,125 @@ discardMode = true; } public static function getInstance() { if (empty(self::$instance)) { self::$instance = new PhutilServiceProfiler(); } return self::$instance; } public function beginServiceCall(array $data) { $data['begin'] = microtime(true); $id = $this->logSize++; $this->events[$id] = $data; foreach ($this->listeners as $listener) { call_user_func($listener, 'begin', $id, $data); } return $id; } public function endServiceCall($call_id, array $data) { $data = ($this->events[$call_id] + $data); $data['end'] = microtime(true); $data['duration'] = ($data['end'] - $data['begin']); $this->events[$call_id] = $data; foreach ($this->listeners as $listener) { call_user_func($listener, 'end', $call_id, $data); } if ($this->discardMode) { unset($this->events[$call_id]); } } public function getServiceCallLog() { return $this->events; } public function addListener($callback) { $this->listeners[] = $callback; } public static function installEchoListener() { $instance = PhutilServiceProfiler::getInstance(); $instance->addListener(array('PhutilServiceProfiler', 'echoListener')); } public static function echoListener($type, $id, $data) { $is_begin = false; $is_end = false; switch ($type) { case 'begin': $is_begin = true; echo '>>> '; break; case 'end': $is_end = true; echo '<<< '; break; default: echo '??? '; break; } echo '['.$id.'] '; $type = idx($data, 'type', 'mystery'); echo '<'.$type.'> '; if ($is_begin) { switch ($type) { case 'query': echo substr($data['query'], 0, 512); break; case 'exec': echo '$ '.$data['command']; break; case 'conduit': echo $data['method'].'()'; break; } } if ($is_end) { echo number_format((int)(1000000 * $data['duration'])).' us'; } echo "\n"; } } diff --git a/src/utils/__tests__/MFilterTestHelper.php b/src/utils/__tests__/MFilterTestHelper.php index a6caf46..9574e26 100644 --- a/src/utils/__tests__/MFilterTestHelper.php +++ b/src/utils/__tests__/MFilterTestHelper.php @@ -1,43 +1,46 @@ h = $h_value; $this->i = $i_value; $this->j = $j_value; } public function getH() { return $this->h; } public function getI() { return $this->i; } public function getJ() { return $this->j; } } diff --git a/src/utils/abstractgraph/AbstractDirectedGraph.php b/src/utils/abstractgraph/AbstractDirectedGraph.php index 70308e1..d32d1d0 100644 --- a/src/utils/abstractgraph/AbstractDirectedGraph.php +++ b/src/utils/abstractgraph/AbstractDirectedGraph.php @@ -1,218 +1,220 @@ addNodes( * array( * $object->getPHID() => $object->getChildPHIDs(), * )); * $detector->loadGraph(); * * Now you can query the graph, e.g. by detecting cycles: * * $cycle = $detector->detectCycles($object->getPHID()); * * If ##$cycle## is empty, no graph cycle is reachable from the node. If it * is nonempty, it contains a list of nodes which form a graph cycle. * * NOTE: Nodes must be represented with scalars. * * @task build Graph Construction * @task cycle Cycle Detection * @task explore Graph Exploration + * + * @group util */ abstract class AbstractDirectedGraph { private $knownNodes = array(); private $graphLoaded = false; /* -( Graph Construction )------------------------------------------------- */ /** * Load the edges for a list of nodes. You must override this method. You * will be passed a list of nodes, and should return a dictionary mapping * each node to the list of nodes that can be reached by following its the * edges which originate at it: for example, the child nodes of an object * which has a parent-child relationship to other objects. * * The intent of this method is to allow you to issue a single query per * graph level for graphs which are stored as edge tables in the database. * Generally, you will load all the objects which correspond to the list of * nodes, and then return a map from each of their IDs to all their children. * * NOTE: You must return an entry for every node you are passed, even if it * is invalid or can not be loaded. Either return an empty array (if this is * acceptable for your application) or throw an exception if you can't satisfy * this requirement. * * @param list A list of nodes. * @return dict A map of nodes to the nodes reachable along their edges. * There must be an entry for each node you were provided. * @task build */ abstract protected function loadEdges(array $nodes); /** * Seed the graph with known nodes. Often, you will provide the candidate * edges that a user is trying to create here, or the initial set of edges * you know about. * * @param dict A map of nodes to the nodes reachable along their edges. * @return this * @task build */ final public function addNodes(array $nodes) { if ($this->graphLoaded) { throw new Exception( "Call addNodes() before calling loadGraph(). You can not add more ". "nodes once you have loaded the graph."); } $this->knownNodes += $nodes; return $this; } /** * Load the graph, building it out so operations can be performed on it. This * constructs the graph level-by-level, calling @{method:loadEdges} to * expand the graph at each stage until it is complete. * * @return this * @task build */ final public function loadGraph() { $new_nodes = $this->knownNodes; while (true) { $load = array(); foreach ($new_nodes as $node => $edges) { foreach ($edges as $edge) { if (!isset($this->knownNodes[$edge])) { $load[$edge] = true; } } } if (empty($load)) { break; } $load = array_keys($load); $new_nodes = $this->loadEdges($load); foreach ($load as $node) { if (!isset($new_nodes[$node]) || !is_array($new_nodes[$node])) { throw new Exception( "loadEdges() must return an edge list array for each provided ". "node, or the cycle detection algorithm may not terminate."); } } $this->addNodes($new_nodes); } $this->graphLoaded = true; return $this; } /* -( Cycle Detection )---------------------------------------------------- */ /** * Detect if there are any cycles reachable from a given node. * * If cycles are reachable, it returns a list of nodes which create a cycle. * Note that this list may include nodes which aren't actually part of the * cycle, but lie on the graph between the specified node and the cycle. * For example, it might return something like this (when passed "A"): * * A, B, C, D, E, C * * This means you can walk from A to B to C to D to E and then back to C, * which forms a cycle. A and B are included even though they are not part * of the cycle. When presenting information about graph cycles to users, * including these nodes is generally useful. This also shouldn't ever happen * if you've vetted prior edges before writing them, because it means there * is a preexisting cycle in the graph. * * NOTE: This only detects cycles reachable from a node. It does not detect * cycles in the entire graph. * * @param scalar The node to walk from, looking for graph cycles. * @return list|null Returns null if no cycles are reachable from the node, * or a list of nodes that form a cycle. * @task cycle */ final public function detectCycles($node) { if (!$this->graphLoaded) { throw new Exception( "Call loadGraph() to build the graph out before calling ". "detectCycles()."); } if (!isset($this->knownNodes[$node])) { throw new Exception( "The node '{$node}' is not known. Call addNodes() to seed the graph ". "with nodes."); } $visited = array(); return $this->performCycleDetection($node, $visited); } /** * Internal cycle detection implementation. Recursively walks the graph, * keeping track of where it's been, and returns the first cycle it finds. * * @param scalar The node to walk from. * @param list Previously visited nodes. * @return null|list Null if no cycles are found, or a list of nodes * which cycle. * @task cycle */ final private function performCycleDetection($node, array $visited) { $visited[$node] = true; foreach ($this->knownNodes[$node] as $edge) { if (isset($visited[$edge])) { $result = array_keys($visited); $result[] = $edge; return $result; } $result = $this->performCycleDetection($edge, $visited); if ($result) { return $result; } } return null; } } diff --git a/src/utils/abstractgraph/__tests__/AbstractDirectedGraphTestCase.php b/src/utils/abstractgraph/__tests__/AbstractDirectedGraphTestCase.php index 53a4d69..f69bac7 100644 --- a/src/utils/abstractgraph/__tests__/AbstractDirectedGraphTestCase.php +++ b/src/utils/abstractgraph/__tests__/AbstractDirectedGraphTestCase.php @@ -1,104 +1,107 @@ array(), ); $cycle = $this->findGraphCycle($graph); $this->assertEqual(null, $cycle, 'Trivial Graph'); } public function testNoncyclicGraph() { $graph = array( 'A' => array('B', 'C'), 'B' => array('D'), 'C' => array(), 'D' => array(), ); $cycle = $this->findGraphCycle($graph); $this->assertEqual(null, $cycle, 'Noncyclic Graph'); } public function testTrivialCyclicGraph() { $graph = array( 'A' => array('A'), ); $cycle = $this->findGraphCycle($graph); $this->assertEqual(array('A', 'A'), $cycle, 'Trivial Cycle'); } public function testCyclicGraph() { $graph = array( 'A' => array('B', 'C'), 'B' => array('D'), 'C' => array('E', 'F'), 'D' => array(), 'E' => array(), 'F' => array('G', 'C'), 'G' => array(), ); $cycle = $this->findGraphCycle($graph); $this->assertEqual(array('A', 'C', 'F', 'C'), $cycle, 'Cyclic Graph'); } public function testNonTreeGraph() { // This graph is non-cyclic, but C is both a child and a grandchild of A. // This is permitted. $graph = array( 'A' => array('B', 'C'), 'B' => array('C'), 'C' => array(), ); $cycle = $this->findGraphCycle($graph); $this->assertEqual(null, $cycle, 'NonTreeGraph'); } public function testEdgeLoadFailure() { $graph = array( 'A' => array('B'), ); $raised = null; try { $this->findGraphCycle($graph); } catch (Exception $ex) { $raised = $ex; } $this->assertEqual( true, (bool)$raised, 'Exception raised by unloadable edges.'); } private function findGraphCycle(array $graph, $seed = 'A', $search = 'A') { $detector = new TestAbstractDirectedGraph(); $detector->setTestData($graph); $detector->addNodes(array_select_keys($graph, array($seed))); $detector->loadGraph(); return $detector->detectCycles($search); } } diff --git a/src/utils/abstractgraph/__tests__/TestAbstractDirectedGraph.php b/src/utils/abstractgraph/__tests__/TestAbstractDirectedGraph.php index 1772570..5f89be3 100644 --- a/src/utils/abstractgraph/__tests__/TestAbstractDirectedGraph.php +++ b/src/utils/abstractgraph/__tests__/TestAbstractDirectedGraph.php @@ -1,35 +1,35 @@ nodes = $nodes; return $this; } protected function loadEdges(array $nodes) { return array_select_keys($this->nodes, $nodes); } } diff --git a/src/utils/utf8.php b/src/utils/utf8.php index d808945..b83ee58 100644 --- a/src/utils/utf8.php +++ b/src/utils/utf8.php @@ -1,255 +1,257 @@ $len) { throw new Exception("Invalid UTF-8 string passed to phutil_utf8v()."); } for ($jj = 1; $jj < $seq_len; ++$jj) { if ($string[$ii + $jj] >= "\xC0") { throw new Exception("Invalid UTF-8 string passed to phutil_utf8v()."); } } $res[] = substr($string, $ii, $seq_len); $ii += $seq_len; } return $res; } /** * Shorten a string to provide a summary, respecting UTF-8 characters. This * function attempts to truncate strings at word boundaries. * * NOTE: This function makes a best effort to apply some reasonable rules but * will not work well for the full range of unicode languages. For instance, * no effort is made to deal with combining characters. * * @param string UTF-8 string to shorten. * @param int Maximum length of the result. * @param string If the string is shortened, add this at the end. Defaults to * horizontal ellipsis. * @return string A string with no more than the specified character length. + * + * @group utf8 */ function phutil_utf8_shorten($string, $length, $terminal = "\xE2\x80\xA6") { $terminal_len = count(phutil_utf8v($terminal)); if ($terminal_len >= $length) { // If you provide a terminal we still enforce that the result (including // the terminal) is no longer than $length, but we can't do that if the // terminal is too long. throw new Exception( "String terminal length must be less than string length!"); } $string_v = phutil_utf8v($string); $string_len = count($string_v); if ($string_len <= $length) { // If the string is already shorter than the requested length, simply return // it unmodified. return $string; } // NOTE: This is not complete, and there are many other word boundary // characters and reasonable places to break words in the UTF-8 character // space. For now, this gives us reasonable behavior for latin langauges. We // don't necessarily have access to PCRE+Unicode so there isn't a great way // for us to look up character attributes. // If we encounter these, prefer to break on them instead of cutting the // string off in the middle of a word. static $break_characters = array( ' ' => true, "\n" => true, ';' => true, ':' => true, '[' => true, '(' => true, ',' => true, '-' => true, ); // If we encounter these, shorten to this character exactly without appending // the terminal. static $stop_characters = array( '.' => true, '!' => true, '?' => true, ); // Search backward in the string, looking for reasonable places to break it. $word_boundary = null; $stop_boundary = null; // If we do a word break with a terminal, we have to look beyond at least the // number of characters in the terminal. $terminal_area = $length - $terminal_len; for ($ii = $length; $ii >= 0; $ii--) { $c = $string_v[$ii]; if (isset($break_characters[$c]) && ($ii <= $terminal_area)) { $word_boundary = $ii; } else if (isset($stop_characters[$c]) && ($ii < $length)) { $stop_boundary = $ii + 1; break; } else { if ($word_boundary !== null) { break; } } } if ($stop_boundary !== null) { // We found a character like ".". Cut the string there, without appending // the terminal. $string_part = array_slice($string_v, 0, $stop_boundary); return implode('', $string_part); } // If we didn't find any boundary characters or we found ONLY boundary // characters, just break at the maximum character length. if ($word_boundary === null || $word_boundary === 0) { $word_boundary = $length - $terminal_len; } $string_part = array_slice($string_v, 0, $word_boundary); $string_part = implode('', $string_part); return $string_part.$terminal; }