diff --git a/src/__phutil_library_init__.php b/src/__phutil_library_init__.php index ee442e5..0c43b39 100644 --- a/src/__phutil_library_init__.php +++ b/src/__phutil_library_init__.php @@ -1,70 +1,69 @@ setType('class') ->setName($class_name) ->selectAndLoadSymbols(); if (!$symbols) { throw new PhutilMissingSymbolException( $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; foreach (debug_backtrace() as $backtrace) { if (empty($backtrace['function'])) { continue; } switch ($backtrace['function']) { case 'class_exists': case 'interface_exists': case 'method_exists': case 'property_exists': case 'trait_exists': $should_throw = false; break; } } if (!$should_throw) { return false; } // If there are other SPL autoloaders installed, we need to give them a // chance to load the class. Throw the exception if we're the last // autoloader; if not, swallow it and let them take a shot. $autoloaders = spl_autoload_functions(); $last = end($autoloaders); if ($last == __FUNCTION__) { throw $ex; } } } spl_autoload_register('__phutil_autoload', $throw = true); phutil_register_library('phutil', __FILE__); diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index 525c5ac..a84892c 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -1,1154 +1,1156 @@ 2, 'class' => array( 'AASTNode' => 'parser/aast/api/AASTNode.php', 'AASTNodeList' => 'parser/aast/api/AASTNodeList.php', 'AASTToken' => 'parser/aast/api/AASTToken.php', 'AASTTree' => 'parser/aast/api/AASTTree.php', 'AbstractDirectedGraph' => 'utils/AbstractDirectedGraph.php', 'AbstractDirectedGraphTestCase' => 'utils/__tests__/AbstractDirectedGraphTestCase.php', 'AphrontAccessDeniedQueryException' => 'aphront/storage/exception/AphrontAccessDeniedQueryException.php', 'AphrontBaseMySQLDatabaseConnection' => 'aphront/storage/connection/mysql/AphrontBaseMySQLDatabaseConnection.php', 'AphrontCharacterSetQueryException' => 'aphront/storage/exception/AphrontCharacterSetQueryException.php', 'AphrontConnectionLostQueryException' => 'aphront/storage/exception/AphrontConnectionLostQueryException.php', 'AphrontConnectionQueryException' => 'aphront/storage/exception/AphrontConnectionQueryException.php', 'AphrontCountQueryException' => 'aphront/storage/exception/AphrontCountQueryException.php', 'AphrontDatabaseConnection' => 'aphront/storage/connection/AphrontDatabaseConnection.php', 'AphrontDatabaseTableRef' => 'xsprintf/AphrontDatabaseTableRef.php', 'AphrontDatabaseTableRefInterface' => 'xsprintf/AphrontDatabaseTableRefInterface.php', 'AphrontDatabaseTransactionState' => 'aphront/storage/connection/AphrontDatabaseTransactionState.php', 'AphrontDeadlockQueryException' => 'aphront/storage/exception/AphrontDeadlockQueryException.php', 'AphrontDuplicateKeyQueryException' => 'aphront/storage/exception/AphrontDuplicateKeyQueryException.php', 'AphrontHTTPHeaderParser' => 'aphront/headerparser/AphrontHTTPHeaderParser.php', 'AphrontHTTPHeaderParserTestCase' => 'aphront/headerparser/__tests__/AphrontHTTPHeaderParserTestCase.php', 'AphrontInvalidCredentialsQueryException' => 'aphront/storage/exception/AphrontInvalidCredentialsQueryException.php', 'AphrontIsolatedDatabaseConnection' => 'aphront/storage/connection/AphrontIsolatedDatabaseConnection.php', 'AphrontLockTimeoutQueryException' => 'aphront/storage/exception/AphrontLockTimeoutQueryException.php', 'AphrontMultipartParser' => 'aphront/multipartparser/AphrontMultipartParser.php', 'AphrontMultipartParserTestCase' => 'aphront/multipartparser/__tests__/AphrontMultipartParserTestCase.php', 'AphrontMultipartPart' => 'aphront/multipartparser/AphrontMultipartPart.php', 'AphrontMySQLDatabaseConnection' => 'aphront/storage/connection/mysql/AphrontMySQLDatabaseConnection.php', 'AphrontMySQLiDatabaseConnection' => 'aphront/storage/connection/mysql/AphrontMySQLiDatabaseConnection.php', 'AphrontNotSupportedQueryException' => 'aphront/storage/exception/AphrontNotSupportedQueryException.php', 'AphrontObjectMissingQueryException' => 'aphront/storage/exception/AphrontObjectMissingQueryException.php', 'AphrontParameterQueryException' => 'aphront/storage/exception/AphrontParameterQueryException.php', 'AphrontQueryException' => 'aphront/storage/exception/AphrontQueryException.php', 'AphrontQueryTimeoutQueryException' => 'aphront/storage/exception/AphrontQueryTimeoutQueryException.php', 'AphrontRecoverableQueryException' => 'aphront/storage/exception/AphrontRecoverableQueryException.php', 'AphrontRequestStream' => 'aphront/requeststream/AphrontRequestStream.php', 'AphrontSchemaQueryException' => 'aphront/storage/exception/AphrontSchemaQueryException.php', 'AphrontScopedUnguardedWriteCapability' => 'aphront/writeguard/AphrontScopedUnguardedWriteCapability.php', 'AphrontWriteGuard' => 'aphront/writeguard/AphrontWriteGuard.php', 'BaseHTTPFuture' => 'future/http/BaseHTTPFuture.php', 'CaseInsensitiveArray' => 'utils/CaseInsensitiveArray.php', 'CaseInsensitiveArrayTestCase' => 'utils/__tests__/CaseInsensitiveArrayTestCase.php', 'CommandException' => 'future/exec/CommandException.php', 'ConduitClient' => 'conduit/ConduitClient.php', 'ConduitClientException' => 'conduit/ConduitClientException.php', 'ConduitClientTestCase' => 'conduit/__tests__/ConduitClientTestCase.php', 'ConduitFuture' => 'conduit/ConduitFuture.php', 'ExecFuture' => 'future/exec/ExecFuture.php', 'ExecFutureTestCase' => 'future/exec/__tests__/ExecFutureTestCase.php', 'ExecPassthruTestCase' => 'future/exec/__tests__/ExecPassthruTestCase.php', 'FileFinder' => 'filesystem/FileFinder.php', 'FileFinderTestCase' => 'filesystem/__tests__/FileFinderTestCase.php', 'FileList' => 'filesystem/FileList.php', 'Filesystem' => 'filesystem/Filesystem.php', 'FilesystemException' => 'filesystem/FilesystemException.php', 'FilesystemTestCase' => 'filesystem/__tests__/FilesystemTestCase.php', 'Future' => 'future/Future.php', 'FutureIterator' => 'future/FutureIterator.php', 'FutureIteratorTestCase' => 'future/__tests__/FutureIteratorTestCase.php', 'FutureProxy' => 'future/FutureProxy.php', 'HTTPFuture' => 'future/http/HTTPFuture.php', 'HTTPFutureCURLResponseStatus' => 'future/http/status/HTTPFutureCURLResponseStatus.php', 'HTTPFutureCertificateResponseStatus' => 'future/http/status/HTTPFutureCertificateResponseStatus.php', 'HTTPFutureHTTPResponseStatus' => 'future/http/status/HTTPFutureHTTPResponseStatus.php', 'HTTPFutureParseResponseStatus' => 'future/http/status/HTTPFutureParseResponseStatus.php', 'HTTPFutureResponseStatus' => 'future/http/status/HTTPFutureResponseStatus.php', 'HTTPFutureTransportResponseStatus' => 'future/http/status/HTTPFutureTransportResponseStatus.php', 'HTTPSFuture' => 'future/http/HTTPSFuture.php', 'ImmediateFuture' => 'future/ImmediateFuture.php', 'LibphutilUSEnglishTranslation' => 'internationalization/translation/LibphutilUSEnglishTranslation.php', 'LinesOfALarge' => 'filesystem/linesofalarge/LinesOfALarge.php', 'LinesOfALargeExecFuture' => 'filesystem/linesofalarge/LinesOfALargeExecFuture.php', 'LinesOfALargeExecFutureTestCase' => 'filesystem/linesofalarge/__tests__/LinesOfALargeExecFutureTestCase.php', 'LinesOfALargeFile' => 'filesystem/linesofalarge/LinesOfALargeFile.php', 'LinesOfALargeFileTestCase' => 'filesystem/linesofalarge/__tests__/LinesOfALargeFileTestCase.php', 'MFilterTestHelper' => 'utils/__tests__/MFilterTestHelper.php', 'PHPASTParserTestCase' => 'parser/xhpast/__tests__/PHPASTParserTestCase.php', 'PhageAction' => 'phage/action/PhageAction.php', 'PhageAgentAction' => 'phage/action/PhageAgentAction.php', 'PhageAgentBootloader' => 'phage/bootloader/PhageAgentBootloader.php', 'PhageAgentTestCase' => 'phage/__tests__/PhageAgentTestCase.php', 'PhageExecuteAction' => 'phage/action/PhageExecuteAction.php', 'PhageLocalAction' => 'phage/action/PhageLocalAction.php', 'PhagePHPAgent' => 'phage/agent/PhagePHPAgent.php', 'PhagePHPAgentBootloader' => 'phage/bootloader/PhagePHPAgentBootloader.php', 'PhagePlanAction' => 'phage/action/PhagePlanAction.php', 'Phobject' => 'object/Phobject.php', 'PhobjectTestCase' => 'object/__tests__/PhobjectTestCase.php', 'PhutilAPCKeyValueCache' => 'cache/PhutilAPCKeyValueCache.php', 'PhutilAWSCloudFormationFuture' => 'future/aws/PhutilAWSCloudFormationFuture.php', 'PhutilAWSCloudWatchFuture' => 'future/aws/PhutilAWSCloudWatchFuture.php', 'PhutilAWSEC2Future' => 'future/aws/PhutilAWSEC2Future.php', 'PhutilAWSException' => 'future/aws/PhutilAWSException.php', 'PhutilAWSFuture' => 'future/aws/PhutilAWSFuture.php', 'PhutilAWSManagementWorkflow' => 'future/aws/management/PhutilAWSManagementWorkflow.php', 'PhutilAWSS3DeleteManagementWorkflow' => 'future/aws/management/PhutilAWSS3DeleteManagementWorkflow.php', 'PhutilAWSS3Future' => 'future/aws/PhutilAWSS3Future.php', 'PhutilAWSS3GetManagementWorkflow' => 'future/aws/management/PhutilAWSS3GetManagementWorkflow.php', 'PhutilAWSS3ManagementWorkflow' => 'future/aws/management/PhutilAWSS3ManagementWorkflow.php', 'PhutilAWSS3PutManagementWorkflow' => 'future/aws/management/PhutilAWSS3PutManagementWorkflow.php', 'PhutilAWSv4Signature' => 'future/aws/PhutilAWSv4Signature.php', 'PhutilAWSv4SignatureTestCase' => 'future/aws/__tests__/PhutilAWSv4SignatureTestCase.php', 'PhutilAggregateException' => 'error/PhutilAggregateException.php', 'PhutilAllCapsEnglishLocale' => 'internationalization/locales/PhutilAllCapsEnglishLocale.php', 'PhutilAmazonAuthAdapter' => 'auth/PhutilAmazonAuthAdapter.php', 'PhutilArgumentParser' => 'parser/argument/PhutilArgumentParser.php', 'PhutilArgumentParserException' => 'parser/argument/exception/PhutilArgumentParserException.php', 'PhutilArgumentParserTestCase' => 'parser/argument/__tests__/PhutilArgumentParserTestCase.php', 'PhutilArgumentSpecification' => 'parser/argument/PhutilArgumentSpecification.php', 'PhutilArgumentSpecificationException' => 'parser/argument/exception/PhutilArgumentSpecificationException.php', 'PhutilArgumentSpecificationTestCase' => 'parser/argument/__tests__/PhutilArgumentSpecificationTestCase.php', 'PhutilArgumentSpellingCorrector' => 'parser/argument/PhutilArgumentSpellingCorrector.php', 'PhutilArgumentSpellingCorrectorTestCase' => 'parser/argument/__tests__/PhutilArgumentSpellingCorrectorTestCase.php', 'PhutilArgumentUsageException' => 'parser/argument/exception/PhutilArgumentUsageException.php', 'PhutilArgumentWorkflow' => 'parser/argument/workflow/PhutilArgumentWorkflow.php', 'PhutilArray' => 'utils/PhutilArray.php', 'PhutilArrayTestCase' => 'utils/__tests__/PhutilArrayTestCase.php', 'PhutilArrayWithDefaultValue' => 'utils/PhutilArrayWithDefaultValue.php', 'PhutilAsanaAuthAdapter' => 'auth/PhutilAsanaAuthAdapter.php', 'PhutilAsanaFuture' => 'future/asana/PhutilAsanaFuture.php', 'PhutilAuthAdapter' => 'auth/PhutilAuthAdapter.php', 'PhutilAuthConfigurationException' => 'auth/exception/PhutilAuthConfigurationException.php', 'PhutilAuthCredentialException' => 'auth/exception/PhutilAuthCredentialException.php', 'PhutilAuthException' => 'auth/exception/PhutilAuthException.php', 'PhutilAuthUserAbortedException' => 'auth/exception/PhutilAuthUserAbortedException.php', 'PhutilBacktraceSignalHandler' => 'future/exec/PhutilBacktraceSignalHandler.php', 'PhutilBallOfPHP' => 'phage/util/PhutilBallOfPHP.php', 'PhutilBinaryAnalyzer' => 'filesystem/binary/PhutilBinaryAnalyzer.php', 'PhutilBinaryAnalyzerTestCase' => 'filesystem/binary/__tests__/PhutilBinaryAnalyzerTestCase.php', 'PhutilBitbucketAuthAdapter' => 'auth/PhutilBitbucketAuthAdapter.php', 'PhutilBootloader' => 'moduleutils/PhutilBootloader.php', 'PhutilBootloaderException' => 'moduleutils/PhutilBootloaderException.php', 'PhutilBritishEnglishLocale' => 'internationalization/locales/PhutilBritishEnglishLocale.php', 'PhutilBufferedIterator' => 'utils/PhutilBufferedIterator.php', 'PhutilBufferedIteratorTestCase' => 'utils/__tests__/PhutilBufferedIteratorTestCase.php', 'PhutilBugtraqParser' => 'parser/PhutilBugtraqParser.php', 'PhutilBugtraqParserTestCase' => 'parser/__tests__/PhutilBugtraqParserTestCase.php', 'PhutilCIDRBlock' => 'ip/PhutilCIDRBlock.php', 'PhutilCIDRList' => 'ip/PhutilCIDRList.php', 'PhutilCLikeCodeSnippetContextFreeGrammar' => 'grammar/code/PhutilCLikeCodeSnippetContextFreeGrammar.php', 'PhutilCalendarAbsoluteDateTime' => 'parser/calendar/data/PhutilCalendarAbsoluteDateTime.php', 'PhutilCalendarContainerNode' => 'parser/calendar/data/PhutilCalendarContainerNode.php', 'PhutilCalendarDateTime' => 'parser/calendar/data/PhutilCalendarDateTime.php', 'PhutilCalendarDateTimeTestCase' => 'parser/calendar/data/__tests__/PhutilCalendarDateTimeTestCase.php', 'PhutilCalendarDocumentNode' => 'parser/calendar/data/PhutilCalendarDocumentNode.php', 'PhutilCalendarDuration' => 'parser/calendar/data/PhutilCalendarDuration.php', 'PhutilCalendarEventNode' => 'parser/calendar/data/PhutilCalendarEventNode.php', 'PhutilCalendarNode' => 'parser/calendar/data/PhutilCalendarNode.php', 'PhutilCalendarProxyDateTime' => 'parser/calendar/data/PhutilCalendarProxyDateTime.php', 'PhutilCalendarRawNode' => 'parser/calendar/data/PhutilCalendarRawNode.php', 'PhutilCalendarRecurrenceList' => 'parser/calendar/data/PhutilCalendarRecurrenceList.php', 'PhutilCalendarRecurrenceRule' => 'parser/calendar/data/PhutilCalendarRecurrenceRule.php', 'PhutilCalendarRecurrenceRuleTestCase' => 'parser/calendar/data/__tests__/PhutilCalendarRecurrenceRuleTestCase.php', 'PhutilCalendarRecurrenceSet' => 'parser/calendar/data/PhutilCalendarRecurrenceSet.php', 'PhutilCalendarRecurrenceSource' => 'parser/calendar/data/PhutilCalendarRecurrenceSource.php', 'PhutilCalendarRecurrenceTestCase' => 'parser/calendar/data/__tests__/PhutilCalendarRecurrenceTestCase.php', 'PhutilCalendarRelativeDateTime' => 'parser/calendar/data/PhutilCalendarRelativeDateTime.php', 'PhutilCalendarRootNode' => 'parser/calendar/data/PhutilCalendarRootNode.php', 'PhutilCalendarUserNode' => 'parser/calendar/data/PhutilCalendarUserNode.php', 'PhutilCallbackFilterIterator' => 'utils/PhutilCallbackFilterIterator.php', 'PhutilCallbackSignalHandler' => 'future/exec/PhutilCallbackSignalHandler.php', 'PhutilChannel' => 'channel/PhutilChannel.php', 'PhutilChannelChannel' => 'channel/PhutilChannelChannel.php', 'PhutilChannelTestCase' => 'channel/__tests__/PhutilChannelTestCase.php', 'PhutilChunkedIterator' => 'utils/PhutilChunkedIterator.php', 'PhutilChunkedIteratorTestCase' => 'utils/__tests__/PhutilChunkedIteratorTestCase.php', 'PhutilClassMapQuery' => 'symbols/PhutilClassMapQuery.php', 'PhutilCloudWatchMetric' => 'future/aws/PhutilCloudWatchMetric.php', 'PhutilCodeSnippetContextFreeGrammar' => 'grammar/code/PhutilCodeSnippetContextFreeGrammar.php', 'PhutilCommandString' => 'xsprintf/PhutilCommandString.php', 'PhutilConsole' => 'console/PhutilConsole.php', 'PhutilConsoleBlock' => 'console/view/PhutilConsoleBlock.php', 'PhutilConsoleError' => 'console/view/PhutilConsoleError.php', 'PhutilConsoleFormatter' => 'console/PhutilConsoleFormatter.php', 'PhutilConsoleInfo' => 'console/view/PhutilConsoleInfo.php', 'PhutilConsoleList' => 'console/view/PhutilConsoleList.php', 'PhutilConsoleLogLine' => 'console/view/PhutilConsoleLogLine.php', 'PhutilConsoleMessage' => 'console/PhutilConsoleMessage.php', 'PhutilConsoleMetrics' => 'console/PhutilConsoleMetrics.php', 'PhutilConsoleMetricsSignalHandler' => 'future/exec/PhutilConsoleMetricsSignalHandler.php', 'PhutilConsoleProgressBar' => 'console/PhutilConsoleProgressBar.php', 'PhutilConsoleServer' => 'console/PhutilConsoleServer.php', 'PhutilConsoleServerChannel' => 'console/PhutilConsoleServerChannel.php', 'PhutilConsoleSkip' => 'console/view/PhutilConsoleSkip.php', 'PhutilConsoleStdinNotInteractiveException' => 'console/PhutilConsoleStdinNotInteractiveException.php', 'PhutilConsoleSyntaxHighlighter' => 'markup/syntax/highlighter/PhutilConsoleSyntaxHighlighter.php', 'PhutilConsoleTable' => 'console/view/PhutilConsoleTable.php', 'PhutilConsoleView' => 'console/view/PhutilConsoleView.php', 'PhutilConsoleWarning' => 'console/view/PhutilConsoleWarning.php', 'PhutilConsoleWrapTestCase' => 'console/__tests__/PhutilConsoleWrapTestCase.php', 'PhutilContextFreeGrammar' => 'grammar/PhutilContextFreeGrammar.php', 'PhutilCowsay' => 'utils/PhutilCowsay.php', 'PhutilCowsayTestCase' => 'utils/__tests__/PhutilCowsayTestCase.php', 'PhutilCsprintfTestCase' => 'xsprintf/__tests__/PhutilCsprintfTestCase.php', 'PhutilCzechLocale' => 'internationalization/locales/PhutilCzechLocale.php', 'PhutilDaemon' => 'daemon/PhutilDaemon.php', 'PhutilDaemonHandle' => 'daemon/PhutilDaemonHandle.php', 'PhutilDaemonOverseer' => 'daemon/PhutilDaemonOverseer.php', 'PhutilDaemonOverseerModule' => 'daemon/PhutilDaemonOverseerModule.php', 'PhutilDaemonPool' => 'daemon/PhutilDaemonPool.php', 'PhutilDefaultSyntaxHighlighter' => 'markup/syntax/highlighter/PhutilDefaultSyntaxHighlighter.php', 'PhutilDefaultSyntaxHighlighterEngine' => 'markup/syntax/engine/PhutilDefaultSyntaxHighlighterEngine.php', 'PhutilDefaultSyntaxHighlighterEnginePygmentsFuture' => 'markup/syntax/highlighter/pygments/PhutilDefaultSyntaxHighlighterEnginePygmentsFuture.php', 'PhutilDefaultSyntaxHighlighterEngineTestCase' => 'markup/syntax/engine/__tests__/PhutilDefaultSyntaxHighlighterEngineTestCase.php', 'PhutilDeferredLog' => 'filesystem/PhutilDeferredLog.php', 'PhutilDeferredLogTestCase' => 'filesystem/__tests__/PhutilDeferredLogTestCase.php', 'PhutilDiffBinaryAnalyzer' => 'filesystem/binary/PhutilDiffBinaryAnalyzer.php', 'PhutilDirectedScalarGraph' => 'utils/PhutilDirectedScalarGraph.php', 'PhutilDirectoryFixture' => 'filesystem/PhutilDirectoryFixture.php', 'PhutilDirectoryKeyValueCache' => 'cache/PhutilDirectoryKeyValueCache.php', 'PhutilDisqusAuthAdapter' => 'auth/PhutilDisqusAuthAdapter.php', 'PhutilDivinerSyntaxHighlighter' => 'markup/syntax/highlighter/PhutilDivinerSyntaxHighlighter.php', 'PhutilDocblockParser' => 'parser/PhutilDocblockParser.php', 'PhutilDocblockParserTestCase' => 'parser/__tests__/PhutilDocblockParserTestCase.php', 'PhutilEditDistanceMatrix' => 'utils/PhutilEditDistanceMatrix.php', 'PhutilEditDistanceMatrixTestCase' => 'utils/__tests__/PhutilEditDistanceMatrixTestCase.php', 'PhutilEditorConfig' => 'parser/PhutilEditorConfig.php', 'PhutilEditorConfigTestCase' => 'parser/__tests__/PhutilEditorConfigTestCase.php', 'PhutilEmailAddress' => 'parser/PhutilEmailAddress.php', 'PhutilEmailAddressTestCase' => 'parser/__tests__/PhutilEmailAddressTestCase.php', 'PhutilEmojiLocale' => 'internationalization/locales/PhutilEmojiLocale.php', 'PhutilEmptyAuthAdapter' => 'auth/PhutilEmptyAuthAdapter.php', 'PhutilEnglishCanadaLocale' => 'internationalization/locales/PhutilEnglishCanadaLocale.php', 'PhutilErrorHandler' => 'error/PhutilErrorHandler.php', 'PhutilErrorHandlerTestCase' => 'error/__tests__/PhutilErrorHandlerTestCase.php', 'PhutilErrorTrap' => 'error/PhutilErrorTrap.php', 'PhutilEvent' => 'events/PhutilEvent.php', 'PhutilEventConstants' => 'events/constant/PhutilEventConstants.php', 'PhutilEventEngine' => 'events/PhutilEventEngine.php', 'PhutilEventListener' => 'events/PhutilEventListener.php', 'PhutilEventType' => 'events/constant/PhutilEventType.php', 'PhutilExampleBufferedIterator' => 'utils/PhutilExampleBufferedIterator.php', 'PhutilExcessiveServiceCallsDaemon' => 'daemon/torture/PhutilExcessiveServiceCallsDaemon.php', 'PhutilExecChannel' => 'channel/PhutilExecChannel.php', 'PhutilExecPassthru' => 'future/exec/PhutilExecPassthru.php', 'PhutilExecutableFuture' => 'future/exec/PhutilExecutableFuture.php', 'PhutilExecutionEnvironment' => 'utils/PhutilExecutionEnvironment.php', 'PhutilExtensionsTestCase' => 'moduleutils/__tests__/PhutilExtensionsTestCase.php', 'PhutilFacebookAuthAdapter' => 'auth/PhutilFacebookAuthAdapter.php', 'PhutilFatalDaemon' => 'daemon/torture/PhutilFatalDaemon.php', 'PhutilFileLock' => 'filesystem/PhutilFileLock.php', 'PhutilFileLockTestCase' => 'filesystem/__tests__/PhutilFileLockTestCase.php', 'PhutilFileTree' => 'filesystem/PhutilFileTree.php', 'PhutilFrenchLocale' => 'internationalization/locales/PhutilFrenchLocale.php', 'PhutilGermanLocale' => 'internationalization/locales/PhutilGermanLocale.php', 'PhutilGitBinaryAnalyzer' => 'filesystem/binary/PhutilGitBinaryAnalyzer.php', 'PhutilGitHubAuthAdapter' => 'auth/PhutilGitHubAuthAdapter.php', 'PhutilGitHubFuture' => 'future/github/PhutilGitHubFuture.php', 'PhutilGitHubResponse' => 'future/github/PhutilGitHubResponse.php', 'PhutilGitURI' => 'parser/PhutilGitURI.php', 'PhutilGitURITestCase' => 'parser/__tests__/PhutilGitURITestCase.php', 'PhutilGoogleAuthAdapter' => 'auth/PhutilGoogleAuthAdapter.php', 'PhutilHTTPEngineExtension' => 'future/http/PhutilHTTPEngineExtension.php', 'PhutilHTTPResponse' => 'parser/http/PhutilHTTPResponse.php', 'PhutilHTTPResponseParser' => 'parser/http/PhutilHTTPResponseParser.php', 'PhutilHTTPResponseParserTestCase' => 'parser/http/__tests__/PhutilHTTPResponseParserTestCase.php', 'PhutilHangForeverDaemon' => 'daemon/torture/PhutilHangForeverDaemon.php', 'PhutilHashingIterator' => 'utils/PhutilHashingIterator.php', 'PhutilHashingIteratorTestCase' => 'utils/__tests__/PhutilHashingIteratorTestCase.php', 'PhutilHelpArgumentWorkflow' => 'parser/argument/workflow/PhutilHelpArgumentWorkflow.php', 'PhutilHgsprintfTestCase' => 'xsprintf/__tests__/PhutilHgsprintfTestCase.php', 'PhutilHighIntensityIntervalDaemon' => 'daemon/torture/PhutilHighIntensityIntervalDaemon.php', 'PhutilICSParser' => 'parser/calendar/ics/PhutilICSParser.php', 'PhutilICSParserException' => 'parser/calendar/ics/PhutilICSParserException.php', 'PhutilICSParserTestCase' => 'parser/calendar/ics/__tests__/PhutilICSParserTestCase.php', 'PhutilICSWriter' => 'parser/calendar/ics/PhutilICSWriter.php', 'PhutilICSWriterTestCase' => 'parser/calendar/ics/__tests__/PhutilICSWriterTestCase.php', 'PhutilINIParserException' => 'parser/exception/PhutilINIParserException.php', 'PhutilIPAddress' => 'ip/PhutilIPAddress.php', 'PhutilIPAddressTestCase' => 'ip/__tests__/PhutilIPAddressTestCase.php', 'PhutilIPv4Address' => 'ip/PhutilIPv4Address.php', 'PhutilIPv6Address' => 'ip/PhutilIPv6Address.php', 'PhutilInRequestKeyValueCache' => 'cache/PhutilInRequestKeyValueCache.php', 'PhutilInteractiveEditor' => 'console/PhutilInteractiveEditor.php', 'PhutilInvalidRuleParserGeneratorException' => 'parser/generator/exception/PhutilInvalidRuleParserGeneratorException.php', 'PhutilInvalidStateException' => 'exception/PhutilInvalidStateException.php', 'PhutilInvalidStateExceptionTestCase' => 'exception/__tests__/PhutilInvalidStateExceptionTestCase.php', 'PhutilInvisibleSyntaxHighlighter' => 'markup/syntax/highlighter/PhutilInvisibleSyntaxHighlighter.php', 'PhutilIrreducibleRuleParserGeneratorException' => 'parser/generator/exception/PhutilIrreducibleRuleParserGeneratorException.php', 'PhutilJIRAAuthAdapter' => 'auth/PhutilJIRAAuthAdapter.php', 'PhutilJSON' => 'parser/PhutilJSON.php', 'PhutilJSONFragmentLexer' => 'lexer/PhutilJSONFragmentLexer.php', 'PhutilJSONFragmentLexerHighlighterTestCase' => 'markup/syntax/highlighter/__tests__/PhutilJSONFragmentLexerHighlighterTestCase.php', 'PhutilJSONParser' => 'parser/PhutilJSONParser.php', 'PhutilJSONParserException' => 'parser/exception/PhutilJSONParserException.php', 'PhutilJSONParserTestCase' => 'parser/__tests__/PhutilJSONParserTestCase.php', 'PhutilJSONProtocolChannel' => 'channel/PhutilJSONProtocolChannel.php', 'PhutilJSONProtocolChannelTestCase' => 'channel/__tests__/PhutilJSONProtocolChannelTestCase.php', 'PhutilJSONTestCase' => 'parser/__tests__/PhutilJSONTestCase.php', 'PhutilJavaCodeSnippetContextFreeGrammar' => 'grammar/code/PhutilJavaCodeSnippetContextFreeGrammar.php', 'PhutilJavaFragmentLexer' => 'lexer/PhutilJavaFragmentLexer.php', 'PhutilKeyValueCache' => 'cache/PhutilKeyValueCache.php', 'PhutilKeyValueCacheNamespace' => 'cache/PhutilKeyValueCacheNamespace.php', 'PhutilKeyValueCacheProfiler' => 'cache/PhutilKeyValueCacheProfiler.php', 'PhutilKeyValueCacheProxy' => 'cache/PhutilKeyValueCacheProxy.php', 'PhutilKeyValueCacheStack' => 'cache/PhutilKeyValueCacheStack.php', 'PhutilKeyValueCacheTestCase' => 'cache/__tests__/PhutilKeyValueCacheTestCase.php', 'PhutilKoreanLocale' => 'internationalization/locales/PhutilKoreanLocale.php', 'PhutilLDAPAuthAdapter' => 'auth/PhutilLDAPAuthAdapter.php', 'PhutilLanguageGuesser' => 'parser/PhutilLanguageGuesser.php', 'PhutilLanguageGuesserTestCase' => 'parser/__tests__/PhutilLanguageGuesserTestCase.php', 'PhutilLexer' => 'lexer/PhutilLexer.php', 'PhutilLexerSyntaxHighlighter' => 'markup/syntax/highlighter/PhutilLexerSyntaxHighlighter.php', 'PhutilLibraryConflictException' => 'moduleutils/PhutilLibraryConflictException.php', 'PhutilLibraryMapBuilder' => 'moduleutils/PhutilLibraryMapBuilder.php', 'PhutilLibraryTestCase' => '__tests__/PhutilLibraryTestCase.php', 'PhutilLipsumContextFreeGrammar' => 'grammar/PhutilLipsumContextFreeGrammar.php', 'PhutilLocale' => 'internationalization/PhutilLocale.php', 'PhutilLocaleTestCase' => 'internationalization/__tests__/PhutilLocaleTestCase.php', 'PhutilLock' => 'filesystem/PhutilLock.php', 'PhutilLockException' => 'filesystem/PhutilLockException.php', 'PhutilLogFileChannel' => 'channel/PhutilLogFileChannel.php', 'PhutilLunarPhase' => 'utils/PhutilLunarPhase.php', 'PhutilLunarPhaseTestCase' => 'utils/__tests__/PhutilLunarPhaseTestCase.php', 'PhutilMarkupEngine' => 'markup/PhutilMarkupEngine.php', 'PhutilMarkupTestCase' => 'markup/__tests__/PhutilMarkupTestCase.php', 'PhutilMemcacheKeyValueCache' => 'cache/PhutilMemcacheKeyValueCache.php', 'PhutilMercurialBinaryAnalyzer' => 'filesystem/binary/PhutilMercurialBinaryAnalyzer.php', 'PhutilMethodNotImplementedException' => 'error/PhutilMethodNotImplementedException.php', 'PhutilMetricsChannel' => 'channel/PhutilMetricsChannel.php', 'PhutilMissingSymbolException' => 'symbols/exception/PhutilMissingSymbolException.php', 'PhutilModuleUtilsTestCase' => 'moduleutils/__tests__/PhutilModuleUtilsTestCase.php', 'PhutilNiceDaemon' => 'daemon/torture/PhutilNiceDaemon.php', 'PhutilNumber' => 'internationalization/PhutilNumber.php', 'PhutilOAuth1AuthAdapter' => 'auth/PhutilOAuth1AuthAdapter.php', 'PhutilOAuth1Future' => 'future/oauth/PhutilOAuth1Future.php', 'PhutilOAuth1FutureTestCase' => 'future/oauth/__tests__/PhutilOAuth1FutureTestCase.php', 'PhutilOAuthAuthAdapter' => 'auth/PhutilOAuthAuthAdapter.php', 'PhutilOnDiskKeyValueCache' => 'cache/PhutilOnDiskKeyValueCache.php', 'PhutilOpaqueEnvelope' => 'error/PhutilOpaqueEnvelope.php', 'PhutilOpaqueEnvelopeKey' => 'error/PhutilOpaqueEnvelopeKey.php', 'PhutilOpaqueEnvelopeTestCase' => 'error/__tests__/PhutilOpaqueEnvelopeTestCase.php', 'PhutilPHPCodeSnippetContextFreeGrammar' => 'grammar/code/PhutilPHPCodeSnippetContextFreeGrammar.php', 'PhutilPHPFragmentLexer' => 'lexer/PhutilPHPFragmentLexer.php', 'PhutilPHPFragmentLexerHighlighterTestCase' => 'markup/syntax/highlighter/__tests__/PhutilPHPFragmentLexerHighlighterTestCase.php', 'PhutilPHPFragmentLexerTestCase' => 'lexer/__tests__/PhutilPHPFragmentLexerTestCase.php', 'PhutilPHPObjectProtocolChannel' => 'channel/PhutilPHPObjectProtocolChannel.php', 'PhutilPHPObjectProtocolChannelTestCase' => 'channel/__tests__/PhutilPHPObjectProtocolChannelTestCase.php', 'PhutilParserGenerator' => 'parser/PhutilParserGenerator.php', 'PhutilParserGeneratorException' => 'parser/generator/exception/PhutilParserGeneratorException.php', 'PhutilParserGeneratorTestCase' => 'parser/__tests__/PhutilParserGeneratorTestCase.php', 'PhutilPayPalAPIFuture' => 'future/paypal/PhutilPayPalAPIFuture.php', 'PhutilPerson' => 'internationalization/PhutilPerson.php', 'PhutilPersonTest' => 'internationalization/__tests__/PhutilPersonTest.php', 'PhutilPhabricatorAuthAdapter' => 'auth/PhutilPhabricatorAuthAdapter.php', 'PhutilPhtTestCase' => 'internationalization/__tests__/PhutilPhtTestCase.php', 'PhutilPirateEnglishLocale' => 'internationalization/locales/PhutilPirateEnglishLocale.php', 'PhutilPortugueseBrazilLocale' => 'internationalization/locales/PhutilPortugueseBrazilLocale.php', 'PhutilPortuguesePortugalLocale' => 'internationalization/locales/PhutilPortuguesePortugalLocale.php', 'PhutilPostmarkFuture' => 'future/postmark/PhutilPostmarkFuture.php', 'PhutilPregsprintfTestCase' => 'xsprintf/__tests__/PhutilPregsprintfTestCase.php', 'PhutilProcessGroupDaemon' => 'daemon/torture/PhutilProcessGroupDaemon.php', 'PhutilProseDiff' => 'utils/PhutilProseDiff.php', 'PhutilProseDiffTestCase' => 'utils/__tests__/PhutilProseDiffTestCase.php', 'PhutilProseDifferenceEngine' => 'utils/PhutilProseDifferenceEngine.php', 'PhutilProtocolChannel' => 'channel/PhutilProtocolChannel.php', 'PhutilProxyException' => 'error/PhutilProxyException.php', 'PhutilProxyIterator' => 'utils/PhutilProxyIterator.php', 'PhutilPygmentizeBinaryAnalyzer' => 'filesystem/binary/PhutilPygmentizeBinaryAnalyzer.php', 'PhutilPygmentizeParser' => 'parser/PhutilPygmentizeParser.php', 'PhutilPygmentizeParserTestCase' => 'parser/__tests__/PhutilPygmentizeParserTestCase.php', 'PhutilPygmentsSyntaxHighlighter' => 'markup/syntax/highlighter/PhutilPygmentsSyntaxHighlighter.php', 'PhutilPythonFragmentLexer' => 'lexer/PhutilPythonFragmentLexer.php', 'PhutilQsprintfInterface' => 'xsprintf/PhutilQsprintfInterface.php', 'PhutilQueryString' => 'xsprintf/PhutilQueryString.php', 'PhutilQueryStringParser' => 'parser/PhutilQueryStringParser.php', 'PhutilQueryStringParserTestCase' => 'parser/__tests__/PhutilQueryStringParserTestCase.php', 'PhutilRainbowSyntaxHighlighter' => 'markup/syntax/highlighter/PhutilRainbowSyntaxHighlighter.php', 'PhutilRawEnglishLocale' => 'internationalization/locales/PhutilRawEnglishLocale.php', 'PhutilReadableSerializer' => 'readableserializer/PhutilReadableSerializer.php', 'PhutilReadableSerializerTestCase' => 'readableserializer/__tests__/PhutilReadableSerializerTestCase.php', 'PhutilRealNameContextFreeGrammar' => 'grammar/PhutilRealNameContextFreeGrammar.php', 'PhutilRemarkupBlockInterpreter' => 'markup/engine/remarkup/blockrule/PhutilRemarkupBlockInterpreter.php', 'PhutilRemarkupBlockRule' => 'markup/engine/remarkup/blockrule/PhutilRemarkupBlockRule.php', 'PhutilRemarkupBlockStorage' => 'markup/engine/remarkup/PhutilRemarkupBlockStorage.php', 'PhutilRemarkupBoldRule' => 'markup/engine/remarkup/markuprule/PhutilRemarkupBoldRule.php', 'PhutilRemarkupCodeBlockRule' => 'markup/engine/remarkup/blockrule/PhutilRemarkupCodeBlockRule.php', 'PhutilRemarkupDefaultBlockRule' => 'markup/engine/remarkup/blockrule/PhutilRemarkupDefaultBlockRule.php', 'PhutilRemarkupDelRule' => 'markup/engine/remarkup/markuprule/PhutilRemarkupDelRule.php', 'PhutilRemarkupDocumentLinkRule' => 'markup/engine/remarkup/markuprule/PhutilRemarkupDocumentLinkRule.php', 'PhutilRemarkupEngine' => 'markup/engine/PhutilRemarkupEngine.php', 'PhutilRemarkupEngineTestCase' => 'markup/engine/__tests__/PhutilRemarkupEngineTestCase.php', 'PhutilRemarkupEscapeRemarkupRule' => 'markup/engine/remarkup/markuprule/PhutilRemarkupEscapeRemarkupRule.php', 'PhutilRemarkupHeaderBlockRule' => 'markup/engine/remarkup/blockrule/PhutilRemarkupHeaderBlockRule.php', 'PhutilRemarkupHighlightRule' => 'markup/engine/remarkup/markuprule/PhutilRemarkupHighlightRule.php', 'PhutilRemarkupHorizontalRuleBlockRule' => 'markup/engine/remarkup/blockrule/PhutilRemarkupHorizontalRuleBlockRule.php', 'PhutilRemarkupHyperlinkRule' => 'markup/engine/remarkup/markuprule/PhutilRemarkupHyperlinkRule.php', 'PhutilRemarkupInlineBlockRule' => 'markup/engine/remarkup/blockrule/PhutilRemarkupInlineBlockRule.php', 'PhutilRemarkupInterpreterBlockRule' => 'markup/engine/remarkup/blockrule/PhutilRemarkupInterpreterBlockRule.php', 'PhutilRemarkupItalicRule' => 'markup/engine/remarkup/markuprule/PhutilRemarkupItalicRule.php', 'PhutilRemarkupLinebreaksRule' => 'markup/engine/remarkup/markuprule/PhutilRemarkupLinebreaksRule.php', 'PhutilRemarkupListBlockRule' => 'markup/engine/remarkup/blockrule/PhutilRemarkupListBlockRule.php', 'PhutilRemarkupLiteralBlockRule' => 'markup/engine/remarkup/blockrule/PhutilRemarkupLiteralBlockRule.php', 'PhutilRemarkupMonospaceRule' => 'markup/engine/remarkup/markuprule/PhutilRemarkupMonospaceRule.php', 'PhutilRemarkupNoteBlockRule' => 'markup/engine/remarkup/blockrule/PhutilRemarkupNoteBlockRule.php', 'PhutilRemarkupQuotesBlockRule' => 'markup/engine/remarkup/blockrule/PhutilRemarkupQuotesBlockRule.php', 'PhutilRemarkupReplyBlockRule' => 'markup/engine/remarkup/blockrule/PhutilRemarkupReplyBlockRule.php', 'PhutilRemarkupRule' => 'markup/engine/remarkup/markuprule/PhutilRemarkupRule.php', 'PhutilRemarkupSimpleTableBlockRule' => 'markup/engine/remarkup/blockrule/PhutilRemarkupSimpleTableBlockRule.php', 'PhutilRemarkupTableBlockRule' => 'markup/engine/remarkup/blockrule/PhutilRemarkupTableBlockRule.php', 'PhutilRemarkupTestInterpreterRule' => 'markup/engine/remarkup/blockrule/PhutilRemarkupTestInterpreterRule.php', 'PhutilRemarkupUnderlineRule' => 'markup/engine/remarkup/markuprule/PhutilRemarkupUnderlineRule.php', 'PhutilRope' => 'utils/PhutilRope.php', 'PhutilRopeTestCase' => 'utils/__tests__/PhutilRopeTestCase.php', 'PhutilSafeHTML' => 'markup/PhutilSafeHTML.php', 'PhutilSafeHTMLProducerInterface' => 'markup/PhutilSafeHTMLProducerInterface.php', 'PhutilSafeHTMLTestCase' => 'markup/__tests__/PhutilSafeHTMLTestCase.php', 'PhutilSaturateStdoutDaemon' => 'daemon/torture/PhutilSaturateStdoutDaemon.php', 'PhutilSearchQueryCompiler' => 'search/PhutilSearchQueryCompiler.php', 'PhutilSearchQueryCompilerSyntaxException' => 'search/PhutilSearchQueryCompilerSyntaxException.php', 'PhutilSearchQueryCompilerTestCase' => 'search/__tests__/PhutilSearchQueryCompilerTestCase.php', 'PhutilSearchQueryToken' => 'search/PhutilSearchQueryToken.php', 'PhutilSearchStemmer' => 'search/PhutilSearchStemmer.php', 'PhutilSearchStemmerTestCase' => 'search/__tests__/PhutilSearchStemmerTestCase.php', 'PhutilServiceProfiler' => 'serviceprofiler/PhutilServiceProfiler.php', 'PhutilShellLexer' => 'lexer/PhutilShellLexer.php', 'PhutilShellLexerTestCase' => 'lexer/__tests__/PhutilShellLexerTestCase.php', 'PhutilSignalHandler' => 'future/exec/PhutilSignalHandler.php', 'PhutilSignalRouter' => 'future/exec/PhutilSignalRouter.php', 'PhutilSimpleOptions' => 'parser/PhutilSimpleOptions.php', 'PhutilSimpleOptionsLexer' => 'lexer/PhutilSimpleOptionsLexer.php', 'PhutilSimpleOptionsLexerTestCase' => 'lexer/__tests__/PhutilSimpleOptionsLexerTestCase.php', 'PhutilSimpleOptionsTestCase' => 'parser/__tests__/PhutilSimpleOptionsTestCase.php', 'PhutilSimplifiedChineseLocale' => 'internationalization/locales/PhutilSimplifiedChineseLocale.php', 'PhutilSlackAuthAdapter' => 'auth/PhutilSlackAuthAdapter.php', 'PhutilSlackFuture' => 'future/slack/PhutilSlackFuture.php', 'PhutilSocketChannel' => 'channel/PhutilSocketChannel.php', 'PhutilSortVector' => 'utils/PhutilSortVector.php', 'PhutilSpanishSpainLocale' => 'internationalization/locales/PhutilSpanishSpainLocale.php', 'PhutilSprite' => 'sprites/PhutilSprite.php', 'PhutilSpriteSheet' => 'sprites/PhutilSpriteSheet.php', 'PhutilStreamIterator' => 'utils/PhutilStreamIterator.php', 'PhutilSubversionBinaryAnalyzer' => 'filesystem/binary/PhutilSubversionBinaryAnalyzer.php', 'PhutilSymbolLoader' => 'symbols/PhutilSymbolLoader.php', 'PhutilSyntaxHighlighter' => 'markup/syntax/highlighter/PhutilSyntaxHighlighter.php', 'PhutilSyntaxHighlighterEngine' => 'markup/syntax/engine/PhutilSyntaxHighlighterEngine.php', 'PhutilSyntaxHighlighterException' => 'markup/syntax/highlighter/PhutilSyntaxHighlighterException.php', 'PhutilSystem' => 'utils/PhutilSystem.php', 'PhutilSystemTestCase' => 'utils/__tests__/PhutilSystemTestCase.php', 'PhutilTerminalString' => 'xsprintf/PhutilTerminalString.php', 'PhutilTestPhobject' => 'object/__tests__/PhutilTestPhobject.php', 'PhutilTortureTestDaemon' => 'daemon/torture/PhutilTortureTestDaemon.php', 'PhutilTraditionalChineseLocale' => 'internationalization/locales/PhutilTraditionalChineseLocale.php', 'PhutilTranslation' => 'internationalization/PhutilTranslation.php', 'PhutilTranslationTestCase' => 'internationalization/__tests__/PhutilTranslationTestCase.php', 'PhutilTranslator' => 'internationalization/PhutilTranslator.php', 'PhutilTranslatorTestCase' => 'internationalization/__tests__/PhutilTranslatorTestCase.php', 'PhutilTsprintfTestCase' => 'xsprintf/__tests__/PhutilTsprintfTestCase.php', 'PhutilTwitchAuthAdapter' => 'auth/PhutilTwitchAuthAdapter.php', 'PhutilTwitchFuture' => 'future/twitch/PhutilTwitchFuture.php', 'PhutilTwitterAuthAdapter' => 'auth/PhutilTwitterAuthAdapter.php', 'PhutilTypeCheckException' => 'parser/exception/PhutilTypeCheckException.php', 'PhutilTypeExtraParametersException' => 'parser/exception/PhutilTypeExtraParametersException.php', 'PhutilTypeLexer' => 'lexer/PhutilTypeLexer.php', 'PhutilTypeMissingParametersException' => 'parser/exception/PhutilTypeMissingParametersException.php', 'PhutilTypeSpec' => 'parser/PhutilTypeSpec.php', 'PhutilTypeSpecTestCase' => 'parser/__tests__/PhutilTypeSpecTestCase.php', 'PhutilURI' => 'parser/PhutilURI.php', 'PhutilURITestCase' => 'parser/__tests__/PhutilURITestCase.php', 'PhutilUSEnglishLocale' => 'internationalization/locales/PhutilUSEnglishLocale.php', 'PhutilUTF8StringTruncator' => 'utils/PhutilUTF8StringTruncator.php', 'PhutilUTF8TestCase' => 'utils/__tests__/PhutilUTF8TestCase.php', 'PhutilUnknownSymbolParserGeneratorException' => 'parser/generator/exception/PhutilUnknownSymbolParserGeneratorException.php', 'PhutilUnreachableRuleParserGeneratorException' => 'parser/generator/exception/PhutilUnreachableRuleParserGeneratorException.php', 'PhutilUnreachableTerminalParserGeneratorException' => 'parser/generator/exception/PhutilUnreachableTerminalParserGeneratorException.php', 'PhutilUrisprintfTestCase' => 'xsprintf/__tests__/PhutilUrisprintfTestCase.php', 'PhutilUtilsTestCase' => 'utils/__tests__/PhutilUtilsTestCase.php', 'PhutilVeryWowEnglishLocale' => 'internationalization/locales/PhutilVeryWowEnglishLocale.php', 'PhutilWordPressAuthAdapter' => 'auth/PhutilWordPressAuthAdapter.php', 'PhutilWordPressFuture' => 'future/wordpress/PhutilWordPressFuture.php', 'PhutilXHPASTBinary' => 'parser/xhpast/bin/PhutilXHPASTBinary.php', 'PhutilXHPASTSyntaxHighlighter' => 'markup/syntax/highlighter/PhutilXHPASTSyntaxHighlighter.php', 'PhutilXHPASTSyntaxHighlighterFuture' => 'markup/syntax/highlighter/xhpast/PhutilXHPASTSyntaxHighlighterFuture.php', 'PhutilXHPASTSyntaxHighlighterTestCase' => 'markup/syntax/highlighter/__tests__/PhutilXHPASTSyntaxHighlighterTestCase.php', 'QueryFuture' => 'future/query/QueryFuture.php', 'TempFile' => 'filesystem/TempFile.php', 'TestAbstractDirectedGraph' => 'utils/__tests__/TestAbstractDirectedGraph.php', 'XHPASTNode' => 'parser/xhpast/api/XHPASTNode.php', 'XHPASTNodeTestCase' => 'parser/xhpast/api/__tests__/XHPASTNodeTestCase.php', 'XHPASTSyntaxErrorException' => 'parser/xhpast/api/XHPASTSyntaxErrorException.php', 'XHPASTToken' => 'parser/xhpast/api/XHPASTToken.php', 'XHPASTTree' => 'parser/xhpast/api/XHPASTTree.php', 'XHPASTTreeTestCase' => 'parser/xhpast/api/__tests__/XHPASTTreeTestCase.php', 'XsprintfUnknownConversionException' => 'xsprintf/exception/XsprintfUnknownConversionException.php', ), 'function' => array( 'array_fuse' => 'utils/utils.php', 'array_interleave' => 'utils/utils.php', 'array_mergev' => 'utils/utils.php', 'array_select_keys' => 'utils/utils.php', 'assert_instances_of' => 'utils/utils.php', 'assert_stringlike' => 'utils/utils.php', 'coalesce' => 'utils/utils.php', 'csprintf' => 'xsprintf/csprintf.php', 'exec_manual' => 'future/exec/execx.php', 'execx' => 'future/exec/execx.php', 'head' => 'utils/utils.php', 'head_key' => 'utils/utils.php', 'hgsprintf' => 'xsprintf/hgsprintf.php', 'hsprintf' => 'markup/render.php', 'id' => 'utils/utils.php', 'idx' => 'utils/utils.php', 'idxv' => 'utils/utils.php', 'ifilter' => 'utils/utils.php', 'igroup' => 'utils/utils.php', 'ipull' => 'utils/utils.php', 'isort' => 'utils/utils.php', 'jsprintf' => 'xsprintf/jsprintf.php', 'last' => 'utils/utils.php', 'last_key' => 'utils/utils.php', 'ldap_sprintf' => 'xsprintf/ldapsprintf.php', 'mfilter' => 'utils/utils.php', 'mgroup' => 'utils/utils.php', 'mpull' => 'utils/utils.php', 'msort' => 'utils/utils.php', 'msortv' => 'utils/utils.php', 'newv' => 'utils/utils.php', 'nonempty' => 'utils/utils.php', 'phlog' => 'error/phlog.php', 'pht' => 'internationalization/pht.php', 'phutil_build_http_querystring' => 'utils/utils.php', 'phutil_build_http_querystring_from_pairs' => 'utils/utils.php', 'phutil_censor_credentials' => 'utils/utils.php', 'phutil_console_confirm' => 'console/format.php', 'phutil_console_format' => 'console/format.php', 'phutil_console_get_terminal_width' => 'console/format.php', 'phutil_console_prompt' => 'console/format.php', 'phutil_console_require_tty' => 'console/format.php', 'phutil_console_select' => 'console/format.php', 'phutil_console_wrap' => 'console/format.php', 'phutil_count' => 'internationalization/pht.php', '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', 'phutil_escape_uri' => 'markup/render.php', 'phutil_escape_uri_path_component' => 'markup/render.php', 'phutil_fnmatch' => 'utils/utils.php', 'phutil_format_bytes' => 'utils/viewutils.php', 'phutil_format_relative_time' => 'utils/viewutils.php', 'phutil_format_relative_time_detailed' => 'utils/viewutils.php', 'phutil_format_units_generic' => 'utils/viewutils.php', 'phutil_fwrite_nonblocking_stream' => 'utils/utils.php', 'phutil_get_current_library_name' => 'moduleutils/moduleutils.php', 'phutil_get_library_name_for_root' => 'moduleutils/moduleutils.php', 'phutil_get_library_root' => 'moduleutils/moduleutils.php', 'phutil_get_library_root_for_path' => 'moduleutils/moduleutils.php', 'phutil_get_signal_name' => 'future/exec/execx.php', 'phutil_get_system_locale' => 'utils/utf8.php', 'phutil_hashes_are_identical' => 'utils/utils.php', 'phutil_http_parameter_pair' => 'utils/utils.php', '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', 'phutil_is_utf8_with_only_bmp_characters' => 'utils/utf8.php', 'phutil_is_windows' => 'utils/utils.php', 'phutil_json_decode' => 'utils/utils.php', 'phutil_json_encode' => 'utils/utils.php', 'phutil_load_library' => 'moduleutils/core.php', 'phutil_loggable_string' => 'utils/utils.php', 'phutil_microseconds_since' => 'utils/utils.php', 'phutil_parse_bytes' => 'utils/viewutils.php', 'phutil_passthru' => 'future/exec/execx.php', 'phutil_person' => 'internationalization/pht.php', 'phutil_register_library' => 'moduleutils/core.php', 'phutil_register_library_map' => 'moduleutils/core.php', 'phutil_safe_html' => 'markup/render.php', 'phutil_set_system_locale' => 'utils/utf8.php', 'phutil_split_lines' => 'utils/utils.php', 'phutil_string_cast' => 'utils/utils.php', 'phutil_tag' => 'markup/render.php', 'phutil_tag_div' => 'markup/render.php', 'phutil_unescape_uri_path_component' => 'markup/render.php', 'phutil_units' => 'utils/utils.php', 'phutil_utf8_console_strlen' => 'utils/utf8.php', 'phutil_utf8_convert' => 'utils/utf8.php', 'phutil_utf8_encode_codepoint' => 'utils/utf8.php', 'phutil_utf8_hard_wrap' => 'utils/utf8.php', 'phutil_utf8_hard_wrap_html' => 'utils/utf8.php', 'phutil_utf8_is_cjk' => 'utils/utf8.php', 'phutil_utf8_is_combining_character' => 'utils/utf8.php', 'phutil_utf8_strlen' => 'utils/utf8.php', 'phutil_utf8_strtolower' => 'utils/utf8.php', 'phutil_utf8_strtoupper' => 'utils/utf8.php', 'phutil_utf8_strtr' => 'utils/utf8.php', 'phutil_utf8_ucwords' => 'utils/utf8.php', 'phutil_utf8ize' => 'utils/utf8.php', 'phutil_utf8v' => 'utils/utf8.php', 'phutil_utf8v_codepoints' => 'utils/utf8.php', 'phutil_utf8v_combine_characters' => 'utils/utf8.php', 'phutil_utf8v_combined' => 'utils/utf8.php', 'phutil_validate_json' => 'utils/utils.php', 'phutil_var_export' => 'utils/utils.php', 'ppull' => 'utils/utils.php', 'pregsprintf' => 'xsprintf/pregsprintf.php', 'qsprintf' => 'xsprintf/qsprintf.php', 'qsprintf_check_scalar_type' => 'xsprintf/qsprintf.php', 'qsprintf_check_type' => 'xsprintf/qsprintf.php', 'queryfx' => 'xsprintf/queryfx.php', 'queryfx_all' => 'xsprintf/queryfx.php', 'queryfx_one' => 'xsprintf/queryfx.php', 'tsprintf' => 'xsprintf/tsprintf.php', 'urisprintf' => 'xsprintf/urisprintf.php', 'vcsprintf' => 'xsprintf/csprintf.php', 'vjsprintf' => 'xsprintf/jsprintf.php', 'vqsprintf' => 'xsprintf/qsprintf.php', 'vurisprintf' => 'xsprintf/urisprintf.php', 'xhp_parser_node_constants' => 'parser/xhpast/parser_nodes.php', 'xhpast_parser_token_constants' => 'parser/xhpast/parser_tokens.php', 'xsprintf' => 'xsprintf/xsprintf.php', 'xsprintf_callback_example' => 'xsprintf/xsprintf.php', 'xsprintf_command' => 'xsprintf/csprintf.php', 'xsprintf_javascript' => 'xsprintf/jsprintf.php', 'xsprintf_ldap' => 'xsprintf/ldapsprintf.php', 'xsprintf_mercurial' => 'xsprintf/hgsprintf.php', 'xsprintf_query' => 'xsprintf/qsprintf.php', 'xsprintf_regex' => 'xsprintf/pregsprintf.php', 'xsprintf_terminal' => 'xsprintf/tsprintf.php', 'xsprintf_uri' => 'xsprintf/urisprintf.php', ), 'xmap' => array( 'AASTNode' => 'Phobject', 'AASTNodeList' => array( 'Phobject', 'Countable', 'Iterator', ), 'AASTToken' => 'Phobject', 'AASTTree' => 'Phobject', 'AbstractDirectedGraph' => 'Phobject', 'AbstractDirectedGraphTestCase' => 'PhutilTestCase', 'AphrontAccessDeniedQueryException' => 'AphrontQueryException', 'AphrontBaseMySQLDatabaseConnection' => 'AphrontDatabaseConnection', 'AphrontCharacterSetQueryException' => 'AphrontQueryException', 'AphrontConnectionLostQueryException' => 'AphrontRecoverableQueryException', 'AphrontConnectionQueryException' => 'AphrontQueryException', 'AphrontCountQueryException' => 'AphrontQueryException', 'AphrontDatabaseConnection' => array( 'Phobject', 'PhutilQsprintfInterface', ), 'AphrontDatabaseTableRef' => array( 'Phobject', 'AphrontDatabaseTableRefInterface', ), 'AphrontDatabaseTransactionState' => 'Phobject', 'AphrontDeadlockQueryException' => 'AphrontRecoverableQueryException', 'AphrontDuplicateKeyQueryException' => 'AphrontQueryException', 'AphrontHTTPHeaderParser' => 'Phobject', 'AphrontHTTPHeaderParserTestCase' => 'PhutilTestCase', 'AphrontInvalidCredentialsQueryException' => 'AphrontQueryException', 'AphrontIsolatedDatabaseConnection' => 'AphrontDatabaseConnection', 'AphrontLockTimeoutQueryException' => 'AphrontRecoverableQueryException', 'AphrontMultipartParser' => 'Phobject', 'AphrontMultipartParserTestCase' => 'PhutilTestCase', 'AphrontMultipartPart' => 'Phobject', 'AphrontMySQLDatabaseConnection' => 'AphrontBaseMySQLDatabaseConnection', 'AphrontMySQLiDatabaseConnection' => 'AphrontBaseMySQLDatabaseConnection', 'AphrontNotSupportedQueryException' => 'AphrontQueryException', 'AphrontObjectMissingQueryException' => 'AphrontQueryException', 'AphrontParameterQueryException' => 'AphrontQueryException', 'AphrontQueryException' => 'Exception', 'AphrontQueryTimeoutQueryException' => 'AphrontRecoverableQueryException', 'AphrontRecoverableQueryException' => 'AphrontQueryException', 'AphrontRequestStream' => 'Phobject', 'AphrontSchemaQueryException' => 'AphrontQueryException', 'AphrontScopedUnguardedWriteCapability' => 'Phobject', 'AphrontWriteGuard' => 'Phobject', 'BaseHTTPFuture' => 'Future', 'CaseInsensitiveArray' => 'PhutilArray', 'CaseInsensitiveArrayTestCase' => 'PhutilTestCase', 'CommandException' => 'Exception', 'ConduitClient' => 'Phobject', 'ConduitClientException' => 'Exception', 'ConduitClientTestCase' => 'PhutilTestCase', 'ConduitFuture' => 'FutureProxy', 'ExecFuture' => 'PhutilExecutableFuture', 'ExecFutureTestCase' => 'PhutilTestCase', 'ExecPassthruTestCase' => 'PhutilTestCase', 'FileFinder' => 'Phobject', 'FileFinderTestCase' => 'PhutilTestCase', 'FileList' => 'Phobject', 'Filesystem' => 'Phobject', 'FilesystemException' => 'Exception', 'FilesystemTestCase' => 'PhutilTestCase', 'Future' => 'Phobject', 'FutureIterator' => array( 'Phobject', 'Iterator', ), 'FutureIteratorTestCase' => 'PhutilTestCase', 'FutureProxy' => 'Future', 'HTTPFuture' => 'BaseHTTPFuture', 'HTTPFutureCURLResponseStatus' => 'HTTPFutureResponseStatus', 'HTTPFutureCertificateResponseStatus' => 'HTTPFutureResponseStatus', 'HTTPFutureHTTPResponseStatus' => 'HTTPFutureResponseStatus', 'HTTPFutureParseResponseStatus' => 'HTTPFutureResponseStatus', 'HTTPFutureResponseStatus' => 'Exception', 'HTTPFutureTransportResponseStatus' => 'HTTPFutureResponseStatus', 'HTTPSFuture' => 'BaseHTTPFuture', 'ImmediateFuture' => 'Future', 'LibphutilUSEnglishTranslation' => 'PhutilTranslation', 'LinesOfALarge' => array( 'Phobject', 'Iterator', ), 'LinesOfALargeExecFuture' => 'LinesOfALarge', 'LinesOfALargeExecFutureTestCase' => 'PhutilTestCase', 'LinesOfALargeFile' => 'LinesOfALarge', 'LinesOfALargeFileTestCase' => 'PhutilTestCase', 'MFilterTestHelper' => 'Phobject', 'PHPASTParserTestCase' => 'PhutilTestCase', 'PhageAction' => 'Phobject', 'PhageAgentAction' => 'PhageAction', 'PhageAgentBootloader' => 'Phobject', 'PhageAgentTestCase' => 'PhutilTestCase', 'PhageExecuteAction' => 'PhageAction', 'PhageLocalAction' => 'PhageAgentAction', 'PhagePHPAgent' => 'Phobject', 'PhagePHPAgentBootloader' => 'PhageAgentBootloader', 'PhagePlanAction' => 'PhageAction', 'Phobject' => 'Iterator', 'PhobjectTestCase' => 'PhutilTestCase', 'PhutilAPCKeyValueCache' => 'PhutilKeyValueCache', 'PhutilAWSCloudFormationFuture' => 'PhutilAWSFuture', 'PhutilAWSCloudWatchFuture' => 'PhutilAWSFuture', 'PhutilAWSEC2Future' => 'PhutilAWSFuture', 'PhutilAWSException' => 'Exception', 'PhutilAWSFuture' => 'FutureProxy', 'PhutilAWSManagementWorkflow' => 'PhutilArgumentWorkflow', 'PhutilAWSS3DeleteManagementWorkflow' => 'PhutilAWSS3ManagementWorkflow', 'PhutilAWSS3Future' => 'PhutilAWSFuture', 'PhutilAWSS3GetManagementWorkflow' => 'PhutilAWSS3ManagementWorkflow', 'PhutilAWSS3ManagementWorkflow' => 'PhutilAWSManagementWorkflow', 'PhutilAWSS3PutManagementWorkflow' => 'PhutilAWSS3ManagementWorkflow', 'PhutilAWSv4Signature' => 'Phobject', 'PhutilAWSv4SignatureTestCase' => 'PhutilTestCase', 'PhutilAggregateException' => 'Exception', 'PhutilAllCapsEnglishLocale' => 'PhutilLocale', 'PhutilAmazonAuthAdapter' => 'PhutilOAuthAuthAdapter', 'PhutilArgumentParser' => 'Phobject', 'PhutilArgumentParserException' => 'Exception', 'PhutilArgumentParserTestCase' => 'PhutilTestCase', 'PhutilArgumentSpecification' => 'Phobject', 'PhutilArgumentSpecificationException' => 'PhutilArgumentParserException', 'PhutilArgumentSpecificationTestCase' => 'PhutilTestCase', 'PhutilArgumentSpellingCorrector' => 'Phobject', 'PhutilArgumentSpellingCorrectorTestCase' => 'PhutilTestCase', 'PhutilArgumentUsageException' => 'PhutilArgumentParserException', 'PhutilArgumentWorkflow' => 'Phobject', 'PhutilArray' => array( 'Phobject', 'Countable', 'ArrayAccess', 'Iterator', ), 'PhutilArrayTestCase' => 'PhutilTestCase', 'PhutilArrayWithDefaultValue' => 'PhutilArray', 'PhutilAsanaAuthAdapter' => 'PhutilOAuthAuthAdapter', 'PhutilAsanaFuture' => 'FutureProxy', 'PhutilAuthAdapter' => 'Phobject', 'PhutilAuthConfigurationException' => 'PhutilAuthException', 'PhutilAuthCredentialException' => 'PhutilAuthException', 'PhutilAuthException' => 'Exception', 'PhutilAuthUserAbortedException' => 'PhutilAuthException', 'PhutilBacktraceSignalHandler' => 'PhutilSignalHandler', 'PhutilBallOfPHP' => 'Phobject', 'PhutilBinaryAnalyzer' => 'Phobject', 'PhutilBinaryAnalyzerTestCase' => 'PhutilTestCase', 'PhutilBitbucketAuthAdapter' => 'PhutilOAuth1AuthAdapter', 'PhutilBootloaderException' => 'Exception', 'PhutilBritishEnglishLocale' => 'PhutilLocale', 'PhutilBufferedIterator' => array( 'Phobject', 'Iterator', ), 'PhutilBufferedIteratorTestCase' => 'PhutilTestCase', 'PhutilBugtraqParser' => 'Phobject', 'PhutilBugtraqParserTestCase' => 'PhutilTestCase', 'PhutilCIDRBlock' => 'Phobject', 'PhutilCIDRList' => 'Phobject', 'PhutilCLikeCodeSnippetContextFreeGrammar' => 'PhutilCodeSnippetContextFreeGrammar', 'PhutilCalendarAbsoluteDateTime' => 'PhutilCalendarDateTime', 'PhutilCalendarContainerNode' => 'PhutilCalendarNode', 'PhutilCalendarDateTime' => 'Phobject', 'PhutilCalendarDateTimeTestCase' => 'PhutilTestCase', 'PhutilCalendarDocumentNode' => 'PhutilCalendarContainerNode', 'PhutilCalendarDuration' => 'Phobject', 'PhutilCalendarEventNode' => 'PhutilCalendarContainerNode', 'PhutilCalendarNode' => 'Phobject', 'PhutilCalendarProxyDateTime' => 'PhutilCalendarDateTime', 'PhutilCalendarRawNode' => 'PhutilCalendarContainerNode', 'PhutilCalendarRecurrenceList' => 'PhutilCalendarRecurrenceSource', 'PhutilCalendarRecurrenceRule' => 'PhutilCalendarRecurrenceSource', 'PhutilCalendarRecurrenceRuleTestCase' => 'PhutilTestCase', 'PhutilCalendarRecurrenceSet' => 'Phobject', 'PhutilCalendarRecurrenceSource' => 'Phobject', 'PhutilCalendarRecurrenceTestCase' => 'PhutilTestCase', 'PhutilCalendarRelativeDateTime' => 'PhutilCalendarProxyDateTime', 'PhutilCalendarRootNode' => 'PhutilCalendarContainerNode', 'PhutilCalendarUserNode' => 'PhutilCalendarNode', 'PhutilCallbackFilterIterator' => 'FilterIterator', 'PhutilCallbackSignalHandler' => 'PhutilSignalHandler', 'PhutilChannel' => 'Phobject', 'PhutilChannelChannel' => 'PhutilChannel', 'PhutilChannelTestCase' => 'PhutilTestCase', 'PhutilChunkedIterator' => array( 'Phobject', 'Iterator', ), 'PhutilChunkedIteratorTestCase' => 'PhutilTestCase', 'PhutilClassMapQuery' => 'Phobject', 'PhutilCloudWatchMetric' => 'Phobject', 'PhutilCodeSnippetContextFreeGrammar' => 'PhutilContextFreeGrammar', 'PhutilCommandString' => 'Phobject', 'PhutilConsole' => 'Phobject', 'PhutilConsoleBlock' => 'PhutilConsoleView', 'PhutilConsoleError' => 'PhutilConsoleLogLine', 'PhutilConsoleFormatter' => 'Phobject', 'PhutilConsoleInfo' => 'PhutilConsoleLogLine', 'PhutilConsoleList' => 'PhutilConsoleView', 'PhutilConsoleLogLine' => 'PhutilConsoleView', 'PhutilConsoleMessage' => 'Phobject', 'PhutilConsoleMetrics' => 'Phobject', 'PhutilConsoleMetricsSignalHandler' => 'PhutilSignalHandler', 'PhutilConsoleProgressBar' => 'Phobject', 'PhutilConsoleServer' => 'Phobject', 'PhutilConsoleServerChannel' => 'PhutilChannelChannel', 'PhutilConsoleSkip' => 'PhutilConsoleLogLine', 'PhutilConsoleStdinNotInteractiveException' => 'Exception', 'PhutilConsoleSyntaxHighlighter' => 'Phobject', 'PhutilConsoleTable' => 'PhutilConsoleView', 'PhutilConsoleView' => 'Phobject', 'PhutilConsoleWarning' => 'PhutilConsoleLogLine', 'PhutilConsoleWrapTestCase' => 'PhutilTestCase', 'PhutilContextFreeGrammar' => 'Phobject', 'PhutilCowsay' => 'Phobject', 'PhutilCowsayTestCase' => 'PhutilTestCase', 'PhutilCsprintfTestCase' => 'PhutilTestCase', 'PhutilCzechLocale' => 'PhutilLocale', 'PhutilDaemon' => 'Phobject', 'PhutilDaemonHandle' => 'Phobject', 'PhutilDaemonOverseer' => 'Phobject', 'PhutilDaemonOverseerModule' => 'Phobject', 'PhutilDaemonPool' => 'Phobject', 'PhutilDefaultSyntaxHighlighter' => 'Phobject', 'PhutilDefaultSyntaxHighlighterEngine' => 'PhutilSyntaxHighlighterEngine', 'PhutilDefaultSyntaxHighlighterEnginePygmentsFuture' => 'FutureProxy', 'PhutilDefaultSyntaxHighlighterEngineTestCase' => 'PhutilTestCase', 'PhutilDeferredLog' => 'Phobject', 'PhutilDeferredLogTestCase' => 'PhutilTestCase', 'PhutilDiffBinaryAnalyzer' => 'PhutilBinaryAnalyzer', 'PhutilDirectedScalarGraph' => 'AbstractDirectedGraph', 'PhutilDirectoryFixture' => 'Phobject', 'PhutilDirectoryKeyValueCache' => 'PhutilKeyValueCache', 'PhutilDisqusAuthAdapter' => 'PhutilOAuthAuthAdapter', 'PhutilDivinerSyntaxHighlighter' => 'Phobject', 'PhutilDocblockParser' => 'Phobject', 'PhutilDocblockParserTestCase' => 'PhutilTestCase', 'PhutilEditDistanceMatrix' => 'Phobject', 'PhutilEditDistanceMatrixTestCase' => 'PhutilTestCase', 'PhutilEditorConfig' => 'Phobject', 'PhutilEditorConfigTestCase' => 'PhutilTestCase', 'PhutilEmailAddress' => 'Phobject', 'PhutilEmailAddressTestCase' => 'PhutilTestCase', 'PhutilEmojiLocale' => 'PhutilLocale', 'PhutilEmptyAuthAdapter' => 'PhutilAuthAdapter', 'PhutilEnglishCanadaLocale' => 'PhutilLocale', 'PhutilErrorHandler' => 'Phobject', 'PhutilErrorHandlerTestCase' => 'PhutilTestCase', 'PhutilErrorTrap' => 'Phobject', 'PhutilEvent' => 'Phobject', 'PhutilEventConstants' => 'Phobject', 'PhutilEventEngine' => 'Phobject', 'PhutilEventListener' => 'Phobject', 'PhutilEventType' => 'PhutilEventConstants', 'PhutilExampleBufferedIterator' => 'PhutilBufferedIterator', 'PhutilExcessiveServiceCallsDaemon' => 'PhutilTortureTestDaemon', 'PhutilExecChannel' => 'PhutilChannel', 'PhutilExecPassthru' => 'PhutilExecutableFuture', 'PhutilExecutableFuture' => 'Future', 'PhutilExecutionEnvironment' => 'Phobject', 'PhutilExtensionsTestCase' => 'PhutilTestCase', 'PhutilFacebookAuthAdapter' => 'PhutilOAuthAuthAdapter', 'PhutilFatalDaemon' => 'PhutilTortureTestDaemon', 'PhutilFileLock' => 'PhutilLock', 'PhutilFileLockTestCase' => 'PhutilTestCase', 'PhutilFileTree' => 'Phobject', 'PhutilFrenchLocale' => 'PhutilLocale', 'PhutilGermanLocale' => 'PhutilLocale', 'PhutilGitBinaryAnalyzer' => 'PhutilBinaryAnalyzer', 'PhutilGitHubAuthAdapter' => 'PhutilOAuthAuthAdapter', 'PhutilGitHubFuture' => 'FutureProxy', 'PhutilGitHubResponse' => 'Phobject', 'PhutilGitURI' => 'Phobject', 'PhutilGitURITestCase' => 'PhutilTestCase', 'PhutilGoogleAuthAdapter' => 'PhutilOAuthAuthAdapter', 'PhutilHTTPEngineExtension' => 'Phobject', 'PhutilHTTPResponse' => 'Phobject', 'PhutilHTTPResponseParser' => 'Phobject', 'PhutilHTTPResponseParserTestCase' => 'PhutilTestCase', 'PhutilHangForeverDaemon' => 'PhutilTortureTestDaemon', 'PhutilHashingIterator' => array( 'PhutilProxyIterator', 'Iterator', ), 'PhutilHashingIteratorTestCase' => 'PhutilTestCase', 'PhutilHelpArgumentWorkflow' => 'PhutilArgumentWorkflow', 'PhutilHgsprintfTestCase' => 'PhutilTestCase', 'PhutilHighIntensityIntervalDaemon' => 'PhutilTortureTestDaemon', 'PhutilICSParser' => 'Phobject', 'PhutilICSParserException' => 'Exception', 'PhutilICSParserTestCase' => 'PhutilTestCase', 'PhutilICSWriter' => 'Phobject', 'PhutilICSWriterTestCase' => 'PhutilTestCase', 'PhutilINIParserException' => 'Exception', 'PhutilIPAddress' => 'Phobject', 'PhutilIPAddressTestCase' => 'PhutilTestCase', 'PhutilIPv4Address' => 'PhutilIPAddress', 'PhutilIPv6Address' => 'PhutilIPAddress', 'PhutilInRequestKeyValueCache' => 'PhutilKeyValueCache', 'PhutilInteractiveEditor' => 'Phobject', 'PhutilInvalidRuleParserGeneratorException' => 'PhutilParserGeneratorException', 'PhutilInvalidStateException' => 'Exception', 'PhutilInvalidStateExceptionTestCase' => 'PhutilTestCase', 'PhutilInvisibleSyntaxHighlighter' => 'Phobject', 'PhutilIrreducibleRuleParserGeneratorException' => 'PhutilParserGeneratorException', 'PhutilJIRAAuthAdapter' => 'PhutilOAuth1AuthAdapter', 'PhutilJSON' => 'Phobject', 'PhutilJSONFragmentLexer' => 'PhutilLexer', 'PhutilJSONFragmentLexerHighlighterTestCase' => 'PhutilTestCase', 'PhutilJSONParser' => 'Phobject', 'PhutilJSONParserException' => 'Exception', 'PhutilJSONParserTestCase' => 'PhutilTestCase', 'PhutilJSONProtocolChannel' => 'PhutilProtocolChannel', 'PhutilJSONProtocolChannelTestCase' => 'PhutilTestCase', 'PhutilJSONTestCase' => 'PhutilTestCase', 'PhutilJavaCodeSnippetContextFreeGrammar' => 'PhutilCLikeCodeSnippetContextFreeGrammar', 'PhutilJavaFragmentLexer' => 'PhutilLexer', 'PhutilKeyValueCache' => 'Phobject', 'PhutilKeyValueCacheNamespace' => 'PhutilKeyValueCacheProxy', 'PhutilKeyValueCacheProfiler' => 'PhutilKeyValueCacheProxy', 'PhutilKeyValueCacheProxy' => 'PhutilKeyValueCache', 'PhutilKeyValueCacheStack' => 'PhutilKeyValueCache', 'PhutilKeyValueCacheTestCase' => 'PhutilTestCase', 'PhutilKoreanLocale' => 'PhutilLocale', 'PhutilLDAPAuthAdapter' => 'PhutilAuthAdapter', 'PhutilLanguageGuesser' => 'Phobject', 'PhutilLanguageGuesserTestCase' => 'PhutilTestCase', 'PhutilLexer' => 'Phobject', 'PhutilLexerSyntaxHighlighter' => 'PhutilSyntaxHighlighter', 'PhutilLibraryConflictException' => 'Exception', 'PhutilLibraryMapBuilder' => 'Phobject', 'PhutilLibraryTestCase' => 'PhutilTestCase', 'PhutilLipsumContextFreeGrammar' => 'PhutilContextFreeGrammar', 'PhutilLocale' => 'Phobject', 'PhutilLocaleTestCase' => 'PhutilTestCase', 'PhutilLock' => 'Phobject', 'PhutilLockException' => 'Exception', 'PhutilLogFileChannel' => 'PhutilChannelChannel', 'PhutilLunarPhase' => 'Phobject', 'PhutilLunarPhaseTestCase' => 'PhutilTestCase', 'PhutilMarkupEngine' => 'Phobject', 'PhutilMarkupTestCase' => 'PhutilTestCase', 'PhutilMemcacheKeyValueCache' => 'PhutilKeyValueCache', 'PhutilMercurialBinaryAnalyzer' => 'PhutilBinaryAnalyzer', 'PhutilMethodNotImplementedException' => 'Exception', 'PhutilMetricsChannel' => 'PhutilChannelChannel', 'PhutilMissingSymbolException' => 'Exception', 'PhutilModuleUtilsTestCase' => 'PhutilTestCase', 'PhutilNiceDaemon' => 'PhutilTortureTestDaemon', 'PhutilNumber' => 'Phobject', 'PhutilOAuth1AuthAdapter' => 'PhutilAuthAdapter', 'PhutilOAuth1Future' => 'FutureProxy', 'PhutilOAuth1FutureTestCase' => 'PhutilTestCase', 'PhutilOAuthAuthAdapter' => 'PhutilAuthAdapter', 'PhutilOnDiskKeyValueCache' => 'PhutilKeyValueCache', 'PhutilOpaqueEnvelope' => 'Phobject', 'PhutilOpaqueEnvelopeKey' => 'Phobject', 'PhutilOpaqueEnvelopeTestCase' => 'PhutilTestCase', 'PhutilPHPCodeSnippetContextFreeGrammar' => 'PhutilCLikeCodeSnippetContextFreeGrammar', 'PhutilPHPFragmentLexer' => 'PhutilLexer', 'PhutilPHPFragmentLexerHighlighterTestCase' => 'PhutilTestCase', 'PhutilPHPFragmentLexerTestCase' => 'PhutilTestCase', 'PhutilPHPObjectProtocolChannel' => 'PhutilProtocolChannel', 'PhutilPHPObjectProtocolChannelTestCase' => 'PhutilTestCase', 'PhutilParserGenerator' => 'Phobject', 'PhutilParserGeneratorException' => 'Exception', 'PhutilParserGeneratorTestCase' => 'PhutilTestCase', 'PhutilPayPalAPIFuture' => 'FutureProxy', 'PhutilPersonTest' => array( 'Phobject', 'PhutilPerson', ), 'PhutilPhabricatorAuthAdapter' => 'PhutilOAuthAuthAdapter', 'PhutilPhtTestCase' => 'PhutilTestCase', 'PhutilPirateEnglishLocale' => 'PhutilLocale', 'PhutilPortugueseBrazilLocale' => 'PhutilLocale', 'PhutilPortuguesePortugalLocale' => 'PhutilLocale', 'PhutilPostmarkFuture' => 'FutureProxy', 'PhutilPregsprintfTestCase' => 'PhutilTestCase', 'PhutilProcessGroupDaemon' => 'PhutilTortureTestDaemon', 'PhutilProseDiff' => 'Phobject', 'PhutilProseDiffTestCase' => 'PhutilTestCase', 'PhutilProseDifferenceEngine' => 'Phobject', 'PhutilProtocolChannel' => 'PhutilChannelChannel', 'PhutilProxyException' => 'Exception', 'PhutilProxyIterator' => array( 'Phobject', 'Iterator', ), 'PhutilPygmentizeBinaryAnalyzer' => 'PhutilBinaryAnalyzer', 'PhutilPygmentizeParser' => 'Phobject', 'PhutilPygmentizeParserTestCase' => 'PhutilTestCase', 'PhutilPygmentsSyntaxHighlighter' => 'Phobject', 'PhutilPythonFragmentLexer' => 'PhutilLexer', 'PhutilQueryString' => 'Phobject', 'PhutilQueryStringParser' => 'Phobject', 'PhutilQueryStringParserTestCase' => 'PhutilTestCase', 'PhutilRainbowSyntaxHighlighter' => 'Phobject', 'PhutilRawEnglishLocale' => 'PhutilLocale', 'PhutilReadableSerializer' => 'Phobject', 'PhutilReadableSerializerTestCase' => 'PhutilTestCase', 'PhutilRealNameContextFreeGrammar' => 'PhutilContextFreeGrammar', 'PhutilRemarkupBlockInterpreter' => 'Phobject', 'PhutilRemarkupBlockRule' => 'Phobject', 'PhutilRemarkupBlockStorage' => 'Phobject', 'PhutilRemarkupBoldRule' => 'PhutilRemarkupRule', 'PhutilRemarkupCodeBlockRule' => 'PhutilRemarkupBlockRule', 'PhutilRemarkupDefaultBlockRule' => 'PhutilRemarkupBlockRule', 'PhutilRemarkupDelRule' => 'PhutilRemarkupRule', 'PhutilRemarkupDocumentLinkRule' => 'PhutilRemarkupRule', 'PhutilRemarkupEngine' => 'PhutilMarkupEngine', 'PhutilRemarkupEngineTestCase' => 'PhutilTestCase', 'PhutilRemarkupEscapeRemarkupRule' => 'PhutilRemarkupRule', 'PhutilRemarkupHeaderBlockRule' => 'PhutilRemarkupBlockRule', 'PhutilRemarkupHighlightRule' => 'PhutilRemarkupRule', 'PhutilRemarkupHorizontalRuleBlockRule' => 'PhutilRemarkupBlockRule', 'PhutilRemarkupHyperlinkRule' => 'PhutilRemarkupRule', 'PhutilRemarkupInlineBlockRule' => 'PhutilRemarkupBlockRule', 'PhutilRemarkupInterpreterBlockRule' => 'PhutilRemarkupBlockRule', 'PhutilRemarkupItalicRule' => 'PhutilRemarkupRule', 'PhutilRemarkupLinebreaksRule' => 'PhutilRemarkupRule', 'PhutilRemarkupListBlockRule' => 'PhutilRemarkupBlockRule', 'PhutilRemarkupLiteralBlockRule' => 'PhutilRemarkupBlockRule', 'PhutilRemarkupMonospaceRule' => 'PhutilRemarkupRule', 'PhutilRemarkupNoteBlockRule' => 'PhutilRemarkupBlockRule', 'PhutilRemarkupQuotesBlockRule' => 'PhutilRemarkupBlockRule', 'PhutilRemarkupReplyBlockRule' => 'PhutilRemarkupBlockRule', 'PhutilRemarkupRule' => 'Phobject', 'PhutilRemarkupSimpleTableBlockRule' => 'PhutilRemarkupBlockRule', 'PhutilRemarkupTableBlockRule' => 'PhutilRemarkupBlockRule', 'PhutilRemarkupTestInterpreterRule' => 'PhutilRemarkupBlockInterpreter', 'PhutilRemarkupUnderlineRule' => 'PhutilRemarkupRule', 'PhutilRope' => 'Phobject', 'PhutilRopeTestCase' => 'PhutilTestCase', 'PhutilSafeHTML' => 'Phobject', 'PhutilSafeHTMLTestCase' => 'PhutilTestCase', 'PhutilSaturateStdoutDaemon' => 'PhutilTortureTestDaemon', 'PhutilSearchQueryCompiler' => 'Phobject', 'PhutilSearchQueryCompilerSyntaxException' => 'Exception', 'PhutilSearchQueryCompilerTestCase' => 'PhutilTestCase', 'PhutilSearchQueryToken' => 'Phobject', 'PhutilSearchStemmer' => 'Phobject', 'PhutilSearchStemmerTestCase' => 'PhutilTestCase', 'PhutilServiceProfiler' => 'Phobject', 'PhutilShellLexer' => 'PhutilLexer', 'PhutilShellLexerTestCase' => 'PhutilTestCase', 'PhutilSignalHandler' => 'Phobject', 'PhutilSignalRouter' => 'Phobject', 'PhutilSimpleOptions' => 'Phobject', 'PhutilSimpleOptionsLexer' => 'PhutilLexer', 'PhutilSimpleOptionsLexerTestCase' => 'PhutilTestCase', 'PhutilSimpleOptionsTestCase' => 'PhutilTestCase', 'PhutilSimplifiedChineseLocale' => 'PhutilLocale', 'PhutilSlackAuthAdapter' => 'PhutilOAuthAuthAdapter', 'PhutilSlackFuture' => 'FutureProxy', 'PhutilSocketChannel' => 'PhutilChannel', 'PhutilSortVector' => 'Phobject', 'PhutilSpanishSpainLocale' => 'PhutilLocale', 'PhutilSprite' => 'Phobject', 'PhutilSpriteSheet' => 'Phobject', 'PhutilStreamIterator' => array( 'Phobject', 'Iterator', ), 'PhutilSubversionBinaryAnalyzer' => 'PhutilBinaryAnalyzer', 'PhutilSyntaxHighlighter' => 'Phobject', 'PhutilSyntaxHighlighterEngine' => 'Phobject', 'PhutilSyntaxHighlighterException' => 'Exception', 'PhutilSystem' => 'Phobject', 'PhutilSystemTestCase' => 'PhutilTestCase', 'PhutilTerminalString' => 'Phobject', 'PhutilTestPhobject' => 'Phobject', 'PhutilTortureTestDaemon' => 'PhutilDaemon', 'PhutilTraditionalChineseLocale' => 'PhutilLocale', 'PhutilTranslation' => 'Phobject', 'PhutilTranslationTestCase' => 'PhutilTestCase', 'PhutilTranslator' => 'Phobject', 'PhutilTranslatorTestCase' => 'PhutilTestCase', 'PhutilTsprintfTestCase' => 'PhutilTestCase', 'PhutilTwitchAuthAdapter' => 'PhutilOAuthAuthAdapter', 'PhutilTwitchFuture' => 'FutureProxy', 'PhutilTwitterAuthAdapter' => 'PhutilOAuth1AuthAdapter', 'PhutilTypeCheckException' => 'Exception', 'PhutilTypeExtraParametersException' => 'Exception', 'PhutilTypeLexer' => 'PhutilLexer', 'PhutilTypeMissingParametersException' => 'Exception', 'PhutilTypeSpec' => 'Phobject', 'PhutilTypeSpecTestCase' => 'PhutilTestCase', 'PhutilURI' => 'Phobject', 'PhutilURITestCase' => 'PhutilTestCase', 'PhutilUSEnglishLocale' => 'PhutilLocale', 'PhutilUTF8StringTruncator' => 'Phobject', 'PhutilUTF8TestCase' => 'PhutilTestCase', 'PhutilUnknownSymbolParserGeneratorException' => 'PhutilParserGeneratorException', 'PhutilUnreachableRuleParserGeneratorException' => 'PhutilParserGeneratorException', 'PhutilUnreachableTerminalParserGeneratorException' => 'PhutilParserGeneratorException', 'PhutilUrisprintfTestCase' => 'PhutilTestCase', 'PhutilUtilsTestCase' => 'PhutilTestCase', 'PhutilVeryWowEnglishLocale' => 'PhutilLocale', 'PhutilWordPressAuthAdapter' => 'PhutilOAuthAuthAdapter', 'PhutilWordPressFuture' => 'FutureProxy', 'PhutilXHPASTBinary' => 'Phobject', 'PhutilXHPASTSyntaxHighlighter' => 'Phobject', 'PhutilXHPASTSyntaxHighlighterFuture' => 'FutureProxy', 'PhutilXHPASTSyntaxHighlighterTestCase' => 'PhutilTestCase', 'QueryFuture' => 'Future', 'TempFile' => 'Phobject', 'TestAbstractDirectedGraph' => 'AbstractDirectedGraph', 'XHPASTNode' => 'AASTNode', 'XHPASTNodeTestCase' => 'PhutilTestCase', 'XHPASTSyntaxErrorException' => 'Exception', 'XHPASTToken' => 'AASTToken', 'XHPASTTree' => 'AASTTree', 'XHPASTTreeTestCase' => 'PhutilTestCase', 'XsprintfUnknownConversionException' => 'InvalidArgumentException', ), )); diff --git a/src/conduit/ConduitClient.php b/src/conduit/ConduitClient.php index 21c5f52..065a42a 100644 --- a/src/conduit/ConduitClient.php +++ b/src/conduit/ConduitClient.php @@ -1,395 +1,395 @@ connectionID; } public function __construct($uri) { $this->uri = new PhutilURI($uri); if (!strlen($this->uri->getDomain())) { throw new Exception( pht("Conduit URI '%s' must include a valid host.", $uri)); } $this->host = $this->uri->getDomain(); } /** * Override the domain specified in the service URI and provide a specific * host identity. * * This can be used to connect to a specific node in a cluster environment. */ public function setHost($host) { $this->host = $host; return $this; } public function getHost() { return $this->host; } public function setConduitToken($conduit_token) { $this->conduitToken = $conduit_token; return $this; } public function getConduitToken() { return $this->conduitToken; } public function setOAuthToken($oauth_token) { $this->oauthToken = $oauth_token; return $this; } public function callMethodSynchronous($method, array $params) { return $this->callMethod($method, $params)->resolve(); } public function didReceiveResponse($method, $data) { if ($method == 'conduit.connect') { $this->sessionKey = idx($data, 'sessionKey'); $this->connectionID = idx($data, 'connectionID'); } return $data; } public function setTimeout($timeout) { $this->timeout = $timeout; return $this; } public function setSigningKeys( $public_key, PhutilOpaqueEnvelope $private_key) { $this->publicKey = $public_key; $this->privateKey = $private_key; return $this; } public function callMethod($method, array $params) { $meta = array(); if ($this->sessionKey) { $meta['sessionKey'] = $this->sessionKey; } if ($this->connectionID) { $meta['connectionID'] = $this->connectionID; } if ($method == 'conduit.connect') { $certificate = idx($params, 'certificate'); if ($certificate) { $token = time(); $params['authToken'] = $token; $params['authSignature'] = sha1($token.$certificate); } unset($params['certificate']); } if ($this->privateKey && $this->publicKey) { $meta['auth.type'] = self::AUTH_ASYMMETRIC; $meta['auth.key'] = $this->publicKey; $meta['auth.host'] = $this->getHostStringForSignature(); $signature = $this->signRequest($method, $params, $meta); $meta['auth.signature'] = $signature; } if ($this->conduitToken) { $meta['token'] = $this->conduitToken; } if ($this->oauthToken) { $meta['access_token'] = $this->oauthToken; } if ($meta) { $params['__conduit__'] = $meta; } $uri = id(clone $this->uri)->setPath('/api/'.$method); $data = array( 'params' => json_encode($params), 'output' => 'json', // This is a hint to Phabricator that the client expects a Conduit // response. It is not necessary, but provides better error messages in // some cases. '__conduit__' => true, ); // Always use the cURL-based HTTPSFuture, for proxy support and other // protocol edge cases that HTTPFuture does not support. $core_future = new HTTPSFuture($uri, $data); $core_future->addHeader('Host', $this->getHostStringForHeader()); $core_future->setMethod('POST'); $core_future->setTimeout($this->timeout); if ($this->username !== null) { $core_future->setHTTPBasicAuthCredentials( $this->username, $this->password); } return id(new ConduitFuture($core_future)) ->setClient($this, $method); } public function setBasicAuthCredentials($username, $password) { $this->username = $username; $this->password = new PhutilOpaqueEnvelope($password); return $this; } private function getHostStringForHeader() { return $this->newHostString(false); } private function getHostStringForSignature() { return $this->newHostString(true); } /** * Build a string describing the host for this request. * * This method builds strings in two modes: with explicit ports for request * signing (which always include the port number) and with implicit ports * for use in the "Host:" header of requests (which omit the port number if * the port is the same as the default port for the protocol). * * This implicit port behavior is similar to what browsers do, so it is less * likely to get us into trouble with webserver configurations. * * @param bool True to include the port explicitly. * @return string String describing the host for the request. */ private function newHostString($with_explicit_port) { $host = $this->getHost(); $uri = new PhutilURI($this->uri); $protocol = $uri->getProtocol(); $port = $uri->getPort(); $implicit_ports = array( 'https' => 443, ); $default_port = 80; $implicit_port = idx($implicit_ports, $protocol, $default_port); if ($with_explicit_port) { if (!$port) { $port = $implicit_port; } } else { if ($port == $implicit_port) { $port = null; } } if (!$port) { $result = $host; } else { $result = $host.':'.$port; } return $result; } private function signRequest( $method, array $params, array $meta) { $input = self::encodeRequestDataForSignature( $method, $params, $meta); $signature = null; $result = openssl_sign( $input, $signature, $this->privateKey->openEnvelope()); if (!$result) { throw new Exception( pht('Unable to sign Conduit request with signing key.')); } return self::SIGNATURE_CONSIGN_1.base64_encode($signature); } public static function verifySignature( $method, array $params, array $meta, $openssl_public_key) { $auth_type = idx($meta, 'auth.type'); switch ($auth_type) { case self::AUTH_ASYMMETRIC: break; default: throw new Exception( pht( 'Unable to verify request signature, specified "%s" '. '("%s") is unknown.', 'auth.type', $auth_type)); } $public_key = idx($meta, 'auth.key'); if (!strlen($public_key)) { throw new Exception( pht( 'Unable to verify request signature, no "%s" present in '. 'request protocol information.', 'auth.key')); } $signature = idx($meta, 'auth.signature'); if (!strlen($signature)) { throw new Exception( pht( 'Unable to verify request signature, no "%s" present '. 'in request protocol information.', 'auth.signature')); } $prefix = self::SIGNATURE_CONSIGN_1; if (strncmp($signature, $prefix, strlen($prefix)) !== 0) { throw new Exception( pht( 'Unable to verify request signature, signature format is not '. 'known.')); } $signature = substr($signature, strlen($prefix)); $input = self::encodeRequestDataForSignature( $method, $params, $meta); $signature = base64_decode($signature); $trap = new PhutilErrorTrap(); $result = @openssl_verify( $input, $signature, $openssl_public_key); $err = $trap->getErrorsAsString(); $trap->destroy(); if ($result === 1) { // Signature is good. return true; } else if ($result === 0) { // Signature is bad. throw new Exception( pht( 'Request signature verification failed: signature is not correct.')); } else { // Some kind of error. if (strlen($err)) { throw new Exception( pht( 'OpenSSL encountered an error verifying the request signature: %s', $err)); } else { throw new Exception( pht( 'OpenSSL encountered an unknown error verifying the request: %s', $err)); } } } private static function encodeRequestDataForSignature( $method, array $params, array $meta) { unset($meta['auth.signature']); $structure = array( 'method' => $method, 'protocol' => $meta, 'parameters' => $params, ); return self::encodeRawDataForSignature($structure); } public static function encodeRawDataForSignature($data) { $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[] = ':'; foreach ($data as $value) { $out[] = self::encodeRawDataForSignature($value); } } else { ksort($data); $out[] = 'O'; $out[] = count($data); $out[] = ':'; foreach ($data as $key => $value) { $out[] = self::encodeRawDataForSignature($key); $out[] = self::encodeRawDataForSignature($value); } } } else if (is_string($data)) { $out[] = 'S'; $out[] = strlen($data); $out[] = ':'; $out[] = $data; } else if (is_int($data)) { $out[] = 'I'; $out[] = strlen((string)$data); $out[] = ':'; $out[] = (string)$data; } else if (is_null($data)) { $out[] = 'N'; $out[] = ':'; } else if ($data === true) { $out[] = 'B1:'; } else if ($data === false) { $out[] = 'B0:'; } else { throw new Exception( pht( 'Unexpected data type in request data: %s.', gettype($data))); } return implode('', $out); } } diff --git a/src/moduleutils/PhutilBootloader.php b/src/moduleutils/PhutilBootloader.php index a090692..394f6ed 100644 --- a/src/moduleutils/PhutilBootloader.php +++ b/src/moduleutils/PhutilBootloader.php @@ -1,338 +1,347 @@ classTree; } public function registerInMemoryLibrary($name, $map) { $this->registeredLibraries[$name] = "memory:$name"; $this->inMemoryMaps[$name] = $map; $this->getLibraryMap($name); } public function registerLibrary($name, $path) { if (basename($path) != '__phutil_library_init__.php') { throw new PhutilBootloaderException( 'Only directories with a __phutil_library_init__.php file may be '. 'registered as libphutil libraries.'); } $path = dirname($path); // Detect attempts to load the same library multiple times from different // locations. This might mean you're doing something silly like trying to // include two different versions of something, or it might mean you're // doing something subtle like running a different version of 'arc' on a // working copy of Arcanist. if (isset($this->registeredLibraries[$name])) { $old_path = $this->registeredLibraries[$name]; if ($old_path != $path) { throw new PhutilLibraryConflictException($name, $old_path, $path); } } $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)) { $root = $this->getLibraryRoot('phutil'); $this->executeInclude($root.'/symbols/PhutilSymbolLoader.php'); } $loader = new PhutilSymbolLoader(); $loader ->setLibrary($name) ->setType('function'); try { $loader->selectAndLoadSymbols(); } catch (PhutilBootloaderException $ex) { // Ignore this, it happens if a global function's file is removed or // similar. Worst case is that we fatal when calling the function, which // is no worse than fataling here. } catch (PhutilMissingSymbolException $ex) { // Ignore this, it happens if a global function is removed. Everything // else loaded so proceed forward: worst case is a fatal when we // hit a function call to a function which no longer exists, which is // no worse than fataling here. } if (empty($_SERVER['PHUTIL_DISABLE_RUNTIME_EXTENSIONS'])) { $extdir = $path.DIRECTORY_SEPARATOR.'extensions'; if (Filesystem::pathExists($extdir)) { $extensions = id(new FileFinder($extdir)) ->withSuffix('php') ->withType('f') ->withFollowSymlinks(true) ->setForceMode('php') ->find(); foreach ($extensions as $extension) { $this->loadExtension( $name, $path, $extdir.DIRECTORY_SEPARATOR.$extension); } } } return $this; } public function registerLibraryMap(array $map) { $this->libraryMaps[$this->currentLibrary] = $map; return $this; } public function getLibraryMap($name) { if (isset($this->extendedMaps[$name])) { return $this->extendedMaps[$name]; } if (empty($this->libraryMaps[$name])) { $root = $this->getLibraryRoot($name); $this->currentLibrary = $name; if (isset($this->inMemoryMaps[$name])) { $this->libraryMaps[$name] = $this->inMemoryMaps[$name]; } else { $okay = include $root.'/__phutil_library_map__.php'; if (!$okay) { throw new PhutilBootloaderException( "Include of '{$root}/__phutil_library_map__.php' failed!"); } } $map = $this->libraryMaps[$name]; $version = isset($map['__library_version__']) ? $map['__library_version__'] : 1; switch ($version) { case 1: throw new Exception( 'libphutil v1 libraries are no longer supported.'); case 2: // NOTE: In version 2 of the library format, all parents (both // classes and interfaces) are stored in the 'xmap'. The value is // either a string for a single parent (the common case) or an array // for multiple parents. foreach ($map['xmap'] as $child => $parents) { foreach ((array)$parents as $parent) { $this->classTree[$parent][] = $child; } } break; default: throw new Exception("Unsupported library version '{$version}'!"); } } $map = $this->libraryMaps[$name]; // If there's an extension map for this library, merge the maps. if (isset($this->extensionMaps[$name])) { $emap = $this->extensionMaps[$name]; foreach (array('function', 'class', 'xmap') as $dict_key) { if (!isset($emap[$dict_key])) { continue; } $map[$dict_key] += $emap[$dict_key]; } } $this->extendedMaps[$name] = $map; return $map; } public function getLibraryMapWithoutExtensions($name) { // This just does all the checks to make sure the library is valid, then // we throw away the result. $this->getLibraryMap($name); return $this->libraryMaps[$name]; } public function getLibraryRoot($name) { if (empty($this->registeredLibraries[$name])) { throw new PhutilBootloaderException( "The phutil library '{$name}' has not been loaded!"); } return $this->registeredLibraries[$name]; } public function getAllLibraries() { return array_keys($this->registeredLibraries); } public function loadLibrary($path) { $root = null; if (!empty($_SERVER['PHUTIL_LIBRARY_ROOT'])) { if ($path[0] != '/') { $root = $_SERVER['PHUTIL_LIBRARY_ROOT']; } } $this->executeInclude($root.$path.'/__phutil_library_init__.php'); } public function loadLibrarySource($library, $source) { $path = $this->getLibraryRoot($library).'/'.$source; $this->executeInclude($path); } private function executeInclude($path) { // Include the source using `include_once`, but convert any warnings or // recoverable errors into exceptions. // Some messages, including "Declaration of X should be compatible with Y", // do not cause `include_once` to return an error code. Use // error_get_last() to make sure we're catching everything in every PHP // version. // (Also, the severity of some messages changed between versions of PHP.) // Note that we may enter this method after some earlier, unrelated error. // In this case, error_get_last() will return information for that error. // In PHP7 and later we could use error_clear_last() to clear that error, // but the function does not exist in earlier versions of PHP. Instead, // check if the value has changed. // Some parser-like errors, including "class must implement all abstract // methods", cause PHP to fatal immediately with an E_ERROR. In these // cases, include_once() does not throw and never returns. We leave // reporting enabled for these errors since we don't have a way to do // anything more graceful. // Likewise, some errors, including "cannot redeclare Class::method()" // cause PHP to fatal immediately with E_COMPILE_ERROR. Treat these like // the similar errors which raise E_ERROR. // See also T12190. $old_last = error_get_last(); try { $old = error_reporting(E_ERROR | E_COMPILE_ERROR); $okay = include_once $path; error_reporting($old); } catch (Exception $ex) { throw $ex; } catch (ParseError $throwable) { // NOTE: As of PHP7, syntax errors may raise a ParseError (which is a // Throwable, not an Exception) with a useless message (like "syntax // error, unexpected ':'") and a trace which ends a level above this. // Treating this object normally results in an unusable message which // does not identify where the syntax error occurred. Converting it to // a string and taking the first line gives us something reasonable, // however. $message = (string)$throwable; $message = preg_split("/\n/", $message); $message = reset($message); throw new Exception($message); } if (!$okay) { throw new Exception("Source file \"{$path}\" failed to load."); } $new_last = error_get_last(); if ($new_last !== null) { if ($new_last !== $old_last) { $message = $new_last['message']; throw new Exception( "Error while loading file \"{$path}\": {$message}"); } } } private function loadExtension($library, $root, $path) { $old_functions = get_defined_functions(); $old_functions = array_fill_keys($old_functions['user'], true); $old_classes = array_fill_keys(get_declared_classes(), true); $old_interfaces = array_fill_keys(get_declared_interfaces(), true); $this->executeInclude($path); $new_functions = get_defined_functions(); $new_functions = array_fill_keys($new_functions['user'], true); $new_classes = array_fill_keys(get_declared_classes(), true); $new_interfaces = array_fill_keys(get_declared_interfaces(), true); $add_functions = array_diff_key($new_functions, $old_functions); $add_classes = array_diff_key($new_classes, $old_classes); $add_interfaces = array_diff_key($new_interfaces, $old_interfaces); // NOTE: We can't trust the path we loaded to be the location of these // symbols, because it might have loaded other paths. foreach ($add_functions as $func => $ignored) { $rfunc = new ReflectionFunction($func); $fpath = Filesystem::resolvePath($rfunc->getFileName(), $root); $this->extensionMaps[$library]['function'][$func] = $fpath; } foreach ($add_classes + $add_interfaces as $class => $ignored) { $rclass = new ReflectionClass($class); $cpath = Filesystem::resolvePath($rclass->getFileName(), $root); $this->extensionMaps[$library]['class'][$class] = $cpath; $xmap = $rclass->getInterfaceNames(); $parent = $rclass->getParentClass(); if ($parent) { $xmap[] = $parent->getName(); } if ($xmap) { foreach ($xmap as $parent_class) { $this->classTree[$parent_class][] = $class; } if (count($xmap) == 1) { $xmap = head($xmap); } $this->extensionMaps[$library]['xmap'][$class] = $xmap; } } // Clear the extended library cache (should one exist) so we know that // we need to rebuild it. unset($this->extendedMaps[$library]); } } diff --git a/src/parser/PhutilJSON.php b/src/parser/PhutilJSON.php index bc9e5b6..0bcc4f7 100644 --- a/src/parser/PhutilJSON.php +++ b/src/parser/PhutilJSON.php @@ -1,155 +1,155 @@ encodeFormattedObject($object, 0)."\n"; } /** * Encode a list in JSON and pretty-print it, discarding keys. * * @param list List to encode in JSON. * @return string Pretty-printed list representation. */ public function encodeAsList(array $list) { return $this->encodeFormattedArray($list, 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.$key.': '.$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)) { + if (phutil_is_natural_list($value)) { return $this->encodeFormattedArray($value, $depth); } else { return $this->encodeFormattedObject($value, $depth); } } else { if (defined('JSON_UNESCAPED_SLASHES')) { // If we have a new enough version of PHP, disable escaping of slashes // when pretty-printing values. Escaping slashes can defuse an attack // where the attacker embeds "" inside a JSON string, but that // isn't relevant when rendering JSON for human viewers. return json_encode($value, JSON_UNESCAPED_SLASHES); } 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/PhutilTypeSpec.php b/src/parser/PhutilTypeSpec.php index dbf3b4f..c49fccf 100644 --- a/src/parser/PhutilTypeSpec.php +++ b/src/parser/PhutilTypeSpec.php @@ -1,1921 +1,1921 @@ * map * type|type * * A type may be marked as optional by suffixing it with "?" or prefixing it * with the word "optional": * * int? * optional int * * A type may have a human-readable comment in parentheses, at the end: * * int (must be even) * * For example, these are valid type specifications: * * int|string * map * list> * optional int * string (uppercase) * */ final class PhutilTypeSpec extends Phobject { private $type; private $subtypes = array(); private $optional; private $comment; private function __construct() {} public function getType() { return $this->type; } public function check($value, $name = null) { switch ($this->type) { case 'int': if (!is_int($value)) { throw new PhutilTypeCheckException($this, $value, $name); } break; case 'float': if (!is_float($value)) { throw new PhutilTypeCheckException($this, $value, $name); } break; case 'bool': if (!is_bool($value)) { throw new PhutilTypeCheckException($this, $value, $name); } break; case 'string': if (!is_string($value)) { throw new PhutilTypeCheckException($this, $value, $name); } break; case 'regex': $trap = new PhutilErrorTrap(); $ok = @preg_match($value, ''); $err = $trap->getErrorsAsString(); $trap->destroy(); if ($ok === false) { throw new PhutilTypeCheckException($this, $value, $name, $err); } break; case 'null': if (!is_null($value)) { throw new PhutilTypeCheckException($this, $value, $name); } break; case 'list': 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 { foreach ($value as $v) { $this->subtypes[0]->check($v); } } catch (PhutilTypeCheckException $ex) { throw new PhutilTypeCheckException($this, $value, $name); } break; case 'map': if (!is_array($value)) { throw new PhutilTypeCheckException($this, $value, $name); } try { foreach ($value as $k => $v) { $this->subtypes[0]->check($k); $this->subtypes[1]->check($v); } } catch (PhutilTypeCheckException $ex) { throw new PhutilTypeCheckException($this, $value, $name); } break; case 'or': foreach ($this->subtypes as $subtype) { try { $subtype->check($value); return; } catch (PhutilTypeCheckException $ex) { // Ignore. } } throw new PhutilTypeCheckException($this, $value, $name); case 'wild': return; default: if (class_exists($this->type, false)) { if ($value instanceof $this->type) { return; } } else if (interface_exists($this->type, false)) { if ($value instanceof $this->type) { return; } } throw new PhutilTypeCheckException($this, $value, $name); } } public static function checkMap(array $values, array $types) { $extra = array_diff_key($values, $types); if ($extra) { throw new PhutilTypeExtraParametersException($extra); } $missing = array(); foreach ($types as $key => $type) { $types[$key] = self::newFromString($type); if (!array_key_exists($key, $values)) { if (!$types[$key]->optional) { $missing[] = $key; } } } if ($missing) { throw new PhutilTypeMissingParametersException($missing); } foreach ($types as $key => $type) { if (array_key_exists($key, $values)) { $type->check($values[$key], $key); } } } public static function getCommonParentClass($class_a, $class_b) { // Make sure both classes are really classes. try { if (!class_exists($class_a) || !class_exists($class_b)) { return null; } } catch (PhutilMissingSymbolException $ex) { return null; } $ancestors_a = array(); do { $ancestors_a[] = $class_a; } while ($class_a = get_parent_class($class_a)); $ancestors_b = array(); do { $ancestors_b[] = $class_b; } while ($class_b = get_parent_class($class_b)); return head(array_intersect($ancestors_a, $ancestors_b)); } public static function getTypeOf($value) { if (is_int($value)) { return 'int'; } else if (is_float($value)) { return 'float'; } else if (is_bool($value)) { return 'bool'; } else if (is_string($value)) { return 'string'; } else if (is_null($value)) { return 'null'; } else if (is_object($value)) { 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)); return "map<{$ktype}, {$vtype}>"; } } else { return 'wild'; } } private static function getTypeOfVector(array $vector) { if (!$vector) { return 'wild'; } $type = null; foreach ($vector as $value) { $vtype = self::getTypeOf($value); if ($type === null) { $type = $vtype; } else if ($type === $vtype) { continue; } else { $parent = self::getCommonParentClass($type, $vtype); if ($parent) { $type = $parent; } else { return 'wild'; } } } return $type; } public function toString() { $sub = array(); foreach ($this->subtypes as $subtype) { $sub[] = $subtype->toString(); } switch ($this->type) { case 'map': $string = 'map<'.$sub[0].', '.$sub[1].'>'; break; case 'list': $string = 'list<'.$sub[0].'>'; break; case 'or': $string = implode('|', $sub); break; default: $string = $this->type; break; } if ($this->optional) { $string = 'optional '.$string; } if ($this->comment) { $string .= ' ('.$this->comment.')'; } return $string; } public static function newFromString($string) { $lexer = self::getLexer(); $tokens = $lexer->getTokens($string); // Strip whitespace tokens. foreach ($tokens as $key => $token) { $type = $token[0]; if ($type == ' ') { unset($tokens[$key]); } } $tokens = array_values($tokens); $callback = array(__CLASS__, 'didReduceTokens'); return self::parseTokens($tokens, $callback); } public static function didReduceTokens($rule, $production, array $tokens) { switch ($rule) { case 'start': case 'some_type': case 'not_or_type': return $tokens[0]; case 'type': if ($production == 'yes') { $tokens[0]->optional = true; } return $tokens[0]; case 'basic_type': $obj = new PhutilTypeSpec(); $obj->type = $tokens[0][1]; return $obj; case 'or_type': $l = $tokens[0]; $r = $tokens[2]; if ($l->type == 'or') { if ($r->type == 'or') { foreach ($r->subtypes as $subtype) { $l->subtypes[] = $subtype; } } else { $l->subtypes[] = $r; } return $l; } else if ($r->type == 'or') { $r->subtypes[] = $l; return $r; } else { $obj = new PhutilTypeSpec(); $obj->type = 'or'; $obj->subtypes[] = $l; $obj->subtypes[] = $r; return $obj; } break; case 'map_type': $obj = new PhutilTypeSpec(); $obj->type = 'map'; $obj->subtypes[] = $tokens[2]; $obj->subtypes[] = $tokens[4]; return $obj; case 'list_type': $obj = new PhutilTypeSpec(); $obj->type = 'list'; $obj->subtypes[] = $tokens[2]; return $obj; case 'maybe_optional': if ($production == 'yes') { $tokens[1]->optional = true; return $tokens[1]; } else { return $tokens[0]; } break; case 'maybe_comment': if ($production == 'yes') { $tokens[0]->comment = $tokens[1]; } return $tokens[0]; case 'comment': return $tokens[1]; case 'comment_text': $result = ''; foreach ($tokens as $token) { if (is_array($token)) { $result .= $token[1]; } else { $result .= $token; } } return $result; default: throw new Exception(pht("Unhandled parser rule '%s'!", $rule)); } } private static function getLexer() { static $lexer; if (!$lexer) { $lexer = new PhutilTypeLexer(); } return $lexer; } private static function parseTokens(array $tokens, $callback) { // NOTE: This is automatically generated by the script // `support/parser/generate-type-parser.php`. return PhutilParserGenerator::parseTokensWithTables( array( 0 => array( 'opt' => array( 0 => 'S', 1 => 3, ), 'k' => array( 0 => 'S', 1 => 20, ), 'map' => array( 0 => 'S', 1 => 21, ), 'list' => array( 0 => 'S', 1 => 71, ), ), 1 => array( '(end-of-file)' => array( 0 => 'A', ), ), 2 => array( '(end-of-file)' => array( 0 => 'R', 1 => array( 0 => 'start', 1 => 0, 2 => 1, ), ), ), 3 => array( 'k' => array( 0 => 'S', 1 => 20, ), 'map' => array( 0 => 'S', 1 => 21, ), 'list' => array( 0 => 'S', 1 => 71, ), ), 4 => array( '(end-of-file)' => array( 0 => 'R', 1 => array( 0 => 'maybe_optional', 1 => 'yes', 2 => 2, ), ), ), 5 => array( '(end-of-file)' => array( 0 => 'R', 1 => array( 0 => 'maybe_comment', 1 => 'no', 2 => 1, ), ), '(' => array( 0 => 'S', 1 => 7, ), ), 6 => array( '(end-of-file)' => array( 0 => 'R', 1 => array( 0 => 'maybe_comment', 1 => 'yes', 2 => 2, ), ), ), 7 => array( 'cm' => array( 0 => 'S', 1 => 11, ), ), 8 => array( ')' => array( 0 => 'S', 1 => 9, ), 'cm' => array( 0 => 'S', 1 => 10, ), ), 9 => array( '(end-of-file)' => array( 0 => 'R', 1 => array( 0 => 'comment', 1 => 0, 2 => 3, ), ), ), 10 => array( ')' => array( 0 => 'R', 1 => array( 0 => 'comment_text', 1 => 0, 2 => 2, ), ), 'cm' => array( 0 => 'R', 1 => array( 0 => 'comment_text', 1 => 0, 2 => 2, ), ), ), 11 => array( ')' => array( 0 => 'R', 1 => array( 0 => 'comment_text', 1 => 1, 2 => 1, ), ), 'cm' => array( 0 => 'R', 1 => array( 0 => 'comment_text', 1 => 1, 2 => 1, ), ), ), 12 => array( '(' => array( 0 => 'R', 1 => array( 0 => 'type', 1 => 'no', 2 => 1, ), ), '(end-of-file)' => array( 0 => 'R', 1 => array( 0 => 'type', 1 => 'no', 2 => 1, ), ), '?' => array( 0 => 'S', 1 => 13, ), ), 13 => array( '(' => array( 0 => 'R', 1 => array( 0 => 'type', 1 => 'yes', 2 => 2, ), ), '(end-of-file)' => array( 0 => 'R', 1 => array( 0 => 'type', 1 => 'yes', 2 => 2, ), ), ), 14 => array( '?' => array( 0 => 'R', 1 => array( 0 => 'some_type', 1 => 0, 2 => 1, ), ), '(' => array( 0 => 'R', 1 => array( 0 => 'some_type', 1 => 0, 2 => 1, ), ), '(end-of-file)' => array( 0 => 'R', 1 => array( 0 => 'some_type', 1 => 0, 2 => 1, ), ), '|' => array( 0 => 'S', 1 => 15, ), ), 15 => array( 'k' => array( 0 => 'S', 1 => 20, ), 'map' => array( 0 => 'S', 1 => 21, ), 'list' => array( 0 => 'S', 1 => 71, ), ), 16 => array( '?' => array( 0 => 'R', 1 => array( 0 => 'or_type', 1 => 0, 2 => 3, ), ), '(' => array( 0 => 'R', 1 => array( 0 => 'or_type', 1 => 0, 2 => 3, ), ), '(end-of-file)' => array( 0 => 'R', 1 => array( 0 => 'or_type', 1 => 0, 2 => 3, ), ), '|' => array( 0 => 'R', 1 => array( 0 => 'or_type', 1 => 0, 2 => 3, ), ), ), 17 => array( '?' => array( 0 => 'R', 1 => array( 0 => 'not_or_type', 1 => 0, 2 => 1, ), ), '(' => array( 0 => 'R', 1 => array( 0 => 'not_or_type', 1 => 0, 2 => 1, ), ), '(end-of-file)' => array( 0 => 'R', 1 => array( 0 => 'not_or_type', 1 => 0, 2 => 1, ), ), '|' => array( 0 => 'R', 1 => array( 0 => 'not_or_type', 1 => 0, 2 => 1, ), ), ), 18 => array( '?' => array( 0 => 'R', 1 => array( 0 => 'not_or_type', 1 => 1, 2 => 1, ), ), '(' => array( 0 => 'R', 1 => array( 0 => 'not_or_type', 1 => 1, 2 => 1, ), ), '(end-of-file)' => array( 0 => 'R', 1 => array( 0 => 'not_or_type', 1 => 1, 2 => 1, ), ), '|' => array( 0 => 'R', 1 => array( 0 => 'not_or_type', 1 => 1, 2 => 1, ), ), ), 19 => array( '?' => array( 0 => 'R', 1 => array( 0 => 'not_or_type', 1 => 2, 2 => 1, ), ), '(' => array( 0 => 'R', 1 => array( 0 => 'not_or_type', 1 => 2, 2 => 1, ), ), '(end-of-file)' => array( 0 => 'R', 1 => array( 0 => 'not_or_type', 1 => 2, 2 => 1, ), ), '|' => array( 0 => 'R', 1 => array( 0 => 'not_or_type', 1 => 2, 2 => 1, ), ), ), 20 => array( '?' => array( 0 => 'R', 1 => array( 0 => 'basic_type', 1 => 0, 2 => 1, ), ), '(' => array( 0 => 'R', 1 => array( 0 => 'basic_type', 1 => 0, 2 => 1, ), ), '(end-of-file)' => array( 0 => 'R', 1 => array( 0 => 'basic_type', 1 => 0, 2 => 1, ), ), '|' => array( 0 => 'R', 1 => array( 0 => 'basic_type', 1 => 0, 2 => 1, ), ), ), 21 => array( '<' => array( 0 => 'S', 1 => 22, ), ), 22 => array( 'k' => array( 0 => 'S', 1 => 57, ), 'map' => array( 0 => 'S', 1 => 58, ), 'list' => array( 0 => 'S', 1 => 67, ), ), 23 => array( ',' => array( 0 => 'S', 1 => 24, ), ), 24 => array( 'k' => array( 0 => 'S', 1 => 35, ), 'map' => array( 0 => 'S', 1 => 36, ), 'list' => array( 0 => 'S', 1 => 45, ), ), 25 => array( '>' => array( 0 => 'S', 1 => 26, ), ), 26 => array( '?' => array( 0 => 'R', 1 => array( 0 => 'map_type', 1 => 0, 2 => 6, ), ), '(' => array( 0 => 'R', 1 => array( 0 => 'map_type', 1 => 0, 2 => 6, ), ), '(end-of-file)' => array( 0 => 'R', 1 => array( 0 => 'map_type', 1 => 0, 2 => 6, ), ), '|' => array( 0 => 'R', 1 => array( 0 => 'map_type', 1 => 0, 2 => 6, ), ), ), 27 => array( '>' => array( 0 => 'R', 1 => array( 0 => 'type', 1 => 'no', 2 => 1, ), ), '?' => array( 0 => 'S', 1 => 28, ), ), 28 => array( '>' => array( 0 => 'R', 1 => array( 0 => 'type', 1 => 'yes', 2 => 2, ), ), ), 29 => array( '?' => array( 0 => 'R', 1 => array( 0 => 'some_type', 1 => 0, 2 => 1, ), ), '>' => array( 0 => 'R', 1 => array( 0 => 'some_type', 1 => 0, 2 => 1, ), ), '|' => array( 0 => 'S', 1 => 30, ), ), 30 => array( 'k' => array( 0 => 'S', 1 => 35, ), 'map' => array( 0 => 'S', 1 => 36, ), 'list' => array( 0 => 'S', 1 => 45, ), ), 31 => array( '?' => array( 0 => 'R', 1 => array( 0 => 'or_type', 1 => 0, 2 => 3, ), ), '>' => array( 0 => 'R', 1 => array( 0 => 'or_type', 1 => 0, 2 => 3, ), ), '|' => array( 0 => 'R', 1 => array( 0 => 'or_type', 1 => 0, 2 => 3, ), ), ), 32 => array( '?' => array( 0 => 'R', 1 => array( 0 => 'not_or_type', 1 => 0, 2 => 1, ), ), '>' => array( 0 => 'R', 1 => array( 0 => 'not_or_type', 1 => 0, 2 => 1, ), ), '|' => array( 0 => 'R', 1 => array( 0 => 'not_or_type', 1 => 0, 2 => 1, ), ), ), 33 => array( '?' => array( 0 => 'R', 1 => array( 0 => 'not_or_type', 1 => 1, 2 => 1, ), ), '>' => array( 0 => 'R', 1 => array( 0 => 'not_or_type', 1 => 1, 2 => 1, ), ), '|' => array( 0 => 'R', 1 => array( 0 => 'not_or_type', 1 => 1, 2 => 1, ), ), ), 34 => array( '?' => array( 0 => 'R', 1 => array( 0 => 'not_or_type', 1 => 2, 2 => 1, ), ), '>' => array( 0 => 'R', 1 => array( 0 => 'not_or_type', 1 => 2, 2 => 1, ), ), '|' => array( 0 => 'R', 1 => array( 0 => 'not_or_type', 1 => 2, 2 => 1, ), ), ), 35 => array( '?' => array( 0 => 'R', 1 => array( 0 => 'basic_type', 1 => 0, 2 => 1, ), ), '>' => array( 0 => 'R', 1 => array( 0 => 'basic_type', 1 => 0, 2 => 1, ), ), '|' => array( 0 => 'R', 1 => array( 0 => 'basic_type', 1 => 0, 2 => 1, ), ), ), 36 => array( '<' => array( 0 => 'S', 1 => 37, ), ), 37 => array( 'k' => array( 0 => 'S', 1 => 57, ), 'map' => array( 0 => 'S', 1 => 58, ), 'list' => array( 0 => 'S', 1 => 67, ), ), 38 => array( ',' => array( 0 => 'S', 1 => 39, ), ), 39 => array( 'k' => array( 0 => 'S', 1 => 35, ), 'map' => array( 0 => 'S', 1 => 36, ), 'list' => array( 0 => 'S', 1 => 45, ), ), 40 => array( '>' => array( 0 => 'S', 1 => 41, ), ), 41 => array( '?' => array( 0 => 'R', 1 => array( 0 => 'map_type', 1 => 0, 2 => 6, ), ), '>' => array( 0 => 'R', 1 => array( 0 => 'map_type', 1 => 0, 2 => 6, ), ), '|' => array( 0 => 'R', 1 => array( 0 => 'map_type', 1 => 0, 2 => 6, ), ), ), 42 => array( '?' => array( 0 => 'R', 1 => array( 0 => 'some_type', 1 => 1, 2 => 1, ), ), '>' => array( 0 => 'R', 1 => array( 0 => 'some_type', 1 => 1, 2 => 1, ), ), '|' => array( 0 => 'S', 1 => 43, ), ), 43 => array( 'k' => array( 0 => 'S', 1 => 35, ), 'map' => array( 0 => 'S', 1 => 36, ), 'list' => array( 0 => 'S', 1 => 45, ), ), 44 => array( '?' => array( 0 => 'R', 1 => array( 0 => 'or_type', 1 => 1, 2 => 3, ), ), '>' => array( 0 => 'R', 1 => array( 0 => 'or_type', 1 => 1, 2 => 3, ), ), '|' => array( 0 => 'R', 1 => array( 0 => 'or_type', 1 => 1, 2 => 3, ), ), ), 45 => array( '<' => array( 0 => 'S', 1 => 46, ), ), 46 => array( 'k' => array( 0 => 'S', 1 => 35, ), 'map' => array( 0 => 'S', 1 => 36, ), 'list' => array( 0 => 'S', 1 => 45, ), ), 47 => array( '>' => array( 0 => 'S', 1 => 48, ), ), 48 => array( '?' => array( 0 => 'R', 1 => array( 0 => 'list_type', 1 => 0, 2 => 4, ), ), '>' => array( 0 => 'R', 1 => array( 0 => 'list_type', 1 => 0, 2 => 4, ), ), '|' => array( 0 => 'R', 1 => array( 0 => 'list_type', 1 => 0, 2 => 4, ), ), ), 49 => array( ',' => array( 0 => 'R', 1 => array( 0 => 'type', 1 => 'no', 2 => 1, ), ), '?' => array( 0 => 'S', 1 => 50, ), ), 50 => array( ',' => array( 0 => 'R', 1 => array( 0 => 'type', 1 => 'yes', 2 => 2, ), ), ), 51 => array( '?' => array( 0 => 'R', 1 => array( 0 => 'some_type', 1 => 0, 2 => 1, ), ), ',' => array( 0 => 'R', 1 => array( 0 => 'some_type', 1 => 0, 2 => 1, ), ), '|' => array( 0 => 'S', 1 => 52, ), ), 52 => array( 'k' => array( 0 => 'S', 1 => 57, ), 'map' => array( 0 => 'S', 1 => 58, ), 'list' => array( 0 => 'S', 1 => 67, ), ), 53 => array( '?' => array( 0 => 'R', 1 => array( 0 => 'or_type', 1 => 0, 2 => 3, ), ), ',' => array( 0 => 'R', 1 => array( 0 => 'or_type', 1 => 0, 2 => 3, ), ), '|' => array( 0 => 'R', 1 => array( 0 => 'or_type', 1 => 0, 2 => 3, ), ), ), 54 => array( '?' => array( 0 => 'R', 1 => array( 0 => 'not_or_type', 1 => 0, 2 => 1, ), ), ',' => array( 0 => 'R', 1 => array( 0 => 'not_or_type', 1 => 0, 2 => 1, ), ), '|' => array( 0 => 'R', 1 => array( 0 => 'not_or_type', 1 => 0, 2 => 1, ), ), ), 55 => array( '?' => array( 0 => 'R', 1 => array( 0 => 'not_or_type', 1 => 1, 2 => 1, ), ), ',' => array( 0 => 'R', 1 => array( 0 => 'not_or_type', 1 => 1, 2 => 1, ), ), '|' => array( 0 => 'R', 1 => array( 0 => 'not_or_type', 1 => 1, 2 => 1, ), ), ), 56 => array( '?' => array( 0 => 'R', 1 => array( 0 => 'not_or_type', 1 => 2, 2 => 1, ), ), ',' => array( 0 => 'R', 1 => array( 0 => 'not_or_type', 1 => 2, 2 => 1, ), ), '|' => array( 0 => 'R', 1 => array( 0 => 'not_or_type', 1 => 2, 2 => 1, ), ), ), 57 => array( '?' => array( 0 => 'R', 1 => array( 0 => 'basic_type', 1 => 0, 2 => 1, ), ), ',' => array( 0 => 'R', 1 => array( 0 => 'basic_type', 1 => 0, 2 => 1, ), ), '|' => array( 0 => 'R', 1 => array( 0 => 'basic_type', 1 => 0, 2 => 1, ), ), ), 58 => array( '<' => array( 0 => 'S', 1 => 59, ), ), 59 => array( 'k' => array( 0 => 'S', 1 => 57, ), 'map' => array( 0 => 'S', 1 => 58, ), 'list' => array( 0 => 'S', 1 => 67, ), ), 60 => array( ',' => array( 0 => 'S', 1 => 61, ), ), 61 => array( 'k' => array( 0 => 'S', 1 => 35, ), 'map' => array( 0 => 'S', 1 => 36, ), 'list' => array( 0 => 'S', 1 => 45, ), ), 62 => array( '>' => array( 0 => 'S', 1 => 63, ), ), 63 => array( '?' => array( 0 => 'R', 1 => array( 0 => 'map_type', 1 => 0, 2 => 6, ), ), ',' => array( 0 => 'R', 1 => array( 0 => 'map_type', 1 => 0, 2 => 6, ), ), '|' => array( 0 => 'R', 1 => array( 0 => 'map_type', 1 => 0, 2 => 6, ), ), ), 64 => array( '?' => array( 0 => 'R', 1 => array( 0 => 'some_type', 1 => 1, 2 => 1, ), ), ',' => array( 0 => 'R', 1 => array( 0 => 'some_type', 1 => 1, 2 => 1, ), ), '|' => array( 0 => 'S', 1 => 65, ), ), 65 => array( 'k' => array( 0 => 'S', 1 => 57, ), 'map' => array( 0 => 'S', 1 => 58, ), 'list' => array( 0 => 'S', 1 => 67, ), ), 66 => array( '?' => array( 0 => 'R', 1 => array( 0 => 'or_type', 1 => 1, 2 => 3, ), ), ',' => array( 0 => 'R', 1 => array( 0 => 'or_type', 1 => 1, 2 => 3, ), ), '|' => array( 0 => 'R', 1 => array( 0 => 'or_type', 1 => 1, 2 => 3, ), ), ), 67 => array( '<' => array( 0 => 'S', 1 => 68, ), ), 68 => array( 'k' => array( 0 => 'S', 1 => 35, ), 'map' => array( 0 => 'S', 1 => 36, ), 'list' => array( 0 => 'S', 1 => 45, ), ), 69 => array( '>' => array( 0 => 'S', 1 => 70, ), ), 70 => array( '?' => array( 0 => 'R', 1 => array( 0 => 'list_type', 1 => 0, 2 => 4, ), ), ',' => array( 0 => 'R', 1 => array( 0 => 'list_type', 1 => 0, 2 => 4, ), ), '|' => array( 0 => 'R', 1 => array( 0 => 'list_type', 1 => 0, 2 => 4, ), ), ), 71 => array( '<' => array( 0 => 'S', 1 => 72, ), ), 72 => array( 'k' => array( 0 => 'S', 1 => 35, ), 'map' => array( 0 => 'S', 1 => 36, ), 'list' => array( 0 => 'S', 1 => 45, ), ), 73 => array( '>' => array( 0 => 'S', 1 => 74, ), ), 74 => array( '?' => array( 0 => 'R', 1 => array( 0 => 'list_type', 1 => 0, 2 => 4, ), ), '(' => array( 0 => 'R', 1 => array( 0 => 'list_type', 1 => 0, 2 => 4, ), ), '(end-of-file)' => array( 0 => 'R', 1 => array( 0 => 'list_type', 1 => 0, 2 => 4, ), ), '|' => array( 0 => 'R', 1 => array( 0 => 'list_type', 1 => 0, 2 => 4, ), ), ), 75 => array( '?' => array( 0 => 'R', 1 => array( 0 => 'some_type', 1 => 1, 2 => 1, ), ), '(' => array( 0 => 'R', 1 => array( 0 => 'some_type', 1 => 1, 2 => 1, ), ), '(end-of-file)' => array( 0 => 'R', 1 => array( 0 => 'some_type', 1 => 1, 2 => 1, ), ), '|' => array( 0 => 'S', 1 => 76, ), ), 76 => array( 'k' => array( 0 => 'S', 1 => 20, ), 'map' => array( 0 => 'S', 1 => 21, ), 'list' => array( 0 => 'S', 1 => 71, ), ), 77 => array( '?' => array( 0 => 'R', 1 => array( 0 => 'or_type', 1 => 1, 2 => 3, ), ), '(' => array( 0 => 'R', 1 => array( 0 => 'or_type', 1 => 1, 2 => 3, ), ), '(end-of-file)' => array( 0 => 'R', 1 => array( 0 => 'or_type', 1 => 1, 2 => 3, ), ), '|' => array( 0 => 'R', 1 => array( 0 => 'or_type', 1 => 1, 2 => 3, ), ), ), 78 => array( '(end-of-file)' => array( 0 => 'R', 1 => array( 0 => 'maybe_optional', 1 => 'no', 2 => 1, ), ), ), ), array( 0 => array( 'start' => 1, 'maybe_optional' => 2, 'maybe_comment' => 78, 'type' => 5, 'some_type' => 12, 'or_type' => 14, 'not_or_type' => 75, 'basic_type' => 17, 'map_type' => 18, 'list_type' => 19, ), 3 => array( 'maybe_comment' => 4, 'type' => 5, 'some_type' => 12, 'or_type' => 14, 'not_or_type' => 75, 'basic_type' => 17, 'map_type' => 18, 'list_type' => 19, ), 5 => array( 'comment' => 6, ), 7 => array( 'comment_text' => 8, ), 15 => array( 'not_or_type' => 16, 'basic_type' => 17, 'map_type' => 18, 'list_type' => 19, ), 22 => array( 'type' => 23, 'some_type' => 49, 'or_type' => 51, 'not_or_type' => 64, 'basic_type' => 54, 'map_type' => 55, 'list_type' => 56, ), 24 => array( 'type' => 25, 'some_type' => 27, 'or_type' => 29, 'not_or_type' => 42, 'basic_type' => 32, 'map_type' => 33, 'list_type' => 34, ), 30 => array( 'not_or_type' => 31, 'basic_type' => 32, 'map_type' => 33, 'list_type' => 34, ), 37 => array( 'type' => 38, 'some_type' => 49, 'or_type' => 51, 'not_or_type' => 64, 'basic_type' => 54, 'map_type' => 55, 'list_type' => 56, ), 39 => array( 'type' => 40, 'some_type' => 27, 'or_type' => 29, 'not_or_type' => 42, 'basic_type' => 32, 'map_type' => 33, 'list_type' => 34, ), 43 => array( 'not_or_type' => 44, 'basic_type' => 32, 'map_type' => 33, 'list_type' => 34, ), 46 => array( 'type' => 47, 'some_type' => 27, 'or_type' => 29, 'not_or_type' => 42, 'basic_type' => 32, 'map_type' => 33, 'list_type' => 34, ), 52 => array( 'not_or_type' => 53, 'basic_type' => 54, 'map_type' => 55, 'list_type' => 56, ), 59 => array( 'type' => 60, 'some_type' => 49, 'or_type' => 51, 'not_or_type' => 64, 'basic_type' => 54, 'map_type' => 55, 'list_type' => 56, ), 61 => array( 'type' => 62, 'some_type' => 27, 'or_type' => 29, 'not_or_type' => 42, 'basic_type' => 32, 'map_type' => 33, 'list_type' => 34, ), 65 => array( 'not_or_type' => 66, 'basic_type' => 54, 'map_type' => 55, 'list_type' => 56, ), 68 => array( 'type' => 69, 'some_type' => 27, 'or_type' => 29, 'not_or_type' => 42, 'basic_type' => 32, 'map_type' => 33, 'list_type' => 34, ), 72 => array( 'type' => 73, 'some_type' => 27, 'or_type' => 29, 'not_or_type' => 42, 'basic_type' => 32, 'map_type' => 33, 'list_type' => 34, ), 76 => array( 'not_or_type' => 77, 'basic_type' => 17, 'map_type' => 18, 'list_type' => 19, ), ), '(end-of-file)', $tokens, $callback); } } diff --git a/src/symbols/PhutilSymbolLoader.php b/src/symbols/PhutilSymbolLoader.php index 1c8fccf..cdf2336 100644 --- a/src/symbols/PhutilSymbolLoader.php +++ b/src/symbols/PhutilSymbolLoader.php @@ -1,414 +1,413 @@ setType('class') * ->setLibrary('example') * ->selectAndLoadSymbols(); * ``` * * When you execute the loading query, it returns a dictionary of matching * symbols: * * ```lang=php * array( * 'class$Example' => array( * 'type' => 'class', * 'name' => 'Example', * 'library' => 'libexample', * 'where' => 'examples/example.php', * ), * // ... more ... * ); * ``` * * The **library** and **where** keys show where the symbol is defined. The * **type** and **name** keys identify the symbol itself. * * NOTE: This class must not use libphutil functions, including @{function:id} * and @{function:idx}. * * @task config Configuring the Query * @task load Loading Symbols * @task internal Internals */ final class PhutilSymbolLoader { private $type; private $library; private $base; private $name; private $concrete; private $pathPrefix; private $suppressLoad; /** * Select the type of symbol to load, either `class`, `function` or * `interface`. * * @param string Type of symbol to load. * @return this * * @task config */ public function setType($type) { $this->type = $type; return $this; } /** * Restrict the symbol query to a specific library; only symbols from this * library will be loaded. * * @param string Library name. * @return this * * @task config */ public function setLibrary($library) { // Validate the library name; this throws if the library in not loaded. $bootloader = PhutilBootloader::getInstance(); $bootloader->getLibraryRoot($library); $this->library = $library; return $this; } /** * Restrict the symbol query to a specific path prefix; only symbols defined * in files below that path will be selected. * * @param string Path relative to library root, like "apps/cheese/". * @return this * * @task config */ public function setPathPrefix($path) { $this->pathPrefix = str_replace(DIRECTORY_SEPARATOR, '/', $path); return $this; } /** * Restrict the symbol query to a single symbol name, e.g. a specific class * or function name. * * @param string Symbol name. * @return this * * @task config */ public function setName($name) { $this->name = $name; return $this; } /** * Restrict the symbol query to only descendants of some class. This will * strictly select descendants, the base class will not be selected. This * implies loading only classes. * * @param string Base class name. * @return this * * @task config */ public function setAncestorClass($base) { $this->base = $base; return $this; } /** * Restrict the symbol query to only concrete symbols; this will filter out * abstract classes. * * NOTE: This currently causes class symbols to load, even if you run * @{method:selectSymbolsWithoutLoading}. * * @param bool True if the query should load only concrete symbols. * @return this * * @task config */ public function setConcreteOnly($concrete) { $this->concrete = $concrete; return $this; } /* -( Load )--------------------------------------------------------------- */ /** * Execute the query and select matching symbols, then load them so they can * be used. * * @return dict A dictionary of matching symbols. See top-level class * documentation for details. These symbols will be loaded * and available. * * @task load */ public function selectAndLoadSymbols() { $map = array(); $bootloader = PhutilBootloader::getInstance(); if ($this->library) { $libraries = array($this->library); } else { $libraries = $bootloader->getAllLibraries(); } if ($this->type) { $types = array($this->type); } else { $types = array( 'class', 'function', ); } $names = null; if ($this->base) { $names = $this->selectDescendantsOf( $bootloader->getClassTree(), $this->base); } $symbols = array(); foreach ($libraries as $library) { $map = $bootloader->getLibraryMap($library); foreach ($types as $type) { if ($type == 'interface') { $lookup_map = $map['class']; } else { $lookup_map = $map[$type]; } // As an optimization, we filter the list of candidate symbols in // several passes, applying a name-based filter first if possible since // it is highly selective and guaranteed to match at most one symbol. // This is the common case and we land here through `__autoload()` so // it's worthwhile to micro-optimize a bit because this code path is // very hot and we save 5-10ms per page for a very moderate increase in // complexity. if ($this->name) { // If we have a name filter, just pick the matching name out if it // exists. if (isset($lookup_map[$this->name])) { $filtered_map = array( $this->name => $lookup_map[$this->name], ); } else { $filtered_map = array(); } } else if ($names !== null) { $filtered_map = array(); foreach ($names as $name => $ignored) { if (isset($lookup_map[$name])) { $filtered_map[$name] = $lookup_map[$name]; } } } else { // Otherwise, start with everything. $filtered_map = $lookup_map; } if ($this->pathPrefix) { $len = strlen($this->pathPrefix); foreach ($filtered_map as $name => $where) { if (strncmp($where, $this->pathPrefix, $len) !== 0) { unset($filtered_map[$name]); } } } foreach ($filtered_map as $name => $where) { $symbols[$type.'$'.$name] = array( 'type' => $type, 'name' => $name, 'library' => $library, 'where' => $where, ); } } } if (!$this->suppressLoad) { $caught = null; foreach ($symbols as $symbol) { try { $this->loadSymbol($symbol); } catch (Exception $ex) { $caught = $ex; } } if ($caught) { // NOTE: We try to load everything even if we fail to load something, // primarily to make it possible to remove functions from a libphutil // library without breaking library startup. throw $caught; } } if ($this->concrete) { // Remove 'abstract' classes. foreach ($symbols as $key => $symbol) { if ($symbol['type'] == 'class') { $reflection = new ReflectionClass($symbol['name']); if ($reflection->isAbstract()) { unset($symbols[$key]); } } } } return $symbols; } /** * Execute the query and select matching symbols, but do not load them. This * will perform slightly better if you are only interested in the existence * of the symbols and don't plan to use them; otherwise, use * @{method:selectAndLoadSymbols}. * * @return dict A dictionary of matching symbols. See top-level class * documentation for details. * * @task load */ public function selectSymbolsWithoutLoading() { $this->suppressLoad = true; $result = $this->selectAndLoadSymbols(); $this->suppressLoad = false; return $result; } /** * Select symbols matching the query and then instantiate them, returning * concrete objects. This is a convenience method which simplifies symbol * handling if you are only interested in building objects. * * If you want to do more than build objects, or want to build objects with * varying constructor arguments, use @{method:selectAndLoadSymbols} for * fine-grained control over results. * * This method implicitly restricts the query to match only concrete * classes. * * @param list List of constructor arguments. * @return map Map of class names to constructed objects. */ public function loadObjects(array $argv = array()) { $symbols = $this ->setConcreteOnly(true) ->setType('class') ->selectAndLoadSymbols(); $objects = array(); foreach ($symbols as $symbol) { $objects[$symbol['name']] = newv($symbol['name'], $argv); } return $objects; } /* -( Internals )---------------------------------------------------------- */ /** * @task internal */ private function selectDescendantsOf(array $tree, $root) { $result = array(); if (empty($tree[$root])) { // No known descendants. return array(); } foreach ($tree[$root] as $child) { $result[$child] = true; if (!empty($tree[$child])) { $result += $this->selectDescendantsOf($tree, $child); } } return $result; } /** * @task internal */ private function loadSymbol(array $symbol_spec) { // Check if we've already loaded the symbol; bail if we have. $name = $symbol_spec['name']; $is_function = ($symbol_spec['type'] == 'function'); if ($is_function) { if (function_exists($name)) { return; } } else { if (class_exists($name, false) || interface_exists($name, false)) { return; } } $lib_name = $symbol_spec['library']; $where = $symbol_spec['where']; $bootloader = PhutilBootloader::getInstance(); $bootloader->loadLibrarySource($lib_name, $where); // Check that we successfully loaded the symbol from wherever it was // supposed to be defined. $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'; } } if ($load_failed !== null) { $lib_path = phutil_get_library_root($lib_name); throw new PhutilMissingSymbolException( $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 index 2265173..f1fc626 100644 --- a/src/symbols/exception/PhutilMissingSymbolException.php +++ b/src/symbols/exception/PhutilMissingSymbolException.php @@ -1,26 +1,28 @@ assertTrue($caught instanceof InvalidArgumentException); } public function testMFilterWithEmptyValueFiltered() { $a = new MFilterTestHelper('o', 'p', 'q'); $b = new MFilterTestHelper('o', '', 'q'); $c = new MFilterTestHelper('o', 'p', 'q'); $list = array( 'a' => $a, 'b' => $b, 'c' => $c, ); $actual = mfilter($list, 'getI'); $expected = array( 'a' => $a, 'c' => $c, ); $this->assertEqual($expected, $actual); } public function testMFilterWithEmptyValueNegateFiltered() { $a = new MFilterTestHelper('o', 'p', 'q'); $b = new MFilterTestHelper('o', '', 'q'); $c = new MFilterTestHelper('o', 'p', 'q'); $list = array( 'a' => $a, 'b' => $b, 'c' => $c, ); $actual = mfilter($list, 'getI', true); $expected = array( 'b' => $b, ); $this->assertEqual($expected, $actual); } public function testIFilterInvalidIndexThrowException() { $caught = null; try { ifilter(array(), null); } catch (InvalidArgumentException $ex) { $caught = $ex; } $this->assertTrue($caught instanceof InvalidArgumentException); } public function testIFilterWithEmptyValueFiltered() { $list = array( 'a' => array('h' => 'o', 'i' => 'p', 'j' => 'q'), 'b' => array('h' => 'o', 'i' => '', 'j' => 'q'), 'c' => array('h' => 'o', 'i' => 'p', 'j' => 'q'), 'd' => array('h' => 'o', 'i' => 0, 'j' => 'q'), 'e' => array('h' => 'o', 'i' => null, 'j' => 'q'), 'f' => array('h' => 'o', 'i' => false, 'j' => 'q'), ); $actual = ifilter($list, 'i'); $expected = array( 'a' => array('h' => 'o', 'i' => 'p', 'j' => 'q'), 'c' => array('h' => 'o', 'i' => 'p', 'j' => 'q'), ); $this->assertEqual($expected, $actual); } public function testIFilterIndexNotExistsAllFiltered() { $list = array( 'a' => array('h' => 'o', 'i' => 'p', 'j' => 'q'), 'b' => array('h' => 'o', 'i' => '', 'j' => 'q'), ); $actual = ifilter($list, 'NoneExisting'); $expected = array(); $this->assertEqual($expected, $actual); } public function testIFilterWithEmptyValueNegateFiltered() { $list = array( 'a' => array('h' => 'o', 'i' => 'p', 'j' => 'q'), 'b' => array('h' => 'o', 'i' => '', 'j' => 'q'), 'c' => array('h' => 'o', 'i' => 'p', 'j' => 'q'), 'd' => array('h' => 'o', 'i' => 0, 'j' => 'q'), 'e' => array('h' => 'o', 'i' => null, 'j' => 'q'), 'f' => array('h' => 'o', 'i' => false, 'j' => 'q'), ); $actual = ifilter($list, 'i', true); $expected = array( 'b' => array('h' => 'o', 'i' => '', 'j' => 'q'), 'd' => array('h' => 'o', 'i' => 0, 'j' => 'q'), 'e' => array('h' => 'o', 'i' => null, 'j' => 'q'), 'f' => array('h' => 'o', 'i' => false, 'j' => 'q'), ); $this->assertEqual($expected, $actual); } public function testIFilterIndexNotExistsNotFiltered() { $list = array( 'a' => array('h' => 'o', 'i' => 'p', 'j' => 'q'), 'b' => array('h' => 'o', 'i' => '', 'j' => 'q'), ); $actual = ifilter($list, 'NoneExisting', true); $expected = array( 'a' => array('h' => 'o', 'i' => 'p', 'j' => 'q'), 'b' => array('h' => 'o', 'i' => '', 'j' => 'q'), ); $this->assertEqual($expected, $actual); } public function testmergevMergingBasicallyWorksCorrectly() { $this->assertEqual( array(), array_mergev( array( // ))); $this->assertEqual( array(), array_mergev( array( array(), array(), array(), ))); $this->assertEqual( array(1, 2, 3, 4, 5), array_mergev( array( array(1, 2), array(3), array(), array(4, 5), ))); $not_valid = array( 'scalar' => array(1), 'array plus scalar' => array(array(), 1), 'null' => array(null), ); foreach ($not_valid as $key => $invalid_input) { $caught = null; try { array_mergev($invalid_input); } catch (InvalidArgumentException $ex) { $caught = $ex; } $this->assertTrue( ($caught instanceof InvalidArgumentException), pht('%s invalid on %s', 'array_mergev()', $key)); } } public function testNonempty() { $this->assertEqual( 'zebra', nonempty(false, null, 0, '', array(), 'zebra')); $this->assertEqual( null, nonempty()); $this->assertEqual( false, nonempty(null, false)); $this->assertEqual( null, nonempty(false, null)); } protected function tryAssertInstancesOfArray($input) { assert_instances_of($input, 'array'); } protected function tryAssertInstancesOfStdClass($input) { assert_instances_of($input, 'stdClass'); } public function testAssertInstancesOf() { $object = new stdClass(); $inputs = array( 'empty' => array(), 'stdClass' => array($object, $object), __CLASS__ => array($object, $this), 'array' => array(array(), array()), 'integer' => array($object, 1), ); $this->tryTestCases( $inputs, array(true, true, false, false, false), array($this, 'tryAssertInstancesOfStdClass'), 'InvalidArgumentException'); $this->tryTestCases( $inputs, array(true, false, false, true, false), array($this, 'tryAssertInstancesOfArray'), 'InvalidArgumentException'); } public function testAssertStringLike() { $this->assertEqual( null, assert_stringlike(null)); $this->assertEqual( null, assert_stringlike('')); $this->assertEqual( null, assert_stringlike('Hello World')); $this->assertEqual( null, assert_stringlike(1)); $this->assertEqual( null, assert_stringlike(9.9999)); $this->assertEqual( null, assert_stringlike(true)); $obj = new Exception('.'); $this->assertEqual( null, assert_stringlike($obj)); $obj = (object)array(); try { assert_stringlike($obj); } catch (InvalidArgumentException $ex) { $caught = $ex; } $this->assertTrue($caught instanceof InvalidArgumentException); $array = array( 'foo' => 'bar', 'bar' => 'foo', ); try { assert_stringlike($array); } catch (InvalidArgumentException $ex) { $caught = $ex; } $this->assertTrue($caught instanceof InvalidArgumentException); $tmp = new TempFile(); $resource = fopen($tmp, 'r'); try { assert_stringlike($resource); } catch (InvalidArgumentException $ex) { $caught = $ex; } fclose($resource); $this->assertTrue($caught instanceof InvalidArgumentException); } public function testCoalesce() { $this->assertEqual( 'zebra', coalesce(null, 'zebra')); $this->assertEqual( null, coalesce()); $this->assertEqual( false, coalesce(false, null)); $this->assertEqual( false, coalesce(null, false)); } public function testHeadLast() { $this->assertEqual( 'a', head(explode('.', 'a.b'))); $this->assertEqual( 'b', last(explode('.', 'a.b'))); } public function testHeadKeyLastKey() { $this->assertEqual( 'a', head_key(array('a' => 0, 'b' => 1))); $this->assertEqual( 'b', last_key(array('a' => 0, 'b' => 1))); $this->assertEqual(null, head_key(array())); $this->assertEqual(null, last_key(array())); } public function testID() { $this->assertEqual(true, id(true)); $this->assertEqual(false, id(false)); } public function testIdx() { $array = array( 'present' => true, 'null' => null, ); $this->assertEqual(true, idx($array, 'present')); $this->assertEqual(true, idx($array, 'present', false)); $this->assertEqual(null, idx($array, 'null')); $this->assertEqual(null, idx($array, 'null', false)); $this->assertEqual(null, idx($array, 'missing')); $this->assertEqual(false, idx($array, 'missing', false)); } public function testSplitLines() { $retain_cases = array( '' => array(''), 'x' => array('x'), "x\n" => array("x\n"), "\n" => array("\n"), "\n\n\n" => array("\n", "\n", "\n"), "\r\n" => array("\r\n"), "x\r\ny\n" => array("x\r\n", "y\n"), "x\ry\nz\r\n" => array("x\ry\n", "z\r\n"), "x\ry\nz\r\n\n" => array("x\ry\n", "z\r\n", "\n"), ); foreach ($retain_cases as $input => $expect) { $this->assertEqual( $expect, phutil_split_lines($input, $retain_endings = true), pht('(Retained) %s', addcslashes($input, "\r\n\\"))); } $discard_cases = array( '' => array(''), 'x' => array('x'), "x\n" => array('x'), "\n" => array(''), "\n\n\n" => array('', '', ''), "\r\n" => array(''), "x\r\ny\n" => array('x', 'y'), "x\ry\nz\r\n" => array("x\ry", 'z'), "x\ry\nz\r\n\n" => array("x\ry", 'z', ''), ); foreach ($discard_cases as $input => $expect) { $this->assertEqual( $expect, phutil_split_lines($input, $retain_endings = false), pht('(Discarded) %s', addcslashes($input, "\r\n\\"))); } } public function testArrayFuse() { $this->assertEqual(array(), array_fuse(array())); $this->assertEqual(array('x' => 'x'), array_fuse(array('x'))); } public function testArrayInterleave() { $this->assertEqual(array(), array_interleave('x', array())); $this->assertEqual(array('y'), array_interleave('x', array('y'))); $this->assertEqual( array('y', 'x', 'z'), array_interleave('x', array('y', 'z'))); $this->assertEqual( array('y', 'x', 'z'), array_interleave( 'x', array( 'kangaroo' => 'y', 'marmoset' => 'z', ))); $obj1 = (object)array(); $obj2 = (object)array(); $this->assertEqual( array($obj1, $obj2, $obj1, $obj2, $obj1), array_interleave( $obj2, array( $obj1, $obj1, $obj1, ))); $implode_tests = array( '' => array(1, 2, 3), 'x' => array(1, 2, 3), 'y' => array(), 'z' => array(1), ); foreach ($implode_tests as $x => $y) { $this->assertEqual( implode('', array_interleave($x, $y)), implode($x, $y)); } } public function testLoggableString() { $this->assertEqual( '', phutil_loggable_string('')); $this->assertEqual( "a\\nb", phutil_loggable_string("a\nb")); $this->assertEqual( "a\\x01b", phutil_loggable_string("a\x01b")); $this->assertEqual( "a\\x1Fb", phutil_loggable_string("a\x1Fb")); } public function testPhutilUnits() { $cases = array( '0 seconds in seconds' => 0, '1 second in seconds' => 1, '2 seconds in seconds' => 2, '100 seconds in seconds' => 100, '2 minutes in seconds' => 120, '1 hour in seconds' => 3600, '1 day in seconds' => 86400, '3 days in seconds' => 259200, '128 bits in bytes' => 16, '1 byte in bytes' => 1, '8 bits in bytes' => 1, '1 minute in milliseconds' => 60000, '2 minutes in microseconds' => 120000000, ); foreach ($cases as $input => $expect) { $this->assertEqual( $expect, phutil_units($input), 'phutil_units("'.$input.'")'); } $bad_cases = array( 'quack', '3 years in seconds', '1 day in days', '-1 minutes in seconds', '1.5 minutes in seconds', '7 bits in bytes', '2 hours in bytes', '1 dram in bytes', '24 bits in seconds', ); foreach ($bad_cases as $input) { $caught = null; try { phutil_units($input); } catch (InvalidArgumentException $ex) { $caught = $ex; } $this->assertTrue( ($caught instanceof InvalidArgumentException), 'phutil_units("'.$input.'")'); } } public function testPhutilJSONDecode() { $valid_cases = array( '{}' => array(), '[]' => array(), '[1, 2]' => array(1, 2), '{"a":"b"}' => array('a' => 'b'), ); foreach ($valid_cases as $input => $expect) { $result = phutil_json_decode($input); $this->assertEqual($expect, $result, 'phutil_json_decode('.$input.')'); } $invalid_cases = array( '', '"a"', '{,}', 'null', '"null"', ); foreach ($invalid_cases as $input) { $caught = null; try { phutil_json_decode($input); } catch (Exception $ex) { $caught = $ex; } $this->assertTrue($caught instanceof PhutilJSONParserException); } } public function testPhutilINIDecode() { // Skip the test if we are using an older version of PHP that doesn't // have the `parse_ini_string` function. try { phutil_ini_decode(''); } catch (PhutilMethodNotImplementedException $ex) { $this->assertSkipped($ex->getMessage()); } $valid_cases = array( '' => array(), 'foo=' => array('foo' => ''), 'foo=bar' => array('foo' => 'bar'), 'foo = bar' => array('foo' => 'bar'), "foo = bar\n" => array('foo' => 'bar'), "foo\nbar = baz" => array('bar' => 'baz'), "[foo]\nbar = baz" => array('foo' => array('bar' => 'baz')), "[foo]\n[bar]\nbaz = foo" => array( 'foo' => array(), 'bar' => array('baz' => 'foo'), ), "[foo]\nbar = baz\n\n[bar]\nbaz = foo" => array( 'foo' => array('bar' => 'baz'), 'bar' => array('baz' => 'foo'), ), "; Comment\n[foo]\nbar = baz" => array('foo' => array('bar' => 'baz')), "# Comment\n[foo]\nbar = baz" => array('foo' => array('bar' => 'baz')), "foo = true\n[bar]\nbaz = false" => array('foo' => true, 'bar' => array('baz' => false)), "foo = 1\nbar = 1.234" => array('foo' => 1, 'bar' => 1.234), 'x = {"foo": "bar"}' => array('x' => '{"foo": "bar"}'), ); foreach ($valid_cases as $input => $expect) { $result = phutil_ini_decode($input); $this->assertEqual($expect, $result, 'phutil_ini_decode('.$input.')'); } $invalid_cases = array( '[' => new PhutilINIParserException(), ); foreach ($invalid_cases as $input => $expect) { $caught = null; try { phutil_ini_decode($input); } catch (Exception $ex) { $caught = $ex; } $this->assertTrue($caught instanceof $expect); } } public function testCensorCredentials() { $cases = array( '' => '', 'abc' => 'abc', // NOTE: We're liberal about censoring here, since we can't tell // if this is a truncated password at the end of an input string // or a domain name. The version with a "/" isn't censored. 'http://example.com' => 'http://********', 'http://example.com/' => 'http://example.com/', 'http://username@example.com' => 'http://********@example.com', 'http://user:pass@example.com' => 'http://********@example.com', // We censor these because they might be truncated credentials at the end // of the string. 'http://user' => 'http://********', "http://user\n" => "http://********\n", 'svn+ssh://user:pass@example.com' => 'svn+ssh://********@example.com', ); foreach ($cases as $input => $expect) { $this->assertEqual( $expect, phutil_censor_credentials($input), pht('Credential censoring for: %s', $input)); } } public function testVarExport() { // Constants $this->assertEqual('null', phutil_var_export(null)); $this->assertEqual('true', phutil_var_export(true)); $this->assertEqual('false', phutil_var_export(false)); $this->assertEqual("'quack'", phutil_var_export('quack')); $this->assertEqual('1234567', phutil_var_export(1234567)); // Arrays $this->assertEqual( 'array()', phutil_var_export(array())); $this->assertEqual( implode("\n", array( 'array(', ' 1,', ' 2,', ' 3,', ')', )), phutil_var_export(array(1, 2, 3))); $this->assertEqual( implode("\n", array( 'array(', " 'foo' => 'bar',", " 'bar' => 'baz',", ')', )), phutil_var_export(array('foo' => 'bar', 'bar' => 'baz'))); $this->assertEqual( implode("\n", array( 'array(', " 'foo' => array(", " 'bar' => array(", " 'baz' => array(),", ' ),', ' ),', ')', )), phutil_var_export( array('foo' => array('bar' => array('baz' => array()))))); // NOTE: Object behavior differs across PHP versions. Older versions of // PHP export objects as "stdClass::__set_state(array())". Newer versions // of PHP (7.3+) export objects as "(object) array()". } public function testFnmatch() { $cases = array( '' => array( array(''), array('.', '/'), ), '*' => array( array('file'), array('dir/', '/dir'), ), '**' => array( array('file', 'dir/', '/dir', 'dir/subdir/file'), array(), ), '**/file' => array( array('file', 'dir/file', 'dir/subdir/file', 'dir/subdir/subdir/file'), array('file/', 'file/dir'), ), 'file.*' => array( array('file.php', 'file.a', 'file.'), array('files.php', 'file.php/blah'), ), 'fo?' => array( array('foo', 'fot'), array('fooo', 'ffoo', 'fo/', 'foo/'), ), 'fo{o,t}' => array( array('foo', 'fot'), array('fob', 'fo/', 'foo/'), ), 'fo{o,\\,}' => array( array('foo', 'fo,'), array('foo/', 'fo,/'), ), 'fo{o,\\\\}' => array( array('foo', 'fo\\'), array('foo/', 'fo\\/'), ), '/foo' => array( array('/foo'), array('foo', '/foo/'), ), // Tests for various `fnmatch` flags. '*.txt' => array( array( 'file.txt', // FNM_PERIOD '.secret-file.txt', ), array( // FNM_PATHNAME 'dir/file.txt', // FNM_CASEFOLD 'file.TXT', ), '\\*.txt' => array( array( // FNM_NOESCAPE '*.txt', ), array( 'file.txt', ), ), ), ); $invalid = array( '{', 'asdf\\', ); foreach ($cases as $input => $expect) { list($matches, $no_matches) = $expect; foreach ($matches as $match) { $this->assertTrue( phutil_fnmatch($input, $match), pht('Expecting "%s" to match "%s".', $input, $match)); } foreach ($no_matches as $no_match) { $this->assertFalse( phutil_fnmatch($input, $no_match), pht('Expecting "%s" not to match "%s".', $input, $no_match)); } } foreach ($invalid as $input) { $caught = null; try { phutil_fnmatch($input, ''); } catch (Exception $ex) { $caught = $ex; } $this->assertTrue($caught instanceof InvalidArgumentException); } } public function testJSONEncode() { $in = array( 'example' => "Not Valid UTF8: \x80", ); $caught = null; try { $value = phutil_json_encode($in); } catch (Exception $ex) { $caught = $ex; } $this->assertTrue(($caught instanceof Exception)); } public function testHashComparisons() { $tests = array( array('1', '12', false), array('0', '0e123', false), array('0e123', '0e124', false), array('', '0', false), array('000', '0e0', false), array('001', '002', false), array('0', '', false), array('987654321', '123456789', false), array('A', 'a', false), array('123456789', '123456789', true), array('hunter42', 'hunter42', true), ); foreach ($tests as $key => $test) { list($u, $v, $expect) = $test; $actual = phutil_hashes_are_identical($u, $v); $this->assertEqual( $expect, $actual, pht('Test Case: "%s" vs "%s"', $u, $v)); } } public function testVectorSortInt() { $original = array( ~PHP_INT_MAX, -2147483648, -5, -3, -1, 0, 1, 2, 3, 100, PHP_INT_MAX, ); $items = $this->shuffleMap($original); foreach ($items as $key => $value) { $items[$key] = (string)id(new PhutilSortVector()) ->addInt($value); } asort($items, SORT_STRING); $this->assertEqual( array_keys($original), array_keys($items)); } public function testVectorSortString() { $original = array( '', "\1", 'A', 'AB', 'Z', "Z\1", 'ZZZ', ); $items = $this->shuffleMap($original); foreach ($items as $key => $value) { $items[$key] = (string)id(new PhutilSortVector()) ->addString($value); } asort($items, SORT_STRING); $this->assertEqual( array_keys($original), array_keys($items)); } private function shuffleMap(array $map) { $keys = array_keys($map); shuffle($keys); return array_select_keys($map, $keys); } public function testQueryStringEncoding() { $expect = array(); // As a starting point, we expect every character to encode as an "%XX" // escaped version. foreach (range(0, 255) as $byte) { $c = chr($byte); $expect[$c] = sprintf('%%%02X', $byte); } // We expect these characters to not be escaped. $ranges = array( range('a', 'z'), range('A', 'Z'), range('0', '9'), array('-', '.', '_', '~'), ); foreach ($ranges as $range) { foreach ($range as $preserve_char) { $expect[$preserve_char] = $preserve_char; } } foreach (range(0, 255) as $byte) { $c = chr($byte); $expect_c = $expect[$c]; $expect_str = "{$expect_c}={$expect_c}"; $actual_str = phutil_build_http_querystring(array($c => $c)); $this->assertEqual( $expect_str, $actual_str, pht('HTTP querystring for byte "%s".', sprintf('0x%02x', $byte))); } } + 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 index 66e6c09..b974ba2 100644 --- a/src/utils/utils.php +++ b/src/utils/utils.php @@ -1,1759 +1,1796 @@ doStuff(); * * ...but this works fine: * * id(new Thing())->doStuff(); * * @param wild Anything. * @return wild Unmodified argument. */ function id($x) { return $x; } /** * Access an array index, retrieving the value stored there if it exists or * a default if it does not. This function allows you to concisely access an * index which may or may not exist without raising a warning. * * @param array Array to access. * @param scalar Index to access in the array. * @param wild Default value to return if the key is not present in the * array. * @return wild If `$array[$key]` exists, that value is returned. If not, * $default is returned without raising a warning. */ function idx(array $array, $key, $default = null) { // isset() is a micro-optimization - it is fast but fails for null values. if (isset($array[$key])) { return $array[$key]; } // Comparing $default is also a micro-optimization. if ($default === null || array_key_exists($key, $array)) { return null; } return $default; } /** * Access a sequence of array indexes, retrieving a deeply nested value if * it exists or a default if it does not. * * For example, `idxv($dict, array('a', 'b', 'c'))` accesses the key at * `$dict['a']['b']['c']`, if it exists. If it does not, or any intermediate * value is not itself an array, it returns the defualt value. * * @param array Array to access. * @param list List of keys to access, in sequence. * @param wild Default value to return. * @return wild Accessed value, or default if the value is not accessible. */ function idxv(array $map, array $path, $default = null) { if (!$path) { return $default; } $last = last($path); $path = array_slice($path, 0, -1); $cursor = $map; foreach ($path as $key) { $cursor = idx($cursor, $key); if (!is_array($cursor)) { return $default; } } return idx($cursor, $last, $default); } /** * Call a method on a list of objects. Short for "method pull", this function * works just like @{function:ipull}, except that it operates on a list of * objects instead of a list of arrays. This function simplifies a common type * of mapping operation: * * COUNTEREXAMPLE * $names = array(); * foreach ($objects as $key => $object) { * $names[$key] = $object->getName(); * } * * You can express this more concisely with mpull(): * * $names = mpull($objects, 'getName'); * * mpull() takes a third argument, which allows you to do the same but for * the array's keys: * * COUNTEREXAMPLE * $names = array(); * foreach ($objects as $object) { * $names[$object->getID()] = $object->getName(); * } * * This is the mpull version(): * * $names = mpull($objects, 'getName', 'getID'); * * If you pass ##null## as the second argument, the objects will be preserved: * * COUNTEREXAMPLE * $id_map = array(); * foreach ($objects as $object) { * $id_map[$object->getID()] = $object; * } * * With mpull(): * * $id_map = mpull($objects, null, 'getID'); * * See also @{function:ipull}, which works similarly but accesses array indexes * instead of calling methods. * * @param list Some list of objects. * @param string|null Determines which **values** will appear in the result * array. Use a string like 'getName' to store the * value of calling the named method in each value, or * ##null## to preserve the original objects. * @param string|null Determines how **keys** will be assigned in the result * array. Use a string like 'getID' to use the result * of calling the named method as each object's key, or * `null` to preserve the original keys. * @return dict A dictionary with keys and values derived according * to whatever you passed as `$method` and `$key_method`. */ function mpull(array $list, $method, $key_method = null) { $result = array(); foreach ($list as $key => $object) { if ($key_method !== null) { $key = $object->$key_method(); } if ($method !== null) { $value = $object->$method(); } else { $value = $object; } $result[$key] = $value; } return $result; } /** * Access a property on a list of objects. Short for "property pull", this * function works just like @{function:mpull}, except that it accesses object * properties instead of methods. This function simplifies a common type of * mapping operation: * * COUNTEREXAMPLE * $names = array(); * foreach ($objects as $key => $object) { * $names[$key] = $object->name; * } * * You can express this more concisely with ppull(): * * $names = ppull($objects, 'name'); * * ppull() takes a third argument, which allows you to do the same but for * the array's keys: * * COUNTEREXAMPLE * $names = array(); * foreach ($objects as $object) { * $names[$object->id] = $object->name; * } * * This is the ppull version(): * * $names = ppull($objects, 'name', 'id'); * * If you pass ##null## as the second argument, the objects will be preserved: * * COUNTEREXAMPLE * $id_map = array(); * foreach ($objects as $object) { * $id_map[$object->id] = $object; * } * * With ppull(): * * $id_map = ppull($objects, null, 'id'); * * See also @{function:mpull}, which works similarly but calls object methods * instead of accessing object properties. * * @param list Some list of objects. * @param string|null Determines which **values** will appear in the result * array. Use a string like 'name' to store the value of * accessing the named property in each value, or * `null` to preserve the original objects. * @param string|null Determines how **keys** will be assigned in the result * array. Use a string like 'id' to use the result of * accessing the named property as each object's key, or * `null` to preserve the original keys. * @return dict A dictionary with keys and values derived according * to whatever you passed as `$property` and * `$key_property`. */ function ppull(array $list, $property, $key_property = null) { $result = array(); foreach ($list as $key => $object) { if ($key_property !== null) { $key = $object->$key_property; } if ($property !== null) { $value = $object->$property; } else { $value = $object; } $result[$key] = $value; } return $result; } /** * Choose an index from a list of arrays. Short for "index pull", this function * works just like @{function:mpull}, except that it operates on a list of * arrays and selects an index from them instead of operating on a list of * objects and calling a method on them. * * This function simplifies a common type of mapping operation: * * COUNTEREXAMPLE * $names = array(); * foreach ($list as $key => $dict) { * $names[$key] = $dict['name']; * } * * With ipull(): * * $names = ipull($list, 'name'); * * See @{function:mpull} for more usage examples. * * @param list Some list of arrays. * @param scalar|null Determines which **values** will appear in the result * array. Use a scalar to select that index from each * array, or null to preserve the arrays unmodified as * values. * @param scalar|null Determines which **keys** will appear in the result * array. Use a scalar to select that index from each * array, or null to preserve the array keys. * @return dict A dictionary with keys and values derived according * to whatever you passed for `$index` and `$key_index`. */ function ipull(array $list, $index, $key_index = null) { $result = array(); foreach ($list as $key => $array) { if ($key_index !== null) { $key = $array[$key_index]; } if ($index !== null) { $value = $array[$index]; } else { $value = $array; } $result[$key] = $value; } return $result; } /** * Group a list of objects by the result of some method, similar to how * GROUP BY works in an SQL query. This function simplifies grouping objects * by some property: * * COUNTEREXAMPLE * $animals_by_species = array(); * foreach ($animals as $animal) { * $animals_by_species[$animal->getSpecies()][] = $animal; * } * * This can be expressed more tersely with mgroup(): * * $animals_by_species = mgroup($animals, 'getSpecies'); * * In either case, the result is a dictionary which maps species (e.g., like * "dog") to lists of animals with that property, so all the dogs are grouped * together and all the cats are grouped together, or whatever super * businessesey thing is actually happening in your problem domain. * * See also @{function:igroup}, which works the same way but operates on * array indexes. * * @param list List of objects to group by some property. * @param string Name of a method, like 'getType', to call on each object * in order to determine which group it should be placed into. * @param ... Zero or more additional method names, to subgroup the * groups. * @return dict Dictionary mapping distinct method returns to lists of * all objects which returned that value. */ function mgroup(array $list, $by /* , ... */) { $map = mpull($list, $by); $groups = array(); foreach ($map as $group) { // Can't array_fill_keys() here because 'false' gets encoded wrong. $groups[$group] = array(); } foreach ($map as $key => $group) { $groups[$group][$key] = $list[$key]; } $args = func_get_args(); $args = array_slice($args, 2); if ($args) { array_unshift($args, null); foreach ($groups as $group_key => $grouped) { $args[0] = $grouped; $groups[$group_key] = call_user_func_array('mgroup', $args); } } return $groups; } /** * Group a list of arrays by the value of some index. This function is the same * as @{function:mgroup}, except it operates on the values of array indexes * rather than the return values of method calls. * * @param list List of arrays to group by some index value. * @param string Name of an index to select from each array in order to * determine which group it should be placed into. * @param ... Zero or more additional indexes names, to subgroup the * groups. * @return dict Dictionary mapping distinct index values to lists of * all objects which had that value at the index. */ function igroup(array $list, $by /* , ... */) { $map = ipull($list, $by); $groups = array(); foreach ($map as $group) { $groups[$group] = array(); } foreach ($map as $key => $group) { $groups[$group][$key] = $list[$key]; } $args = func_get_args(); $args = array_slice($args, 2); if ($args) { array_unshift($args, null); foreach ($groups as $group_key => $grouped) { $args[0] = $grouped; $groups[$group_key] = call_user_func_array('igroup', $args); } } return $groups; } /** * Sort a list of objects by the return value of some method. In PHP, this is * often vastly more efficient than `usort()` and similar. * * // Sort a list of Duck objects by name. * $sorted = msort($ducks, 'getName'); * * It is usually significantly more efficient to define an ordering method * on objects and call `msort()` than to write a comparator. It is often more * convenient, as well. * * NOTE: This method does not take the list by reference; it returns a new list. * * @param list List of objects to sort by some property. * @param string Name of a method to call on each object; the return values * will be used to sort the list. * @return list Objects ordered by the return values of the method calls. */ function msort(array $list, $method) { $surrogate = mpull($list, $method); asort($surrogate); $result = array(); foreach ($surrogate as $key => $value) { $result[$key] = $list[$key]; } return $result; } /** * Sort a list of objects by a sort vector. * * This sort is stable, well-behaved, and more efficient than `usort()`. * * @param list List of objects to sort. * @param string Name of a method to call on each object. The method must * return a @{class:PhutilSortVector}. * @return list Objects ordered by the vectors. */ function msortv(array $list, $method) { $surrogate = mpull($list, $method); $index = 0; foreach ($surrogate as $key => $value) { if (!($value instanceof PhutilSortVector)) { throw new Exception( pht( 'Objects passed to "%s" must return sort vectors (objects of '. 'class "%s") from the specified method ("%s"). One object (with '. 'key "%s") did not.', 'msortv()', 'PhutilSortVector', $method, $key)); } // Add the original index to keep the sort stable. $value->addInt($index++); $surrogate[$key] = (string)$value; } asort($surrogate, SORT_STRING); $result = array(); foreach ($surrogate as $key => $value) { $result[$key] = $list[$key]; } return $result; } /** * Sort a list of arrays by the value of some index. This method is identical to * @{function:msort}, but operates on a list of arrays instead of a list of * objects. * * @param list List of arrays to sort by some index value. * @param string Index to access on each object; the return values * will be used to sort the list. * @return list Arrays ordered by the index values. */ function isort(array $list, $index) { $surrogate = ipull($list, $index); asort($surrogate); $result = array(); foreach ($surrogate as $key => $value) { $result[$key] = $list[$key]; } return $result; } /** * Filter a list of objects by executing a method across all the objects and * filter out the ones with empty() results. this function works just like * @{function:ifilter}, except that it operates on a list of objects instead * of a list of arrays. * * For example, to remove all objects with no children from a list, where * 'hasChildren' is a method name, do this: * * mfilter($list, 'hasChildren'); * * The optional third parameter allows you to negate the operation and filter * out nonempty objects. To remove all objects that DO have children, do this: * * mfilter($list, 'hasChildren', true); * * @param array List of objects to filter. * @param string A method name. * @param bool Optionally, pass true to drop objects which pass the * filter instead of keeping them. * @return array List of objects which pass the filter. */ function mfilter(array $list, $method, $negate = false) { if (!is_string($method)) { throw new InvalidArgumentException(pht('Argument method is not a string.')); } $result = array(); foreach ($list as $key => $object) { $value = $object->$method(); if (!$negate) { if (!empty($value)) { $result[$key] = $object; } } else { if (empty($value)) { $result[$key] = $object; } } } return $result; } /** * Filter a list of arrays by removing the ones with an empty() value for some * index. This function works just like @{function:mfilter}, except that it * operates on a list of arrays instead of a list of objects. * * For example, to remove all arrays without value for key 'username', do this: * * ifilter($list, 'username'); * * The optional third parameter allows you to negate the operation and filter * out nonempty arrays. To remove all arrays that DO have value for key * 'username', do this: * * ifilter($list, 'username', true); * * @param array List of arrays to filter. * @param scalar The index. * @param bool Optionally, pass true to drop arrays which pass the * filter instead of keeping them. * @return array List of arrays which pass the filter. */ function ifilter(array $list, $index, $negate = false) { if (!is_scalar($index)) { throw new InvalidArgumentException(pht('Argument index is not a scalar.')); } $result = array(); if (!$negate) { foreach ($list as $key => $array) { if (!empty($array[$index])) { $result[$key] = $array; } } } else { foreach ($list as $key => $array) { if (empty($array[$index])) { $result[$key] = $array; } } } return $result; } /** * Selects a list of keys from an array, returning a new array with only the * key-value pairs identified by the selected keys, in the specified order. * * Note that since this function orders keys in the result according to the * order they appear in the list of keys, there are effectively two common * uses: either reducing a large dictionary to a smaller one, or changing the * key order on an existing dictionary. * * @param dict Dictionary of key-value pairs to select from. * @param list List of keys to select. * @return dict Dictionary of only those key-value pairs where the key was * present in the list of keys to select. Ordering is * determined by the list order. */ function array_select_keys(array $dict, array $keys) { $result = array(); foreach ($keys as $key) { if (array_key_exists($key, $dict)) { $result[$key] = $dict[$key]; } } return $result; } /** * Checks if all values of array are instances of the passed class. Throws * `InvalidArgumentException` if it isn't true for any value. * * @param array * @param string Name of the class or 'array' to check arrays. * @return array Returns passed array. */ function assert_instances_of(array $arr, $class) { $is_array = !strcasecmp($class, 'array'); foreach ($arr as $key => $object) { if ($is_array) { if (!is_array($object)) { $given = gettype($object); throw new InvalidArgumentException( pht( "Array item with key '%s' must be of type array, %s given.", $key, $given)); } } else if (!($object instanceof $class)) { $given = gettype($object); if (is_object($object)) { $given = pht('instance of %s', get_class($object)); } throw new InvalidArgumentException( pht( "Array item with key '%s' must be an instance of %s, %s given.", $key, $class, $given)); } } return $arr; } /** * Assert that passed data can be converted to string. * * @param string Assert that this data is valid. * @return void * * @task assert */ function assert_stringlike($parameter) { switch (gettype($parameter)) { case 'string': case 'NULL': case 'boolean': case 'double': case 'integer': return; case 'object': if (method_exists($parameter, '__toString')) { return; } break; case 'array': case 'resource': case 'unknown type': default: break; } throw new InvalidArgumentException( pht( 'Argument must be scalar or object which implements %s!', '__toString()')); } /** * Returns the first argument which is not strictly null, or `null` if there * are no such arguments. Identical to the MySQL function of the same name. * * @param ... Zero or more arguments of any type. * @return mixed First non-`null` arg, or null if no such arg exists. */ function coalesce(/* ... */) { $args = func_get_args(); foreach ($args as $arg) { if ($arg !== null) { return $arg; } } return null; } /** * Similar to @{function:coalesce}, but less strict: returns the first * non-`empty()` argument, instead of the first argument that is strictly * non-`null`. If no argument is nonempty, it returns the last argument. This * is useful idiomatically for setting defaults: * * $display_name = nonempty($user_name, $full_name, "Anonymous"); * * @param ... Zero or more arguments of any type. * @return mixed First non-`empty()` arg, or last arg if no such arg * exists, or null if you passed in zero args. */ function nonempty(/* ... */) { $args = func_get_args(); $result = null; foreach ($args as $arg) { $result = $arg; if ($arg) { break; } } return $result; } /** * Invokes the "new" operator with a vector of arguments. There is no way to * `call_user_func_array()` on a class constructor, so you can instead use this * function: * * $obj = newv($class_name, $argv); * * That is, these two statements are equivalent: * * $pancake = new Pancake('Blueberry', 'Maple Syrup', true); * $pancake = newv('Pancake', array('Blueberry', 'Maple Syrup', true)); * * DO NOT solve this problem in other, more creative ways! Three popular * alternatives are: * * - Build a fake serialized object and unserialize it. * - Invoke the constructor twice. * - just use `eval()` lol * * These are really bad solutions to the problem because they can have side * effects (e.g., __wakeup()) and give you an object in an otherwise impossible * state. Please endeavor to keep your objects in possible states. * * If you own the classes you're doing this for, you should consider whether * or not restructuring your code (for instance, by creating static * construction methods) might make it cleaner before using `newv()`. Static * constructors can be invoked with `call_user_func_array()`, and may give your * class a cleaner and more descriptive API. * * @param string The name of a class. * @param list Array of arguments to pass to its constructor. * @return obj A new object of the specified class, constructed by passing * the argument vector to its constructor. */ function newv($class_name, array $argv) { $reflector = new ReflectionClass($class_name); if ($argv) { return $reflector->newInstanceArgs($argv); } else { return $reflector->newInstance(); } } /** * Returns the first element of an array. Exactly like reset(), but doesn't * choke if you pass it some non-referenceable value like the return value of * a function. * * @param array Array to retrieve the first element from. * @return wild The first value of the array. */ function head(array $arr) { return reset($arr); } /** * Returns the last element of an array. This is exactly like `end()` except * that it won't warn you if you pass some non-referencable array to * it -- e.g., the result of some other array operation. * * @param array Array to retrieve the last element from. * @return wild The last value of the array. */ function last(array $arr) { return end($arr); } /** * Returns the first key of an array. * * @param array Array to retrieve the first key from. * @return int|string The first key of the array. */ function head_key(array $arr) { reset($arr); return key($arr); } /** * Returns the last key of an array. * * @param array Array to retrieve the last key from. * @return int|string The last key of the array. */ function last_key(array $arr) { end($arr); return key($arr); } /** * Merge a vector of arrays performantly. This has the same semantics as * array_merge(), so these calls are equivalent: * * array_merge($a, $b, $c); * array_mergev(array($a, $b, $c)); * * However, when you have a vector of arrays, it is vastly more performant to * merge them with this function than by calling array_merge() in a loop, * because using a loop generates an intermediary array on each iteration. * * @param list Vector of arrays to merge. * @return list Arrays, merged with array_merge() semantics. */ function array_mergev(array $arrayv) { if (!$arrayv) { return array(); } foreach ($arrayv as $key => $item) { if (!is_array($item)) { throw new InvalidArgumentException( pht( 'Expected all items passed to `%s` to be arrays, but '. 'argument with key "%s" has type "%s".', __FUNCTION__.'()', $key, gettype($item))); } } return call_user_func_array('array_merge', $arrayv); } /** * Split a corpus of text into lines. This function splits on "\n", "\r\n", or * a mixture of any of them. * * NOTE: This function does not treat "\r" on its own as a newline because none * of SVN, Git or Mercurial do on any OS. * * @param string Block of text to be split into lines. * @param bool If true, retain line endings in result strings. * @return list List of lines. */ function phutil_split_lines($corpus, $retain_endings = true) { if (!strlen($corpus)) { return array(''); } // Split on "\r\n" or "\n". if ($retain_endings) { $lines = preg_split('/(?<=\n)/', $corpus); } else { $lines = preg_split('/\r?\n/', $corpus); } // If the text ends with "\n" or similar, we'll end up with an empty string // at the end; discard it. if (end($lines) == '') { array_pop($lines); } if ($corpus instanceof PhutilSafeHTML) { return array_map('phutil_safe_html', $lines); } return $lines; } /** * Simplifies a common use of `array_combine()`. Specifically, this: * * COUNTEREXAMPLE: * if ($list) { * $result = array_combine($list, $list); * } else { * // Prior to PHP 5.4, array_combine() failed if given empty arrays. * $result = array(); * } * * ...is equivalent to this: * * $result = array_fuse($list); * * @param list List of scalars. * @return dict Dictionary with inputs mapped to themselves. */ function array_fuse(array $list) { if ($list) { return array_combine($list, $list); } return array(); } /** * Add an element between every two elements of some array. That is, given a * list `A, B, C, D`, and some element to interleave, `x`, this function returns * `A, x, B, x, C, x, D`. This works like `implode()`, but does not concatenate * the list into a string. In particular: * * implode('', array_interleave($x, $list)); * * ...is equivalent to: * * implode($x, $list); * * This function does not preserve keys. * * @param wild Element to interleave. * @param list List of elements to be interleaved. * @return list Original list with the new element interleaved. */ function array_interleave($interleave, array $array) { $result = array(); foreach ($array as $item) { $result[] = $item; $result[] = $interleave; } array_pop($result); return $result; } function phutil_is_windows() { // We can also use PHP_OS, but that's kind of sketchy because it returns // "WINNT" for Windows 7 and "Darwin" for Mac OS X. Practically, testing for // DIRECTORY_SEPARATOR is more straightforward. return (DIRECTORY_SEPARATOR != '/'); } function phutil_is_hiphop_runtime() { return (array_key_exists('HPHP', $_ENV) && $_ENV['HPHP'] === 1); } /** * Converts a string to a loggable one, with unprintables and newlines escaped. * * @param string Any string. * @return string String with control and newline characters escaped, suitable * for printing on a single log line. */ function phutil_loggable_string($string) { if (preg_match('/^[\x20-\x7E]+$/', $string)) { return $string; } $result = ''; static $c_map = array( '\\' => '\\\\', "\n" => '\\n', "\r" => '\\r', "\t" => '\\t', ); $len = strlen($string); for ($ii = 0; $ii < $len; $ii++) { $c = $string[$ii]; if (isset($c_map[$c])) { $result .= $c_map[$c]; } else { $o = ord($c); if ($o < 0x20 || $o == 0x7F) { $result .= '\\x'.sprintf('%02X', $o); } else { $result .= $c; } } } return $result; } /** * Perform an `fwrite()` which distinguishes between EAGAIN and EPIPE. * * PHP's `fwrite()` is broken, and never returns `false` for writes to broken * nonblocking pipes: it always returns 0, and provides no straightforward * mechanism for distinguishing between EAGAIN (buffer is full, can't write any * more right now) and EPIPE or similar (no write will ever succeed). * * See: https://bugs.php.net/bug.php?id=39598 * * If you call this method instead of `fwrite()`, it will attempt to detect * when a zero-length write is caused by EAGAIN and return `0` only if the * write really should be retried. * * @param resource Socket or pipe stream. * @param string Bytes to write. * @return bool|int Number of bytes written, or `false` on any error (including * errors which `fwrite()` can not detect, like a broken pipe). */ function phutil_fwrite_nonblocking_stream($stream, $bytes) { if (!strlen($bytes)) { return 0; } $result = @fwrite($stream, $bytes); if ($result !== 0) { // In cases where some bytes are witten (`$result > 0`) or // an error occurs (`$result === false`), the behavior of fwrite() is // correct. We can return the value as-is. return $result; } // If we make it here, we performed a 0-length write. Try to distinguish // between EAGAIN and EPIPE. To do this, we're going to `stream_select()` // the stream, write to it again if PHP claims that it's writable, and // consider the pipe broken if the write fails. // (Signals received signals during the "fwrite()" do not appear to affect // anything, see D20083.) $read = array(); $write = array($stream); $except = array(); $result = @stream_select($read, $write, $except, 0); if ($result === false) { // See T13243. If the select is interrupted by a signal, it may return // "false" indicating an underlying EINTR condition. In this case, the // results (notably, "$write") are not usable because "stream_select()" // didn't update them. // In this case, treat this stream as blocked and tell the caller to // retry, since EINTR is the only condition we're currently aware of that // can cause "fwrite()" to return "0" and "stream_select()" to return // "false" on the same stream. return 0; } if (!$write) { // The stream isn't writable, so we conclude that it probably really is // blocked and the underlying error was EAGAIN. Return 0 to indicate that // no data could be written yet. return 0; } // If we make it here, PHP **just** claimed that this stream is writable, so // perform a write. If the write also fails, conclude that these failures are // EPIPE or some other permanent failure. $result = @fwrite($stream, $bytes); if ($result !== 0) { // The write worked or failed explicitly. This value is fine to return. return $result; } // We performed a 0-length write, were told that the stream was writable, and // then immediately performed another 0-length write. Conclude that the pipe // is broken and return `false`. return false; } /** * Convert a human-readable unit description into a numeric one. This function * allows you to replace this: * * COUNTEREXAMPLE * $ttl = (60 * 60 * 24 * 30); // 30 days * * ...with this: * * $ttl = phutil_units('30 days in seconds'); * * ...which is self-documenting and difficult to make a mistake with. * * @param string Human readable description of a unit quantity. * @return int Quantity of specified unit. */ function phutil_units($description) { $matches = null; if (!preg_match('/^(\d+) (\w+) in (\w+)$/', $description, $matches)) { throw new InvalidArgumentException( pht( 'Unable to parse unit specification (expected a specification in the '. 'form "%s"): %s', '5 days in seconds', $description)); } $quantity = (int)$matches[1]; $src_unit = $matches[2]; $dst_unit = $matches[3]; $is_divisor = false; switch ($dst_unit) { case 'seconds': switch ($src_unit) { case 'second': case 'seconds': $factor = 1; break; case 'minute': case 'minutes': $factor = 60; break; case 'hour': case 'hours': $factor = 60 * 60; break; case 'day': case 'days': $factor = 60 * 60 * 24; break; default: throw new InvalidArgumentException( pht( 'This function can not convert from the unit "%s".', $src_unit)); } break; case 'bytes': switch ($src_unit) { case 'byte': case 'bytes': $factor = 1; break; case 'bit': case 'bits': $factor = 8; $is_divisor = true; break; default: throw new InvalidArgumentException( pht( 'This function can not convert from the unit "%s".', $src_unit)); } break; case 'milliseconds': switch ($src_unit) { case 'second': case 'seconds': $factor = 1000; break; case 'minute': case 'minutes': $factor = 1000 * 60; break; case 'hour': case 'hours': $factor = 1000 * 60 * 60; break; case 'day': case 'days': $factor = 1000 * 60 * 60 * 24; break; default: throw new InvalidArgumentException( pht( 'This function can not convert from the unit "%s".', $src_unit)); } break; case 'microseconds': switch ($src_unit) { case 'second': case 'seconds': $factor = 1000000; break; case 'minute': case 'minutes': $factor = 1000000 * 60; break; case 'hour': case 'hours': $factor = 1000000 * 60 * 60; break; case 'day': case 'days': $factor = 1000000 * 60 * 60 * 24; break; default: throw new InvalidArgumentException( pht( 'This function can not convert from the unit "%s".', $src_unit)); } break; default: throw new InvalidArgumentException( pht( 'This function can not convert into the unit "%s".', $dst_unit)); } if ($is_divisor) { if ($quantity % $factor) { throw new InvalidArgumentException( pht( '"%s" is not an exact quantity.', $description)); } return (int)($quantity / $factor); } else { return $quantity * $factor; } } /** * Compute the number of microseconds that have elapsed since an earlier * timestamp (from `microtime(true)`). * * @param double Microsecond-precision timestamp, from `microtime(true)`. * @return int Elapsed microseconds. */ function phutil_microseconds_since($timestamp) { if (!is_float($timestamp)) { throw new Exception( pht( 'Argument to "phutil_microseconds_since(...)" should be a value '. 'returned from "microtime(true)".')); } $delta = (microtime(true) - $timestamp); $delta = 1000000 * $delta; $delta = (int)$delta; return $delta; } /** * Decode a JSON dictionary. * * @param string A string which ostensibly contains a JSON-encoded list or * dictionary. * @return mixed Decoded list/dictionary. */ function phutil_json_decode($string) { $result = @json_decode($string, true); if (!is_array($result)) { // Failed to decode the JSON. Try to use @{class:PhutilJSONParser} instead. // This will probably fail, but will throw a useful exception. $parser = new PhutilJSONParser(); $result = $parser->parse($string); } return $result; } /** * Encode a value in JSON, raising an exception if it can not be encoded. * * @param wild A value to encode. * @return string JSON representation of the value. */ function phutil_json_encode($value) { $result = @json_encode($value); if ($result === false) { $reason = phutil_validate_json($value); if (function_exists('json_last_error')) { $err = json_last_error(); if (function_exists('json_last_error_msg')) { $msg = json_last_error_msg(); $extra = pht('#%d: %s', $err, $msg); } else { $extra = pht('#%d', $err); } } else { $extra = null; } if ($extra) { $message = pht( 'Failed to JSON encode value (%s): %s.', $extra, $reason); } else { $message = pht( 'Failed to JSON encode value: %s.', $reason); } throw new Exception($message); } return $result; } /** * Produce a human-readable explanation why a value can not be JSON-encoded. * * @param wild Value to validate. * @param string Path within the object to provide context. * @return string|null Explanation of why it can't be encoded, or null. */ function phutil_validate_json($value, $path = '') { if ($value === null) { return; } if ($value === true) { return; } if ($value === false) { return; } if (is_int($value)) { return; } if (is_float($value)) { return; } if (is_array($value)) { foreach ($value as $key => $subvalue) { if (strlen($path)) { $full_key = $path.' > '; } else { $full_key = ''; } if (!phutil_is_utf8($key)) { $full_key = $full_key.phutil_utf8ize($key); return pht( 'Dictionary key "%s" is not valid UTF8, and cannot be JSON encoded.', $full_key); } $full_key .= $key; $result = phutil_validate_json($subvalue, $full_key); if ($result !== null) { return $result; } } } if (is_string($value)) { if (!phutil_is_utf8($value)) { $display = substr($value, 0, 256); $display = phutil_utf8ize($display); if (!strlen($path)) { return pht( 'String value is not valid UTF8, and can not be JSON encoded: %s', $display); } else { return pht( 'Dictionary value at key "%s" is not valid UTF8, and cannot be '. 'JSON encoded: %s', $path, $display); } } } return; } /** * Decode an INI string. * * @param string * @return mixed */ function phutil_ini_decode($string) { $results = null; $trap = new PhutilErrorTrap(); try { $have_call = false; if (function_exists('parse_ini_string')) { if (defined('INI_SCANNER_RAW')) { $results = @parse_ini_string($string, true, INI_SCANNER_RAW); $have_call = true; } } if (!$have_call) { throw new PhutilMethodNotImplementedException( pht( '%s is not compatible with your version of PHP (%s). This function '. 'is only supported on PHP versions newer than 5.3.0.', __FUNCTION__, phpversion())); } if ($results === false) { throw new PhutilINIParserException(trim($trap->getErrorsAsString())); } foreach ($results as $section => $result) { if (!is_array($result)) { // We JSON decode the value in ordering to perform the following // conversions: // // - `'true'` => `true` // - `'false'` => `false` // - `'123'` => `123` // - `'1.234'` => `1.234` // $result = json_decode($result, true); if ($result !== null && !is_array($result)) { $results[$section] = $result; } continue; } foreach ($result as $key => $value) { $value = json_decode($value, true); if ($value !== null && !is_array($value)) { $results[$section][$key] = $value; } } } } catch (Exception $ex) { $trap->destroy(); throw $ex; } $trap->destroy(); return $results; } /** * Attempt to censor any plaintext credentials from a string. * * The major use case here is to censor usernames and passwords from command * output. For example, when `git fetch` fails, the output includes credentials * for authenticated HTTP remotes. * * @param string Some block of text. * @return string A similar block of text, but with credentials that could * be identified censored. */ function phutil_censor_credentials($string) { return preg_replace(',(?<=://)([^/@\s]+)(?=@|$),', '********', $string); } /** * Returns a parsable string representation of a variable. * * This function is intended to behave similarly to PHP's `var_export` function, * but the output is intended to follow our style conventions. * * @param wild The variable you want to export. * @return string */ function phutil_var_export($var) { // `var_export(null, true)` returns `"NULL"` (in uppercase). if ($var === null) { return 'null'; } // PHP's `var_export` doesn't format arrays very nicely. In particular: // // - An empty array is split over two lines (`"array (\n)"`). // - A space separates "array" and the first opening brace. // - Non-associative arrays are returned as associative arrays with an // integer key. // if (is_array($var)) { if (count($var) === 0) { return 'array()'; } // 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('; foreach ($var as $key => $value) { // Adjust the indentation of the value. $value = str_replace("\n", "\n ", phutil_var_export($value)); $output[] = ' '. ($show_keys ? var_export($key, true).' => ' : ''). $value.','; } $output[] = ')'; return implode("\n", $output); } // Let PHP handle everything else. return var_export($var, true); } /** * An improved version of `fnmatch`. * * @param string A glob pattern. * @param string A path. * @return bool */ function phutil_fnmatch($glob, $path) { // Modify the glob to allow `**/` to match files in the root directory. $glob = preg_replace('@(?:(? Dictionary of parameters. * @return string HTTP query string. */ function phutil_build_http_querystring(array $parameters) { $pairs = array(); foreach ($parameters as $key => $value) { $pairs[] = array($key, $value); } return phutil_build_http_querystring_from_pairs($pairs); } /** * Build a query string from a list of parameter pairs. * * @param list> List of pairs. * @return string HTTP query string. */ function phutil_build_http_querystring_from_pairs(array $pairs) { // We want to encode in RFC3986 mode, but "http_build_query()" did not get // a flag for that mode until PHP 5.4.0. This is equivalent to calling // "http_build_query()" with the "PHP_QUERY_RFC3986" flag. $query = array(); foreach ($pairs as $pair_key => $pair) { if (!is_array($pair) || (count($pair) !== 2)) { throw new Exception( pht( 'HTTP parameter pair (with key "%s") is not valid: each pair must '. 'be an array with exactly two elements.', $pair_key)); } list($key, $value) = $pair; list($key, $value) = phutil_http_parameter_pair($key, $value); $query[] = rawurlencode($key).'='.rawurlencode($value); } $query = implode($query, '&'); return $query; } /** * Typecheck and cast an HTTP key-value parameter pair. * * Scalar values are converted to strings. Nonscalar values raise exceptions. * * @param scalar HTTP parameter key. * @param scalar HTTP parameter value. * @return pair Key and value as strings. */ function phutil_http_parameter_pair($key, $value) { try { assert_stringlike($key); } catch (InvalidArgumentException $ex) { throw new PhutilProxyException( pht('HTTP query parameter key must be a scalar.'), $ex); } $key = phutil_string_cast($key); try { assert_stringlike($value); } catch (InvalidArgumentException $ex) { throw new PhutilProxyException( pht( 'HTTP query parameter value (for key "%s") must be a scalar.', $key), $ex); } $value = phutil_string_cast($value); return array($key, $value); } function phutil_decode_mime_header($header) { if (function_exists('iconv_mime_decode')) { return iconv_mime_decode($header, 0, 'UTF-8'); } if (function_exists('mb_decode_mimeheader')) { return mb_decode_mimeheader($header); } throw new Exception( pht( 'Unable to decode MIME header: install "iconv" or "mbstring" '. 'extension.')); } /** * Perform a "(string)" cast without disabling standard exception behavior. * * When PHP invokes "__toString()" automatically, it fatals if the method * raises an exception. In older versions of PHP (until PHP 7.1), this fatal is * fairly opaque and does not give you any information about the exception * itself, although newer versions of PHP at least include the exception * message. * * This is documented on the "__toString()" manual page: * * Warning * You cannot throw an exception from within a __toString() method. Doing * so will result in a fatal error. * * However, this only applies to implicit invocation by the language runtime. * Application code can safely call `__toString()` directly without any effect * on exception handling behavior. Very cool. * * We also reject arrays. PHP casts them to the string "Array". This behavior * is, charitably, evil. * * @param wild Any value which aspires to be represented as a string. * @return string String representation of the provided value. */ function phutil_string_cast($value) { if (is_array($value)) { throw new Exception( pht( 'Value passed to "phutil_string_cast()" is an array; arrays can '. 'not be sensibly cast to strings.')); } if (is_object($value)) { $string = $value->__toString(); if (!is_string($string)) { throw new Exception( pht( 'Object (of class "%s") did not return a string from "__toString()".', get_class($value))); } return $string; } 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; +}